From cfa1f182c62103bd35b708b6504bf378bf6365c8 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Fri, 24 Jul 2026 14:14:27 +0200 Subject: [PATCH] ci: per-spec e2e results in the run summary via Playwright json (refs #136) --- .gitea/workflows/ci.yaml | 7 +++++ .gitignore | 1 + infra/playwright-summary.py | 57 ++++++++++++++++++++++++++++++++++ infra/run-e2e-check.sh | 6 +++- tests/e2e/playwright.config.ts | 4 ++- 5 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 infra/playwright-summary.py diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index a23aaa8..2790001 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -260,6 +260,13 @@ jobs: echo "| Golden-signal metrics (Prometheus) | $(icon "$METRICS") |" echo "| Self-service e2e (Playwright) | $(icon "$E2E") |" } >> "$GITHUB_STEP_SUMMARY" + # Job summary (#136): per-spec Playwright results, from the JSON report run-e2e-check.sh copied + # out of the e2e container. Turns a red e2e into a one-glance "which spec" instead of a log dive. + - name: e2e spec summary + if: always() + run: | + [ -n "${GITHUB_STEP_SUMMARY:-}" ] || exit 0 + python3 infra/playwright-summary.py tests/e2e/playwright-report.json >> "$GITHUB_STEP_SUMMARY" # Log dump must precede teardown (which removes the containers). - name: Dump container logs on failure if: failure() diff --git a/.gitignore b/.gitignore index 9efb10b..b8d2203 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,4 @@ tests/e2e/playwright-report/ __pycache__/ TestResults/ test-output/ +tests/e2e/playwright-report.json diff --git a/infra/playwright-summary.py b/infra/playwright-summary.py new file mode 100644 index 0000000..25e1490 --- /dev/null +++ b/infra/playwright-summary.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Render a per-spec table from a Playwright JSON report for a Gitea job summary (#136). + +Reads the JSON report (default: tests/e2e/playwright-report.json) that run-e2e-check.sh copies out +of the e2e container, and prints a markdown table (one row per spec) to stdout. The CI step +redirects it into $GITHUB_STEP_SUMMARY. Stdlib only. +""" +import json +import os +import sys + +STATUS_ICON = {"expected": "✅", "unexpected": "❌", "skipped": "⏭️", "flaky": "⚠️"} + + +def walk(suite, out): + for spec in suite.get("specs", []): + # A spec's status is carried on its test(s): expected/unexpected/skipped/flaky. + statuses = [t.get("status") for t in spec.get("tests", [])] + status = ("unexpected" if "unexpected" in statuses + else "flaky" if "flaky" in statuses + else "skipped" if statuses and all(s == "skipped" for s in statuses) + else "expected" if spec.get("ok", False) + else "unexpected") + out.append({"file": spec.get("file") or suite.get("file") or suite.get("title", ""), + "title": spec.get("title", ""), "status": status}) + for child in suite.get("suites", []): + walk(child, out) + + +def main(path): + if not os.path.exists(path): + print("## 🎭 e2e (Playwright)\n\n_No e2e report — the run did not reach the e2e step._") + return 0 + with open(path) as fh: + report = json.load(fh) + specs = [] + for suite in report.get("suites", []): + walk(suite, specs) + + print("## 🎭 e2e (Playwright)\n") + stats = report.get("stats", {}) + if stats: + print(f"**{stats.get('expected', 0)} passed · {stats.get('unexpected', 0)} failed · " + f"{stats.get('flaky', 0)} flaky · {stats.get('skipped', 0)} skipped** " + f"({round(stats.get('duration', 0) / 1000)}s)\n") + if not specs: + print("_No specs ran._") + return 0 + print("| Spec | Result |") + print("| ---- | :----: |") + for s in specs: + print(f"| {s['file']} › {s['title']} | {STATUS_ICON.get(s['status'], '❔')} |") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "tests/e2e/playwright-report.json")) diff --git a/infra/run-e2e-check.sh b/infra/run-e2e-check.sh index e781a9d..65f0197 100755 --- a/infra/run-e2e-check.sh +++ b/infra/run-e2e-check.sh @@ -26,4 +26,8 @@ cid="$(docker create --network "$net" -w /e2e --ipc=host \ mcr.microsoft.com/playwright:v1.61.1-noble sh -c 'npm install --no-audit --no-fund && npx playwright test')" trap 'docker rm -f "$cid" >/dev/null 2>&1 || true' EXIT docker cp "$root/tests/e2e/." "$cid:/e2e" >/dev/null -docker start -a "$cid" +rc=0 +docker start -a "$cid" || rc=$? +# Copy the Playwright JSON report out — regardless of pass/fail — for the CI job summary (#136). +docker cp "$cid:/e2e/playwright-report.json" "$root/tests/e2e/playwright-report.json" 2>/dev/null || true +exit $rc diff --git a/tests/e2e/playwright.config.ts b/tests/e2e/playwright.config.ts index 43298c0..f0106f6 100644 --- a/tests/e2e/playwright.config.ts +++ b/tests/e2e/playwright.config.ts @@ -21,7 +21,9 @@ export default defineConfig({ // OOM-killed mid-action ("Page crashed") — fixing the flakiness at its source rather than leaning // on `retries` (CLAUDE.md §15). Only two long-running happy-path specs, so serial costs little. workers: 1, - reporter: [['list']], + // `list` for the live log; `json` (→ /e2e/playwright-report.json in the container) is copied out + // by run-e2e-check.sh and rendered as a per-spec table in the CI job summary (#136). + reporter: [['list'], ['json', { outputFile: 'playwright-report.json' }]], use: { baseURL, trace: 'on-first-retry',