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"))