Files
Rpa_Skills/tests/page_check/test_contract.py
T
VasanthandClaude Opus 5.5 162edd41df No-AI desktop path: PyQt bot wired to the page
- templates/bot_runner.py (new, no Qt): runs the bot as a child process
  (unbuffered, UTF-8), tails its output, keeps raw lines in bot_raw.log,
  and turns lines into step/log/status events. Step markers and friendly
  rules use specific substrings, first match wins; unmatched error lines
  are always shown, other unmatched lines hidden. Dry run:
  python dashboard/bot_runner.py prints the events the page would get.
- bridge.py: delegates to bot_runner, one run at a time, restores the
  saved dark mode on every page load (fixes sample-data mode in PyQt),
  adds load_latest_report / download_report slots the page calls.
- SKILL.md: skip the stack question for a PyQt project; desktop wiring
  steps from the bot's real output; verify steps for the host.
- tests: assert-based self-check for the line logic and run loop; Seam 1
  run replay (status dot, Run button, steps, log) and wired-page mode;
  fixture b-pyqt-bot and its Seam 2 eval (all 14 checks pass).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 14:28:51 +05:30

173 lines
6.9 KiB
Python

"""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", "<b>raw bot line</b>", "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()