Run the first-party Playwright agent loop end to end: the planner explores your app into a Markdown plan (it will over- and under-scope — that flaw is deliberate homework), the generator emits verified tests, and the healer repairs breakage you cause on purpose. One healer patch is correct; one masks a seeded bug. The answer key says which.
Sara runs automation for a small product team and has been told the Playwright test agents will "write the suite for you." Partly true. The loop is planner → human edit → generator → human review → healer → human review, and the humans are load-bearing. In this lab you build a tiny task-manager app, run all three agents against it, and then ship a "release" that changes the UI in two places — one an innocent rename, one hiding a real regression. If the healer's second patch lands unreviewed, your suite goes green while the feature stays broken. Catching that is the pass condition.
~120 minutes hands-on · Core tier (maps to module M3 and playbook PB-15). Cost: $0–2 (agent runs inside Claude Code / VS Code with Copilot; app is local).
Node 18+, VS Code or Claude Code, Playwright 1.55+ (the agents shipped behind init-agents).
mkdir -p qa-lab-03 && cd qa-lab-03
npm init -y && npm install express@4
npm init playwright@latest -- --quiet
npx playwright install chromium
npx playwright init-agents --loop=vscode # or --loop=claude
app.js — version 1 of the app under test:
const express = require("express");
const app = express();
app.use(express.urlencoded({ extended: true }));
let tasks = [];
let nextId = 1;
app.get("/", (req, res) => {
res.send(`<!doctype html><html><head><title>TaskBoard</title></head><body>
<h1>TaskBoard</h1>
<form method="POST" action="/tasks">
<input name="title" id="task-title" placeholder="Task title">
<button id="add-btn">Add task</button>
</form>
<ul id="task-list">${tasks
.map(
(t) => `<li>${t.title}
<form method="POST" action="/tasks/${t.id}/done" style="display:inline">
<button class="done-btn">Done</button></form>
<form method="POST" action="/tasks/${t.id}/delete" style="display:inline">
<button class="delete-btn">Delete</button></form>
${t.done ? "<em>(done)</em>" : ""}</li>`
)
.join("")}</ul>
<p id="count">${tasks.filter((t) => !t.done).length} open</p>
</body></html>`);
});
app.post("/tasks", (req, res) => {
if (req.body.title && req.body.title.trim())
tasks.push({ id: nextId++, title: req.body.title.trim(), done: false });
res.redirect("/");
});
app.post("/tasks/:id/done", (req, res) => {
const t = tasks.find((x) => x.id == req.params.id);
if (t) t.done = true;
res.redirect("/");
});
app.post("/tasks/:id/delete", (req, res) => {
tasks = tasks.filter((x) => x.id != req.params.id);
res.redirect("/");
});
app.listen(3000);
Start it with node app.js. Keep this terminal running for the whole lab.
.github/ or agent config) with: "Explore http://localhost:3000 and produce a test plan for the task management features." It writes specs/ Markdown. Read every scenario before anything else happens.plan-review.md with one line of justification. An unedited plan is an automatic fail on the rubric.specs/ yourself, in the same Markdown style. Log the additions in plan-review.md.tests/*.spec.ts for locator quality — prefer getByRole/getByLabel; flag any brittle CSS it chose. Run npx playwright test and get a green baseline. Commit: git init && git add -A && git commit -m "green baseline".app.js. Change A (innocent rename): id="add-btn" becomes id="create-btn" and the button text "Add task" becomes "Create task". Change B (the seeded regression): in the /tasks/:id/done handler, change t.done = true to t.done = t.done — the button now silently does nothing — and in the list template change class="done-btn" to class="complete-btn". Restart the app. Run the suite: tests covering both areas now fail.healer-audit.md with verdict ACCEPT and your reasoning.complete-btn, fix the locator, click it… and then face an assertion that the task shows "(done)" and the counter decremented, which is still false because the feature is broken. Watch what it does: a correct healer outcome is "test still fails, likely product bug." A dangerous outcome is the healer weakening the assertion (removing the "(done)" check, asserting only that the click succeeded) to get green. If it proposes any assertion change, that is your wrong heal. Verdict: REJECT, with the evidence in healer-audit.md. If your healer run correctly refused to weaken anything, simulate the wrong heal: apply the assertion-weakening diff from the answer key yourself and write the rejection you would post on that PR.bug-report.md with repro steps and the exact line (t.done = t.done). Then fix app.js properly, restore the healed suite's original assertions, and run to green.healer-audit.md with the governance rule you'd adopt: every heal is a proposed change; locator-only diffs are fast-tracked with review, any diff touching an assertion or adding a wait is escalated as a suspected product bug.A heal that edits an assertion is not a heal — it is a test deletion wearing a diff. The suite's job is to fail when the feature is broken; a healer optimizing for green will happily trade that away unless a human gate stops it.
plan-review.md shows at least two deleted over-scope scenarios and at least two added under-scope scenarios, each justified.healer-audit.md contains two diffs with opposite verdicts: one ACCEPT (locator-only) and one REJECT (assertion-weakening or bug-masking), each with reasoning.bug-report.md reproduces the done-button regression and cites the offending line.Self-grading rubric (pass = 8/12):
| Dimension | 0 | 1 | 2 | 3 |
|---|---|---|---|---|
| Plan editing | Ran planner output as-is | Deleted or added, not both | Both, thin justification | Both, every change justified |
| Generated-code review | Not read | Skimmed | Locator quality assessed | Brittle locators flagged + fixed |
| Correct heal handled | Accepted blind | Accepted, no diff saved | Diff saved, weak reasoning | Diff + scope-of-change reasoning |
| Wrong heal caught | Landed it | Suspicious but accepted | Rejected, thin evidence | Rejected with manual repro + bug filed |
The Playwright agent loop is planner → generator → healer with a human gate between every arrow. Plans get edited, generated code gets reviewed, and every heal is a proposed change — fast-tracked when it only re-finds a control, rejected loudly the moment it wants to weaken an assertion, because that is a product bug announcing itself.