feat(tests): ui suite — toolbar groups, scroll preservation, DOM keep-alive

6 Playwright tests against nyx-test (localhost:30802, auth disabled):
- toolbar group counts per route (nyx=4, tests=2, home=1)
- toolbar survives full nav roundtrip without losing groups
- scroll position preserved across navigation (keep-alive working)
- all visited views stay in DOM (not removed on nav)

Run: NYX_URL=http://localhost:30802 python tests/run_tests.py ui

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Nico 2026-04-03 23:24:51 +02:00
parent 5964ca55e1
commit 44f6116855
2 changed files with 148 additions and 0 deletions

View File

@ -134,6 +134,13 @@ def get_node_tests() -> dict:
return TESTS 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 = { SUITES = {
'engine': get_engine_tests, 'engine': get_engine_tests,
'api': get_api_tests, 'api': get_api_tests,
@ -141,6 +148,7 @@ SUITES = {
'matrix': get_matrix_tests, 'matrix': get_matrix_tests,
'testcases': get_testcase_tests, 'testcases': get_testcase_tests,
'roundtrip': get_roundtrip_tests, 'roundtrip': get_roundtrip_tests,
'ui': get_ui_tests,
} }

140
tests/test_ui.py Normal file
View File

@ -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,
}