@ralphralphai/playwright 2.9.0 → 2.11.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 +52 -0
- package/dist/index.cjs +11 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +37 -1
- package/dist/index.mjs +11 -1
- package/dist/index.mjs.map +1 -1
- package/dist/reporter.cjs +1 -1
- package/dist/reporter.mjs +1 -1
- package/dist/{upload-8iYQZaCa.cjs → upload-B7tSC0NA.cjs} +146 -13
- package/dist/upload-B7tSC0NA.cjs.map +1 -0
- package/dist/{upload-DKeVoDQ2.mjs → upload-C_KLRH9y.mjs} +146 -13
- package/dist/upload-C_KLRH9y.mjs.map +1 -0
- package/package.json +2 -2
- package/dist/upload-8iYQZaCa.cjs.map +0 -1
- package/dist/upload-DKeVoDQ2.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -223,6 +223,52 @@ This changes no app state. A declaration that moves the page to a different
|
|
|
223
223
|
node records it again, and declarations merge until the URL changes. A dialog
|
|
224
224
|
at the same URL is the same node unless you give it a page tag like this.
|
|
225
225
|
|
|
226
|
+
When the conditions belong to the page a step navigates to, wait for it with
|
|
227
|
+
`ralph.waitForNode`:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
await page.getByRole("button", { name: "Upgrade" }).click();
|
|
231
|
+
await ralph.waitForNode(page, "**/checkout?plan=*", /*tag to apply*/{
|
|
232
|
+
visitorTags: { plan: "pro" },
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
It takes the URL as `page.waitForURL` does. Every URL change is recorded
|
|
237
|
+
automatically once the page settles, which can be before the test declares
|
|
238
|
+
anything. `waitForNode` gives that recording the conditions instead of
|
|
239
|
+
recording the page a second time, so the URL gets one node with them, however
|
|
240
|
+
the timing falls. `setNodeConditions` right after `page.waitForURL` would
|
|
241
|
+
record the page twice when the automatic capture wins: once without
|
|
242
|
+
conditions, once with them.
|
|
243
|
+
|
|
244
|
+
### Flows
|
|
245
|
+
|
|
246
|
+
Name the journey a test walks, such as sign-up or onboarding, so the graph can
|
|
247
|
+
show it on its own and Ralph can report its funnel:
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
await page.goto("https://example.com/");
|
|
251
|
+
ralph.startFlow("Sign up"); // the page on screen is the first step
|
|
252
|
+
await page.getByRole("link", { name: "Sign up" }).click();
|
|
253
|
+
await page.getByRole("button", { name: "Continue" }).click();
|
|
254
|
+
ralph.endFlow("Sign up");
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Every node recorded while a flow is open joins it, on every tab. A flow still
|
|
258
|
+
open when the test finishes ends there. Flows can overlap, and one page can be
|
|
259
|
+
in several. Tests that walk a flow differently, such as sign-up by email and by
|
|
260
|
+
Google, become separate paths of the same flow.
|
|
261
|
+
|
|
262
|
+
`startFlow` also returns a handle, so a helper can end what it started:
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
const flow = ralph.startFlow("Onboarding");
|
|
266
|
+
await completeProfile(page);
|
|
267
|
+
flow.end();
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
In a test without the `@ralph` tag, both calls do nothing.
|
|
271
|
+
|
|
226
272
|
### Waiting for capture
|
|
227
273
|
|
|
228
274
|
To make sure a page is captured before leaving it:
|
|
@@ -457,6 +503,12 @@ and is not removed. Wrap it in an element with a role, or match the whole
|
|
|
457
503
|
paragraph. The space ignored elements take up still moves the elements around
|
|
458
504
|
them.
|
|
459
505
|
|
|
506
|
+
Elements are checked against `ignoreInDiff` after the snapshot is taken. An
|
|
507
|
+
element the page removes in between, like a chat widget swapping its launcher
|
|
508
|
+
for an iframe while the page is captured, is dropped from the snapshot, because
|
|
509
|
+
it is no longer on the page. Each capture that drops one logs
|
|
510
|
+
`Ralph: N element(s) left <url> during capture ...` with the first few.
|
|
511
|
+
|
|
460
512
|
### `freezeDuringCapture` in detail
|
|
461
513
|
|
|
462
514
|
Ralph installs Playwright's clock before the app loads. Before capture it
|
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_upload = require("./upload-
|
|
2
|
+
const require_upload = require("./upload-B7tSC0NA.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");
|
|
@@ -104,6 +104,16 @@ const test = _playwright_test.test.extend({
|
|
|
104
104
|
checkOptIn();
|
|
105
105
|
return async () => {};
|
|
106
106
|
},
|
|
107
|
+
waitForNode: async (page, url, conditions) => {
|
|
108
|
+
if (conditions !== void 0) checkOptIn();
|
|
109
|
+
await page.waitForURL(url, { waitUntil: "commit" });
|
|
110
|
+
return async () => {};
|
|
111
|
+
},
|
|
112
|
+
startFlow: (name) => ({
|
|
113
|
+
name,
|
|
114
|
+
end: () => {}
|
|
115
|
+
}),
|
|
116
|
+
endFlow: () => {},
|
|
107
117
|
waitForCapture: async () => {},
|
|
108
118
|
visitAllLinks: (links, options) => require_upload.visitAllLinks(links, async () => {}, options),
|
|
109
119
|
pause: () => {},
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["loadConfigSync","base","resolveMode","path","loadConfig","Recorder","visitAllLinks"],"sources":["../src/screens.ts","../src/index.ts"],"sourcesContent":["import type {\n PlaywrightTestOptions,\n PlaywrightWorkerOptions,\n Project,\n TestInfo,\n} from '@playwright/test';\nimport type { ScreenSize } from '@ralphralphai/config';\n\nimport { loadConfigSync } from './config';\nimport type { RalphOptions } from './types';\n\ntype RalphProject = Project<\n PlaywrightTestOptions & { ralphOptions: RalphOptions },\n PlaywrightWorkerOptions\n>;\n\nexport const SCREEN_TAG_PREFIX = '@ralph-screen:';\n\nexport type RalphProjectOptions = Pick<\n RalphOptions,\n 'config' | 'configPath'\n> & {\n /** Resolves a relative `configPath`; defaults to the current directory. */\n root?: string;\n};\n\ntype ScreenMetadata = { screenSize: ScreenSize; screenSizes: ScreenSize[] };\n\nexport function screenLabel(size: ScreenSize): string {\n return size.width + 'x' + size.height;\n}\n\n/**\n * Expands `base` into one project per `screenSizes` entry of the Ralph config.\n * Each project's viewport is that screen size.\n */\nexport function ralphProjects(\n base: RalphProject = {},\n options: RalphProjectOptions = {},\n): RalphProject[] {\n const { screenSizes } = loadConfigSync(\n options,\n options.root ?? process.cwd(),\n ).config;\n if (!screenSizes.length) {\n throw new Error('The Ralph config declares no screenSizes.');\n }\n return screenSizes.map((screenSize) => {\n const label = screenLabel(screenSize);\n const ralph: ScreenMetadata = { screenSize, screenSizes };\n return {\n ...base,\n name: base.name ? base.name + ' ' + label : label,\n use: {\n ...base.use,\n viewport: { width: screenSize.width, height: screenSize.height },\n },\n metadata: { ...base.metadata, ralph },\n };\n });\n}\n\n/**\n * Skips a test whose `@ralph-screen:WxH` tags exclude the current project's\n * screen size. Untagged tests run at every size; other projects are untouched.\n */\nexport function applyScreenSelection(testInfo: TestInfo): void {\n const selected = testInfo.tags\n .filter((tag) => tag.startsWith(SCREEN_TAG_PREFIX))\n .map((tag) => tag.slice(SCREEN_TAG_PREFIX.length));\n const metadata = (testInfo.project.metadata as { ralph?: ScreenMetadata })\n .ralph;\n if (!selected.length || !metadata) {\n return;\n }\n const known = metadata.screenSizes.map(screenLabel);\n const unknown = selected.filter((label) => !known.includes(label));\n if (unknown.length) {\n throw new Error(\n 'Unknown Ralph screen size ' +\n unknown.join(', ') +\n '; the config declares ' +\n known.join(', ') +\n '.',\n );\n }\n const current = screenLabel(metadata.screenSize);\n testInfo.skip(\n !selected.includes(current),\n 'Not selected for Ralph screen size ' + current + '.',\n );\n}\n","import path from 'node:path';\n\nimport { test as base } from '@playwright/test';\nimport type {\n PlaywrightTestArgs,\n PlaywrightTestOptions,\n PlaywrightWorkerArgs,\n PlaywrightWorkerOptions,\n TestType,\n} from '@playwright/test';\n\nimport { loadConfig, resolveMode } from './config';\nimport { Recorder, REPORTER_ENV } from './recorder';\nimport { applyScreenSelection } from './screens';\nimport type { ConfigSnapshot, Ralph, RalphOptions } from './types';\nimport { visitAllLinks } from './visit_links';\n\nexport { expect, mergeTests } from '@playwright/test';\nexport type { WebRalphConfig } from '@ralphralphai/config';\nexport {\n createRawGraphBundle,\n prepareArtifactUpload,\n uploadRawGraph,\n} from './upload';\nexport type {\n PreparedUpload,\n UploadOptions,\n UploadReceipt,\n RawGraphScreenshot,\n RawGraphBundleIndex,\n} from './upload';\nexport { MANIFEST_ATTACHMENT } from './recorder';\nexport { ralphProjects, SCREEN_TAG_PREFIX } from './screens';\nexport type { RalphProjectOptions } from './screens';\nexport type { RalphReporterOptions } from './reporter';\nexport type {\n RawGraphManifest,\n RawGraph,\n RawGraphFile,\n RawPage,\n RawNode,\n RawAction,\n ActionKind,\n ActionTarget,\n ConditionsDeclaration,\n NodeConditions,\n RecordedTags,\n RecordNodeOptions,\n TagConditions,\n TagDeclaration,\n TagValue,\n ConfigSnapshot,\n PageLayout,\n Ralph,\n RalphOptions,\n TrackedElement,\n VisitAllLinksOptions,\n} from './types';\n\nexport type RalphFixtures = { ralph: Ralph; ralphOptions: RalphOptions };\n\ntype InternalFixtures = {\n _ralphScreen: void;\n _ralphRecorder: Recorder | null;\n};\ntype WorkerFixtures = { _ralphConfigs: Map<string, Promise<ConfigSnapshot>> };\n\nexport const test: TestType<\n PlaywrightTestArgs & PlaywrightTestOptions & RalphFixtures,\n PlaywrightWorkerArgs & PlaywrightWorkerOptions\n> = base.extend<RalphFixtures & InternalFixtures, WorkerFixtures>({\n ralphOptions: [{}, { option: true }],\n _ralphConfigs: [\n // Playwright requires destructuring here to discover fixture dependencies.\n // oxlint-disable-next-line no-empty-pattern\n async ({}, use) => {\n await use(new Map());\n },\n { scope: 'worker' },\n ],\n _ralphScreen: [\n // Playwright requires destructuring here to discover fixture dependencies.\n // oxlint-disable-next-line no-empty-pattern\n async ({}, use, testInfo) => {\n // Screen selection chooses which flows run, so it applies in every mode.\n applyScreenSelection(testInfo);\n await use();\n },\n { auto: true },\n ],\n _ralphRecorder: [\n // _ralphScreen is specified here so that the playwright treat that it is\n // a dependency of this fixture.\n async (\n { ralphOptions, _ralphConfigs, _ralphScreen, playwright },\n use,\n testInfo,\n ) => {\n const mode = resolveMode(ralphOptions);\n if (!testInfo.tags.includes('@ralph') || mode === 'off') {\n await use(null);\n return;\n }\n\n // A sharded run's blob reports are merged, and uploaded by the Ralph\n // reporter, in `playwright merge-reports`.\n const reported =\n process.env[REPORTER_ENV] ||\n testInfo.config.reporter.some(([name]) => name === 'blob');\n if ((mode === 'upload' || mode === 'deploy') && !reported) {\n throw new Error(\n \"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.\",\n );\n }\n\n const root = testInfo.config.configFile\n ? path.dirname(testInfo.config.configFile)\n : process.cwd();\n\n const key = JSON.stringify({\n root,\n config: ralphOptions.config,\n configPath: ralphOptions.configPath,\n });\n\n let pending = _ralphConfigs.get(key);\n if (!pending) {\n pending = loadConfig(ralphOptions, root);\n _ralphConfigs.set(key, pending);\n }\n\n const recorder = new Recorder(\n ralphOptions,\n await pending,\n testInfo,\n mode,\n playwright,\n );\n\n try {\n await use(recorder);\n } finally {\n await recorder.finish();\n }\n },\n { auto: true, timeout: 30_000 },\n ],\n ralph: async ({ _ralphRecorder, ralphOptions }, use) => {\n const checkOptIn = () => {\n if (\n resolveMode(ralphOptions) !== 'off' &&\n !ralphOptions.ignoreUntaggedCalls\n ) {\n throw new Error('Opt this test into Ralph with the @ralph tag.');\n }\n };\n\n await use(\n _ralphRecorder ?? {\n recordNode: async () => {\n checkOptIn();\n },\n setNodeConditions: async () => {\n checkOptIn();\n return async () => {};\n },\n waitForCapture: async () => {},\n visitAllLinks: (links, options) =>\n visitAllLinks(links, async () => {}, options),\n pause: () => {},\n resume: () => {},\n observe: async () => {\n checkOptIn();\n },\n },\n );\n },\n // On the context rather than per page, so popups start out reduced too.\n contextOptions: async ({ contextOptions, ralphOptions }, use, testInfo) => {\n const recording =\n testInfo.tags.includes('@ralph') && resolveMode(ralphOptions) !== 'off';\n await use(\n recording && ralphOptions.reduceMotion\n ? { ...contextOptions, reducedMotion: 'reduce' }\n : contextOptions,\n );\n },\n context: async ({ context, _ralphRecorder }, use) => {\n await _ralphRecorder?.observe(context);\n try {\n await use(context);\n } finally {\n // The base context fixture closes its pages next; pending recordings still need them alive.\n await _ralphRecorder?.stopContext(context);\n }\n },\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["loadConfigSync","base","resolveMode","path","loadConfig","Recorder","visitAllLinks"],"sources":["../src/screens.ts","../src/index.ts"],"sourcesContent":["import type {\n PlaywrightTestOptions,\n PlaywrightWorkerOptions,\n Project,\n TestInfo,\n} from '@playwright/test';\nimport type { ScreenSize } from '@ralphralphai/config';\n\nimport { loadConfigSync } from './config';\nimport type { RalphOptions } from './types';\n\ntype RalphProject = Project<\n PlaywrightTestOptions & { ralphOptions: RalphOptions },\n PlaywrightWorkerOptions\n>;\n\nexport const SCREEN_TAG_PREFIX = '@ralph-screen:';\n\nexport type RalphProjectOptions = Pick<\n RalphOptions,\n 'config' | 'configPath'\n> & {\n /** Resolves a relative `configPath`; defaults to the current directory. */\n root?: string;\n};\n\ntype ScreenMetadata = { screenSize: ScreenSize; screenSizes: ScreenSize[] };\n\nexport function screenLabel(size: ScreenSize): string {\n return size.width + 'x' + size.height;\n}\n\n/**\n * Expands `base` into one project per `screenSizes` entry of the Ralph config.\n * Each project's viewport is that screen size.\n */\nexport function ralphProjects(\n base: RalphProject = {},\n options: RalphProjectOptions = {},\n): RalphProject[] {\n const { screenSizes } = loadConfigSync(\n options,\n options.root ?? process.cwd(),\n ).config;\n if (!screenSizes.length) {\n throw new Error('The Ralph config declares no screenSizes.');\n }\n return screenSizes.map((screenSize) => {\n const label = screenLabel(screenSize);\n const ralph: ScreenMetadata = { screenSize, screenSizes };\n return {\n ...base,\n name: base.name ? base.name + ' ' + label : label,\n use: {\n ...base.use,\n viewport: { width: screenSize.width, height: screenSize.height },\n },\n metadata: { ...base.metadata, ralph },\n };\n });\n}\n\n/**\n * Skips a test whose `@ralph-screen:WxH` tags exclude the current project's\n * screen size. Untagged tests run at every size; other projects are untouched.\n */\nexport function applyScreenSelection(testInfo: TestInfo): void {\n const selected = testInfo.tags\n .filter((tag) => tag.startsWith(SCREEN_TAG_PREFIX))\n .map((tag) => tag.slice(SCREEN_TAG_PREFIX.length));\n const metadata = (testInfo.project.metadata as { ralph?: ScreenMetadata })\n .ralph;\n if (!selected.length || !metadata) {\n return;\n }\n const known = metadata.screenSizes.map(screenLabel);\n const unknown = selected.filter((label) => !known.includes(label));\n if (unknown.length) {\n throw new Error(\n 'Unknown Ralph screen size ' +\n unknown.join(', ') +\n '; the config declares ' +\n known.join(', ') +\n '.',\n );\n }\n const current = screenLabel(metadata.screenSize);\n testInfo.skip(\n !selected.includes(current),\n 'Not selected for Ralph screen size ' + current + '.',\n );\n}\n","import path from 'node:path';\n\nimport { test as base } from '@playwright/test';\nimport type {\n PlaywrightTestArgs,\n PlaywrightTestOptions,\n PlaywrightWorkerArgs,\n PlaywrightWorkerOptions,\n TestType,\n} from '@playwright/test';\n\nimport { loadConfig, resolveMode } from './config';\nimport { Recorder, REPORTER_ENV } from './recorder';\nimport { applyScreenSelection } from './screens';\nimport type { ConfigSnapshot, Ralph, RalphOptions } from './types';\nimport { visitAllLinks } from './visit_links';\n\nexport { expect, mergeTests } from '@playwright/test';\nexport type { WebRalphConfig } from '@ralphralphai/config';\nexport {\n createRawGraphBundle,\n prepareArtifactUpload,\n uploadRawGraph,\n} from './upload';\nexport type {\n PreparedUpload,\n UploadOptions,\n UploadReceipt,\n RawGraphScreenshot,\n RawGraphBundleIndex,\n} from './upload';\nexport { MANIFEST_ATTACHMENT } from './recorder';\nexport { ralphProjects, SCREEN_TAG_PREFIX } from './screens';\nexport type { RalphProjectOptions } from './screens';\nexport type { RalphReporterOptions } from './reporter';\nexport type {\n RawGraphManifest,\n RawGraph,\n RawGraphFile,\n RawPage,\n RawNode,\n RawAction,\n ActionKind,\n ActionTarget,\n ConditionsDeclaration,\n FlowRun,\n FlowRunHandle,\n NodeConditions,\n RecordedTags,\n RecordNodeOptions,\n TagConditions,\n TagDeclaration,\n TagValue,\n ConfigSnapshot,\n PageLayout,\n Ralph,\n RalphOptions,\n TrackedElement,\n VisitAllLinksOptions,\n} from './types';\n\nexport type RalphFixtures = { ralph: Ralph; ralphOptions: RalphOptions };\n\ntype InternalFixtures = {\n _ralphScreen: void;\n _ralphRecorder: Recorder | null;\n};\ntype WorkerFixtures = { _ralphConfigs: Map<string, Promise<ConfigSnapshot>> };\n\nexport const test: TestType<\n PlaywrightTestArgs & PlaywrightTestOptions & RalphFixtures,\n PlaywrightWorkerArgs & PlaywrightWorkerOptions\n> = base.extend<RalphFixtures & InternalFixtures, WorkerFixtures>({\n ralphOptions: [{}, { option: true }],\n _ralphConfigs: [\n // Playwright requires destructuring here to discover fixture dependencies.\n // oxlint-disable-next-line no-empty-pattern\n async ({}, use) => {\n await use(new Map());\n },\n { scope: 'worker' },\n ],\n _ralphScreen: [\n // Playwright requires destructuring here to discover fixture dependencies.\n // oxlint-disable-next-line no-empty-pattern\n async ({}, use, testInfo) => {\n // Screen selection chooses which flows run, so it applies in every mode.\n applyScreenSelection(testInfo);\n await use();\n },\n { auto: true },\n ],\n _ralphRecorder: [\n // _ralphScreen is specified here so that the playwright treat that it is\n // a dependency of this fixture.\n async (\n { ralphOptions, _ralphConfigs, _ralphScreen, playwright },\n use,\n testInfo,\n ) => {\n const mode = resolveMode(ralphOptions);\n if (!testInfo.tags.includes('@ralph') || mode === 'off') {\n await use(null);\n return;\n }\n\n // A sharded run's blob reports are merged, and uploaded by the Ralph\n // reporter, in `playwright merge-reports`.\n const reported =\n process.env[REPORTER_ENV] ||\n testInfo.config.reporter.some(([name]) => name === 'blob');\n if ((mode === 'upload' || mode === 'deploy') && !reported) {\n throw new Error(\n \"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.\",\n );\n }\n\n const root = testInfo.config.configFile\n ? path.dirname(testInfo.config.configFile)\n : process.cwd();\n\n const key = JSON.stringify({\n root,\n config: ralphOptions.config,\n configPath: ralphOptions.configPath,\n });\n\n let pending = _ralphConfigs.get(key);\n if (!pending) {\n pending = loadConfig(ralphOptions, root);\n _ralphConfigs.set(key, pending);\n }\n\n const recorder = new Recorder(\n ralphOptions,\n await pending,\n testInfo,\n mode,\n playwright,\n );\n\n try {\n await use(recorder);\n } finally {\n await recorder.finish();\n }\n },\n { auto: true, timeout: 30_000 },\n ],\n ralph: async ({ _ralphRecorder, ralphOptions }, use) => {\n const checkOptIn = () => {\n if (\n resolveMode(ralphOptions) !== 'off' &&\n !ralphOptions.ignoreUntaggedCalls\n ) {\n throw new Error('Opt this test into Ralph with the @ralph tag.');\n }\n };\n\n await use(\n _ralphRecorder ?? {\n recordNode: async () => {\n checkOptIn();\n },\n setNodeConditions: async () => {\n checkOptIn();\n return async () => {};\n },\n // The wait is the test's own step, so it happens unrecorded too.\n waitForNode: async (page, url, conditions) => {\n if (conditions !== undefined) {\n checkOptIn();\n }\n await page.waitForURL(url, { waitUntil: 'commit' });\n return async () => {};\n },\n // Flows only label nodes, so a helper shared with untagged tests can\n // declare them without opting those tests in.\n startFlow: (name) => ({ name, end: () => {} }),\n endFlow: () => {},\n waitForCapture: async () => {},\n visitAllLinks: (links, options) =>\n visitAllLinks(links, async () => {}, options),\n pause: () => {},\n resume: () => {},\n observe: async () => {\n checkOptIn();\n },\n },\n );\n },\n // On the context rather than per page, so popups start out reduced too.\n contextOptions: async ({ contextOptions, ralphOptions }, use, testInfo) => {\n const recording =\n testInfo.tags.includes('@ralph') && resolveMode(ralphOptions) !== 'off';\n await use(\n recording && ralphOptions.reduceMotion\n ? { ...contextOptions, reducedMotion: 'reduce' }\n : contextOptions,\n );\n },\n context: async ({ context, _ralphRecorder }, use) => {\n await _ralphRecorder?.observe(context);\n try {\n await use(context);\n } finally {\n // The base context fixture closes its pages next; pending recordings still need them alive.\n await _ralphRecorder?.stopContext(context);\n }\n },\n});\n"],"mappings":"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"}
|
package/dist/index.d.mts
CHANGED
|
@@ -68,6 +68,15 @@ type RecordNodeOptions = {
|
|
|
68
68
|
*/
|
|
69
69
|
edgeLabel?: string;
|
|
70
70
|
};
|
|
71
|
+
/** The run `startFlow` began, so a helper can end what it started. */
|
|
72
|
+
type FlowRunHandle = {
|
|
73
|
+
name: string;
|
|
74
|
+
/**
|
|
75
|
+
* Ends this run, as `endFlow(name)` would. Throws when the run has already
|
|
76
|
+
* ended, even if a new run of the same name is open.
|
|
77
|
+
*/
|
|
78
|
+
end(): void;
|
|
79
|
+
};
|
|
71
80
|
type VisitAllLinksOptions = {
|
|
72
81
|
/**
|
|
73
82
|
* Runs in each opened tab before it closes, e.g. to assert where it landed
|
|
@@ -89,6 +98,22 @@ type Ralph = {
|
|
|
89
98
|
* declaration and records again.
|
|
90
99
|
*/
|
|
91
100
|
setNodeConditions(page: Page, conditions: ConditionsDeclaration): Promise<() => Promise<void>>;
|
|
101
|
+
/**
|
|
102
|
+
* Waits for the page to reach `url`, as `page.waitForURL` does, and declares
|
|
103
|
+
* `conditions` for the node it lands on. The URL's automatic recording
|
|
104
|
+
* carries them even when it captured before this call: it is retagged
|
|
105
|
+
* rather than recorded again. Returns what `setNodeConditions` returns.
|
|
106
|
+
*/
|
|
107
|
+
waitForNode(page: Page, url: Parameters<Page['waitForURL']>[0], conditions?: ConditionsDeclaration): Promise<() => Promise<void>>;
|
|
108
|
+
/**
|
|
109
|
+
* Starts a run of the named flow, such as `Sign up`. The node most recently
|
|
110
|
+
* requested in the test is its first step, and every node requested until
|
|
111
|
+
* `endFlow` joins it, on every tab. A run still open when the test finishes
|
|
112
|
+
* ends there. Throws when a run of that name is already open.
|
|
113
|
+
*/
|
|
114
|
+
startFlow(name: string): FlowRunHandle;
|
|
115
|
+
/** Ends the open run of the named flow. Throws when none is open. */
|
|
116
|
+
endFlow(name: string): void;
|
|
92
117
|
waitForCapture(): Promise<void>;
|
|
93
118
|
/**
|
|
94
119
|
* Opens each link `links` matches in its own tab, one at a time, records
|
|
@@ -235,6 +260,15 @@ type RawPage = {
|
|
|
235
260
|
browserName?: string;
|
|
236
261
|
openerPageId?: string;
|
|
237
262
|
};
|
|
263
|
+
/** One start-to-end span of a flow within a test. */
|
|
264
|
+
type FlowRun = {
|
|
265
|
+
name: string;
|
|
266
|
+
startedAt: string;
|
|
267
|
+
endedAt: string;
|
|
268
|
+
endedBy: 'endFlow' | 'test-end';
|
|
269
|
+
/** The nodes requested while the run was open, in request order. */
|
|
270
|
+
nodeIds: string[];
|
|
271
|
+
};
|
|
238
272
|
/** One test's recordings. Its nodes link only to each other. */
|
|
239
273
|
type RawGraph = {
|
|
240
274
|
id: string;
|
|
@@ -250,6 +284,8 @@ type RawGraph = {
|
|
|
250
284
|
finishedAt: string;
|
|
251
285
|
pages: RawPage[];
|
|
252
286
|
nodes: RawNode[];
|
|
287
|
+
/** Absent when the test declared no flow. */
|
|
288
|
+
flowRuns?: FlowRun[];
|
|
253
289
|
/** The test passed and every node was recorded with stable geometry. */
|
|
254
290
|
complete: boolean;
|
|
255
291
|
};
|
|
@@ -380,5 +416,5 @@ type RalphFixtures = {
|
|
|
380
416
|
};
|
|
381
417
|
declare const test: TestType<PlaywrightTestArgs & PlaywrightTestOptions & RalphFixtures, PlaywrightWorkerArgs & PlaywrightWorkerOptions>;
|
|
382
418
|
//#endregion
|
|
383
|
-
export { type ActionKind, type ActionTarget, type ConditionsDeclaration, type ConfigSnapshot, MANIFEST_ATTACHMENT, type NodeConditions, type PageLayout, type PreparedUpload, type Ralph, RalphFixtures, type RalphOptions, type RalphProjectOptions, type RalphReporterOptions, type RawAction, type RawGraph, type RawGraphBundleIndex, type RawGraphFile, type RawGraphManifest, type RawGraphScreenshot, type RawNode, type RawPage, type RecordNodeOptions, type RecordedTags, SCREEN_TAG_PREFIX, type TagConditions, type TagDeclaration, type TagValue, type TrackedElement, type UploadOptions, type UploadReceipt, type VisitAllLinksOptions, type WebRalphConfig, createRawGraphBundle, expect, mergeTests, prepareArtifactUpload, ralphProjects, test, uploadRawGraph };
|
|
419
|
+
export { type ActionKind, type ActionTarget, type ConditionsDeclaration, type ConfigSnapshot, type FlowRun, type FlowRunHandle, MANIFEST_ATTACHMENT, type NodeConditions, type PageLayout, type PreparedUpload, type Ralph, RalphFixtures, type RalphOptions, type RalphProjectOptions, type RalphReporterOptions, type RawAction, type RawGraph, type RawGraphBundleIndex, type RawGraphFile, type RawGraphManifest, type RawGraphScreenshot, type RawNode, type RawPage, type RecordNodeOptions, type RecordedTags, SCREEN_TAG_PREFIX, type TagConditions, type TagDeclaration, type TagValue, type TrackedElement, type UploadOptions, type UploadReceipt, type VisitAllLinksOptions, type WebRalphConfig, createRawGraphBundle, expect, mergeTests, prepareArtifactUpload, ralphProjects, test, uploadRawGraph };
|
|
384
420
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as resolveMode, a as prepareArtifactUpload, f as Recorder, g as loadConfigSync, h as loadConfig, i as createRawGraphBundle, l as uploadRawGraph, p as visitAllLinks, u as MANIFEST_ATTACHMENT } from "./upload-
|
|
1
|
+
import { _ as resolveMode, a as prepareArtifactUpload, f as Recorder, g as loadConfigSync, h as loadConfig, i as createRawGraphBundle, l as uploadRawGraph, p as visitAllLinks, u as MANIFEST_ATTACHMENT } from "./upload-C_KLRH9y.mjs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { expect, mergeTests, test as test$1 } from "@playwright/test";
|
|
4
4
|
//#region src/screens.ts
|
|
@@ -102,6 +102,16 @@ const test = test$1.extend({
|
|
|
102
102
|
checkOptIn();
|
|
103
103
|
return async () => {};
|
|
104
104
|
},
|
|
105
|
+
waitForNode: async (page, url, conditions) => {
|
|
106
|
+
if (conditions !== void 0) checkOptIn();
|
|
107
|
+
await page.waitForURL(url, { waitUntil: "commit" });
|
|
108
|
+
return async () => {};
|
|
109
|
+
},
|
|
110
|
+
startFlow: (name) => ({
|
|
111
|
+
name,
|
|
112
|
+
end: () => {}
|
|
113
|
+
}),
|
|
114
|
+
endFlow: () => {},
|
|
105
115
|
waitForCapture: async () => {},
|
|
106
116
|
visitAllLinks: (links, options) => visitAllLinks(links, async () => {}, options),
|
|
107
117
|
pause: () => {},
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["base"],"sources":["../src/screens.ts","../src/index.ts"],"sourcesContent":["import type {\n PlaywrightTestOptions,\n PlaywrightWorkerOptions,\n Project,\n TestInfo,\n} from '@playwright/test';\nimport type { ScreenSize } from '@ralphralphai/config';\n\nimport { loadConfigSync } from './config';\nimport type { RalphOptions } from './types';\n\ntype RalphProject = Project<\n PlaywrightTestOptions & { ralphOptions: RalphOptions },\n PlaywrightWorkerOptions\n>;\n\nexport const SCREEN_TAG_PREFIX = '@ralph-screen:';\n\nexport type RalphProjectOptions = Pick<\n RalphOptions,\n 'config' | 'configPath'\n> & {\n /** Resolves a relative `configPath`; defaults to the current directory. */\n root?: string;\n};\n\ntype ScreenMetadata = { screenSize: ScreenSize; screenSizes: ScreenSize[] };\n\nexport function screenLabel(size: ScreenSize): string {\n return size.width + 'x' + size.height;\n}\n\n/**\n * Expands `base` into one project per `screenSizes` entry of the Ralph config.\n * Each project's viewport is that screen size.\n */\nexport function ralphProjects(\n base: RalphProject = {},\n options: RalphProjectOptions = {},\n): RalphProject[] {\n const { screenSizes } = loadConfigSync(\n options,\n options.root ?? process.cwd(),\n ).config;\n if (!screenSizes.length) {\n throw new Error('The Ralph config declares no screenSizes.');\n }\n return screenSizes.map((screenSize) => {\n const label = screenLabel(screenSize);\n const ralph: ScreenMetadata = { screenSize, screenSizes };\n return {\n ...base,\n name: base.name ? base.name + ' ' + label : label,\n use: {\n ...base.use,\n viewport: { width: screenSize.width, height: screenSize.height },\n },\n metadata: { ...base.metadata, ralph },\n };\n });\n}\n\n/**\n * Skips a test whose `@ralph-screen:WxH` tags exclude the current project's\n * screen size. Untagged tests run at every size; other projects are untouched.\n */\nexport function applyScreenSelection(testInfo: TestInfo): void {\n const selected = testInfo.tags\n .filter((tag) => tag.startsWith(SCREEN_TAG_PREFIX))\n .map((tag) => tag.slice(SCREEN_TAG_PREFIX.length));\n const metadata = (testInfo.project.metadata as { ralph?: ScreenMetadata })\n .ralph;\n if (!selected.length || !metadata) {\n return;\n }\n const known = metadata.screenSizes.map(screenLabel);\n const unknown = selected.filter((label) => !known.includes(label));\n if (unknown.length) {\n throw new Error(\n 'Unknown Ralph screen size ' +\n unknown.join(', ') +\n '; the config declares ' +\n known.join(', ') +\n '.',\n );\n }\n const current = screenLabel(metadata.screenSize);\n testInfo.skip(\n !selected.includes(current),\n 'Not selected for Ralph screen size ' + current + '.',\n );\n}\n","import path from 'node:path';\n\nimport { test as base } from '@playwright/test';\nimport type {\n PlaywrightTestArgs,\n PlaywrightTestOptions,\n PlaywrightWorkerArgs,\n PlaywrightWorkerOptions,\n TestType,\n} from '@playwright/test';\n\nimport { loadConfig, resolveMode } from './config';\nimport { Recorder, REPORTER_ENV } from './recorder';\nimport { applyScreenSelection } from './screens';\nimport type { ConfigSnapshot, Ralph, RalphOptions } from './types';\nimport { visitAllLinks } from './visit_links';\n\nexport { expect, mergeTests } from '@playwright/test';\nexport type { WebRalphConfig } from '@ralphralphai/config';\nexport {\n createRawGraphBundle,\n prepareArtifactUpload,\n uploadRawGraph,\n} from './upload';\nexport type {\n PreparedUpload,\n UploadOptions,\n UploadReceipt,\n RawGraphScreenshot,\n RawGraphBundleIndex,\n} from './upload';\nexport { MANIFEST_ATTACHMENT } from './recorder';\nexport { ralphProjects, SCREEN_TAG_PREFIX } from './screens';\nexport type { RalphProjectOptions } from './screens';\nexport type { RalphReporterOptions } from './reporter';\nexport type {\n RawGraphManifest,\n RawGraph,\n RawGraphFile,\n RawPage,\n RawNode,\n RawAction,\n ActionKind,\n ActionTarget,\n ConditionsDeclaration,\n NodeConditions,\n RecordedTags,\n RecordNodeOptions,\n TagConditions,\n TagDeclaration,\n TagValue,\n ConfigSnapshot,\n PageLayout,\n Ralph,\n RalphOptions,\n TrackedElement,\n VisitAllLinksOptions,\n} from './types';\n\nexport type RalphFixtures = { ralph: Ralph; ralphOptions: RalphOptions };\n\ntype InternalFixtures = {\n _ralphScreen: void;\n _ralphRecorder: Recorder | null;\n};\ntype WorkerFixtures = { _ralphConfigs: Map<string, Promise<ConfigSnapshot>> };\n\nexport const test: TestType<\n PlaywrightTestArgs & PlaywrightTestOptions & RalphFixtures,\n PlaywrightWorkerArgs & PlaywrightWorkerOptions\n> = base.extend<RalphFixtures & InternalFixtures, WorkerFixtures>({\n ralphOptions: [{}, { option: true }],\n _ralphConfigs: [\n // Playwright requires destructuring here to discover fixture dependencies.\n // oxlint-disable-next-line no-empty-pattern\n async ({}, use) => {\n await use(new Map());\n },\n { scope: 'worker' },\n ],\n _ralphScreen: [\n // Playwright requires destructuring here to discover fixture dependencies.\n // oxlint-disable-next-line no-empty-pattern\n async ({}, use, testInfo) => {\n // Screen selection chooses which flows run, so it applies in every mode.\n applyScreenSelection(testInfo);\n await use();\n },\n { auto: true },\n ],\n _ralphRecorder: [\n // _ralphScreen is specified here so that the playwright treat that it is\n // a dependency of this fixture.\n async (\n { ralphOptions, _ralphConfigs, _ralphScreen, playwright },\n use,\n testInfo,\n ) => {\n const mode = resolveMode(ralphOptions);\n if (!testInfo.tags.includes('@ralph') || mode === 'off') {\n await use(null);\n return;\n }\n\n // A sharded run's blob reports are merged, and uploaded by the Ralph\n // reporter, in `playwright merge-reports`.\n const reported =\n process.env[REPORTER_ENV] ||\n testInfo.config.reporter.some(([name]) => name === 'blob');\n if ((mode === 'upload' || mode === 'deploy') && !reported) {\n throw new Error(\n \"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.\",\n );\n }\n\n const root = testInfo.config.configFile\n ? path.dirname(testInfo.config.configFile)\n : process.cwd();\n\n const key = JSON.stringify({\n root,\n config: ralphOptions.config,\n configPath: ralphOptions.configPath,\n });\n\n let pending = _ralphConfigs.get(key);\n if (!pending) {\n pending = loadConfig(ralphOptions, root);\n _ralphConfigs.set(key, pending);\n }\n\n const recorder = new Recorder(\n ralphOptions,\n await pending,\n testInfo,\n mode,\n playwright,\n );\n\n try {\n await use(recorder);\n } finally {\n await recorder.finish();\n }\n },\n { auto: true, timeout: 30_000 },\n ],\n ralph: async ({ _ralphRecorder, ralphOptions }, use) => {\n const checkOptIn = () => {\n if (\n resolveMode(ralphOptions) !== 'off' &&\n !ralphOptions.ignoreUntaggedCalls\n ) {\n throw new Error('Opt this test into Ralph with the @ralph tag.');\n }\n };\n\n await use(\n _ralphRecorder ?? {\n recordNode: async () => {\n checkOptIn();\n },\n setNodeConditions: async () => {\n checkOptIn();\n return async () => {};\n },\n waitForCapture: async () => {},\n visitAllLinks: (links, options) =>\n visitAllLinks(links, async () => {}, options),\n pause: () => {},\n resume: () => {},\n observe: async () => {\n checkOptIn();\n },\n },\n );\n },\n // On the context rather than per page, so popups start out reduced too.\n contextOptions: async ({ contextOptions, ralphOptions }, use, testInfo) => {\n const recording =\n testInfo.tags.includes('@ralph') && resolveMode(ralphOptions) !== 'off';\n await use(\n recording && ralphOptions.reduceMotion\n ? { ...contextOptions, reducedMotion: 'reduce' }\n : contextOptions,\n );\n },\n context: async ({ context, _ralphRecorder }, use) => {\n await _ralphRecorder?.observe(context);\n try {\n await use(context);\n } finally {\n // The base context fixture closes its pages next; pending recordings still need them alive.\n await _ralphRecorder?.stopContext(context);\n }\n },\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["base"],"sources":["../src/screens.ts","../src/index.ts"],"sourcesContent":["import type {\n PlaywrightTestOptions,\n PlaywrightWorkerOptions,\n Project,\n TestInfo,\n} from '@playwright/test';\nimport type { ScreenSize } from '@ralphralphai/config';\n\nimport { loadConfigSync } from './config';\nimport type { RalphOptions } from './types';\n\ntype RalphProject = Project<\n PlaywrightTestOptions & { ralphOptions: RalphOptions },\n PlaywrightWorkerOptions\n>;\n\nexport const SCREEN_TAG_PREFIX = '@ralph-screen:';\n\nexport type RalphProjectOptions = Pick<\n RalphOptions,\n 'config' | 'configPath'\n> & {\n /** Resolves a relative `configPath`; defaults to the current directory. */\n root?: string;\n};\n\ntype ScreenMetadata = { screenSize: ScreenSize; screenSizes: ScreenSize[] };\n\nexport function screenLabel(size: ScreenSize): string {\n return size.width + 'x' + size.height;\n}\n\n/**\n * Expands `base` into one project per `screenSizes` entry of the Ralph config.\n * Each project's viewport is that screen size.\n */\nexport function ralphProjects(\n base: RalphProject = {},\n options: RalphProjectOptions = {},\n): RalphProject[] {\n const { screenSizes } = loadConfigSync(\n options,\n options.root ?? process.cwd(),\n ).config;\n if (!screenSizes.length) {\n throw new Error('The Ralph config declares no screenSizes.');\n }\n return screenSizes.map((screenSize) => {\n const label = screenLabel(screenSize);\n const ralph: ScreenMetadata = { screenSize, screenSizes };\n return {\n ...base,\n name: base.name ? base.name + ' ' + label : label,\n use: {\n ...base.use,\n viewport: { width: screenSize.width, height: screenSize.height },\n },\n metadata: { ...base.metadata, ralph },\n };\n });\n}\n\n/**\n * Skips a test whose `@ralph-screen:WxH` tags exclude the current project's\n * screen size. Untagged tests run at every size; other projects are untouched.\n */\nexport function applyScreenSelection(testInfo: TestInfo): void {\n const selected = testInfo.tags\n .filter((tag) => tag.startsWith(SCREEN_TAG_PREFIX))\n .map((tag) => tag.slice(SCREEN_TAG_PREFIX.length));\n const metadata = (testInfo.project.metadata as { ralph?: ScreenMetadata })\n .ralph;\n if (!selected.length || !metadata) {\n return;\n }\n const known = metadata.screenSizes.map(screenLabel);\n const unknown = selected.filter((label) => !known.includes(label));\n if (unknown.length) {\n throw new Error(\n 'Unknown Ralph screen size ' +\n unknown.join(', ') +\n '; the config declares ' +\n known.join(', ') +\n '.',\n );\n }\n const current = screenLabel(metadata.screenSize);\n testInfo.skip(\n !selected.includes(current),\n 'Not selected for Ralph screen size ' + current + '.',\n );\n}\n","import path from 'node:path';\n\nimport { test as base } from '@playwright/test';\nimport type {\n PlaywrightTestArgs,\n PlaywrightTestOptions,\n PlaywrightWorkerArgs,\n PlaywrightWorkerOptions,\n TestType,\n} from '@playwright/test';\n\nimport { loadConfig, resolveMode } from './config';\nimport { Recorder, REPORTER_ENV } from './recorder';\nimport { applyScreenSelection } from './screens';\nimport type { ConfigSnapshot, Ralph, RalphOptions } from './types';\nimport { visitAllLinks } from './visit_links';\n\nexport { expect, mergeTests } from '@playwright/test';\nexport type { WebRalphConfig } from '@ralphralphai/config';\nexport {\n createRawGraphBundle,\n prepareArtifactUpload,\n uploadRawGraph,\n} from './upload';\nexport type {\n PreparedUpload,\n UploadOptions,\n UploadReceipt,\n RawGraphScreenshot,\n RawGraphBundleIndex,\n} from './upload';\nexport { MANIFEST_ATTACHMENT } from './recorder';\nexport { ralphProjects, SCREEN_TAG_PREFIX } from './screens';\nexport type { RalphProjectOptions } from './screens';\nexport type { RalphReporterOptions } from './reporter';\nexport type {\n RawGraphManifest,\n RawGraph,\n RawGraphFile,\n RawPage,\n RawNode,\n RawAction,\n ActionKind,\n ActionTarget,\n ConditionsDeclaration,\n FlowRun,\n FlowRunHandle,\n NodeConditions,\n RecordedTags,\n RecordNodeOptions,\n TagConditions,\n TagDeclaration,\n TagValue,\n ConfigSnapshot,\n PageLayout,\n Ralph,\n RalphOptions,\n TrackedElement,\n VisitAllLinksOptions,\n} from './types';\n\nexport type RalphFixtures = { ralph: Ralph; ralphOptions: RalphOptions };\n\ntype InternalFixtures = {\n _ralphScreen: void;\n _ralphRecorder: Recorder | null;\n};\ntype WorkerFixtures = { _ralphConfigs: Map<string, Promise<ConfigSnapshot>> };\n\nexport const test: TestType<\n PlaywrightTestArgs & PlaywrightTestOptions & RalphFixtures,\n PlaywrightWorkerArgs & PlaywrightWorkerOptions\n> = base.extend<RalphFixtures & InternalFixtures, WorkerFixtures>({\n ralphOptions: [{}, { option: true }],\n _ralphConfigs: [\n // Playwright requires destructuring here to discover fixture dependencies.\n // oxlint-disable-next-line no-empty-pattern\n async ({}, use) => {\n await use(new Map());\n },\n { scope: 'worker' },\n ],\n _ralphScreen: [\n // Playwright requires destructuring here to discover fixture dependencies.\n // oxlint-disable-next-line no-empty-pattern\n async ({}, use, testInfo) => {\n // Screen selection chooses which flows run, so it applies in every mode.\n applyScreenSelection(testInfo);\n await use();\n },\n { auto: true },\n ],\n _ralphRecorder: [\n // _ralphScreen is specified here so that the playwright treat that it is\n // a dependency of this fixture.\n async (\n { ralphOptions, _ralphConfigs, _ralphScreen, playwright },\n use,\n testInfo,\n ) => {\n const mode = resolveMode(ralphOptions);\n if (!testInfo.tags.includes('@ralph') || mode === 'off') {\n await use(null);\n return;\n }\n\n // A sharded run's blob reports are merged, and uploaded by the Ralph\n // reporter, in `playwright merge-reports`.\n const reported =\n process.env[REPORTER_ENV] ||\n testInfo.config.reporter.some(([name]) => name === 'blob');\n if ((mode === 'upload' || mode === 'deploy') && !reported) {\n throw new Error(\n \"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.\",\n );\n }\n\n const root = testInfo.config.configFile\n ? path.dirname(testInfo.config.configFile)\n : process.cwd();\n\n const key = JSON.stringify({\n root,\n config: ralphOptions.config,\n configPath: ralphOptions.configPath,\n });\n\n let pending = _ralphConfigs.get(key);\n if (!pending) {\n pending = loadConfig(ralphOptions, root);\n _ralphConfigs.set(key, pending);\n }\n\n const recorder = new Recorder(\n ralphOptions,\n await pending,\n testInfo,\n mode,\n playwright,\n );\n\n try {\n await use(recorder);\n } finally {\n await recorder.finish();\n }\n },\n { auto: true, timeout: 30_000 },\n ],\n ralph: async ({ _ralphRecorder, ralphOptions }, use) => {\n const checkOptIn = () => {\n if (\n resolveMode(ralphOptions) !== 'off' &&\n !ralphOptions.ignoreUntaggedCalls\n ) {\n throw new Error('Opt this test into Ralph with the @ralph tag.');\n }\n };\n\n await use(\n _ralphRecorder ?? {\n recordNode: async () => {\n checkOptIn();\n },\n setNodeConditions: async () => {\n checkOptIn();\n return async () => {};\n },\n // The wait is the test's own step, so it happens unrecorded too.\n waitForNode: async (page, url, conditions) => {\n if (conditions !== undefined) {\n checkOptIn();\n }\n await page.waitForURL(url, { waitUntil: 'commit' });\n return async () => {};\n },\n // Flows only label nodes, so a helper shared with untagged tests can\n // declare them without opting those tests in.\n startFlow: (name) => ({ name, end: () => {} }),\n endFlow: () => {},\n waitForCapture: async () => {},\n visitAllLinks: (links, options) =>\n visitAllLinks(links, async () => {}, options),\n pause: () => {},\n resume: () => {},\n observe: async () => {\n checkOptIn();\n },\n },\n );\n },\n // On the context rather than per page, so popups start out reduced too.\n contextOptions: async ({ contextOptions, ralphOptions }, use, testInfo) => {\n const recording =\n testInfo.tags.includes('@ralph') && resolveMode(ralphOptions) !== 'off';\n await use(\n recording && ralphOptions.reduceMotion\n ? { ...contextOptions, reducedMotion: 'reduce' }\n : contextOptions,\n );\n },\n context: async ({ context, _ralphRecorder }, use) => {\n await _ralphRecorder?.observe(context);\n try {\n await use(context);\n } finally {\n // The base context fixture closes its pages next; pending recordings still need them alive.\n await _ralphRecorder?.stopContext(context);\n }\n },\n});\n"],"mappings":";;;;AAgBA,MAAa,oBAAoB;AAYjC,SAAgB,YAAY,MAA0B;CACpD,OAAO,KAAK,QAAQ,MAAM,KAAK;AACjC;;;;;AAMA,SAAgB,cACd,OAAqB,CAAC,GACtB,UAA+B,CAAC,GAChB;CAChB,MAAM,EAAE,gBAAgB,eACtB,SACA,QAAQ,QAAQ,QAAQ,IAAI,CAC9B,CAAC,CAAC;CACF,IAAI,CAAC,YAAY,QACf,MAAM,IAAI,MAAM,2CAA2C;CAE7D,OAAO,YAAY,KAAK,eAAe;EACrC,MAAM,QAAQ,YAAY,UAAU;EACpC,MAAM,QAAwB;GAAE;GAAY;EAAY;EACxD,OAAO;GACL,GAAG;GACH,MAAM,KAAK,OAAO,KAAK,OAAO,MAAM,QAAQ;GAC5C,KAAK;IACH,GAAG,KAAK;IACR,UAAU;KAAE,OAAO,WAAW;KAAO,QAAQ,WAAW;IAAO;GACjE;GACA,UAAU;IAAE,GAAG,KAAK;IAAU;GAAM;EACtC;CACF,CAAC;AACH;;;;;AAMA,SAAgB,qBAAqB,UAA0B;CAC7D,MAAM,WAAW,SAAS,KACvB,QAAQ,QAAQ,IAAI,WAAW,iBAAiB,CAAC,CAAC,CAClD,KAAK,QAAQ,IAAI,MAAM,EAAwB,CAAC;CACnD,MAAM,WAAY,SAAS,QAAQ,SAChC;CACH,IAAI,CAAC,SAAS,UAAU,CAAC,UACvB;CAEF,MAAM,QAAQ,SAAS,YAAY,IAAI,WAAW;CAClD,MAAM,UAAU,SAAS,QAAQ,UAAU,CAAC,MAAM,SAAS,KAAK,CAAC;CACjE,IAAI,QAAQ,QACV,MAAM,IAAI,MACR,+BACE,QAAQ,KAAK,IAAI,IACjB,2BACA,MAAM,KAAK,IAAI,IACf,GACJ;CAEF,MAAM,UAAU,YAAY,SAAS,UAAU;CAC/C,SAAS,KACP,CAAC,SAAS,SAAS,OAAO,GAC1B,wCAAwC,UAAU,GACpD;AACF;;;ACtBA,MAAa,OAGTA,OAAK,OAAyD;CAChE,cAAc,CAAC,CAAC,GAAG,EAAE,QAAQ,KAAK,CAAC;CACnC,eAAe,CAGb,OAAO,IAAI,QAAQ;EACjB,MAAM,oBAAI,IAAI,IAAI,CAAC;CACrB,GACA,EAAE,OAAO,SAAS,CACpB;CACA,cAAc,CAGZ,OAAO,IAAI,KAAK,aAAa;EAE3B,qBAAqB,QAAQ;EAC7B,MAAM,IAAI;CACZ,GACA,EAAE,MAAM,KAAK,CACf;CACA,gBAAgB,CAGd,OACE,EAAE,cAAc,eAAe,cAAc,cAC7C,KACA,aACG;EACH,MAAM,OAAO,YAAY,YAAY;EACrC,IAAI,CAAC,SAAS,KAAK,SAAS,QAAQ,KAAK,SAAS,OAAO;GACvD,MAAM,IAAI,IAAI;GACd;EACF;EAIA,MAAM,WACJ,QAAQ,IAAA,gCACR,SAAS,OAAO,SAAS,MAAM,CAAC,UAAU,SAAS,MAAM;EAC3D,KAAK,SAAS,YAAY,SAAS,aAAa,CAAC,UAC/C,MAAM,IAAI,MACR,mMACF;EAGF,MAAM,OAAO,SAAS,OAAO,aACzB,KAAK,QAAQ,SAAS,OAAO,UAAU,IACvC,QAAQ,IAAI;EAEhB,MAAM,MAAM,KAAK,UAAU;GACzB;GACA,QAAQ,aAAa;GACrB,YAAY,aAAa;EAC3B,CAAC;EAED,IAAI,UAAU,cAAc,IAAI,GAAG;EACnC,IAAI,CAAC,SAAS;GACZ,UAAU,WAAW,cAAc,IAAI;GACvC,cAAc,IAAI,KAAK,OAAO;EAChC;EAEA,MAAM,WAAW,IAAI,SACnB,cACA,MAAM,SACN,UACA,MACA,UACF;EAEA,IAAI;GACF,MAAM,IAAI,QAAQ;EACpB,UAAU;GACR,MAAM,SAAS,OAAO;EACxB;CACF,GACA;EAAE,MAAM;EAAM,SAAS;CAAO,CAChC;CACA,OAAO,OAAO,EAAE,gBAAgB,gBAAgB,QAAQ;EACtD,MAAM,mBAAmB;GACvB,IACE,YAAY,YAAY,MAAM,SAC9B,CAAC,aAAa,qBAEd,MAAM,IAAI,MAAM,+CAA+C;EAEnE;EAEA,MAAM,IACJ,kBAAkB;GAChB,YAAY,YAAY;IACtB,WAAW;GACb;GACA,mBAAmB,YAAY;IAC7B,WAAW;IACX,OAAO,YAAY,CAAC;GACtB;GAEA,aAAa,OAAO,MAAM,KAAK,eAAe;IAC5C,IAAI,eAAe,KAAA,GACjB,WAAW;IAEb,MAAM,KAAK,WAAW,KAAK,EAAE,WAAW,SAAS,CAAC;IAClD,OAAO,YAAY,CAAC;GACtB;GAGA,YAAY,UAAU;IAAE;IAAM,WAAW,CAAC;GAAE;GAC5C,eAAe,CAAC;GAChB,gBAAgB,YAAY,CAAC;GAC7B,gBAAgB,OAAO,YACrB,cAAc,OAAO,YAAY,CAAC,GAAG,OAAO;GAC9C,aAAa,CAAC;GACd,cAAc,CAAC;GACf,SAAS,YAAY;IACnB,WAAW;GACb;EACF,CACF;CACF;CAEA,gBAAgB,OAAO,EAAE,gBAAgB,gBAAgB,KAAK,aAAa;EAGzE,MAAM,IADJ,SAAS,KAAK,SAAS,QAAQ,KAAK,YAAY,YAAY,MAAM,SAErD,aAAa,eACtB;GAAE,GAAG;GAAgB,eAAe;EAAS,IAC7C,cACN;CACF;CACA,SAAS,OAAO,EAAE,SAAS,kBAAkB,QAAQ;EACnD,MAAM,gBAAgB,QAAQ,OAAO;EACrC,IAAI;GACF,MAAM,IAAI,OAAO;EACnB,UAAU;GAER,MAAM,gBAAgB,YAAY,OAAO;EAC3C;CACF;AACF,CAAC"}
|
package/dist/reporter.cjs
CHANGED
package/dist/reporter.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as resolveMode, a as prepareArtifactUpload, c as resultLink, d as REPORTER_ENV, l as uploadRawGraph, m as version, n as bundleImagePath, o as registerDeployment, r as bundleSnapshotPath, s as resolveUploadOptions, t as RAW_GRAPH_BUNDLE_INDEX, u as MANIFEST_ATTACHMENT } from "./upload-
|
|
1
|
+
import { _ as resolveMode, a as prepareArtifactUpload, c as resultLink, d as REPORTER_ENV, l as uploadRawGraph, m as version, n as bundleImagePath, o as registerDeployment, r as bundleSnapshotPath, s as resolveUploadOptions, t as RAW_GRAPH_BUNDLE_INDEX, u as MANIFEST_ATTACHMENT } from "./upload-C_KLRH9y.mjs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
@@ -70,7 +70,7 @@ function snapshot(value) {
|
|
|
70
70
|
}
|
|
71
71
|
//#endregion
|
|
72
72
|
//#region package.json
|
|
73
|
-
var version = "2.
|
|
73
|
+
var version = "2.11.0";
|
|
74
74
|
//#endregion
|
|
75
75
|
//#region src/actions.ts
|
|
76
76
|
const BINDING = "__ralphAction";
|
|
@@ -689,7 +689,12 @@ async function captureAriaSnapshot(page, { viewport, scroll }, ignore = []) {
|
|
|
689
689
|
});
|
|
690
690
|
const ignoreSelector = ignore.length === 0 ? void 0 : ignore.join(", ");
|
|
691
691
|
const facts = /* @__PURE__ */ new Map();
|
|
692
|
-
const
|
|
692
|
+
const detached = [];
|
|
693
|
+
const normalized = (await Promise.all(visible.map((node) => readTree(page, node, ignoreSelector, facts, detached)))).filter((node) => node !== void 0);
|
|
694
|
+
if (detached.length > 0) {
|
|
695
|
+
const listed = detached.slice(0, 5).map(({ role, name }) => name ? `${role} "${name}"` : role).join(", ");
|
|
696
|
+
console.warn(`Ralph: ${detached.length} element(s) left ${page.url()} during capture and were dropped from its aria snapshot: ${listed}${detached.length > 5 ? ", ..." : ""}`);
|
|
697
|
+
}
|
|
693
698
|
const described = await Promise.all(normalized.map((node) => describe(page, node, {}, false, facts)));
|
|
694
699
|
const json = Buffer.from(JSON.stringify(fillMissingRefs(described)));
|
|
695
700
|
return json.byteLength > 4194304 ? void 0 : json;
|
|
@@ -904,8 +909,11 @@ function fillMissingRefs(nodes) {
|
|
|
904
909
|
return nodes.map(fill);
|
|
905
910
|
}
|
|
906
911
|
async function readElement(page, ref, isImage, ignoreSelector) {
|
|
912
|
+
const locator = page.locator(`aria-ref=${ref}`);
|
|
913
|
+
const isDetached = async () => await locator.count().catch(() => 0) === 0;
|
|
907
914
|
try {
|
|
908
|
-
|
|
915
|
+
if (await isDetached()) return "detached";
|
|
916
|
+
return await locator.evaluate((element, { properties, isImage, dynamicAttribute, ignoreSelector }) => {
|
|
909
917
|
const computed = getComputedStyle(element);
|
|
910
918
|
const typed = "computedStyleMap" in element ? element.computedStyleMap() : void 0;
|
|
911
919
|
const style = Object.fromEntries(properties.map((property) => [property, property.startsWith("margin-") && typed?.get(property)?.toString() === "auto" ? "auto" : computed.getPropertyValue(property)]));
|
|
@@ -950,7 +958,7 @@ async function readElement(page, ref, isImage, ignoreSelector) {
|
|
|
950
958
|
ignoreSelector
|
|
951
959
|
}, { timeout: ELEMENT_LOOKUP_TIMEOUT_MS });
|
|
952
960
|
} catch {
|
|
953
|
-
return;
|
|
961
|
+
return await isDetached() ? "detached" : void 0;
|
|
954
962
|
}
|
|
955
963
|
}
|
|
956
964
|
async function hashImage(page, image) {
|
|
@@ -969,12 +977,16 @@ function distinctStyle(style, inherited) {
|
|
|
969
977
|
});
|
|
970
978
|
return distinct.length === 0 ? void 0 : Object.fromEntries(distinct);
|
|
971
979
|
}
|
|
972
|
-
async function readTree(page, child, ignoreSelector, factsByRef) {
|
|
980
|
+
async function readTree(page, child, ignoreSelector, factsByRef, detached) {
|
|
973
981
|
if (typeof child === "string") return child;
|
|
974
982
|
const facts = child.ref === void 0 ? void 0 : await readElement(page, child.ref, child.role === "img", ignoreSelector);
|
|
983
|
+
if (facts === "detached") {
|
|
984
|
+
detached.push(child);
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
975
987
|
if (facts?.ignored) return;
|
|
976
988
|
if (child.ref !== void 0 && facts !== void 0) factsByRef.set(child.ref, facts);
|
|
977
|
-
const children = child.children === void 0 ? void 0 : (await Promise.all(child.children.map((grandchild) => readTree(page, grandchild, ignoreSelector, factsByRef)))).filter((grandchild) => grandchild !== void 0);
|
|
989
|
+
const children = child.children === void 0 ? void 0 : (await Promise.all(child.children.map((grandchild) => readTree(page, grandchild, ignoreSelector, factsByRef, detached)))).filter((grandchild) => grandchild !== void 0);
|
|
978
990
|
if (children !== void 0) {
|
|
979
991
|
if (children.length === 0 && (child.ref === void 0 || child.role === "generic" && !child.name && (child.children?.length ?? 0) > 0)) return;
|
|
980
992
|
const only = loneChild(child, children);
|
|
@@ -1229,6 +1241,71 @@ function nodeKey(conditions) {
|
|
|
1229
1241
|
return JSON.stringify([sorted(conditions.visitorTags), sorted(conditions.pageTags)]);
|
|
1230
1242
|
}
|
|
1231
1243
|
//#endregion
|
|
1244
|
+
//#region src/flows.ts
|
|
1245
|
+
const MAX_FLOW_NAME_LENGTH = 64;
|
|
1246
|
+
var FlowRuns = class {
|
|
1247
|
+
runs = [];
|
|
1248
|
+
retainedNodeIds = /* @__PURE__ */ new Map();
|
|
1249
|
+
/** `currentNodeId` is the node on screen, the run's first step. */
|
|
1250
|
+
start(name, currentNodeId) {
|
|
1251
|
+
const flowName = flowNameOf(name);
|
|
1252
|
+
if (this.openRun(flowName)) throw new Error(`Ralph flow "${flowName}" is already open.`);
|
|
1253
|
+
const run = {
|
|
1254
|
+
name: flowName,
|
|
1255
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1256
|
+
nodeIds: currentNodeId === void 0 ? [] : [currentNodeId]
|
|
1257
|
+
};
|
|
1258
|
+
this.runs.push(run);
|
|
1259
|
+
return {
|
|
1260
|
+
name: flowName,
|
|
1261
|
+
end: () => {
|
|
1262
|
+
if (run.ended) throw new Error(`This run of Ralph flow "${flowName}" has already ended.`);
|
|
1263
|
+
endRun(run, "endFlow");
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
end(name) {
|
|
1268
|
+
const flowName = flowNameOf(name);
|
|
1269
|
+
const run = this.openRun(flowName);
|
|
1270
|
+
if (!run) throw new Error(`Ralph flow "${flowName}" is not open.`);
|
|
1271
|
+
endRun(run, "endFlow");
|
|
1272
|
+
}
|
|
1273
|
+
nodeRequested(nodeId) {
|
|
1274
|
+
for (const run of this.runs) if (!run.ended) run.nodeIds.push(nodeId);
|
|
1275
|
+
}
|
|
1276
|
+
/** A retake or unchanged condition update kept an existing node's ID. */
|
|
1277
|
+
nodeReused(requestId, retainedNodeId) {
|
|
1278
|
+
this.retainedNodeIds.set(requestId, retainedNodeId);
|
|
1279
|
+
}
|
|
1280
|
+
/** Ends the runs still open, as the test has, and returns every run. */
|
|
1281
|
+
finish() {
|
|
1282
|
+
return this.runs.map((run) => {
|
|
1283
|
+
if (!run.ended) endRun(run, "test-end");
|
|
1284
|
+
return {
|
|
1285
|
+
name: run.name,
|
|
1286
|
+
startedAt: run.startedAt,
|
|
1287
|
+
endedAt: run.ended.at,
|
|
1288
|
+
endedBy: run.ended.by,
|
|
1289
|
+
nodeIds: run.nodeIds.map((nodeId) => this.retainedNodeIds.get(nodeId) ?? nodeId)
|
|
1290
|
+
};
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
openRun(name) {
|
|
1294
|
+
return this.runs.find((run) => run.name === name && !run.ended);
|
|
1295
|
+
}
|
|
1296
|
+
};
|
|
1297
|
+
function endRun(run, by) {
|
|
1298
|
+
run.ended = {
|
|
1299
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1300
|
+
by
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
function flowNameOf(name) {
|
|
1304
|
+
const trimmed = name.trim();
|
|
1305
|
+
if (!trimmed || trimmed.length > MAX_FLOW_NAME_LENGTH) throw new Error(`A Ralph flow name must be 1 to ${MAX_FLOW_NAME_LENGTH} characters.`);
|
|
1306
|
+
return trimmed;
|
|
1307
|
+
}
|
|
1308
|
+
//#endregion
|
|
1232
1309
|
//#region src/image.ts
|
|
1233
1310
|
/**
|
|
1234
1311
|
* Matches the crawler's full-page screenshots: lossless measured larger than this
|
|
@@ -1455,6 +1532,7 @@ var Recorder = class {
|
|
|
1455
1532
|
*/
|
|
1456
1533
|
order = 0;
|
|
1457
1534
|
requestOrder = /* @__PURE__ */ new Map();
|
|
1535
|
+
flows = new FlowRuns();
|
|
1458
1536
|
actions = new ActionLog(() => ++this.order, () => !this.paused && !this.closed, (page) => this.pages.get(page)?.info.pageId, () => [...this.pages.values()].filter((state) => !state.page.isClosed()).map((state) => state.info.pageId));
|
|
1459
1537
|
constructor(options, config, testInfo, mode, playwright) {
|
|
1460
1538
|
this.options = options;
|
|
@@ -1499,9 +1577,26 @@ var Recorder = class {
|
|
|
1499
1577
|
const key = Symbol();
|
|
1500
1578
|
state.declared.set(key, declaredConditions(declaration));
|
|
1501
1579
|
await this.recordConditionChange(state);
|
|
1502
|
-
return
|
|
1503
|
-
|
|
1504
|
-
|
|
1580
|
+
return this.undoDeclaration(state, key);
|
|
1581
|
+
}
|
|
1582
|
+
async waitForNode(page, url, declaration = {}) {
|
|
1583
|
+
await this.observe(page.context());
|
|
1584
|
+
const state = this.pages.get(page);
|
|
1585
|
+
if (!state) throw new Error("Cannot wait on a closed browser page.");
|
|
1586
|
+
await page.waitForURL(url, { waitUntil: "commit" });
|
|
1587
|
+
this.urlChanged(state);
|
|
1588
|
+
const generation = state.generation;
|
|
1589
|
+
const key = Symbol();
|
|
1590
|
+
state.declared.set(key, declaredConditions(declaration));
|
|
1591
|
+
await state.tail;
|
|
1592
|
+
if (!this.paused && state.generation === generation && !this.retagVisitNode(state)) await this.recordConditionChange(state);
|
|
1593
|
+
return this.undoDeclaration(state, key);
|
|
1594
|
+
}
|
|
1595
|
+
startFlow(name) {
|
|
1596
|
+
return this.flows.start(name, this.lastRequestedNodeId);
|
|
1597
|
+
}
|
|
1598
|
+
endFlow(name) {
|
|
1599
|
+
this.flows.end(name);
|
|
1505
1600
|
}
|
|
1506
1601
|
/**
|
|
1507
1602
|
* Stops requesting nodes, automatic or explicit, until `resume`. Recordings
|
|
@@ -1546,7 +1641,7 @@ var Recorder = class {
|
|
|
1546
1641
|
linkPreviousNodes(nodes, (pageId) => this.pageInfo(pageId)?.openerPageId, (nodeId) => this.requestedAtMs.get(nodeId) ?? 0, (pageId) => this.pageState(pageId)?.pauseAnchor);
|
|
1547
1642
|
this.attachActions(nodes);
|
|
1548
1643
|
const failures = nodes.filter((item) => item.status === "uncaptured" && item.reason === "error");
|
|
1549
|
-
await this.writeRawGraph(this.buildRawGraph(nodes, failures.length));
|
|
1644
|
+
await this.writeRawGraph(this.buildRawGraph(nodes, this.flows.finish(), failures.length));
|
|
1550
1645
|
const mismatched = nodes.filter((item) => item.tagMismatches?.length);
|
|
1551
1646
|
if (this.options.strictNodeTags && mismatched.length && this.testInfo.status === "passed") throw new Error("Ralph: the app reported tags outside what the test declared, on " + mismatched.map((item) => item.url + " (" + item.tagMismatches.join(", ") + ")").join("; ") + ".");
|
|
1552
1647
|
if (failures.length && this.testInfo.status === "passed") throw new Error("Ralph could not record " + failures.length + " page state(s). See the ralph-raw-graph attachment.");
|
|
@@ -1590,11 +1685,42 @@ var Recorder = class {
|
|
|
1590
1685
|
state.generation++;
|
|
1591
1686
|
state.lastUrl = url;
|
|
1592
1687
|
state.declared.clear();
|
|
1688
|
+
state.visitNodeId = void 0;
|
|
1593
1689
|
if (!this.paused && isHttpUrl(url)) this.enqueueCreateNodeRequest(state, "url-change").catch(() => void 0);
|
|
1594
1690
|
}
|
|
1595
1691
|
async recordConditionChange(state) {
|
|
1596
1692
|
if (!this.paused && isHttpUrl(state.page.url())) await this.enqueueCreateNodeRequest(state, "condition-change");
|
|
1597
1693
|
}
|
|
1694
|
+
undoDeclaration(state, key) {
|
|
1695
|
+
return async () => {
|
|
1696
|
+
if (state.declared.delete(key)) await this.recordConditionChange(state);
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
/**
|
|
1700
|
+
* Gives the URL's automatic capture the conditions declared since. They
|
|
1701
|
+
* describe the visit from its start, and declarations change no app state,
|
|
1702
|
+
* so the screenshot already taken stands for the declared node.
|
|
1703
|
+
*/
|
|
1704
|
+
retagVisitNode(state) {
|
|
1705
|
+
const nodeId = state.visitNodeId;
|
|
1706
|
+
if (!nodeId || state.lastRecorded?.nodeId !== nodeId) return false;
|
|
1707
|
+
const index = this.nodes.findIndex((item) => item.nodeId === nodeId);
|
|
1708
|
+
const visit = this.nodes[index];
|
|
1709
|
+
if (visit?.status !== "recorded") return false;
|
|
1710
|
+
const conditions = this.declaredConditions(state);
|
|
1711
|
+
const mismatches = mismatchesOf(conditions, visit.reportedTags);
|
|
1712
|
+
const { conditions: _conditions, tagMismatches: _tagMismatches, ...rest } = visit;
|
|
1713
|
+
this.nodes[index] = {
|
|
1714
|
+
...rest,
|
|
1715
|
+
...withConditions(conditions),
|
|
1716
|
+
...mismatches.length > 0 ? { tagMismatches: mismatches } : {}
|
|
1717
|
+
};
|
|
1718
|
+
state.lastRecorded = {
|
|
1719
|
+
...state.lastRecorded,
|
|
1720
|
+
node: nodeKey(conditions)
|
|
1721
|
+
};
|
|
1722
|
+
return true;
|
|
1723
|
+
}
|
|
1598
1724
|
declaredConditions(state) {
|
|
1599
1725
|
return mergeConditions([...state.declared.values()]);
|
|
1600
1726
|
}
|
|
@@ -1619,6 +1745,7 @@ var Recorder = class {
|
|
|
1619
1745
|
this.requestedAtMs.set(request.nodeId, Date.now());
|
|
1620
1746
|
this.requestOrder.set(request.nodeId, ++this.order);
|
|
1621
1747
|
this.lastRequestedNodeId = request.nodeId;
|
|
1748
|
+
this.flows.nodeRequested(request.nodeId);
|
|
1622
1749
|
const generation = state.generation;
|
|
1623
1750
|
const task = state.tail.then(() => this.createNode(state, generation, request));
|
|
1624
1751
|
state.tail = task.catch(() => void 0);
|
|
@@ -1651,7 +1778,10 @@ var Recorder = class {
|
|
|
1651
1778
|
const mismatches = mismatchesOf(conditions, reported);
|
|
1652
1779
|
const node = nodeKey(conditions);
|
|
1653
1780
|
const sameNode = state.lastRecorded?.url === request.url && state.lastRecorded.node === node;
|
|
1654
|
-
if (sameNode && request.trigger !== "explicit")
|
|
1781
|
+
if (sameNode && request.trigger !== "explicit") {
|
|
1782
|
+
this.flows.nodeReused(request.nodeId, state.lastRecorded.nodeId);
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1655
1785
|
if (reported === void 0 && compactConditions(conditions) !== void 0 && !warnedNoTracker) {
|
|
1656
1786
|
warnedNoTracker = true;
|
|
1657
1787
|
console.warn("Ralph: this page runs no tracker, so the node conditions the test declares cannot be checked against the tags the app reports. Production events must carry the same tags, or these nodes will match no visitors.");
|
|
@@ -1698,6 +1828,7 @@ var Recorder = class {
|
|
|
1698
1828
|
node,
|
|
1699
1829
|
nodeId: request.nodeId
|
|
1700
1830
|
};
|
|
1831
|
+
if (request.trigger === "url-change" && state.generation === generation) state.visitNodeId = request.nodeId;
|
|
1701
1832
|
} else {
|
|
1702
1833
|
const { nodeId, sequence, requestedAt, name, edgeLabel } = this.nodes[retaken];
|
|
1703
1834
|
this.nodes[retaken] = {
|
|
@@ -1708,6 +1839,7 @@ var Recorder = class {
|
|
|
1708
1839
|
sequence,
|
|
1709
1840
|
requestedAt
|
|
1710
1841
|
};
|
|
1842
|
+
this.flows.nodeReused(request.nodeId, nodeId);
|
|
1711
1843
|
}
|
|
1712
1844
|
} catch (error) {
|
|
1713
1845
|
this.nodes.push({
|
|
@@ -1787,7 +1919,7 @@ var Recorder = class {
|
|
|
1787
1919
|
pageInfo(pageId) {
|
|
1788
1920
|
return this.pageState(pageId)?.info;
|
|
1789
1921
|
}
|
|
1790
|
-
buildRawGraph(nodes, failureCount) {
|
|
1922
|
+
buildRawGraph(nodes, flowRuns, failureCount) {
|
|
1791
1923
|
const { testInfo } = this;
|
|
1792
1924
|
const testPassed = testInfo.status === "passed";
|
|
1793
1925
|
return {
|
|
@@ -1815,6 +1947,7 @@ var Recorder = class {
|
|
|
1815
1947
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1816
1948
|
pages: [...this.pages.values()].map((state) => state.info),
|
|
1817
1949
|
nodes,
|
|
1950
|
+
...flowRuns.length ? { flowRuns } : {},
|
|
1818
1951
|
complete: testPassed && nodes.length > 0 && nodes.every((item) => item.status === "recorded" && item.geometryStable)
|
|
1819
1952
|
}
|
|
1820
1953
|
};
|
|
@@ -2214,4 +2347,4 @@ Object.defineProperty(exports, "visitAllLinks", {
|
|
|
2214
2347
|
}
|
|
2215
2348
|
});
|
|
2216
2349
|
|
|
2217
|
-
//# sourceMappingURL=upload-
|
|
2350
|
+
//# sourceMappingURL=upload-B7tSC0NA.cjs.map
|