@ralphralphai/playwright 0.1.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 ADDED
@@ -0,0 +1,350 @@
1
+ # @ralphralphai/playwright
2
+
3
+ Opt-in Playwright fixtures for recording the page states a test visits. Ralph
4
+ automatically captures URL changes, and explicit captures handle dialogs and
5
+ other states at the same URL. Each capture contains a screenshot, the recorded
6
+ and actual URLs, and `data-track-id` element positions.
7
+
8
+ The fixture saves local artifacts and can upload a gzip-compressed tar bundle per
9
+ test attempt. Graph construction runs on the server; ingestion currently returns
10
+ an attempt receipt, with graph processing and the dashboard URL still pending.
11
+
12
+ ## Install and enable
13
+
14
+ Requires Node.js 20.19+ and Playwright Test 1.62.1+ within major version 1.
15
+
16
+ ```sh
17
+ pnpm add -D @ralphralphai/playwright @playwright/test
18
+ pnpm exec playwright install chromium
19
+ ```
20
+
21
+ Put a `ralph.jsonc` next to `playwright.config.ts`:
22
+
23
+ ```json
24
+ {
25
+ "screenSizes": []
26
+ }
27
+ ```
28
+
29
+ The actual viewport is controlled by Playwright; use `ralphProjects` to run
30
+ tests at the config's `screenSizes` (see [Screen sizes](#screen-sizes)). The
31
+ fixture preserves the Ralph config; it does not run its simple URL crawls.
32
+
33
+ Use Ralph's extended `test` and tag the tests that should participate:
34
+
35
+ ```ts
36
+ import { test, expect } from '@ralphralphai/playwright';
37
+
38
+ test('checkout', { tag: '@ralph' }, async ({ page, ralph }) => {
39
+ await page.goto('https://example.com/cart');
40
+ await ralph.flush();
41
+
42
+ await page.getByRole('button', { name: 'Shipping options' }).click();
43
+ await expect(page.getByRole('dialog')).toBeVisible();
44
+
45
+ await ralph.captureNode(page, {
46
+ state: 'shipping-options-open',
47
+ url: '/checkout/shipping-options',
48
+ });
49
+ });
50
+
51
+ test('ordinary validation', async ({ page }) => {
52
+ await page.goto('https://example.com/login');
53
+ await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible();
54
+ });
55
+ ```
56
+
57
+ ```sh
58
+ RALPH_MODE=local pnpm exec playwright test
59
+ ```
60
+
61
+ Recording defaults to off. Normal `pnpm exec playwright test` runs need no
62
+ Ralph config or credentials. Unmarked tests stay unrecorded in every mode.
63
+ Calling `ralph.capture` in an unmarked test while recording is enabled reports
64
+ the missing `@ralph` tag.
65
+
66
+ `RALPH_MODE=off` disables recording and makes explicit capture calls no-ops.
67
+ Tests that import directly from `@playwright/test` continue to work alongside
68
+ Ralph tests. Importing the integration does not launch a browser for API-only
69
+ tests.
70
+
71
+ `RALPH_MODE=upload`, or setting `RALPH_UPLOAD_KEY` without an explicit mode,
72
+ enables uploading. Set `RALPH_API_URL` to your Ralph server origin (or set
73
+ `ralphOptions.apiUrl`). The origin must use HTTPS; plain HTTP is accepted only
74
+ for loopback hosts (`localhost`, `127.x.x.x`, `[::1]`) during local development.
75
+ Issue a private upload key for your Ralph project with the authenticated
76
+ `/api/project/playwright/key/create` endpoint and keep it in CI secrets. A key belongs to
77
+ the project, so one key serves every app in it. Set `RALPH_APP_ID` (or
78
+ `ralphOptions.appId`) to the app the captures belong to.
79
+
80
+ ```sh
81
+ RALPH_MODE=upload RALPH_API_URL=https://your-ralph-server.example RALPH_APP_ID=<app id> pnpm exec playwright test
82
+ ```
83
+
84
+ This uses `RALPH_UPLOAD_KEY` from the environment. Each opted-in attempt uploads
85
+ once during fixture teardown, after its local manifest and screenshots are saved.
86
+ No reporter configuration is required. `off`, `local`, and unmarked tests do not
87
+ upload, even if CI provides a key. Upload failures fail fixture teardown and leave
88
+ local artifacts available according to Playwright's `preserveOutput` setting.
89
+ Successful uploads attach a `ralph-upload-receipt`; credentials are never written
90
+ to the manifest or receipt.
91
+
92
+ ## Capture behavior
93
+
94
+ Automatic capture watches main-frame HTTP(S) URLs in the standard `context`
95
+ fixture, including new tabs and popups. It includes the first application URL,
96
+ full-document navigation, SPA history changes, query changes, and hash changes.
97
+ It skips `about:blank`, subframe navigation, same-URL reloads, and DOM-only changes.
98
+
99
+ Automatic captures wait for DOM content loaded and then settle for 150 ms by
100
+ default. A URL change does not prove application readiness. Supply `ready` for
101
+ an application-specific condition, or explicitly capture after your assertions.
102
+
103
+ Capture runs in the background and is serialized per browser page. A test can
104
+ navigate again before a screenshot completes. Such nodes are retained as
105
+ `superseded`, without a screenshot attributed to the wrong URL. Use
106
+ `await ralph.flush()` to wait for currently queued captures before deliberately
107
+ leaving a state whose screenshot you need. This is also useful before manually
108
+ closing a page or context.
109
+
110
+ The standard context fixture drains captures before closing its pages. Manually
111
+ created contexts can be enrolled with `ralph.observe(context)`; their owner must
112
+ flush before closing them. An explicit capture enrolls its page's context too.
113
+
114
+ `await ralph.captureNode(page, { state?, url? })` always requests a node,
115
+ even at the same URL. It uses the current state without the automatic settling
116
+ delay or readiness hook. A relative URL override resolves against the actual
117
+ browser URL. It does not navigate, change history, or affect subsequent automatic
118
+ captures. Only HTTP(S) recorded URLs are accepted.
119
+
120
+ Explicit capture errors reject the call. Automatic capture errors are written
121
+ to the manifest and fail an otherwise passing opted-in test at fixture teardown.
122
+ Superseded captures and detected geometry changes mark the manifest incomplete,
123
+ without independently failing the test.
124
+
125
+ ## Screen sizes
126
+
127
+ A viewport is fixed for a browser context, and a mobile flow is usually a
128
+ different test from its desktop flow. So each `screenSizes` entry becomes its
129
+ own Playwright project. `ralphProjects` reads the Ralph config synchronously
130
+ and expands a base project into one project per size. Each project's viewport
131
+ is that size:
132
+
133
+ ```ts
134
+ import { defineConfig, devices } from '@playwright/test';
135
+ import { ralphProjects, type RalphFixtures } from '@ralphralphai/playwright';
136
+
137
+ export default defineConfig<RalphFixtures>({
138
+ projects: [
139
+ // Ordinary tests keep their single project.
140
+ { name: 'chromium', grepInvert: /@ralph\b/, use: devices['Desktop Chrome'] },
141
+ // With screenSizes 375x812, 768x1024 and 1280x720, this yields the projects
142
+ // "chromium 375x812", "chromium 768x1024" and "chromium 1280x720".
143
+ ...ralphProjects({
144
+ name: 'chromium',
145
+ grep: /@ralph\b/,
146
+ use: devices['Desktop Chrome'],
147
+ }),
148
+ ],
149
+ });
150
+ ```
151
+
152
+ A relative `configPath` resolves against the current directory. Pass
153
+ `{ root: import.meta.dirname }` (or `__dirname`) or an inline `config` as the
154
+ second argument when Playwright runs from elsewhere. Keep it pointing at the
155
+ same config as `ralphOptions`.
156
+
157
+ By default, a test runs at every screen size. Tag a test or a `describe` block
158
+ with `@ralph-screen:WIDTHxHEIGHT`, once per size, to run it only at the listed
159
+ sizes. Tags on a `describe` block and its tests combine. The other
160
+ screen projects skip it:
161
+
162
+ ```ts
163
+ test(
164
+ 'mobile navigation',
165
+ { tag: ['@ralph', '@ralph-screen:375x812'] },
166
+ async ({ page }) => {
167
+ await page.getByRole('button', { name: 'Menu' }).click();
168
+ },
169
+ );
170
+
171
+ // One tag per size: runs at 768x1024 and 1280x720, skipped at 375x812.
172
+ test(
173
+ 'data table',
174
+ { tag: ['@ralph', '@ralph-screen:768x1024', '@ralph-screen:1280x720'] },
175
+ async ({ page }) => {},
176
+ );
177
+
178
+ test.describe('desktop layouts', { tag: '@ralph-screen:1280x720' }, () => {
179
+ test('sidebar', { tag: '@ralph' }, async ({ page }) => {});
180
+ });
181
+ ```
182
+
183
+ Screen tags apply in every mode, including `off`, and to untagged tests, because
184
+ they choose which flows run. A size the config does not declare fails the test,
185
+ so typos don't silently skip it. Projects not created by `ralphProjects` ignore
186
+ screen tags. To run one size from the command line, use Playwright's project
187
+ filter: `playwright test --project "chromium 375x812"`.
188
+
189
+ ## Options
190
+
191
+ `WebRalphConfig` is re-exported as a type from this package for inline config
192
+ annotations. Its definition remains in `@ralphralphai/config`:
193
+
194
+ ```ts
195
+ import type { WebRalphConfig } from '@ralphralphai/playwright';
196
+
197
+ const config: WebRalphConfig = { screenSizes: [] };
198
+ ```
199
+
200
+ Set `ralphOptions` with `test.use` or Playwright's configuration:
201
+
202
+ ```ts
203
+ import { defineConfig } from '@playwright/test';
204
+ import type { RalphFixtures } from '@ralphralphai/playwright';
205
+
206
+ export default defineConfig<RalphFixtures>({
207
+ use: {
208
+ viewport: { width: 1280, height: 720 },
209
+ ralphOptions: {
210
+ mode: 'local',
211
+ configPath: './ralph.jsonc',
212
+ buildId: 'application-build-id',
213
+ runId: 'ci-run-id',
214
+ settleMs: 150,
215
+ captureTimeoutMs: 5000,
216
+ ready: async (page, signal) => {
217
+ signal.throwIfAborted();
218
+ await page.getByTestId('app-ready').waitFor({ timeout: 3000 });
219
+ },
220
+ },
221
+ },
222
+ });
223
+ ```
224
+
225
+ | Option | Behavior |
226
+ | --- | --- |
227
+ | `mode` | `off`, `local`, or `upload`; overrides `RALPH_MODE` |
228
+ | `apiUrl` | Server origin for uploads; falls back to `RALPH_API_URL` |
229
+ | `uploadTimeoutMs` | Upload request deadline; defaults to 15000 ms |
230
+ | `configPath` | Relative to the Playwright config directory; defaults to `ralph.jsonc` |
231
+ | `config` | Inline `WebRalphConfig` instead of a config file |
232
+ | `buildId` | Optional application build ID; falls back to `RALPH_BUILD_ID` |
233
+ | `runId` | Optional shared CI run ID; falls back to `RALPH_RUN_ID` |
234
+ | `settleMs` | Automatic settling delay; defaults to 150 ms |
235
+ | `captureTimeoutMs` | Per-capture browser-work deadline; defaults to 5000 ms |
236
+ | `ready` | Optional automatic-capture readiness callback with an abort signal |
237
+
238
+ The deadline includes settling and readiness. Custom readiness work should
239
+ honor the signal or use its own bounded Playwright operations. Ralph can stop
240
+ waiting for a callback but cannot cancel arbitrary user code.
241
+
242
+ Config files accept JSONC comments and trailing commas and are validated against
243
+ the published config schema. The complete parsed values are snapshotted without
244
+ injecting schema defaults, along with the config package version.
245
+ Snapshots are cached per config source within a worker. Keep the config unchanged
246
+ during a run; the server compares configs across workers before combining
247
+ attempts. Run/build IDs are optional for local exploration, and unique
248
+ attempt IDs are always generated.
249
+
250
+ ## Existing fixtures
251
+
252
+ Compose Ralph with the test object your project already uses:
253
+
254
+ ```ts
255
+ import { mergeTests } from '@playwright/test';
256
+ import { test as ralphTest } from '@ralphralphai/playwright';
257
+ import { test as applicationTest } from './application-fixtures';
258
+
259
+ export const test = mergeTests(applicationTest, ralphTest);
260
+ ```
261
+
262
+ If a custom fixture replaces the standard context entirely, enroll that context
263
+ using `ralph.observe(context)`. Capture it while it is still alive.
264
+
265
+ ## Artifacts
266
+
267
+ Each opted-in attempt writes `ralph/captures.json` and full-page screenshots within
268
+ Playwright's per-test output directory. The JSON is attached as
269
+ `ralph-captures`; screenshots are attached with unique names referenced by the
270
+ manifest. Copy the full test output or retain the Playwright attachments.
271
+ Configure Playwright to preserve successful test output when collecting captures.
272
+
273
+ The exported `CaptureManifest` and `RawNode` types describe format
274
+ version 1. The manifest includes the config snapshot, producer versions, optional run and
275
+ build IDs, test/project/retry/repeat/shard identity, separate browser-page streams,
276
+ opener relationships, and the `nodes` the test produced.
277
+
278
+ Each entry in `nodes` is a raw node: one page state as the test saw it, with a
279
+ sequence within its page. The same page visited twice produces two raw nodes,
280
+ and superseded or failed captures are kept too. The server merges raw nodes
281
+ into the graph's nodes; this package does no normalization or merging.
282
+
283
+ Each node records `previousNodeId`, the node its page state was reached from:
284
+ the previous one on the same page, or for a popup's first node, its opener's
285
+ latest. Superseded nodes are skipped. No code is injected into
286
+ the page; the flow comes from the order of URL changes.
287
+
288
+ Screenshots are WebP at quality 80, falling back to PNG for pages over 16,383
289
+ pixels in either dimension, which WebP cannot encode. They use one image pixel
290
+ per CSS pixel, including on high-DPI devices.
291
+ Tracked-element boxes use document CSS coordinates and preserve duplicate track
292
+ IDs as separate boxes. Collection includes visible elements in the main frame,
293
+ including open shadow roots; it does not collect child-frame geometry. Elements
294
+ with zero width or height, or entirely off-screen horizontally, are dropped;
295
+ elements below the fold are kept.
296
+ The manifest records viewport, document extent, scroll position, screenshot size,
297
+ and device pixel ratio.
298
+
299
+ Geometry is measured before and after the screenshot. Differences set
300
+ `geometryStable: false`. This detects layout drift, not every visual change;
301
+ animations, sticky elements, and virtualized content can still need explicit
302
+ application preparation. The package does not freeze or alter the application.
303
+
304
+ An attempt is `complete` only when the test has passed at fixture finalization,
305
+ there is at least one node, and every node has a screenshot with
306
+ stable measured geometry. A future reporter must reconcile this with the final
307
+ Playwright result if another fixture fails later. Retries have distinct manifests
308
+ and attempt IDs; partial nodes are preserved.
309
+
310
+ ## Upload format and retries
311
+
312
+ The HTTP body is one `application/gzip` tar archive. `capture.json` holds UTF-8
313
+ JSON `{ manifest, screenshots: [{ nodeId, path }] }`, and each `path` points to
314
+ a raw WebP (or PNG fallback) under `assets/images/`. The manifest includes the exact config
315
+ snapshot.
316
+ The package emits only its current `formatVersion: 1`; the server independently
317
+ keeps versioned Zod schemas and selects one by `formatVersion`. No crawler API
318
+ package or server schema is imported by this integration.
319
+
320
+ The limits per attempt are 64 MiB compressed, 2 MiB manifest JSON, 64 MiB total
321
+ screenshot bytes, and 500 screenshots. Server validation
322
+ also limits nodes to 5,000. Split larger scenarios into smaller tests.
323
+ Screenshots are already compressed; gzip mainly compresses the manifest.
324
+
325
+ `uploadCapture(manifest, screenshots, { apiUrl, uploadKey, timeoutMs? })` can retry
326
+ saved artifacts, where each screenshot is `{ nodeId, contentType, bytes: Uint8Array }`.
327
+ `createCaptureBundle(manifest, screenshots)` returns the gzip bytes when a caller
328
+ needs to handle transport itself. An identical retry preserves `attemptId` and
329
+ returns `replayed: true`; changed contents under the same ID are rejected. There
330
+ are no automatic HTTP retries. Playwright test retries create new attempt IDs.
331
+ The manifest's status reflects fixture finalization; a future reporter can
332
+ reconcile failures in fixtures that finish later.
333
+
334
+ ## Development
335
+
336
+ From the repository root:
337
+
338
+ ```sh
339
+ pnpm --filter @ralphralphai/config build
340
+ pnpm --filter @ralphralphai/playwright build
341
+ pnpm --filter @ralphralphai/playwright check
342
+ pnpm --filter @ralphralphai/playwright lint
343
+ pnpm --filter @ralphralphai/playwright test
344
+ pnpm --filter @ralphralphai/playwright exec playwright install chromium
345
+ pnpm --filter @ralphralphai/playwright test:browser
346
+ ```
347
+
348
+ The browser suite runs real Playwright tests against locally supplied pages,
349
+ then checks the emitted manifests and screenshot attachments. It includes an
350
+ intentional failed first attempt to verify retry isolation.