@storyshelf/runner-playwright 0.8.0 → 0.9.0-next-d2cc082
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 +1 -1
- package/dist/capture-runner.mjs +1 -1
- package/dist/pipeline.mjs +13 -5
- package/dist/pipeline.mjs.map +1 -1
- package/dist/remote/azure.d.mts +4 -1
- package/dist/remote/azure.mjs +6 -2
- package/dist/remote/azure.mjs.map +1 -1
- package/dist/remote/browserstack.d.mts +2 -1
- package/dist/remote/browserstack.mjs +4 -2
- package/dist/remote/browserstack.mjs.map +1 -1
- package/package.json +6 -15
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ createPlaywrightCaptureRunner({
|
|
|
46
46
|
|
|
47
47
|
## Remote browsers
|
|
48
48
|
|
|
49
|
-
An entry can also be a **remote service**, from the `/remote` subpath: the runner then connects to a browser on another machine instead of launching one, and offers `<engine>@<name>` targets (`chromium@grid`) with their own baselines.
|
|
49
|
+
An entry can also be a **remote service**, from the `/remote` subpath: the runner then connects to a browser on another machine instead of launching one, and offers `<engine>@<name>` targets (`chromium@grid`) with their own baselines. The vendor presets (`azurePlaywright`, `browserstack`) are experimental: not verified against live infrastructure.
|
|
50
50
|
|
|
51
51
|
```ts
|
|
52
52
|
import { createPlaywrightCaptureRunner } from "@storyshelf/runner-playwright";
|
package/dist/capture-runner.mjs
CHANGED
package/dist/pipeline.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { measureRemote } from "./remote-environment.mjs";
|
|
|
8
8
|
import { createRuntimeParamsState, extractAllParameters } from "./runtime-params.mjs";
|
|
9
9
|
import { captureScreenshot } from "./screenshot.mjs";
|
|
10
10
|
import { createStaticServer } from "./static-server.mjs";
|
|
11
|
-
import { TargetLaunchError, engineOf } from "@storyshelf/core/adapter/capture-runner";
|
|
11
|
+
import { TargetLaunchError, engineOf, logRemoteSizing } from "@storyshelf/core/adapter/capture-runner";
|
|
12
12
|
import { DEFAULT_RECYCLE_BROWSER_EVERY, chunk, createPageLimiter, describeSizing, detectCaptureEnvironment, isTimeoutError, sessionGateFor, storybookBase } from "@storyshelf/core/utils";
|
|
13
13
|
import { StorybookAdapter, resolveStoryViewports } from "@storyshelf/core/capture";
|
|
14
14
|
// src/pipeline.ts
|
|
@@ -20,10 +20,7 @@ async function renderAll(rawInput, active, driver, defaultTarget) {
|
|
|
20
20
|
const launch = async () => await driver.launch(name, input.logger, launchContextOf(input, "render", active.abort?.signal));
|
|
21
21
|
const browser = await launch();
|
|
22
22
|
const server = await serve(input, browser);
|
|
23
|
-
input
|
|
24
|
-
...input,
|
|
25
|
-
browser: engineOf(name)
|
|
26
|
-
}), "capture sizing");
|
|
23
|
+
logHostSizing(input, name, remote);
|
|
27
24
|
const measured = remote ? await measureRemote(browser, remote.remote, {
|
|
28
25
|
logger: input.logger,
|
|
29
26
|
signal: active.abort?.signal
|
|
@@ -61,6 +58,10 @@ function planRun(rawInput, driver, defaultTarget) {
|
|
|
61
58
|
value: String(value)
|
|
62
59
|
}, "sessions() returned an invalid value; keeping the last good one");
|
|
63
60
|
});
|
|
61
|
+
if (remote) logRemoteSizing(rawInput.logger, name, remote.remote, {
|
|
62
|
+
gate,
|
|
63
|
+
concurrency: rawInput.concurrency
|
|
64
|
+
});
|
|
64
65
|
return {
|
|
65
66
|
input: {
|
|
66
67
|
...rawInput,
|
|
@@ -281,6 +282,13 @@ function pushRenderFailure(failures, input, storyId, viewportName, error) {
|
|
|
281
282
|
err: error
|
|
282
283
|
}, "render failed for story");
|
|
283
284
|
}
|
|
285
|
+
/** The host's sizing of a capture; a remote target runs elsewhere, so nothing is measured here. */
|
|
286
|
+
function logHostSizing(input, name, remote) {
|
|
287
|
+
if (!remote) input.logger?.info(describeSizing({
|
|
288
|
+
...input,
|
|
289
|
+
browser: engineOf(name)
|
|
290
|
+
}), "capture sizing");
|
|
291
|
+
}
|
|
284
292
|
export { assertReachable, connectionConcurrency, extractAll, launchContextOf, planRun, renderAll, screenshotWithRetry, serve };
|
|
285
293
|
|
|
286
294
|
//# sourceMappingURL=pipeline.mjs.map
|
package/dist/pipeline.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pipeline.mjs","names":[],"sources":["../src/pipeline.ts"],"sourcesContent":["/** Render orchestration: static server, browser, and per-story captures. */\n\nimport {\n TargetLaunchError,\n engineOf,\n type CaptureEnvironment,\n type CaptureTarget,\n type RemoteTarget,\n type RenderResult,\n type RenderedSnapshot,\n type StoryEntry,\n type StorySourceAdapter,\n type Viewport,\n} from \"@storyshelf/core/adapter/capture-runner\";\nimport { StorybookAdapter, resolveStoryViewports } from \"@storyshelf/core/capture\";\nimport type { StoryParameters } from \"@storyshelf/core/capture\";\nimport {\n DEFAULT_RECYCLE_BROWSER_EVERY,\n chunk,\n describeSizing,\n createPageLimiter,\n detectCaptureEnvironment,\n isTimeoutError,\n sessionGateFor,\n storybookBase,\n type SessionGate,\n} from \"@storyshelf/core/utils\";\nimport type { Browser } from \"playwright-core\";\nimport { finishing, messageOf, outcomeOf, safeCloseBrowser, safeCloseServer } from \"./browser.ts\";\nimport { TransientCaptureError } from \"./capture-retry.ts\";\nimport { PagePool } from \"./pages.ts\";\nimport { measureRemote } from \"./remote-environment.ts\";\nimport { createRuntimeParamsState, extractAllParameters } from \"./runtime-params.ts\";\nimport { captureScreenshot } from \"./screenshot.ts\";\nimport { createStaticServer } from \"./static-server.ts\";\nimport type {\n ActiveRun,\n CaptureContext,\n ExtractInput,\n PlaywrightRenderInput,\n RunInput,\n RuntimeParamsPage,\n LaunchContext,\n TargetDriver,\n} from \"./types.ts\";\n\n/** Where the browser finds the Storybook: the loopback server, or the URL core handed over. */\ninterface Serving {\n url: string;\n close(): Promise<void>;\n}\n\n/** Render all story/viewport combinations for a build. */\nexport async function renderAll(\n rawInput: RunInput,\n active: ActiveRun,\n driver: TargetDriver,\n defaultTarget: CaptureTarget,\n): Promise<RenderResult> {\n const { input, name, remote } = planRun(rawInput, driver, defaultTarget);\n assertReachable(name, remote, input.storybookUrl);\n // Launch first: a target that will not start must fail before a static server is bound.\n const launch = async (): Promise<Browser> =>\n await driver.launch(name, input.logger, launchContextOf(input, \"render\", active.abort?.signal));\n const browser = await launch();\n const server = await serve(input, browser);\n input.logger?.info(describeSizing({ ...input, browser: engineOf(name) }), \"capture sizing\");\n const measured = remote\n ? await measureRemote(browser, remote.remote, {\n logger: input.logger,\n signal: active.abort?.signal,\n }).catch(async (error: unknown) => {\n // A cancel that interrupted the probe releases the lease as cancelled, not failed.\n await Promise.all([\n safeCloseBrowser(browser, outcomeOf(active, true), error),\n safeCloseServer(server),\n ]);\n throw error;\n })\n : undefined;\n const result = await runWithBrowser(\n { ...input, retryRemoteCapture: remote !== undefined },\n active,\n browser,\n server,\n launch,\n );\n // Echo the target so persisted rows are attributable without the request.\n for (const row of [...result.captures, ...result.failures]) row.browser = name;\n return { ...result, environment: measured ?? (await localEnvironment(browser)) };\n}\n\n/** What a remote service's `acquire` is told about the run a browser is for. */\nexport function launchContextOf(\n input: { buildId: string; runId?: string | undefined; shardIndex?: number | undefined },\n purpose: LaunchContext[\"purpose\"],\n signal?: AbortSignal,\n): LaunchContext {\n return {\n buildId: input.buildId,\n runId: input.runId ?? input.buildId,\n shardIndex: input.shardIndex,\n purpose,\n signal,\n };\n}\n\n/** The input with its target resolved, and the session gate when the target is remote. */\nexport function planRun<\n T extends { target?: CaptureTarget | undefined; logger?: PlaywrightRenderInput[\"logger\"] },\n>(\n rawInput: T,\n driver: TargetDriver,\n defaultTarget: CaptureTarget,\n): {\n input: T & {\n target: CaptureTarget;\n gate?: SessionGate | undefined;\n maxPagesPerConnection?: number | undefined;\n };\n name: CaptureTarget;\n remote: RemoteTarget | undefined;\n} {\n const name = rawInput.target ?? defaultTarget;\n const remote = driver.remote(name);\n const gate =\n remote &&\n sessionGateFor(name, remote.remote.sessions, (value) => {\n rawInput.logger?.warn(\n { target: name, value: String(value) },\n \"sessions() returned an invalid value; keeping the last good one\",\n );\n });\n return {\n input: {\n ...rawInput,\n target: name,\n gate,\n maxPagesPerConnection: remote?.remote.maxPagesPerConnection,\n },\n name,\n remote,\n };\n}\n\n/** Apply the connection's cap without changing the target-wide session budget. */\nexport function connectionConcurrency(input: {\n concurrency?: number | undefined;\n maxPagesPerConnection?: number | undefined;\n}): number | undefined {\n return input.maxPagesPerConnection === undefined\n ? input.concurrency\n : Math.min(input.concurrency ?? Infinity, input.maxPagesPerConnection);\n}\n\n/**\n * Load the preview once and read every story's resolved parameters\n * (`__STORYBOOK_PREVIEW__.extract()`), so the orchestrator can plan modes.\n */\nexport async function extractAll(\n input: ExtractInput,\n driver: TargetDriver,\n defaultTarget: CaptureTarget,\n): Promise<Record<string, StoryParameters>> {\n const first = input.stories[0];\n if (!first) return {};\n const name = input.target ?? defaultTarget;\n assertReachable(name, driver.remote(name), input.storybookUrl);\n const browser = await driver.launch(name, input.logger, launchContextOf(input, \"extract\"));\n const server = await serve(input, browser);\n return await finishing(\n async () => await readParameters(browser, server.url, first.id, input.adapter),\n async (failed, error) => {\n await Promise.all([\n safeCloseBrowser(browser, failed ? \"failed\" : \"finished\", error),\n safeCloseServer(server),\n ]);\n },\n );\n}\n\n/** A target whose browser cannot route back to this host needs a URL; failing here names why. */\nexport function assertReachable(\n target: CaptureTarget,\n remote: RemoteTarget | undefined,\n storybookUrl: string | undefined,\n): void {\n if (remote?.reach === \"url\" && storybookUrl === undefined) {\n throw new TargetLaunchError(\n target,\n \"its browser cannot reach this host, so it needs a Storybook URL (a public origin and a secret to sign it) and none was given\",\n { transient: true },\n );\n }\n if (storybookUrl !== undefined) {\n assertStorybookRoot(target, storybookUrl);\n }\n}\n\n/** The URL must be the build's directory root; refused before any browser connects. */\nfunction assertStorybookRoot(target: CaptureTarget, storybookUrl: string): void {\n try {\n storybookBase(storybookUrl);\n } catch (error) {\n throw new TargetLaunchError(target, error instanceof Error ? error.message : String(error));\n }\n}\n\n/** The loopback server, or the URL given; a server that cannot start closes the browser it was for. */\nexport async function serve(\n input: Pick<RunInput, \"storybookDir\" | \"storybookUrl\" | \"staticCompression\">,\n browser: Browser,\n): Promise<Serving> {\n if (input.storybookUrl !== undefined) {\n return {\n url: storybookBase(input.storybookUrl),\n close: async () => {\n await Promise.resolve();\n },\n };\n }\n return await createStaticServer(input.storybookDir, {\n compression: input.staticCompression,\n }).catch(async (error: unknown) => {\n await safeCloseBrowser(browser, \"failed\", error);\n throw error;\n });\n}\n\nasync function readParameters(\n browser: Browser,\n baseUrl: string,\n storyId: string,\n custom: StorySourceAdapter | undefined,\n): Promise<Record<string, StoryParameters>> {\n const adapter: StorySourceAdapter = custom ?? new StorybookAdapter();\n const page = await browser.newPage();\n await page.goto(adapter.buildUrl(baseUrl, storyId), { waitUntil: \"networkidle\" });\n await adapter.waitForReady?.(page);\n return await extractAllParameters(page as unknown as RuntimeParamsPage);\n}\n\n/** The driver's browser version; undefined for a driver (or test double) that does not report one. */\nfunction browserVersionOf(browser: Browser): string | undefined {\n return typeof browser.version === \"function\" ? browser.version() : undefined;\n}\n\n/** This host's environment: a local browser renders on the machine the runner runs on. */\nasync function localEnvironment(browser: Browser): Promise<CaptureEnvironment> {\n return await detectCaptureEnvironment(browserVersionOf(browser));\n}\n\nasync function runWithBrowser(\n input: RunInput,\n active: ActiveRun,\n browser: Browser,\n server: Serving,\n relaunch: () => Promise<Browser>,\n): Promise<RenderResult> {\n active.browser = browser;\n const ctx = buildContext(browser, server.url, input);\n const captures: RenderedSnapshot[] = [];\n const failures: RenderResult[\"failures\"] = [];\n return await finishing(\n async () => {\n await runAllCaptures(input, active, ctx, captures, failures, relaunch);\n return { captures, failures };\n },\n async (failed, error) => {\n active.browser = null;\n await ctx.pagePool?.dispose();\n // ctx.browser is the latest one: recycling replaces the browser mid-run.\n await Promise.all([\n safeCloseBrowser(ctx.browser, outcomeOf(active, failed), error),\n safeCloseServer(server),\n ]);\n },\n );\n}\n\nfunction buildContext(browser: Browser, baseUrl: string, input: RunInput): CaptureContext {\n return {\n browser,\n adapter: input.adapter ?? new StorybookAdapter(),\n baseUrl,\n navigationTimeoutMs: input.navigationTimeoutMs,\n executePlay: input.executePlay,\n playTimeoutMs: input.playTimeoutMs,\n runA11y: input.runA11y,\n ...(input.logger ? { logger: input.logger } : {}),\n runtimeParams: createRuntimeParamsState(),\n ...(input.reusePages ? { pagePool: new PagePool(browser) } : {}),\n };\n}\n\nasync function runAllCaptures(\n input: RunInput,\n active: ActiveRun,\n ctx: CaptureContext,\n captures: RenderedSnapshot[],\n failures: RenderResult[\"failures\"],\n relaunch: () => Promise<Browser>,\n): Promise<void> {\n // Bounded: one page per story x viewport all at once exhausts memory on large Storybooks.\n const jobs = input.stories.flatMap((story) =>\n resolveStoryViewports(story, input.viewports).map((viewport) => ({ story, viewport })),\n );\n // Batches drain in-flight pages, so the browser can be swapped between them.\n const batches = chunk(jobs, input.recycleBrowserEvery ?? DEFAULT_RECYCLE_BROWSER_EVERY);\n const pages = createPageLimiter({\n concurrency: connectionConcurrency(input),\n ...(input.gate === undefined ? {} : { gate: input.gate }),\n browser: engineOf(input.target ?? \"chromium\"),\n logger: input.logger,\n });\n try {\n await runBatches(batches, pages, input, active, ctx, { captures, failures, relaunch });\n } finally {\n pages.dispose();\n }\n}\n\nasync function runBatches(\n batches: { story: StoryEntry; viewport: Viewport }[][],\n pages: ReturnType<typeof createPageLimiter>,\n input: RunInput,\n active: ActiveRun,\n ctx: CaptureContext,\n out: {\n captures: RenderedSnapshot[];\n failures: RenderResult[\"failures\"];\n relaunch: () => Promise<Browser>;\n },\n): Promise<void> {\n for (const [index, batch] of batches.entries()) {\n if (index > 0) {\n // oxlint-disable-next-line eslint/no-await-in-loop -- recycle only between drained batches\n await recycleBrowser(active, ctx, out.relaunch, input.logger);\n }\n // oxlint-disable-next-line eslint/no-await-in-loop -- batches run one after another\n await pages.each(\n batch,\n async (job) => await captureOne(input, active, ctx, job.story, job.viewport, out),\n );\n }\n}\n\n/** Swap in a fresh browser; the old one is closed first so two never coexist. */\nasync function recycleBrowser(\n active: ActiveRun,\n ctx: CaptureContext,\n relaunch: () => Promise<Browser>,\n logger: PlaywrightRenderInput[\"logger\"],\n): Promise<void> {\n if (active.cancelled) throw new Error(\"Capture cancelled\");\n await ctx.pagePool?.dispose();\n await safeCloseBrowser(ctx.browser);\n // A cancel that arrived while the old browser closed must not start a new one.\n if (active.cancelled) throw new Error(\"Capture cancelled\");\n ctx.browser = await relaunch();\n if (ctx.pagePool) ctx.pagePool = new PagePool(ctx.browser);\n active.browser = ctx.browser;\n logger?.info({}, \"browser recycled\");\n}\n\n/** Capture one story x viewport; resolves to whether it timed out (failures are recorded, not thrown). */\nasync function captureOne(\n input: RunInput,\n active: ActiveRun,\n ctx: CaptureContext,\n story: StoryEntry,\n viewport: Viewport,\n out: { captures: RenderedSnapshot[]; failures: RenderResult[\"failures\"] },\n): Promise<boolean> {\n if (active.cancelled) throw new Error(\"Capture cancelled\");\n try {\n await pushCapture(ctx, story, viewport, out.captures, out.failures, input, active);\n return false;\n } catch (error) {\n pushRenderFailure(out.failures, input, story.id, viewport.name, error);\n return isTimeoutError(error instanceof TransientCaptureError ? error.cause : error);\n }\n}\n\nasync function pushCapture(\n ctx: CaptureContext,\n story: StoryEntry,\n viewport: Viewport,\n captures: RenderedSnapshot[],\n failures: RenderResult[\"failures\"],\n input: RunInput,\n active: ActiveRun,\n): Promise<void> {\n const started = performance.now();\n const result = await screenshotWithRetry(ctx, story, viewport, input, active);\n const snapshot: RenderedSnapshot = {\n story: result.story,\n viewportName: viewport.name,\n viewport,\n screenshot: result.screenshot,\n durationMs: Math.round(performance.now() - started),\n ...(result.a11yViolations.length > 0 ? { a11y: result.a11yViolations } : {}),\n };\n // Streaming: hand it over and drop the buffer rather than holding every screenshot.\n if (input.onCapture) await input.onCapture(snapshot);\n else captures.push(snapshot);\n if (result.a11yViolations.length > 0)\n handleA11y(input, failures, story, viewport, result.a11yViolations);\n}\n\n/** Retry capture only: delivery and accessibility reporting must never be repeated. */\nexport async function screenshotWithRetry(\n ctx: CaptureContext,\n story: StoryEntry,\n viewport: Viewport,\n input: RunInput,\n active: ActiveRun,\n): ReturnType<typeof captureScreenshot> {\n try {\n const result = await captureScreenshot(ctx, story, viewport);\n if (active.cancelled || active.abort?.signal.aborted) throw new Error(\"Capture cancelled\");\n return result;\n } catch (error) {\n if (\n !input.retryRemoteCapture ||\n !(error instanceof TransientCaptureError) ||\n active.cancelled ||\n active.abort?.signal.aborted ||\n !ctx.browser.isConnected()\n )\n throw error;\n return await freshScreenshot(ctx, story, viewport, input, active, error);\n }\n}\n\nasync function freshScreenshot(\n ctx: CaptureContext,\n story: StoryEntry,\n viewport: Viewport,\n input: RunInput,\n active: ActiveRun,\n firstError: TransientCaptureError,\n): ReturnType<typeof captureScreenshot> {\n const detail = {\n storyId: story.id,\n viewport: viewport.name,\n phase: firstError.phase,\n attempt: 2,\n };\n input.logger?.warn(detail, \"retrying remote capture on a fresh page\");\n try {\n const result = await captureScreenshot(ctx, story, viewport, true);\n if (active.cancelled || active.abort?.signal.aborted)\n throw new Error(\"Capture cancelled\", { cause: firstError });\n input.logger?.info(detail, \"remote capture retry succeeded\");\n return result;\n } catch (retryError) {\n input.logger?.warn(detail, \"remote capture retry failed\");\n throw retryError;\n }\n}\nfunction handleA11y(\n input: PlaywrightRenderInput,\n failures: RenderResult[\"failures\"],\n story: StoryEntry,\n viewport: Viewport,\n violations: string[],\n): void {\n failures.push({\n storyId: story.id,\n viewportName: viewport.name,\n error: `a11y: ${violations.join(\"; \")}`,\n });\n input.logger?.warn(\n { storyId: story.id, viewport: viewport.name, violations },\n \"a11y violations found\",\n );\n}\n\nfunction pushRenderFailure(\n failures: RenderResult[\"failures\"],\n input: PlaywrightRenderInput,\n storyId: string,\n viewportName: string,\n error: unknown,\n): void {\n failures.push({ storyId, viewportName, error: messageOf(error) });\n input.logger?.error({ storyId, viewport: viewportName, err: error }, \"render failed for story\");\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"pipeline.mjs","names":[],"sources":["../src/pipeline.ts"],"sourcesContent":["/** Render orchestration: static server, browser, and per-story captures. */\n\nimport {\n TargetLaunchError,\n engineOf,\n logRemoteSizing,\n type CaptureEnvironment,\n type CaptureTarget,\n type RemoteTarget,\n type RenderResult,\n type RenderedSnapshot,\n type StoryEntry,\n type StorySourceAdapter,\n type Viewport,\n} from \"@storyshelf/core/adapter/capture-runner\";\nimport { StorybookAdapter, resolveStoryViewports } from \"@storyshelf/core/capture\";\nimport type { StoryParameters } from \"@storyshelf/core/capture\";\nimport {\n DEFAULT_RECYCLE_BROWSER_EVERY,\n chunk,\n describeSizing,\n createPageLimiter,\n detectCaptureEnvironment,\n isTimeoutError,\n sessionGateFor,\n storybookBase,\n type SessionGate,\n} from \"@storyshelf/core/utils\";\nimport type { Browser } from \"playwright-core\";\nimport { finishing, messageOf, outcomeOf, safeCloseBrowser, safeCloseServer } from \"./browser.ts\";\nimport { TransientCaptureError } from \"./capture-retry.ts\";\nimport { PagePool } from \"./pages.ts\";\nimport { measureRemote } from \"./remote-environment.ts\";\nimport { createRuntimeParamsState, extractAllParameters } from \"./runtime-params.ts\";\nimport { captureScreenshot } from \"./screenshot.ts\";\nimport { createStaticServer } from \"./static-server.ts\";\nimport type {\n ActiveRun,\n CaptureContext,\n ExtractInput,\n PlaywrightRenderInput,\n RunInput,\n RuntimeParamsPage,\n LaunchContext,\n TargetDriver,\n} from \"./types.ts\";\n\n/** Where the browser finds the Storybook: the loopback server, or the URL core handed over. */\ninterface Serving {\n url: string;\n close(): Promise<void>;\n}\n\n/** Render all story/viewport combinations for a build. */\nexport async function renderAll(\n rawInput: RunInput,\n active: ActiveRun,\n driver: TargetDriver,\n defaultTarget: CaptureTarget,\n): Promise<RenderResult> {\n const { input, name, remote } = planRun(rawInput, driver, defaultTarget);\n assertReachable(name, remote, input.storybookUrl);\n // Launch first: a target that will not start must fail before a static server is bound.\n const launch = async (): Promise<Browser> =>\n await driver.launch(name, input.logger, launchContextOf(input, \"render\", active.abort?.signal));\n const browser = await launch();\n const server = await serve(input, browser);\n logHostSizing(input, name, remote);\n const measured = remote\n ? await measureRemote(browser, remote.remote, {\n logger: input.logger,\n signal: active.abort?.signal,\n }).catch(async (error: unknown) => {\n // A cancel that interrupted the probe releases the lease as cancelled, not failed.\n await Promise.all([\n safeCloseBrowser(browser, outcomeOf(active, true), error),\n safeCloseServer(server),\n ]);\n throw error;\n })\n : undefined;\n const result = await runWithBrowser(\n { ...input, retryRemoteCapture: remote !== undefined },\n active,\n browser,\n server,\n launch,\n );\n // Echo the target so persisted rows are attributable without the request.\n for (const row of [...result.captures, ...result.failures]) row.browser = name;\n return { ...result, environment: measured ?? (await localEnvironment(browser)) };\n}\n\n/** What a remote service's `acquire` is told about the run a browser is for. */\nexport function launchContextOf(\n input: { buildId: string; runId?: string | undefined; shardIndex?: number | undefined },\n purpose: LaunchContext[\"purpose\"],\n signal?: AbortSignal,\n): LaunchContext {\n return {\n buildId: input.buildId,\n runId: input.runId ?? input.buildId,\n shardIndex: input.shardIndex,\n purpose,\n signal,\n };\n}\n\n/** The input with its target resolved, and the session gate when the target is remote. */\nexport function planRun<\n T extends {\n target?: CaptureTarget | undefined;\n concurrency?: number | undefined;\n logger?: PlaywrightRenderInput[\"logger\"];\n },\n>(\n rawInput: T,\n driver: TargetDriver,\n defaultTarget: CaptureTarget,\n): {\n input: T & {\n target: CaptureTarget;\n gate?: SessionGate | undefined;\n maxPagesPerConnection?: number | undefined;\n };\n name: CaptureTarget;\n remote: RemoteTarget | undefined;\n} {\n const name = rawInput.target ?? defaultTarget;\n const remote = driver.remote(name);\n const gate =\n remote &&\n sessionGateFor(name, remote.remote.sessions, (value) => {\n rawInput.logger?.warn(\n { target: name, value: String(value) },\n \"sessions() returned an invalid value; keeping the last good one\",\n );\n });\n if (remote) {\n logRemoteSizing(rawInput.logger, name, remote.remote, {\n gate,\n concurrency: rawInput.concurrency,\n });\n }\n return {\n input: {\n ...rawInput,\n target: name,\n gate,\n maxPagesPerConnection: remote?.remote.maxPagesPerConnection,\n },\n name,\n remote,\n };\n}\n\n/** Apply the connection's cap without changing the target-wide session budget. */\nexport function connectionConcurrency(input: {\n concurrency?: number | undefined;\n maxPagesPerConnection?: number | undefined;\n}): number | undefined {\n return input.maxPagesPerConnection === undefined\n ? input.concurrency\n : Math.min(input.concurrency ?? Infinity, input.maxPagesPerConnection);\n}\n\n/**\n * Load the preview once and read every story's resolved parameters\n * (`__STORYBOOK_PREVIEW__.extract()`), so the orchestrator can plan modes.\n */\nexport async function extractAll(\n input: ExtractInput,\n driver: TargetDriver,\n defaultTarget: CaptureTarget,\n): Promise<Record<string, StoryParameters>> {\n const first = input.stories[0];\n if (!first) return {};\n const name = input.target ?? defaultTarget;\n assertReachable(name, driver.remote(name), input.storybookUrl);\n const browser = await driver.launch(name, input.logger, launchContextOf(input, \"extract\"));\n const server = await serve(input, browser);\n return await finishing(\n async () => await readParameters(browser, server.url, first.id, input.adapter),\n async (failed, error) => {\n await Promise.all([\n safeCloseBrowser(browser, failed ? \"failed\" : \"finished\", error),\n safeCloseServer(server),\n ]);\n },\n );\n}\n\n/** A target whose browser cannot route back to this host needs a URL; failing here names why. */\nexport function assertReachable(\n target: CaptureTarget,\n remote: RemoteTarget | undefined,\n storybookUrl: string | undefined,\n): void {\n if (remote?.reach === \"url\" && storybookUrl === undefined) {\n throw new TargetLaunchError(\n target,\n \"its browser cannot reach this host, so it needs a Storybook URL (a public origin and a secret to sign it) and none was given\",\n { transient: true },\n );\n }\n if (storybookUrl !== undefined) {\n assertStorybookRoot(target, storybookUrl);\n }\n}\n\n/** The URL must be the build's directory root; refused before any browser connects. */\nfunction assertStorybookRoot(target: CaptureTarget, storybookUrl: string): void {\n try {\n storybookBase(storybookUrl);\n } catch (error) {\n throw new TargetLaunchError(target, error instanceof Error ? error.message : String(error));\n }\n}\n\n/** The loopback server, or the URL given; a server that cannot start closes the browser it was for. */\nexport async function serve(\n input: Pick<RunInput, \"storybookDir\" | \"storybookUrl\" | \"staticCompression\">,\n browser: Browser,\n): Promise<Serving> {\n if (input.storybookUrl !== undefined) {\n return {\n url: storybookBase(input.storybookUrl),\n close: async () => {\n await Promise.resolve();\n },\n };\n }\n return await createStaticServer(input.storybookDir, {\n compression: input.staticCompression,\n }).catch(async (error: unknown) => {\n await safeCloseBrowser(browser, \"failed\", error);\n throw error;\n });\n}\n\nasync function readParameters(\n browser: Browser,\n baseUrl: string,\n storyId: string,\n custom: StorySourceAdapter | undefined,\n): Promise<Record<string, StoryParameters>> {\n const adapter: StorySourceAdapter = custom ?? new StorybookAdapter();\n const page = await browser.newPage();\n await page.goto(adapter.buildUrl(baseUrl, storyId), { waitUntil: \"networkidle\" });\n await adapter.waitForReady?.(page);\n return await extractAllParameters(page as unknown as RuntimeParamsPage);\n}\n\n/** The driver's browser version; undefined for a driver (or test double) that does not report one. */\nfunction browserVersionOf(browser: Browser): string | undefined {\n return typeof browser.version === \"function\" ? browser.version() : undefined;\n}\n\n/** This host's environment: a local browser renders on the machine the runner runs on. */\nasync function localEnvironment(browser: Browser): Promise<CaptureEnvironment> {\n return await detectCaptureEnvironment(browserVersionOf(browser));\n}\n\nasync function runWithBrowser(\n input: RunInput,\n active: ActiveRun,\n browser: Browser,\n server: Serving,\n relaunch: () => Promise<Browser>,\n): Promise<RenderResult> {\n active.browser = browser;\n const ctx = buildContext(browser, server.url, input);\n const captures: RenderedSnapshot[] = [];\n const failures: RenderResult[\"failures\"] = [];\n return await finishing(\n async () => {\n await runAllCaptures(input, active, ctx, captures, failures, relaunch);\n return { captures, failures };\n },\n async (failed, error) => {\n active.browser = null;\n await ctx.pagePool?.dispose();\n // ctx.browser is the latest one: recycling replaces the browser mid-run.\n await Promise.all([\n safeCloseBrowser(ctx.browser, outcomeOf(active, failed), error),\n safeCloseServer(server),\n ]);\n },\n );\n}\n\nfunction buildContext(browser: Browser, baseUrl: string, input: RunInput): CaptureContext {\n return {\n browser,\n adapter: input.adapter ?? new StorybookAdapter(),\n baseUrl,\n navigationTimeoutMs: input.navigationTimeoutMs,\n executePlay: input.executePlay,\n playTimeoutMs: input.playTimeoutMs,\n runA11y: input.runA11y,\n ...(input.logger ? { logger: input.logger } : {}),\n runtimeParams: createRuntimeParamsState(),\n ...(input.reusePages ? { pagePool: new PagePool(browser) } : {}),\n };\n}\n\nasync function runAllCaptures(\n input: RunInput,\n active: ActiveRun,\n ctx: CaptureContext,\n captures: RenderedSnapshot[],\n failures: RenderResult[\"failures\"],\n relaunch: () => Promise<Browser>,\n): Promise<void> {\n // Bounded: one page per story x viewport all at once exhausts memory on large Storybooks.\n const jobs = input.stories.flatMap((story) =>\n resolveStoryViewports(story, input.viewports).map((viewport) => ({ story, viewport })),\n );\n // Batches drain in-flight pages, so the browser can be swapped between them.\n const batches = chunk(jobs, input.recycleBrowserEvery ?? DEFAULT_RECYCLE_BROWSER_EVERY);\n const pages = createPageLimiter({\n concurrency: connectionConcurrency(input),\n ...(input.gate === undefined ? {} : { gate: input.gate }),\n browser: engineOf(input.target ?? \"chromium\"),\n logger: input.logger,\n });\n try {\n await runBatches(batches, pages, input, active, ctx, { captures, failures, relaunch });\n } finally {\n pages.dispose();\n }\n}\n\nasync function runBatches(\n batches: { story: StoryEntry; viewport: Viewport }[][],\n pages: ReturnType<typeof createPageLimiter>,\n input: RunInput,\n active: ActiveRun,\n ctx: CaptureContext,\n out: {\n captures: RenderedSnapshot[];\n failures: RenderResult[\"failures\"];\n relaunch: () => Promise<Browser>;\n },\n): Promise<void> {\n for (const [index, batch] of batches.entries()) {\n if (index > 0) {\n // oxlint-disable-next-line eslint/no-await-in-loop -- recycle only between drained batches\n await recycleBrowser(active, ctx, out.relaunch, input.logger);\n }\n // oxlint-disable-next-line eslint/no-await-in-loop -- batches run one after another\n await pages.each(\n batch,\n async (job) => await captureOne(input, active, ctx, job.story, job.viewport, out),\n );\n }\n}\n\n/** Swap in a fresh browser; the old one is closed first so two never coexist. */\nasync function recycleBrowser(\n active: ActiveRun,\n ctx: CaptureContext,\n relaunch: () => Promise<Browser>,\n logger: PlaywrightRenderInput[\"logger\"],\n): Promise<void> {\n if (active.cancelled) throw new Error(\"Capture cancelled\");\n await ctx.pagePool?.dispose();\n await safeCloseBrowser(ctx.browser);\n // A cancel that arrived while the old browser closed must not start a new one.\n if (active.cancelled) throw new Error(\"Capture cancelled\");\n ctx.browser = await relaunch();\n if (ctx.pagePool) ctx.pagePool = new PagePool(ctx.browser);\n active.browser = ctx.browser;\n logger?.info({}, \"browser recycled\");\n}\n\n/** Capture one story x viewport; resolves to whether it timed out (failures are recorded, not thrown). */\nasync function captureOne(\n input: RunInput,\n active: ActiveRun,\n ctx: CaptureContext,\n story: StoryEntry,\n viewport: Viewport,\n out: { captures: RenderedSnapshot[]; failures: RenderResult[\"failures\"] },\n): Promise<boolean> {\n if (active.cancelled) throw new Error(\"Capture cancelled\");\n try {\n await pushCapture(ctx, story, viewport, out.captures, out.failures, input, active);\n return false;\n } catch (error) {\n pushRenderFailure(out.failures, input, story.id, viewport.name, error);\n return isTimeoutError(error instanceof TransientCaptureError ? error.cause : error);\n }\n}\n\nasync function pushCapture(\n ctx: CaptureContext,\n story: StoryEntry,\n viewport: Viewport,\n captures: RenderedSnapshot[],\n failures: RenderResult[\"failures\"],\n input: RunInput,\n active: ActiveRun,\n): Promise<void> {\n const started = performance.now();\n const result = await screenshotWithRetry(ctx, story, viewport, input, active);\n const snapshot: RenderedSnapshot = {\n story: result.story,\n viewportName: viewport.name,\n viewport,\n screenshot: result.screenshot,\n durationMs: Math.round(performance.now() - started),\n ...(result.a11yViolations.length > 0 ? { a11y: result.a11yViolations } : {}),\n };\n // Streaming: hand it over and drop the buffer rather than holding every screenshot.\n if (input.onCapture) await input.onCapture(snapshot);\n else captures.push(snapshot);\n if (result.a11yViolations.length > 0)\n handleA11y(input, failures, story, viewport, result.a11yViolations);\n}\n\n/** Retry capture only: delivery and accessibility reporting must never be repeated. */\nexport async function screenshotWithRetry(\n ctx: CaptureContext,\n story: StoryEntry,\n viewport: Viewport,\n input: RunInput,\n active: ActiveRun,\n): ReturnType<typeof captureScreenshot> {\n try {\n const result = await captureScreenshot(ctx, story, viewport);\n if (active.cancelled || active.abort?.signal.aborted) throw new Error(\"Capture cancelled\");\n return result;\n } catch (error) {\n if (\n !input.retryRemoteCapture ||\n !(error instanceof TransientCaptureError) ||\n active.cancelled ||\n active.abort?.signal.aborted ||\n !ctx.browser.isConnected()\n )\n throw error;\n return await freshScreenshot(ctx, story, viewport, input, active, error);\n }\n}\n\nasync function freshScreenshot(\n ctx: CaptureContext,\n story: StoryEntry,\n viewport: Viewport,\n input: RunInput,\n active: ActiveRun,\n firstError: TransientCaptureError,\n): ReturnType<typeof captureScreenshot> {\n const detail = {\n storyId: story.id,\n viewport: viewport.name,\n phase: firstError.phase,\n attempt: 2,\n };\n input.logger?.warn(detail, \"retrying remote capture on a fresh page\");\n try {\n const result = await captureScreenshot(ctx, story, viewport, true);\n if (active.cancelled || active.abort?.signal.aborted)\n throw new Error(\"Capture cancelled\", { cause: firstError });\n input.logger?.info(detail, \"remote capture retry succeeded\");\n return result;\n } catch (retryError) {\n input.logger?.warn(detail, \"remote capture retry failed\");\n throw retryError;\n }\n}\nfunction handleA11y(\n input: PlaywrightRenderInput,\n failures: RenderResult[\"failures\"],\n story: StoryEntry,\n viewport: Viewport,\n violations: string[],\n): void {\n failures.push({\n storyId: story.id,\n viewportName: viewport.name,\n error: `a11y: ${violations.join(\"; \")}`,\n });\n input.logger?.warn(\n { storyId: story.id, viewport: viewport.name, violations },\n \"a11y violations found\",\n );\n}\n\nfunction pushRenderFailure(\n failures: RenderResult[\"failures\"],\n input: PlaywrightRenderInput,\n storyId: string,\n viewportName: string,\n error: unknown,\n): void {\n failures.push({ storyId, viewportName, error: messageOf(error) });\n input.logger?.error({ storyId, viewport: viewportName, err: error }, \"render failed for story\");\n}\n\n/** The host's sizing of a capture; a remote target runs elsewhere, so nothing is measured here. */\nfunction logHostSizing(\n input: { logger?: PlaywrightRenderInput[\"logger\"]; concurrency?: number | undefined },\n name: CaptureTarget,\n remote: RemoteTarget | undefined,\n): void {\n if (!remote) {\n input.logger?.info(describeSizing({ ...input, browser: engineOf(name) }), \"capture sizing\");\n }\n}\n"],"mappings":"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"}
|
package/dist/remote/azure.d.mts
CHANGED
|
@@ -18,7 +18,10 @@ interface AzurePlaywrightOptions {
|
|
|
18
18
|
/** Engines to offer (default all three). */
|
|
19
19
|
engines?: readonly BrowserName[];
|
|
20
20
|
}
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Targets on a workspace: `chromium@azure`, `firefox@azure`, `webkit@azure`. Experimental: not
|
|
23
|
+
* verified against live infrastructure.
|
|
24
|
+
*/
|
|
22
25
|
declare function azurePlaywright(options: AzurePlaywrightOptions): RemoteBrowser;
|
|
23
26
|
export { AzurePlaywrightOptions, azurePlaywright };
|
|
24
27
|
//# sourceMappingURL=azure.d.mts.map
|
package/dist/remote/azure.mjs
CHANGED
|
@@ -3,7 +3,10 @@ import "node:url";
|
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { remote } from "./remote.mjs";
|
|
5
5
|
// src/remote/azure.ts
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Targets on a workspace: `chromium@azure`, `firefox@azure`, `webkit@azure`. Experimental: not
|
|
8
|
+
* verified against live infrastructure.
|
|
9
|
+
*/
|
|
7
10
|
function azurePlaywright(options) {
|
|
8
11
|
const os = options.os ?? "linux";
|
|
9
12
|
return remote({
|
|
@@ -23,7 +26,8 @@ function azurePlaywright(options) {
|
|
|
23
26
|
headers: { authorization: `Bearer ${options.token}` }
|
|
24
27
|
},
|
|
25
28
|
sessions: options.sessions ?? 1,
|
|
26
|
-
platform: os
|
|
29
|
+
platform: os,
|
|
30
|
+
experimental: true
|
|
27
31
|
});
|
|
28
32
|
}
|
|
29
33
|
function withParams(endpoint, params) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"azure.mjs","names":[],"sources":["../../src/remote/azure.ts"],"sourcesContent":["/** Microsoft Azure Playwright Workspaces (Azure App Testing). */\nimport type { BrowserName, RemoteBrowser } from \"@storyshelf/core/adapter/capture-runner\";\nimport { remote } from \"./remote.ts\";\n\n/** Options of {@link azurePlaywright}. */\nexport interface AzurePlaywrightOptions {\n /** The target name half; default `azure`. */\n name?: string;\n /** The workspace's browser endpoint (`PLAYWRIGHT_SERVICE_URL`): `wss://<region>.api.playwright.microsoft.com/playwrightworkspaces/<id>/browsers`. */\n endpoint: string;\n /** Access token (`PLAYWRIGHT_SERVICE_ACCESS_TOKEN`). */\n token: string;\n /** OS of the cloud browsers (default `linux`). */\n os?: \"linux\" | \"windows\";\n /** API version the service is called with (default `2025-09-01`). */\n apiVersion?: string;\n /** Parallel pages (the workspace's parallel-worker limit; default 1). */\n sessions?: number;\n /** Engines to offer (default all three). */\n engines?: readonly BrowserName[];\n}\n\n
|
|
1
|
+
{"version":3,"file":"azure.mjs","names":[],"sources":["../../src/remote/azure.ts"],"sourcesContent":["/** Microsoft Azure Playwright Workspaces (Azure App Testing). */\nimport type { BrowserName, RemoteBrowser } from \"@storyshelf/core/adapter/capture-runner\";\nimport { remote } from \"./remote.ts\";\n\n/** Options of {@link azurePlaywright}. */\nexport interface AzurePlaywrightOptions {\n /** The target name half; default `azure`. */\n name?: string;\n /** The workspace's browser endpoint (`PLAYWRIGHT_SERVICE_URL`): `wss://<region>.api.playwright.microsoft.com/playwrightworkspaces/<id>/browsers`. */\n endpoint: string;\n /** Access token (`PLAYWRIGHT_SERVICE_ACCESS_TOKEN`). */\n token: string;\n /** OS of the cloud browsers (default `linux`). */\n os?: \"linux\" | \"windows\";\n /** API version the service is called with (default `2025-09-01`). */\n apiVersion?: string;\n /** Parallel pages (the workspace's parallel-worker limit; default 1). */\n sessions?: number;\n /** Engines to offer (default all three). */\n engines?: readonly BrowserName[];\n}\n\n/**\n * Targets on a workspace: `chromium@azure`, `firefox@azure`, `webkit@azure`. Experimental: not\n * verified against live infrastructure.\n */\nexport function azurePlaywright(options: AzurePlaywrightOptions): RemoteBrowser {\n const os = options.os ?? \"linux\";\n return remote({\n name: options.name ?? \"azure\",\n engines: options.engines ?? [\"chromium\", \"firefox\", \"webkit\"],\n protocol: \"playwright\",\n // The service routes requests for the host's loopback back through the connection.\n reach: \"loopback\",\n connect: {\n endpoint: withParams(options.endpoint, {\n os,\n \"api-version\": options.apiVersion ?? \"2025-09-01\",\n }),\n headers: { authorization: `Bearer ${options.token}` },\n },\n sessions: options.sessions ?? 1,\n platform: os,\n experimental: true,\n });\n}\n\nfunction withParams(endpoint: string, params: Record<string, string>): string {\n const url = new URL(endpoint);\n for (const [key, value] of Object.entries(params)) {\n if (!url.searchParams.has(key)) url.searchParams.set(key, value);\n }\n return url.toString();\n}\n"],"mappings":";;;;;;;;;AA0BA,SAAgB,gBAAgB,SAAgD;CAC9E,MAAM,KAAK,QAAQ,MAAM;CACzB,OAAO,OAAO;EACZ,MAAM,QAAQ,QAAQ;EACtB,SAAS,QAAQ,WAAW;GAAC;GAAY;GAAW;EAAQ;EAC5D,UAAU;EAEV,OAAO;EACP,SAAS;GACP,UAAU,WAAW,QAAQ,UAAU;IACrC;IACA,eAAe,QAAQ,cAAc;GACvC,CAAC;GACD,SAAS,EAAE,eAAe,UAAU,QAAQ,QAAQ;EACtD;EACA,UAAU,QAAQ,YAAY;EAC9B,UAAU;EACV,cAAc;CAChB,CAAC;AACH;AAEA,SAAS,WAAW,UAAkB,QAAwC;CAC5E,MAAM,MAAM,IAAI,IAAI,QAAQ;CAC5B,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAC9C,IAAI,CAAC,IAAI,aAAa,IAAI,GAAG,GAAG,IAAI,aAAa,IAAI,KAAK,KAAK;CAEjE,OAAO,IAAI,SAAS;AACtB"}
|
|
@@ -24,7 +24,8 @@ interface BrowserStackOptions {
|
|
|
24
24
|
/**
|
|
25
25
|
* Targets on BrowserStack, for example `chromium@browserstack`. BrowserStack cannot route back to
|
|
26
26
|
* this host, so each target needs a signed Storybook URL (ADR 0007, decision 5). The credentials
|
|
27
|
-
* travel in the endpoint's capabilities and are scrubbed from every message and log.
|
|
27
|
+
* travel in the endpoint's capabilities and are scrubbed from every message and log. Experimental:
|
|
28
|
+
* not verified against live infrastructure.
|
|
28
29
|
*/
|
|
29
30
|
declare function browserstack(options: BrowserStackOptions): RemoteBrowser;
|
|
30
31
|
export { BrowserStackOptions, browserstack };
|
|
@@ -14,7 +14,8 @@ const BROWSER = {
|
|
|
14
14
|
/**
|
|
15
15
|
* Targets on BrowserStack, for example `chromium@browserstack`. BrowserStack cannot route back to
|
|
16
16
|
* this host, so each target needs a signed Storybook URL (ADR 0007, decision 5). The credentials
|
|
17
|
-
* travel in the endpoint's capabilities and are scrubbed from every message and log.
|
|
17
|
+
* travel in the endpoint's capabilities and are scrubbed from every message and log. Experimental:
|
|
18
|
+
* not verified against live infrastructure.
|
|
18
19
|
*/
|
|
19
20
|
function browserstack(options) {
|
|
20
21
|
return remote({
|
|
@@ -28,7 +29,8 @@ function browserstack(options) {
|
|
|
28
29
|
reach: "url",
|
|
29
30
|
connect: ({ engine }) => ({ endpoint: `wss://cdp.browserstack.com/playwright?caps=${encodeURIComponent(JSON.stringify(capabilities(options, engine)))}` }),
|
|
30
31
|
sessions: options.sessions ?? 1,
|
|
31
|
-
platform: options.os?.name.toLowerCase().replace(/^os x$/u, "macos")
|
|
32
|
+
platform: options.os?.name.toLowerCase().replace(/^os x$/u, "macos"),
|
|
33
|
+
experimental: true
|
|
32
34
|
});
|
|
33
35
|
}
|
|
34
36
|
function capabilities(options, engine) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browserstack.mjs","names":[],"sources":["../../src/remote/browserstack.ts"],"sourcesContent":["/** BrowserStack Automate over Playwright's protocol. */\nimport type { BrowserName, RemoteBrowser } from \"@storyshelf/core/adapter/capture-runner\";\nimport { playwrightVersion } from \"../version.ts\";\nimport { remote } from \"./remote.ts\";\n\n/** Options of {@link browserstack}. */\nexport interface BrowserStackOptions {\n /** The target name half; default `browserstack`. A second entry needs its own. */\n name?: string;\n username: string;\n accessKey: string;\n /** OS the browsers run on, as BrowserStack names it (`{ name: \"Windows\", version: \"11\" }`). */\n os?: { name: string; version: string };\n /** Browser version (default `latest`). Pin it: a moving build is a baseline migration. */\n browserVersion?: string;\n /** Engines to offer (default `chromium`, `firefox`, `webkit`; `chrome` is real Chrome). */\n engines?: readonly BrowserName[];\n /** Parallel sessions of the plan (default 1). */\n sessions?: number;\n /** Build name shown in BrowserStack's dashboard. */\n build?: string;\n}\n\n/** BrowserStack's `browser` capability per engine. */\nconst BROWSER: Record<BrowserName, string> = {\n chromium: \"playwright-chromium\",\n firefox: \"playwright-firefox\",\n webkit: \"playwright-webkit\",\n chrome: \"chrome\",\n};\n\n/**\n * Targets on BrowserStack, for example `chromium@browserstack`. BrowserStack cannot route back to\n * this host, so each target needs a signed Storybook URL (ADR 0007, decision 5). The credentials\n * travel in the endpoint's capabilities and are scrubbed from every message and log.\n */\nexport function browserstack(options: BrowserStackOptions): RemoteBrowser {\n return remote({\n name: options.name ?? \"browserstack\",\n engines: options.engines ?? [\"chromium\", \"firefox\", \"webkit\"],\n protocol: \"playwright\",\n reach: \"url\",\n connect: ({ engine }) => ({\n endpoint: `wss://cdp.browserstack.com/playwright?caps=${encodeURIComponent(\n JSON.stringify(capabilities(options, engine)),\n )}`,\n }),\n sessions: options.sessions ?? 1,\n platform: options.os?.name.toLowerCase().replace(/^os x$/u, \"macos\"),\n });\n}\n\nfunction capabilities(options: BrowserStackOptions, engine: BrowserName): Record<string, unknown> {\n return {\n browser: BROWSER[engine],\n browser_version: options.browserVersion ?? \"latest\",\n ...(options.os === undefined ? {} : { os: options.os.name, os_version: options.os.version }),\n \"browserstack.username\": options.username,\n \"browserstack.accessKey\": options.accessKey,\n \"client.playwrightVersion\": playwrightVersion(),\n ...(options.build === undefined ? {} : { build: options.build }),\n };\n}\n"],"mappings":";;;;;;;AAwBA,MAAM,UAAuC;CAC3C,UAAU;CACV,SAAS;CACT,QAAQ;CACR,QAAQ;AACV
|
|
1
|
+
{"version":3,"file":"browserstack.mjs","names":[],"sources":["../../src/remote/browserstack.ts"],"sourcesContent":["/** BrowserStack Automate over Playwright's protocol. */\nimport type { BrowserName, RemoteBrowser } from \"@storyshelf/core/adapter/capture-runner\";\nimport { playwrightVersion } from \"../version.ts\";\nimport { remote } from \"./remote.ts\";\n\n/** Options of {@link browserstack}. */\nexport interface BrowserStackOptions {\n /** The target name half; default `browserstack`. A second entry needs its own. */\n name?: string;\n username: string;\n accessKey: string;\n /** OS the browsers run on, as BrowserStack names it (`{ name: \"Windows\", version: \"11\" }`). */\n os?: { name: string; version: string };\n /** Browser version (default `latest`). Pin it: a moving build is a baseline migration. */\n browserVersion?: string;\n /** Engines to offer (default `chromium`, `firefox`, `webkit`; `chrome` is real Chrome). */\n engines?: readonly BrowserName[];\n /** Parallel sessions of the plan (default 1). */\n sessions?: number;\n /** Build name shown in BrowserStack's dashboard. */\n build?: string;\n}\n\n/** BrowserStack's `browser` capability per engine. */\nconst BROWSER: Record<BrowserName, string> = {\n chromium: \"playwright-chromium\",\n firefox: \"playwright-firefox\",\n webkit: \"playwright-webkit\",\n chrome: \"chrome\",\n};\n\n/**\n * Targets on BrowserStack, for example `chromium@browserstack`. BrowserStack cannot route back to\n * this host, so each target needs a signed Storybook URL (ADR 0007, decision 5). The credentials\n * travel in the endpoint's capabilities and are scrubbed from every message and log. Experimental:\n * not verified against live infrastructure.\n */\nexport function browserstack(options: BrowserStackOptions): RemoteBrowser {\n return remote({\n name: options.name ?? \"browserstack\",\n engines: options.engines ?? [\"chromium\", \"firefox\", \"webkit\"],\n protocol: \"playwright\",\n reach: \"url\",\n connect: ({ engine }) => ({\n endpoint: `wss://cdp.browserstack.com/playwright?caps=${encodeURIComponent(\n JSON.stringify(capabilities(options, engine)),\n )}`,\n }),\n sessions: options.sessions ?? 1,\n platform: options.os?.name.toLowerCase().replace(/^os x$/u, \"macos\"),\n experimental: true,\n });\n}\n\nfunction capabilities(options: BrowserStackOptions, engine: BrowserName): Record<string, unknown> {\n return {\n browser: BROWSER[engine],\n browser_version: options.browserVersion ?? \"latest\",\n ...(options.os === undefined ? {} : { os: options.os.name, os_version: options.os.version }),\n \"browserstack.username\": options.username,\n \"browserstack.accessKey\": options.accessKey,\n \"client.playwrightVersion\": playwrightVersion(),\n ...(options.build === undefined ? {} : { build: options.build }),\n };\n}\n"],"mappings":";;;;;;;AAwBA,MAAM,UAAuC;CAC3C,UAAU;CACV,SAAS;CACT,QAAQ;CACR,QAAQ;AACV;;;;;;;AAQA,SAAgB,aAAa,SAA6C;CACxE,OAAO,OAAO;EACZ,MAAM,QAAQ,QAAQ;EACtB,SAAS,QAAQ,WAAW;GAAC;GAAY;GAAW;EAAQ;EAC5D,UAAU;EACV,OAAO;EACP,UAAU,EAAE,cAAc,EACxB,UAAU,8CAA8C,mBACtD,KAAK,UAAU,aAAa,SAAS,MAAM,CAAC,CAC9C,IACF;EACA,UAAU,QAAQ,YAAY;EAC9B,UAAU,QAAQ,IAAI,KAAK,YAAY,CAAC,CAAC,QAAQ,WAAW,OAAO;EACnE,cAAc;CAChB,CAAC;AACH;AAEA,SAAS,aAAa,SAA8B,QAA8C;CAChG,OAAO;EACL,SAAS,QAAQ;EACjB,iBAAiB,QAAQ,kBAAkB;EAC3C,GAAI,QAAQ,OAAO,KAAA,IAAY,CAAC,IAAI;GAAE,IAAI,QAAQ,GAAG;GAAM,YAAY,QAAQ,GAAG;EAAQ;EAC1F,yBAAyB,QAAQ;EACjC,0BAA0B,QAAQ;EAClC,4BAA4B,kBAAkB;EAC9C,GAAI,QAAQ,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO,QAAQ,MAAM;CAChE;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@storyshelf/runner-playwright",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0-next-d2cc082",
|
|
4
4
|
"description": "Pure Playwright CaptureRunner implementation for StoryShelf: renders Storybook screenshots in-process (no db/storage coupling).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"playwright",
|
|
@@ -59,15 +59,15 @@
|
|
|
59
59
|
"test:flake": "vitest run --repeats=20 --maxWorkers=1 --no-file-parallelism src/capture-runner.test.ts src/settle.test.ts src/screenshot.test.ts"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@storyshelf/core": "0.
|
|
62
|
+
"@storyshelf/core": "0.9.0-next-d2cc082",
|
|
63
63
|
"playwright-core": "1.63.0"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@hono/node-server": "^1.17.0",
|
|
67
|
-
"@storyshelf/app": "0.
|
|
68
|
-
"@storyshelf/auth": "0.
|
|
69
|
-
"@storyshelf/db-sqlite": "0.
|
|
70
|
-
"@storyshelf/storage-local": "0.
|
|
67
|
+
"@storyshelf/app": "0.9.0-next-d2cc082",
|
|
68
|
+
"@storyshelf/auth": "0.9.0-next-d2cc082",
|
|
69
|
+
"@storyshelf/db-sqlite": "0.9.0-next-d2cc082",
|
|
70
|
+
"@storyshelf/storage-local": "0.9.0-next-d2cc082",
|
|
71
71
|
"@types/node": "^26.0.0",
|
|
72
72
|
"@vitest/coverage-v8": "^4.0.0",
|
|
73
73
|
"adm-zip": "0.6.1",
|
|
@@ -80,14 +80,5 @@
|
|
|
80
80
|
},
|
|
81
81
|
"engines": {
|
|
82
82
|
"node": ">=24"
|
|
83
|
-
},
|
|
84
|
-
"jsr": {
|
|
85
|
-
"runtimeCompat": {
|
|
86
|
-
"node": true,
|
|
87
|
-
"deno": true,
|
|
88
|
-
"browser": false,
|
|
89
|
-
"workerd": false,
|
|
90
|
-
"bun": true
|
|
91
|
-
}
|
|
92
83
|
}
|
|
93
84
|
}
|