@storyshelf/runner-playwright 0.6.1 → 0.7.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 +48 -5
- package/dist/a11y.mjs +18 -20
- package/dist/a11y.mjs.map +1 -1
- package/dist/availability.mjs +65 -0
- package/dist/availability.mjs.map +1 -0
- package/dist/browser.mjs +4 -2
- package/dist/browser.mjs.map +1 -1
- package/dist/capture-runner.d.mts +18 -3
- package/dist/capture-runner.mjs +72 -13
- package/dist/capture-runner.mjs.map +1 -1
- package/dist/connect.mjs +79 -0
- package/dist/connect.mjs.map +1 -0
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +2 -1
- package/dist/launch.mjs +64 -0
- package/dist/launch.mjs.map +1 -0
- package/dist/pages.d.mts +4 -0
- package/dist/pages.mjs +91 -0
- package/dist/pages.mjs.map +1 -0
- package/dist/pipeline.mjs +154 -26
- package/dist/pipeline.mjs.map +1 -1
- package/dist/play.mjs +14 -1
- package/dist/play.mjs.map +1 -1
- package/dist/remote/azure.d.mts +26 -0
- package/dist/remote/azure.mjs +37 -0
- package/dist/remote/azure.mjs.map +1 -0
- package/dist/remote/browserstack.d.mts +33 -0
- package/dist/remote/browserstack.mjs +52 -0
- package/dist/remote/browserstack.mjs.map +1 -0
- package/dist/remote/index.d.mts +7 -0
- package/dist/remote/index.mjs +8 -0
- package/dist/remote/playwright-server.d.mts +40 -0
- package/dist/remote/playwright-server.mjs +34 -0
- package/dist/remote/playwright-server.mjs.map +1 -0
- package/dist/remote/remote.d.mts +22 -0
- package/dist/remote/remote.mjs +38 -0
- package/dist/remote/remote.mjs.map +1 -0
- package/dist/remote-environment.mjs +36 -0
- package/dist/remote-environment.mjs.map +1 -0
- package/dist/runtime-params.mjs +8 -1
- package/dist/runtime-params.mjs.map +1 -1
- package/dist/screenshot.mjs +40 -18
- package/dist/screenshot.mjs.map +1 -1
- package/dist/settle.mjs +150 -0
- package/dist/settle.mjs.map +1 -0
- package/dist/static-server.mjs +1 -1
- package/dist/targets.d.mts +26 -0
- package/dist/targets.mjs +73 -0
- package/dist/targets.mjs.map +1 -0
- package/dist/tests.mjs +114 -0
- package/dist/tests.mjs.map +1 -0
- package/dist/types.d.mts +5 -3
- package/dist/version.mjs +16 -0
- package/dist/version.mjs.map +1 -0
- package/package.json +17 -8
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"launch.mjs","names":[],"sources":["../src/launch.ts"],"sourcesContent":["/** Starting a local engine, and telling a browser that would not start from one that crashed. */\nimport { TargetLaunchError, type BrowserName } from \"@storyshelf/core/adapter/capture-runner\";\nimport type { Browser } from \"playwright-core\";\nimport type { LocalTarget } from \"./targets.ts\";\n\n/**\n * Low-memory Chromium flags. Deliberately excludes rendering-affecting flags\n * (`--disable-gpu`, `--single-process`): they can shift pixels or crash pages,\n * which would show up as false visual diffs.\n */\nconst CHROMIUM_ARGS = [\n \"--disable-dev-shm-usage\",\n \"--disable-extensions\",\n \"--disable-background-networking\",\n \"--mute-audio\",\n];\n\n/**\n * Playwright closes its browsers on SIGINT/SIGTERM/SIGHUP by default, which fails a render that a\n * graceful drain is still finishing (`TargetClosedError`). The host owns shutdown: it drains, then\n * closes every browser through `teardown`.\n */\nconst HOST_OWNS_SIGNALS = {\n handleSIGINT: false,\n handleSIGTERM: false,\n handleSIGHUP: false,\n} as const;\n\n/**\n * Launch `entry`'s engine. Playwright is loaded here, on first use, not at import: it costs\n * ~150 ms and tens of MB, which a server that only serves reviews or hosts Storybooks should not\n * pay at boot. A failure is a {@link TargetLaunchError}: the browser never started, so no story is\n * at fault and the job fails at once instead of going through crash isolation.\n */\nexport async function launchLocal(entry: LocalTarget): Promise<Browser> {\n try {\n const playwright = await loadPlaywright();\n return await launcherOf(entry.target, playwright)(entry.channel);\n } catch (error) {\n throw new TargetLaunchError(entry.target, firstLine(error), { cause: error });\n }\n}\n\nlet loaded: Promise<typeof import(\"playwright-core\")> | undefined;\n\n/** Load Playwright once, however many renders ask at the same time. */\nexport async function loadPlaywright(): Promise<typeof import(\"playwright-core\")> {\n loaded ??= import(\"playwright-core\");\n return await loaded;\n}\n\nfunction launcherOf(\n engine: BrowserName,\n playwright: typeof import(\"playwright-core\"),\n): (channel: string | undefined) => Promise<Browser> {\n if (engine === \"firefox\") {\n return async () => await playwright.firefox.launch(HOST_OWNS_SIGNALS);\n }\n if (engine === \"webkit\") {\n return async () => await playwright.webkit.launch(HOST_OWNS_SIGNALS);\n }\n // Containers ship a tiny /dev/shm; Chromium crashes (looks like OOM) unless it uses /tmp.\n return async (channel) =>\n await playwright.chromium.launch({\n ...HOST_OWNS_SIGNALS,\n args: CHROMIUM_ARGS,\n ...(channel === undefined ? {} : { channel }),\n });\n}\n\n/** Playwright's launch errors run to a screenful of install hints; the first line says what failed. */\nfunction firstLine(error: unknown): string {\n const text = error instanceof Error ? error.message : String(error);\n return (\n text\n .split(\"\\n\")\n .find((line) => line.trim() !== \"\")\n ?.trim() ?? \"the browser did not start\"\n );\n}\n"],"mappings":";;;;;;;;;;;AAUA,MAAM,gBAAgB;CACpB;CACA;CACA;CACA;AACF;;;;;;AAOA,MAAM,oBAAoB;CACxB,cAAc;CACd,eAAe;CACf,cAAc;AAChB;;;;;;;AAQA,eAAsB,YAAY,OAAsC;CACtE,IAAI;EACF,MAAM,aAAa,MAAM,eAAe;EACxC,OAAO,MAAM,WAAW,MAAM,QAAQ,UAAU,CAAC,CAAC,MAAM,OAAO;CACjE,SAAS,OAAO;EACd,MAAM,IAAI,kBAAkB,MAAM,QAAQ,UAAU,KAAK,GAAG,EAAE,OAAO,MAAM,CAAC;CAC9E;AACF;AAEA,IAAI;;AAGJ,eAAsB,iBAA4D;CAChF,WAAW,OAAO;CAClB,OAAO,MAAM;AACf;AAEA,SAAS,WACP,QACA,YACmD;CACnD,IAAI,WAAW,WACb,OAAO,YAAY,MAAM,WAAW,QAAQ,OAAO,iBAAiB;CAEtE,IAAI,WAAW,UACb,OAAO,YAAY,MAAM,WAAW,OAAO,OAAO,iBAAiB;CAGrE,OAAO,OAAO,YACZ,MAAM,WAAW,SAAS,OAAO;EAC/B,GAAG;EACH,MAAM;EACN,GAAI,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,QAAQ;CAC7C,CAAC;AACL;;AAGA,SAAS,UAAU,OAAwB;CAEzC,QADa,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAA,CAG7D,MAAM,IAAI,CAAC,CACX,MAAM,SAAS,KAAK,KAAK,MAAM,EAAE,CAAC,EACjC,KAAK,KAAK;AAElB"}
|
package/dist/pages.d.mts
ADDED
package/dist/pages.mjs
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { installSettleRecorder } from "./settle.mjs";
|
|
5
|
+
//#region src/pages.ts
|
|
6
|
+
/** Lease a page for `viewport`: pooled when the context has a pool, otherwise a throwaway. */
|
|
7
|
+
async function acquirePage(ctx, viewport) {
|
|
8
|
+
if (ctx.pagePool) return await ctx.pagePool.acquire(viewport);
|
|
9
|
+
const page = await ctx.browser.newPage({ viewport: {
|
|
10
|
+
width: viewport.width,
|
|
11
|
+
height: viewport.height
|
|
12
|
+
} });
|
|
13
|
+
await installSettleRecorder(page);
|
|
14
|
+
return {
|
|
15
|
+
page,
|
|
16
|
+
release: async () => {
|
|
17
|
+
await page.close();
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Reuses pages (each in its own context) across captures to skip per-capture
|
|
23
|
+
* context/page startup and keep Storybook's JS in the HTTP cache. A page is
|
|
24
|
+
* reset between captures (storage and cookies cleared, blank document) and
|
|
25
|
+
* discarded, never reused, after a failed capture.
|
|
26
|
+
*/
|
|
27
|
+
var PagePool = class {
|
|
28
|
+
free = [];
|
|
29
|
+
browser;
|
|
30
|
+
constructor(browser) {
|
|
31
|
+
this.browser = browser;
|
|
32
|
+
}
|
|
33
|
+
async acquire(viewport) {
|
|
34
|
+
const entry = this.free.pop() ?? await this.open(viewport);
|
|
35
|
+
await entry.page.setViewportSize({
|
|
36
|
+
width: viewport.width,
|
|
37
|
+
height: viewport.height
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
page: entry.page,
|
|
41
|
+
release: async (reusable) => {
|
|
42
|
+
if (reusable && await reset(entry)) this.free.push(entry);
|
|
43
|
+
else await closeEntry(entry);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Close every idle page; call before the browser is replaced or closed. */
|
|
48
|
+
async dispose() {
|
|
49
|
+
await Promise.all(this.free.splice(0).map(async (entry) => {
|
|
50
|
+
await closeEntry(entry);
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
async open(viewport) {
|
|
54
|
+
const context = await this.browser.newContext({ viewport: {
|
|
55
|
+
width: viewport.width,
|
|
56
|
+
height: viewport.height
|
|
57
|
+
} });
|
|
58
|
+
const page = await context.newPage();
|
|
59
|
+
await installSettleRecorder(page);
|
|
60
|
+
return {
|
|
61
|
+
context,
|
|
62
|
+
page
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
/** Wipe everything a story could leave behind; false when the page can't be trusted again. */
|
|
67
|
+
async function reset(entry) {
|
|
68
|
+
try {
|
|
69
|
+
await entry.page.evaluate(clearBrowserState);
|
|
70
|
+
await entry.context.clearCookies();
|
|
71
|
+
await entry.page.goto("about:blank");
|
|
72
|
+
return true;
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function closeEntry(entry) {
|
|
78
|
+
await entry.context.close().catch(() => {});
|
|
79
|
+
}
|
|
80
|
+
/** Runs in the page: clears web storage and IndexedDB for the current origin. */
|
|
81
|
+
async function clearBrowserState() {
|
|
82
|
+
const scope = globalThis;
|
|
83
|
+
scope.localStorage.clear();
|
|
84
|
+
scope.sessionStorage.clear();
|
|
85
|
+
const databases = await scope.indexedDB?.databases?.() ?? [];
|
|
86
|
+
for (const database of databases) if (database.name) scope.indexedDB?.deleteDatabase(database.name);
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
89
|
+
export { PagePool, acquirePage };
|
|
90
|
+
|
|
91
|
+
//# sourceMappingURL=pages.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pages.mjs","names":[],"sources":["../src/pages.ts"],"sourcesContent":["/** Page leasing for captures: a fresh page each time, or a small reusable pool (opt-in). */\n\nimport type { Viewport } from \"@storyshelf/core/adapter/capture-runner\";\nimport type { Browser, BrowserContext, Page } from \"playwright-core\";\nimport { installSettleRecorder } from \"./settle.ts\";\n\n/** A page on loan for one capture; `release(false)` discards it instead of reusing it. */\nexport interface PageLease {\n page: Page;\n release(reusable: boolean): Promise<void>;\n}\n\n/** Lease a page for `viewport`: pooled when the context has a pool, otherwise a throwaway. */\nexport async function acquirePage(\n ctx: { browser: Browser; pagePool?: PagePool },\n viewport: Viewport,\n): Promise<PageLease> {\n if (ctx.pagePool) return await ctx.pagePool.acquire(viewport);\n const page = await ctx.browser.newPage({\n viewport: { width: viewport.width, height: viewport.height },\n });\n await installSettleRecorder(page);\n return {\n page,\n release: async () => {\n await page.close();\n },\n };\n}\n\ninterface Entry {\n context: BrowserContext;\n page: Page;\n}\n\n/**\n * Reuses pages (each in its own context) across captures to skip per-capture\n * context/page startup and keep Storybook's JS in the HTTP cache. A page is\n * reset between captures (storage and cookies cleared, blank document) and\n * discarded, never reused, after a failed capture.\n */\nexport class PagePool {\n private readonly free: Entry[] = [];\n\n private readonly browser: Browser;\n\n constructor(browser: Browser) {\n this.browser = browser;\n }\n\n async acquire(viewport: Viewport): Promise<PageLease> {\n const entry = this.free.pop() ?? (await this.open(viewport));\n await entry.page.setViewportSize({ width: viewport.width, height: viewport.height });\n return {\n page: entry.page,\n release: async (reusable) => {\n if (reusable && (await reset(entry))) this.free.push(entry);\n else await closeEntry(entry);\n },\n };\n }\n\n /** Close every idle page; call before the browser is replaced or closed. */\n async dispose(): Promise<void> {\n await Promise.all(\n this.free.splice(0).map(async (entry) => {\n await closeEntry(entry);\n }),\n );\n }\n\n private async open(viewport: Viewport): Promise<Entry> {\n const context = await this.browser.newContext({\n viewport: { width: viewport.width, height: viewport.height },\n });\n const page = await context.newPage();\n await installSettleRecorder(page);\n return { context, page };\n }\n}\n\n/** Wipe everything a story could leave behind; false when the page can't be trusted again. */\nasync function reset(entry: Entry): Promise<boolean> {\n try {\n await entry.page.evaluate(clearBrowserState);\n await entry.context.clearCookies();\n await entry.page.goto(\"about:blank\");\n return true;\n } catch {\n return false;\n }\n}\n\nasync function closeEntry(entry: Entry): Promise<void> {\n await entry.context.close().catch(() => {});\n}\n\n/** Runs in the page: clears web storage and IndexedDB for the current origin. */\nasync function clearBrowserState(): Promise<void> {\n const scope = globalThis as unknown as {\n localStorage: { clear(): void };\n sessionStorage: { clear(): void };\n indexedDB?: {\n databases?: () => Promise<{ name?: string }[]>;\n deleteDatabase(name: string): unknown;\n };\n };\n scope.localStorage.clear();\n scope.sessionStorage.clear();\n const databases = (await scope.indexedDB?.databases?.()) ?? [];\n for (const database of databases) {\n if (database.name) scope.indexedDB?.deleteDatabase(database.name);\n }\n}\n"],"mappings":";;;;;;AAaA,eAAsB,YACpB,KACA,UACoB;CACpB,IAAI,IAAI,UAAU,OAAO,MAAM,IAAI,SAAS,QAAQ,QAAQ;CAC5D,MAAM,OAAO,MAAM,IAAI,QAAQ,QAAQ,EACrC,UAAU;EAAE,OAAO,SAAS;EAAO,QAAQ,SAAS;CAAO,EAC7D,CAAC;CACD,MAAM,sBAAsB,IAAI;CAChC,OAAO;EACL;EACA,SAAS,YAAY;GACnB,MAAM,KAAK,MAAM;EACnB;CACF;AACF;;;;;;;AAaA,IAAa,WAAb,MAAsB;CACpB,OAAiC,CAAC;CAElC;CAEA,YAAY,SAAkB;EAC5B,KAAK,UAAU;CACjB;CAEA,MAAM,QAAQ,UAAwC;EACpD,MAAM,QAAQ,KAAK,KAAK,IAAI,KAAM,MAAM,KAAK,KAAK,QAAQ;EAC1D,MAAM,MAAM,KAAK,gBAAgB;GAAE,OAAO,SAAS;GAAO,QAAQ,SAAS;EAAO,CAAC;EACnF,OAAO;GACL,MAAM,MAAM;GACZ,SAAS,OAAO,aAAa;IAC3B,IAAI,YAAa,MAAM,MAAM,KAAK,GAAI,KAAK,KAAK,KAAK,KAAK;SACrD,MAAM,WAAW,KAAK;GAC7B;EACF;CACF;;CAGA,MAAM,UAAyB;EAC7B,MAAM,QAAQ,IACZ,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC,IAAI,OAAO,UAAU;GACvC,MAAM,WAAW,KAAK;EACxB,CAAC,CACH;CACF;CAEA,MAAc,KAAK,UAAoC;EACrD,MAAM,UAAU,MAAM,KAAK,QAAQ,WAAW,EAC5C,UAAU;GAAE,OAAO,SAAS;GAAO,QAAQ,SAAS;EAAO,EAC7D,CAAC;EACD,MAAM,OAAO,MAAM,QAAQ,QAAQ;EACnC,MAAM,sBAAsB,IAAI;EAChC,OAAO;GAAE;GAAS;EAAK;CACzB;AACF;;AAGA,eAAe,MAAM,OAAgC;CACnD,IAAI;EACF,MAAM,MAAM,KAAK,SAAS,iBAAiB;EAC3C,MAAM,MAAM,QAAQ,aAAa;EACjC,MAAM,MAAM,KAAK,KAAK,aAAa;EACnC,OAAO;CACT,QAAQ;EACN,OAAO;CACT;AACF;AAEA,eAAe,WAAW,OAA6B;CACrD,MAAM,MAAM,QAAQ,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;AAC5C;;AAGA,eAAe,oBAAmC;CAChD,MAAM,QAAQ;CAQd,MAAM,aAAa,MAAM;CACzB,MAAM,eAAe,MAAM;CAC3B,MAAM,YAAa,MAAM,MAAM,WAAW,YAAY,KAAM,CAAC;CAC7D,KAAK,MAAM,YAAY,WACrB,IAAI,SAAS,MAAM,MAAM,WAAW,eAAe,SAAS,IAAI;AAEpE"}
|
package/dist/pipeline.mjs
CHANGED
|
@@ -2,72 +2,200 @@ import "node:path";
|
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { messageOf, safeCloseBrowser, safeCloseServer } from "./browser.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { PagePool } from "./pages.mjs";
|
|
6
|
+
import { measureRemote } from "./remote-environment.mjs";
|
|
7
|
+
import { createRuntimeParamsState, extractAllParameters } from "./runtime-params.mjs";
|
|
6
8
|
import { captureScreenshot } from "./screenshot.mjs";
|
|
7
9
|
import { createStaticServer } from "./static-server.mjs";
|
|
10
|
+
import { TargetLaunchError, engineOf } from "@storyshelf/core/adapter/capture-runner";
|
|
11
|
+
import { DEFAULT_RECYCLE_BROWSER_EVERY, chunk, createPageLimiter, describeSizing, detectCaptureEnvironment, isTimeoutError, sessionGateFor, storybookBase } from "@storyshelf/core/utils";
|
|
8
12
|
import { StorybookAdapter, resolveStoryViewports } from "@storyshelf/core/capture";
|
|
9
|
-
import { chromium, firefox, webkit } from "playwright-core";
|
|
10
13
|
//#region src/pipeline.ts
|
|
14
|
+
/** Render orchestration: static server, browser, and per-story captures. */
|
|
11
15
|
/** Render all story/viewport combinations for a build. */
|
|
12
|
-
async function renderAll(
|
|
13
|
-
const
|
|
14
|
-
|
|
16
|
+
async function renderAll(rawInput, active, driver, defaultTarget) {
|
|
17
|
+
const { input, name, remote } = planRun(rawInput, driver, defaultTarget);
|
|
18
|
+
assertReachable(name, remote, input.storybookUrl);
|
|
19
|
+
const browser = await driver.launch(name, input.logger);
|
|
20
|
+
const server = await serve(input, browser);
|
|
21
|
+
input.logger?.info(describeSizing({
|
|
22
|
+
...input,
|
|
23
|
+
browser: engineOf(name)
|
|
24
|
+
}), "capture sizing");
|
|
25
|
+
const measured = remote ? await measureRemote(browser, remote.remote, input.logger) : void 0;
|
|
26
|
+
const result = await runWithBrowser(input, active, browser, server, async () => await driver.launch(name, input.logger));
|
|
27
|
+
for (const row of [...result.captures, ...result.failures]) row.browser = name;
|
|
28
|
+
return {
|
|
29
|
+
...result,
|
|
30
|
+
environment: measured ?? await localEnvironment(browser)
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** The input with its target resolved, and the session gate when the target is remote. */
|
|
34
|
+
function planRun(rawInput, driver, defaultTarget) {
|
|
35
|
+
const name = rawInput.target ?? defaultTarget;
|
|
36
|
+
const remote = driver.remote(name);
|
|
37
|
+
const gate = remote && sessionGateFor(name, remote.remote.sessions);
|
|
38
|
+
return {
|
|
39
|
+
input: {
|
|
40
|
+
...rawInput,
|
|
41
|
+
target: name,
|
|
42
|
+
gate
|
|
43
|
+
},
|
|
44
|
+
name,
|
|
45
|
+
remote
|
|
46
|
+
};
|
|
15
47
|
}
|
|
16
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Load the preview once and read every story's resolved parameters
|
|
50
|
+
* (`__STORYBOOK_PREVIEW__.extract()`), so the orchestrator can plan modes.
|
|
51
|
+
*/
|
|
52
|
+
async function extractAll(input, driver, defaultTarget) {
|
|
53
|
+
const first = input.stories[0];
|
|
54
|
+
if (!first) return {};
|
|
55
|
+
const name = input.target ?? defaultTarget;
|
|
56
|
+
assertReachable(name, driver.remote(name), input.storybookUrl);
|
|
57
|
+
const browser = await driver.launch(name, input.logger);
|
|
58
|
+
const server = await serve(input, browser);
|
|
59
|
+
try {
|
|
60
|
+
return await readParameters(browser, server.url, first.id, input.adapter);
|
|
61
|
+
} finally {
|
|
62
|
+
await Promise.all([safeCloseBrowser(browser), safeCloseServer(server)]);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** A target whose browser cannot route back to this host needs a URL; failing here names why. */
|
|
66
|
+
function assertReachable(target, remote, storybookUrl) {
|
|
67
|
+
if (remote?.reach === "url" && storybookUrl === void 0) throw new TargetLaunchError(target, "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", { transient: true });
|
|
68
|
+
if (storybookUrl !== void 0) assertStorybookRoot(target, storybookUrl);
|
|
69
|
+
}
|
|
70
|
+
/** The URL must be the build's directory root; refused before any browser connects. */
|
|
71
|
+
function assertStorybookRoot(target, storybookUrl) {
|
|
72
|
+
try {
|
|
73
|
+
storybookBase(storybookUrl);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
throw new TargetLaunchError(target, error instanceof Error ? error.message : String(error));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** The loopback server, or the URL given; a server that cannot start closes the browser it was for. */
|
|
79
|
+
async function serve(input, browser) {
|
|
80
|
+
if (input.storybookUrl !== void 0) return {
|
|
81
|
+
url: storybookBase(input.storybookUrl),
|
|
82
|
+
close: async () => {
|
|
83
|
+
await Promise.resolve();
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
return await createStaticServer(input.storybookDir).catch(async (error) => {
|
|
87
|
+
await safeCloseBrowser(browser);
|
|
88
|
+
throw error;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
async function readParameters(browser, baseUrl, storyId, custom) {
|
|
92
|
+
const adapter = custom ?? new StorybookAdapter();
|
|
93
|
+
const page = await browser.newPage();
|
|
94
|
+
await page.goto(adapter.buildUrl(baseUrl, storyId), { waitUntil: "networkidle" });
|
|
95
|
+
await adapter.waitForReady?.(page);
|
|
96
|
+
return await extractAllParameters(page);
|
|
97
|
+
}
|
|
98
|
+
/** The driver's browser version; undefined for a driver (or test double) that does not report one. */
|
|
99
|
+
function browserVersionOf(browser) {
|
|
100
|
+
return typeof browser.version === "function" ? browser.version() : void 0;
|
|
101
|
+
}
|
|
102
|
+
/** This host's environment: a local browser renders on the machine the runner runs on. */
|
|
103
|
+
async function localEnvironment(browser) {
|
|
104
|
+
return await detectCaptureEnvironment(browserVersionOf(browser));
|
|
105
|
+
}
|
|
106
|
+
async function runWithBrowser(input, active, browser, server, relaunch) {
|
|
17
107
|
active.browser = browser;
|
|
18
108
|
const ctx = buildContext(browser, server.url, input);
|
|
19
109
|
const captures = [];
|
|
20
110
|
const failures = [];
|
|
21
111
|
try {
|
|
22
|
-
await runAllCaptures(input, active, ctx, captures, failures);
|
|
112
|
+
await runAllCaptures(input, active, ctx, captures, failures, relaunch);
|
|
23
113
|
return {
|
|
24
114
|
captures,
|
|
25
115
|
failures
|
|
26
116
|
};
|
|
27
117
|
} finally {
|
|
28
118
|
active.browser = null;
|
|
29
|
-
await
|
|
119
|
+
await ctx.pagePool?.dispose();
|
|
120
|
+
await Promise.all([safeCloseBrowser(ctx.browser), safeCloseServer(server)]);
|
|
30
121
|
}
|
|
31
122
|
}
|
|
32
|
-
async function launchBrowser(name) {
|
|
33
|
-
if (name === "firefox") return await firefox.launch();
|
|
34
|
-
if (name === "webkit") return await webkit.launch();
|
|
35
|
-
return await chromium.launch();
|
|
36
|
-
}
|
|
37
123
|
function buildContext(browser, baseUrl, input) {
|
|
38
124
|
return {
|
|
39
125
|
browser,
|
|
40
126
|
adapter: input.adapter ?? new StorybookAdapter(),
|
|
41
127
|
baseUrl,
|
|
128
|
+
navigationTimeoutMs: input.navigationTimeoutMs,
|
|
42
129
|
executePlay: input.executePlay,
|
|
43
130
|
playTimeoutMs: input.playTimeoutMs,
|
|
44
131
|
runA11y: input.runA11y,
|
|
45
|
-
|
|
132
|
+
...input.logger ? { logger: input.logger } : {},
|
|
133
|
+
runtimeParams: createRuntimeParamsState(),
|
|
134
|
+
...input.reusePages ? { pagePool: new PagePool(browser) } : {}
|
|
46
135
|
};
|
|
47
136
|
}
|
|
48
|
-
async function runAllCaptures(input, active, ctx, captures, failures) {
|
|
49
|
-
const
|
|
50
|
-
|
|
137
|
+
async function runAllCaptures(input, active, ctx, captures, failures, relaunch) {
|
|
138
|
+
const jobs = input.stories.flatMap((story) => resolveStoryViewports(story, input.viewports).map((viewport) => ({
|
|
139
|
+
story,
|
|
140
|
+
viewport
|
|
141
|
+
})));
|
|
142
|
+
const batches = chunk(jobs, input.recycleBrowserEvery ?? DEFAULT_RECYCLE_BROWSER_EVERY);
|
|
143
|
+
const pages = createPageLimiter({
|
|
144
|
+
concurrency: input.concurrency,
|
|
145
|
+
...input.gate === void 0 ? {} : { gate: input.gate },
|
|
146
|
+
browser: engineOf(input.target ?? "chromium"),
|
|
147
|
+
logger: input.logger
|
|
148
|
+
});
|
|
149
|
+
try {
|
|
150
|
+
await runBatches(batches, pages, input, active, ctx, {
|
|
151
|
+
captures,
|
|
152
|
+
failures,
|
|
153
|
+
relaunch
|
|
154
|
+
});
|
|
155
|
+
} finally {
|
|
156
|
+
pages.dispose();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function runBatches(batches, pages, input, active, ctx, out) {
|
|
160
|
+
for (const [index, batch] of batches.entries()) {
|
|
161
|
+
if (index > 0) await recycleBrowser(active, ctx, out.relaunch, input.logger);
|
|
162
|
+
await pages.each(batch, async (job) => await captureOne(input, active, ctx, job.story, job.viewport, out));
|
|
163
|
+
}
|
|
51
164
|
}
|
|
52
|
-
|
|
53
|
-
|
|
165
|
+
/** Swap in a fresh browser; the old one is closed first so two never coexist. */
|
|
166
|
+
async function recycleBrowser(active, ctx, relaunch, logger) {
|
|
167
|
+
if (active.cancelled) throw new Error("Capture cancelled");
|
|
168
|
+
await ctx.pagePool?.dispose();
|
|
169
|
+
await safeCloseBrowser(ctx.browser);
|
|
170
|
+
ctx.browser = await relaunch();
|
|
171
|
+
if (ctx.pagePool) ctx.pagePool = new PagePool(ctx.browser);
|
|
172
|
+
active.browser = ctx.browser;
|
|
173
|
+
logger?.info({}, "browser recycled");
|
|
54
174
|
}
|
|
55
|
-
|
|
175
|
+
/** Capture one story x viewport; resolves to whether it timed out (failures are recorded, not thrown). */
|
|
176
|
+
async function captureOne(input, active, ctx, story, viewport, out) {
|
|
56
177
|
if (active.cancelled) throw new Error("Capture cancelled");
|
|
57
178
|
try {
|
|
58
|
-
await pushCapture(ctx, story, viewport, captures, failures, input);
|
|
179
|
+
await pushCapture(ctx, story, viewport, out.captures, out.failures, input);
|
|
180
|
+
return false;
|
|
59
181
|
} catch (error) {
|
|
60
|
-
pushRenderFailure(failures, input, story.id, viewport.name, error);
|
|
182
|
+
pushRenderFailure(out.failures, input, story.id, viewport.name, error);
|
|
183
|
+
return isTimeoutError(error);
|
|
61
184
|
}
|
|
62
185
|
}
|
|
63
186
|
async function pushCapture(ctx, story, viewport, captures, failures, input) {
|
|
187
|
+
const started = performance.now();
|
|
64
188
|
const result = await captureScreenshot(ctx, story, viewport);
|
|
65
|
-
|
|
189
|
+
const snapshot = {
|
|
66
190
|
story: result.story,
|
|
67
191
|
viewportName: viewport.name,
|
|
68
192
|
viewport,
|
|
69
|
-
screenshot: result.screenshot
|
|
70
|
-
|
|
193
|
+
screenshot: result.screenshot,
|
|
194
|
+
durationMs: Math.round(performance.now() - started),
|
|
195
|
+
...result.a11yViolations.length > 0 ? { a11y: result.a11yViolations } : {}
|
|
196
|
+
};
|
|
197
|
+
if (input.onCapture) await input.onCapture(snapshot);
|
|
198
|
+
else captures.push(snapshot);
|
|
71
199
|
if (result.a11yViolations.length > 0) handleA11y(input, failures, story, viewport, result.a11yViolations);
|
|
72
200
|
}
|
|
73
201
|
function handleA11y(input, failures, story, viewport, violations) {
|
|
@@ -95,6 +223,6 @@ function pushRenderFailure(failures, input, storyId, viewportName, error) {
|
|
|
95
223
|
}, "render failed for story");
|
|
96
224
|
}
|
|
97
225
|
//#endregion
|
|
98
|
-
export { renderAll };
|
|
226
|
+
export { assertReachable, extractAll, planRun, renderAll, serve };
|
|
99
227
|
|
|
100
228
|
//# 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 type {\n BrowserName,\n RenderResult,\n RenderedSnapshot,\n StoryEntry,\n Viewport,\n} from \"@storyshelf/core/adapter/capture-runner\";\nimport { StorybookAdapter, resolveStoryViewports } from \"@storyshelf/core/capture\";\nimport type { Browser } from \"playwright-core\";\nimport { chromium, firefox, webkit } from \"playwright-core\";\nimport { messageOf, safeCloseBrowser, safeCloseServer } from \"./browser.ts\";\nimport { createRuntimeParamsState } from \"./runtime-params.ts\";\nimport { captureScreenshot } from \"./screenshot.ts\";\nimport { createStaticServer } from \"./static-server.ts\";\nimport type { ActiveRun, CaptureContext, PlaywrightRenderInput } from \"./types.ts\";\n\n/** Render all story/viewport combinations for a build. */\nexport async function renderAll(\n input: PlaywrightRenderInput,\n active: ActiveRun,\n defaultBrowser: BrowserName,\n): Promise<RenderResult> {\n const server = await createStaticServer(input.storybookDir);\n const browser = await launchBrowser(input.browser ?? defaultBrowser);\n return await runWithBrowser(input, active, browser, server);\n}\n\nasync function runWithBrowser(\n input: PlaywrightRenderInput,\n active: ActiveRun,\n browser: Browser,\n server: Awaited<ReturnType<typeof createStaticServer>>,\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 try {\n await runAllCaptures(input, active, ctx, captures, failures);\n return { captures, failures };\n } finally {\n active.browser = null;\n await Promise.all([safeCloseBrowser(browser), safeCloseServer(server)]);\n }\n}\n\nasync function launchBrowser(name: BrowserName): Promise<Browser> {\n if (name === \"firefox\") return await firefox.launch();\n if (name === \"webkit\") return await webkit.launch();\n return await chromium.launch();\n}\n\nfunction buildContext(\n browser: Browser,\n baseUrl: string,\n input: PlaywrightRenderInput,\n): CaptureContext {\n return {\n browser,\n adapter: input.adapter ?? new StorybookAdapter(),\n baseUrl,\n executePlay: input.executePlay,\n playTimeoutMs: input.playTimeoutMs,\n runA11y: input.runA11y,\n runtimeParams: createRuntimeParamsState(),\n };\n}\n\nasync function runAllCaptures(\n input: PlaywrightRenderInput,\n active: ActiveRun,\n ctx: CaptureContext,\n captures: RenderedSnapshot[],\n failures: RenderResult[\"failures\"],\n): Promise<void> {\n const tasks = input.stories.flatMap((story) =>\n viewportsTasks(story, input, active, ctx, captures, failures),\n );\n await Promise.all(tasks);\n}\nfunction viewportsTasks(\n story: StoryEntry,\n input: PlaywrightRenderInput,\n active: ActiveRun,\n ctx: CaptureContext,\n captures: RenderedSnapshot[],\n failures: RenderResult[\"failures\"],\n): Promise<void>[] {\n // oxlint-disable-next-line typescript/promise-function-async -- returns array of promises, not a single promise\n return resolveStoryViewports(story, input.viewports).map((viewport) =>\n captureOne(input, active, ctx, story, viewport, captures, failures),\n );\n}\n\nasync function captureOne(\n input: PlaywrightRenderInput,\n active: ActiveRun,\n ctx: CaptureContext,\n story: StoryEntry,\n viewport: Viewport,\n captures: RenderedSnapshot[],\n failures: RenderResult[\"failures\"],\n): Promise<void> {\n if (active.cancelled) throw new Error(\"Capture cancelled\");\n try {\n await pushCapture(ctx, story, viewport, captures, failures, input);\n } catch (error) {\n pushRenderFailure(failures, input, story.id, viewport.name, 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: PlaywrightRenderInput,\n): Promise<void> {\n const result = await captureScreenshot(ctx, story, viewport);\n captures.push({\n story: result.story,\n viewportName: viewport.name,\n viewport,\n screenshot: result.screenshot,\n });\n if (result.a11yViolations.length > 0)\n handleA11y(input, failures, story, viewport, result.a11yViolations);\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":";;;;;;;;;;;AAmBA,eAAsB,UACpB,OACA,QACA,gBACuB;CACvB,MAAM,SAAS,MAAM,mBAAmB,MAAM,YAAY;CAE1D,OAAO,MAAM,eAAe,OAAO,QAAQ,MADrB,cAAc,MAAM,WAAW,cAAc,GACf,MAAM;AAC5D;AAEA,eAAe,eACb,OACA,QACA,SACA,QACuB;CACvB,OAAO,UAAU;CACjB,MAAM,MAAM,aAAa,SAAS,OAAO,KAAK,KAAK;CACnD,MAAM,WAA+B,CAAC;CACtC,MAAM,WAAqC,CAAC;CAC5C,IAAI;EACF,MAAM,eAAe,OAAO,QAAQ,KAAK,UAAU,QAAQ;EAC3D,OAAO;GAAE;GAAU;EAAS;CAC9B,UAAU;EACR,OAAO,UAAU;EACjB,MAAM,QAAQ,IAAI,CAAC,iBAAiB,OAAO,GAAG,gBAAgB,MAAM,CAAC,CAAC;CACxE;AACF;AAEA,eAAe,cAAc,MAAqC;CAChE,IAAI,SAAS,WAAW,OAAO,MAAM,QAAQ,OAAO;CACpD,IAAI,SAAS,UAAU,OAAO,MAAM,OAAO,OAAO;CAClD,OAAO,MAAM,SAAS,OAAO;AAC/B;AAEA,SAAS,aACP,SACA,SACA,OACgB;CAChB,OAAO;EACL;EACA,SAAS,MAAM,WAAW,IAAI,iBAAiB;EAC/C;EACA,aAAa,MAAM;EACnB,eAAe,MAAM;EACrB,SAAS,MAAM;EACf,eAAe,yBAAyB;CAC1C;AACF;AAEA,eAAe,eACb,OACA,QACA,KACA,UACA,UACe;CACf,MAAM,QAAQ,MAAM,QAAQ,SAAS,UACnC,eAAe,OAAO,OAAO,QAAQ,KAAK,UAAU,QAAQ,CAC9D;CACA,MAAM,QAAQ,IAAI,KAAK;AACzB;AACA,SAAS,eACP,OACA,OACA,QACA,KACA,UACA,UACiB;CAEjB,OAAO,sBAAsB,OAAO,MAAM,SAAS,CAAC,CAAC,KAAK,aACxD,WAAW,OAAO,QAAQ,KAAK,OAAO,UAAU,UAAU,QAAQ,CACpE;AACF;AAEA,eAAe,WACb,OACA,QACA,KACA,OACA,UACA,UACA,UACe;CACf,IAAI,OAAO,WAAW,MAAM,IAAI,MAAM,mBAAmB;CACzD,IAAI;EACF,MAAM,YAAY,KAAK,OAAO,UAAU,UAAU,UAAU,KAAK;CACnE,SAAS,OAAO;EACd,kBAAkB,UAAU,OAAO,MAAM,IAAI,SAAS,MAAM,KAAK;CACnE;AACF;AAEA,eAAe,YACb,KACA,OACA,UACA,UACA,UACA,OACe;CACf,MAAM,SAAS,MAAM,kBAAkB,KAAK,OAAO,QAAQ;CAC3D,SAAS,KAAK;EACZ,OAAO,OAAO;EACd,cAAc,SAAS;EACvB;EACA,YAAY,OAAO;CACrB,CAAC;CACD,IAAI,OAAO,eAAe,SAAS,GACjC,WAAW,OAAO,UAAU,OAAO,UAAU,OAAO,cAAc;AACtE;AACA,SAAS,WACP,OACA,UACA,OACA,UACA,YACM;CACN,SAAS,KAAK;EACZ,SAAS,MAAM;EACf,cAAc,SAAS;EACvB,OAAO,SAAS,WAAW,KAAK,IAAI;CACtC,CAAC;CACD,MAAM,QAAQ,KACZ;EAAE,SAAS,MAAM;EAAI,UAAU,SAAS;EAAM;CAAW,GACzD,uBACF;AACF;AAEA,SAAS,kBACP,UACA,OACA,SACA,cACA,OACM;CACN,SAAS,KAAK;EAAE;EAAS;EAAc,OAAO,UAAU,KAAK;CAAE,CAAC;CAChE,MAAM,QAAQ,MAAM;EAAE;EAAS,UAAU;EAAc,KAAK;CAAM,GAAG,yBAAyB;AAChG"}
|
|
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 { messageOf, safeCloseBrowser, safeCloseServer } from \"./browser.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 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: PlaywrightRenderInput,\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 browser = await driver.launch(name, input.logger);\n const server = await serve(input, browser);\n input.logger?.info(describeSizing({ ...input, browser: engineOf(name) }), \"capture sizing\");\n const measured = remote ? await measureRemote(browser, remote.remote, input.logger) : undefined;\n const result = await runWithBrowser(\n input,\n active,\n browser,\n server,\n async () => await driver.launch(name, input.logger),\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/** The input with its target resolved, and the session gate when the target is remote. */\nexport function planRun<T extends { target?: CaptureTarget | undefined }>(\n rawInput: T,\n driver: TargetDriver,\n defaultTarget: CaptureTarget,\n): {\n input: T & { target: CaptureTarget; gate?: SessionGate | undefined };\n name: CaptureTarget;\n remote: RemoteTarget | undefined;\n} {\n const name = rawInput.target ?? defaultTarget;\n const remote = driver.remote(name);\n const gate = remote && sessionGateFor(name, remote.remote.sessions);\n return { input: { ...rawInput, target: name, gate }, name, remote };\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);\n const server = await serve(input, browser);\n try {\n return await readParameters(browser, server.url, first.id, input.adapter);\n } finally {\n await Promise.all([safeCloseBrowser(browser), safeCloseServer(server)]);\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\">,\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).catch(async (error: unknown) => {\n await safeCloseBrowser(browser);\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 try {\n await runAllCaptures(input, active, ctx, captures, failures, relaunch);\n return { captures, failures };\n } finally {\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([safeCloseBrowser(ctx.browser), safeCloseServer(server)]);\n }\n}\n\nfunction buildContext(\n browser: Browser,\n baseUrl: string,\n input: PlaywrightRenderInput,\n): 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: input.concurrency,\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 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: PlaywrightRenderInput,\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);\n return false;\n } catch (error) {\n pushRenderFailure(out.failures, input, story.id, viewport.name, error);\n return isTimeoutError(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: PlaywrightRenderInput,\n): Promise<void> {\n const started = performance.now();\n const result = await captureScreenshot(ctx, story, viewport);\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}\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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"}
|
package/dist/play.mjs
CHANGED
|
@@ -29,7 +29,20 @@ async function inPagePlay({ storyId, timeoutMs }) {
|
|
|
29
29
|
});
|
|
30
30
|
} catch {}
|
|
31
31
|
const canvasElement = globalThis.document?.querySelector("#storybook-root");
|
|
32
|
-
const
|
|
32
|
+
const store = preview.storyStoreValue ?? preview.storyStore;
|
|
33
|
+
const runLoaded = async () => {
|
|
34
|
+
const story = await store?.loadStory?.({ storyId });
|
|
35
|
+
if (!story?.playFunction) return;
|
|
36
|
+
const context = (preview.storyRenders?.find((r) => r.id === storyId))?.storyContext?.() ?? store?.getStoryContext?.(story) ?? {};
|
|
37
|
+
await story.playFunction({
|
|
38
|
+
...context,
|
|
39
|
+
canvasElement
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
let work;
|
|
43
|
+
if (preview.executePlay) work = preview.executePlay(storyId);
|
|
44
|
+
else if (store?.loadStory) work = runLoaded();
|
|
45
|
+
else work = store?.fromId?.(storyId)?.play?.({ canvasElement });
|
|
33
46
|
if (!work) return;
|
|
34
47
|
let timer;
|
|
35
48
|
const timeout = new Promise((_resolve, reject) => {
|
package/dist/play.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"play.mjs","names":[],"sources":["../src/play.ts"],"sourcesContent":["/** Storybook `play` function execution inside the page. */\n\nimport type { Page } from \"playwright-core\";\nimport { messageOf } from \"./browser.ts\";\n\n/** Arguments serialized into the page for {@link inPagePlay}. */\nexport interface PlayArgs {\n storyId: string;\n timeoutMs: number;\n}\n\n/** Execute the story's play function with a timeout race. */\nexport async function runPlay(page: Page, storyId: string, timeoutMs: number): Promise<void> {\n try {\n await page.evaluate(inPagePlay, { storyId, timeoutMs });\n } catch (error) {\n throw new Error(`play failed: ${messageOf(error)}`, { cause: error });\n }\n}\n\n/**\n * Runs inside the browser. Playwright serializes only this function's source, so it must not\n * reference anything from this module (helpers, imports): split it up and the page throws\n * `ReferenceError`. `play.test.ts` evaluates it in a scope without module bindings to guard that.\n */\n// oxlint-disable-next-line eslint/max-statements -- must stay one self-contained function (see above)\nexport async function inPagePlay({ storyId, timeoutMs }: PlayArgs): Promise<void> {\n type
|
|
1
|
+
{"version":3,"file":"play.mjs","names":[],"sources":["../src/play.ts"],"sourcesContent":["/** Storybook `play` function execution inside the page. */\n\nimport type { Page } from \"playwright-core\";\nimport { messageOf } from \"./browser.ts\";\n\n/** Arguments serialized into the page for {@link inPagePlay}. */\nexport interface PlayArgs {\n storyId: string;\n timeoutMs: number;\n}\n\n/** Execute the story's play function with a timeout race. */\nexport async function runPlay(page: Page, storyId: string, timeoutMs: number): Promise<void> {\n try {\n await page.evaluate(inPagePlay, { storyId, timeoutMs });\n } catch (error) {\n throw new Error(`play failed: ${messageOf(error)}`, { cause: error });\n }\n}\n\n/**\n * Runs inside the browser. Playwright serializes only this function's source, so it must not\n * reference anything from this module (helpers, imports): split it up and the page throws\n * `ReferenceError`. `play.test.ts` evaluates it in a scope without module bindings to guard that.\n */\n// oxlint-disable-next-line eslint/max-statements, eslint/max-lines-per-function -- must stay one self-contained function (see above)\nexport async function inPagePlay({ storyId, timeoutMs }: PlayArgs): Promise<void> {\n type Story = { playFunction?: (ctx: unknown) => Promise<void> };\n type Store = {\n loadStory?: (arg: { storyId: string }) => Promise<Story>;\n getStoryContext?: (story: Story) => Record<string, unknown>;\n fromId?: (id: string) => { play?: (ctx: unknown) => Promise<void> };\n };\n type Preview = {\n executePlay?: (id: string) => Promise<void>;\n storyStoreValue?: Store;\n storyStore?: Store;\n storyRenders?: { id: string; storyContext?: () => Record<string, unknown> }[];\n channel?: { on: (event: string, cb: (err: unknown) => void) => void };\n };\n const preview = (globalThis as unknown as Record<string, Preview | undefined>)[\n \"__STORYBOOK_PREVIEW__\"\n ];\n if (!preview) return;\n let playError: unknown = null;\n try {\n // Best-effort: play still runs without the channel hookup.\n preview.channel?.on(\"playFunctionThrewException\", (err) => {\n playError = err;\n });\n } catch {\n // ignore\n }\n const canvasElement = globalThis.document?.querySelector(\"#storybook-root\");\n // SB 8+ has no `executePlay`, and `fromId` throws until every CSF file is cached, so load just\n // this story and run its `playFunction` with the context of the render that is already mounted.\n const store = preview.storyStoreValue ?? preview.storyStore;\n const runLoaded = async (): Promise<void> => {\n const story = await store?.loadStory?.({ storyId });\n if (!story?.playFunction) return;\n const render = preview.storyRenders?.find((r) => r.id === storyId);\n const context = render?.storyContext?.() ?? store?.getStoryContext?.(story) ?? {};\n await story.playFunction({ ...context, canvasElement });\n };\n let work: Promise<void> | undefined;\n if (preview.executePlay) work = preview.executePlay(storyId);\n else if (store?.loadStory) work = runLoaded();\n else work = store?.fromId?.(storyId)?.play?.({ canvasElement });\n if (!work) return;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<never>((_resolve, reject) => {\n timer = setTimeout(() => {\n reject(new Error(`play timeout after ${timeoutMs}ms`));\n }, timeoutMs);\n });\n try {\n await Promise.race([work, timeout]);\n } finally {\n clearTimeout(timer);\n }\n if (playError) throw playError;\n}\n"],"mappings":";;;;;;AAYA,eAAsB,QAAQ,MAAY,SAAiB,WAAkC;CAC3F,IAAI;EACF,MAAM,KAAK,SAAS,YAAY;GAAE;GAAS;EAAU,CAAC;CACxD,SAAS,OAAO;EACd,MAAM,IAAI,MAAM,gBAAgB,UAAU,KAAK,KAAK,EAAE,OAAO,MAAM,CAAC;CACtE;AACF;;;;;;AAQA,eAAsB,WAAW,EAAE,SAAS,aAAsC;CAchF,MAAM,UAAW,WACf;CAEF,IAAI,CAAC,SAAS;CACd,IAAI,YAAqB;CACzB,IAAI;EAEF,QAAQ,SAAS,GAAG,+BAA+B,QAAQ;GACzD,YAAY;EACd,CAAC;CACH,QAAQ,CAER;CACA,MAAM,gBAAgB,WAAW,UAAU,cAAc,iBAAiB;CAG1E,MAAM,QAAQ,QAAQ,mBAAmB,QAAQ;CACjD,MAAM,YAAY,YAA2B;EAC3C,MAAM,QAAQ,MAAM,OAAO,YAAY,EAAE,QAAQ,CAAC;EAClD,IAAI,CAAC,OAAO,cAAc;EAE1B,MAAM,WADS,QAAQ,cAAc,MAAM,MAAM,EAAE,OAAO,OAAO,EAAA,EACzC,eAAe,KAAK,OAAO,kBAAkB,KAAK,KAAK,CAAC;EAChF,MAAM,MAAM,aAAa;GAAE,GAAG;GAAS;EAAc,CAAC;CACxD;CACA,IAAI;CACJ,IAAI,QAAQ,aAAa,OAAO,QAAQ,YAAY,OAAO;MACtD,IAAI,OAAO,WAAW,OAAO,UAAU;MACvC,OAAO,OAAO,SAAS,OAAO,CAAC,EAAE,OAAO,EAAE,cAAc,CAAC;CAC9D,IAAI,CAAC,MAAM;CACX,IAAI;CACJ,MAAM,UAAU,IAAI,SAAgB,UAAU,WAAW;EACvD,QAAQ,iBAAiB;GACvB,uBAAO,IAAI,MAAM,sBAAsB,UAAU,GAAG,CAAC;EACvD,GAAG,SAAS;CACd,CAAC;CACD,IAAI;EACF,MAAM,QAAQ,KAAK,CAAC,MAAM,OAAO,CAAC;CACpC,UAAU;EACR,aAAa,KAAK;CACpB;CACA,IAAI,WAAW,MAAM;AACvB"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { BrowserName, RemoteBrowser } from "@storyshelf/core/adapter/capture-runner";
|
|
4
|
+
//#region src/remote/azure.d.ts
|
|
5
|
+
/** Options of {@link azurePlaywright}. */
|
|
6
|
+
interface AzurePlaywrightOptions {
|
|
7
|
+
/** The target name half; default `azure`. */
|
|
8
|
+
name?: string;
|
|
9
|
+
/** The workspace's browser endpoint (`PLAYWRIGHT_SERVICE_URL`): `wss://<region>.api.playwright.microsoft.com/playwrightworkspaces/<id>/browsers`. */
|
|
10
|
+
endpoint: string;
|
|
11
|
+
/** Access token (`PLAYWRIGHT_SERVICE_ACCESS_TOKEN`). */
|
|
12
|
+
token: string;
|
|
13
|
+
/** OS of the cloud browsers (default `linux`). */
|
|
14
|
+
os?: "linux" | "windows";
|
|
15
|
+
/** API version the service is called with (default `2025-09-01`). */
|
|
16
|
+
apiVersion?: string;
|
|
17
|
+
/** Parallel pages (the workspace's parallel-worker limit; default 1). */
|
|
18
|
+
sessions?: number;
|
|
19
|
+
/** Engines to offer (default all three). */
|
|
20
|
+
engines?: readonly BrowserName[];
|
|
21
|
+
}
|
|
22
|
+
/** Targets on a workspace: `chromium@azure`, `firefox@azure`, `webkit@azure`. */
|
|
23
|
+
declare function azurePlaywright(options: AzurePlaywrightOptions): RemoteBrowser;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { AzurePlaywrightOptions, azurePlaywright };
|
|
26
|
+
//# sourceMappingURL=azure.d.mts.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { remote } from "./remote.mjs";
|
|
5
|
+
//#region src/remote/azure.ts
|
|
6
|
+
/** Targets on a workspace: `chromium@azure`, `firefox@azure`, `webkit@azure`. */
|
|
7
|
+
function azurePlaywright(options) {
|
|
8
|
+
const os = options.os ?? "linux";
|
|
9
|
+
return remote({
|
|
10
|
+
name: options.name ?? "azure",
|
|
11
|
+
engines: options.engines ?? [
|
|
12
|
+
"chromium",
|
|
13
|
+
"firefox",
|
|
14
|
+
"webkit"
|
|
15
|
+
],
|
|
16
|
+
connect: () => ({
|
|
17
|
+
protocol: "playwright",
|
|
18
|
+
endpoint: withParams(options.endpoint, {
|
|
19
|
+
os,
|
|
20
|
+
"api-version": options.apiVersion ?? "2025-09-01"
|
|
21
|
+
}),
|
|
22
|
+
headers: { authorization: `Bearer ${options.token}` },
|
|
23
|
+
exposeNetwork: "<loopback>"
|
|
24
|
+
}),
|
|
25
|
+
sessions: options.sessions ?? 1,
|
|
26
|
+
platform: os
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function withParams(endpoint, params) {
|
|
30
|
+
const url = new URL(endpoint);
|
|
31
|
+
for (const [key, value] of Object.entries(params)) if (!url.searchParams.has(key)) url.searchParams.set(key, value);
|
|
32
|
+
return url.toString();
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
export { azurePlaywright };
|
|
36
|
+
|
|
37
|
+
//# sourceMappingURL=azure.mjs.map
|
|
@@ -0,0 +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/** Targets on a workspace: `chromium@azure`, `firefox@azure`, `webkit@azure`. */\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 connect: () => ({\n protocol: \"playwright\",\n endpoint: withParams(options.endpoint, {\n os,\n \"api-version\": options.apiVersion ?? \"2025-09-01\",\n }),\n headers: { authorization: `Bearer ${options.token}` },\n // The service routes requests for the host's loopback back through the connection.\n exposeNetwork: \"<loopback>\",\n }),\n sessions: options.sessions ?? 1,\n platform: os,\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":";;;;;;AAuBA,SAAgB,gBAAgB,SAAgD;CAC9E,MAAM,KAAK,QAAQ,MAAM;CACzB,OAAO,OAAO;EACZ,MAAM,QAAQ,QAAQ;EACtB,SAAS,QAAQ,WAAW;GAAC;GAAY;GAAW;EAAQ;EAC5D,gBAAgB;GACd,UAAU;GACV,UAAU,WAAW,QAAQ,UAAU;IACrC;IACA,eAAe,QAAQ,cAAc;GACvC,CAAC;GACD,SAAS,EAAE,eAAe,UAAU,QAAQ,QAAQ;GAEpD,eAAe;EACjB;EACA,UAAU,QAAQ,YAAY;EAC9B,UAAU;CACZ,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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { BrowserName, RemoteBrowser } from "@storyshelf/core/adapter/capture-runner";
|
|
4
|
+
//#region src/remote/browserstack.d.ts
|
|
5
|
+
/** Options of {@link browserstack}. */
|
|
6
|
+
interface BrowserStackOptions {
|
|
7
|
+
/** The target name half; default `browserstack`. A second entry needs its own. */
|
|
8
|
+
name?: string;
|
|
9
|
+
username: string;
|
|
10
|
+
accessKey: string;
|
|
11
|
+
/** OS the browsers run on, as BrowserStack names it (`{ name: "Windows", version: "11" }`). */
|
|
12
|
+
os?: {
|
|
13
|
+
name: string;
|
|
14
|
+
version: string;
|
|
15
|
+
};
|
|
16
|
+
/** Browser version (default `latest`). Pin it: a moving build is a baseline migration. */
|
|
17
|
+
browserVersion?: string;
|
|
18
|
+
/** Engines to offer (default `chromium`, `firefox`, `webkit`; `chrome` is real Chrome). */
|
|
19
|
+
engines?: readonly BrowserName[];
|
|
20
|
+
/** Parallel sessions of the plan (default 1). */
|
|
21
|
+
sessions?: number;
|
|
22
|
+
/** Build name shown in BrowserStack's dashboard. */
|
|
23
|
+
build?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Targets on BrowserStack, for example `chromium@browserstack`. BrowserStack cannot route back to
|
|
27
|
+
* this host, so each target needs a signed Storybook URL (ADR 0007, decision 5). The credentials
|
|
28
|
+
* travel in the endpoint's capabilities and are scrubbed from every message and log.
|
|
29
|
+
*/
|
|
30
|
+
declare function browserstack(options: BrowserStackOptions): RemoteBrowser;
|
|
31
|
+
//#endregion
|
|
32
|
+
export { BrowserStackOptions, browserstack };
|
|
33
|
+
//# sourceMappingURL=browserstack.d.mts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { playwrightVersion } from "../version.mjs";
|
|
5
|
+
import { remote } from "./remote.mjs";
|
|
6
|
+
//#region src/remote/browserstack.ts
|
|
7
|
+
/** BrowserStack's `browser` capability per engine. */
|
|
8
|
+
const BROWSER = {
|
|
9
|
+
chromium: "playwright-chromium",
|
|
10
|
+
firefox: "playwright-firefox",
|
|
11
|
+
webkit: "playwright-webkit",
|
|
12
|
+
chrome: "chrome"
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Targets on BrowserStack, for example `chromium@browserstack`. BrowserStack cannot route back to
|
|
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.
|
|
18
|
+
*/
|
|
19
|
+
function browserstack(options) {
|
|
20
|
+
return remote({
|
|
21
|
+
name: options.name ?? "browserstack",
|
|
22
|
+
engines: options.engines ?? [
|
|
23
|
+
"chromium",
|
|
24
|
+
"firefox",
|
|
25
|
+
"webkit"
|
|
26
|
+
],
|
|
27
|
+
connect: (engine) => ({
|
|
28
|
+
protocol: "playwright",
|
|
29
|
+
endpoint: `wss://cdp.browserstack.com/playwright?caps=${encodeURIComponent(JSON.stringify(capabilities(options, engine)))}`
|
|
30
|
+
}),
|
|
31
|
+
sessions: options.sessions ?? 1,
|
|
32
|
+
platform: options.os?.name.toLowerCase().replace(/^os x$/u, "macos")
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function capabilities(options, engine) {
|
|
36
|
+
return {
|
|
37
|
+
browser: BROWSER[engine],
|
|
38
|
+
browser_version: options.browserVersion ?? "latest",
|
|
39
|
+
...options.os === void 0 ? {} : {
|
|
40
|
+
os: options.os.name,
|
|
41
|
+
os_version: options.os.version
|
|
42
|
+
},
|
|
43
|
+
"browserstack.username": options.username,
|
|
44
|
+
"browserstack.accessKey": options.accessKey,
|
|
45
|
+
"client.playwrightVersion": playwrightVersion(),
|
|
46
|
+
...options.build === void 0 ? {} : { build: options.build }
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { browserstack };
|
|
51
|
+
|
|
52
|
+
//# sourceMappingURL=browserstack.mjs.map
|
|
@@ -0,0 +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 connect: (engine) => ({\n protocol: \"playwright\",\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;;;;;;AAOA,SAAgB,aAAa,SAA6C;CACxE,OAAO,OAAO;EACZ,MAAM,QAAQ,QAAQ;EACtB,SAAS,QAAQ,WAAW;GAAC;GAAY;GAAW;EAAQ;EAC5D,UAAU,YAAY;GACpB,UAAU;GACV,UAAU,8CAA8C,mBACtD,KAAK,UAAU,aAAa,SAAS,MAAM,CAAC,CAC9C;EACF;EACA,UAAU,QAAQ,YAAY;EAC9B,UAAU,QAAQ,IAAI,KAAK,YAAY,CAAC,CAAC,QAAQ,WAAW,OAAO;CACrE,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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { BrowserName, RemoteBrowser, RemoteConnection, RemoteOptions, remote } from "./remote.mjs";
|
|
4
|
+
import { PlaywrightServerOptions, playwrightServer } from "./playwright-server.mjs";
|
|
5
|
+
import { AzurePlaywrightOptions, azurePlaywright } from "./azure.mjs";
|
|
6
|
+
import { BrowserStackOptions, browserstack } from "./browserstack.mjs";
|
|
7
|
+
export { type AzurePlaywrightOptions, type BrowserName, type BrowserStackOptions, type PlaywrightServerOptions, type RemoteBrowser, type RemoteConnection, type RemoteOptions, azurePlaywright, browserstack, playwrightServer, remote };
|