Spaces:
Sleeping
Sleeping
File size: 6,177 Bytes
1aa566a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | """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"))
|