🪞 Mirror for Playwright
Automatically compares your Playwright test runs across QA and PROD (or any two environments), and tells you exactly what changed — not just that something failed.
What it catches
- Element ID / attribute drift:
#username in QA is #userid in PROD — even when id, name, and placeholder all changed at once.
- Value drift: a cart total showing
$50.00 in QA and $45.00 in PROD, with the same element id.
- Missing content: a validation message that shows in QA but never renders in PROD.
- Dialogs:
alert() / confirm() / prompt() text differences.
- Iframes and extra windows/tabs: scoped separately so a field inside a popup is never confused with one on the main page.
All of this works for any page, any form, any workflow — there's no config file, no selector alias list, and no per-page setup. It reads your test's own actions (what you .fill(), what you .click()) to scope evidence to exactly what that test touched.
Zero changes to your existing project
This is the core design principle: Mirror never modifies your real playwright.config.ts, and your spec files never need environment-branching logic.
- Mirror generates a separate
mirror-playwright.config.ts (or .js) that runs your existing spec files — same testDir, same tests — against QA and PROD as two projects with different baseURLs. Your own config is untouched.
- Because
baseURL is handled per-project, your tests just need relative navigation (page.goto('/login')) — no if (projectName === 'QA') { ... } else { ... } needed anywhere.
- Only
"mirror:*" scripts get added to package.json — your own "test" script is never touched.
Works with your actual stack — automatically detected
Mirror detects your project's setup and generates the right files for it, no configuration needed:
| Your stack |
What Mirror generates |
| Playwright + TypeScript |
mirror-playwright.config.ts, mirror-fixtures.js |
| Playwright + JavaScript |
mirror-playwright.config.js, mirror-fixtures.js |
| Playwright + TS + Cucumber |
Both of the above, plus mirror-cucumber.js + .d.ts + a TypeScript hooks example |
| Playwright + JS + Cucumber |
Both of the above, plus mirror-cucumber.js + a JavaScript hooks example |
Cucumber only (no @playwright/test runner) |
mirror-cucumber.js + hooks example — no Playwright config needed |
Mixed Playwright + Cucumber suites produce one unified report covering both.
Quick start — step by step
Pick the scenario that matches your project. Both work the same way underneath — you just wire in one file differently depending on whether you use Cucumber or not.
🟦 Scenario 1: Playwright only (JavaScript or TypeScript, no Cucumber)
This is for projects where your tests are written directly with Playwright's test() function — no Gherkin/feature files involved.
Step 1 — Install the extension
Search "Mirror for Playwright" in the VS Code Extensions tab, or install from the Marketplace link, or run:
code --install-extension raamlakshmanravuri.mirror-for-playwright
Step 2 — Open your project folder in VS Code
Just your normal Playwright project — nothing needs to be prepared in advance.
Step 3 — Run the setup command
Press Ctrl+Shift+P (Command Palette), type MIRROR, and choose:
MIRROR: Setup Project
Mirror looks at your project and automatically creates:
scripts/mirror-fixtures.js — the file that watches your tests and records what happens
scripts/mirror-engine.js and scripts/mirror-reporter.js — compare results and build the report
mirror-playwright.config.ts (or .js) — a separate config file just for Mirror. Your real playwright.config.ts is never touched.
.env — a small file to hold your QA and PROD website addresses
Step 4 — Tell Mirror your QA and PROD URLs
Run:
MIRROR: Configure Environments
Type in your QA URL, then your PROD URL. (You can also just open .env and edit it directly if you prefer.)
Step 5 — Connect your existing test files to Mirror
Run:
MIRROR: Patch Test Imports
This changes ONE line at the top of each of your spec files:
// before
import { test, expect } from '@playwright/test';
// after — Mirror automatically changes it to this
import { test, expect } from '../scripts/mirror-fixtures';
That's it — nothing else in your test files changes. Every Playwright command you already know (page.fill, page.click, expect(...)) still works exactly the same. This one line just lets Mirror quietly watch and record what's happening.
Step 6 — Run the comparison
Run:
MIRROR: Run Full Comparison
Mirror runs your tests against QA, then against PROD, and builds a report comparing them.
Step 7 — Open the report
Open the file mirror-report/mirror-report.html in your browser. Green rows mean QA and PROD matched. Red rows mean something's different — click "🔍 Mismatch" on any red row to see exactly what changed (which element, which attribute, which value).
Done! That's the whole flow for a plain Playwright project.
🟩 Scenario 2: Playwright + Cucumber (JavaScript or TypeScript)
This is for projects where your tests are written as .feature files (Gherkin: Given, When, Then) run through cucumber-js, with step definitions that use Playwright underneath.
Steps 1–4 are identical to Scenario 1 — install the extension, open your project, run MIRROR: Setup Project, run MIRROR: Configure Environments.
The difference starts at Step 3: because this is a Cucumber project, Mirror automatically creates different files instead of mirror-fixtures.js:
scripts/mirror-cucumber.js — the Cucumber version of the "watcher" file
scripts/mirror-cucumber.d.ts — so TypeScript understands it (TypeScript projects only)
scripts/mirror-cucumber-hooks.example.js (or .ts) — a ready-made example showing exactly what to add to your project
Step 5 (different from Scenario 1) — Add two lines to your existing hooks
Cucumber doesn't have spec files to auto-patch the way Playwright does — instead, you add two small lines yourself, by hand, to your project's existing Before and After hooks (the file where you already create your browser and page — usually called something like hooks.js or world.js).
Open the example file Mirror created (scripts/mirror-cucumber-hooks.example.js or .ts) — it shows you exactly what to copy. It looks like this:
const { mirrorBeforeScenario, mirrorAfterScenario } = require('./mirror-cucumber');
Before(async function () {
this.browser = await chromium.launch();
this.page = await this.browser.newPage();
// ✅ ADD THIS ONE LINE — right after you create the page
this.mirror = mirrorBeforeScenario(this.page, this.browser);
});
After(async function (scenario) {
// ✅ ADD THIS BLOCK — right before you close the browser
await mirrorAfterScenario(this.mirror, {
title: scenario.pickle.name,
project: (process.env.ENV || 'unknown').toUpperCase(),
status: scenario.result.status === Status.PASSED ? 'passed' : 'failed',
duration: scenario.result.duration ? scenario.result.duration.seconds * 1000 : 0,
error: scenario.result.message || undefined
});
await this.browser.close();
});
You're just adding these two pieces to hooks you already have — nothing about how you write .feature files or step definitions changes at all.
Step 6 — Run the comparison
Cucumber doesn't have Playwright's built-in "run against two projects" feature, so Mirror runs your whole suite twice instead — once with ENV=QA, once with ENV=PROD:
MIRROR: Run Full Comparison
(Behind the scenes this runs npm run mirror:run:qa then npm run mirror:run:prod, both added to your package.json automatically.)
Step 7 — Open the report
Same as Scenario 1 — open mirror-report/mirror-report.html in your browser.
Done! Same report, same evidence, just wired in through your hooks instead of an import line, since that's how Cucumber projects work.
🟨 Bonus: using Playwright AND Cucumber together in the same project
Some projects have both — some tests written directly in Playwright, others in Cucumber. Mirror detects this automatically and sets up both Scenario 1 and Scenario 2's files together. Just follow both sets of steps above; Mirror combines everything into one single report at the end, so you don't need two separate reports for two separate test styles.
Running one test, a few tests, or everything
No file changes needed for any of these — just different commands. (Works the same for plain Playwright and Playwright + Cucumber, since both go through mirror-playwright.config.ts.)
Run ONE test:
npm run mirror:run:single -- "exact test name"
Only that one test runs — against QA and PROD both — nothing else in your suite runs.
Run SEVERAL specific tests:
List the spec files you want, directly:
npx playwright test --config=mirror-playwright.config.ts tests/login.spec.ts tests/cart.spec.ts
Or, if the tests share part of their name, -g also matches multiple at once:
npm run mirror:run:single -- "Login|Cart"
(Runs every test whose name contains "Login" or "Cart".)
Run ALL tests:
npm run mirror:run
Running in a visible browser, or a specific browser
See the browser while it runs (headed mode):
npm run mirror:run:headed
Pick a specific browser (defaults to Chrome if you don't choose):
npm run mirror:run:chromium
npm run mirror:run:firefox
npm run mirror:run:webkit
npm run mirror:run:edge
Combine any of these freely — headed mode, a specific browser, and a single test together:
cross-env MIRROR_BROWSER=firefox npx playwright test --config=mirror-playwright.config.ts --headed -g "Login"
Then build and open the report, same as always:
npm run mirror:report
npm run mirror:open
or just npm run mirror:full to do both automatically after any of the runs above.
When your environments are one host with different pages, not different baseURLs
Most setups have QA and PROD on genuinely different hosts (qa.example.com vs example.com), so mirror-playwright.config.ts's per-project baseURL handles the switch automatically — your spec files never need to know which environment they're running against.
But some setups (common with static demo pages, or pre-launch environments served from one box) use the same host with different page paths instead — e.g. /QA-app.html vs /PROD-app.html. In that case, baseURL alone can't distinguish them, and your beforeEach genuinely does need a small amount of environment-aware logic. This is the one legitimate exception to "zero test changes" — here's the recommended pattern:
test.describe('QA Incident Ticket Application - Test Suite', () => {
test.beforeEach(async ({ page }) => {
const projectName = test.info().project.name || 'QA';
const baseURL = 'http://localhost:8000';
if (projectName === 'QA') {
await page.goto(`${baseURL}/QA-INCEDENT_TICKET_APP.html`);
} else {
await page.goto(`${baseURL}/PROD-INCEDENT_TICKET_APP.html`);
}
await page.waitForLoadState('networkidle');
await page.waitForTimeout(500);
});
// ... your test cases below, unchanged
});
test.info().project.name reads the current Playwright project's name — mirror-playwright.config.ts defines these as 'QA' and 'PROD', so this branch reliably tells you which environment the test is currently running against, without needing separate spec files or any change to how you write the tests themselves.
Commands
| Command |
What it does |
MIRROR: Setup Project |
Detects your framework combo and scaffolds .env, scripts/, and (for Playwright) mirror-playwright.config.ts |
MIRROR: Configure Environments |
Set QA/PROD URLs |
MIRROR: Patch Test Imports |
Rewrites Playwright spec imports to use mirror-fixtures.js; for Cucumber-only projects, points you to the hooks example instead |
MIRROR: Run Full Comparison |
Runs mirror:run + mirror:report and opens the result |
MIRROR: Regenerate Scripts |
Updates scripts/mirror-*.js to the latest bundled version |
How it works (briefly)
Mirror ships plain JavaScript files into your project's scripts/ folder — nothing hidden inside the extension, all inspectable and version-controllable:
mirror-fixtures.js (Playwright projects) — a thin wrapper around @playwright/test that captures a snapshot of the page (plus every iframe and window) whenever a test runs, and tracks which elements you actually interacted with.
mirror-cucumber.js (Cucumber projects) — the same capture logic, exposed as plain functions (mirrorBeforeScenario/mirrorAfterScenario) to call from your own Before/After hooks, since Cucumber has no fixture system to hook into automatically.
mirror-engine.js — reads results from both Playwright's test-results.json and Cucumber's mirror-raw/ folder, merging both into one comparison. Matches fields by what a user would actually recognize (label, placeholder, data-testid) rather than by id/name (usually the thing that drifted), and falls back to positional matching when even those changed.
mirror-reporter.js — renders the comparison as a dark-themed HTML report with screenshots and a searchable mismatch table.
Requirements
- Node.js 18+
@playwright/test and/or @cucumber/cucumber (Mirror will offer to install whichever's missing)
License
MIT