TTA · Build Sheet
The Testing Academy · Hands-on Build

Build a QA Code-Review Skill with GitHub Copilot + Playwright MCP

Stop leaving the same review comment forever. In this build you write one SKILL.md that makes Copilot review every Playwright diff against your QA policy, and wire in Playwright MCP so the reviewer opens the real app and verifies locators instead of guessing. The same folder then powers Copilot code review on pull requests.

~45 minute build 🧱VS Code + Copilot + Node 18+ 📦1 SKILL.md + 1 mcp.json Agent mode + PR reviews
What you ship
01

A review skill

qa-code-review/SKILL.md with your locator ladder, wait rules, and output contract

02

A wired browser

.vscode/mcp.json pointing Copilot at the Playwright MCP server

03

Two review surfaces

the same skill fires in VS Code agent mode and in Copilot code review on PRs

0

The idea

2 min read

Goal: understand the loop you are about to build before typing anything.

A reviewer that checks the app, not just the diff

Every QA team repeats the same review comments: hard-coded waits, brittle selectors, assertions that cannot fail. A skill turns those comments into a file the agent loads on demand. Adding Playwright MCP turns it from a linter into a reviewer with hands.

Diff lands

a PR or local change touches tests/**

Skill loads

Copilot matches the description, pulls in your policy

Checklist runs

locators, waits, assertions, isolation, hygiene

MCP verifies

opens the app, snapshots, checks each suspect locator

Report lands

findings table, verdict, verified-live list

Why MCP matters here. A plain instructions file can only pattern-match the diff. With the Playwright MCP server connected, the reviewing agent can browser_navigate to your running app, take an accessibility browser_snapshot, and confirm the locator it is about to complain about (or suggest) actually resolves. Locator comments arrive with evidence attached instead of a guess (still evidence from one page state, so a human stays in the loop).

Two facts make this build worth doing now. First, GitHub Copilot reads the same open Agent Skills format Claude Code uses, from shared folders, so one file serves both tools. Second, Copilot code review on pull requests supports agent skills and MCP servers, now generally available on Pro, Pro+, Business, and Enterprise plans, and its MCP tool calls are read-only by design.

1

Prereqs

~5 min

Goal: VS Code with Copilot working, Node 18+, and a Playwright repo to review.

Check the four prerequisites

  • VS Code with GitHub Copilot signed in, and agent mode available in the Chat view.
  • A Copilot plan. Skills in the editor work with Copilot; the PR-review bonus in step 8 needs Pro, Pro+, Business, or Enterprise.
  • Node.js 18 or newer. The Playwright MCP server runs via npx and requires it.
  • A repo with Playwright tests so the skill has something real to review. Any project with tests/*.spec.ts works.
terminal
node -v
npx -v
code -v
Windows check. If node or npx prints "not recognized", reinstall Node with the "Add to PATH" option and restart VS Code. If PowerShell blocks npm scripts with an execution-policy error, the usual fix is Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser in PowerShell once, then reopen the terminal (on a managed work machine, check your org's policy first, or just use cmd.exe instead).
Checkpoint

node -v prints 18 or higher, and the Copilot Chat view opens in VS Code.

2

Wire Playwright MCP

~8 min

Goal: Copilot's agent mode can drive a real browser through MCP tools.

Connect the Playwright MCP server

The server is the official @playwright/mcp package. Fastest path, one shell command that registers it for VS Code:

terminal
code --add-mcp '{"name":"playwright","command":"npx","args":["@playwright/mcp@latest"]}'

Prefer a file your whole team gets with git pull? Create .vscode/mcp.json in the repo instead:

.vscode/mcp.json
{
  "servers": {
    "playwright": {
      "command": "npx",
      "args": ["@playwright/mcp@latest"]
    }
  }
}
  • Trust prompt. The first start shows a trust dialog; approve it. You can also add servers through the Command Palette: MCP: Add Server (workspace or global), and MCP: Open User Configuration edits your user-level file.
  • See the tools. In agent mode, the Configure Tools button in the chat input lists everything the server exposed: browser_navigate, browser_snapshot, browser_find, browser_click, browser_type, browser_console_messages, browser_network_requests, browser_take_screenshot, and more.
  • Optional, recommended for this build: add "--caps=testing" to the args. The testing pack adds browser_generate_locator and the browser_verify_* assert-style tools, which fit a reviewer perfectly.
  • Browsers install themselves. Playwright handles browser installation automatically on first use; add "--headless" if you never want a visible window, and --browser chrome|firefox|webkit|msedge to pick the engine.

Smoke-test it in agent mode with a plain prompt:

copilot · agent mode
Open https://demo.playwright.dev/todomvc, take a snapshot,
and tell me the role and name of the input field.
Checkpoint

A browser opens (or runs headless), and Copilot answers from the accessibility snapshot, something like: textbox named "What needs to be done?".

3

Scaffold

~3 min

Goal: an empty skill folder in the location both your tools read.

Create the skill folder in the right place

Copilot discovers project skills from three folders, and the choice decides who else can use your skill:

LocationVS Code CopilotCopilot code review (PRs)Claude Code
.github/skills/yesyesno
.claude/skills/yesnoyes
.agents/skills/yesnono
~/.copilot/skills/ (personal)yesnono

For a review skill the PR bot should also run, .github/skills/ is the home. If your team also lives in Claude Code, keep a copy in .claude/skills/ (same file, two folders, still zero conversion).

your-repo/
├─ .github/
│  └─ skills/
│     └─ qa-code-review/
│        └─ SKILL.md  ← the whole skill
├─ .vscode/
│  └─ mcp.json  ← from step 2
└─ tests/
    └─ login.spec.ts
Naming rule. The frontmatter name must be lowercase letters, numbers, and hyphens only, 64 characters max. Keep the folder named the same: it is the convention, Claude Code expects the match, and qa-code-review becomes the slash command you will type later.
Checkpoint

.github/skills/qa-code-review/SKILL.md exists (empty is fine for now).

4

Frontmatter

~5 min

Goal: a description that makes the skill fire on its own.

Write the frontmatter (the part that decides triggering)

Only the name and description sit in the model's context permanently; the body loads when the description matches the conversation. So the description carries three jobs: what the skill does, when to use it, and the exact phrases a tester would say.

SKILL.md (top)
---
name: qa-code-review
description: >-
  Reviews Playwright test changes against our QA policy: locator ladder,
  wait hygiene, web-first assertions, isolation, and secret handling.
  Use when someone says "review my test changes", "QA review this diff",
  "check my Playwright tests", or a change touches tests/** or *.spec.ts.
  Live-verifies suspicious locators with Playwright MCP browser tools
  when the app is running.
---
Optional fieldDefaultUse it when
argument-hintnoneyou want /qa-code-review <hint> to prompt for input, e.g. a PR number
user-invocabletrueset false to hide the slash command and keep it model-triggered only
disable-model-invocationfalseset true to make it manual-only, never auto-fired
The first suspect. When a skill does not trigger, check the description before anything else: it is the culprit far more often than the body (the troubleshooting list at the bottom covers the rarer causes: frontmatter syntax, discovery path, settings). Description limit is 1024 characters: spend them on trigger phrases, not marketing.
Checkpoint

Type /skills in Copilot chat: the manager lists qa-code-review.

5

The policy

~12 min

Goal: the checklist body: what gets flagged, at what severity, in what output shape.

Encode the review policy

This is your team's judgment, written once. Ours below is a sane default for Playwright + TypeScript; edit the ladder and severities to taste, but keep the two structural ideas: severity tiers (so the verdict is mechanical) and an output contract (so every review looks the same).

SevCategoryWhat gets flagged
P0Broken safety net.only / .skip left behind; assertions wrapped in try/catch or if/else that can silently pass; missing await on expect()
P1Flake sourcespage.waitForTimeout(); waitUntil: "networkidle"; one-shot asserts like expect(await el.isVisible()).toBe(true); raw CSS/XPath/nth() on dynamic UI
P2Maintainabilitylocator ladder violations; copy-pasted setup instead of fixtures; hard-coded URLs instead of baseURL
P3Styleconsole.log debris, dead code, vague test names

The locator ladder the skill enforces

the ladder, best to last resort
getByRole  >  getByLabel  >  getByPlaceholder  >  getByText  >  getByTestId
// raw CSS, XPath, and nth() on dynamic UI are findings, not preferences

Web-first assertions, the one diff to memorize

login.spec.ts · assertion style
- expect(await page.locator('.toast').isVisible()).toBe(true);
+ await expect(page.getByRole('status')).toBeVisible();

The first line checks once and moves on (race condition included, free of charge). The second retries until the element appears or times out: that retry is where most Playwright flake dies.

Keep the body lean. Do not paste Playwright's manual into the skill; the model knows Playwright. The skill carries only what the model cannot know: your ladder, your severities, your output contract.
Checkpoint

Your SKILL.md body has the four severity tiers and the findings-table output format (full file in the listing below).

6

Live verify

~8 min

Goal: the section that separates this skill from a lint config.

Teach the reviewer to verify, not guess

Add a section to the skill that tells the agent exactly when and how to use the MCP tools. The contract has one hard rule: every locator finding is VERIFIED or UNVERIFIED, and the agent never invents a result.

SKILL.md (verification section)
## Live verification with Playwright MCP
When the Playwright MCP browser tools are available AND a locator
finding exists:
1. `browser_navigate` to the page under test. Use the project's
   `baseURL` from playwright.config, or the BASE_URL env value.
2. `browser_snapshot` and check the changed locator's target exists,
   uniquely, in the accessibility tree.
3. Prefer the role and name the snapshot shows; suggest that exact
   replacement locator in the finding.
4. Tag the finding VERIFIED (say what you saw) or UNVERIFIED
   (app not running or page unreachable).
Never fabricate a verification result. In read-only contexts
(Copilot code review on PRs) inspect only: navigate and snapshot,
no clicking, no typing, no state changes.
  • Why snapshots, not screenshots. browser_snapshot returns the accessibility tree as structured text, so the agent can read roles and names directly and propose getByRole('textbox', { name: 'Username' }) with evidence.
  • With --caps=testing the agent can also call browser_generate_locator for a ready-made suggestion and browser_verify_element_visible to assert presence the way a test would.
  • Dev server discipline. Verification needs the app running. Start it before the review, or accept UNVERIFIED tags: both are honest, silent guessing is not.
Design note. The skill degrades gracefully: no MCP server, or no running app, and it still reviews the diff as a checklist, it just marks locator findings UNVERIFIED. One file, useful in every context.
Checkpoint

The verification section is in your SKILL.md and names the two tools it depends on: browser_navigate and browser_snapshot.

7

Test it

~8 min

Goal: two tests: does it fire, and does it review well.

Trigger-test, then review a real (bad) diff

Test 1, triggering. Ask a natural question without naming the skill: "review my Playwright changes". If Copilot does not load qa-code-review, fix the description (add the phrase you just used) and try again. Explicit invocation always works meanwhile: /qa-code-review.

Test 2, quality. Feed it a deliberately bad spec and check every planted bug gets caught:

tests/login.spec.ts (bait)
test('login works', async ({ page }) => {
  await page.goto('http://localhost:3000/login');   // P2: hard-coded URL
  await page.waitForTimeout(3000);                  // P1: hard wait
  await page.locator('#root > form > input:nth-child(2)').fill('admin'); // P1: brittle
  await page.locator('.btn-primary').click();       // P2: class locator
  expect(await page.locator('.toast').isVisible()).toBe(true); // P1: one-shot
});

With the dev server running, a good pass looks like this:

qa-code-review · sample outputIllustration
File:LineSevFindingFix
login.spec.ts:3P1waitForTimeout(3000)delete; the web-first assertion below already waits
login.spec.ts:4P1nth-child CSS locatorVERIFIED: snapshot shows textbox "Username"; use getByRole('textbox', { name: 'Username' })
login.spec.ts:5P2.btn-primary class locatorVERIFIED: snapshot shows button "Sign in"; use getByRole('button', { name: 'Sign in' })
login.spec.ts:6P1one-shot assertionawait expect(page.getByRole('status')).toBeVisible()
login.spec.ts:2P2hard-coded URLuse baseURL + page.goto('/login')

Verdict: request changes (3 x P1). Verified live: lines 4 and 5 against http://localhost:3000.

Grade the reviewer. Five planted bugs, five findings, two of them verified against the real DOM. Anything missed goes back into the skill body as an explicit rule: the skill improves the same way a junior reviewer does, one miss at a time.
Checkpoint

The natural phrase fires the skill, and the bait file produces findings at the right severities with at least one VERIFIED tag.

8

Ship it

~5 min

Goal: teammates and the PR bot both run your reviewer.

Commit it, and light up the PR bot

Shipping is a commit. Everyone who pulls the repo (and has step 1's prereqs: Copilot signed in, Node 18+) now has the reviewer in VS Code, the only remaining click being the one-time MCP trust prompt.

terminal
git add .github/skills/qa-code-review/SKILL.md .vscode/mcp.json
git commit -m "Add qa-code-review skill + Playwright MCP config"
git push

Then the bonus surface. Copilot code review on github.com reads the same .github/skills/ folder, and its agent-skills + MCP support is now generally available:

VS Code agent mode

interactive
  • skill from .github/skills/ or .claude/skills/
  • MCP from .vscode/mcp.json, full browser tools
  • can click, type, and re-run while you iterate
  • invoke naturally or with /qa-code-review

Copilot code review (PRs)

automatic
  • skill from .github/skills/ on the PR head branch
  • MCP from repo Settings → Copilot → MCP servers
  • Playwright and GitHub MCP servers enabled by default
  • all MCP calls read-only; comments show skill attribution
  • Configure PR-side MCP in repository Settings → Copilot → MCP servers; auth secrets live under Settings → Secrets and variables → Agents.
  • Test from a branch. Custom instructions and skills are read from the PR head branch, so you can iterate on the skill in a feature branch and watch the bot's review change before merging.
  • Claude Code teammates: drop the same folder in .claude/skills/ and it fires there too, identical file.
Checkpoint

A teammate pulls, asks "review my test changes", and your policy answers. On a PR, Copilot's review comments start citing the skill.

The full file

copy-paste

Goal: the complete SKILL.md, everything above in one file.

The complete SKILL.md

Skip the typing. Download qa-code-review-skill.zip (under 2 KB) and unzip it into .github/skills/ (Copilot + PR reviews) or .claude/skills/ (adds Claude Code). It expands to the exact qa-code-review/SKILL.md below.
.github/skills/qa-code-review/SKILL.md
---
name: qa-code-review
description: >-
  Reviews Playwright test changes against our QA policy: locator ladder,
  wait hygiene, web-first assertions, isolation, and secret handling.
  Use when someone says "review my test changes", "QA review this diff",
  "check my Playwright tests", or a change touches tests/** or *.spec.ts.
  Live-verifies suspicious locators with Playwright MCP browser tools
  when the app is running.
---

# QA code review for Playwright changes

## When to use
- A diff or PR touches tests/**, *.spec.ts, playwright.config.*, or page objects.
- Someone asks for a QA review, test review, or flake check on changed tests.

## Review workflow
1. Collect the changed test files. Review ONLY the diff, not the whole repo.
2. Walk the findings categories below in order. Note file and line for every hit.
3. If the app is reachable, live-verify every locator finding (see below).
4. Output the findings table, then the verdict, then the verified-live list.

## Findings to flag
### P0, broken safety net
- .only or .skip left on tests
- assertions inside try/catch or behind if/else that can silently pass
- missing await on expect() web-first assertions
### P1, flake sources
- page.waitForTimeout() in any form
- waitUntil: "networkidle"
- one-shot assertions: expect(await locator.isVisible()).toBe(true)
- raw CSS, XPath, or nth() locators on dynamic UI
### P2, maintainability
- locator ladder violations; prefer getByRole > getByLabel >
  getByPlaceholder > getByText > getByTestId, and anything below
  the ladder needs a comment explaining why
- copy-pasted setup instead of fixtures
- hard-coded URLs or credentials; use baseURL and env config
### P3, style
- console.log debris, dead code, vague test names

## Live verification with Playwright MCP
When the Playwright MCP browser tools are available AND a locator
finding exists:
1. browser_navigate to the page under test. Use the project's baseURL
   from playwright.config, or the BASE_URL env value.
2. browser_snapshot and check the changed locator's target exists,
   uniquely, in the accessibility tree.
3. Prefer the role and name the snapshot shows; suggest that exact
   replacement locator in the finding.
4. Tag the finding VERIFIED (say what you saw) or UNVERIFIED (app not
   running or page unreachable).
Never fabricate a verification result. In read-only contexts (Copilot
code review on PRs) inspect only: navigate and snapshot, no clicking,
no typing, no state changes.

## Output format
A findings table with columns: File:Line | Severity | Finding | Fix.
Then: Verdict: approve or request changes, with one line of reasoning
(any P0 or two or more P1 findings mean request changes).
Then: Verified live: the list of checks performed, or "none
(app not reachable)".

## Guardrails
- Review the diff you were given; do not rewrite files unless asked.
- Quote the exact offending line in every finding.
- If there are zero findings, say so, and still report what you verified.
- The output is advice for a human reviewer, not an auto-merge signal.

Troubleshooting

The skill never triggers on its own

Description problem, almost always. Add the exact phrase you keep typing to the description, keep it under 1024 characters, and confirm the skill shows up in /skills. Explicit /qa-code-review proves the body works while you tune triggering.

MCP server never starts, or no browser tools appear

Check Node with node -v (needs 18+). Approve the trust dialog on first start. Open Configure Tools in agent mode and confirm the playwright entry is toggled on. On Windows, if npx fails with an execution-policy error, run Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser once in PowerShell and restart VS Code.

Everything comes back UNVERIFIED

That is the skill being honest: the app is not running or the baseURL is wrong. Start the dev server, check baseURL in playwright.config.ts, and re-run. UNVERIFIED findings are still valid checklist findings.

The PR bot ignores the skill

Confirm the file is at .github/skills/qa-code-review/SKILL.md on the PR's head branch, and that your plan includes Copilot code review (Pro, Pro+, Business, Enterprise). Remember PR-side MCP is configured in repo Settings, not .vscode/mcp.json, and every MCP call there is read-only.