Files

115 lines
4.4 KiB
Python
Raw Permalink Normal View History

"""Playwright browser smoke test for the dashboard UI.
Verifies the dashboard boots, renders tasks, and preserves column scroll
position across auto-refresh (the fix for the scroll-reset bug). Skipped
when playwright/chromium is not installed so the main suite stays green
in CI environments without a browser.
Run standalone:
python3 -m pytest tests/test_dashboard_ui.py -v
"""
from __future__ import annotations
import socket
import threading
import time
from http.server import HTTPServer
from pathlib import Path
import pytest
from automaton.dashboard.ui.app import DashboardHandler, DashboardApp
playwright = pytest.importorskip("playwright")
from playwright.sync_api import sync_playwright # noqa: E402
def _free_port() -> int:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(("127.0.0.1", 0))
port = s.getsockname()[1]
s.close()
return port
def _make_done_tasks(project_root: Path, count: int) -> None:
tasks = project_root / ".automaton" / "tasks"
tasks.mkdir(parents=True, exist_ok=True)
for i in range(count):
d = tasks / f"done-task-{i:02d}"
d.mkdir(exist_ok=True)
(d / "SPEC.md").write_text(f"# Done task {i}\n" + ("line. " * 40) + "\n")
(d / ".state").write_text("complete\n")
@pytest.fixture
def dashboard_server(tmp_path: Path):
_make_done_tasks(tmp_path, 40)
app = DashboardApp(start_path=tmp_path)
assert app.initialize(), "dashboard failed to init"
port = _free_port()
DashboardHandler.config = app.config
DashboardHandler.project_root = app.project_root
DashboardHandler.scope = app.scope
server = HTTPServer(("127.0.0.1", port), DashboardHandler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
base_url = f"http://127.0.0.1:{port}"
try:
yield base_url
finally:
server.shutdown()
server.server_close()
thread.join(timeout=5)
def _wait_for(predicate, timeout=10.0, interval=0.2):
deadline = time.time() + timeout
while time.time() < deadline:
if predicate():
return True
time.sleep(interval)
return False
class TestDashboardUI:
def test_board_renders_done_tasks(self, dashboard_server):
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto(dashboard_server)
# Footer refresh counter increments once auto-refresh fires.
assert _wait_for(lambda: page.text_content("#refresh-count") not in (None, "0", ""))
# The Done (resolution) group contains our tasks.
cards = page.locator(".task-card")
assert _wait_for(lambda: cards.count() >= 40, timeout=15)
assert page.text_content("#done-tasks") == "40"
browser.close()
def test_refresh_preserves_column_scroll(self, dashboard_server):
"""The scroll-reset bug: re-render snapped a column's scrollTop to 0,
so users couldn't scroll the Done group down to review older tasks.
Verify the column-body scrollTop survives an auto-refresh tick."""
with sync_playwright() as p:
browser = p.chromium.launch()
# Wide viewport so columns fit and the Done column-body overflows
# vertically (rather than the board squeezing them horizontally).
page = browser.new_page(viewport={"width": 1400, "height": 500})
page.goto(dashboard_server)
assert _wait_for(lambda: page.locator(".task-card").count() >= 40, timeout=15)
# The Done (resolution) group is the last column.
bodies = page.locator(".column-body")
n = bodies.count()
done_body = bodies.nth(n - 1)
assert done_body.evaluate("el => el.scrollHeight > el.clientHeight")
# Scroll the column-body well past the top.
target = 400
done_body.evaluate("(el, y) => { el.scrollTop = y; }", target)
assert done_body.evaluate("el => el.scrollTop") == target
# Wait for an auto-refresh re-render (default interval 2s).
before = int(page.text_content("#refresh-count") or "0")
assert _wait_for(lambda: int(page.text_content("#refresh-count") or "0") > before, timeout=10)
# Scroll must survive the re-render (the bug fix).
assert done_body.evaluate("el => el.scrollTop") == target
browser.close()