@ralphralphai/playwright 2.8.0 → 2.9.0

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/README.md CHANGED
@@ -3,19 +3,26 @@
3
3
  Turn your Playwright tests into a map of your app for
4
4
  [Ralph](https://ralphralph.ai).
5
5
 
6
- Tag a test with `@ralph` and every page it visits is recorded: a full-page
7
- screenshot, the URL, and the position of every element with a `data-track-id`.
6
+ Every page an `@ralph`-tagged test visits becomes a **node** in a graph: a
7
+ full-page screenshot, the URL, and the position of every element with a
8
+ `data-track-id`. The actions the test takes between two pages (clicks, fills,
9
+ navigation) become the **edge** between them. Ralph matches your production
10
+ tracker events against this graph.
11
+
8
12
  Untagged tests are untouched, and recording is off unless you turn it on.
9
13
 
10
14
  ## Install
11
15
 
12
- Requires Node.js 20.19+ and Playwright Test 1.63+ (major version 1).
16
+ Requires Node.js 20.19+ and Playwright Test 1.63+.
13
17
 
14
18
  ```sh
15
19
  pnpm add -D @ralphralphai/playwright @playwright/test
16
20
  pnpm exec playwright install chromium
17
21
  ```
18
22
 
23
+ **IMPORTANT** It is currently only tested on chromium. The Firefox support will
24
+ be added soon.
25
+
19
26
  ## Quick start
20
27
 
21
28
  **1. Add a `ralph.jsonc`** next to `playwright.config.ts`:
@@ -30,7 +37,15 @@ pnpm exec playwright install chromium
30
37
  See [`@ralphralphai/config`](https://www.npmjs.com/package/@ralphralphai/config)
31
38
  for all fields.
32
39
 
33
- **2. Import `test` from this package and tag tests with `@ralph`:**
40
+ **2. Tag your elements** with `data-track-id`, using the same ids as your
41
+ [`@ralphralphai/tracker`](https://www.npmjs.com/package/@ralphralphai/tracker)
42
+ setup:
43
+
44
+ ```html
45
+ <button data-track-id="add-to-cart">Add to cart</button>
46
+ ```
47
+
48
+ **3. Import `test` from this package and tag tests with `@ralph`:**
34
49
 
35
50
  ```ts
36
51
  import { test, expect } from "@ralphralphai/playwright";
@@ -42,7 +57,7 @@ test("checkout", { tag: "@ralph" }, async ({ page }) => {
42
57
  });
43
58
  ```
44
59
 
45
- **3. Add the Ralph reporter** in `playwright.config.ts`:
60
+ **4. Add the Ralph reporter** in `playwright.config.ts`:
46
61
 
47
62
  ```ts
48
63
  export default defineConfig({
@@ -50,25 +65,92 @@ export default defineConfig({
50
65
  });
51
66
  ```
52
67
 
53
- **4. Run with your upload key** (see [Uploading to Ralph](#uploading-to-ralph)):
68
+ **5. Run with an upload key.** Create one in the project dashboard, then:
54
69
 
55
70
  ```sh
56
71
  RALPH_UPLOAD_KEY=… RALPH_APP_ID=… pnpm exec playwright test
57
72
  ```
58
73
 
59
- ## Tag your elements
74
+ This runs in `upload` mode, the default whenever `RALPH_UPLOAD_KEY` is set. The
75
+ reporter sends the recorded run to Ralph, which builds the graph from it. An
76
+ upload is a preview: it isn't tied to any release, so production tracker
77
+ events aren't matched against it. Use it to check the graph while you write
78
+ tests, then [deploy](#deploying-a-release) once it looks right.
60
79
 
61
- Add `data-track-id` to elements, using the same ids as your
62
- [`@ralphralphai/tracker`](https://www.npmjs.com/package/@ralphralphai/tracker)
63
- setup:
80
+ The reporter prints a link before the upload starts. Open it to watch the run
81
+ upload, get processed, and then browse the graph:
64
82
 
65
- ```html
66
- <button data-track-id="add-to-cart">Add to cart</button>
67
83
  ```
84
+ Ralph: uploading the run. Follow the upload and processing at https://dash.ralphralph.ai/uploads/0f6c6a2e-…
85
+ Ralph: uploaded the run. Ralph is processing it at https://dash.ralphralph.ai/uploads/0f6c6a2e-…
86
+ ```
87
+
88
+ `RALPH_UPLOAD_KEY` must be an environment variable. `RALPH_APP_ID` can be
89
+ set as the reporter's `appId` option.
90
+
91
+ ## Deploying a release
92
+
93
+ Once the previewed graph looks right, upload it as the graph of the build you
94
+ deploy. Ralph then matches that build's tracker events to it. Set
95
+ `RALPH_MODE=deploy` and `RALPH_BUILD_ID`, usually the commit SHA:
96
+
97
+ ```sh
98
+ RALPH_MODE=deploy RALPH_BUILD_ID="$COMMIT_SHA" \
99
+ RALPH_UPLOAD_KEY=… RALPH_APP_ID=… pnpm exec playwright test
100
+ ```
101
+
102
+ If the build goes live before the test run finishes, register it first so the
103
+ tracker picks up its config right away:
104
+
105
+ ```sh
106
+ curl --fail-with-body -X POST https://api.ralphralph.ai/api/upload/deployment/register \
107
+ -H "Authorization: Bearer $RALPH_UPLOAD_KEY" -H 'content-type: application/json' \
108
+ -d "{\"externalAppId\":\"$RALPH_APP_ID\",\"buildId\":\"$COMMIT_SHA\",
109
+ \"reserveArtifact\":false,\"config\":$(cat ralph.json)}"
110
+ ```
111
+
112
+ `ralph.json` is your `ralph.jsonc` as plain JSON, without comments or trailing
113
+ commas. The test run that follows registers the same build again and uploads
114
+ into it.
115
+
116
+ ## Diffing deployments
117
+
118
+ Along with the screenshot, each node records an ARIA snapshot of the page: the
119
+ accessibility tree, each element's box, its distinctive styles (fonts, colors,
120
+ spacing, borders), and a hash of each image. Ralph diffs a node's snapshot
121
+ against the same node in the previous deployment to show what changed.
122
+
123
+ Hidden elements, elements clipped by an ancestor's overflow, and tiny iframes
124
+ such as tracking pixels are left out, so they never show up as changes.
125
+
126
+ Two tools keep content that changes on its own from showing up as a change:
68
127
 
69
- ## Recording pages
128
+ - **`data-ralph-dynamic`** marks a subtree whose content the server generates
129
+ on every request, such as a blog listing or a price quote. The diff compares
130
+ only its structure and styles, not its text, links, or images:
70
131
 
71
- ### Automatically
132
+ ```html
133
+ <div>
134
+ <article>
135
+ <p data-ralph-dynamic>Posted 3 days ago</p>
136
+ </article>
137
+ </div>
138
+ ```
139
+
140
+ - **`ignoreInDiff`** removes a subtree from the snapshot entirely, for parts
141
+ that aren't yours to diff, such as ads, chat widgets, or live counters.
142
+ Matched elements still appear in the screenshot:
143
+
144
+ ```ts
145
+ ralphOptions: {
146
+ ignoreInDiff: ['.ad-slot', '#intercom-container', 'live-clock'],
147
+ },
148
+ ```
149
+
150
+ Use `data-ralph-dynamic` when you still want layout and style changes in that
151
+ area reported, and `ignoreInDiff` when you don't.
152
+
153
+ ## How recording works
72
154
 
73
155
  A page is recorded whenever the URL changes in any tab of the test: page loads,
74
156
  redirects (the final page), SPA route changes, query and hash changes, and new
@@ -86,56 +168,25 @@ ralphOptions: {
86
168
  },
87
169
  ```
88
170
 
89
- Screenshots are WebP. A page taller or wider than 16,383 px, the most WebP can
90
- hold, is recorded up to that size, and what lies beyond it has no screenshot.
171
+ Recordings run in the background. A page the test leaves before its screenshot
172
+ is done stays in the graph as `uncaptured`, with no screenshot. See
173
+ [`waitForCapture`](#waiting-for-capture).
91
174
 
92
- CSS animations and transitions are frozen for the screenshot. For animations
93
- driven from JavaScript, set `reduceMotion` if your app honors
94
- `prefers-reduced-motion`.
175
+ Each action between two nodes is stored on the edge with the element it landed
176
+ on (role, name, `data-track-id`) and where that element was on the earlier
177
+ screenshot. Ralph shows it as, e.g., `Click button "Checkout"`. Recorded
178
+ actions are clicks, hovers, fills, key presses, selects, uploads, drags, and
179
+ `goto` / back / forward / reload. Text typed into password or one-time-code
180
+ fields is never recorded.
95
181
 
96
- To hold timers and animations at the same position across the screenshot,
97
- tracked-element geometry, and ARIA snapshot, enable:
182
+ ## Test API
98
183
 
99
- ```ts
100
- ralphOptions: {
101
- freezeDuringCapture: true,
102
- },
103
- ```
184
+ The `ralph` fixture controls recording from inside a test.
104
185
 
105
- Ralph installs Playwright's clock before the app loads. Before capture it
106
- finishes running finite CSS transitions, CSS animations, and Web Animations,
107
- and watches DOM changes in the page, its frames, and open shadow roots. Timers
108
- and animation frames keep running while it waits for 150 ms without changes,
109
- so JavaScript-driven animations can finish without adding callbacks or markers
110
- to your app. Animations started by completion callbacks are checked again.
186
+ ### Recording once the page is ready
111
187
 
112
- This extra settling wait is capped at 1.5 seconds, or half the remaining
113
- `recordTimeoutMs` budget, whichever is smaller. Looping browser animations are
114
- temporarily paused while settling. Then timers and all remaining browser
115
- animations pause during capture and resume afterward, including on failure.
116
- Animations already paused by the app stay paused; finished animations stay
117
- finished.
118
-
119
- The DOM quiet check is a heuristic, not a guarantee that every animation has
120
- ended. Long-running JavaScript animations can be captured midway when the
121
- budget expires. Future delayed work and drawing inside canvas or WebGL aren't
122
- detected by this check.
123
-
124
- This option defaults to `false`. The clock replaces the native timer APIs for
125
- the lifetime of the context, so don't combine it with manual `page.clock` or
126
- `context.clock` control. Playwright requires a future pause time: Ralph uses
127
- the page's current time plus 100 ms, which advances the clock and can fire due
128
- callbacks once before freezing. Time resumes from there without catching up
129
- through the capture duration. All tabs in a context share the clock, so their
130
- timers pause together and their captures run one at a time. Network responses,
131
- workers, and media playback are not frozen, so unrelated updates can still
132
- change the page.
133
-
134
- ### Explicitly, once the page is ready
135
-
136
- A capture taken when the URL or the declared conditions change can catch the
137
- page mid-transition. Call `ralph.recordNode` after your assertions to retake
138
- the screenshot of the node just recorded:
188
+ An automatic capture can catch the page mid-transition. Call `ralph.recordNode`
189
+ after your assertions to retake the screenshot of the current node:
139
190
 
140
191
  ```ts
141
192
  test("cart", { tag: "@ralph" }, async ({ page, ralph }) => {
@@ -146,19 +197,18 @@ test("cart", { tag: "@ralph" }, async ({ page, ralph }) => {
146
197
  });
147
198
  ```
148
199
 
149
- Pass a `name` to label the node in the graph, e.g.
150
- `ralph.recordNode(page, { name: 'Cart' })`. A later call without one keeps it.
200
+ Each call replaces the screenshot, so the last one wins. Options:
151
201
 
152
- Each call replaces the screenshot, so the last one wins. A dialog at the same
153
- URL is the same node, so recording it would replace the page's screenshot: give
154
- it its own node by declaring a page tag for it (below), since that is what
155
- production events carry.
202
+ - `name` labels the node in the graph: `ralph.recordNode(page, { name: 'Cart' })`.
203
+ - `edgeLabel` describes the step into it in your own words:
204
+ `ralph.recordNode(page, { name: 'Done', edgeLabel: 'Sign in' })`.
205
+
206
+ A later call without either one keeps the earlier value.
156
207
 
157
- ### Node conditions, for pages that render differently at one URL
208
+ ### One URL, several renderings
158
209
 
159
210
  When one URL renders differently by visitor (a plan, an experiment group) or by
160
- page state (an open dialog), declare what each screenshot stands for. The test is
161
- the only source of a node's conditions, including sets of values:
211
+ page state (an open dialog), declare what each screenshot stands for:
162
212
 
163
213
  ```ts
164
214
  await page.goto("https://example.com/checkout");
@@ -169,18 +219,13 @@ const undo = await ralph.setNodeConditions(page, {
169
219
  await undo();
170
220
  ```
171
221
 
172
- It changes no app state. A declaration that moves the page to a different node
173
- records it again; declarations merge until the URL changes. If your app tags
174
- visitors and pages with `@ralphralphai/tracker`, each recording also checks the
175
- declaration against those tags: where the app reports a value outside it, the node
176
- lists it in `tagMismatches` and the upload page shows it, because those nodes would
177
- match no production traffic. `strictNodeTags` fails the test instead.
222
+ This changes no app state. A declaration that moves the page to a different
223
+ node records it again, and declarations merge until the URL changes. A dialog
224
+ at the same URL is the same node unless you give it a page tag like this.
178
225
 
179
226
  ### Waiting for capture
180
227
 
181
- Recordings run in the background. If the test leaves a page before its
182
- screenshot is done, the page is `uncaptured`: it's in the graph without a
183
- screenshot. To make sure a page is captured before leaving it:
228
+ To make sure a page is captured before leaving it:
184
229
 
185
230
  ```ts
186
231
  await page.goto("https://example.com/pricing");
@@ -204,9 +249,7 @@ await ralph.visitAllLinks(page.locator("a.article:visible"), {
204
249
  ```
205
250
 
206
251
  Each link opens in its own tab, is recorded, and closes before the next one
207
- opens. The edge into each target carries the click. Every match must be a link
208
- (`<a href>`): a button that navigates by script can't open in a new tab, so the
209
- call throws before opening any.
252
+ opens. Every match must be an `<a href>`, or the call throws before opening any.
210
253
 
211
254
  ### Pausing capture
212
255
 
@@ -221,8 +264,8 @@ ralph.resume();
221
264
  await page.goto("https://example.com/d"); // recorded, linked from /a
222
265
  ```
223
266
 
224
- This works across tabs. While paused, `recordNode` does nothing. `resume` does
225
- not record the current page.
267
+ This works across tabs. While paused, `recordNode` does nothing and actions are
268
+ left off the edge. `resume` does not record the current page.
226
269
 
227
270
  ### Browser contexts you create yourself
228
271
 
@@ -234,131 +277,36 @@ await ralph.waitForCapture();
234
277
  await context.close();
235
278
  ```
236
279
 
237
- With `freezeDuringCapture`, await `ralph.observe(context)` before creating or
238
- navigating any pages, so no app scripts schedule native timers first. A context
239
- with an already navigated page cannot be enrolled with this option enabled.
240
-
241
- ### What happened between two nodes
242
-
243
- The Playwright actions a test takes on the way from one node to the next are
244
- recorded on the edge between them: clicks, hovers, fills, key presses,
245
- selects, uploads, drags, and `goto` / back / forward / reload. Each one keeps
246
- the element it landed on (its role, name, and `data-track-id`) and where that
247
- element was on the earlier node's screenshot, which Ralph shows as, e.g.,
248
- `Click button "Checkout"`.
249
-
250
- What was typed into a password or one-time-code field is never recorded.
251
- Actions taken while recording is paused are left out.
252
-
253
- Pass an `edgeLabel` to describe the step in your own words, e.g.
254
- `ralph.recordNode(page, { name: 'Done', edgeLabel: 'Sign in' })`. A later call
255
- without one keeps it.
280
+ ## Stable captures
256
281
 
257
- ### Excluding elements from snapshots
282
+ For apps with animations or live content:
258
283
 
259
- For parts of the page that change on their own, such as ads, chat widgets or
260
- live counters, list CSS selectors in `ignoreInDiff`:
261
-
262
- ```ts
263
- ralphOptions: {
264
- ignoreInDiff: ['.ad-slot', '#intercom-container', 'live-clock'],
265
- },
266
- ```
267
-
268
- Matched elements and their descendants, including content in open shadow roots
269
- and slots, are left out of the ARIA snapshot used for diffing. They still appear
270
- in the screenshot, and the space they take up still moves the elements around
271
- them. Text that sits inline in a larger element, like a bare `<span>` in a
272
- paragraph, belongs to that element's text and is not removed. Wrap it in an
273
- element with a role, or match the whole paragraph.
274
-
275
- ### When recording fails
276
-
277
- - A failed `recordNode` call rejects.
278
- - A failed automatic recording fails the test at the end.
279
- - Pages left before capture, and pages whose layout moved during the
280
- screenshot, don't fail the test.
284
+ - `reduceMotion: true` emulates `prefers-reduced-motion: reduce`. CSS
285
+ animations and transitions are always frozen for the screenshot.
286
+ - `freezeDuringCapture: true` lets the page settle, then freezes timers and
287
+ animations so the screenshot, element positions, and ARIA snapshot match.
288
+ See [Troubleshooting](#freezeduringcapture-in-detail) before enabling it.
289
+ - `data-ralph-dynamic` and `ignoreInDiff` keep content that changes on its own
290
+ out of the diff. See [Diffing deployments](#diffing-deployments).
281
291
 
282
292
  ## Modes
283
293
 
284
294
  Set with `RALPH_MODE` or the `mode` option:
285
295
 
286
- | Mode | What happens |
287
- | -------- | ------------------------------------------------------------------------------------------------------ |
288
- | `off` | Default. Nothing is recorded, and no config or credentials are needed. |
289
- | `upload` | `@ralph` tests are recorded, and the reporter uploads the run. Default when `RALPH_UPLOAD_KEY` is set. |
296
+ | Mode | What happens |
297
+ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
298
+ | `off` | Default. Nothing is recorded, and no config or credentials are needed. |
299
+ | `upload` | `@ralph` tests are recorded, and the reporter uploads the run as a preview graph not tied to a release. Default when `RALPH_UPLOAD_KEY` is set. |
300
+ | `deploy` | Like `upload`, but the run is uploaded as its build's graph. See [Deploying a release](#deploying-a-release). |
290
301
 
291
- Untagged tests are never recorded. In `upload` mode, calling
302
+ The reporter uploads the run when any test ran in `upload` or `deploy` mode,
303
+ and as its build's graph when any ran in `deploy` mode.
304
+
305
+ Untagged tests are never recorded. In `upload` and `deploy` modes, calling
292
306
  `ralph.recordNode`, `ralph.setNodeConditions`, or `ralph.observe` in an
293
307
  untagged test throws. Set `ignoreUntaggedCalls` to make them do nothing
294
308
  instead, for helpers shared between tagged and untagged tests.
295
309
 
296
- ## Uploading to Ralph
297
-
298
- 1. Create an upload key for your project in the project dashboard.
299
- 2. Set these as environment variables, or `appId` as
300
- [reporter options](#reporter-options):
301
-
302
- | Variable | Value |
303
- | ------------------ | --------------------------------- |
304
- | `RALPH_UPLOAD_KEY` | The upload key. Environment only. |
305
- | `RALPH_APP_ID` | The app the recordings belong to. |
306
-
307
- 3. Run:
308
-
309
- ```sh
310
- RALPH_MODE=upload pnpm exec playwright test
311
- ```
312
-
313
- The reporter uploads the whole run once. It prints the result link before the
314
- upload starts, so you can open it straight away and watch the run upload and
315
- then be processed:
316
-
317
- ```
318
- Ralph: uploading the run. Follow the upload and processing at https://dash.ralphralph.ai/uploads/0f6c6a2e-…
319
- Ralph: uploaded the run. Ralph is processing it at https://dash.ralphralph.ai/uploads/0f6c6a2e-…
320
- ```
321
-
322
- ### Sharded runs
323
-
324
- Use the `blob` reporter on each shard, then merge with the Ralph reporter:
325
-
326
- ```sh
327
- # Each shard (no upload credentials needed):
328
- RALPH_MODE=upload pnpm exec playwright test --shard=1/4 --reporter blob
329
-
330
- # Once, after collecting every shard's blob-report/ into all-blob-reports/:
331
- RALPH_MODE=upload RALPH_UPLOAD_KEY=… RALPH_APP_ID=… \
332
- pnpm exec playwright merge-reports --reporter @ralphralphai/playwright/reporter ./all-blob-reports
333
- ```
334
-
335
- ### Deployments
336
-
337
- When you deploy, upload the run as the graph of the deployed build. Ralph then
338
- matches tracker events from that build to it.
339
-
340
- Set `RALPH_DEPLOY=1` and `RALPH_BUILD_ID` to the build's id, usually the commit
341
- SHA:
342
-
343
- ```sh
344
- RALPH_MODE=upload RALPH_DEPLOY=1 RALPH_BUILD_ID="$COMMIT_SHA" \
345
- RALPH_UPLOAD_KEY=… RALPH_APP_ID=… pnpm exec playwright test
346
- ```
347
-
348
- If the build goes live before the test run finishes, register it first so the
349
- tracker picks up its config right away:
350
-
351
- ```sh
352
- curl --fail-with-body -X POST https://api.ralphralph.ai/api/upload/deployment/register \
353
- -H "Authorization: Bearer $RALPH_UPLOAD_KEY" -H 'content-type: application/json' \
354
- -d "{\"externalAppId\":\"$RALPH_APP_ID\",\"buildId\":\"$COMMIT_SHA\",
355
- \"reserveArtifact\":false,\"config\":$(cat ralph.json)}"
356
- ```
357
-
358
- `ralph.json` here is your `ralph.jsonc` as plain JSON, without comments or
359
- trailing commas. The test run that follows registers the same build again and
360
- uploads into it.
361
-
362
310
  ## Screen sizes
363
311
 
364
312
  Use `ralphProjects` to create one project per entry in `screenSizes`:
@@ -404,9 +352,21 @@ test.describe("desktop layouts", { tag: "@ralph-screen:1280x720" }, () => {
404
352
  });
405
353
  ```
406
354
 
407
- A size missing from `screenSizes` fails the test.
355
+ A size missing from `screenSizes` fails the test. To run one size:
356
+ `playwright test --project "chromium 375x812"`.
357
+
358
+ ## Sharded runs
408
359
 
409
- To run one size: `playwright test --project "chromium 375x812"`.
360
+ Use the `blob` reporter on each shard, then merge with the Ralph reporter:
361
+
362
+ ```sh
363
+ # Each shard (no upload credentials needed):
364
+ RALPH_MODE=upload pnpm exec playwright test --shard=1/4 --reporter blob
365
+
366
+ # Once, after collecting every shard's blob-report/ into all-blob-reports/:
367
+ RALPH_MODE=upload RALPH_UPLOAD_KEY=… RALPH_APP_ID=… \
368
+ pnpm exec playwright merge-reports --reporter @ralphralphai/playwright/reporter ./all-blob-reports
369
+ ```
410
370
 
411
371
  ## Options
412
372
 
@@ -420,37 +380,114 @@ export default defineConfig<RalphFixtures>({
420
380
  });
421
381
  ```
422
382
 
423
- | Option | Default | What it does |
424
- | --------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------- |
425
- | `mode` | `RALPH_MODE`, else `off` | `off` or `upload`. See [Modes](#modes). |
426
- | `configPath` | `ralph.jsonc` | Path to the config, relative to the Playwright config. |
427
- | `config` | | An inline config instead of a file (type: `WebRalphConfig`). |
428
- | `buildId` | `RALPH_BUILD_ID` | Your app's build, to line up recordings with its analytics. |
429
- | `runId` | `RALPH_RUN_ID` | Identifies the CI run. |
430
- | `settleMs` | 150 | Wait after the DOM loads before an automatic recording. |
431
- | `recordTimeoutMs` | 5000 | Budget for one recording, including `settleMs` and `ready`. Must exceed `settleMs`. |
432
- | `ready` | | `(page, signal) => Promise<void>`. Waits for your app before an automatic recording. Honor `signal`. |
433
- | `reduceMotion` | `false` | Emulates `prefers-reduced-motion: reduce` and waits for finite animations before recording. |
434
- | `freezeDuringCapture` | `false` | Finishes finite browser animations, allows JS-driven DOM changes to settle within a bounded wait, then freezes timers and animations across capture. |
435
- | `ignoreInDiff` | | CSS selectors for subtrees to omit from the ARIA snapshot. See [Excluding elements from snapshots](#excluding-elements-from-snapshots). |
436
- | `strictNodeTags` | `false` | Fails the test when the app reports a tag outside what `ralph.setNodeConditions` declared. |
437
- | `ignoreUntaggedCalls` | `false` | Makes `recordNode`, `setNodeConditions`, and `observe` no-ops in untagged tests instead of throwing. |
438
-
439
- ## Reporter options
383
+ | Option | Default | What it does |
384
+ | --------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
385
+ | `mode` | `RALPH_MODE`, else `off` | `off`, `upload`, or `deploy`. See [Modes](#modes). |
386
+ | `configPath` | `ralph.jsonc` | Path to the config, relative to the Playwright config. |
387
+ | `config` | | An inline config instead of a file (type: `WebRalphConfig`). |
388
+ | `buildId` | `RALPH_BUILD_ID` | Your app's build, to line up recordings with its analytics. |
389
+ | `runId` | `RALPH_RUN_ID` | Identifies the CI run. |
390
+ | `settleMs` | 150 | Wait after the DOM loads before an automatic recording. |
391
+ | `recordTimeoutMs` | 5000 | Budget for one recording, including `settleMs` and `ready`. Must exceed `settleMs`. |
392
+ | `ready` | | `(page, signal) => Promise<void>`. Waits for your app before an automatic recording. Honor `signal`. |
393
+ | `reduceMotion` | `false` | Emulates `prefers-reduced-motion: reduce` and waits for finite animations before recording. |
394
+ | `freezeDuringCapture` | `false` | Lets the page settle, then freezes timers and animations across capture. See [Troubleshooting](#freezeduringcapture-in-detail). |
395
+ | `ignoreInDiff` | | CSS selectors for subtrees to omit from the ARIA snapshot. |
396
+ | `strictNodeTags` | `false` | Fails the test when the app reports a tag outside what `ralph.setNodeConditions` declared. |
397
+ | `ignoreUntaggedCalls` | `false` | Makes `recordNode`, `setNodeConditions`, and `observe` no-ops in untagged tests instead of throwing. |
398
+
399
+ ### Reporter options
440
400
 
441
401
  ```ts
442
402
  reporter: [['list'], ['@ralphralphai/playwright/reporter', { appId: 'my-app' }]],
443
403
  ```
444
404
 
445
- | Option | Default | What it does |
446
- | ----------------- | ------------------------------------------------- | ---------------------------------------------------------------- |
447
- | `mode` | `RALPH_MODE`, else `upload` if `RALPH_UPLOAD_KEY` | `upload` uploads the run. |
448
- | `appId` | `RALPH_APP_ID` | The app the recordings belong to. |
449
- | `uploadTimeoutMs` | 120000 | Upload timeout. |
450
- | `deploy` | `RALPH_DEPLOY` is `1` | Uploads the run as its build's. See [Deployments](#deployments). |
451
- | `outputDir` | `ralph/` in the first project's output directory | Where the merged graph is written. |
405
+ | Option | Default | What it does |
406
+ | ----------------- | ------------------------------------------------- | ------------------------------------------------------------- |
407
+ | `mode` | `RALPH_MODE`, else `upload` if `RALPH_UPLOAD_KEY` | `upload` uploads the run. `deploy` uploads it as its build's. |
408
+ | `appId` | `RALPH_APP_ID` | The app the recordings belong to. |
409
+ | `uploadTimeoutMs` | 120000 | Upload timeout. |
410
+ | `outputDir` | `ralph/` in the first project's output directory | Where the merged graph is written. |
411
+
412
+ ## Troubleshooting
413
+
414
+ ### A test failed because of recording
415
+
416
+ - A failed `recordNode` call rejects.
417
+ - A failed automatic recording fails the test at the end.
418
+ - Pages left before capture, and pages whose layout moved during the
419
+ screenshot, don't fail the test.
420
+
421
+ ### A page has no screenshot
422
+
423
+ The test left the page before capture finished, so it's `uncaptured`. Call
424
+ `ralph.waitForCapture()` before navigating away.
452
425
 
453
- The run is also uploaded when any test ran in `upload` mode.
426
+ ### Part of a very large page is missing
427
+
428
+ Screenshots are WebP, which holds at most 16,383 px per side. A taller or wider
429
+ page is recorded up to that size, and what lies beyond it has no screenshot.
430
+
431
+ ### A dialog replaced the page's screenshot
432
+
433
+ A dialog at the same URL is the same node. Give it its own node with a page tag
434
+ via `ralph.setNodeConditions` (see
435
+ [One URL, several renderings](#one-url-several-renderings)), since that is what
436
+ production events carry.
437
+
438
+ ### The upload page shows `tagMismatches`
439
+
440
+ If your app tags visitors and pages with `@ralphralphai/tracker`, each
441
+ recording checks the declared node conditions against those tags. Where the app
442
+ reports a value outside the declaration, the node lists it in `tagMismatches`,
443
+ because that node would match no production traffic. Fix the declaration, or set
444
+ `strictNodeTags` to fail the test instead.
445
+
446
+ ### `visitAllLinks` throws
447
+
448
+ Every match must be a link (`<a href>`). A button that navigates by script
449
+ can't open in a new tab, so the call throws before opening any.
450
+
451
+ ### An ignored element's text still shows in the diff
452
+
453
+ `ignoreInDiff` removes matched elements and their descendants, including
454
+ content in open shadow roots and slots. Text that sits inline in a larger
455
+ element, like a bare `<span>` in a paragraph, belongs to that element's text
456
+ and is not removed. Wrap it in an element with a role, or match the whole
457
+ paragraph. The space ignored elements take up still moves the elements around
458
+ them.
459
+
460
+ ### `freezeDuringCapture` in detail
461
+
462
+ Ralph installs Playwright's clock before the app loads. Before capture it
463
+ finishes running finite CSS transitions, CSS animations, and Web Animations,
464
+ and watches DOM changes in the page, its frames, and open shadow roots. Timers
465
+ and animation frames keep running until the DOM has been quiet for 150 ms, so
466
+ JavaScript-driven animations can finish without changes to your app.
467
+ Animations started by completion callbacks are checked again.
468
+
469
+ This settling wait is capped at 1.5 seconds or half the remaining
470
+ `recordTimeoutMs` budget, whichever is smaller. Looping browser animations are
471
+ paused while settling. Then timers and all remaining browser animations pause
472
+ during capture and resume afterward, including on failure. Animations the app
473
+ already paused stay paused, and finished animations stay finished.
474
+
475
+ Limits:
476
+
477
+ - The quiet check is a heuristic. Long JavaScript animations can be captured
478
+ midway when the budget runs out, and future delayed work and canvas or WebGL
479
+ drawing aren't detected.
480
+ - The clock replaces native timer APIs for the lifetime of the context, so
481
+ don't combine it with manual `page.clock` or `context.clock` control.
482
+ - Playwright requires a future pause time. Ralph uses the page's current time
483
+ plus 100 ms, which advances the clock and can fire due callbacks once before
484
+ freezing. Time resumes from there without catching up through the capture.
485
+ - All tabs in a context share the clock, so their timers pause together and
486
+ their captures run one at a time.
487
+ - Network responses, workers, and media playback are not frozen.
488
+ - With contexts you create yourself, await `ralph.observe(context)` before
489
+ creating or navigating any pages. A context with an already navigated page
490
+ can't be enrolled with this option on.
454
491
 
455
492
  ## Limits
456
493
 
@@ -458,6 +495,9 @@ Per run: 5,000 screenshots, 256 MiB of screenshots, 16 MiB of other recorded
458
495
  data, 256 MiB compressed, 20,000 recorded pages, and 5,000 recorded pages per
459
496
  test.
460
497
 
498
+ Per page: an ARIA snapshot over 4 MiB or nested deeper than the server
499
+ allows is left out. The page still uploads, but it can't be diffed.
500
+
461
501
  ## License
462
502
 
463
503
  Apache-2.0
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_upload = require("./upload-KEwXpYxG.cjs");
2
+ const require_upload = require("./upload-8iYQZaCa.cjs");
3
3
  let node_path = require("node:path");
4
4
  node_path = require_upload.__toESM(node_path, 1);
5
5
  let _playwright_test = require("@playwright/test");
@@ -70,7 +70,7 @@ const test = _playwright_test.test.extend({
70
70
  return;
71
71
  }
72
72
  const reported = process.env["RALPH_PLAYWRIGHT_REPORTER"] || testInfo.config.reporter.some(([name]) => name === "blob");
73
- if (mode === "upload" && !reported) throw new Error("Ralph uploads one merged graph per run from its reporter. Add ['@ralphralphai/playwright/reporter'] to `reporter` in playwright.config, or use the blob reporter and merge-reports when sharding.");
73
+ if ((mode === "upload" || mode === "deploy") && !reported) throw new Error("Ralph uploads one merged graph per run from its reporter. Add ['@ralphralphai/playwright/reporter'] to `reporter` in playwright.config, or use the blob reporter and merge-reports when sharding.");
74
74
  const root = testInfo.config.configFile ? node_path.default.dirname(testInfo.config.configFile) : process.cwd();
75
75
  const key = JSON.stringify({
76
76
  root,