@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,8 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { remote } from "./remote.mjs";
|
|
5
|
+
import { playwrightServer } from "./playwright-server.mjs";
|
|
6
|
+
import { azurePlaywright } from "./azure.mjs";
|
|
7
|
+
import { browserstack } from "./browserstack.mjs";
|
|
8
|
+
export { azurePlaywright, browserstack, playwrightServer, remote };
|
|
@@ -0,0 +1,40 @@
|
|
|
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/playwright-server.d.ts
|
|
5
|
+
/** Options of {@link playwrightServer}. */
|
|
6
|
+
interface PlaywrightServerOptions {
|
|
7
|
+
/** The target name half (`chromium@<name>`); default `grid`. */
|
|
8
|
+
name?: string;
|
|
9
|
+
/** `ws://host:3000/` or `wss://…`: the server's address and `--path`. */
|
|
10
|
+
endpoint: string;
|
|
11
|
+
/**
|
|
12
|
+
* Bearer token for a reverse proxy in front of the server: a bare `run-server` has no
|
|
13
|
+
* authentication of its own. Sent as `authorization: Bearer …`.
|
|
14
|
+
*/
|
|
15
|
+
token?: string;
|
|
16
|
+
/** Further handshake headers. */
|
|
17
|
+
headers?: Record<string, string>;
|
|
18
|
+
/**
|
|
19
|
+
* Route the remote browser's requests for the loopback Storybook server back through the
|
|
20
|
+
* connection (default `"<loopback>"`). `false` when the server cannot, which makes every target
|
|
21
|
+
* need a signed Storybook URL.
|
|
22
|
+
*/
|
|
23
|
+
exposeNetwork?: string | false;
|
|
24
|
+
/** Parallel pages the farm should take from this runner (default 1: set it to the farm's cap). */
|
|
25
|
+
sessions?: number;
|
|
26
|
+
/** Engines to offer (default all three; the server must have them installed). */
|
|
27
|
+
engines?: readonly BrowserName[];
|
|
28
|
+
/** OS the server runs on, reported when the page cannot tell. */
|
|
29
|
+
platform?: string;
|
|
30
|
+
/**
|
|
31
|
+
* How long one connect attempt waits in ms (default 30 000). A server started with
|
|
32
|
+
* `--max-clients` queues extra connections: raise this when renders outnumber its clients.
|
|
33
|
+
*/
|
|
34
|
+
connectTimeoutMs?: number;
|
|
35
|
+
}
|
|
36
|
+
/** Targets on a Playwright server: `chromium@grid`, `firefox@grid`, `webkit@grid`. */
|
|
37
|
+
declare function playwrightServer(options: PlaywrightServerOptions): RemoteBrowser;
|
|
38
|
+
//#endregion
|
|
39
|
+
export { PlaywrightServerOptions, playwrightServer };
|
|
40
|
+
//# sourceMappingURL=playwright-server.d.mts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { remote } from "./remote.mjs";
|
|
5
|
+
//#region src/remote/playwright-server.ts
|
|
6
|
+
/** Targets on a Playwright server: `chromium@grid`, `firefox@grid`, `webkit@grid`. */
|
|
7
|
+
function playwrightServer(options) {
|
|
8
|
+
const headers = {
|
|
9
|
+
...options.token === void 0 ? {} : { authorization: `Bearer ${options.token}` },
|
|
10
|
+
...options.headers
|
|
11
|
+
};
|
|
12
|
+
const exposeNetwork = options.exposeNetwork ?? "<loopback>";
|
|
13
|
+
return remote({
|
|
14
|
+
name: options.name ?? "grid",
|
|
15
|
+
engines: options.engines ?? [
|
|
16
|
+
"chromium",
|
|
17
|
+
"firefox",
|
|
18
|
+
"webkit"
|
|
19
|
+
],
|
|
20
|
+
connect: () => ({
|
|
21
|
+
protocol: "playwright",
|
|
22
|
+
endpoint: options.endpoint,
|
|
23
|
+
headers,
|
|
24
|
+
...exposeNetwork === false ? {} : { exposeNetwork }
|
|
25
|
+
}),
|
|
26
|
+
sessions: options.sessions ?? 1,
|
|
27
|
+
platform: options.platform,
|
|
28
|
+
connectTimeoutMs: options.connectTimeoutMs
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { playwrightServer };
|
|
33
|
+
|
|
34
|
+
//# sourceMappingURL=playwright-server.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"playwright-server.mjs","names":[],"sources":["../../src/remote/playwright-server.ts"],"sourcesContent":["/** A self-hosted Playwright server (`playwright run-server`), or anything speaking its protocol. */\nimport type { BrowserName, RemoteBrowser } from \"@storyshelf/core/adapter/capture-runner\";\nimport { remote } from \"./remote.ts\";\n\n/** Options of {@link playwrightServer}. */\nexport interface PlaywrightServerOptions {\n /** The target name half (`chromium@<name>`); default `grid`. */\n name?: string;\n /** `ws://host:3000/` or `wss://…`: the server's address and `--path`. */\n endpoint: string;\n /**\n * Bearer token for a reverse proxy in front of the server: a bare `run-server` has no\n * authentication of its own. Sent as `authorization: Bearer …`.\n */\n token?: string;\n /** Further handshake headers. */\n headers?: Record<string, string>;\n /**\n * Route the remote browser's requests for the loopback Storybook server back through the\n * connection (default `\"<loopback>\"`). `false` when the server cannot, which makes every target\n * need a signed Storybook URL.\n */\n exposeNetwork?: string | false;\n /** Parallel pages the farm should take from this runner (default 1: set it to the farm's cap). */\n sessions?: number;\n /** Engines to offer (default all three; the server must have them installed). */\n engines?: readonly BrowserName[];\n /** OS the server runs on, reported when the page cannot tell. */\n platform?: string;\n /**\n * How long one connect attempt waits in ms (default 30 000). A server started with\n * `--max-clients` queues extra connections: raise this when renders outnumber its clients.\n */\n connectTimeoutMs?: number;\n}\n\n/** Targets on a Playwright server: `chromium@grid`, `firefox@grid`, `webkit@grid`. */\nexport function playwrightServer(options: PlaywrightServerOptions): RemoteBrowser {\n const headers = {\n ...(options.token === undefined ? {} : { authorization: `Bearer ${options.token}` }),\n ...options.headers,\n };\n const exposeNetwork = options.exposeNetwork ?? \"<loopback>\";\n return remote({\n name: options.name ?? \"grid\",\n engines: options.engines ?? [\"chromium\", \"firefox\", \"webkit\"],\n connect: () => ({\n protocol: \"playwright\",\n endpoint: options.endpoint,\n headers,\n ...(exposeNetwork === false ? {} : { exposeNetwork }),\n }),\n sessions: options.sessions ?? 1,\n platform: options.platform,\n connectTimeoutMs: options.connectTimeoutMs,\n });\n}\n"],"mappings":";;;;;;AAqCA,SAAgB,iBAAiB,SAAiD;CAChF,MAAM,UAAU;EACd,GAAI,QAAQ,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,eAAe,UAAU,QAAQ,QAAQ;EAClF,GAAG,QAAQ;CACb;CACA,MAAM,gBAAgB,QAAQ,iBAAiB;CAC/C,OAAO,OAAO;EACZ,MAAM,QAAQ,QAAQ;EACtB,SAAS,QAAQ,WAAW;GAAC;GAAY;GAAW;EAAQ;EAC5D,gBAAgB;GACd,UAAU;GACV,UAAU,QAAQ;GAClB;GACA,GAAI,kBAAkB,QAAQ,CAAC,IAAI,EAAE,cAAc;EACrD;EACA,UAAU,QAAQ,YAAY;EAC9B,UAAU,QAAQ;EAClB,kBAAkB,QAAQ;CAC5B,CAAC;AACH"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { BrowserName as BrowserName$1, RemoteBrowser as RemoteBrowser$1, RemoteConnection, RemoteOptions } from "@storyshelf/core/adapter/capture-runner";
|
|
4
|
+
//#region src/remote/remote.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* A browser service the runner connects to instead of launching, for `browsers`. It offers one
|
|
7
|
+
* target per engine, `<engine>@<name>`, each with its own baselines (ADR 0007, decision 3), and
|
|
8
|
+
* throws when the runner is created for a bad name, engine, protocol or endpoint.
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* remote({
|
|
12
|
+
* name: "mygrid",
|
|
13
|
+
* engines: ["chromium", "firefox", "webkit"],
|
|
14
|
+
* connect: () => ({ protocol: "playwright", endpoint: "wss://grid.example/", exposeNetwork: "<loopback>" }),
|
|
15
|
+
* sessions: 4,
|
|
16
|
+
* });
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
declare function remote(options: RemoteOptions): RemoteBrowser$1;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { type BrowserName$1 as BrowserName, type RemoteBrowser$1 as RemoteBrowser, type RemoteConnection, type RemoteOptions, remote };
|
|
22
|
+
//# sourceMappingURL=remote.d.mts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { defineRemote } from "@storyshelf/core/adapter/capture-runner";
|
|
5
|
+
//#region src/remote/remote.ts
|
|
6
|
+
/** The generic remote browser entry: every preset is sugar over it (ADR 0007, decision 1). */
|
|
7
|
+
/** What the Playwright runner can drive: every engine, over its own protocol or CDP (Chromium only). */
|
|
8
|
+
const SUPPORT = {
|
|
9
|
+
engines: [
|
|
10
|
+
"chromium",
|
|
11
|
+
"firefox",
|
|
12
|
+
"webkit",
|
|
13
|
+
"chrome"
|
|
14
|
+
],
|
|
15
|
+
protocols: ["playwright", "cdp"],
|
|
16
|
+
label: "remote()"
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* A browser service the runner connects to instead of launching, for `browsers`. It offers one
|
|
20
|
+
* target per engine, `<engine>@<name>`, each with its own baselines (ADR 0007, decision 3), and
|
|
21
|
+
* throws when the runner is created for a bad name, engine, protocol or endpoint.
|
|
22
|
+
*
|
|
23
|
+
* ```ts
|
|
24
|
+
* remote({
|
|
25
|
+
* name: "mygrid",
|
|
26
|
+
* engines: ["chromium", "firefox", "webkit"],
|
|
27
|
+
* connect: () => ({ protocol: "playwright", endpoint: "wss://grid.example/", exposeNetwork: "<loopback>" }),
|
|
28
|
+
* sessions: 4,
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
function remote(options) {
|
|
33
|
+
return defineRemote(options, SUPPORT);
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { remote };
|
|
37
|
+
|
|
38
|
+
//# sourceMappingURL=remote.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote.mjs","names":[],"sources":["../../src/remote/remote.ts"],"sourcesContent":["/** The generic remote browser entry: every preset is sugar over it (ADR 0007, decision 1). */\nimport {\n defineRemote,\n type BrowserName,\n type RemoteBrowser,\n type RemoteConnection,\n type RemoteOptions,\n type RemoteSupport,\n} from \"@storyshelf/core/adapter/capture-runner\";\n\n/** What the Playwright runner can drive: every engine, over its own protocol or CDP (Chromium only). */\nconst SUPPORT: RemoteSupport = {\n engines: [\"chromium\", \"firefox\", \"webkit\", \"chrome\"],\n protocols: [\"playwright\", \"cdp\"],\n label: \"remote()\",\n};\n\n/**\n * A browser service the runner connects to instead of launching, for `browsers`. It offers one\n * target per engine, `<engine>@<name>`, each with its own baselines (ADR 0007, decision 3), and\n * throws when the runner is created for a bad name, engine, protocol or endpoint.\n *\n * ```ts\n * remote({\n * name: \"mygrid\",\n * engines: [\"chromium\", \"firefox\", \"webkit\"],\n * connect: () => ({ protocol: \"playwright\", endpoint: \"wss://grid.example/\", exposeNetwork: \"<loopback>\" }),\n * sessions: 4,\n * });\n * ```\n */\nexport function remote(options: RemoteOptions): RemoteBrowser {\n return defineRemote(options, SUPPORT);\n}\n\nexport type { BrowserName, RemoteBrowser, RemoteConnection, RemoteOptions };\n"],"mappings":";;;;;;;AAWA,MAAM,UAAyB;CAC7B,SAAS;EAAC;EAAY;EAAW;EAAU;CAAQ;CACnD,WAAW,CAAC,cAAc,KAAK;CAC/B,OAAO;AACT;;;;;;;;;;;;;;;AAgBA,SAAgB,OAAO,SAAuC;CAC5D,OAAO,aAAa,SAAS,OAAO;AACtC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { messageOf } from "./browser.mjs";
|
|
5
|
+
import { FONT_PROBE_FAMILIES, inPageProbe, remoteEnvironment } from "@storyshelf/core/utils";
|
|
6
|
+
//#region src/remote-environment.ts
|
|
7
|
+
/**
|
|
8
|
+
* The fingerprint of the browser a render drove: its version, and what a blank page in it reports
|
|
9
|
+
* (OS, pixel ratio, installed fonts). Never `process.platform` or `fc-list`, which describe this
|
|
10
|
+
* host. A page that cannot be probed leaves those fields out, and an unknown component is not a
|
|
11
|
+
* change (ADR 0005), so a service outage here cannot rewrite baselines.
|
|
12
|
+
*/
|
|
13
|
+
async function measureRemote(browser, remote, logger) {
|
|
14
|
+
return remoteEnvironment({
|
|
15
|
+
browserVersion: typeof browser.version === "function" ? browser.version() : void 0,
|
|
16
|
+
probe: await probeBlankPage(browser, logger),
|
|
17
|
+
declaredPlatform: remote.platform
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
async function probeBlankPage(browser, logger) {
|
|
21
|
+
try {
|
|
22
|
+
const page = await browser.newPage();
|
|
23
|
+
try {
|
|
24
|
+
return await page.evaluate(inPageProbe, [...FONT_PROBE_FAMILIES]);
|
|
25
|
+
} finally {
|
|
26
|
+
await page.close();
|
|
27
|
+
}
|
|
28
|
+
} catch (error) {
|
|
29
|
+
logger?.warn({ reason: messageOf(error) }, "could not measure the remote browser");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { measureRemote };
|
|
35
|
+
|
|
36
|
+
//# sourceMappingURL=remote-environment.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-environment.mjs","names":[],"sources":["../src/remote-environment.ts"],"sourcesContent":["/** Measuring a remote browser's environment from inside it (ADR 0007, decision 2). */\nimport type { CaptureEnvironment, RemoteBrowser } from \"@storyshelf/core/adapter/capture-runner\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport {\n FONT_PROBE_FAMILIES,\n inPageProbe,\n remoteEnvironment,\n type BrowserProbe,\n} from \"@storyshelf/core/utils\";\nimport type { Browser } from \"playwright-core\";\nimport { messageOf } from \"./browser.ts\";\n\n/**\n * The fingerprint of the browser a render drove: its version, and what a blank page in it reports\n * (OS, pixel ratio, installed fonts). Never `process.platform` or `fc-list`, which describe this\n * host. A page that cannot be probed leaves those fields out, and an unknown component is not a\n * change (ADR 0005), so a service outage here cannot rewrite baselines.\n */\nexport async function measureRemote(\n browser: Browser,\n remote: RemoteBrowser,\n logger?: Logger,\n): Promise<CaptureEnvironment> {\n return remoteEnvironment({\n browserVersion: typeof browser.version === \"function\" ? browser.version() : undefined,\n probe: await probeBlankPage(browser, logger),\n declaredPlatform: remote.platform,\n });\n}\n\nasync function probeBlankPage(\n browser: Browser,\n logger?: Logger,\n): Promise<BrowserProbe | undefined> {\n try {\n const page = await browser.newPage();\n try {\n return await page.evaluate(inPageProbe, [...FONT_PROBE_FAMILIES]);\n } finally {\n await page.close();\n }\n } catch (error) {\n logger?.warn({ reason: messageOf(error) }, \"could not measure the remote browser\");\n return undefined;\n }\n}\n"],"mappings":";;;;;;;;;;;;AAkBA,eAAsB,cACpB,SACA,QACA,QAC6B;CAC7B,OAAO,kBAAkB;EACvB,gBAAgB,OAAO,QAAQ,YAAY,aAAa,QAAQ,QAAQ,IAAI,KAAA;EAC5E,OAAO,MAAM,eAAe,SAAS,MAAM;EAC3C,kBAAkB,OAAO;CAC3B,CAAC;AACH;AAEA,eAAe,eACb,SACA,QACmC;CACnC,IAAI;EACF,MAAM,OAAO,MAAM,QAAQ,QAAQ;EACnC,IAAI;GACF,OAAO,MAAM,KAAK,SAAS,aAAa,CAAC,GAAG,mBAAmB,CAAC;EAClE,UAAU;GACR,MAAM,KAAK,MAAM;EACnB;CACF,SAAS,OAAO;EACd,QAAQ,KAAK,EAAE,QAAQ,UAAU,KAAK,EAAE,GAAG,sCAAsC;EACjF;CACF;AACF"}
|
package/dist/runtime-params.mjs
CHANGED
|
@@ -15,6 +15,13 @@ function createRuntimeParamsState() {
|
|
|
15
15
|
async function runtimeParametersForStory(state, page, storyId) {
|
|
16
16
|
return (state.extractFailed ? void 0 : await extractedParametersMap(state, page))?.get(storyId);
|
|
17
17
|
}
|
|
18
|
+
/** Read every story's resolved parameters from a loaded preview page in one call. */
|
|
19
|
+
async function extractAllParameters(page) {
|
|
20
|
+
const raw = await readExtractedParameters(page);
|
|
21
|
+
const out = /* @__PURE__ */ new Map();
|
|
22
|
+
if (raw) fillParamsMap(out, raw);
|
|
23
|
+
return Object.fromEntries(out);
|
|
24
|
+
}
|
|
18
25
|
async function extractedParametersMap(state, page) {
|
|
19
26
|
state.extractPromise ??= loadExtractedParametersMap(state, page);
|
|
20
27
|
return await state.extractPromise;
|
|
@@ -55,6 +62,6 @@ function hasEntries(map) {
|
|
|
55
62
|
return Boolean(map && Object.keys(map).length > 0);
|
|
56
63
|
}
|
|
57
64
|
//#endregion
|
|
58
|
-
export { createRuntimeParamsState, runtimeParametersForStory };
|
|
65
|
+
export { createRuntimeParamsState, extractAllParameters, runtimeParametersForStory };
|
|
59
66
|
|
|
60
67
|
//# sourceMappingURL=runtime-params.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-params.mjs","names":["win"],"sources":["../src/runtime-params.ts"],"sourcesContent":["/** Per-run runtime-parameter extraction cache (ADR 0017). */\n\nimport { mergeParameters } from \"@storyshelf/core/capture\";\nimport type { StoryParameters } from \"@storyshelf/core/capture\";\nimport type { ExtractedEntry, RuntimeParamsPage, RuntimeParamsState } from \"./types.ts\";\n\n/** Create the per-run runtime-parameter cache shared across captures. */\nexport function createRuntimeParamsState(): RuntimeParamsState {\n return {};\n}\n\n/**\n * Read story parameters from the running preview via\n * `__STORYBOOK_PREVIEW__.extract()`, once per run and lazily.\n */\nexport async function runtimeParametersForStory(\n state: RuntimeParamsState,\n page: RuntimeParamsPage,\n storyId: string,\n): Promise<StoryParameters | undefined> {\n const map = state.extractFailed ? undefined : await extractedParametersMap(state, page);\n return map?.get(storyId);\n}\n\nasync function extractedParametersMap(\n state: RuntimeParamsState,\n page: RuntimeParamsPage,\n): Promise<Map<string, StoryParameters>> {\n state.extractPromise ??= loadExtractedParametersMap(state, page);\n return await state.extractPromise;\n}\n\nasync function loadExtractedParametersMap(\n state: RuntimeParamsState,\n page: RuntimeParamsPage,\n): Promise<Map<string, StoryParameters>> {\n const raw = await safeReadExtracted(state, page);\n return buildParamsMap(state, raw);\n}\n\nasync function safeReadExtracted(\n state: RuntimeParamsState,\n page: RuntimeParamsPage,\n): Promise<Record<string, ExtractedEntry> | null> {\n try {\n return await readExtractedParameters(page);\n } catch {\n state.extractFailed = true;\n return null;\n }\n}\n\nfunction buildParamsMap(\n state: RuntimeParamsState,\n raw: Record<string, ExtractedEntry> | null,\n): Map<string, StoryParameters> {\n const out = new Map<string, StoryParameters>();\n if (raw) fillParamsMap(out, raw);\n if (out.size === 0) state.extractFailed = true;\n return out;\n}\n\nfunction fillParamsMap(\n out: Map<string, StoryParameters>,\n raw: Record<string, ExtractedEntry>,\n): void {\n for (const [id, entry] of Object.entries(raw)) {\n const merged = mergeParameters(entry);\n if (merged) out.set(id, merged);\n }\n}\n\nasync function readExtractedParameters(\n page: RuntimeParamsPage,\n): Promise<Record<string, ExtractedEntry> | null> {\n const map = (await page.evaluate(() => {\n const win = globalThis as unknown as {\n __STORYBOOK_PREVIEW__?: { extract?: () => unknown };\n };\n // oxlint-disable-next-line typescript/dot-notation -- __STORYBOOK_PREVIEW__ is a Storybook global\n const preview = win[\"__STORYBOOK_PREVIEW__\"];\n if (!preview || typeof preview.extract !== \"function\") return null;\n const extracted = preview.extract();\n return extracted && typeof extracted === \"object\"\n ? (extracted as Record<string, ExtractedEntry>)\n : null;\n })) as Record<string, ExtractedEntry> | null;\n return hasEntries(map) ? map : null;\n}\n\nfunction hasEntries(map: Record<string, ExtractedEntry> | null): boolean {\n return Boolean(map && Object.keys(map).length > 0);\n}\n"],"mappings":";;;;;;;AAOA,SAAgB,2BAA+C;CAC7D,OAAO,CAAC;AACV;;;;;AAMA,eAAsB,0BACpB,OACA,MACA,SACsC;CAEtC,QADY,MAAM,gBAAgB,KAAA,IAAY,MAAM,uBAAuB,OAAO,IAAI,EAAA,EAC1E,IAAI,OAAO;AACzB;AAEA,eAAe,uBACb,OACA,MACuC;CACvC,MAAM,mBAAmB,2BAA2B,OAAO,IAAI;CAC/D,OAAO,MAAM,MAAM;AACrB;AAEA,eAAe,2BACb,OACA,MACuC;CAEvC,OAAO,eAAe,OAAO,MADX,kBAAkB,OAAO,IAAI,CACf;AAClC;AAEA,eAAe,kBACb,OACA,MACgD;CAChD,IAAI;EACF,OAAO,MAAM,wBAAwB,IAAI;CAC3C,QAAQ;EACN,MAAM,gBAAgB;EACtB,OAAO;CACT;AACF;AAEA,SAAS,eACP,OACA,KAC8B;CAC9B,MAAM,sBAAM,IAAI,IAA6B;CAC7C,IAAI,KAAK,cAAc,KAAK,GAAG;CAC/B,IAAI,IAAI,SAAS,GAAG,MAAM,gBAAgB;CAC1C,OAAO;AACT;AAEA,SAAS,cACP,KACA,KACM;CACN,KAAK,MAAM,CAAC,IAAI,UAAU,OAAO,QAAQ,GAAG,GAAG;EAC7C,MAAM,SAAS,gBAAgB,KAAK;EACpC,IAAI,QAAQ,IAAI,IAAI,IAAI,MAAM;CAChC;AACF;AAEA,eAAe,wBACb,MACgD;CAChD,MAAM,MAAO,MAAM,KAAK,eAAe;EAKrC,MAAM,UAAUA,WAAI;EACpB,IAAI,CAAC,WAAW,OAAO,QAAQ,YAAY,YAAY,OAAO;EAC9D,MAAM,YAAY,QAAQ,QAAQ;EAClC,OAAO,aAAa,OAAO,cAAc,WACpC,YACD;CACN,CAAC;CACD,OAAO,WAAW,GAAG,IAAI,MAAM;AACjC;AAEA,SAAS,WAAW,KAAqD;CACvE,OAAO,QAAQ,OAAO,OAAO,KAAK,GAAG,CAAC,CAAC,SAAS,CAAC;AACnD"}
|
|
1
|
+
{"version":3,"file":"runtime-params.mjs","names":["win"],"sources":["../src/runtime-params.ts"],"sourcesContent":["/** Per-run runtime-parameter extraction cache (ADR 0017). */\n\nimport { mergeParameters } from \"@storyshelf/core/capture\";\nimport type { StoryParameters } from \"@storyshelf/core/capture\";\nimport type { ExtractedEntry, RuntimeParamsPage, RuntimeParamsState } from \"./types.ts\";\n\n/** Create the per-run runtime-parameter cache shared across captures. */\nexport function createRuntimeParamsState(): RuntimeParamsState {\n return {};\n}\n\n/**\n * Read story parameters from the running preview via\n * `__STORYBOOK_PREVIEW__.extract()`, once per run and lazily.\n */\nexport async function runtimeParametersForStory(\n state: RuntimeParamsState,\n page: RuntimeParamsPage,\n storyId: string,\n): Promise<StoryParameters | undefined> {\n const map = state.extractFailed ? undefined : await extractedParametersMap(state, page);\n return map?.get(storyId);\n}\n\n/** Read every story's resolved parameters from a loaded preview page in one call. */\nexport async function extractAllParameters(\n page: RuntimeParamsPage,\n): Promise<Record<string, StoryParameters>> {\n const raw = await readExtractedParameters(page);\n const out = new Map<string, StoryParameters>();\n if (raw) fillParamsMap(out, raw);\n return Object.fromEntries(out);\n}\n\nasync function extractedParametersMap(\n state: RuntimeParamsState,\n page: RuntimeParamsPage,\n): Promise<Map<string, StoryParameters>> {\n state.extractPromise ??= loadExtractedParametersMap(state, page);\n return await state.extractPromise;\n}\n\nasync function loadExtractedParametersMap(\n state: RuntimeParamsState,\n page: RuntimeParamsPage,\n): Promise<Map<string, StoryParameters>> {\n const raw = await safeReadExtracted(state, page);\n return buildParamsMap(state, raw);\n}\n\nasync function safeReadExtracted(\n state: RuntimeParamsState,\n page: RuntimeParamsPage,\n): Promise<Record<string, ExtractedEntry> | null> {\n try {\n return await readExtractedParameters(page);\n } catch {\n state.extractFailed = true;\n return null;\n }\n}\n\nfunction buildParamsMap(\n state: RuntimeParamsState,\n raw: Record<string, ExtractedEntry> | null,\n): Map<string, StoryParameters> {\n const out = new Map<string, StoryParameters>();\n if (raw) fillParamsMap(out, raw);\n if (out.size === 0) state.extractFailed = true;\n return out;\n}\n\nfunction fillParamsMap(\n out: Map<string, StoryParameters>,\n raw: Record<string, ExtractedEntry>,\n): void {\n for (const [id, entry] of Object.entries(raw)) {\n const merged = mergeParameters(entry);\n if (merged) out.set(id, merged);\n }\n}\n\nasync function readExtractedParameters(\n page: RuntimeParamsPage,\n): Promise<Record<string, ExtractedEntry> | null> {\n const map = (await page.evaluate(() => {\n const win = globalThis as unknown as {\n __STORYBOOK_PREVIEW__?: { extract?: () => unknown };\n };\n // oxlint-disable-next-line typescript/dot-notation -- __STORYBOOK_PREVIEW__ is a Storybook global\n const preview = win[\"__STORYBOOK_PREVIEW__\"];\n if (!preview || typeof preview.extract !== \"function\") return null;\n const extracted = preview.extract();\n return extracted && typeof extracted === \"object\"\n ? (extracted as Record<string, ExtractedEntry>)\n : null;\n })) as Record<string, ExtractedEntry> | null;\n return hasEntries(map) ? map : null;\n}\n\nfunction hasEntries(map: Record<string, ExtractedEntry> | null): boolean {\n return Boolean(map && Object.keys(map).length > 0);\n}\n"],"mappings":";;;;;;;AAOA,SAAgB,2BAA+C;CAC7D,OAAO,CAAC;AACV;;;;;AAMA,eAAsB,0BACpB,OACA,MACA,SACsC;CAEtC,QADY,MAAM,gBAAgB,KAAA,IAAY,MAAM,uBAAuB,OAAO,IAAI,EAAA,EAC1E,IAAI,OAAO;AACzB;;AAGA,eAAsB,qBACpB,MAC0C;CAC1C,MAAM,MAAM,MAAM,wBAAwB,IAAI;CAC9C,MAAM,sBAAM,IAAI,IAA6B;CAC7C,IAAI,KAAK,cAAc,KAAK,GAAG;CAC/B,OAAO,OAAO,YAAY,GAAG;AAC/B;AAEA,eAAe,uBACb,OACA,MACuC;CACvC,MAAM,mBAAmB,2BAA2B,OAAO,IAAI;CAC/D,OAAO,MAAM,MAAM;AACrB;AAEA,eAAe,2BACb,OACA,MACuC;CAEvC,OAAO,eAAe,OAAO,MADX,kBAAkB,OAAO,IAAI,CACf;AAClC;AAEA,eAAe,kBACb,OACA,MACgD;CAChD,IAAI;EACF,OAAO,MAAM,wBAAwB,IAAI;CAC3C,QAAQ;EACN,MAAM,gBAAgB;EACtB,OAAO;CACT;AACF;AAEA,SAAS,eACP,OACA,KAC8B;CAC9B,MAAM,sBAAM,IAAI,IAA6B;CAC7C,IAAI,KAAK,cAAc,KAAK,GAAG;CAC/B,IAAI,IAAI,SAAS,GAAG,MAAM,gBAAgB;CAC1C,OAAO;AACT;AAEA,SAAS,cACP,KACA,KACM;CACN,KAAK,MAAM,CAAC,IAAI,UAAU,OAAO,QAAQ,GAAG,GAAG;EAC7C,MAAM,SAAS,gBAAgB,KAAK;EACpC,IAAI,QAAQ,IAAI,IAAI,IAAI,MAAM;CAChC;AACF;AAEA,eAAe,wBACb,MACgD;CAChD,MAAM,MAAO,MAAM,KAAK,eAAe;EAKrC,MAAM,UAAUA,WAAI;EACpB,IAAI,CAAC,WAAW,OAAO,QAAQ,YAAY,YAAY,OAAO;EAC9D,MAAM,YAAY,QAAQ,QAAQ;EAClC,OAAO,aAAa,OAAO,cAAc,WACpC,YACD;CACN,CAAC;CACD,OAAO,WAAW,GAAG,IAAI,MAAM;AACjC;AAEA,SAAS,WAAW,KAAqD;CACvE,OAAO,QAAQ,OAAO,OAAO,KAAK,GAAG,CAAC,CAAC,SAAS,CAAC;AACnD"}
|
package/dist/screenshot.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
+
import { awaitPaint, settleTimeoutOf, waitForSettled } from "./settle.mjs";
|
|
5
|
+
import { acquirePage } from "./pages.mjs";
|
|
4
6
|
import { runtimeParametersForStory } from "./runtime-params.mjs";
|
|
5
7
|
import { checkA11y } from "./a11y.mjs";
|
|
6
8
|
import { runPlay } from "./play.mjs";
|
|
@@ -8,19 +10,20 @@ import { getScreenshotPlan } from "@storyshelf/core/capture";
|
|
|
8
10
|
//#region src/screenshot.ts
|
|
9
11
|
/** Capture a screenshot for one story/viewport, returning buffer + a11y violations. */
|
|
10
12
|
async function captureScreenshot(ctx, inputStory, viewport) {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
} });
|
|
13
|
+
const lease = await acquirePage(ctx, viewport);
|
|
14
|
+
let reusable = false;
|
|
15
|
+
if (ctx.navigationTimeoutMs) lease.page.setDefaultTimeout(ctx.navigationTimeoutMs);
|
|
15
16
|
try {
|
|
16
|
-
|
|
17
|
+
const result = await runCapture(lease.page, ctx, inputStory, viewport);
|
|
18
|
+
reusable = true;
|
|
19
|
+
return result;
|
|
17
20
|
} finally {
|
|
18
|
-
await
|
|
21
|
+
await lease.release(reusable);
|
|
19
22
|
}
|
|
20
23
|
}
|
|
21
24
|
async function runCapture(page, ctx, inputStory, viewport) {
|
|
22
|
-
const story = await navigateAndEnrich(ctx, page, inputStory);
|
|
23
|
-
await
|
|
25
|
+
const story = await navigateAndEnrich(ctx, page, inputStory, viewport);
|
|
26
|
+
await waitUntilReady(ctx, page, story);
|
|
24
27
|
if (ctx.executePlay) await runPlay(page, story.id, ctx.playTimeoutMs ?? 1e4);
|
|
25
28
|
const a11yViolations = await getA11y(ctx, page);
|
|
26
29
|
await settlePage(page, story);
|
|
@@ -32,8 +35,8 @@ async function runCapture(page, ctx, inputStory, viewport) {
|
|
|
32
35
|
story
|
|
33
36
|
};
|
|
34
37
|
}
|
|
35
|
-
async function navigateAndEnrich(ctx, page, story) {
|
|
36
|
-
await page.goto(ctx.adapter.buildUrl(ctx.baseUrl, story.id), { waitUntil: "networkidle" });
|
|
38
|
+
async function navigateAndEnrich(ctx, page, story, viewport) {
|
|
39
|
+
await page.goto(ctx.adapter.buildUrl(ctx.baseUrl, story.id, viewport.globals), { waitUntil: "networkidle" });
|
|
37
40
|
await ctx.adapter.waitForReady?.(page);
|
|
38
41
|
if (story.parameters) return story;
|
|
39
42
|
const rpPage = page;
|
|
@@ -43,22 +46,41 @@ async function navigateAndEnrich(ctx, page, story) {
|
|
|
43
46
|
parameters: params
|
|
44
47
|
} : story;
|
|
45
48
|
}
|
|
46
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Wait until the story is ready to be photographed: the selector exists, then Storybook says the
|
|
51
|
+
* render (and its play function) finished, then an explicit `parameters.delay` runs on top. A page
|
|
52
|
+
* that never signals (no channel, or no event before the timeout) gets today's fixed wait instead:
|
|
53
|
+
* `delay ?? 500` ms.
|
|
54
|
+
*/
|
|
55
|
+
async function waitUntilReady(ctx, page, story) {
|
|
47
56
|
if (!ctx.adapter.screenshotSelector) return;
|
|
48
57
|
await page.waitForSelector(ctx.adapter.screenshotSelector, { state: "attached" });
|
|
49
|
-
await page
|
|
58
|
+
const report = await waitForSettled(page, settleTimeoutOf(ctx));
|
|
59
|
+
if (report.settled) {
|
|
60
|
+
await awaitPaint(page);
|
|
61
|
+
if (story.parameters?.delay !== void 0) await page.waitForTimeout(story.parameters.delay);
|
|
62
|
+
} else await page.waitForTimeout(story.parameters?.delay ?? 500);
|
|
63
|
+
logSettle(ctx, story, report);
|
|
50
64
|
if (story.parameters?.pauseAnimationAtEnd) await page.evaluate(() => {
|
|
51
65
|
const doc = globalThis.document;
|
|
52
66
|
for (const anim of doc.getAnimations()) anim.pause();
|
|
53
67
|
});
|
|
54
68
|
}
|
|
69
|
+
/** One line per story that did not render cleanly: where it stopped, and what Storybook said. */
|
|
70
|
+
function logSettle(ctx, story, report) {
|
|
71
|
+
const problem = report.renderError ?? report.playError ?? report.unhandled?.[0];
|
|
72
|
+
if (report.settled && problem === void 0) return;
|
|
73
|
+
const detail = {
|
|
74
|
+
storyId: story.id,
|
|
75
|
+
phase: report.phase,
|
|
76
|
+
waitedMs: report.elapsedMs,
|
|
77
|
+
problem
|
|
78
|
+
};
|
|
79
|
+
if (!report.settled && report.hooked) ctx.logger?.warn(detail, "story did not signal that it finished rendering; used a fixed wait");
|
|
80
|
+
else if (problem !== void 0) ctx.logger?.warn(detail, "story reported an error while rendering");
|
|
81
|
+
}
|
|
55
82
|
async function getA11y(ctx, page) {
|
|
56
|
-
|
|
57
|
-
try {
|
|
58
|
-
return await checkA11y(page);
|
|
59
|
-
} catch {
|
|
60
|
-
return [];
|
|
61
|
-
}
|
|
83
|
+
return ctx.runA11y ? await checkA11y(page, ctx.logger) : [];
|
|
62
84
|
}
|
|
63
85
|
async function settlePage(page, story) {
|
|
64
86
|
const burst = story.parameters?.burst ?? 1;
|
package/dist/screenshot.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"screenshot.mjs","names":[],"sources":["../src/screenshot.ts"],"sourcesContent":["/** Screenshot pipeline for a single story + viewport. */\n\nimport type { StoryEntry, Viewport } from \"@storyshelf/core/adapter/capture-runner\";\nimport { getScreenshotPlan } from \"@storyshelf/core/capture\";\nimport type { Page } from \"playwright-core\";\nimport { checkA11y } from \"./a11y.ts\";\nimport { runPlay } from \"./play.ts\";\nimport { runtimeParametersForStory } from \"./runtime-params.ts\";\nimport type { CaptureContext, RuntimeParamsPage } from \"./types.ts\";\n\n/** Capture a screenshot for one story/viewport, returning buffer + a11y violations. */\nexport async function captureScreenshot(\n ctx: CaptureContext,\n inputStory: StoryEntry,\n viewport: Viewport,\n): Promise<{ screenshot: Buffer; a11yViolations: string[]; story: StoryEntry }> {\n const page = await ctx.browser.newPage({\n viewport: { width: viewport.width, height: viewport.height },\n });\n try {\n return await runCapture(page, ctx, inputStory, viewport);\n } finally {\n await page.close();\n }\n}\n\nasync function runCapture(\n page: Page,\n ctx: CaptureContext,\n inputStory: StoryEntry,\n viewport: Viewport,\n): Promise<{ screenshot: Buffer; a11yViolations: string[]; story: StoryEntry }> {\n const story = await navigateAndEnrich(ctx, page, inputStory);\n await maybeWaitForSelector(ctx, page, story);\n if (ctx.executePlay) await runPlay(page, story.id, ctx.playTimeoutMs ?? 10_000);\n const a11yViolations = await getA11y(ctx, page);\n await settlePage(page, story);\n const plan = await resolvePlan(page, viewport, story);\n if (plan.viewport.width !== viewport.width || plan.viewport.height !== viewport.height)\n await page.setViewportSize(plan.viewport);\n const screenshot = await burstShots(page, plan, story);\n return { screenshot, a11yViolations, story };\n}\n\nasync function navigateAndEnrich(\n ctx: CaptureContext,\n page: Page,\n story: StoryEntry,\n): Promise<StoryEntry> {\n await page.goto(ctx.adapter.buildUrl(ctx.baseUrl, story.id), { waitUntil: \"networkidle\" });\n await ctx.adapter.waitForReady?.(page);\n if (story.parameters) return story;\n const rpPage = page as unknown as RuntimeParamsPage;\n const params = await runtimeParametersForStory(ctx.runtimeParams, rpPage, story.id);\n return params ? { ...story, parameters: params } : story;\n}\nasync function maybeWaitForSelector(\n ctx: CaptureContext,\n page: Page,\n story: StoryEntry,\n): Promise<void> {\n if (!ctx.adapter.screenshotSelector) return;\n await page.waitForSelector(ctx.adapter.screenshotSelector, { state: \"attached\" });\n await page.waitForTimeout(story.parameters?.delay ?? 500);\n if (story.parameters?.pauseAnimationAtEnd) {\n await page.evaluate(() => {\n const doc = globalThis.document as unknown as {\n getAnimations: () => { pause: () => void }[];\n };\n for (const anim of doc.getAnimations()) anim.pause();\n });\n }\n}\nasync function getA11y(ctx: CaptureContext, page: Page): Promise<string[]> {\n if (!ctx.runA11y) return [];\n try {\n return await checkA11y(page);\n } catch {\n return [];\n }\n}\nasync function settlePage(page: Page, story: StoryEntry): Promise<void> {\n const burst = story.parameters?.burst ?? 1;\n const waitFonts = story.parameters?.waitForFonts ?? burst > 1;\n if (waitFonts) {\n try {\n await page.evaluate(async () => {\n await (globalThis as unknown as { document: { fonts: { ready: Promise<void> } } }).document\n .fonts.ready;\n });\n } catch {\n // best-effort\n }\n }\n try {\n await page.evaluate(() => {\n for (const vid of (globalThis as unknown as { document: Document }).document.querySelectorAll(\n \"video\",\n ))\n // oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- narrow to HTMLVideoElement\n (vid as unknown as HTMLVideoElement).pause();\n });\n } catch {\n // best-effort\n }\n}\nasync function resolvePlan(\n page: Page,\n viewport: Viewport,\n story: StoryEntry,\n): Promise<ReturnType<typeof getScreenshotPlan>> {\n const box = await page\n .locator(\"#storybook-root\")\n .boundingBox()\n .catch(() => null);\n return getScreenshotPlan(viewport, box, {\n autoCrop: story.parameters?.autoCrop,\n whitespaceThreshold: story.parameters?.autoCropThreshold,\n fullPageOnOverflow: story.parameters?.fullPageOnOverflow,\n minWidth: story.parameters?.minWidth,\n minHeight: story.parameters?.minHeight,\n maxWidth: story.parameters?.maxWidth,\n maxHeight: story.parameters?.maxHeight,\n });\n}\nasync function burstShots(\n page: Page,\n plan: ReturnType<typeof getScreenshotPlan>,\n story: StoryEntry,\n): Promise<Buffer> {\n const count = Math.max(1, Math.min(5, story.parameters?.burst ?? 1));\n const shots: Buffer[] = [];\n for (let i = 0; i < count; i += 1) {\n if (i > 0) {\n // oxlint-disable-next-line eslint/no-await-in-loop -- sequential burst\n await page.waitForTimeout(100);\n }\n // oxlint-disable-next-line eslint/no-await-in-loop -- sequential burst\n shots.push(await shotOne(page, plan, story));\n }\n const preferred = shots[Math.floor(shots.length / 2)] ?? shots[0];\n if (preferred) return preferred;\n const fallback: unknown = await page.screenshot({\n animations: story.parameters?.pauseAnimationAtEnd ? \"allow\" : \"disabled\",\n ...(plan.fullPage ? { fullPage: true } : {}),\n ...(plan.clip ? { clip: plan.clip } : {}),\n });\n // oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- Buffer narrowing\n return fallback as unknown as Buffer;\n}\nasync function shotOne(\n page: Page,\n plan: ReturnType<typeof getScreenshotPlan>,\n story: StoryEntry,\n): Promise<Buffer> {\n const shot: unknown = await page.screenshot({\n animations: story.parameters?.pauseAnimationAtEnd ? \"allow\" : \"disabled\",\n ...(plan.fullPage ? { fullPage: true } : {}),\n ...(plan.clip ? { clip: plan.clip } : {}),\n });\n // oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- Buffer narrowing\n return shot as unknown as Buffer;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"screenshot.mjs","names":[],"sources":["../src/screenshot.ts"],"sourcesContent":["/** Screenshot pipeline for a single story + viewport. */\n\nimport type { StoryEntry, Viewport } from \"@storyshelf/core/adapter/capture-runner\";\nimport { getScreenshotPlan } from \"@storyshelf/core/capture\";\nimport type { Page } from \"playwright-core\";\nimport { checkA11y } from \"./a11y.ts\";\nimport { acquirePage } from \"./pages.ts\";\nimport { runPlay } from \"./play.ts\";\nimport { runtimeParametersForStory } from \"./runtime-params.ts\";\nimport { awaitPaint, settleTimeoutOf, waitForSettled, type SettleReport } from \"./settle.ts\";\nimport type { CaptureContext, RuntimeParamsPage } from \"./types.ts\";\n\n/** Capture a screenshot for one story/viewport, returning buffer + a11y violations. */\nexport async function captureScreenshot(\n ctx: CaptureContext,\n inputStory: StoryEntry,\n viewport: Viewport,\n): Promise<{ screenshot: Buffer; a11yViolations: string[]; story: StoryEntry }> {\n const lease = await acquirePage(ctx, viewport);\n let reusable = false;\n if (ctx.navigationTimeoutMs) lease.page.setDefaultTimeout(ctx.navigationTimeoutMs);\n try {\n const result = await runCapture(lease.page, ctx, inputStory, viewport);\n reusable = true;\n return result;\n } finally {\n await lease.release(reusable);\n }\n}\n\nasync function runCapture(\n page: Page,\n ctx: CaptureContext,\n inputStory: StoryEntry,\n viewport: Viewport,\n): Promise<{ screenshot: Buffer; a11yViolations: string[]; story: StoryEntry }> {\n const story = await navigateAndEnrich(ctx, page, inputStory, viewport);\n await waitUntilReady(ctx, page, story);\n if (ctx.executePlay) await runPlay(page, story.id, ctx.playTimeoutMs ?? 10_000);\n const a11yViolations = await getA11y(ctx, page);\n await settlePage(page, story);\n const plan = await resolvePlan(page, viewport, story);\n if (plan.viewport.width !== viewport.width || plan.viewport.height !== viewport.height)\n await page.setViewportSize(plan.viewport);\n const screenshot = await burstShots(page, plan, story);\n return { screenshot, a11yViolations, story };\n}\n\nasync function navigateAndEnrich(\n ctx: CaptureContext,\n page: Page,\n story: StoryEntry,\n viewport: Viewport,\n): Promise<StoryEntry> {\n await page.goto(ctx.adapter.buildUrl(ctx.baseUrl, story.id, viewport.globals), {\n waitUntil: \"networkidle\",\n });\n await ctx.adapter.waitForReady?.(page);\n if (story.parameters) return story;\n const rpPage = page as unknown as RuntimeParamsPage;\n const params = await runtimeParametersForStory(ctx.runtimeParams, rpPage, story.id);\n return params ? { ...story, parameters: params } : story;\n}\n/**\n * Wait until the story is ready to be photographed: the selector exists, then Storybook says the\n * render (and its play function) finished, then an explicit `parameters.delay` runs on top. A page\n * that never signals (no channel, or no event before the timeout) gets today's fixed wait instead:\n * `delay ?? 500` ms.\n */\nasync function waitUntilReady(ctx: CaptureContext, page: Page, story: StoryEntry): Promise<void> {\n if (!ctx.adapter.screenshotSelector) return;\n await page.waitForSelector(ctx.adapter.screenshotSelector, { state: \"attached\" });\n const report = await waitForSettled(page, settleTimeoutOf(ctx));\n if (report.settled) {\n await awaitPaint(page);\n if (story.parameters?.delay !== undefined) await page.waitForTimeout(story.parameters.delay);\n } else {\n await page.waitForTimeout(story.parameters?.delay ?? 500);\n }\n logSettle(ctx, story, report);\n if (story.parameters?.pauseAnimationAtEnd) {\n await page.evaluate(() => {\n const doc = globalThis.document as unknown as {\n getAnimations: () => { pause: () => void }[];\n };\n for (const anim of doc.getAnimations()) anim.pause();\n });\n }\n}\n\n/** One line per story that did not render cleanly: where it stopped, and what Storybook said. */\nfunction logSettle(ctx: CaptureContext, story: StoryEntry, report: SettleReport): void {\n const problem = report.renderError ?? report.playError ?? report.unhandled?.[0];\n if (report.settled && problem === undefined) return;\n const detail = { storyId: story.id, phase: report.phase, waitedMs: report.elapsedMs, problem };\n if (!report.settled && report.hooked) {\n ctx.logger?.warn(detail, \"story did not signal that it finished rendering; used a fixed wait\");\n } else if (problem !== undefined) {\n ctx.logger?.warn(detail, \"story reported an error while rendering\");\n }\n}\nasync function getA11y(ctx: CaptureContext, page: Page): Promise<string[]> {\n // checkA11y reports its own failures as an annotation, so it never throws.\n return ctx.runA11y ? await checkA11y(page, ctx.logger) : [];\n}\nasync function settlePage(page: Page, story: StoryEntry): Promise<void> {\n const burst = story.parameters?.burst ?? 1;\n const waitFonts = story.parameters?.waitForFonts ?? burst > 1;\n if (waitFonts) {\n try {\n await page.evaluate(async () => {\n await (globalThis as unknown as { document: { fonts: { ready: Promise<void> } } }).document\n .fonts.ready;\n });\n } catch {\n // best-effort\n }\n }\n try {\n await page.evaluate(() => {\n for (const vid of (globalThis as unknown as { document: Document }).document.querySelectorAll(\n \"video\",\n ))\n // oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- narrow to HTMLVideoElement\n (vid as unknown as HTMLVideoElement).pause();\n });\n } catch {\n // best-effort\n }\n}\nasync function resolvePlan(\n page: Page,\n viewport: Viewport,\n story: StoryEntry,\n): Promise<ReturnType<typeof getScreenshotPlan>> {\n const box = await page\n .locator(\"#storybook-root\")\n .boundingBox()\n .catch(() => null);\n return getScreenshotPlan(viewport, box, {\n autoCrop: story.parameters?.autoCrop,\n whitespaceThreshold: story.parameters?.autoCropThreshold,\n fullPageOnOverflow: story.parameters?.fullPageOnOverflow,\n minWidth: story.parameters?.minWidth,\n minHeight: story.parameters?.minHeight,\n maxWidth: story.parameters?.maxWidth,\n maxHeight: story.parameters?.maxHeight,\n });\n}\nasync function burstShots(\n page: Page,\n plan: ReturnType<typeof getScreenshotPlan>,\n story: StoryEntry,\n): Promise<Buffer> {\n const count = Math.max(1, Math.min(5, story.parameters?.burst ?? 1));\n const shots: Buffer[] = [];\n for (let i = 0; i < count; i += 1) {\n if (i > 0) {\n // oxlint-disable-next-line eslint/no-await-in-loop -- sequential burst\n await page.waitForTimeout(100);\n }\n // oxlint-disable-next-line eslint/no-await-in-loop -- sequential burst\n shots.push(await shotOne(page, plan, story));\n }\n const preferred = shots[Math.floor(shots.length / 2)] ?? shots[0];\n if (preferred) return preferred;\n const fallback: unknown = await page.screenshot({\n animations: story.parameters?.pauseAnimationAtEnd ? \"allow\" : \"disabled\",\n ...(plan.fullPage ? { fullPage: true } : {}),\n ...(plan.clip ? { clip: plan.clip } : {}),\n });\n // oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- Buffer narrowing\n return fallback as unknown as Buffer;\n}\nasync function shotOne(\n page: Page,\n plan: ReturnType<typeof getScreenshotPlan>,\n story: StoryEntry,\n): Promise<Buffer> {\n const shot: unknown = await page.screenshot({\n animations: story.parameters?.pauseAnimationAtEnd ? \"allow\" : \"disabled\",\n ...(plan.fullPage ? { fullPage: true } : {}),\n ...(plan.clip ? { clip: plan.clip } : {}),\n });\n // oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- Buffer narrowing\n return shot as unknown as Buffer;\n}\n"],"mappings":"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"}
|
package/dist/settle.mjs
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
//#region src/settle.ts
|
|
5
|
+
/** How long to wait for the event when nothing else says (the play timeout raises it). */
|
|
6
|
+
const DEFAULT_SETTLE_TIMEOUT_MS = 5e3;
|
|
7
|
+
/** The wait for the event: the context's override, else the play timeout when plays run, else 5 s. */
|
|
8
|
+
function settleTimeoutOf(ctx) {
|
|
9
|
+
if (ctx.settleTimeoutMs !== void 0) return ctx.settleTimeoutMs;
|
|
10
|
+
return ctx.executePlay ? Math.max(DEFAULT_SETTLE_TIMEOUT_MS, ctx.playTimeoutMs ?? 0) : DEFAULT_SETTLE_TIMEOUT_MS;
|
|
11
|
+
}
|
|
12
|
+
/** Register the recorder on a page, once; it runs before the page's own scripts on every load. */
|
|
13
|
+
async function installSettleRecorder(page) {
|
|
14
|
+
await page.addInitScript(inPageRecorder);
|
|
15
|
+
}
|
|
16
|
+
/** Wait until Storybook says the story is done (or `timeoutMs` passes); never throws. */
|
|
17
|
+
async function waitForSettled(page, timeoutMs) {
|
|
18
|
+
try {
|
|
19
|
+
const report = await page.evaluate(inPageWaitSettled, { timeoutMs });
|
|
20
|
+
return isSettleReport(report) ? report : unsettled(false);
|
|
21
|
+
} catch {
|
|
22
|
+
return unsettled(false);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** A report for a wait that saw no signal. */
|
|
26
|
+
function unsettled(hooked) {
|
|
27
|
+
return {
|
|
28
|
+
settled: false,
|
|
29
|
+
hooked,
|
|
30
|
+
phase: null,
|
|
31
|
+
elapsedMs: 0
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function isSettleReport(value) {
|
|
35
|
+
if (typeof value !== "object" || value === null) return false;
|
|
36
|
+
const record = value;
|
|
37
|
+
return typeof record["settled"] === "boolean" && typeof record["hooked"] === "boolean";
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Runs in the browser before the page's scripts, on every document. It watches the preview's
|
|
41
|
+
* channel (`__STORYBOOK_ADDONS_CHANNEL__`, the public `Channel.on` API; nothing is patched) and
|
|
42
|
+
* records the events StoryShelf needs. Playwright serializes only this function's source, so it
|
|
43
|
+
* must not reference anything from this module; `settle.test.ts` evaluates it without bindings.
|
|
44
|
+
*/
|
|
45
|
+
function inPageRecorder() {
|
|
46
|
+
const scope = globalThis;
|
|
47
|
+
if (scope["__STORYSHELF_SETTLE__"]) return;
|
|
48
|
+
const state = {
|
|
49
|
+
hooked: false,
|
|
50
|
+
settled: false,
|
|
51
|
+
phase: null,
|
|
52
|
+
unhandled: [],
|
|
53
|
+
hook: () => false
|
|
54
|
+
};
|
|
55
|
+
const text = { of(payload) {
|
|
56
|
+
if (typeof payload === "string") return payload;
|
|
57
|
+
const record = payload ?? {};
|
|
58
|
+
const found = record["message"] ?? record["description"] ?? record["title"];
|
|
59
|
+
return typeof found === "string" ? found : "unknown error";
|
|
60
|
+
} };
|
|
61
|
+
const markSettled = () => {
|
|
62
|
+
state.settled = true;
|
|
63
|
+
};
|
|
64
|
+
const failRender = (payload) => {
|
|
65
|
+
state["renderError"] = text.of(payload);
|
|
66
|
+
markSettled();
|
|
67
|
+
};
|
|
68
|
+
const handlers = {
|
|
69
|
+
storyRenderPhaseChanged: (payload) => {
|
|
70
|
+
const phase = payload?.newPhase;
|
|
71
|
+
if (typeof phase === "string") state.phase = phase;
|
|
72
|
+
},
|
|
73
|
+
storyRendered: markSettled,
|
|
74
|
+
storyFinished: markSettled,
|
|
75
|
+
storyThrewException: failRender,
|
|
76
|
+
storyErrored: failRender,
|
|
77
|
+
storyMissing: failRender,
|
|
78
|
+
playFunctionThrewException: (payload) => {
|
|
79
|
+
state["playError"] = text.of(payload);
|
|
80
|
+
},
|
|
81
|
+
unhandledErrorsWhilePlaying: (payload) => {
|
|
82
|
+
for (const error of Array.isArray(payload) ? payload : [payload]) state.unhandled.push(text.of(error));
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
state.hook = () => {
|
|
86
|
+
if (state.hooked) return true;
|
|
87
|
+
const found = scope["__STORYBOOK_ADDONS_CHANNEL__"] ?? scope["__STORYBOOK_PREVIEW__"]?.channel;
|
|
88
|
+
if (!found || typeof found.on !== "function") return false;
|
|
89
|
+
for (const [event, handler] of Object.entries(handlers)) found.on(event, handler);
|
|
90
|
+
state.hooked = true;
|
|
91
|
+
return true;
|
|
92
|
+
};
|
|
93
|
+
scope["__STORYSHELF_SETTLE__"] = state;
|
|
94
|
+
if (state.hook()) return;
|
|
95
|
+
const started = Date.now();
|
|
96
|
+
const timer = setInterval(() => {
|
|
97
|
+
if (state.hook() || Date.now() - started > 3e4) clearInterval(timer);
|
|
98
|
+
}, 2);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Runs in the browser: resolves when the recorder saw a terminal event, the page has no channel
|
|
102
|
+
* to listen to, or `timeoutMs` passed. Self-contained for the same reason as the recorder.
|
|
103
|
+
*/
|
|
104
|
+
async function inPageWaitSettled({ timeoutMs }) {
|
|
105
|
+
const state = globalThis["__STORYSHELF_SETTLE__"];
|
|
106
|
+
const started = performance.now();
|
|
107
|
+
const report = (settled, hooked) => ({
|
|
108
|
+
settled,
|
|
109
|
+
hooked,
|
|
110
|
+
phase: state?.phase ?? null,
|
|
111
|
+
elapsedMs: Math.round(performance.now() - started),
|
|
112
|
+
...state?.renderError === void 0 ? {} : { renderError: state.renderError },
|
|
113
|
+
...state?.playError === void 0 ? {} : { playError: state.playError },
|
|
114
|
+
...state && state.unhandled.length > 0 ? { unhandled: [...state.unhandled] } : {}
|
|
115
|
+
});
|
|
116
|
+
if (!state) return report(false, false);
|
|
117
|
+
if (!state.hook() && globalThis.document?.readyState === "complete") return report(false, false);
|
|
118
|
+
while (performance.now() - started < timeoutMs) {
|
|
119
|
+
if (state.settled) return report(true, true);
|
|
120
|
+
await new Promise((resolve) => {
|
|
121
|
+
setTimeout(resolve, 10);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return report(state.settled, state.hook());
|
|
125
|
+
}
|
|
126
|
+
/** After the signal: let web fonts finish and two frames paint, so the screenshot sees the final DOM. */
|
|
127
|
+
async function awaitPaint(page) {
|
|
128
|
+
try {
|
|
129
|
+
await page.evaluate(inPagePaint);
|
|
130
|
+
} catch {}
|
|
131
|
+
}
|
|
132
|
+
/** Runs in the browser; bounded so a page that never paints (hidden tab) cannot hang the capture. */
|
|
133
|
+
async function inPagePaint() {
|
|
134
|
+
const scope = globalThis;
|
|
135
|
+
const frames = new Promise((resolve) => {
|
|
136
|
+
scope.requestAnimationFrame(() => {
|
|
137
|
+
scope.requestAnimationFrame(() => {
|
|
138
|
+
resolve();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
const cap = new Promise((resolve) => {
|
|
143
|
+
setTimeout(resolve, 250);
|
|
144
|
+
});
|
|
145
|
+
await Promise.race([Promise.all([scope.document.fonts?.ready, frames]), cap]);
|
|
146
|
+
}
|
|
147
|
+
//#endregion
|
|
148
|
+
export { DEFAULT_SETTLE_TIMEOUT_MS, awaitPaint, inPagePaint, inPageRecorder, inPageWaitSettled, installSettleRecorder, settleTimeoutOf, unsettled, waitForSettled };
|
|
149
|
+
|
|
150
|
+
//# sourceMappingURL=settle.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settle.mjs","names":[],"sources":["../src/settle.ts"],"sourcesContent":["/** Render-settled signalling: wait for Storybook's own \"render finished\" event, not a clock. */\n\nimport type { Page } from \"playwright-core\";\n\n/** What the page knew when the wait ended (all of it from Storybook's public channel events). */\nexport interface SettleReport {\n /** Storybook announced the story rendered, finished, errored or missing before the timeout. */\n settled: boolean;\n /** The preview's channel was found, so events could have arrived at all. */\n hooked: boolean;\n /** The last `storyRenderPhaseChanged` phase seen (`playing`, `errored`, `completed` ...). */\n phase: string | null;\n /** Milliseconds the wait took. */\n elapsedMs: number;\n /** The story failed to render (`storyThrewException`, `storyErrored`, `storyMissing`). */\n renderError?: string;\n /** The play function (or a `.test()` body) threw (`playFunctionThrewException`). */\n playError?: string;\n /** Errors Storybook reported after the play function (`unhandledErrorsWhilePlaying`). */\n unhandled?: string[];\n}\n\n/** How long to wait for the event when nothing else says (the play timeout raises it). */\nexport const DEFAULT_SETTLE_TIMEOUT_MS = 5000;\n\n/** The wait for the event: the context's override, else the play timeout when plays run, else 5 s. */\nexport function settleTimeoutOf(ctx: {\n settleTimeoutMs?: number | undefined;\n executePlay?: boolean | undefined;\n playTimeoutMs?: number | undefined;\n}): number {\n if (ctx.settleTimeoutMs !== undefined) return ctx.settleTimeoutMs;\n return ctx.executePlay\n ? Math.max(DEFAULT_SETTLE_TIMEOUT_MS, ctx.playTimeoutMs ?? 0)\n : DEFAULT_SETTLE_TIMEOUT_MS;\n}\n\n/** Register the recorder on a page, once; it runs before the page's own scripts on every load. */\nexport async function installSettleRecorder(page: Page): Promise<void> {\n await page.addInitScript(inPageRecorder);\n}\n\n/** Wait until Storybook says the story is done (or `timeoutMs` passes); never throws. */\nexport async function waitForSettled(page: Page, timeoutMs: number): Promise<SettleReport> {\n try {\n const report: unknown = await page.evaluate(inPageWaitSettled, { timeoutMs });\n return isSettleReport(report) ? report : unsettled(false);\n } catch {\n return unsettled(false);\n }\n}\n\n/** A report for a wait that saw no signal. */\nexport function unsettled(hooked: boolean): SettleReport {\n return { settled: false, hooked, phase: null, elapsedMs: 0 };\n}\n\nfunction isSettleReport(value: unknown): value is SettleReport {\n if (typeof value !== \"object\" || value === null) return false;\n const record = value as Record<string, unknown>;\n return typeof record[\"settled\"] === \"boolean\" && typeof record[\"hooked\"] === \"boolean\";\n}\n\n/**\n * Runs in the browser before the page's scripts, on every document. It watches the preview's\n * channel (`__STORYBOOK_ADDONS_CHANNEL__`, the public `Channel.on` API; nothing is patched) and\n * records the events StoryShelf needs. Playwright serializes only this function's source, so it\n * must not reference anything from this module; `settle.test.ts` evaluates it without bindings.\n */\n// oxlint-disable-next-line eslint/max-statements, eslint/max-lines-per-function -- must stay one self-contained function (see above)\nexport function inPageRecorder(): void {\n type Channel = { on: (event: string, cb: (payload: unknown) => void) => void };\n type State = Record<string, unknown> & {\n hooked: boolean;\n settled: boolean;\n phase: string | null;\n unhandled: string[];\n hook: () => boolean;\n };\n const scope = globalThis as unknown as Record<string, unknown>;\n if (scope[\"__STORYSHELF_SETTLE__\"]) return;\n const state: State = {\n hooked: false,\n settled: false,\n phase: null,\n unhandled: [],\n hook: () => false,\n };\n const text = {\n of(payload: unknown): string {\n if (typeof payload === \"string\") return payload;\n const record = (payload ?? {}) as Record<string, unknown>;\n const found = record[\"message\"] ?? record[\"description\"] ?? record[\"title\"];\n return typeof found === \"string\" ? found : \"unknown error\";\n },\n };\n const markSettled = (): void => {\n state.settled = true;\n };\n const failRender = (payload: unknown): void => {\n state[\"renderError\"] = text.of(payload);\n markSettled();\n };\n const handlers: Record<string, (payload: unknown) => void> = {\n storyRenderPhaseChanged: (payload) => {\n const phase = (payload as { newPhase?: unknown } | undefined)?.newPhase;\n if (typeof phase === \"string\") state.phase = phase;\n },\n storyRendered: markSettled,\n storyFinished: markSettled,\n storyThrewException: failRender,\n storyErrored: failRender,\n storyMissing: failRender,\n playFunctionThrewException: (payload) => {\n state[\"playError\"] = text.of(payload);\n },\n unhandledErrorsWhilePlaying: (payload) => {\n for (const error of Array.isArray(payload) ? payload : [payload]) {\n state.unhandled.push(text.of(error));\n }\n },\n };\n state.hook = (): boolean => {\n if (state.hooked) return true;\n const found = (scope[\"__STORYBOOK_ADDONS_CHANNEL__\"] ??\n (scope[\"__STORYBOOK_PREVIEW__\"] as { channel?: unknown } | undefined)?.channel) as\n | Channel\n | undefined;\n if (!found || typeof found.on !== \"function\") return false;\n for (const [event, handler] of Object.entries(handlers)) found.on(event, handler);\n state.hooked = true;\n return true;\n };\n scope[\"__STORYSHELF_SETTLE__\"] = state;\n // The channel is created by iframe.html's bundle, before the first story renders: catch it\n // within a few ms. Give up after a while (a page that is not Storybook never gets one).\n if (state.hook()) return;\n const started = Date.now();\n const timer = setInterval(() => {\n if (state.hook() || Date.now() - started > 30_000) clearInterval(timer);\n }, 2);\n}\n\n/** Arguments serialized into the page for {@link inPageWaitSettled}. */\nexport interface SettleArgs {\n timeoutMs: number;\n}\n\n/**\n * Runs in the browser: resolves when the recorder saw a terminal event, the page has no channel\n * to listen to, or `timeoutMs` passed. Self-contained for the same reason as the recorder.\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 inPageWaitSettled({ timeoutMs }: SettleArgs): Promise<SettleReport> {\n type State = {\n hooked: boolean;\n settled: boolean;\n phase: string | null;\n unhandled: string[];\n hook: () => boolean;\n renderError?: string;\n playError?: string;\n };\n const state = (globalThis as unknown as Record<string, State | undefined>)[\n \"__STORYSHELF_SETTLE__\"\n ];\n const started = performance.now();\n const report = (settled: boolean, hooked: boolean): SettleReport => ({\n settled,\n hooked,\n phase: state?.phase ?? null,\n elapsedMs: Math.round(performance.now() - started),\n ...(state?.renderError === undefined ? {} : { renderError: state.renderError }),\n ...(state?.playError === undefined ? {} : { playError: state.playError }),\n ...(state && state.unhandled.length > 0 ? { unhandled: [...state.unhandled] } : {}),\n });\n if (!state) return report(false, false);\n // After `load` every script has run: no channel by now means there will never be one.\n if (!state.hook() && globalThis.document?.readyState === \"complete\") return report(false, false);\n while (performance.now() - started < timeoutMs) {\n if (state.settled) return report(true, true);\n // oxlint-disable-next-line eslint/no-await-in-loop -- polling the recorded state\n await new Promise<void>((resolve) => {\n setTimeout(resolve, 10);\n });\n }\n return report(state.settled, state.hook());\n}\n\n/** After the signal: let web fonts finish and two frames paint, so the screenshot sees the final DOM. */\nexport async function awaitPaint(page: Page): Promise<void> {\n try {\n await page.evaluate(inPagePaint);\n } catch {\n // best-effort: the screenshot still follows\n }\n}\n\n/** Runs in the browser; bounded so a page that never paints (hidden tab) cannot hang the capture. */\nexport async function inPagePaint(): Promise<void> {\n const scope = globalThis as unknown as {\n document: { fonts?: { ready: Promise<unknown> } };\n requestAnimationFrame: (cb: () => void) => void;\n };\n const frames = new Promise<void>((resolve) => {\n scope.requestAnimationFrame(() => {\n scope.requestAnimationFrame(() => {\n resolve();\n });\n });\n });\n const cap = new Promise<void>((resolve) => {\n setTimeout(resolve, 250);\n });\n await Promise.race([Promise.all([scope.document.fonts?.ready, frames]), cap]);\n}\n"],"mappings":"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"}
|
package/dist/static-server.mjs
CHANGED
|
@@ -3,9 +3,9 @@ import "node:url";
|
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { isPathSafe, mimeFor } from "@storyshelf/core/utils";
|
|
5
5
|
import { createReadStream } from "node:fs";
|
|
6
|
+
import { join, normalize } from "node:path";
|
|
6
7
|
import { stat } from "node:fs/promises";
|
|
7
8
|
import { createServer } from "node:http";
|
|
8
|
-
import { join, normalize } from "node:path";
|
|
9
9
|
//#region src/static-server.ts
|
|
10
10
|
/**
|
|
11
11
|
* Serve a built Storybook directory over HTTP on 127.0.0.1 for capture.
|
|
@@ -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/targets.d.ts
|
|
5
|
+
/** Options of {@link local}. */
|
|
6
|
+
interface LocalOptions {
|
|
7
|
+
/** Playwright channel (`chrome`, `chrome-beta`, `msedge`, …); only for `chromium` and `chrome`. */
|
|
8
|
+
channel?: string;
|
|
9
|
+
}
|
|
10
|
+
/** A local engine with options (ADR 0007, decision 1). */
|
|
11
|
+
interface LocalBrowser {
|
|
12
|
+
readonly kind: "local";
|
|
13
|
+
readonly engine: BrowserName;
|
|
14
|
+
readonly options: LocalOptions;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* One entry of `browsers`: a bare engine name, {@link local}, or a remote service from
|
|
18
|
+
* `@storyshelf/runner-playwright/remote` (`playwrightServer(...)`, `remote(...)`, ...), which
|
|
19
|
+
* stands for one target per engine.
|
|
20
|
+
*/
|
|
21
|
+
type BrowserEntry = BrowserName | LocalBrowser | RemoteBrowser;
|
|
22
|
+
/** A local engine with options, for `browsers`: `local("chrome", { channel: "chrome-beta" })`. */
|
|
23
|
+
declare function local(engine: BrowserName, options?: LocalOptions): LocalBrowser;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { BrowserEntry, LocalBrowser, LocalOptions, local };
|
|
26
|
+
//# sourceMappingURL=targets.d.mts.map
|