@tamsensedev/runner 0.0.0-stage → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +5 -0
- package/README.md +47 -3
- package/dist/cli.js +2914 -0
- package/dist/cli.js.map +1 -0
- package/dist/recorder.cjs +396 -0
- package/dist/recorder.cjs.map +1 -0
- package/library/.claude-plugin/plugin.json +5 -0
- package/library/THIRD_PARTY_NOTICES.md +123 -0
- package/library/agents/code-architect.md +67 -0
- package/library/agents/code-explorer.md +61 -0
- package/library/agents/code-reviewer.md +73 -0
- package/library/agents/code-simplifier.md +62 -0
- package/library/agents/design-critic.md +52 -0
- package/library/agents/second-opinion.md +64 -0
- package/library/agents/security-auditor.md +86 -0
- package/library/agents/silent-failure-hunter.md +70 -0
- package/library/agents/test-coverage-analyzer.md +78 -0
- package/library/agents/test-runner.md +48 -0
- package/library/agents/ui-tester.md +49 -0
- package/library/licenses/Apache-2.0.txt +202 -0
- package/library/skills/accessibility/SKILL.md +144 -0
- package/library/skills/accessibility/references/wcag-checks.md +168 -0
- package/library/skills/addressing-review-feedback/SKILL.md +109 -0
- package/library/skills/app-screens/SKILL.md +96 -0
- package/library/skills/app-screens/references/shell-kpis-charts.md +108 -0
- package/library/skills/app-screens/references/states-density-phone.md +54 -0
- package/library/skills/app-screens/references/tables-forms-settings.md +104 -0
- package/library/skills/avoiding-generic-ai-design/SKILL.md +92 -0
- package/library/skills/avoiding-generic-ai-design/references/tells.md +112 -0
- package/library/skills/clean-commits/SKILL.md +85 -0
- package/library/skills/design-review/SKILL.md +107 -0
- package/library/skills/design-review/references/critique-passes.md +101 -0
- package/library/skills/design-review/references/screenshot-procedure.md +121 -0
- package/library/skills/differential-security-review/LICENSE +427 -0
- package/library/skills/differential-security-review/SKILL.md +105 -0
- package/library/skills/differential-security-review/references/adversarial.md +115 -0
- package/library/skills/differential-security-review/references/methodology.md +145 -0
- package/library/skills/differential-security-review/references/patterns.md +108 -0
- package/library/skills/false-positive-check/LICENSE +427 -0
- package/library/skills/false-positive-check/SKILL.md +95 -0
- package/library/skills/false-positive-check/references/bug-class-verification.md +128 -0
- package/library/skills/false-positive-check/references/deep-verification.md +113 -0
- package/library/skills/false-positive-check/references/evidence-templates.md +93 -0
- package/library/skills/false-positive-check/references/false-positive-patterns.md +122 -0
- package/library/skills/false-positive-check/references/gate-reviews.md +36 -0
- package/library/skills/false-positive-check/references/standard-verification.md +80 -0
- package/library/skills/focused-implementation/SKILL.md +104 -0
- package/library/skills/implementation-plan/SKILL.md +66 -0
- package/library/skills/interface-design/SKILL.md +144 -0
- package/library/skills/interface-design/references/craft-defaults.md +130 -0
- package/library/skills/interface-design/references/new-direction.md +62 -0
- package/library/skills/interface-design/references/writing.md +52 -0
- package/library/skills/interface-polish/SKILL.md +103 -0
- package/library/skills/interface-polish/references/detail-rules.md +154 -0
- package/library/skills/interface-polish/references/worst-case-data.md +158 -0
- package/library/skills/motion-design/SKILL.md +106 -0
- package/library/skills/motion-design/references/recipes.md +243 -0
- package/library/skills/postgres-performance/SKILL.md +97 -0
- package/library/skills/postgres-performance/references/rules.md +265 -0
- package/library/skills/project-orientation/SKILL.md +77 -0
- package/library/skills/reviewing-changes/SKILL.md +183 -0
- package/library/skills/safe-database-changes/SKILL.md +111 -0
- package/library/skills/safe-database-changes/references/backfills.md +75 -0
- package/library/skills/safe-database-changes/references/migration-tools.md +69 -0
- package/library/skills/safe-database-changes/references/postgres-ddl.md +199 -0
- package/library/skills/security-review/SKILL.md +132 -0
- package/library/skills/security-review/references/checks-by-area.md +107 -0
- package/library/skills/security-review/references/do-not-report.md +86 -0
- package/library/skills/spec-writing/SKILL.md +144 -0
- package/library/skills/systematic-debugging/SKILL.md +130 -0
- package/library/skills/systematic-debugging/references/flaky-tests.md +51 -0
- package/library/skills/systematic-debugging/references/root-cause-tracing.md +55 -0
- package/library/skills/task-interview/SKILL.md +84 -0
- package/library/skills/test-driven-development/SKILL.md +116 -0
- package/library/skills/test-driven-development/references/writing-good-tests.md +85 -0
- package/library/skills/ui-testing/SKILL.md +102 -0
- package/library/skills/ui-testing/references/headless-check.md +280 -0
- package/library/skills/ui-testing/references/playwright-patterns.md +134 -0
- package/library/skills/ui-testing/references/recording.md +57 -0
- package/library/skills/verification-before-completion/SKILL.md +113 -0
- package/library/skills/web-interface-guidelines/SKILL.md +202 -0
- package/package.json +46 -6
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Writing good tests
|
|
2
|
+
|
|
3
|
+
Read this when writing or changing a test, adding a mock, or adding a test helper.
|
|
4
|
+
|
|
5
|
+
A test exists to catch a specific break. Two rules carry most of the weight:
|
|
6
|
+
|
|
7
|
+
1. Every test names the break it catches.
|
|
8
|
+
2. Every test exercises the real thing.
|
|
9
|
+
|
|
10
|
+
## Rule 1: name the break
|
|
11
|
+
|
|
12
|
+
Before writing the body, answer: which production change would make this test fail, and is that change a bug or a deliberate decision? A good test catches a wrong branch, a missing side effect, a wrong argument, a boundary case or a broken contract.
|
|
13
|
+
|
|
14
|
+
**Derive expected values independently.** Use literals and hand-checked fixtures. An expected value computed by the code under test (or its helpers) passes whatever the code does.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
// Bad: the same builder produces both sides, so it is always equal
|
|
18
|
+
const expected = buildSearchQuery({ tag: 'urgent' })
|
|
19
|
+
expect(buildSearchQuery({ tag: 'urgent' })).toBe(expected)
|
|
20
|
+
|
|
21
|
+
// Bad: recomputes the answer the way the code does
|
|
22
|
+
expect(add(a, b)).toBe(a + b)
|
|
23
|
+
|
|
24
|
+
// Good: written out by hand
|
|
25
|
+
expect(buildSearchQuery({ tag: 'urgent' })).toBe('tag:"urgent"')
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Table-driven tests with literal expected values are a good shape for many cases.
|
|
29
|
+
|
|
30
|
+
**No change detectors.** A test that only fails on intentional decisions (a constant's value, exact message wording, private structure) fires on every redesign and sleeps through real bugs. Test the behaviour that depends on the decision: not `expect(MAX_RETRIES).toBe(5)` but "a failing call is retried 5 times and a sixth attempt never happens".
|
|
31
|
+
|
|
32
|
+
**Behaviour, not source text.** Asserting that a file contains a certain line proves only that the file is the file. Run the script, config or template against a controlled input and check the output, side effect or exit code.
|
|
33
|
+
|
|
34
|
+
**Your code, not the framework.** Test the contract your code makes at its boundary: the route you register, the query you build, the payload you send. Do not test that the router calls a registered handler; that is the framework's job. Constructors, getters and simple pass-throughs earn a test only when they validate, normalise, default, derive or cause a side effect.
|
|
35
|
+
|
|
36
|
+
**Through the public surface.** Test via the function, endpoint, component or command that callers use. A test that breaks when you refactor internals without changing behaviour is coupled to implementation. Avoid checking results through a side channel (querying the database directly) when the public interface can show them.
|
|
37
|
+
|
|
38
|
+
## Rule 2: exercise the real thing
|
|
39
|
+
|
|
40
|
+
**Never assert on a mock.** An assertion that a mock was rendered or exists passes when the mock is there and says nothing about the component.
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
// Bad: tests the mock
|
|
44
|
+
expect(screen.getByTestId('sidebar-mock')).toBeInTheDocument()
|
|
45
|
+
|
|
46
|
+
// Good: tests the real behaviour
|
|
47
|
+
expect(screen.getByRole('navigation')).toBeInTheDocument()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Mock at the right level.** Before replacing a method, learn all its side effects. Mock the slow or external part below it (network, paid API, clock, email) and keep what the test depends on real. A mock that swallows a write that the code later reads makes the test pass while the real thing breaks.
|
|
51
|
+
|
|
52
|
+
**Make doubles specific.** When arguments, call counts or order are part of the contract, assert them. Give success, error and malformed responses their own fixtures, so the wrong branch cannot satisfy the test.
|
|
53
|
+
|
|
54
|
+
**Mirror real data completely.** Fake responses should have the full structure of the real one, not only the fields this test reads. Partial fakes hide failures in code that reads the other fields.
|
|
55
|
+
|
|
56
|
+
**Production code carries production methods only.** Cleanup or reset methods that only tests call belong in test utilities, not on the production class.
|
|
57
|
+
|
|
58
|
+
**Prefer real components over elaborate mocks.** When mock setup is longer than the test logic, or the test breaks whenever the mock changes, switch to a test with real collaborators (an in-memory database, the real module, the project's existing test server).
|
|
59
|
+
|
|
60
|
+
## The mutation check
|
|
61
|
+
|
|
62
|
+
Before finishing, imagine small breaks in the production code. At least one test should fail for each realistic one:
|
|
63
|
+
|
|
64
|
+
- A wrong constant or argument.
|
|
65
|
+
- A wrong branch taken.
|
|
66
|
+
- A missing state change or side effect.
|
|
67
|
+
- An empty or default return value.
|
|
68
|
+
- Missing validation for zero, empty, null, unauthorised or malformed input.
|
|
69
|
+
|
|
70
|
+
If nothing fails for a realistic mutation, that behaviour is unprotected. For an important change you can make the mutation for real: invert one condition, run the tests, confirm a failure, restore the line, and confirm with `git diff` that it is restored.
|
|
71
|
+
|
|
72
|
+
## Warning signs
|
|
73
|
+
|
|
74
|
+
- Setup and assertion share the same object, so they are equal by construction.
|
|
75
|
+
- The test can only fail by crashing or by a missing selector.
|
|
76
|
+
- The test fails on every intentional change but never on accidental breakage.
|
|
77
|
+
- Expected values are hidden behind loops, builders or helpers.
|
|
78
|
+
- The test greps source text.
|
|
79
|
+
- The test exists for coverage and checks no outcome.
|
|
80
|
+
- An assertion checks a `*-mock` test id, or fails if you remove the mock.
|
|
81
|
+
- A method is called only from test files.
|
|
82
|
+
- Mock setup is more than half the test, or you cannot say why the mock is needed.
|
|
83
|
+
- Writing all the tests for a feature before any implementation: tests for imagined behaviour test the shape of things, not what users see. Go one test at a time.
|
|
84
|
+
|
|
85
|
+
Sources: obra/superpowers (MIT, commit bb92a77) writing-good-tests and mattpocock/skills (MIT, commit 5beb35b) tdd. Changed: shortened and merged.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ui-testing
|
|
3
|
+
description: Use when a change affects what users see or click in a web app and should be checked in a real browser, when writing or fixing Playwright or Cypress tests, or when a test step must verify UI behaviour. Runs the project's E2E suite or a headless check from a temp folder and reports screenshots and errors as evidence.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# UI testing in a headless browser
|
|
7
|
+
|
|
8
|
+
Reading the code tells you what should happen; a browser shows what does. This skill gets the app running, drives the changed flows headless, and turns what it saw into evidence for the report. Nobody watches the run, so every step must finish on its own and leave the machine as it was.
|
|
9
|
+
|
|
10
|
+
The project's own setup wins: its E2E framework, config, fixtures, page objects, test data, auth helpers and scripts. Use them before anything from here.
|
|
11
|
+
|
|
12
|
+
## Hard rules
|
|
13
|
+
|
|
14
|
+
1. **Headless only.** Never open a window: no `--headed`, `--ui`, `--debug`, `show-report`, `show-trace`, `cypress open`, `PWDEBUG`. Playwright's HTML reporter opens a browser on failure when not in CI, so run with `--reporter=line` and `PW_TEST_HTML_REPORT_OPEN=never`. Set `BROWSER=none` when starting dev servers so they do not open one either.
|
|
15
|
+
2. **No new dependencies.** Do not add packages to the repository and do not download browsers (`playwright install`, `npx some-tool@latest`) just to check. Use what is installed. If the project has no Playwright but the `TAMSENSE_PLAYWRIGHT` environment variable is set, the TAMsense runner has already installed `playwright-core` and a headless browser outside the project, and the headless check script uses them. If nothing is, check from the code and say the browser check could not run.
|
|
16
|
+
3. **The project's own start command, on a free port.** Start the app with its dev or preview script, never a hand-made server that skips its config. Prefer the headless check script, which starts the server and stops its whole process tree for you. Stop it at the end, including child processes, and confirm the port is free again.
|
|
17
|
+
8. **Stop only what you started, by process id.** This is someone's own computer and their other programs may be running. Never kill processes by name or pattern: no `taskkill /IM`, `pkill`, `killall`, `Stop-Process -Name`. If you start a server yourself, record its PID and stop that process tree (`taskkill /PID <pid> /T /F` on Windows, `kill -- -<pgid>` elsewhere). If a process you started cannot be stopped that way, say so in the report and leave the rest alone.
|
|
18
|
+
4. **Outputs outside the repository.** Screenshots, traces, reports, scratch scripts and auth state go to a new folder in the OS temp directory. `git status` must show nothing new from you when you finish.
|
|
19
|
+
5. **Never fake it.** If the app cannot start (missing environment variables, secrets, a database or service you may not use), report exactly what is missing. Do not mock the whole backend and call the result a pass, and do not invent screenshots or results.
|
|
20
|
+
6. **Do not write to shared systems.** If the local app is configured against a shared or production backend or database, look but do not submit forms or trigger writes.
|
|
21
|
+
7. **Never hide failures.** Do not skip, `fixme`, delete or loosen a failing test to get a green run. In a check step, a failing test is a finding.
|
|
22
|
+
9. **Every browser is recorded for the task.** In a TAMsense run each Playwright context becomes a replay people watch on the task, and the tested site's TAMsense tracker is blocked. Open contexts through the recorder with a title and a purpose, mark the meaningful steps, and add a video for canvas, maps, video players or iframes from another site: see [references/recording.md](references/recording.md).
|
|
23
|
+
|
|
24
|
+
## Process
|
|
25
|
+
|
|
26
|
+
### 1. Decide what to check
|
|
27
|
+
|
|
28
|
+
From the task, the spec's acceptance criteria and the diff (`git diff origin/<base>...HEAD --stat`, then the changed components, routes and styles), write a short list of scenarios before touching the browser:
|
|
29
|
+
|
|
30
|
+
- The main flow the task changed, end to end.
|
|
31
|
+
- Its states: loading, empty, error, long or unusual content, many items, disabled controls.
|
|
32
|
+
- A narrow screen (390 px wide) if layout or styles changed, plus a desktop width.
|
|
33
|
+
- Keyboard use for any new or changed control (Tab to it, activate with Enter or Space, Escape closes overlays). The `accessibility` skill has the full checks.
|
|
34
|
+
|
|
35
|
+
### 2. Find how this project runs and tests its UI
|
|
36
|
+
|
|
37
|
+
- E2E setup: `playwright.config.*`, `cypress.config.*`, `e2e/` or `tests/` folders, scripts like `test:e2e` in `package.json`, and the CI workflow steps that run them (they show the exact command, env vars and services).
|
|
38
|
+
- The app: dev and preview scripts, required env vars (`.env.example`, config validation, README), services it needs (database, cache, other apps in a monorepo). Check whether required variables are set by name; never print their values.
|
|
39
|
+
- Auth: seeded test users, a test login endpoint, existing storage-state setup.
|
|
40
|
+
- The installed Playwright or Cypress version.
|
|
41
|
+
|
|
42
|
+
### 3a. The project has an E2E suite
|
|
43
|
+
|
|
44
|
+
Run the specs related to the change, then the whole suite if it is fast enough.
|
|
45
|
+
|
|
46
|
+
- Playwright: `PW_TEST_HTML_REPORT_OPEN=never npx playwright test <specs> --reporter=line --output <tmp>/test-results` (use the package manager the project uses). Its browsers are recorded by themselves under the step's name; a long suite fills the run's limit of 20 replays, so run the related specs rather than everything when you can. Its `webServer` config usually starts the app: check the port and `reuseExistingServer`, and make sure no stale server of yours is still running.
|
|
47
|
+
- Cypress: `npx cypress run --spec <specs> --config screenshotsFolder=<tmp>/screens,videosFolder=<tmp>/videos,video=false` (headless by default).
|
|
48
|
+
- If a run fails, rerun the failing spec alone to separate a real failure from a flaky one, and read the error and screenshot. Decide whether the failure comes from the change (the diff touches that flow) or existed before; say which and why.
|
|
49
|
+
|
|
50
|
+
In a code step, add or update tests for the changed flow following the suite's structure; see [references/playwright-patterns.md](references/playwright-patterns.md) for locators, waiting, auth, mocking and the plan, write, repair loop. In a review or test step, do not edit the repository's tests; check extra scenarios with 3b instead.
|
|
51
|
+
|
|
52
|
+
### 3b. No suite, or scenarios the suite does not cover
|
|
53
|
+
|
|
54
|
+
Use the headless check in [references/headless-check.md](references/headless-check.md): a Node.js script you write to a temp folder that resolves Playwright from the project, starts the app with the project's command on a free port, visits the pages at 1280 and 390 px, runs your steps with marks, records each page for the task, saves screenshots and a JSON summary (console errors, failed requests, horizontal overflow, optional axe scan), and stops the app.
|
|
55
|
+
|
|
56
|
+
If Playwright is not installed in the project and no other headless browser tool is available, do not install one. Check the change from the code and say so.
|
|
57
|
+
|
|
58
|
+
### 4. Look at the evidence
|
|
59
|
+
|
|
60
|
+
- Open every screenshot with the Read tool and compare it with what the scenario expects. Describe what you see in plain words: what is on the screen, what is wrong, where.
|
|
61
|
+
- Console errors, page errors and failed requests from the changed code are findings; third-party noise is not, but name it so the reader knows you saw it.
|
|
62
|
+
- Sideways scrolling at 390 px, cut-off text, overlapping elements, missing states and dead buttons are findings.
|
|
63
|
+
- For visual quality and consistency with the design system, apply the `design-review` skill; for the rules list, `web-interface-guidelines`; for accessibility, `accessibility`.
|
|
64
|
+
|
|
65
|
+
### 5. Clean up
|
|
66
|
+
|
|
67
|
+
Stop the servers you started, by their process id only (hard rule 8), check the port no longer answers, remove containers and temp folders you created unless the task asks to keep artifacts, and run `git status` in every repository to confirm you left nothing behind.
|
|
68
|
+
|
|
69
|
+
## Report
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
UI check: <passed | failed | could not run>
|
|
73
|
+
How: <suite command or headless check>, browser <chromium | chrome | msedge>, widths <1280, 390>
|
|
74
|
+
Recordings: <titles of the replays and videos attached to the task>
|
|
75
|
+
Scenarios:
|
|
76
|
+
- <scenario>: pass | fail | not checked (why). Evidence: <what the screenshot or log shows>
|
|
77
|
+
Findings:
|
|
78
|
+
- <file:line if traced> <what is wrong, how to reproduce, what was expected>
|
|
79
|
+
Not checked: <and why: missing env vars X, Y; no browser available; flow needs an external provider>
|
|
80
|
+
Assumptions: <test account used, data you created locally, version>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
In a test step, base the verdict on the scenarios: a broken acceptance criterion, an error from the changed code or a layout broken on narrow screens fails it. If the browser check could not run, do not report it as passed; judge from the other evidence (tests, code) and state clearly that the UI was not seen. If the change itself stops the app from starting or building, that is a failure.
|
|
84
|
+
|
|
85
|
+
## Common mistakes
|
|
86
|
+
|
|
87
|
+
- Assuming the page works because the build passed.
|
|
88
|
+
- Fixed sleeps instead of waiting for the element that proves readiness.
|
|
89
|
+
- Letting the HTML reporter or a dev server open a browser window.
|
|
90
|
+
- Leaving the dev server running, or `test-results/` and screenshots inside the repository.
|
|
91
|
+
- Testing only the happy path at desktop width.
|
|
92
|
+
- Pointing the check at staging or production because the local app did not start.
|
|
93
|
+
- Listing screenshot paths without saying what they show.
|
|
94
|
+
- Generic names for screenshots and recordings ("screenshot1", "test run") instead of what they show and why.
|
|
95
|
+
|
|
96
|
+
## Done when
|
|
97
|
+
|
|
98
|
+
- Every scenario from step 1 is checked or listed as not checked with a reason.
|
|
99
|
+
- Evidence (described screenshots, error lines, failing assertions) backs each finding.
|
|
100
|
+
- No window was opened, no dependency added, nothing left running or written into the repository.
|
|
101
|
+
|
|
102
|
+
Sources: the start, wait, act, stop approach of anthropics/skills webapp-testing (Apache-2.0, commit dbd4588), testdino-hq/playwright-skill (MIT, commit 178d1cb), ideas from the Playwright test agents (Apache-2.0). Changed: rewritten for Node and the project's own setup, unattended rules added.
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# Headless check without an E2E suite
|
|
2
|
+
|
|
3
|
+
Read this when the project has no Playwright or Cypress tests you can run for the change, but you still need to see the pages: screenshots, console errors, failed requests, horizontal overflow on narrow screens, and optionally an axe scan.
|
|
4
|
+
|
|
5
|
+
The script below starts the app with the project's own command on a free port, waits for it, opens each page headless at a desktop and a phone width, runs optional steps you write, saves screenshots and a JSON summary to a temp folder, and stops the app and all its child processes at the end. In a TAMsense run each page and width is also recorded as a replay for the task, named from `CONFIG.title` and `CONFIG.purpose` (see [recording.md](recording.md)).
|
|
6
|
+
|
|
7
|
+
## What it needs
|
|
8
|
+
|
|
9
|
+
- Node.js 20 or newer.
|
|
10
|
+
- The `playwright`, `@playwright/test` or `playwright-core` package already installed in the project (it is resolved from the project folder you give it; in a monorepo, use the package folder of the web app). If the project has none and the `TAMSENSE_PLAYWRIGHT` environment variable is set, the script loads the `playwright-core` that the TAMsense runner installed in that folder, with the headless browser from the `TAMSENSE_PLAYWRIGHT_BROWSERS` folder. Nothing is installed.
|
|
11
|
+
- A browser that is already on the machine: Playwright's downloaded Chromium (the project's or the runner's), or the system Chrome or Edge as a fallback. If none works, the script stops and says so. Do not run `playwright install` or add packages to get one; report that the check could not run.
|
|
12
|
+
- `axe-core` is optional: if the project already has it, each page also gets an accessibility scan.
|
|
13
|
+
|
|
14
|
+
## How to use it
|
|
15
|
+
|
|
16
|
+
1. Make a new folder in the OS temp directory (`node -p "require('os').tmpdir()"`), for example `<tmp>/ui-check-<random>`. Never inside the repository.
|
|
17
|
+
2. Write the script there as `ui-check.mjs` and edit only the `CONFIG` block and the `steps` function. Set `title` and `purpose` to what this check is and why ("Feature testing, attempt 2", "Checking the empty orders state after the fix"), and `video: true` when the pages have a canvas, a map, a video player or iframes from another site.
|
|
18
|
+
3. Run `node <tmp>/ui-check-<random>/ui-check.mjs` with a timeout of a few minutes. It prints the summary; screenshots are in `CONFIG.outDir`.
|
|
19
|
+
4. Open the screenshots with the Read tool and look at them. Describe in the report what each one shows; the paths alone mean nothing to the reader.
|
|
20
|
+
5. Delete the folder when the report is written, unless the task asks you to keep artifacts.
|
|
21
|
+
|
|
22
|
+
`startCommand` is the project's own dev or preview command with `{port}` where the port goes, for example `pnpm dev --port {port} --strictPort` (Vite), `npm run dev -- -p {port}` (Next.js), `pnpm nuxt dev --port {port}`, `bin/rails server -p {port}`, `python manage.py runserver 127.0.0.1:{port}`. The script also sets `PORT`. Leave it empty and set `baseUrl` if the app is already running or is static files you serve another way.
|
|
23
|
+
|
|
24
|
+
On Windows the command runs in `cmd.exe`, which may not see the same PATH as your shell. If the summary says the command is not recognized, use absolute paths (for example the full path to `node.exe` and the package manager's script) rather than changing anything in the project.
|
|
25
|
+
|
|
26
|
+
## The script
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
import { createRequire } from 'node:module'
|
|
30
|
+
import { spawn, execFileSync } from 'node:child_process'
|
|
31
|
+
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
32
|
+
import { join } from 'node:path'
|
|
33
|
+
import net from 'node:net'
|
|
34
|
+
|
|
35
|
+
const CONFIG = {
|
|
36
|
+
projectDir: '/absolute/path/to/web/app',
|
|
37
|
+
outDir: '/absolute/path/to/tmp/ui-check-1234/out',
|
|
38
|
+
startCommand: 'pnpm dev --port {port} --strictPort',
|
|
39
|
+
baseUrl: '',
|
|
40
|
+
paths: ['/'],
|
|
41
|
+
widths: [1280, 390],
|
|
42
|
+
startTimeoutMs: 120_000,
|
|
43
|
+
navigationTimeoutMs: 30_000,
|
|
44
|
+
title: 'Feature testing',
|
|
45
|
+
purpose: 'Checking the changed screens at desktop and phone width',
|
|
46
|
+
video: false,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function steps(page, { path, width, mark }) {
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const MAX_ITEMS = 20
|
|
53
|
+
const MAX_TITLE = 60
|
|
54
|
+
const serverOutput = []
|
|
55
|
+
const recorder = process.env.TAMSENSE_RECORDER ? createRequire(import.meta.url)(process.env.TAMSENSE_RECORDER) : null
|
|
56
|
+
|
|
57
|
+
function keepTail(chunk) {
|
|
58
|
+
serverOutput.push(...String(chunk).split('\n').filter(Boolean))
|
|
59
|
+
serverOutput.splice(0, Math.max(0, serverOutput.length - 40))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function requireFrom(dir, names) {
|
|
63
|
+
const require = createRequire(join(dir, 'package.json'))
|
|
64
|
+
for (const name of names) {
|
|
65
|
+
try {
|
|
66
|
+
return require(name)
|
|
67
|
+
} catch {}
|
|
68
|
+
}
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function loadFromProject(names) {
|
|
73
|
+
const found = requireFrom(CONFIG.projectDir, names)
|
|
74
|
+
const runnerCopy = process.env.TAMSENSE_PLAYWRIGHT
|
|
75
|
+
if (found || !names.includes('playwright-core') || !runnerCopy) return found
|
|
76
|
+
if (process.env.TAMSENSE_PLAYWRIGHT_BROWSERS) process.env.PLAYWRIGHT_BROWSERS_PATH = process.env.TAMSENSE_PLAYWRIGHT_BROWSERS
|
|
77
|
+
return requireFrom(runnerCopy, ['playwright-core'])
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function freePort() {
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
const server = net.createServer()
|
|
83
|
+
server.on('error', reject)
|
|
84
|
+
server.listen(0, '127.0.0.1', () => {
|
|
85
|
+
const { port } = server.address()
|
|
86
|
+
server.close(() => resolve(port))
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function startServer(command, port) {
|
|
92
|
+
const child = spawn(command.replaceAll('{port}', String(port)), {
|
|
93
|
+
cwd: CONFIG.projectDir,
|
|
94
|
+
shell: true,
|
|
95
|
+
env: { ...process.env, PORT: String(port), BROWSER: 'none' },
|
|
96
|
+
detached: process.platform !== 'win32',
|
|
97
|
+
windowsHide: true,
|
|
98
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
99
|
+
})
|
|
100
|
+
child.stdout.on('data', keepTail)
|
|
101
|
+
child.stderr.on('data', keepTail)
|
|
102
|
+
return child
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function stopServer(child) {
|
|
106
|
+
if (!child || child.exitCode !== null) return 'not running'
|
|
107
|
+
try {
|
|
108
|
+
if (process.platform === 'win32') {
|
|
109
|
+
const taskkill = join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'taskkill.exe')
|
|
110
|
+
execFileSync(taskkill, ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore' })
|
|
111
|
+
} else {
|
|
112
|
+
process.kill(-child.pid, 'SIGKILL')
|
|
113
|
+
}
|
|
114
|
+
return 'stopped'
|
|
115
|
+
} catch (error) {
|
|
116
|
+
child.kill('SIGKILL')
|
|
117
|
+
return `could not stop the process tree of pid ${child.pid}: ${error.message}`
|
|
118
|
+
} finally {
|
|
119
|
+
child.stdout.destroy()
|
|
120
|
+
child.stderr.destroy()
|
|
121
|
+
child.unref()
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function waitForServer(url, child) {
|
|
126
|
+
const deadline = Date.now() + CONFIG.startTimeoutMs
|
|
127
|
+
while (Date.now() < deadline) {
|
|
128
|
+
if (child && child.exitCode !== null) throw new Error(`start command exited with code ${child.exitCode}`)
|
|
129
|
+
try {
|
|
130
|
+
await fetch(url, { redirect: 'manual', signal: AbortSignal.timeout(5_000) })
|
|
131
|
+
return
|
|
132
|
+
} catch {}
|
|
133
|
+
await new Promise(resolve => setTimeout(resolve, 1_000))
|
|
134
|
+
}
|
|
135
|
+
throw new Error(`no response from ${url} within ${CONFIG.startTimeoutMs / 1000}s`)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function launchBrowser(chromium) {
|
|
139
|
+
const failures = []
|
|
140
|
+
for (const channel of [undefined, 'chromium', 'chrome', 'msedge']) {
|
|
141
|
+
try {
|
|
142
|
+
return await chromium.launch({ headless: true, channel })
|
|
143
|
+
} catch (error) {
|
|
144
|
+
failures.push(`${channel ?? 'playwright chromium'}: ${String(error.message).split('\n')[0]}`)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
throw new Error(`no browser available without downloading one: ${failures.join(' | ')}`)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function runAxe(page, axeSource) {
|
|
151
|
+
try {
|
|
152
|
+
await page.addScriptTag({ content: axeSource })
|
|
153
|
+
return await page.evaluate(async () => {
|
|
154
|
+
const result = await window.axe.run(document, {
|
|
155
|
+
runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa'] },
|
|
156
|
+
})
|
|
157
|
+
return result.violations.map(violation => ({
|
|
158
|
+
rule: violation.id,
|
|
159
|
+
impact: violation.impact,
|
|
160
|
+
help: violation.help,
|
|
161
|
+
count: violation.nodes.length,
|
|
162
|
+
targets: violation.nodes.slice(0, 5).map(node => JSON.stringify(node.target)),
|
|
163
|
+
}))
|
|
164
|
+
})
|
|
165
|
+
} catch (error) {
|
|
166
|
+
return { error: String(error.message).split('\n')[0] }
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function openContext(browser, path, width) {
|
|
171
|
+
const options = { viewport: { width, height: 900 } }
|
|
172
|
+
if (!recorder) return browser.newContext(options)
|
|
173
|
+
const where = `, ${path} at ${width}px`
|
|
174
|
+
const title = `${CONFIG.title.slice(0, MAX_TITLE - where.length)}${where}`
|
|
175
|
+
return recorder.newRecordedContext(browser, { title, purpose: CONFIG.purpose, video: CONFIG.video, context: options })
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function checkPage(browser, url, path, width, axeSource) {
|
|
179
|
+
const context = await openContext(browser, path, width)
|
|
180
|
+
const page = await context.newPage()
|
|
181
|
+
const mark = label => recorder?.mark(page, label)
|
|
182
|
+
const result = { path, width, consoleErrors: [], pageErrors: [], failedRequests: [], badResponses: [] }
|
|
183
|
+
const add = (list, item) => { if (list.length < MAX_ITEMS) list.push(item) }
|
|
184
|
+
page.on('console', message => { if (message.type() === 'error') add(result.consoleErrors, message.text()) })
|
|
185
|
+
page.on('pageerror', error => add(result.pageErrors, String(error.message)))
|
|
186
|
+
page.on('requestfailed', request => add(result.failedRequests, `${request.method()} ${request.url()} ${request.failure()?.errorText ?? ''}`))
|
|
187
|
+
page.on('response', response => { if (response.status() >= 400) add(result.badResponses, `${response.status()} ${response.url()}`) })
|
|
188
|
+
try {
|
|
189
|
+
const response = await page.goto(url, { waitUntil: 'load', timeout: CONFIG.navigationTimeoutMs })
|
|
190
|
+
result.status = response?.status() ?? null
|
|
191
|
+
await page.waitForLoadState('networkidle', { timeout: 5_000 }).catch(() => {})
|
|
192
|
+
await mark(`Opened ${path}`)
|
|
193
|
+
result.steps = await steps(page, { path, width, mark })
|
|
194
|
+
result.title = await page.title()
|
|
195
|
+
result.finalUrl = page.url()
|
|
196
|
+
result.horizontalOverflowPx = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth)
|
|
197
|
+
const name = `${path.replace(/[^a-z0-9]+/gi, '_').replace(/^_|_$/g, '') || 'root'}-${width}.png`
|
|
198
|
+
result.screenshot = join(CONFIG.outDir, name)
|
|
199
|
+
await page.screenshot({ path: result.screenshot, fullPage: true })
|
|
200
|
+
if (axeSource) result.axe = await runAxe(page, axeSource)
|
|
201
|
+
} catch (error) {
|
|
202
|
+
result.error = String(error.message).split('\n')[0]
|
|
203
|
+
} finally {
|
|
204
|
+
result.recording = recorder ? await recorder.closeRecorded(context) : await context.close()
|
|
205
|
+
}
|
|
206
|
+
return result
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function main() {
|
|
210
|
+
mkdirSync(CONFIG.outDir, { recursive: true })
|
|
211
|
+
const summary = { pages: [], serverOutputTail: serverOutput }
|
|
212
|
+
const playwright = loadFromProject(['playwright', '@playwright/test', 'playwright-core'])
|
|
213
|
+
if (!playwright?.chromium) throw new Error(`Playwright is not installed in ${CONFIG.projectDir} and TAMSENSE_PLAYWRIGHT is not set`)
|
|
214
|
+
const axeSource = loadFromProject(['axe-core'])?.source ?? null
|
|
215
|
+
summary.axe = axeSource ? 'axe-core found in the project' : 'axe-core not installed, scan skipped'
|
|
216
|
+
|
|
217
|
+
let server = null
|
|
218
|
+
let browser = null
|
|
219
|
+
try {
|
|
220
|
+
let baseUrl = CONFIG.baseUrl
|
|
221
|
+
if (CONFIG.startCommand) {
|
|
222
|
+
const port = await freePort()
|
|
223
|
+
server = startServer(CONFIG.startCommand, port)
|
|
224
|
+
baseUrl = `http://localhost:${port}`
|
|
225
|
+
}
|
|
226
|
+
if (!baseUrl) throw new Error('set CONFIG.startCommand or CONFIG.baseUrl')
|
|
227
|
+
summary.baseUrl = baseUrl
|
|
228
|
+
await waitForServer(baseUrl, server)
|
|
229
|
+
browser = await launchBrowser(playwright.chromium)
|
|
230
|
+
for (const path of CONFIG.paths) {
|
|
231
|
+
for (const width of CONFIG.widths) {
|
|
232
|
+
summary.pages.push(await checkPage(browser, new URL(path, baseUrl).href, path, width, axeSource))
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
} catch (error) {
|
|
236
|
+
summary.setupError = String(error.message)
|
|
237
|
+
process.exitCode = 2
|
|
238
|
+
} finally {
|
|
239
|
+
await browser?.close().catch(() => {})
|
|
240
|
+
if (server) summary.server = stopServer(server)
|
|
241
|
+
writeFileSync(join(CONFIG.outDir, 'summary.json'), JSON.stringify(summary, null, 2))
|
|
242
|
+
console.log(JSON.stringify(summary, null, 2))
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
main().catch(error => {
|
|
247
|
+
console.error(error.message)
|
|
248
|
+
process.exitCode = 2
|
|
249
|
+
})
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Writing steps
|
|
253
|
+
|
|
254
|
+
`steps` runs on every page and width before the screenshot. Use it to reach the state the task changed: open the dialog, fill and submit the form, switch the tab. Branch on `path` when pages need different steps. Call `await mark('...')` after each meaningful step: the marks show on the replay's timeline for the people on the task. Whatever it returns is saved as `steps` in that page's summary (for example the focus order from a keyboard pass, see the `accessibility` skill). Use the same rules as in tests: role and label locators, web-first waits, no fixed sleeps (see [playwright-patterns.md](playwright-patterns.md)).
|
|
255
|
+
|
|
256
|
+
```js
|
|
257
|
+
async function steps(page, { path, mark }) {
|
|
258
|
+
if (path !== '/settings') return
|
|
259
|
+
await page.getByRole('button', { name: 'Edit profile' }).click()
|
|
260
|
+
await mark('Opened the profile form')
|
|
261
|
+
await page.getByLabel('Display name').fill('A very long display name that should wrap or truncate')
|
|
262
|
+
await page.getByRole('button', { name: 'Save' }).click()
|
|
263
|
+
await page.getByRole('status').waitFor()
|
|
264
|
+
await mark('Saved a long display name')
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Steps that write data only touch the local app and whatever local database it uses. If the dev server is configured to talk to a shared or production backend or database, do not submit forms or trigger writes; check that first (see the `safe-database-changes` skill for how to read where the app connects).
|
|
269
|
+
|
|
270
|
+
## Reading the summary
|
|
271
|
+
|
|
272
|
+
- `server`: should be `stopped`. Anything else names a process you must stop yourself before finishing.
|
|
273
|
+
- `recording`: where each page's replay (and video) is. They are attached to the task when the run ends; `attach_replay` and `attach_video` attach one sooner.
|
|
274
|
+
- `setupError`: the app or the browser did not start. Report it with the last lines of `serverOutputTail` (strip anything that looks like a secret). Do not retry with a different command unless the first one was clearly wrong, and never invent results.
|
|
275
|
+
- `status` 4xx or 5xx, `pageErrors`, `consoleErrors`: real findings when they come from the changed code; noise from browser extensions does not exist here, but third-party scripts and dev-server warnings do. Say which is which.
|
|
276
|
+
- `badResponses` and `failedRequests`: API calls the page needed that failed. A 401 or a redirect to a login page means the page needs a signed-in user (see the auth section of [playwright-patterns.md](playwright-patterns.md)).
|
|
277
|
+
- `horizontalOverflowPx` above 0 at 390 px: the page scrolls sideways on phones.
|
|
278
|
+
- `axe`: list each rule with impact and a target; the `accessibility` skill explains how to judge them.
|
|
279
|
+
|
|
280
|
+
Written for this library. The start, wait, act, stop approach follows anthropics/skills `webapp-testing` (Apache-2.0, commit dbd4588f9e1033efb41dad4bef2f7947c8993d44, Copyright 2026 Anthropic, PBC); the script is new Node.js code, not a translation of its Python helper.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Playwright patterns: locators, waiting, flakes, auth, mocking
|
|
2
|
+
|
|
3
|
+
Read this when you write, fix or review browser tests or the `steps` of a headless check. In a TAMsense run every browser context, also those of the project's tests, is recorded for the task; scripts of your own name their recordings as [recording.md](recording.md) shows. Examples are Playwright Test in TypeScript; translate to the project's language and runner. If the project uses Cypress, the same ideas apply (see the last section). Check the installed version (`npx playwright --version`) before using an API you are not sure exists in it.
|
|
4
|
+
|
|
5
|
+
## Locators
|
|
6
|
+
|
|
7
|
+
Use the first one that works, in this order:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
page.getByRole('button', { name: 'Save changes' }) // 1. role + accessible name
|
|
11
|
+
page.getByLabel('Email address') // 2. form fields by label
|
|
12
|
+
page.getByText('Welcome back') // 3. non-interactive text
|
|
13
|
+
page.getByPlaceholder('Search') // 4. only if there is no label
|
|
14
|
+
page.getByAltText('Company logo') // 5. images
|
|
15
|
+
page.getByTestId('checkout-summary') // 6. when nothing semantic is stable
|
|
16
|
+
page.locator('.legacy-widget button') // 7. CSS or XPath, last resort
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- Role locators survive class renames and component swaps, and they fail when the control is not accessible, which is a finding in itself.
|
|
20
|
+
- `exact: true` stops "Log" from matching "Log out".
|
|
21
|
+
- Scope instead of indexing: `page.getByRole('dialog', { name: 'Delete project' }).getByRole('button', { name: 'Delete' })` beats `.nth(2)`.
|
|
22
|
+
- Rows and cards: `page.getByRole('row').filter({ hasText: 'Invoice 42' }).getByRole('button', { name: 'Open' })`.
|
|
23
|
+
- Use the project's existing `data-testid` convention if it has one; do not add test ids to product code during a check step.
|
|
24
|
+
- Strict mode errors ("resolved to 2 elements") mean the locator is ambiguous: scope it, do not add `.first()` to silence it.
|
|
25
|
+
|
|
26
|
+
## Waiting and assertions
|
|
27
|
+
|
|
28
|
+
Actions (`click`, `fill`, `check`) already wait for the element to be attached, visible, stable and enabled. Assertions on locators retry until they pass or time out. Use them; do not sleep.
|
|
29
|
+
|
|
30
|
+
| Need | Use |
|
|
31
|
+
|---|---|
|
|
32
|
+
| Element shown or hidden | `await expect(locator).toBeVisible()` / `.toBeHidden()` |
|
|
33
|
+
| Text | `toHaveText` (full) or `toContainText` |
|
|
34
|
+
| Count | `toHaveCount(n)` |
|
|
35
|
+
| Input value, attribute, focus | `toHaveValue`, `toHaveAttribute`, `toBeFocused` |
|
|
36
|
+
| Navigation | `await expect(page).toHaveURL(/\/dashboard/)` |
|
|
37
|
+
| A request or response | register `page.waitForResponse(...)` **before** the action that triggers it |
|
|
38
|
+
| A non-DOM condition | `await expect.poll(() => readStatus()).toBe('done')` |
|
|
39
|
+
|
|
40
|
+
Wrong and right:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
await page.waitForTimeout(2000); // wrong: too slow or too short
|
|
44
|
+
expect(await page.getByRole('alert').isVisible()).toBe(true); // wrong: checks once, no retry
|
|
45
|
+
expect(await row.textContent()).toBe('Paid'); // wrong: no retry
|
|
46
|
+
|
|
47
|
+
await expect(page.getByRole('alert')).toBeVisible(); // right
|
|
48
|
+
await expect(row).toHaveText('Paid'); // right
|
|
49
|
+
|
|
50
|
+
const saved = page.waitForResponse(r => r.url().includes('/api/profile') && r.ok());
|
|
51
|
+
await page.getByRole('button', { name: 'Save' }).click();
|
|
52
|
+
await saved; // right: registered before the click
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- `networkidle` is unreliable with websockets, polling or analytics. Wait for the element or URL that proves the page is ready.
|
|
56
|
+
- Every `expect` and every action is awaited. A missing `await` makes a test pass before it checked anything.
|
|
57
|
+
- Do not wrap assertions in try/catch; do not raise timeouts above about 30 s to make something pass.
|
|
58
|
+
|
|
59
|
+
## Flaky tests
|
|
60
|
+
|
|
61
|
+
A flaky test has a cause; retries and longer timeouts only hide it. Classify first:
|
|
62
|
+
|
|
63
|
+
| Symptom | Likely cause | How to confirm |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| Fails sometimes even alone | Timing: missing await, sleep, race between action and assertion | `npx playwright test <file> --repeat-each=10` (raise the count for rare races) |
|
|
66
|
+
| Passes alone, fails in the suite | Isolation: shared state, shared test data, order dependence | `--grep "<name>" --workers=1` passes, full run fails |
|
|
67
|
+
| Fails only in CI | Environment: viewport, fonts, slower machine, missing service | Compare traces and screenshots from both |
|
|
68
|
+
| Random crashes, "Target closed" | Infrastructure: memory, browser crash | Errors mention the browser, not the app |
|
|
69
|
+
|
|
70
|
+
Fixes: replace sleeps with web-first assertions; register waits before actions; give each test its own data (unique names, per-test setup); avoid module-level mutable variables; set viewport and `reducedMotion: 'reduce'` explicitly when animation or layout matters. For evidence, run with `--trace retain-on-failure --reporter=line --output <tmp>/test-results` and read the error message and the error-context and screenshot files in that output folder. Do not open `show-trace`, `show-report`, `--ui` or `--headed`: they open windows, and nobody is there to look.
|
|
71
|
+
|
|
72
|
+
## Authentication
|
|
73
|
+
|
|
74
|
+
- Use the project's existing auth setup first: a setup project, a global setup that saves `storageState`, a fixture, a test-only login endpoint.
|
|
75
|
+
- Without one, log in once per run through the UI or the API with a **test account the project documents for local development** (seed data, README, fixtures), save `storageState` to the temp folder, and reuse it.
|
|
76
|
+
- Never use real user credentials, never read secrets the task did not give you, and never write auth state files inside the repository.
|
|
77
|
+
- Testing the login page itself: start from an empty state (`storageState: { cookies: [], origins: [] }`).
|
|
78
|
+
- OAuth or SSO with an external provider: do not hit the real provider; check the pages around it or report that the flow could not be exercised.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// global setup or the first step of a headless check
|
|
82
|
+
await page.goto('/login');
|
|
83
|
+
await page.getByLabel('Email').fill(process.env.E2E_USER ?? 'demo@example.test');
|
|
84
|
+
await page.getByLabel('Password').fill(process.env.E2E_PASSWORD ?? 'from-the-seed-file');
|
|
85
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
86
|
+
await expect(page).toHaveURL(/dashboard/);
|
|
87
|
+
await page.context().storageState({ path: `${tmpDir}/auth.json` });
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Network mocking
|
|
91
|
+
|
|
92
|
+
Mock what you do not own (payment providers, email, maps, analytics) and error states you cannot trigger otherwise. Do not mock the app's own pages, scripts or styles, and prefer the real backend for the happy path.
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
await page.route('**/api/orders', route =>
|
|
96
|
+
route.fulfill({ status: 500, json: { error: 'Internal error' } })); // error state
|
|
97
|
+
await page.route('**/api/orders', route => route.fulfill({ json: [] })); // empty state
|
|
98
|
+
await page.route('**/api/orders', async route => { // slow: loading state
|
|
99
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
100
|
+
await route.continue();
|
|
101
|
+
});
|
|
102
|
+
await page.route(/analytics|segment|hotjar/, route => route.abort()); // third-party noise
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
- Register routes before `page.goto`.
|
|
106
|
+
- Every handler calls exactly one of `fulfill`, `continue` or `abort`, otherwise the request hangs.
|
|
107
|
+
- Keep globs narrow (`**/api/orders`, not `**/*`).
|
|
108
|
+
- Loading, empty and error states are where UI changes break most often; mocking is the cheapest way to see them.
|
|
109
|
+
|
|
110
|
+
## Writing new tests (code step only)
|
|
111
|
+
|
|
112
|
+
When the task changes a user flow and the project has an E2E suite:
|
|
113
|
+
|
|
114
|
+
1. **Plan:** list the scenarios from the acceptance criteria: the happy path, the main error state, the empty state, the narrow screen if layout changed. Note what each one must assert.
|
|
115
|
+
2. **Generate:** follow the suite's existing structure, fixtures, page objects and naming. One behaviour per test; several related `expect`s are fine.
|
|
116
|
+
3. **Run** the new tests several times (`--repeat-each=3` at least) and the related existing tests once.
|
|
117
|
+
4. **Repair** failures by finding the cause. If a test fails because the app is wrong, fix the app (in a code step) or report the bug (in a check step).
|
|
118
|
+
|
|
119
|
+
Never make a run green by marking tests `test.skip`, `test.fixme`, `test.fail`, `.only`-ing others, deleting assertions, loosening them (`toHaveText` to `toContainText` without reason), or adding sleeps. A healed test that hides a regression is worse than a red one.
|
|
120
|
+
|
|
121
|
+
## Cypress equivalents
|
|
122
|
+
|
|
123
|
+
| Playwright | Cypress |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `getByRole`, `getByLabel` | `cy.findByRole`, `cy.findByLabelText` if Testing Library is installed; otherwise `cy.contains` and the project's `data-cy` / `data-testid` |
|
|
126
|
+
| web-first `expect(locator)` | `cy.get(...).should(...)` (retries) |
|
|
127
|
+
| `waitForResponse` | `cy.intercept(...).as('x')` then `cy.wait('@x')` |
|
|
128
|
+
| `page.route(...).fulfill` | `cy.intercept(method, url, { statusCode, body })` |
|
|
129
|
+
| `storageState` | `cy.session()` |
|
|
130
|
+
| `npx playwright test <file>` | `npx cypress run --spec <file>` (headless by default; never `cypress open`) |
|
|
131
|
+
|
|
132
|
+
The same bans apply: no `cy.wait(<milliseconds>)`, no skipping failing tests.
|
|
133
|
+
|
|
134
|
+
Adapted from testdino-hq/playwright-skill guides `core/locators.md`, `core/assertions-and-waiting.md`, `core/flaky-tests.md`, `core/authentication.md`, `core/network-mocking.md` and `core/test-agents.md` (MIT, commit 178d1cb537604279d7b54500c4823a6db42b9de6, Copyright (c) 2026 TestDino). Changed: condensed several thousand lines into one page, removed version-specific APIs and vendor mentions, added unattended rules (test accounts, temp folders, no skips), added the Cypress table. The plan, generate, repair loop restates the idea of the Playwright test agents (microsoft/playwright, Apache-2.0) in our own words; no text copied.
|