@ralphralphai/playwright 1.7.0 → 1.8.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
@@ -176,6 +176,22 @@ await ralph.waitForCapture();
176
176
  await context.close();
177
177
  ```
178
178
 
179
+ ### What happened between two nodes
180
+
181
+ The Playwright actions a test takes on the way from one node to the next are
182
+ recorded on the edge between them: clicks, hovers, fills, key presses,
183
+ selects, uploads, drags, and `goto` / back / forward / reload. Each one keeps
184
+ the element it landed on (its role, name, and `data-track-id`) and where that
185
+ element was on the earlier node's screenshot, which Ralph shows as, e.g.,
186
+ `Click button "Checkout"`.
187
+
188
+ What was typed into a password or one-time-code field is never recorded.
189
+ Actions taken while recording is paused are left out.
190
+
191
+ Pass an `edgeLabel` to describe the step in your own words, e.g.
192
+ `ralph.recordNode(page, { name: 'Done', edgeLabel: 'Sign in' })`. A later call
193
+ without one keeps it.
194
+
179
195
  ### When recording fails
180
196
 
181
197
  - A failed `recordNode` call rejects.
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_upload = require("./upload-ZMnTCjs2.cjs");
2
+ const require_upload = require("./upload-BvAi7mMY.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");
@@ -63,7 +63,7 @@ const test = _playwright_test.test.extend({
63
63
  applyScreenSelection(testInfo);
64
64
  await use();
65
65
  }, { auto: true }],
66
- _ralphRecorder: [async ({ ralphOptions, _ralphConfigs, _ralphScreen }, use, testInfo) => {
66
+ _ralphRecorder: [async ({ ralphOptions, _ralphConfigs, _ralphScreen, playwright }, use, testInfo) => {
67
67
  const mode = require_upload.resolveMode(ralphOptions);
68
68
  if (!testInfo.tags.includes("@ralph") || mode === "off") {
69
69
  await use(null);
@@ -82,7 +82,7 @@ const test = _playwright_test.test.extend({
82
82
  pending = require_upload.loadConfig(ralphOptions, root);
83
83
  _ralphConfigs.set(key, pending);
84
84
  }
85
- const recorder = new require_upload.Recorder(ralphOptions, await pending, testInfo, mode);
85
+ const recorder = new require_upload.Recorder(ralphOptions, await pending, testInfo, mode, playwright);
86
86
  try {
87
87
  await use(recorder);
88
88
  } finally {
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["loadConfigSync","base","resolveMode","path","loadConfig","Recorder"],"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';\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 ConditionsDeclaration,\n NodeConditions,\n RecordedTags,\n RecordNodeOptions,\n TagConditions,\n TagDeclaration,\n TagValue,\n ConfigSnapshot,\n PageLayout,\n Ralph,\n RalphOptions,\n TrackedElement,\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 ({ ralphOptions, _ralphConfigs, _ralphScreen }, use, testInfo) => {\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' && !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 );\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 pause: () => {},\n resume: () => {},\n observe: () => {\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 _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"],"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';\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} 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' && !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 pause: () => {},\n resume: () => {},\n observe: () => {\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 _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
@@ -49,6 +49,12 @@ type RecordNodeOptions = {
49
49
  * graph. A retake without one keeps the name already given.
50
50
  */
51
51
  name?: string;
52
+ /**
53
+ * How the test got here from the node before, such as `Add to cart`, shown
54
+ * on the edge into this node beside the actions recorded on it. A retake
55
+ * without one keeps the label already given.
56
+ */
57
+ edgeLabel?: string;
52
58
  };
53
59
  type Ralph = {
54
60
  /**
@@ -76,6 +82,39 @@ type TrackedElement = {
76
82
  width: number;
77
83
  height: number;
78
84
  };
85
+ type ActionKind = 'click' | 'dblclick' | 'tap' | 'hover' | 'check' | 'uncheck' | 'fill' | 'type' | 'press' | 'selectOption' | 'setInputFiles' | 'dragTo' | 'goto' | 'goBack' | 'goForward' | 'reload';
86
+ /** The element an action landed on, as the page showed it at that moment. */
87
+ type ActionTarget = {
88
+ tag: string;
89
+ role?: string;
90
+ /** What a reader would call it: its label, its text, or its alt text. */
91
+ name?: string;
92
+ trackId?: string;
93
+ /** In CSS pixels from the document's top-left, the space of the screenshot of the node the action was taken on. */
94
+ box: {
95
+ x: number;
96
+ y: number;
97
+ width: number;
98
+ height: number;
99
+ };
100
+ };
101
+ type RawAction = {
102
+ kind: ActionKind;
103
+ startedAt: string;
104
+ /** The tab it happened in, known when it reached an element. */
105
+ pageId?: string;
106
+ /** What the test acted on, in Playwright's selector syntax. */
107
+ selector?: string;
108
+ /**
109
+ * The text, key, URL, option, or file names, or for `dragTo`, the
110
+ * selector it was dropped on. Left out for what was typed
111
+ * into a password or one-time-code field, or into a field the page did not
112
+ * report.
113
+ */
114
+ value?: string;
115
+ /** Absent when the page reported no element for it, e.g. `goto`. */
116
+ target?: ActionTarget;
117
+ };
79
118
  type PageLayout = {
80
119
  viewport: {
81
120
  width: number;
@@ -113,6 +152,13 @@ type RawNode = {
113
152
  * failed nodes are included; the server walks past the ones it doesn't show.
114
153
  */
115
154
  previousNodeId?: string;
155
+ /** The test's words for how it got here from `previousNodeId`. */
156
+ edgeLabel?: string;
157
+ /**
158
+ * What the test did between requesting `previousNodeId` and this node, in
159
+ * order. Actions taken while recording was paused are left out.
160
+ */
161
+ actions?: RawAction[];
116
162
  } & ({
117
163
  status: 'recorded';
118
164
  recordedAt: string;
@@ -287,5 +333,5 @@ type RalphFixtures = {
287
333
  };
288
334
  declare const test: TestType<PlaywrightTestArgs & PlaywrightTestOptions & RalphFixtures, PlaywrightWorkerArgs & PlaywrightWorkerOptions>;
289
335
  //#endregion
290
- export { type ConditionsDeclaration, type ConfigSnapshot, MANIFEST_ATTACHMENT, type NodeConditions, type PageLayout, type PreparedUpload, type Ralph, RalphFixtures, type RalphOptions, type RalphProjectOptions, type RalphReporterOptions, 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 WebRalphConfig, createRawGraphBundle, expect, mergeTests, prepareArtifactUpload, ralphProjects, test, uploadRawGraph };
336
+ 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 WebRalphConfig, createRawGraphBundle, expect, mergeTests, prepareArtifactUpload, ralphProjects, test, uploadRawGraph };
291
337
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { c as MANIFEST_ATTACHMENT, f as loadConfig, i as prepareArtifactUpload, m as resolveMode, p as loadConfigSync, r as createRawGraphBundle, s as uploadRawGraph, u as Recorder } from "./upload-DQn3CLvx.mjs";
1
+ import { c as MANIFEST_ATTACHMENT, f as loadConfig, i as prepareArtifactUpload, m as resolveMode, p as loadConfigSync, r as createRawGraphBundle, s as uploadRawGraph, u as Recorder } from "./upload-D5hePvmA.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
@@ -61,7 +61,7 @@ const test = test$1.extend({
61
61
  applyScreenSelection(testInfo);
62
62
  await use();
63
63
  }, { auto: true }],
64
- _ralphRecorder: [async ({ ralphOptions, _ralphConfigs, _ralphScreen }, use, testInfo) => {
64
+ _ralphRecorder: [async ({ ralphOptions, _ralphConfigs, _ralphScreen, playwright }, use, testInfo) => {
65
65
  const mode = resolveMode(ralphOptions);
66
66
  if (!testInfo.tags.includes("@ralph") || mode === "off") {
67
67
  await use(null);
@@ -80,7 +80,7 @@ const test = test$1.extend({
80
80
  pending = loadConfig(ralphOptions, root);
81
81
  _ralphConfigs.set(key, pending);
82
82
  }
83
- const recorder = new Recorder(ralphOptions, await pending, testInfo, mode);
83
+ const recorder = new Recorder(ralphOptions, await pending, testInfo, mode, playwright);
84
84
  try {
85
85
  await use(recorder);
86
86
  } finally {
@@ -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';\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 ConditionsDeclaration,\n NodeConditions,\n RecordedTags,\n RecordNodeOptions,\n TagConditions,\n TagDeclaration,\n TagValue,\n ConfigSnapshot,\n PageLayout,\n Ralph,\n RalphOptions,\n TrackedElement,\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 ({ ralphOptions, _ralphConfigs, _ralphScreen }, use, testInfo) => {\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' && !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 );\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 pause: () => {},\n resume: () => {},\n observe: () => {\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 _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';\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} 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' && !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 pause: () => {},\n resume: () => {},\n observe: () => {\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 _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/reporter.cjs CHANGED
@@ -1,4 +1,4 @@
1
- const require_upload = require("./upload-ZMnTCjs2.cjs");
1
+ const require_upload = require("./upload-BvAi7mMY.cjs");
2
2
  let node_path = require("node:path");
3
3
  node_path = require_upload.__toESM(node_path, 1);
4
4
  let node_fs_promises = require("node:fs/promises");
package/dist/reporter.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as resolveUploadOptions, c as MANIFEST_ATTACHMENT, d as version, i as prepareArtifactUpload, l as REPORTER_ENV, m as resolveMode, n as bundleImagePath, o as resultLink, s as uploadRawGraph, t as RAW_GRAPH_BUNDLE_INDEX } from "./upload-DQn3CLvx.mjs";
1
+ import { a as resolveUploadOptions, c as MANIFEST_ATTACHMENT, d as version, i as prepareArtifactUpload, l as REPORTER_ENV, m as resolveMode, n as bundleImagePath, o as resultLink, s as uploadRawGraph, t as RAW_GRAPH_BUNDLE_INDEX } from "./upload-D5hePvmA.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";