A working end-to-end test suite you can run today, plus a repo layout that Claude Code can extend on its own. It tests the public demo store saucedemo.com — login, cart, and checkout flows — using TypeScript and the Page Object Model.
- Node.js 18+ and npm
- Claude Code installed (
npm install -g @anthropic-ai/claude-code), if you want Claude to drive this repo from your terminal
playwright-e2e/
├── CLAUDE.md # context Claude Code reads automatically
├── playwright.config.ts # browsers, retries, reporters, base URL
├── eslint.config.mjs # flat config: typescript-eslint + Playwright rules
├── global-setup.ts # logs in once, saves storageState for reuse
├── pages/ # Page Objects (locators + actions)
│ ├── LoginPage.ts
│ ├── InventoryPage.ts
│ ├── CartPage.ts
│ └── CheckoutPage.ts
├── fixtures/
│ ├── pageFixtures.ts # injects Page Objects into `test`
│ └── testData.ts # shared users/credentials
├── tests/
│ ├── login.spec.ts
│ ├── cart.spec.ts
│ ├── checkout.spec.ts
│ └── accessibility.spec.ts # axe-core scans on key pages
├── allure/
│ └── config.ts # Allure environment info + defect categories
├── scripts/
│ ├── report-flaky.js # flags retried tests in the CI job summary
│ └── strip-language-label.js # drops the redundant "language" chip
└── .github/workflows/playwright.yml
If you're starting a brand-new project rather than using this sample, this is the sequence:
mkdir my-e2e-suite && cd my-e2e-suite
npm init -y
npm init playwright@latest # scaffolds config, an example test, and CIThe npm init playwright@latest wizard asks: TypeScript or JavaScript,
test folder name, whether to add a GitHub Actions workflow, and whether to
install browsers now. This sample already answers all of those, so you can
skip the wizard and just run the install step below.
cd playwright-e2e
npm install
npx playwright install --with-deps # downloads Chromium/Firefox/WebKit
npm test # run everything, headless, all 4 projects
npm run test:ui # interactive UI mode — best for authoring/debugging
npm run test:headed # watch the browser while it runs
npx playwright show-reportA passing run produces playwright-report/ (HTML report with traces,
screenshots, and video on failure) and test-results/.
Every run also writes raw results to allure-results/ via the
allure-playwright reporter. To view them as an Allure report (requires a
Java runtime locally; GitHub Actions runners already
have one):
npm run report:allure:generate # builds allure-report/ from allure-results/
npm run report:allure:open # serves it in a browserEvery merge to main also publishes the merged Allure report to
https://gunashekarryml.github.io/playwright-e2e/ automatically — see
.github/workflows/playwright.yml.
npm run report:allure:generate does three things in sequence (see the
script chain in package.json):
scripts/strip-language-label.jsremoves thelanguage: javascriptlabelallure-playwrightstamps on every result (it has no config option to disable it) — one less redundant chip on every test page.allure generatebuilds the report, reading:allure/config.ts→environment.properties(Environment tab) andcategories.json(Categories tab: Product defects / Test-framework defects / Environment issues / Ignored), wired in via theallure-playwrightreporter'senvironmentInfo/categoriesoptions inplaywright.config.ts.executor.json, written by a CI-only step inplaywright.yml(build number, link back to the GitHub Actions run, link to the published Pages report) — powers the Executions widget.
- Smoke vs. full regression — pull requests run only tests tagged
@smoke(fast feedback); pushes tomain, the nightly cron, and manual runs (suite: full) run everything. Tag a test with a second argument —test('...', { tag: '@smoke' }, async (...) => { ... })— to add it to the fast gate. Run the same subset locally withnpm run test:smoke. - Authenticated session reuse —
global-setup.tslogs in once asstandard_userand savesstorageState;cart.spec.tsandcheckout.spec.tsreuse it (test.use({ storageState: ... })) instead of repeating the login flow in every test.login.spec.tsitself stays unauthenticated since it's testing the login flow. - Accessibility checks —
accessibility.spec.tsruns an axe-core scan against login/inventory/cart/checkout and fails on anycriticalorseriousviolation (SauceDemo's few pre-existingmoderateissues are left as-is rather than chased). - Flaky-test visibility —
scripts/report-flaky.jsreads the JSON reporter output after each run and lists any test that only passed after a retry in that job's summary, so retries don't silently hide flakiness. - Lint & type-check gate — a standalone
lintjob runstsc --noEmitandnpm run lint(ESLint, flat config ineslint.config.mjs) on every push/PR, independently of the browser test matrix. It combinestypescript-eslint's type-aware rules witheslint-plugin-playwright(catches missingawaits, focused/skipped tests left in by accident,page.waitForTimeoutusage, etc.). Run it locally withnpm run lint(ornpm run lint:fixfor auto-fixable issues).
CLAUDE.md in this repo tells Claude the folder conventions (Page Object
pattern, where fixtures live, how specs should import test/expect) so
generated code matches the existing style instead of reinventing it. Point
Claude Code at this folder and it can:
- Generate new tests from a description — "Add a spec that verifies the cart persists after a page reload" — Claude reads the existing Page Objects and fixtures first, then writes a spec that fits the pattern.
- Turn a recorded flow into a test — run
npm run codegento record clicks in a real browser, paste the generated code to Claude, and ask it to refactor that into a Page Object + spec. - Debug a failing/flaky test — point Claude at the trace file Playwright
writes on failure (
npx playwright show-trace test-results/.../trace.zip) or just ask it to runnpx playwright test --debugand reason about the output. - Keep the suite in sync with UI changes — paste a diff or describe a UI change, and ask Claude to update the affected Page Object's locators.
- Extend CI — ask Claude to add sharding, a nightly cron run, or Slack
notifications to
.github/workflows/playwright.yml.
Example prompts are listed at the bottom of CLAUDE.md.
npm run mcp:playwright starts the Playwright MCP server
(@playwright/mcp, launched on demand via npx — no install step). It's a
plain Model Context Protocol server with
no dependency on any specific AI tool or vendor — it lets any MCP client
drive a real, visible browser directly (navigate, click, fill forms, read
the accessibility tree) instead of writing a throwaway script just to see
what a page looks like. It's independent of this repo's own
playwright.config.ts/test suite; think of it as a live "hands on the
keyboard" tool for exploring SauceDemo, debugging a selector, or sanity
checking a flow before turning it into a Page Object + spec, complementing
npm run codegen.
To use it, point your MCP client of choice at the same command
(npx -y @playwright/mcp@latest, or npm run mcp:playwright from this
directory) in whatever config format that client expects, for example:
- Claude Code:
claude mcp add playwright -- npx -y @playwright/mcp@latest(or add it to a project's own.mcp.jsonif you want it checked in and shared with a team) - Cursor: add the same command under
mcpServersin.cursor/mcp.json - VS Code (Copilot): add it under
serversin.vscode/mcp.json
It can also run as a standalone network server instead of being spawned
per-client — npx @playwright/mcp@latest --port 8931 starts it listening
over SSE/HTTP, so multiple MCP clients (or machines) can connect to the
same instance; add --shared-browser-context to have them share one
browser instead of each getting their own.
- Change
use.baseURLinplaywright.config.ts(or setBASE_URLenv var). - Replace
pages/*.tswith Page Objects for your app's screens. - Replace
fixtures/testData.tswith your own test accounts/data — never commit real production credentials; use a.envfile (already gitignored) andprocess.envfor anything sensitive. - Update selectors to prefer accessible queries (
getByRole,getByLabel) or stabledata-testidattributes your app exposes.
SauceDemo intentionally ships a few broken users for testing error paths:
locked_out_user (blocked), problem_user (broken images/UI), and
performance_glitch_user (slow responses) — useful for practicing
resilient locators and negative-path tests.
The same GitHub Pages site serves two reports:
/— Allure report/dashboard/— QA dashboard (dashboard/index.html)
On every non-PR run on main, the deploy-pages job fetches the previous dashboard/data/runs.json
from the live site, appends this run (last 60 kept, built by scripts/build-dashboard-run.js and
scripts/update-dashboard-history.js) and deploys both together. No extra repo or token needed.
Preview locally: cd dashboard && python3 -m http.server, then open http://localhost:8000.
Results are posted to Slack after every run, and /qa-run opens a Slack form (environment, suite, tag) that starts a build. Setup (incl. where each secret goes): slack/README.md.
- Use workflow from (GitHub's built-in dropdown): the branch to run.
- environment: QA / DEV / STAGING / PROD. The URL for each comes from a repo variable (Settings → Secrets and variables → Actions → Variables):
QA_URL,DEV_URL,STAGING_URL,PROD_URL. If a variable is missing it falls back toBASE_URL, then https://www.saucedemo.com. - suite: Smoke or Full Regression (or give a
greptag, which wins). - Only QA/default runs on
mainupdate the published Allure report and dashboard history.
Six tests titled [Demo] … and tagged @demo-failure fail on purpose, to show how failures are analysed on the dashboard:
| Test | Fails because | Root cause shown |
|---|---|---|
| Checkout: tax is 10% | The shop charges 8% | Product behaves differently than expected |
| Checkout: promo code field | The shop has no promo field | Screen element missing or changed |
| Login: agreed error wording | The site words the error differently | Product behaves differently than expected |
| Dynamic Catalog: 50 ms load budget | The page takes longer | Page too slow or not responding |
| Menu: status page reachable | Nothing listens on that address | Environment problem |
| Cart: item count (intermittent) | Fails on the first attempt, passes on retry | Shown as an unstable (flaky) check |
They run with Full Regression and with the feature suites that contain them (Smoke is unaffected), so Full Regression shows a Conditional verdict.
To switch them off: set the GitHub repo variable DEMO_FAILURES to off (Settings → Secrets and variables → Actions → Variables), or run locally with DEMO_FAILURES=off npx playwright test. They are then excluded entirely.
To demonstrate a No-Go verdict, change one demo test's tag from @high to @critical.
The dashboard and the Slack message judge the latest full regression with the same rules. Severity decides the release; raw failure counts do not.
| Check | Green | Amber (Conditional) | Red (No-Go) |
|---|---|---|---|
| Critical journeys (smoke) | all pass | no smoke tests ran | any failure |
| Critical & high-severity cases | all pass | any high-severity failure (pass rate ≥ 90%) | any critical failure, or high-severity pass rate < 90% |
| Overall pass rate | ≥ 95% | 85–95% | < 85% |
| Medium & low issues | medium ≥ 90% and low ≥ 80% | below those limits | never red (known issues) |
| Test stability (flaky) | ≤ 5% | 5–10% | > 10% |
| Every feature | lowest ≥ 85% | 70–85% | < 70% |
| Every browser | lowest ≥ 90% | 80–90% | < 80% |
| Quality trend | drop ≤ 5 pts | drop > 5 pts | never red |
Verdict: any red check = No-Go; no red but any amber = Conditional (release only with owner sign-off on the known issues); all green = Go.
Edit the numbers in POLICY at the top of the script in dashboard/index.html and in scripts/notify-slack.js (the Slack copy has no trend check).
A smoke or feature run can never approve a release, so the dashboard never shows GO for one:
- No full regression in the current view → the verdict is SMOKE PASSED · full regression not run (blue), or NO-GO if the smoke tests fail. The big number is the smoke pass rate, and the regression tile reads "not run yet".
- Full regression exists but is older than the latest run → the verdict comes from that full run, and the header says which one ("Verdict from full regression #59 · 20h ago").
- Full regression older than 48 h → an extra amber check, "Full regression is recent" (
POLICY.staleHours). - The Slack message uses its earlier format: it judges every run on its own numbers, so a smoke-only run can still read GO there. (The dashboard is the place to read the release verdict.)
| What | Where | Rule |
|---|---|---|
| Allure report at the site root | / |
Always the latest full regression. A smoke or feature run does not replace it. |
| Allure report of the newest run | /latest-run/ |
Whatever ran last (smoke, feature or full). |
| Dashboard | /dashboard/ |
Reads data/runs.json. |
| Run history | dashboard/data/runs.json on Pages |
Newest 120 runs; check-level detail for the newest 3 runs per environment. |
| History backup | branch dashboard-data (file runs.json, one commit per run) |
Used automatically if the live site cannot be read. |
If the live history cannot be read and there is no backup, the publish job stops instead of starting from an empty history (an empty one is only created on the very first run). The last full-regression report is kept in the GitHub Actions cache; if it has expired, the root falls back to the newest report.