argus-mlops / tests /test_navigation.py
hodfa840's picture
Fix scroll reset for HF Spaces double-iframe context
1aa566a
"""Navigation and scroll-position tests.
Verifies that switching between dashboard pages resets the scroll
position to the top of the main content area.
Usage:
pytest tests/test_navigation.py -v
(dashboard must be running at DASHBOARD_URL)
"""
from __future__ import annotations
import os
import time
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from webdriver_manager.chrome import ChromeDriverManager
DASHBOARD_URL = os.environ.get("DASHBOARD_URL", "http://localhost:8501")
NAV_PAGES = ["Overview", "Drift Analysis", "Feature Insights", "Retraining Log", "Live Demo"]
WAIT = 6
def _dashboard_reachable() -> bool:
try:
import urllib.request
urllib.request.urlopen(DASHBOARD_URL, timeout=3)
return True
except Exception:
return False
def _make_driver() -> webdriver.Chrome:
opts = Options()
opts.add_argument("--headless=new")
opts.add_argument("--no-sandbox")
opts.add_argument("--disable-dev-shm-usage")
opts.add_argument("--window-size=1600,1000")
service = Service(ChromeDriverManager().install())
return webdriver.Chrome(service=service, options=opts)
def _navigate_to(driver: webdriver.Chrome, page: str, wait: int = WAIT) -> None:
"""Click a sidebar navigation radio button and wait for the page to load."""
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "[data-testid='stSidebar']"))
)
radios = driver.find_elements(By.CSS_SELECTOR, "[data-testid='stSidebar'] label")
for label in radios:
if label.text.strip() == page:
label.click()
time.sleep(wait)
return
raise AssertionError(f"Navigation label '{page}' not found in sidebar")
def _main_scroll_top(driver: webdriver.Chrome) -> int:
"""Return the scrollTop of the main Streamlit content container."""
return driver.execute_script(
"var el = document.querySelector('[data-testid=\"stMain\"]') "
"|| document.querySelector('.main') "
"|| document.querySelector('[data-testid=\"stAppViewContainer\"]');"
"return el ? el.scrollTop : window.scrollY;"
)
def _scroll_down(driver: webdriver.Chrome, px: int = 600) -> None:
driver.execute_script(
f"var el = document.querySelector('[data-testid=\"stMain\"]') "
f"|| document.querySelector('.main') "
f"|| document.querySelector('[data-testid=\"stAppViewContainer\"]');"
f"if (el) {{ el.scrollTop = {px}; }} else {{ window.scrollBy(0, {px}); }}"
)
time.sleep(0.5)
@pytest.fixture(scope="module")
def driver():
if not _dashboard_reachable():
pytest.skip(f"Dashboard not running at {DASHBOARD_URL}")
drv = _make_driver()
drv.get(DASHBOARD_URL)
time.sleep(WAIT)
yield drv
drv.quit()
@pytest.mark.selenium
class TestNavigation:
def test_all_pages_load_without_error(self, driver):
"""Cycle through every page and confirm no Python traceback appears."""
for page in NAV_PAGES:
_navigate_to(driver, page)
body = driver.find_element(By.TAG_NAME, "body").text
assert "Traceback (most recent call last)" not in body, (
f"Python traceback on page '{page}'"
)
def test_page_content_changes_on_navigation(self, driver):
"""Each page must show its own title, not the previous page's title."""
_navigate_to(driver, "Live Demo")
live_body = driver.find_element(By.TAG_NAME, "body").text
assert "Live Demo" in live_body
_navigate_to(driver, "Overview")
overview_body = driver.find_element(By.TAG_NAME, "body").text
assert "Rolling RMSE" in overview_body, (
"Overview content not visible after navigating from Live Demo"
)
def test_scroll_resets_to_top_on_page_change(self, driver):
"""
Reproduce the reported bug:
Navigate to Live Demo, scroll down, then go to Overview.
The main content area must scroll back to y=0.
"""
_navigate_to(driver, "Live Demo")
_scroll_down(driver, px=700)
scroll_before = _main_scroll_top(driver)
_navigate_to(driver, "Overview")
scroll_after = _main_scroll_top(driver)
assert scroll_after <= 50, (
f"BUG: page did not scroll to top after tab change. "
f"scrollTop before={scroll_before}px, after={scroll_after}px. "
f"User sees content from the old page position."
)
def test_scroll_resets_across_all_page_transitions(self, driver):
"""Scroll down on every page, then switch — top must always reset."""
failures = []
pairs = [
("Drift Analysis", "Feature Insights"),
("Feature Insights", "Retraining Log"),
("Retraining Log", "Live Demo"),
("Live Demo", "Overview"),
("Overview", "Drift Analysis"),
]
for src, dst in pairs:
_navigate_to(driver, src)
_scroll_down(driver, px=500)
_navigate_to(driver, dst)
pos = _main_scroll_top(driver)
if pos > 50:
failures.append(f"{src} -> {dst}: scrollTop={pos}px (expected <=50)")
assert not failures, (
"Scroll position did not reset on these transitions:\n" +
"\n".join(failures)
)
def test_screenshot_scroll_bug(self, driver):
"""Save before/after screenshots of the scroll bug for visual inspection."""
from pathlib import Path
assets = Path(__file__).resolve().parent.parent / "assets"
_navigate_to(driver, "Live Demo")
_scroll_down(driver, px=700)
driver.save_screenshot(str(assets / "nav_test_before.png"))
_navigate_to(driver, "Overview")
driver.save_screenshot(str(assets / "nav_test_after.png"))