"""No-AI contract cases: events in, visible result out (see templates/contract.md).""" import re import pytest from playwright.sync_api import expect def batch(n, state): return {"batch_id": n, "date": "2026-10-16", "state": state, "items_total": 10, "items_done": 4} BATCHES = [batch(1, "completed"), batch(2, "completed"), batch(3, "completed"), batch(4, "inprogress"), batch(5, "notcompleted"), batch(6, "cleared")] def kpi(page, label, n): return expect(page.get_by_role("group", name=label, exact=True)).to_have_text( re.compile(rf"^\s*{n}\s*{label}\s*$")) def test_kpi_counts_match_batch_states(page, push): push("dashboard_data", {"batches": BATCHES}) kpi(page, "Total batches", 6) kpi(page, "Completed", 3) kpi(page, "In progress", 1) kpi(page, "Not completed", 1) def colour_name(css): """'rgb(r, g, b)' -> red | amber | green | blue | grey, so checks survive any palette.""" r, g, b = (int(x) / 255 for x in re.findall(r"\d+", css)[:3]) hi, lo = max(r, g, b), min(r, g, b) if hi - lo < 0.25 * min(hi + lo, 2 - hi - lo): # low HSL saturation return "grey" d = hi - lo h = 60 * (((g - b) / d) % 6 if hi == r else (b - r) / d + 2 if hi == g else (r - g) / d + 4) return "red" if h < 20 or h >= 330 else "amber" if h < 70 else "green" if h < 175 else "blue" def donut_segments(donut): """[(colour name, percent of the ring)] read from the rendered conic gradient.""" css = donut.evaluate("el => getComputedStyle(el).backgroundImage") segs = [] for colour, positions in re.findall(r"(rgba?\([^)]*\))((?:\s+[\d.]+%)+)", css): pos = [float(p) for p in re.findall(r"[\d.]+", positions)] name = colour_name(colour) if segs and segs[-1][0] == name: segs[-1][2] = pos[-1] else: segs.append([name, pos[0], pos[-1]]) return [(name, round(end - start)) for name, start, end in segs if end > start] def test_donut_and_legend_match_batch_states(page, push): push("dashboard_data", {"batches": BATCHES}) legend = page.get_by_role("list", name="Batch status") for row in ["Completed · 3", "In progress · 1", "Not completed · 1", "Cleared · 1"]: expect(legend.get_by_text(row, exact=True)).to_be_visible() donut = page.get_by_role("img", name=re.compile("^Batch status")) expect(donut).to_have_text(re.compile(r"^\s*6\s*batches\s*$")) assert donut_segments(donut) == [("green", 50), ("amber", 17), ("red", 17), ("grey", 17)] def open_tab(page, name): page.get_by_role("navigation").get_by_role("button", name=name, exact=True).click() def test_step_list_follows_step_label(page, push): steps = page.evaluate("STEPS") open_tab(page, "Run") push("step", {"label": steps[1], "pct": 40}) items = page.get_by_role("list", name="Steps").get_by_role("listitem") expect(items).to_have_count(len(steps)) expect(items.nth(0)).to_have_text(re.compile(rf"^\s*✓\s*{re.escape(steps[0])}\s*$")) expect(items.nth(1)).to_have_attribute("aria-current", "step") for i in range(2, len(steps)): expect(items.nth(i)).to_have_text(re.compile(rf"^\s*{i + 1}\s*{re.escape(steps[i])}\s*$")) expect(page.get_by_text("40%", exact=True)).to_be_visible() push("step", {"label": steps[-1], "pct": 100}) for i in range(len(steps)): expect(items.nth(i)).to_have_text(re.compile(r"^\s*✓")) def colour_of(locator, prop="color"): return colour_name(locator.evaluate(f"el => getComputedStyle(el).{prop}")) def test_log_lines_show_their_type(page, push): open_tab(page, "Run") lines = [("info", "Opening the portal", "blue"), ("success", "Logged in.", "green"), ("warn", "Slow response", "amber"), ("error", "Login failed", "red"), ("error", "raw bot line", "red")] for kind, msg, _ in lines: push("log", {"msg": msg, "type": kind}) log = page.get_by_role("log") for _, msg, colour in lines: line = log.get_by_text(msg, exact=True) expect(line).to_be_visible() assert colour_of(line) == colour, msg def test_report_search_filters_rows_and_shows_bucket_pill(page, push): keys = [c["key"] for c in page.evaluate("REPORT_COLUMNS")] assert "status" in keys, "the report needs a status column for the bucket pill" cases = [("alpha", "Posted", "success", "green"), ("bravo", "Needs review", "attention", "amber"), ("charlie", "Error", "failed", "red")] rows = [{**{k: f"{k}-{name}" for k in keys}, "status": status, "bucket": bucket} for name, status, bucket, _ in cases] open_tab(page, "Report") push("report_data", {"rows": rows}) body = page.get_by_role("table").locator("tbody").get_by_role("row") expect(body).to_have_count(3) search = page.get_by_role("searchbox", name="Search report") for name, status, _, colour in cases: search.fill(name) expect(body).to_have_count(1) pill = body.get_by_text(status, exact=True) expect(pill).to_be_visible() assert colour_of(pill, "backgroundColor") == colour, status search.fill("") expect(body).to_have_count(3) def is_dark(page): """Dark theme = light text on the page.""" rgb = re.findall(r"\d+", page.evaluate("getComputedStyle(document.body).color"))[:3] return sum(int(x) for x in rgb) / 3 > 128 def test_dark_toggle_flips_theme(page, request): before = is_dark(page) assert before == (request.node.callspec.params["page"] == "dark") # apply_dark took effect toggle = page.get_by_role("button", name="Dark mode", exact=True) expect(toggle).to_have_attribute("aria-pressed", str(before).lower()) toggle.click() assert is_dark(page) != before expect(toggle).to_have_attribute("aria-pressed", str(not before).lower()) assert ["set_dark", not before] in page.evaluate("window.__calls") # host is asked to remember it def expect_sample_dashboard(page): legend = page.get_by_role("list", name="Batch status") expect(legend).to_contain_text("·") n = sum(int(c) for c in re.findall(r"· (\d+)", legend.inner_text())) assert n > 0 kpi(page, "Total batches", n) @pytest.mark.usefixtures("sample_data") def test_no_bot_page_runs_on_sample_data(page): """No bot yet: the page answers its own data actions, so it works on day one.""" expect_sample_dashboard(page) open_tab(page, "Report") rows = page.get_by_role("table").locator("tbody").get_by_role("row") expect(rows.first).not_to_have_text("No rows yet") run = page.get_by_role("button", name="▶ Run", exact=True) run.click() expect(run).to_be_disabled() expect(page.get_by_role("list", name="Steps").get_by_role("listitem").last).to_have_text( re.compile(r"^\s*✓"), timeout=10000) expect(run).to_be_enabled() @pytest.mark.usefixtures("sample_data") def test_plain_browser_first_open_shows_sample_data(browser, page_url): pg = browser.new_page() pg.goto(page_url) expect_sample_dashboard(pg) pg.close()