A Playwright TypeScript test suite built around the Page Object Model, a small local sample app with seed data to test against, and a workflow for turning Jira tickets into reviewed test plans and publishing results to TestRail.
- Project structure
- The sample app
- Getting started
- Running tests
- Playwright UI mode
- Page Object Model
- Agent-driven test generation
- Jira integration
- TestRail integration
- CI/CD
playwright-agent/
├── .claude/agents/ # Playwright Planner/Generator/Healer agent definitions
├── .github/workflows/
│ ├── playwright.yml # runs the suite, reports results to TestRail on push
│ └── jira-plan.yml # PR trigger that fetches a Jira description into a plan
├── .mcp.json # registers the Playwright MCP server for Claude Code
├── app/ # local sample app under test (static HTML/CSS/JS)
├── data/ # sample data (users, products, coupons) — seeds the app AND the tests
├── pages/ # Page Object Model classes
├── components/ # shared widgets composed into page objects (e.g. NavBar)
├── fixtures/ # Playwright fixtures that inject page objects + sample test data
├── scripts/
│ ├── jira/ # Jira → test plan fetcher
│ └── testrail/ # test results → TestRail publisher
├── tests/
│ ├── seed.spec.ts # authenticated start state, copied by generated tests
│ ├── plans/ # Markdown test plans (human/Planner/Jira-sourced input for the Generator)
│ └── e2e/ # the actual .spec.ts test files
├── server.js # zero-dependency static file server for app/
├── playwright.config.ts
└── .env.example # required env vars for the Jira/TestRail scripts
There's no external site to point this suite at, so app/ is a small, dependency-free static app (login → products → cart → checkout → confirmation) served locally by server.js. It exists purely as something realistic to test — swap it for your real app by changing baseURL and the webServer command in playwright.config.ts, and update the page objects' selectors to match.
Sample data lives in data/*.json and is used by both the app (fetched over HTTP at runtime) and the tests (imported directly in fixtures/test-data.ts), so there's a single source of truth:
| File | Contents |
|---|---|
data/users.json |
One valid user, one locked-out user |
data/products.json |
Four sample products |
data/coupons.json |
Two coupon codes (SAVE10, SAVE20) |
The checkout form also recognizes a hardcoded decline test card (4000000000000002, the standard Stripe test-decline number) to exercise the payment-failure path.
npm install
npx playwright install --with-deps # first time only, installs browser binaries
cp .env.example .env # only needed for the Jira/TestRail scripts — see belownpm test # headless, all projects — starts app/ automatically via webServer
npm run test:headed # same, with a visible browser
npm run report # opens the last HTML reportYou don't need to start the app yourself — playwright.config.ts has a webServer entry that runs node server.js and waits for http://127.0.0.1:4173 before tests start (and reuses an already-running server locally). npm run serve starts it standalone if you want to poke at the app manually.
npm run test:uiThis opens Playwright's interactive UI mode — the fastest way to work on this suite day to day:
- Test list on the left, grouped by file/describe block; click any test to run just that one.
- Watch mode: leave it open while editing a
.spec.tsor a page object — it reruns automatically on save. - Timeline scrubber: step through every action in a run (click, fill, navigation) and see the DOM snapshot at that instant, alongside network and console logs.
- Pick locator: hover the page preview to get a suggested Playwright locator for anything on screen — useful when extending a page object.
- Trace viewer built in: failed tests show their full trace (screenshots, network, actions) without needing
npx playwright show-traceseparately.
Run a single file or grep a title from the CLI instead if you don't need the UI:
npx playwright test tests/e2e/checkout.spec.ts
npx playwright test -g "coupon"pages/*.page.ts— one class per page (LoginPage,ProductsPage,CartPage,CheckoutPage), extendingpages/base.page.ts. Locators usegetByRole/getByLabel/getByTestIdonly — no CSS or XPath — so they stay resilient and stay legible to the agents that generate/heal tests.components/nav-bar.component.ts— the nav bar is identical across pages, so it's a composed component (page.nav) rather than duplicated locators in every page object.- Page objects hold no assertions — they expose locators and actions; the
expect()calls live in the test files. fixtures/page-fixtures.tsinjectsloginPage/productsPage/cartPage/checkoutPageintotest()via fixtures, so specs never donew LoginPage(page)by hand.fixtures/auth-fixtures.tsoverrides thepagefixture to sign in as the sample valid user first — use this (import { test, expect } from '../../fixtures/auth-fixtures') for any test that needs to start already logged in, instead of repeating the login flow.fixtures/test-data.tsre-exportsdata/*.jsonas typed sample data (validUser,sampleProducts,validCoupon,declinedCardNumber, …) for tests to consume directly.
.claude/agents/ and .mcp.json were generated by npx playwright init-agents --loop=claude and wire up Playwright's official Planner / Generator / Healer agents through the Playwright MCP server:
- Planner (
@playwright-test-planner) explores a flow and writes a Markdown plan tospecs/(ortests/plans/— see the Jira integration below, which writes there too). - Generator (
@playwright-test-generator) turns an approved plan into a real.spec.tsfile using the existing page objects and fixtures. - Healer (
@playwright-test-healer) inspects a failing test against the live app and patches selectors/assertions, or marks itfixmeif the app itself is broken.
Always review Planner output before generating, and review Generator/Healer output like a PR before merging
npm run jira:fetch -- PROJ-123 pulls an issue's summary and description straight from Jira and writes it as Planner input.
Setup — add to .env (or CI secrets):
JIRA_BASE_URL=https://your-team.atlassian.net
JIRA_EMAIL=you@example.com
JIRA_API_TOKEN= # Atlassian API token, not your password
Generate an API token at id.atlassian.com → Security → API tokens.
What it does:
- Calls
GET /rest/api/3/issue/{key}with Basic auth (email:token). - Converts the description from Jira's Atlassian Document Format (rich-text JSON) to Markdown — headings, bold/italic/code, links, bullet/ordered lists, code blocks, and blockquotes are all handled (
scripts/jira/adf-to-markdown.js). - Writes
tests/plans/PROJ-123.md, formatted the same way astests/plans/checkout-flow.md, with a## Descriptionsection and an empty## Scenariossection left for a human (or the Planner agent) to fill in.
This is intentionally a fetch, not a full plan — the description alone isn't enough to write good test scenarios from, so the Scenarios section is left for review, matching the "always review Planner output" rule above.
Automatic PR trigger — .github/workflows/jira-plan.yml runs on every PR open/edit/sync:
- Extracts a Jira key (pattern
[A-Z][A-Z0-9]+-[0-9]+) from the PR title, falling back to the branch name. - If found, runs the same fetch script and uploads the result as a workflow artifact (
jira-plan-PROJ-123). - Posts (or updates, on subsequent pushes) a single PR comment containing the fetched plan.
It deliberately does not commit the plan file back to the branch — a human downloads it from the artifact/comment and commits it under tests/plans/ once they've reviewed it, keeping the same review gate as manually-run Planner output.
Requires repo secrets JIRA_BASE_URL, JIRA_EMAIL, JIRA_API_TOKEN to be set for the workflow to actually fetch anything; without them the step still runs but the Jira request will fail (the workflow doesn't currently no-op gracefully on missing secrets the way the TestRail script does).
npm run report:testrail reads Playwright's JSON report and publishes results to a new TestRail run.
Setup — add to .env (or CI secrets):
TESTRAIL_BASE_URL=https://your-team.testrail.io
TESTRAIL_USERNAME=you@example.com
TESTRAIL_API_KEY= # generate under My Settings → API Keys
TESTRAIL_PROJECT_ID=
TESTRAIL_SUITE_ID= # only needed if the project has multiple suites
Case-ID convention: tag each test title with its TestRail case ID, prefixed C:
test('C1001 user can sign in with valid credentials', async ({ loginPage, page }) => {
...
});All 12 reportable tests in tests/e2e/ are already tagged this way (C1001–C1012) as a working example — these are placeholder IDs; replace them with real case IDs from your TestRail project before relying on the report. tests/seed.spec.ts is deliberately left untagged since it's setup, not a reportable case — untagged tests are silently skipped by the reporter, so tag only the tests that map to real TestRail cases.
What it does (scripts/testrail/report-results.js):
- Reads
test-results/results.json(written by thejsonreporter configured inplaywright.config.ts— this only exists if you ranplaywright testwith the default reporters, not--reporter=list). - Walks the suite tree, extracts the
C<id>from each test's title, and maps its last attempt's status to a TestRail status:passed → 1,failed/timedOut/interrupted → 5. Tests without aC<id>tag, or with no mapped status (e.g. skipped), are left out of the report. - Creates a new run via
add_runscoped to just the case IDs seen in this execution (include_all: false), then bulk-submits results viaadd_results_for_cases, including duration and any error message as the result comment.
If any of the four required env vars are missing, the script logs a message and exits 0 instead of failing — so it's safe to leave in a pipeline before secrets are configured.
CI wiring — .github/workflows/playwright.yml runs this after the test step, but only on push (not on every PR), so TestRail doesn't fill up with in-progress PR runs. It reads the same four (plus optional fifth) vars from repository secrets.
.github/workflows/playwright.yml— installs dependencies and browsers, runsplaywright test, reports to TestRail onpush, and uploads the HTML report as a build artifact regardless of outcome..github/workflows/jira-plan.yml— see Jira integration above.