@ralphralphai/playwright 1.1.0 → 1.2.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.
@@ -1 +0,0 @@
1
- {"version":3,"file":"upload-giSVxpun.mjs","names":["delay","packageInfo.version"],"sources":["../src/config.ts","../package.json","../src/animations.ts","../src/image.ts","../src/layout.ts","../src/recorder.ts","../src/upload.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport path from 'node:path';\n\nimport configPackage from '@ralphralphai/config/package.json' with { type: 'json' };\nimport { WebRalphConfigSchema } from '@ralphralphai/config/zod';\nimport { parse, printParseErrorCode, type ParseError } from 'jsonc-parser';\n\nimport type { ConfigSnapshot, RalphOptions } from './types';\n\nexport function resolveMode(options: RalphOptions): 'off' | 'local' | 'upload' {\n const mode =\n options.mode ??\n process.env['RALPH_MODE'] ??\n (process.env['RALPH_UPLOAD_KEY'] ? 'upload' : 'off');\n if (mode === 'off' || mode === 'local' || mode === 'upload') {\n return mode;\n }\n\n throw new Error('RALPH_MODE must be off, local, or upload.');\n}\n\ntype ConfigSource = Pick<RalphOptions, 'config' | 'configPath'>;\n\nexport async function loadConfig(\n options: ConfigSource,\n root: string,\n): Promise<ConfigSnapshot> {\n const file = configFile(options, root);\n return snapshot(options.config ?? parseConfig(await readFile(file, 'utf8')));\n}\n\n// Playwright config files may be CommonJS, so project generation cannot await.\nexport function loadConfigSync(\n options: ConfigSource,\n root: string,\n): ConfigSnapshot {\n const file = configFile(options, root);\n return snapshot(options.config ?? parseConfig(readFileSync(file, 'utf8')));\n}\n\nfunction configFile(options: ConfigSource, root: string): string {\n if (options.config && options.configPath) {\n throw new Error(\n 'Provide either ralphOptions.config or configPath, not both.',\n );\n }\n return path.resolve(root, options.configPath ?? 'ralph.jsonc');\n}\n\nfunction parseConfig(text: string): unknown {\n const errors: ParseError[] = [];\n const value: unknown = parse(text, errors, { allowTrailingComma: true });\n if (errors.length) {\n throw new Error(\n 'Invalid Ralph config: ' +\n errors\n .map(\n (error) => printParseErrorCode(error.error) + ' at ' + error.offset,\n )\n .join(', '),\n );\n }\n return value;\n}\n\nfunction snapshot(value: unknown): ConfigSnapshot {\n WebRalphConfigSchema.parse(value);\n return {\n config: JSON.parse(JSON.stringify(value)) as ConfigSnapshot['config'],\n packageVersion: configPackage.version,\n };\n}\n","","import { errors, type Page } from '@playwright/test';\n\n/**\n * Waits until no finite CSS animation, CSS transition, or Web Animation is\n * running. Infinite ones (spinners, shimmers) never finish, so they are\n * ignored. Animations driven by `requestAnimationFrame` are invisible here.\n *\n * Stops waiting after `timeoutMs` without failing; the screenshot then jumps\n * whatever is left to its end state.\n */\nexport async function waitForAnimations(\n page: Page,\n timeoutMs: number,\n): Promise<void> {\n try {\n await page.waitForFunction(\n () =>\n document\n .getAnimations()\n .every(\n (animation) =>\n animation.playState !== 'running' ||\n animation.playbackRate === 0 ||\n !Number.isFinite(animation.effect?.getComputedTiming().endTime),\n ),\n undefined,\n // Polled every frame: animations often start a frame or two late.\n { polling: 'raf', timeout: Math.max(1, timeoutMs) },\n );\n } catch (error) {\n if (!(error instanceof errors.TimeoutError)) {\n throw error;\n }\n }\n}\n","import type { Page } from '@playwright/test';\n\n/**\n * Matches the crawler's full-page screenshots: lossless measured larger than this\n * on real sites, which are mostly photographic content.\n */\nconst WEBP_QUALITY = 80;\n\nexport type Screenshot = {\n bytes: Buffer;\n contentType: 'image/webp' | 'image/png';\n width: number;\n height: number;\n};\n\nexport async function takeScreenshot(\n page: Page,\n deadline: number,\n): Promise<Screenshot> {\n // Finite animations are jumped to their end state and infinite ones paused,\n // only while the screenshot is taken.\n const options = {\n fullPage: true,\n scale: 'css',\n animations: 'disabled',\n } as const;\n const webp = await page.screenshot({\n ...options,\n type: 'webp',\n quality: WEBP_QUALITY,\n timeout: Math.max(1, deadline - Date.now()),\n });\n // WebP stores each dimension in 14 bits, so a page over 16383px has no valid\n // encoding. Chromium signals that with an empty buffer rather than an error;\n // PNG has no such limit.\n if (webp.length > 0) {\n return { bytes: webp, contentType: 'image/webp', ...webpSize(webp) };\n }\n const png = await page.screenshot({\n ...options,\n type: 'png',\n timeout: Math.max(1, deadline - Date.now()),\n });\n return {\n bytes: png,\n contentType: 'image/png',\n width: png.readUInt32BE(16),\n height: png.readUInt32BE(20),\n };\n}\n\nexport function imageExtension(contentType: Screenshot['contentType']): string {\n return contentType === 'image/png' ? '.png' : '.webp';\n}\n\nfunction webpSize(bytes: Buffer): { width: number; height: number } {\n if (\n bytes.toString('ascii', 0, 4) !== 'RIFF' ||\n bytes.toString('ascii', 8, 12) !== 'WEBP'\n ) {\n throw new Error('The screenshot is not a WebP image.');\n }\n const chunk = bytes.toString('ascii', 12, 16);\n if (chunk === 'VP8X') {\n return {\n width: 1 + bytes.readUIntLE(24, 3),\n height: 1 + bytes.readUIntLE(27, 3),\n };\n }\n if (chunk === 'VP8 ') {\n return {\n width: bytes.readUInt16LE(26) & 0x3fff,\n height: bytes.readUInt16LE(28) & 0x3fff,\n };\n }\n if (chunk === 'VP8L') {\n const bits = bytes.readUInt32LE(21);\n return { width: 1 + (bits & 0x3fff), height: 1 + ((bits >> 14) & 0x3fff) };\n }\n throw new Error('Unsupported WebP chunk: ' + chunk);\n}\n","import type { Page } from '@playwright/test';\n\nimport type { PageLayout } from './types';\n\nexport function readLayout(page: Page): Promise<PageLayout> {\n return page\n .locator('[data-track-id]')\n .filter({ visible: true })\n .evaluateAll((elements) => ({\n viewport: { width: window.innerWidth, height: window.innerHeight },\n document: {\n width: Math.max(\n document.documentElement.scrollWidth,\n document.documentElement.clientWidth,\n document.body?.scrollWidth ?? 0,\n ),\n height: Math.max(\n document.documentElement.scrollHeight,\n document.documentElement.clientHeight,\n document.body?.scrollHeight ?? 0,\n ),\n },\n scroll: { x: window.scrollX, y: window.scrollY },\n devicePixelRatio: window.devicePixelRatio,\n trackedElements: elements.flatMap((element) => {\n const trackId = element.getAttribute('data-track-id');\n const rect = element.getBoundingClientRect();\n // Off-screen horizontally (drawers, carousel slides) is dropped like\n // the crawler does; below the fold is kept, since the screenshot is\n // full-page.\n if (\n !trackId ||\n rect.width <= 0 ||\n rect.height <= 0 ||\n rect.right <= 0 ||\n rect.left >= window.innerWidth\n ) {\n return [];\n }\n return [\n {\n trackId,\n x: rect.x + window.scrollX,\n y: rect.y + window.scrollY,\n width: rect.width,\n height: rect.height,\n },\n ];\n }),\n }));\n}\n","import { randomUUID } from 'node:crypto';\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport { setTimeout as delay } from 'node:timers/promises';\n\nimport type { BrowserContext, Frame, Page, TestInfo } from '@playwright/test';\nimport playwrightPackage from '@playwright/test/package.json' with { type: 'json' };\n\nimport packageInfo from '../package.json';\nimport { waitForAnimations } from './animations';\nimport { imageExtension, takeScreenshot, type Screenshot } from './image';\nimport { readLayout } from './layout';\nimport type {\n RawNode,\n RawPage,\n RawGraphFile,\n RecordNodeOptions,\n ConfigSnapshot,\n Ralph,\n RalphOptions,\n} from './types';\n\n/** The attachment holding a test's `RawGraphFile`, which the reporter merges. */\nexport const MANIFEST_ATTACHMENT = 'ralph-raw-graph';\n\n/**\n * Set by the reporter in the main process before any worker starts, so the\n * fixtures can refuse to upload without it.\n */\nexport const REPORTER_ENV = 'RALPH_PLAYWRIGHT_REPORTER';\n\n/** Prefix of the attachment holding each recorded node's screenshot; the node id follows. */\nexport const SCREENSHOT_ATTACHMENT_PREFIX = 'ralph-screenshot-';\n\nconst DEFAULT_SETTLE_MS = 150;\nconst DEFAULT_RECORD_TIMEOUT_MS = 5_000;\n\n/** Where the raw graph and screenshots are written, inside Playwright's per-test output directory. */\nconst OUTPUT_DIR = 'ralph';\n\ntype PageInfo = RawPage;\n\ntype CreateNodeRequest = Omit<RawNode, 'status'>;\n\ntype RecordedNode = Extract<RawNode, { status: 'recorded' }>;\n\n/** What the recorder tracks for one browser tab. */\ntype PageState = {\n page: Page;\n info: PageInfo;\n\n /** The URL of the last navigation, so a reload is not recorded again. */\n lastUrl: string;\n\n /**\n * Bumped on every main-frame navigation, including reloads. A recording that\n * sees a different generation when it finishes was overtaken by one.\n */\n generation: number;\n\n /** The next node's position within this tab. */\n sequence: number;\n\n /** The last queued recording. Recordings in a tab run one at a time. */\n tail: Promise<void>;\n\n /** Resolves once `info.openerPageId` is known. */\n opener: Promise<void>;\n\n /**\n * For a tab first used while recording was paused: the last node requested\n * before the pause. Its first node links here when neither the tab nor its\n * opener has an earlier one. Popups inherit it from their opener.\n */\n pauseAnchor?: string;\n\n onNavigation: (frame: Frame) => void;\n};\n\nexport class Recorder implements Ralph {\n private readonly startedAt = new Date().toISOString();\n\n /** How long to wait after a navigation's DOM loads before recording. */\n private readonly settleMs: number;\n\n /**\n * The budget for recording one node, from settling through the screenshot.\n * Past it, the node is recorded as failed.\n */\n private readonly recordTimeoutMs: number;\n\n /**\n * Every context being watched, with its page listener, kept so\n * `stopContext` can remove the exact listener `observe` added.\n */\n private readonly contexts = new Map<BrowserContext, (page: Page) => void>();\n\n /**\n * Every tab seen in a watched context, including closed ones, so their\n * nodes and opener links still reach the manifest.\n */\n private readonly pages = new Map<Page, PageState>();\n\n /**\n * Each node's outcome, in completion order. Nodes in one tab complete\n * in sequence, but tabs interleave; `finish` sorts them.\n */\n private readonly nodes: RawNode[] = [];\n\n /**\n * When each node was requested, keyed by node id, for linking a\n * popup's first node to its opener's latest. Numbers, so they compare\n * without parsing each node's `requestedAt` string.\n */\n private readonly requestedAtMs = new Map<string, number>();\n\n /** Set by `finish`, after which `observe` throws. */\n private closed = false;\n\n /** Set by `pause`; while set, no new nodes are requested. */\n private paused = false;\n\n /** The most recently requested node, the anchor for tabs used while paused. */\n private lastRequestedNodeId?: string;\n\n constructor(\n private readonly options: RalphOptions,\n private readonly config: ConfigSnapshot,\n private readonly testInfo: TestInfo,\n private readonly mode: RawGraphFile['mode'],\n ) {\n this.settleMs = options.settleMs ?? DEFAULT_SETTLE_MS;\n this.recordTimeoutMs = options.recordTimeoutMs ?? DEFAULT_RECORD_TIMEOUT_MS;\n\n const validTimings =\n Number.isFinite(this.settleMs) &&\n this.settleMs >= 0 &&\n Number.isFinite(this.recordTimeoutMs) &&\n this.recordTimeoutMs > this.settleMs;\n if (!validTimings) {\n throw new Error(\n 'Ralph recordTimeoutMs must be finite and greater than settleMs, which must be non-negative.',\n );\n }\n }\n\n // ---------------------------------------------------------------------------\n // Public API\n // ---------------------------------------------------------------------------\n\n /** Starts recording URL changes in every current and future tab of `context`. */\n observe(context: BrowserContext): void {\n if (this.closed) {\n throw new Error('The Ralph recorder has finished.');\n }\n if (this.contexts.has(context)) {\n return;\n }\n\n const onPage = (page: Page) => this.watchPage(page);\n this.contexts.set(context, onPage);\n\n context.on('page', onPage);\n for (const page of context.pages()) {\n onPage(page);\n }\n }\n\n /** Records the page as it is now, e.g. an open dialog at the same URL. */\n async recordNode(page: Page, options: RecordNodeOptions = {}): Promise<void> {\n this.observe(page.context());\n const state = this.pages.get(page);\n if (!state) {\n throw new Error('Cannot record a closed browser page.');\n }\n\n if (this.paused) {\n return;\n }\n\n await this.enqueueCreateNodeRequest(state, 'explicit', options);\n }\n\n /**\n * Stops requesting nodes, automatic or explicit, until `resume`. Recordings\n * already requested still complete. The next node after `resume` links to\n * the last one before `pause`, so the skipped steps drop out of the graph.\n */\n pause(): void {\n this.paused = true;\n }\n\n /** Undoes `pause`. The current page is not recorded until its URL changes. */\n resume(): void {\n this.paused = false;\n }\n\n /** Waits until every page recording requested so far has finished. */\n async waitForCapture(): Promise<void> {\n await Promise.all([...this.pages.values()].map((state) => state.tail));\n }\n\n /** Stops watching `context` and waits for its queued recordings. */\n async stopContext(context: BrowserContext): Promise<void> {\n const onPage = this.contexts.get(context);\n if (onPage) {\n context.off('page', onPage);\n }\n\n const states = [...this.pages.values()].filter(\n (state) => state.page.context() === context,\n );\n for (const state of states) {\n state.page.off('framenavigated', state.onNavigation);\n }\n await Promise.all(states.map((state) => state.tail));\n }\n\n /**\n * Stops recording, then writes the test's raw graph for the reporter, and\n * fails a passing test whose nodes failed to record.\n */\n async finish(): Promise<void> {\n this.closed = true;\n await Promise.all(\n [...this.contexts.keys()].map((context) => this.stopContext(context)),\n );\n await Promise.all([...this.pages.values()].map((state) => state.opener));\n\n const nodes = this.nodes.sort(\n (a, b) => a.pageId.localeCompare(b.pageId) || a.sequence - b.sequence,\n );\n linkPreviousNodes(\n nodes,\n (pageId) => this.pageInfo(pageId)?.openerPageId,\n (nodeId) => this.requestedAtMs.get(nodeId) ?? 0,\n (pageId) => this.pageState(pageId)?.pauseAnchor,\n );\n\n const failures = nodes.filter(\n (item) => item.status === 'uncaptured' && item.reason === 'error',\n );\n await this.writeRawGraph(this.buildRawGraph(nodes, failures.length));\n\n if (failures.length && this.testInfo.status === 'passed') {\n throw new Error(\n 'Ralph could not record ' +\n failures.length +\n ' page state(s). See the ralph-raw-graph attachment.',\n );\n }\n }\n\n // ---------------------------------------------------------------------------\n // Watching tabs\n // ---------------------------------------------------------------------------\n\n private watchPage(page: Page): void {\n if (this.pages.has(page)) {\n return;\n }\n\n const state: PageState = {\n page,\n info: {\n pageId: 'page-' + this.pages.size,\n browserName: page.context().browser()?.browserType().name(),\n },\n lastUrl: '',\n generation: 0,\n sequence: 0,\n tail: Promise.resolve(),\n opener: Promise.resolve(),\n onNavigation: (frame) => {\n if (frame === page.mainFrame()) {\n this.urlChanged(state);\n }\n },\n };\n this.pages.set(page, state);\n\n state.opener = page\n .opener()\n .then((opener) => {\n if (opener) {\n const openerState = this.pages.get(opener);\n state.info.openerPageId = openerState?.info.pageId;\n state.pauseAnchor ??= openerState?.pauseAnchor;\n }\n })\n .catch(() => undefined);\n\n page.on('framenavigated', state.onNavigation);\n this.urlChanged(state);\n }\n\n private urlChanged(state: PageState): void {\n if (this.paused && state.sequence === 0) {\n state.pauseAnchor ??= this.lastRequestedNodeId;\n }\n\n // A same-URL navigation is not leaving the page.\n const url = state.page.url();\n if (url === state.lastUrl) {\n return;\n }\n\n state.generation++;\n state.lastUrl = url;\n\n if (!this.paused && isHttpUrl(url)) {\n // Automatic recordings never fail the test; failures land in the manifest.\n void this.enqueueCreateNodeRequest(state, 'url-change', {}).catch(\n () => undefined,\n );\n }\n }\n\n // ---------------------------------------------------------------------------\n // Recording\n // ---------------------------------------------------------------------------\n\n /** Queues a recording behind the tab's earlier ones. */\n private enqueueCreateNodeRequest(\n state: PageState,\n trigger: RawNode['trigger'],\n options: RecordNodeOptions,\n ): Promise<void> {\n const actualUrl = state.page.url();\n const url =\n options.url === undefined\n ? actualUrl\n : new URL(options.url, actualUrl).href;\n if (!isHttpUrl(actualUrl) || !isHttpUrl(url)) {\n throw new Error(\n 'Ralph recordings require HTTP or HTTPS page and recorded URLs.',\n );\n }\n\n const request: CreateNodeRequest = {\n nodeId: randomUUID(),\n pageId: state.info.pageId,\n sequence: state.sequence++,\n requestedAt: new Date().toISOString(),\n actualUrl,\n url,\n trigger,\n ...(options.state === undefined ? {} : { state: options.state }),\n };\n this.requestedAtMs.set(request.nodeId, Date.now());\n this.lastRequestedNodeId = request.nodeId;\n\n const generation = state.generation;\n const task = state.tail.then(() =>\n this.createNode(state, generation, request),\n );\n state.tail = task.catch(() => undefined);\n\n return task;\n }\n\n /**\n * Creates the requested node: `recorded`, or `uncaptured` if the tab\n * navigates or closes first, or the record timeout passes.\n */\n private async createNode(\n state: PageState,\n generation: number,\n request: CreateNodeRequest,\n ): Promise<void> {\n const { page } = state;\n const deadline = startDeadline(\n this.recordTimeoutMs,\n 'Ralph recording timed out.',\n );\n const { wait } = deadline;\n const navigatedAway = () =>\n state.generation !== generation || page.url() !== request.actualUrl;\n\n const check = () => {\n deadline.signal.throwIfAborted();\n if (navigatedAway()) {\n throw new Error('The page navigated before its recording completed.');\n }\n if (page.isClosed()) {\n throw new Error('The page closed before its recording completed.');\n }\n };\n\n try {\n check();\n if (request.trigger === 'url-change') {\n await this.waitForPageToSettle(page, deadline, check);\n }\n\n if (this.options.reduceMotion) {\n check();\n // Half the remaining budget, so the screenshot still has time.\n await wait(waitForAnimations(page, (deadline.endsAt - Date.now()) / 2));\n }\n\n // Geometry is read on both sides of the screenshot to detect layout drift.\n check();\n const before = await wait(readLayout(page));\n check();\n const image = await wait(takeScreenshot(page, deadline.endsAt));\n check();\n const after = await wait(readLayout(page));\n check();\n\n this.nodes.push({\n ...request,\n status: 'recorded',\n recordedAt: new Date().toISOString(),\n layout: before,\n geometryStable: JSON.stringify(before) === JSON.stringify(after),\n screenshot: await this.saveScreenshot(request.nodeId, image),\n });\n } catch (error) {\n this.nodes.push({\n ...request,\n status: 'uncaptured',\n reason: navigatedAway() ? 'navigated-away' : 'error',\n message: error instanceof Error ? error.message : String(error),\n });\n if (request.trigger === 'explicit') {\n throw error;\n }\n } finally {\n deadline.cancel();\n }\n }\n\n /** After a navigation: the DOM, the settle delay, then the `ready` hook. */\n private async waitForPageToSettle(\n page: Page,\n { wait, signal }: Deadline,\n check: () => void,\n ): Promise<void> {\n await wait(\n page.waitForLoadState('domcontentloaded', {\n timeout: this.recordTimeoutMs,\n }),\n );\n await wait(delay(this.settleMs, undefined, { signal }));\n check();\n if (this.options.ready) {\n await wait(this.options.ready(page, signal));\n }\n }\n\n /** Writes the image to the test output and attaches it to the report. */\n private async saveScreenshot(\n nodeId: string,\n image: Screenshot,\n ): Promise<RecordedNode['screenshot']> {\n const path = OUTPUT_DIR + '/' + nodeId + imageExtension(image.contentType);\n const attachmentName = SCREENSHOT_ATTACHMENT_PREFIX + nodeId;\n\n await mkdir(this.testInfo.outputPath(OUTPUT_DIR), { recursive: true });\n await writeFile(this.testInfo.outputPath(path), image.bytes);\n await this.testInfo.attach(attachmentName, {\n // Playwright can delete test output immediately after testEnd, before\n // the reporter merges it. Body attachments survive that cleanup.\n ...(this.testInfo.config.preserveOutput === 'always'\n ? { path: this.testInfo.outputPath(path) }\n : { body: image.bytes }),\n contentType: image.contentType,\n });\n\n return {\n path,\n attachmentName,\n width: image.width,\n height: image.height,\n scale: 'css',\n contentType: image.contentType,\n };\n }\n\n // ---------------------------------------------------------------------------\n // Manifest\n // ---------------------------------------------------------------------------\n\n private pageState(pageId: string): PageState | undefined {\n return [...this.pages.values()].find(\n (state) => state.info.pageId === pageId,\n );\n }\n\n private pageInfo(pageId: string): PageInfo | undefined {\n return this.pageState(pageId)?.info;\n }\n\n private buildRawGraph(nodes: RawNode[], failureCount: number): RawGraphFile {\n const { testInfo } = this;\n const testPassed = testInfo.status === 'passed';\n\n return {\n formatVersion: 1,\n producer: {\n name: '@ralphralphai/playwright',\n version: packageInfo.version,\n playwrightVersion: playwrightPackage.version,\n },\n runId: this.options.runId ?? process.env['RALPH_RUN_ID'],\n buildId: this.options.buildId ?? process.env['RALPH_BUILD_ID'],\n config: this.config,\n mode: this.mode,\n rawGraph: {\n id: testInfo.testId,\n titlePath: testInfo.titlePath,\n project: testInfo.project.name,\n retry: testInfo.retry,\n repeatEachIndex: testInfo.repeatEachIndex,\n workerIndex: testInfo.workerIndex,\n parallelIndex: testInfo.parallelIndex,\n // Failed nodes fail the test in `finish`, after this is written. The\n // reporter replaces it with the final status anyway.\n status:\n failureCount && testPassed\n ? 'failed'\n : (testInfo.status ?? 'unknown'),\n expectedStatus: testInfo.expectedStatus,\n startedAt: this.startedAt,\n finishedAt: new Date().toISOString(),\n pages: [...this.pages.values()].map((state) => state.info),\n nodes,\n complete:\n testPassed &&\n nodes.length > 0 &&\n nodes.every(\n (item) => item.status === 'recorded' && item.geometryStable,\n ),\n },\n };\n }\n\n private async writeRawGraph(file: RawGraphFile): Promise<void> {\n const path = this.testInfo.outputPath(OUTPUT_DIR, 'raw_graph.json');\n const body = JSON.stringify(file, null, 2) + '\\n';\n await mkdir(this.testInfo.outputPath(OUTPUT_DIR), { recursive: true });\n await writeFile(path, body);\n await this.testInfo.attach(MANIFEST_ATTACHMENT, {\n ...(this.testInfo.config.preserveOutput === 'always'\n ? { path }\n : { body }),\n contentType: 'application/json',\n });\n }\n}\n\n// -----------------------------------------------------------------------------\n// Helpers\n// -----------------------------------------------------------------------------\n\nfunction isHttpUrl(url: string): boolean {\n return /^https?:/.test(url);\n}\n\ntype Deadline = {\n signal: AbortSignal;\n /** Epoch milliseconds at which the deadline passes. */\n endsAt: number;\n /** Rejects with the timeout error if `promise` outlives the deadline. */\n wait: <T>(promise: Promise<T>) => Promise<T>;\n cancel: () => void;\n};\n\nfunction startDeadline(timeoutMs: number, message: string): Deadline {\n const controller = new AbortController();\n const { signal } = controller;\n const timer = setTimeout(\n () => controller.abort(new Error(message)),\n timeoutMs,\n );\n const aborted = new Promise<never>((_, reject) => {\n signal.addEventListener('abort', () => reject(signal.reason), {\n once: true,\n });\n });\n // Nothing may be waiting when the deadline passes.\n aborted.catch(() => undefined);\n\n return {\n signal,\n endsAt: Date.now() + timeoutMs,\n wait: (promise) => Promise.race([promise, aborted]),\n cancel: () => clearTimeout(timer),\n };\n}\n\n/**\n * Sets `previousNodeId` on each node: the one before it in the same\n * tab, or for a popup's first node, its opener's latest. Failing both, a tab\n * first used while paused links to the last node before the pause.\n * Uncaptured nodes stay in the chain, as regular nodes in the server's graph.\n *\n * `nodes` must be sorted by page, then sequence.\n */\nfunction linkPreviousNodes(\n nodes: RawNode[],\n openerOf: (pageId: string) => string | undefined,\n requestedAtMs: (nodeId: string) => number,\n pauseAnchorOf: (pageId: string) => string | undefined,\n): void {\n const byPage = new Map<string, RawNode[]>();\n const byId = new Map<string, RawNode>();\n for (const item of nodes) {\n byPage.set(item.pageId, [...(byPage.get(item.pageId) ?? []), item]);\n byId.set(item.nodeId, item);\n }\n\n const openerNodeBefore = (item: RawNode) => {\n const openerPageId = openerOf(item.pageId);\n const requestedAt = requestedAtMs(item.nodeId);\n return byPage\n .get(openerPageId ?? '')\n ?.filter((candidate) => requestedAtMs(candidate.nodeId) <= requestedAt)\n .at(-1);\n };\n\n for (const items of byPage.values()) {\n items.forEach((item, index) => {\n const from =\n index > 0\n ? items[index - 1]\n : (openerNodeBefore(item) ??\n byId.get(pauseAnchorOf(item.pageId) ?? ''));\n if (from) {\n item.previousNodeId = from.nodeId;\n }\n });\n }\n}\n","import { promisify } from 'node:util';\nimport { gzip } from 'node:zlib';\n\nimport { pack } from 'tar-stream';\n\nimport { imageExtension, type Screenshot } from './image';\nimport type { RawGraphManifest } from './types';\n\nexport type UploadOptions = {\n apiUrl: string;\n appId: string;\n uploadKey: string;\n timeoutMs?: number;\n};\nexport type RawGraphScreenshot = {\n nodeId: string;\n contentType: Screenshot['contentType'];\n bytes: Uint8Array;\n};\nexport type UploadReceipt = {\n result: 'ok';\n status: 'received';\n artifactId: string;\n resultId: string;\n /** Where the result opens in the Ralph dashboard, when the server knows. */\n resultUrl?: string;\n replayed: boolean;\n};\n\n/** Where the reporter uploads: its options, else the environment. */\nexport function resolveUploadOptions(options: {\n apiUrl?: string;\n appId?: string;\n uploadTimeoutMs?: number;\n}): UploadOptions {\n const uploadKey = process.env['RALPH_UPLOAD_KEY'];\n const apiUrl = options.apiUrl ?? process.env['RALPH_API_URL'];\n const appId = options.appId ?? process.env['RALPH_APP_ID'];\n if (!uploadKey || !apiUrl || !appId) {\n throw new Error(\n 'Ralph upload requires RALPH_UPLOAD_KEY, RALPH_API_URL (or the reporter option apiUrl), and RALPH_APP_ID (or appId).',\n );\n }\n return { uploadKey, apiUrl, appId, timeoutMs: options.uploadTimeoutMs };\n}\n\n// Mirrors the server's limits, so an oversized run fails before it is sent.\nexport const MAX_MANIFEST_BYTES = 16 * 1024 * 1024;\nexport const MAX_SCREENSHOTS = 5000;\nexport const MAX_SCREENSHOT_BYTES = 256 * 1024 * 1024;\nexport const MAX_COMPRESSED_BYTES = 256 * 1024 * 1024;\n\nexport const RAW_GRAPH_BUNDLE_INDEX = 'raw_graph.json';\nexport const RAW_GRAPH_BUNDLE_IMAGE_DIR = 'assets/images';\n\nexport type RawGraphBundleIndex = {\n manifest: RawGraphManifest;\n screenshots: { nodeId: string; path: string }[];\n};\n\n/** Where a node's screenshot sits inside the bundle. */\nexport function bundleImagePath(\n nodeId: string,\n contentType: Screenshot['contentType'],\n): string {\n if (!/^[A-Za-z0-9-]{1,64}$/.test(nodeId)) {\n throw new Error(`Ralph node id is not path-safe: ${nodeId}`);\n }\n return `${RAW_GRAPH_BUNDLE_IMAGE_DIR}/${nodeId}${imageExtension(contentType)}`;\n}\n\n/**\n * Packs the manifest and screenshots into a gzipped tar archive:\n *\n * raw_graph.json { manifest, screenshots: [{ nodeId, path }] }\n * assets/images/<id>.webp raw screenshot bytes (.png when too large for WebP)\n */\nexport async function createRawGraphBundle(\n manifest: RawGraphManifest,\n screenshots: readonly RawGraphScreenshot[],\n): Promise<Uint8Array> {\n if (\n Buffer.byteLength(JSON.stringify(manifest)) > MAX_MANIFEST_BYTES ||\n screenshots.length > MAX_SCREENSHOTS ||\n screenshots.reduce(\n (total, screenshot) => total + screenshot.bytes.byteLength,\n 0,\n ) > MAX_SCREENSHOT_BYTES\n ) {\n throw new Error('Ralph raw graph exceeds upload limits.');\n }\n\n const images = screenshots.map(({ nodeId, contentType, bytes }) => ({\n nodeId,\n path: bundleImagePath(nodeId, contentType),\n bytes,\n }));\n const index: RawGraphBundleIndex = {\n manifest,\n screenshots: images.map(({ nodeId, path }) => ({ nodeId, path })),\n };\n\n // Fixed attributes keep retries of the same artifact byte-identical.\n const attrs = { mtime: new Date(0), uid: 0, gid: 0 };\n const tar = pack();\n tar.entry({ name: RAW_GRAPH_BUNDLE_INDEX, ...attrs }, JSON.stringify(index));\n for (const { path, bytes } of images) {\n tar.entry({ name: path, ...attrs }, Buffer.from(bytes));\n }\n tar.finalize();\n const chunks: Buffer[] = [];\n for await (const chunk of tar) {\n chunks.push(chunk as Buffer);\n }\n const compressed = await promisify(gzip)(Buffer.concat(chunks));\n if (compressed.byteLength > MAX_COMPRESSED_BYTES) {\n throw new Error('Ralph compressed raw graph bundle exceeds upload limits.');\n }\n\n return compressed;\n}\n\nfunction isLoopback(url: URL): boolean {\n return (\n url.hostname === 'localhost' ||\n url.hostname === '[::1]' ||\n /^127(\\.\\d{1,3}){3}$/.test(url.hostname)\n );\n}\n\nexport async function uploadRawGraph(\n manifest: RawGraphManifest,\n screenshots: readonly RawGraphScreenshot[],\n options: UploadOptions,\n): Promise<UploadReceipt> {\n if (!/^[A-Za-z0-9_-]{1,64}$/.test(options.appId)) {\n throw new Error(`Ralph app id is not valid: ${options.appId}`);\n }\n const url = new URL(\n `/api/upload/playwright/apps/${options.appId}/artifact`,\n options.apiUrl,\n );\n // Environment only, and absent from the public options, because only Ralph\n // developers run a server that accepts local storage.\n const storage = process.env['RALPH_UPLOAD_STORAGE'] || 'gcs';\n if (storage !== 'gcs' && storage !== 'local') {\n throw new Error(\n `RALPH_UPLOAD_STORAGE must be gcs or local, not ${storage}.`,\n );\n }\n if (storage === 'local') {\n url.searchParams.set('storage', 'local');\n }\n if (\n !(\n url.protocol === 'https:' ||\n (url.protocol === 'http:' && isLoopback(url))\n ) ||\n !options.uploadKey\n ) {\n throw new Error(\n 'Ralph upload requires an HTTPS API URL (HTTP only for loopback hosts) and an upload key.',\n );\n }\n\n const timeoutMs = options.timeoutMs ?? 120_000;\n if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {\n throw new Error('Ralph uploadTimeoutMs must be positive and finite.');\n }\n\n const blob = new Blob(\n [new Uint8Array(await createRawGraphBundle(manifest, screenshots))],\n { type: 'application/gzip' },\n );\n\n const response = await fetch(url, {\n method: 'POST',\n headers: { Authorization: `Bearer ${options.uploadKey}` },\n body: blob,\n redirect: 'error',\n signal: AbortSignal.timeout(timeoutMs),\n });\n\n if (!response.ok) {\n throw new Error(\n `Ralph upload failed (HTTP ${response.status}). Local Ralph artifacts are retained for retry.`,\n );\n }\n\n const receipt = (await response.json()) as Partial<UploadReceipt>;\n if (\n receipt.result !== 'ok' ||\n receipt.status !== 'received' ||\n receipt.artifactId !== manifest.artifactId ||\n typeof receipt.resultId !== 'string' ||\n (receipt.resultUrl !== undefined &&\n typeof receipt.resultUrl !== 'string') ||\n typeof receipt.replayed !== 'boolean'\n ) {\n throw new Error('Ralph server returned an invalid upload receipt.');\n }\n return receipt as UploadReceipt;\n}\n"],"mappings":"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"}