Test dashboard: full menu system, all-service tests, filtering
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m11s

- Run tests for all 10 services via per-service pytest subprocesses
- Group results by service with section headers
- Clickable summary cards filter by outcome (passed/failed/errors/skipped)
- Service filter nav using ~nav-link buttons in menu bar
- Full menu integration: ~header-row + ~header-child + ~menu-row
- Show logo image via cart-mini rendering
- Mount full service directories in docker-compose for test access
- Add 24 unit test files across 9 services

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 22:54:25 +00:00
parent 81e51ae7bc
commit 3809affcab
41 changed files with 2484 additions and 110 deletions

View File

@@ -18,6 +18,65 @@ def _format_time(ts: float | None) -> str:
return datetime.fromtimestamp(ts).strftime("%-d %b %Y, %H:%M:%S")
# ---------------------------------------------------------------------------
# Menu / header
# ---------------------------------------------------------------------------
_FILTER_MAP = {
"passed": "passed",
"failed": "failed",
"errors": "error",
"skipped": "skipped",
}
def _test_header_html(ctx: dict, active_service: str | None = None) -> str:
"""Build the Tests menu-row (level 1) with service nav links."""
nav = _service_nav_html(ctx, active_service)
return render(
"menu-row",
id="test-row", level=1, colour="sky",
link_href="/", link_label="Tests", icon="fa fa-flask",
nav_html=nav,
child_id="test-header-child",
)
def _service_nav_html(ctx: dict, active_service: str | None = None) -> str:
"""Render service filter nav links using ~nav-link component."""
from runner import _SERVICE_ORDER
parts = []
# "All" link
parts.append(render(
"nav-link",
href="/",
label="all",
is_selected="true" if not active_service else None,
select_colours="aria-selected:bg-sky-200 aria-selected:text-sky-900",
))
for svc in _SERVICE_ORDER:
parts.append(render(
"nav-link",
href=f"/?service={svc}",
label=svc,
is_selected="true" if active_service == svc else None,
select_colours="aria-selected:bg-sky-200 aria-selected:text-sky-900",
))
return "".join(parts)
def _header_stack_html(ctx: dict, active_service: str | None = None) -> str:
"""Full header stack: root row + tests child row."""
hdr = root_header_html(ctx)
inner = _test_header_html(ctx, active_service)
hdr += render("header-child", inner_html=inner)
return hdr
# ---------------------------------------------------------------------------
# Test rows / grouping
# ---------------------------------------------------------------------------
def _test_rows_html(tests: list[dict]) -> str:
"""Render all test result rows."""
parts = []
@@ -32,7 +91,43 @@ def _test_rows_html(tests: list[dict]) -> str:
return "".join(parts)
def _results_partial_html(result: dict | None, running: bool, csrf: str) -> str:
def _grouped_rows_html(tests: list[dict]) -> str:
"""Render test rows grouped by service with section headers."""
from runner import group_tests_by_service
sections = group_tests_by_service(tests)
parts = []
for sec in sections:
parts.append(render(
"test-service-header",
service=sec["service"],
total=str(sec["total"]),
passed=str(sec["passed"]),
failed=str(sec["failed"]),
))
parts.append(_test_rows_html(sec["tests"]))
return "".join(parts)
def _filter_tests(tests: list[dict], active_filter: str | None,
active_service: str | None) -> list[dict]:
"""Filter tests by outcome and/or service."""
from runner import _service_from_nodeid
filtered = tests
if active_filter and active_filter in _FILTER_MAP:
outcome = _FILTER_MAP[active_filter]
filtered = [t for t in filtered if t["outcome"] == outcome]
if active_service:
filtered = [t for t in filtered if _service_from_nodeid(t["nodeid"]) == active_service]
return filtered
# ---------------------------------------------------------------------------
# Results partial
# ---------------------------------------------------------------------------
def _results_partial_html(result: dict | None, running: bool, csrf: str,
active_filter: str | None = None,
active_service: str | None = None) -> str:
"""Render the results section (summary + table or running indicator)."""
if running and not result:
summary = render(
@@ -40,6 +135,7 @@ def _results_partial_html(result: dict | None, running: bool, csrf: str) -> str:
status="running", passed="0", failed="0", errors="0",
skipped="0", total="0", duration="...",
last_run="in progress", running=True, csrf=csrf,
active_filter=active_filter,
)
return summary + render("test-running-indicator")
@@ -49,6 +145,7 @@ def _results_partial_html(result: dict | None, running: bool, csrf: str) -> str:
status=None, passed="0", failed="0", errors="0",
skipped="0", total="0", duration="0",
last_run="never", running=running, csrf=csrf,
active_filter=active_filter,
)
return summary + render("test-no-results")
@@ -65,17 +162,19 @@ def _results_partial_html(result: dict | None, running: bool, csrf: str) -> str:
last_run=_format_time(result["finished_at"]) if not running else "in progress",
running=running,
csrf=csrf,
active_filter=active_filter,
)
if running:
return summary + render("test-running-indicator")
tests = result.get("tests", [])
tests = _filter_tests(tests, active_filter, active_service)
if not tests:
return summary + render("test-no-results")
has_failures = result["failed"] > 0 or result["errors"] > 0
rows = _test_rows_html(tests)
rows = _grouped_rows_html(tests)
table = render("test-results-table", rows_html=rows,
has_failures=str(has_failures).lower())
return summary + table
@@ -90,16 +189,20 @@ def _wrap_results_div(inner_html: str, running: bool) -> str:
async def render_dashboard_page(ctx: dict, result: dict | None,
running: bool, csrf: str) -> str:
running: bool, csrf: str,
active_filter: str | None = None,
active_service: str | None = None) -> str:
"""Full page: test dashboard."""
hdr = root_header_html(ctx)
inner = _results_partial_html(result, running, csrf)
hdr = _header_stack_html(ctx, active_service)
inner = _results_partial_html(result, running, csrf, active_filter, active_service)
content = _wrap_results_div(inner, running)
return full_page(ctx, header_rows_html=hdr, content_html=content)
async def render_results_partial(result: dict | None, running: bool,
csrf: str) -> str:
csrf: str,
active_filter: str | None = None,
active_service: str | None = None) -> str:
"""HTMX partial: just the results section (wrapped in polling div)."""
inner = _results_partial_html(result, running, csrf)
inner = _results_partial_html(result, running, csrf, active_filter, active_service)
return _wrap_results_div(inner, running)