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
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
npxand requires it. - A repo with Playwright tests so the skill has something real to review. Any project with
tests/*.spec.tsworks.
node -v npx -v code -v
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).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:
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:
{
"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), andMCP: Open User Configurationedits 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 addsbrowser_generate_locatorand thebrowser_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|msedgeto pick the engine.
Smoke-test it in agent mode with a plain prompt:
Open https://demo.playwright.dev/todomvc, take a snapshot, and tell me the role and name of the input field.
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:
| Location | VS Code Copilot | Copilot code review (PRs) | Claude Code |
|---|---|---|---|
.github/skills/ | yes | yes | no |
.claude/skills/ | yes | no | yes |
.agents/skills/ | yes | no | no |
~/.copilot/skills/ (personal) | yes | no | no |
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).
├─ .github/
│ └─ skills/
│ └─ qa-code-review/
│ └─ SKILL.md ← the whole skill
├─ .vscode/
│ └─ mcp.json ← from step 2
└─ tests/
└─ login.spec.ts
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..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.
--- 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 field | Default | Use it when |
|---|---|---|
argument-hint | none | you want /qa-code-review <hint> to prompt for input, e.g. a PR number |
user-invocable | true | set false to hide the slash command and keep it model-triggered only |
disable-model-invocation | false | set true to make it manual-only, never auto-fired |
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).
| Sev | Category | What gets flagged |
|---|---|---|
| P0 | Broken safety net | .only / .skip left behind; assertions wrapped in try/catch or if/else that can silently pass; missing await on expect() |
| P1 | Flake sources | page.waitForTimeout(); waitUntil: "networkidle"; one-shot asserts like expect(await el.isVisible()).toBe(true); raw CSS/XPath/nth() on dynamic UI |
| P2 | Maintainability | locator ladder violations; copy-pasted setup instead of fixtures; hard-coded URLs instead of baseURL |
| P3 | Style | console.log debris, dead code, vague test names |
The locator ladder the skill enforces
getByRole > getByLabel > getByPlaceholder > getByText > getByTestId
// raw CSS, XPath, and nth() on dynamic UI are findings, not preferencesWeb-first assertions, the one diff to memorize
- 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.
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.
## 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_snapshotreturns the accessibility tree as structured text, so the agent can read roles and names directly and proposegetByRole('textbox', { name: 'Username' })with evidence. - With
--caps=testingthe agent can also callbrowser_generate_locatorfor a ready-made suggestion andbrowser_verify_element_visibleto 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.
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:
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:
| File:Line | Sev | Finding | Fix |
|---|---|---|---|
login.spec.ts:3 | P1 | waitForTimeout(3000) | delete; the web-first assertion below already waits |
login.spec.ts:4 | P1 | nth-child CSS locator | VERIFIED: snapshot shows textbox "Username"; use getByRole('textbox', { name: 'Username' }) |
login.spec.ts:5 | P2 | .btn-primary class locator | VERIFIED: snapshot shows button "Sign in"; use getByRole('button', { name: 'Sign in' }) |
login.spec.ts:6 | P1 | one-shot assertion | await expect(page.getByRole('status')).toBeVisible() |
login.spec.ts:2 | P2 | hard-coded URL | use baseURL + page.goto('/login') |
Verdict: request changes (3 x P1). Verified live: lines 4 and 5 against http://localhost:3000.
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.
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.
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
.github/skills/ (Copilot + PR reviews) or .claude/skills/ (adds Claude Code). It expands to the exact qa-code-review/SKILL.md below.--- 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.