diff --git a/tests/run_tests.py b/tests/run_tests.py index 99c0c7b..4804f60 100644 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -134,6 +134,13 @@ def get_node_tests() -> dict: return TESTS +def get_ui_tests() -> dict: + """Load UI tests — toolbar, navigation, scroll (Playwright, no backend needed).""" + sys.path.insert(0, os.path.dirname(__file__)) + from test_ui import TESTS + return TESTS + + SUITES = { 'engine': get_engine_tests, 'api': get_api_tests, @@ -141,6 +148,7 @@ SUITES = { 'matrix': get_matrix_tests, 'testcases': get_testcase_tests, 'roundtrip': get_roundtrip_tests, + 'ui': get_ui_tests, } diff --git a/tests/test_ui.py b/tests/test_ui.py new file mode 100644 index 0000000..9598bd1 --- /dev/null +++ b/tests/test_ui.py @@ -0,0 +1,140 @@ +""" +UI tests — toolbar, navigation, scroll preservation. + +Runs against a nyx instance with VITE_AUTH_DISABLED=true (auth skipped). + +Local dev (after restarting Vite with .env.local VITE_AUTH_DISABLED=true): + NYX_URL=http://localhost:5173 python tests/run_tests.py ui + +K3s test build (no restart needed — already built with auth disabled): + NYX_URL=http://localhost:30802 python tests/run_tests.py ui +""" + +import os +from playwright.sync_api import sync_playwright, Page, expect + +NYX_URL = os.environ.get('NYX_URL', 'http://localhost:30802') + +_pw = None +_browser = None + + +def _ensure_browser(): + global _pw, _browser + if _browser is None: + _pw = sync_playwright().start() + _browser = _pw.chromium.launch(headless=True) + return _browser + + +def _page(path: str = '/nyx') -> tuple: + browser = _ensure_browser() + ctx = browser.new_context(viewport={'width': 1280, 'height': 800}) + page = ctx.new_page() + page.goto(f'{NYX_URL}{path}') + page.wait_for_selector('.app-toolbar', timeout=15000) + page.wait_for_timeout(500) # let Vue commit reactive toolbar updates + return page, ctx + + +def _click_nav(page: Page, text: str): + page.locator('.sidebar-link', has_text=text).click() + page.wait_for_timeout(800) + + +# ── Tests ──────────────────────────────────────────────────────────────────── + +def test_toolbar_nyx_has_all_groups(): + """nyx shows 4 toolbar groups: connection, quad-view, themes, panels.""" + page, ctx = _page('/nyx') + try: + expect(page.locator('.toolbar-group')).to_have_count(4, timeout=5000) + finally: + ctx.close() + + +def test_toolbar_tests_has_two_groups(): + """tests view shows 2 toolbar groups: connection + themes.""" + page, ctx = _page('/tests') + try: + expect(page.locator('.toolbar-group')).to_have_count(2, timeout=5000) + finally: + ctx.close() + + +def test_toolbar_home_has_one_group(): + """home page shows 1 toolbar group: themes only.""" + page, ctx = _page('/') + try: + expect(page.locator('.toolbar-group')).to_have_count(1, timeout=5000) + finally: + ctx.close() + + +def test_toolbar_survives_roundtrip(): + """Navigate nyx→tests→home→nyx — toolbar groups correct at each stop.""" + page, ctx = _page('/nyx') + try: + expect(page.locator('.toolbar-group')).to_have_count(4, timeout=5000) + + _click_nav(page, 'Tests') + expect(page.locator('.toolbar-group')).to_have_count(2, timeout=3000) + + _click_nav(page, 'Home') + expect(page.locator('.toolbar-group')).to_have_count(1, timeout=3000) + + _click_nav(page, 'nyx') + expect(page.locator('.toolbar-group')).to_have_count(4, timeout=3000) + + _click_nav(page, 'Tests') + expect(page.locator('.toolbar-group')).to_have_count(2, timeout=3000) + finally: + ctx.close() + + +def test_scroll_preserved_across_navigation(): + """Scroll down in tests view, navigate away and back — position preserved.""" + page, ctx = _page('/tests') + try: + page.wait_for_selector('.tests-view', timeout=5000) + + # Scroll the tests container + page.evaluate('() => { const el = document.querySelector(".tests-view"); if (el) el.scrollTop = 200; }') + page.wait_for_timeout(200) + before = page.evaluate('() => document.querySelector(".tests-view")?.scrollTop ?? 0') + + _click_nav(page, 'Home') + _click_nav(page, 'Tests') + + after = page.evaluate('() => document.querySelector(".tests-view")?.scrollTop ?? 0') + assert after == before, f'scroll not preserved: was {before}, now {after}' + finally: + ctx.close() + + +def test_all_views_stay_in_dom(): + """After visiting nyx and tests, both stay in DOM (hidden not removed).""" + page, ctx = _page('/nyx') + try: + expect(page.locator('.toolbar-group')).to_have_count(4, timeout=5000) + + _click_nav(page, 'Tests') + # AgentsView should still be in DOM (just hidden) + assert page.locator('.agents-view').count() > 0, 'AgentsView removed from DOM' + + _click_nav(page, 'nyx') + # TestsView should still be in DOM + assert page.locator('.tests-view').count() > 0, 'TestsView removed from DOM' + finally: + ctx.close() + + +# Test registry +TESTS = { + 'ui_toolbar_nyx_all_groups': test_toolbar_nyx_has_all_groups, + 'ui_toolbar_tests_two_groups': test_toolbar_tests_has_two_groups, + 'ui_toolbar_home_one_group': test_toolbar_home_has_one_group, + 'ui_toolbar_roundtrip': test_toolbar_survives_roundtrip, + 'ui_scroll_preserved': test_scroll_preserved_across_navigation, + 'ui_views_stay_in_dom': test_all_views_stay_in_dom, +}