This topic walks one feature end to end, the way it actually plays out on a real project: a product manager writes a short feature story, you turn it into a live-browser-verified Playwright spec with an AI agent, run it, hit a real failure, fix it, and fold the result into a standing regression suite. No tool-specific setup here — this applies to Claude Code, Cursor, Gemini CLI, or OpenCode equally once Playwright MCP is connected (see the earlier topics in this module for per-tool config).
The feature: a "bulk archive" action on a task-management app's list view — select multiple tasks via checkboxes, click "Archive selected," confirm in a dialog, and the tasks disappear from the active list and reappear under an "Archived" filter.
Workflow Overview: From Feature Story to Automated Regression Suite
The shape of this workflow, regardless of which agent drives it:
- Describe the feature in terms of user-observable behavior, not implementation. Include the happy path and at least the one or two edge cases you actually care about — the agent won't invent edge cases you didn't mention.
- Generate a scaffold by having the agent drive the live app via Playwright MCP, confirming the flow actually works as described, then translating that into a
.spec.tsfile. - Run it immediately — never trust a generated test until it's actually been executed at least once outside the agent's own live session.
- Interpret failures with the agent's help, using Playwright MCP to re-investigate the live page rather than guessing from a stack trace.
- Fix, expand, and commit — once the core flow is solid, ask for edge-case coverage (empty selection, single item, all items, undo) and fold the finished file into whatever regression suite runs in CI.
The point of using MCP specifically, rather than asking a model to write a test from a text description alone, is that steps 2 and 4 involve actually looking at the running app — real selector names, real timing behavior, real error text — instead of a plausible-sounding guess.
Tips
- Keep the feature description scoped to one cohesive user action per generation pass — "bulk archive" is a good unit; "the whole task list page" is not.
- Decide up front which edge cases matter to you and say them explicitly; don't rely on the agent to surface them unprompted.
- Never skip the "run it immediately" step even when the agent reports success during its live walkthrough — a passing live session and a passing saved test file are two different claims.
Step 1: Describing the Feature and Generating a Playwright Test Scaffold
The prompt, written the way you'd actually type it — specific about the app URL, specific about the exact confirmation text, and naming the one edge case that matters most (archiving with nothing selected should be a no-op, not an error):
Navigate to http://localhost:3000/tasks (test user already seeded, 5 tasks
visible). Select the checkboxes for tasks 1 and 3, click "Archive selected",
confirm in the dialog that appears ("Archive 2 tasks?" with Confirm/Cancel
buttons), and verify: (a) the active list now shows 3 tasks, (b) switching to
the "Archived" filter shows the 2 archived tasks. Also check: with nothing
selected, the "Archive selected" button should be disabled, not clickable.
Write the result as tests/e2e/bulk-archive.spec.ts.
Live investigation surfaces real details a text-only description would miss — for instance, whether the confirm dialog is a native window.confirm (not automatable the normal way, requires page.on('dialog')) or a custom in-page modal (just another set of DOM elements). In this app it turns out to be a custom modal, which is the more common case in modern SPAs and the easier one to test:
import { test, expect } from '@playwright/test';
test.describe('bulk archive', () => {
test('archives selected tasks and moves them to the Archived filter', async ({ page }) => {
await page.goto('/tasks');
await page.getByRole('checkbox', { name: 'Select task 1' }).check();
await page.getByRole('checkbox', { name: 'Select task 3' }).check();
await page.getByRole('button', { name: 'Archive selected' }).click();
await expect(page.getByText('Archive 2 tasks?')).toBeVisible();
await page.getByRole('button', { name: 'Confirm' }).click();
await expect(page.getByTestId('task-row')).toHaveCount(3);
await page.getByRole('button', { name: 'Archived' }).click();
await expect(page.getByTestId('task-row')).toHaveCount(2);
});
test('disables the archive button when no tasks are selected', async ({ page }) => {
await page.goto('/tasks');
await expect(page.getByRole('button', { name: 'Archive selected' })).toBeDisabled();
});
});
Run it right away, before doing anything else:
npx playwright test tests/e2e/bulk-archive.spec.ts --reporter=list
Tips
- Ask the agent explicitly whether a confirmation dialog is native (window.confirm) or a custom DOM modal before it writes the test — the required Playwright API differs completely (page.on('dialog')vs. normal locators), and getting this wrong is a common source of a test that hangs indefinitely.
- Name the exact confirmation text you expect ("Archive 2 tasks?") in the prompt — it forces the agent to verify the count is dynamic and correctly pluralized/numbered rather than a static string.
- Test the "nothing selected" disabled-state case in the same file as the happy path — it's cheap to add while the agent already has full context on the button's behavior.
Step 2: Running the Test and Using AI to Interpret Failures
Here's a realistic failure from that first run:
1) bulk archive › archives selected tasks and moves them to the Archived filter
Error: expect(locator).toHaveCount(expected) failed
Locator: getByTestId('task-row')
Expected: 3
Received: 5
Timeout: 5000ms
The count never dropped to 3 — the archive action didn't visibly take effect within the test's default timeout. Feed this straight back to the agent along with the raw failure:
This test failed:
[paste failure above]
Use Playwright MCP to repeat the flow manually — navigate, select tasks 1
and 3, click Archive selected, confirm — and this time also check
browser_network_requests to see if the archive API call actually fires and
what it returns. I want to know if this is a timing issue in the test or a
real app bug.
A realistic outcome: the agent finds the archive PATCH request does fire and returns 200, but the UI doesn't re-render the list until a manual page interaction — a real optimistic-update bug in the app, not a flaky test. This is exactly the value of MCP-driven debugging: the agent can distinguish "my test's timing assumption is wrong" from "the feature is actually broken," because it's checking network activity against DOM state directly rather than guessing from a timeout message alone.
Agent: The PATCH /api/tasks/archive request returns 200 with the updated
task list in the response body, but the UI's task list doesn't re-render
until something else triggers a re-fetch — I confirmed by manually clicking
the "Archived" filter afterward, which did show 2 tasks were actually
archived server-side. This looks like a real bug: the active list view isn't
invalidating its cache after a successful archive mutation.
That's a genuine finding from AI-driven test generation surfacing a real production bug before it reached users, not a hypothetical — cache invalidation gaps after a mutation are a very common class of bug in apps using client-side data caching (React Query, SWR, Apollo), and this workflow catches them because the test drives the actual UI rather than mocking the API layer entirely.
Tips
- Always ask the agent to checkbrowser_network_requestsalongside DOM state when a count/visibility assertion fails unexpectedly — it's the fastest way to separate "API didn't do what we expected" from "API worked, UI didn't reflect it."
- Treat a test failure that turns out to be a real bug as a genuine deliverable of this workflow, not a distraction from finishing the test — file it (or fix it) before treating the test task as done.
- Increase the assertion timeout only after confirming there isn't a real underlying bug — bumping{ timeout: 15000 }to paper over a genuine cache-invalidation issue just delays discovery to production.
Step 3: Fixing Failures, Expanding Coverage, and Adding to Regression Suite
With the real bug identified (list not invalidating cache after archive), the fix is application code, not test code — outside Playwright MCP's scope, but worth having the agent locate the relevant hook while it has full context:
Find the React Query hook responsible for fetching the active task list
(likely in src/hooks/useTasks.ts) and check whether the archive mutation
invalidates that query key on success.
// src/hooks/useTasks.ts — before
export function useArchiveTasks() {
return useMutation({
mutationFn: (ids: string[]) => api.patch('/tasks/archive', { ids }),
});
}
// after — invalidate the active list query on success
export function useArchiveTasks() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (ids: string[]) => api.patch('/tasks/archive', { ids }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tasks', 'active'] });
queryClient.invalidateQueries({ queryKey: ['tasks', 'archived'] });
},
});
}
Re-run the test — it should now pass without any timeout adjustment, confirming the fix rather than the test being loosened:
npx playwright test tests/e2e/bulk-archive.spec.ts --reporter=list
Running 2 tests using 1 worker
✓ bulk archive › archives selected tasks and moves them to the Archived filter (1.4s)
✓ bulk archive › disables the archive button when no tasks are selected (312ms)
2 passed (2.1s)
Now expand coverage — ask explicitly for the edge cases that matter, since the agent won't add them unprompted:
Add two more test cases to the same file: (1) archiving all 5 visible tasks
at once, verifying the active list shows an empty state message
"No active tasks", and (2) archiving a single task via its individual
checkbox (not select-all) to confirm the singular confirmation text reads
"Archive 1 task?" not "Archive 1 tasks?".
test('archives all visible tasks and shows the empty state', async ({ page }) => {
await page.goto('/tasks');
await page.getByRole('checkbox', { name: 'Select all' }).check();
await page.getByRole('button', { name: 'Archive selected' }).click();
await page.getByRole('button', { name: 'Confirm' }).click();
await expect(page.getByText('No active tasks')).toBeVisible();
});
test('uses correct singular confirmation text for a single task', async ({ page }) => {
await page.goto('/tasks');
await page.getByRole('checkbox', { name: 'Select task 1' }).check();
await page.getByRole('button', { name: 'Archive selected' }).click();
await expect(page.getByText('Archive 1 task?')).toBeVisible();
await page.getByRole('button', { name: 'Cancel' }).click();
});
That singular/plural test is a small thing, but it's exactly the class of bug ("Archive 1 tasks?" grammatically wrong) that's easy to ship and mildly embarrassing in a demo — cheap to catch here.
Finally, fold the file into whatever regression suite runs on every PR. If the project already has a playwright.config.ts with a test-suite project or a CI workflow globbing tests/e2e/**/*.spec.ts, no extra wiring is needed — just commit:
git add tests/e2e/bulk-archive.spec.ts src/hooks/useTasks.ts
git commit -m "Add bulk archive E2E coverage and fix list cache invalidation bug"
- name: Run Playwright E2E suite
run: npx playwright test --reporter=github
For teams running a nightly or pre-release full regression pass rather than E2E on every PR (common when a suite gets large enough that per-PR run time becomes a bottleneck), tag the new spec accordingly so it's included in the right run:
test.describe('bulk archive @regression', () => {
// ...
});
npx playwright test --grep @regression
Tips
- Fix the real bug in application code before declaring the test task done — a test that only passes because you loosened a timeout is worse than no test, since it hides the actual defect.
- Explicitly request boundary-condition coverage (all-selected, single-item, zero-selected) after the core flow passes — this is where AI-generated suites most often stay shallow if you don't ask.
- Use@regression-style tags (or your project's existing convention) to slot new specs into the right CI cadence rather than assuming every new test belongs in the fastest, most frequent run.
Tips
Tips
- The MCP-driven live investigation step is what turns "AI writes tests" into "AI finds bugs" — always let the agent check both DOM state and network activity when a failure is ambiguous, rather than accepting the first plausible explanation.
- Budget for this workflow producing real application bugs, not just test files — treat that as a feature of the process, not scope creep.
- Close the loop by committing the generated spec into your actual CI-run suite the same day you write it; a generated test sitting unused in a branch provides zero regression protection.