@storyshelf/runner-puppeteer 0.6.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +73 -1
  2. package/dist/a11y.mjs +22 -22
  3. package/dist/a11y.mjs.map +1 -1
  4. package/dist/availability.mjs +30 -0
  5. package/dist/availability.mjs.map +1 -0
  6. package/dist/browser.mjs +27 -16
  7. package/dist/browser.mjs.map +1 -1
  8. package/dist/capture-runner.d.mts +20 -5
  9. package/dist/capture-runner.mjs +87 -14
  10. package/dist/capture-runner.mjs.map +1 -1
  11. package/dist/connect.mjs +45 -0
  12. package/dist/connect.mjs.map +1 -0
  13. package/dist/index.d.mts +2 -1
  14. package/dist/index.mjs +2 -1
  15. package/dist/launch.mjs +52 -0
  16. package/dist/launch.mjs.map +1 -0
  17. package/dist/pages.d.mts +5 -0
  18. package/dist/pages.mjs +90 -0
  19. package/dist/pages.mjs.map +1 -0
  20. package/dist/pipeline.mjs +178 -39
  21. package/dist/pipeline.mjs.map +1 -1
  22. package/dist/play.mjs +16 -4
  23. package/dist/play.mjs.map +1 -1
  24. package/dist/remote/cdp.d.mts +54 -0
  25. package/dist/remote/cdp.mjs +48 -0
  26. package/dist/remote/cdp.mjs.map +1 -0
  27. package/dist/remote/index.d.mts +5 -0
  28. package/dist/remote/index.mjs +6 -0
  29. package/dist/remote-environment.mjs +59 -0
  30. package/dist/remote-environment.mjs.map +1 -0
  31. package/dist/runtime-params.mjs +1 -2
  32. package/dist/screenshot.mjs +77 -24
  33. package/dist/screenshot.mjs.map +1 -1
  34. package/dist/session.d.mts +3 -0
  35. package/dist/session.mjs +141 -0
  36. package/dist/session.mjs.map +1 -0
  37. package/dist/settle.mjs +157 -0
  38. package/dist/settle.mjs.map +1 -0
  39. package/dist/static-server.mjs +2 -4
  40. package/dist/static-server.mjs.map +1 -1
  41. package/dist/targets.d.mts +23 -0
  42. package/dist/targets.mjs +79 -0
  43. package/dist/targets.mjs.map +1 -0
  44. package/dist/tests.mjs +119 -0
  45. package/dist/tests.mjs.map +1 -0
  46. package/dist/types.d.mts +4 -2
  47. package/package.json +18 -11
@@ -1,12 +1,11 @@
1
- import "node:path";
1
+ import { join, normalize } from "node:path";
2
2
  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
6
  import { stat } from "node:fs/promises";
7
7
  import { createServer } from "node:http";
8
- import { join, normalize } from "node:path";
9
- //#region src/static-server.ts
8
+ // src/static-server.ts
10
9
  /**
11
10
  * Serve a built Storybook directory over HTTP on 127.0.0.1 for capture.
12
11
  *
@@ -68,7 +67,6 @@ async function closeServer(server) {
68
67
  });
69
68
  });
70
69
  }
71
- //#endregion
72
70
  export { createStaticServer };
73
71
 
74
72
  //# sourceMappingURL=static-server.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"static-server.mjs","names":[],"sources":["../src/static-server.ts"],"sourcesContent":["import { isPathSafe, mimeFor } from \"@storyshelf/core/utils\";\nimport { createReadStream } from \"node:fs\";\nimport { stat } from \"node:fs/promises\";\nimport { createServer, type Server, type ServerResponse } from \"node:http\";\nimport { join, normalize } from \"node:path\";\n\n/** A local HTTP server serving a built Storybook directory for capture. */\nexport interface StaticServer {\n url: string;\n close(): Promise<void>;\n}\n\n/**\n * Serve a built Storybook directory over HTTP on 127.0.0.1 for capture.\n *\n * @param rootDir - Directory containing the built Storybook (`index.html` entry).\n * @returns The server URL and a `close` function to stop it.\n */\nexport async function createStaticServer(rootDir: string): Promise<StaticServer> {\n const root = normalize(rootDir);\n const info = await stat(root).catch(() => null);\n if (!info || !info.isDirectory()) {\n throw new Error(\n `Storybook directory not found: ${rootDir}. Upload the built Storybook before capturing.`,\n );\n }\n\n const server = createServer((req, res) => {\n handleRequest(root, req.url ?? \"/\", res).catch(() => {\n sendNotFound(res);\n });\n });\n\n const port = await listen(server);\n\n return {\n url: `http://127.0.0.1:${port}`,\n close: async () => {\n await closeServer(server);\n },\n };\n}\n\nfunction sendNotFound(res: ServerResponse): void {\n res.writeHead(404, { \"content-type\": \"text/plain\" });\n res.end(\"Not found\");\n}\n\nasync function resolveFile(root: string, urlPath: string): Promise<string | null> {\n const clean = (urlPath.split(\"?\")[0] ?? \"/\").replace(/^\\/+/u, \"\");\n const target = normalize(join(root, clean === \"\" ? \"index.html\" : clean));\n if (!isPathSafe(root, target)) {\n return null;\n }\n const info = await stat(target).catch(() => null);\n if (!info) {\n return null;\n }\n if (info.isDirectory()) {\n return resolveFile(root, `${clean}/index.html`);\n }\n return info.isFile() ? target : null;\n}\n\nasync function handleRequest(root: string, url: string, res: ServerResponse): Promise<void> {\n const file = await resolveFile(root, url);\n if (!file) {\n sendNotFound(res);\n return;\n }\n res.writeHead(200, { \"content-type\": mimeFor(file) });\n createReadStream(file).pipe(res);\n}\n\nasync function listen(server: Server): Promise<number> {\n return await new Promise((resolve, reject) => {\n server.once(\"error\", reject);\n server.listen(0, \"127.0.0.1\", () => {\n const address = server.address();\n resolve(typeof address === \"object\" && address !== null ? address.port : 0);\n });\n });\n}\n\nasync function closeServer(server: Server): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n server.close((error) => {\n if (error) {\n reject(error);\n } else {\n resolve();\n }\n });\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;AAkBA,eAAsB,mBAAmB,SAAwC;CAC/E,MAAM,OAAO,UAAU,OAAO;CAC9B,MAAM,OAAO,MAAM,KAAK,IAAI,CAAC,CAAC,YAAY,IAAI;CAC9C,IAAI,CAAC,QAAQ,CAAC,KAAK,YAAY,GAC7B,MAAM,IAAI,MACR,kCAAkC,QAAQ,+CAC5C;CAGF,MAAM,SAAS,cAAc,KAAK,QAAQ;EACxC,cAAc,MAAM,IAAI,OAAO,KAAK,GAAG,CAAC,CAAC,YAAY;GACnD,aAAa,GAAG;EAClB,CAAC;CACH,CAAC;CAID,OAAO;EACL,KAAK,oBAAoB,MAHR,OAAO,MAAM;EAI9B,OAAO,YAAY;GACjB,MAAM,YAAY,MAAM;EAC1B;CACF;AACF;AAEA,SAAS,aAAa,KAA2B;CAC/C,IAAI,UAAU,KAAK,EAAE,gBAAgB,aAAa,CAAC;CACnD,IAAI,IAAI,WAAW;AACrB;AAEA,eAAe,YAAY,MAAc,SAAyC;CAChF,MAAM,SAAS,QAAQ,MAAM,GAAG,CAAC,CAAC,MAAM,IAAA,CAAK,QAAQ,SAAS,EAAE;CAChE,MAAM,SAAS,UAAU,KAAK,MAAM,UAAU,KAAK,eAAe,KAAK,CAAC;CACxE,IAAI,CAAC,WAAW,MAAM,MAAM,GAC1B,OAAO;CAET,MAAM,OAAO,MAAM,KAAK,MAAM,CAAC,CAAC,YAAY,IAAI;CAChD,IAAI,CAAC,MACH,OAAO;CAET,IAAI,KAAK,YAAY,GACnB,OAAO,YAAY,MAAM,GAAG,MAAM,YAAY;CAEhD,OAAO,KAAK,OAAO,IAAI,SAAS;AAClC;AAEA,eAAe,cAAc,MAAc,KAAa,KAAoC;CAC1F,MAAM,OAAO,MAAM,YAAY,MAAM,GAAG;CACxC,IAAI,CAAC,MAAM;EACT,aAAa,GAAG;EAChB;CACF;CACA,IAAI,UAAU,KAAK,EAAE,gBAAgB,QAAQ,IAAI,EAAE,CAAC;CACpD,iBAAiB,IAAI,CAAC,CAAC,KAAK,GAAG;AACjC;AAEA,eAAe,OAAO,QAAiC;CACrD,OAAO,MAAM,IAAI,SAAS,SAAS,WAAW;EAC5C,OAAO,KAAK,SAAS,MAAM;EAC3B,OAAO,OAAO,GAAG,mBAAmB;GAClC,MAAM,UAAU,OAAO,QAAQ;GAC/B,QAAQ,OAAO,YAAY,YAAY,YAAY,OAAO,QAAQ,OAAO,CAAC;EAC5E,CAAC;CACH,CAAC;AACH;AAEA,eAAe,YAAY,QAA+B;CACxD,MAAM,IAAI,SAAe,SAAS,WAAW;EAC3C,OAAO,OAAO,UAAU;GACtB,IAAI,OACF,OAAO,KAAK;QAEZ,QAAQ;EAEZ,CAAC;CACH,CAAC;AACH"}
1
+ {"version":3,"file":"static-server.mjs","names":[],"sources":["../src/static-server.ts"],"sourcesContent":["import { isPathSafe, mimeFor } from \"@storyshelf/core/utils\";\nimport { createReadStream } from \"node:fs\";\nimport { stat } from \"node:fs/promises\";\nimport { createServer, type Server, type ServerResponse } from \"node:http\";\nimport { join, normalize } from \"node:path\";\n\n/** A local HTTP server serving a built Storybook directory for capture. */\nexport interface StaticServer {\n url: string;\n close(): Promise<void>;\n}\n\n/**\n * Serve a built Storybook directory over HTTP on 127.0.0.1 for capture.\n *\n * @param rootDir - Directory containing the built Storybook (`index.html` entry).\n * @returns The server URL and a `close` function to stop it.\n */\nexport async function createStaticServer(rootDir: string): Promise<StaticServer> {\n const root = normalize(rootDir);\n const info = await stat(root).catch(() => null);\n if (!info || !info.isDirectory()) {\n throw new Error(\n `Storybook directory not found: ${rootDir}. Upload the built Storybook before capturing.`,\n );\n }\n\n const server = createServer((req, res) => {\n handleRequest(root, req.url ?? \"/\", res).catch(() => {\n sendNotFound(res);\n });\n });\n\n const port = await listen(server);\n\n return {\n url: `http://127.0.0.1:${port}`,\n close: async () => {\n await closeServer(server);\n },\n };\n}\n\nfunction sendNotFound(res: ServerResponse): void {\n res.writeHead(404, { \"content-type\": \"text/plain\" });\n res.end(\"Not found\");\n}\n\nasync function resolveFile(root: string, urlPath: string): Promise<string | null> {\n const clean = (urlPath.split(\"?\")[0] ?? \"/\").replace(/^\\/+/u, \"\");\n const target = normalize(join(root, clean === \"\" ? \"index.html\" : clean));\n if (!isPathSafe(root, target)) {\n return null;\n }\n const info = await stat(target).catch(() => null);\n if (!info) {\n return null;\n }\n if (info.isDirectory()) {\n return resolveFile(root, `${clean}/index.html`);\n }\n return info.isFile() ? target : null;\n}\n\nasync function handleRequest(root: string, url: string, res: ServerResponse): Promise<void> {\n const file = await resolveFile(root, url);\n if (!file) {\n sendNotFound(res);\n return;\n }\n res.writeHead(200, { \"content-type\": mimeFor(file) });\n createReadStream(file).pipe(res);\n}\n\nasync function listen(server: Server): Promise<number> {\n return await new Promise((resolve, reject) => {\n server.once(\"error\", reject);\n server.listen(0, \"127.0.0.1\", () => {\n const address = server.address();\n resolve(typeof address === \"object\" && address !== null ? address.port : 0);\n });\n });\n}\n\nasync function closeServer(server: Server): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n server.close((error) => {\n if (error) {\n reject(error);\n } else {\n resolve();\n }\n });\n });\n}\n"],"mappings":";;;;;;;;;;;;;;AAkBA,eAAsB,mBAAmB,SAAwC;CAC/E,MAAM,OAAO,UAAU,OAAO;CAC9B,MAAM,OAAO,MAAM,KAAK,IAAI,CAAC,CAAC,YAAY,IAAI;CAC9C,IAAI,CAAC,QAAQ,CAAC,KAAK,YAAY,GAC7B,MAAM,IAAI,MACR,kCAAkC,QAAQ,+CAC5C;CAGF,MAAM,SAAS,cAAc,KAAK,QAAQ;EACxC,cAAc,MAAM,IAAI,OAAO,KAAK,GAAG,CAAC,CAAC,YAAY;GACnD,aAAa,GAAG;EAClB,CAAC;CACH,CAAC;CAID,OAAO;EACL,KAAK,oBAAoB,MAHR,OAAO,MAAM;EAI9B,OAAO,YAAY;GACjB,MAAM,YAAY,MAAM;EAC1B;CACF;AACF;AAEA,SAAS,aAAa,KAA2B;CAC/C,IAAI,UAAU,KAAK,EAAE,gBAAgB,aAAa,CAAC;CACnD,IAAI,IAAI,WAAW;AACrB;AAEA,eAAe,YAAY,MAAc,SAAyC;CAChF,MAAM,SAAS,QAAQ,MAAM,GAAG,CAAC,CAAC,MAAM,IAAA,CAAK,QAAQ,SAAS,EAAE;CAChE,MAAM,SAAS,UAAU,KAAK,MAAM,UAAU,KAAK,eAAe,KAAK,CAAC;CACxE,IAAI,CAAC,WAAW,MAAM,MAAM,GAC1B,OAAO;CAET,MAAM,OAAO,MAAM,KAAK,MAAM,CAAC,CAAC,YAAY,IAAI;CAChD,IAAI,CAAC,MACH,OAAO;CAET,IAAI,KAAK,YAAY,GACnB,OAAO,YAAY,MAAM,GAAG,MAAM,YAAY;CAEhD,OAAO,KAAK,OAAO,IAAI,SAAS;AAClC;AAEA,eAAe,cAAc,MAAc,KAAa,KAAoC;CAC1F,MAAM,OAAO,MAAM,YAAY,MAAM,GAAG;CACxC,IAAI,CAAC,MAAM;EACT,aAAa,GAAG;EAChB;CACF;CACA,IAAI,UAAU,KAAK,EAAE,gBAAgB,QAAQ,IAAI,EAAE,CAAC;CACpD,iBAAiB,IAAI,CAAC,CAAC,KAAK,GAAG;AACjC;AAEA,eAAe,OAAO,QAAiC;CACrD,OAAO,MAAM,IAAI,SAAS,SAAS,WAAW;EAC5C,OAAO,KAAK,SAAS,MAAM;EAC3B,OAAO,OAAO,GAAG,mBAAmB;GAClC,MAAM,UAAU,OAAO,QAAQ;GAC/B,QAAQ,OAAO,YAAY,YAAY,YAAY,OAAO,QAAQ,OAAO,CAAC;EAC5E,CAAC;CACH,CAAC;AACH;AAEA,eAAe,YAAY,QAA+B;CACxD,MAAM,IAAI,SAAe,SAAS,WAAW;EAC3C,OAAO,OAAO,UAAU;GACtB,IAAI,OACF,OAAO,KAAK;QAEZ,QAAQ;EAEZ,CAAC;CACH,CAAC;AACH"}
@@ -0,0 +1,23 @@
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
+ /** Options of {@link local}. */
5
+ interface LocalOptions {
6
+ /** The browser binary this target launches (`puppeteer-core` ships none). */
7
+ executablePath?: string;
8
+ }
9
+ /** A local engine with options (ADR 0007, decision 1). */
10
+ interface LocalBrowser {
11
+ readonly kind: "local";
12
+ readonly engine: BrowserName;
13
+ readonly options: LocalOptions;
14
+ }
15
+ /**
16
+ * One entry of `browsers`: a bare engine name, {@link local}, or a remote CDP service from
17
+ * `@storyshelf/runner-puppeteer/remote` (`cdp(...)`), which stands for one target per engine.
18
+ */
19
+ type BrowserEntry = BrowserName | LocalBrowser | RemoteBrowser;
20
+ /** A local engine with options, for `browsers`: `local("chrome", { executablePath: "/usr/bin/google-chrome" })`. */
21
+ declare function local(engine: BrowserName, options?: LocalOptions): LocalBrowser;
22
+ export { BrowserEntry, LocalBrowser, LocalOptions, local };
23
+ //# sourceMappingURL=targets.d.mts.map
@@ -0,0 +1,79 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { isBrowserName, isRemoteBrowser, remoteTargetsOf } from "@storyshelf/core/adapter/capture-runner";
5
+ // src/targets.ts
6
+ /** The runner's `browsers` option: what an operator lists, and the local targets it flattens to. */
7
+ /** Engines Puppeteer drives: the Chromium family. */
8
+ const SUPPORTED = ["chromium", "chrome"];
9
+ /** A local engine with options, for `browsers`: `local("chrome", { executablePath: "/usr/bin/google-chrome" })`. */
10
+ function local(engine, options = {}) {
11
+ if (!isBrowserName(engine)) throw new Error(`local(): unknown engine "${String(engine)}"`);
12
+ if (!SUPPORTED.includes(engine)) throw new Error(`local("${engine}"): the Puppeteer runner drives Chromium-family engines only (${SUPPORTED.join(", ")})`);
13
+ return {
14
+ kind: "local",
15
+ engine,
16
+ options
17
+ };
18
+ }
19
+ /**
20
+ * Flatten the `browsers` list into targets, first = default. Throws on an empty list, an engine
21
+ * Puppeteer cannot drive, bare `chrome` (it needs its own `executablePath`, since the runner's
22
+ * would launch whatever it points at under Chrome's name), or two entries producing one target,
23
+ * so a mistake fails when the runner is created and not when the first build runs.
24
+ */
25
+ function flattenBrowsers(entries, runnerExecutablePath) {
26
+ if (entries.length === 0) throw new Error("browsers must list at least one target");
27
+ const seen = /* @__PURE__ */ new Map();
28
+ const flat = {
29
+ declared: [],
30
+ locals: [],
31
+ remotes: []
32
+ };
33
+ for (const [index, entry] of entries.entries()) {
34
+ const produced = isRemoteBrowser(entry) ? remoteTargetsOf(entry) : [localOf(entry, runnerExecutablePath)];
35
+ for (const target of produced) {
36
+ claim(seen, target.target, index);
37
+ add(flat, target);
38
+ }
39
+ }
40
+ return flat;
41
+ }
42
+ /** Record that entry `index` produces `target`, refusing a second entry that does. */
43
+ function claim(seen, target, index) {
44
+ const first = seen.get(target);
45
+ if (first !== void 0) throw new Error(`browsers entries ${first + 1} and ${index + 1} both produce the target "${target}"`);
46
+ seen.set(target, index);
47
+ }
48
+ function localOf(entry, runnerExecutablePath) {
49
+ const { engine, options } = typeof entry === "string" ? local(entry) : entry;
50
+ return {
51
+ target: engine,
52
+ executablePath: pathOf(engine, options, runnerExecutablePath)
53
+ };
54
+ }
55
+ function add(flat, target) {
56
+ if ("kind" in target) {
57
+ flat.remotes.push(target);
58
+ flat.declared.push({
59
+ target: target.target,
60
+ reach: target.reach,
61
+ status: "ready"
62
+ });
63
+ } else {
64
+ flat.locals.push(target);
65
+ flat.declared.push({
66
+ target: target.target,
67
+ reach: "loopback",
68
+ status: "ready"
69
+ });
70
+ }
71
+ }
72
+ function pathOf(engine, options, runnerExecutablePath) {
73
+ if (options.executablePath !== void 0) return options.executablePath;
74
+ if (engine === "chrome") throw new Error(`"chrome" needs an executablePath: use local("chrome", { executablePath: "/usr/bin/google-chrome" })`);
75
+ return runnerExecutablePath;
76
+ }
77
+ export { flattenBrowsers, local };
78
+
79
+ //# sourceMappingURL=targets.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"targets.mjs","names":[],"sources":["../src/targets.ts"],"sourcesContent":["/** The runner's `browsers` option: what an operator lists, and the local targets it flattens to. */\nimport {\n isBrowserName,\n isRemoteBrowser,\n remoteTargetsOf,\n type BrowserName,\n type RemoteBrowser,\n type RemoteTarget,\n type RunnerTarget,\n} from \"@storyshelf/core/adapter/capture-runner\";\n\n/** Options of {@link local}. */\nexport interface LocalOptions {\n /** The browser binary this target launches (`puppeteer-core` ships none). */\n executablePath?: string;\n}\n\n/** A local engine with options (ADR 0007, decision 1). */\nexport interface LocalBrowser {\n readonly kind: \"local\";\n readonly engine: BrowserName;\n readonly options: LocalOptions;\n}\n\n/**\n * One entry of `browsers`: a bare engine name, {@link local}, or a remote CDP service from\n * `@storyshelf/runner-puppeteer/remote` (`cdp(...)`), which stands for one target per engine.\n */\nexport type BrowserEntry = BrowserName | LocalBrowser | RemoteBrowser;\n\n/** A local engine this runner launches. */\nexport interface LocalTarget {\n /** The engine name, which is also the target. */\n target: BrowserName;\n /** The binary to launch: the entry's own, else the runner's for `chromium`; none for bare `chrome`. */\n executablePath: string | undefined;\n}\n\n/** Engines Puppeteer drives: the Chromium family. */\nconst SUPPORTED: readonly BrowserName[] = [\"chromium\", \"chrome\"];\n\n/** A local engine with options, for `browsers`: `local(\"chrome\", { executablePath: \"/usr/bin/google-chrome\" })`. */\nexport function local(engine: BrowserName, options: LocalOptions = {}): LocalBrowser {\n if (!isBrowserName(engine)) {\n throw new Error(`local(): unknown engine \"${String(engine)}\"`);\n }\n if (!SUPPORTED.includes(engine)) {\n throw new Error(\n `local(\"${engine}\"): the Puppeteer runner drives Chromium-family engines only (${SUPPORTED.join(\", \")})`,\n );\n }\n return { kind: \"local\", engine, options };\n}\n\n/** The `browsers` list, flattened: what the runner offers and how to start each target. */\nexport interface FlatTargets {\n /** Every target as the runner declares it, default first (all `ready` until verified). */\n declared: RunnerTarget[];\n locals: LocalTarget[];\n remotes: RemoteTarget[];\n}\n\n/**\n * Flatten the `browsers` list into targets, first = default. Throws on an empty list, an engine\n * Puppeteer cannot drive, bare `chrome` (it needs its own `executablePath`, since the runner's\n * would launch whatever it points at under Chrome's name), or two entries producing one target,\n * so a mistake fails when the runner is created and not when the first build runs.\n */\nexport function flattenBrowsers(\n entries: readonly BrowserEntry[],\n runnerExecutablePath?: string,\n): FlatTargets {\n if (entries.length === 0) {\n throw new Error(\"browsers must list at least one target\");\n }\n const seen = new Map<string, number>();\n const flat: FlatTargets = { declared: [], locals: [], remotes: [] };\n for (const [index, entry] of entries.entries()) {\n const produced = isRemoteBrowser(entry)\n ? remoteTargetsOf(entry)\n : [localOf(entry, runnerExecutablePath)];\n for (const target of produced) {\n claim(seen, target.target, index);\n add(flat, target);\n }\n }\n return flat;\n}\n\n/** Record that entry `index` produces `target`, refusing a second entry that does. */\nfunction claim(seen: Map<string, number>, target: string, index: number): void {\n const first = seen.get(target);\n if (first !== undefined) {\n throw new Error(\n `browsers entries ${first + 1} and ${index + 1} both produce the target \"${target}\"`,\n );\n }\n seen.set(target, index);\n}\n\nfunction localOf(entry: BrowserName | LocalBrowser, runnerExecutablePath?: string): LocalTarget {\n const { engine, options } = typeof entry === \"string\" ? local(entry) : entry;\n return { target: engine, executablePath: pathOf(engine, options, runnerExecutablePath) };\n}\n\nfunction add(flat: FlatTargets, target: LocalTarget | RemoteTarget): void {\n if (\"kind\" in target) {\n flat.remotes.push(target);\n flat.declared.push({ target: target.target, reach: target.reach, status: \"ready\" });\n } else {\n flat.locals.push(target);\n flat.declared.push({ target: target.target, reach: \"loopback\", status: \"ready\" });\n }\n}\n\nfunction pathOf(\n engine: BrowserName,\n options: LocalOptions,\n runnerExecutablePath?: string,\n): string | undefined {\n if (options.executablePath !== undefined) {\n return options.executablePath;\n }\n if (engine === \"chrome\") {\n throw new Error(\n `\"chrome\" needs an executablePath: use local(\"chrome\", { executablePath: \"/usr/bin/google-chrome\" })`,\n );\n }\n return runnerExecutablePath;\n}\n"],"mappings":";;;;;;;AAuCA,MAAM,YAAoC,CAAC,YAAY,QAAQ;;AAG/D,SAAgB,MAAM,QAAqB,UAAwB,CAAC,GAAiB;CACnF,IAAI,CAAC,cAAc,MAAM,GACvB,MAAM,IAAI,MAAM,4BAA4B,OAAO,MAAM,EAAE,EAAE;CAE/D,IAAI,CAAC,UAAU,SAAS,MAAM,GAC5B,MAAM,IAAI,MACR,UAAU,OAAO,gEAAgE,UAAU,KAAK,IAAI,EAAE,EACxG;CAEF,OAAO;EAAE,MAAM;EAAS;EAAQ;CAAQ;AAC1C;;;;;;;AAgBA,SAAgB,gBACd,SACA,sBACa;CACb,IAAI,QAAQ,WAAW,GACrB,MAAM,IAAI,MAAM,wCAAwC;CAE1D,MAAM,uBAAO,IAAI,IAAoB;CACrC,MAAM,OAAoB;EAAE,UAAU,CAAC;EAAG,QAAQ,CAAC;EAAG,SAAS,CAAC;CAAE;CAClE,KAAK,MAAM,CAAC,OAAO,UAAU,QAAQ,QAAQ,GAAG;EAC9C,MAAM,WAAW,gBAAgB,KAAK,IAClC,gBAAgB,KAAK,IACrB,CAAC,QAAQ,OAAO,oBAAoB,CAAC;EACzC,KAAK,MAAM,UAAU,UAAU;GAC7B,MAAM,MAAM,OAAO,QAAQ,KAAK;GAChC,IAAI,MAAM,MAAM;EAClB;CACF;CACA,OAAO;AACT;;AAGA,SAAS,MAAM,MAA2B,QAAgB,OAAqB;CAC7E,MAAM,QAAQ,KAAK,IAAI,MAAM;CAC7B,IAAI,UAAU,KAAA,GACZ,MAAM,IAAI,MACR,oBAAoB,QAAQ,EAAE,OAAO,QAAQ,EAAE,4BAA4B,OAAO,EACpF;CAEF,KAAK,IAAI,QAAQ,KAAK;AACxB;AAEA,SAAS,QAAQ,OAAmC,sBAA4C;CAC9F,MAAM,EAAE,QAAQ,YAAY,OAAO,UAAU,WAAW,MAAM,KAAK,IAAI;CACvE,OAAO;EAAE,QAAQ;EAAQ,gBAAgB,OAAO,QAAQ,SAAS,oBAAoB;CAAE;AACzF;AAEA,SAAS,IAAI,MAAmB,QAA0C;CACxE,IAAI,UAAU,QAAQ;EACpB,KAAK,QAAQ,KAAK,MAAM;EACxB,KAAK,SAAS,KAAK;GAAE,QAAQ,OAAO;GAAQ,OAAO,OAAO;GAAO,QAAQ;EAAQ,CAAC;CACpF,OAAO;EACL,KAAK,OAAO,KAAK,MAAM;EACvB,KAAK,SAAS,KAAK;GAAE,QAAQ,OAAO;GAAQ,OAAO;GAAY,QAAQ;EAAQ,CAAC;CAClF;AACF;AAEA,SAAS,OACP,QACA,SACA,sBACoB;CACpB,IAAI,QAAQ,mBAAmB,KAAA,GAC7B,OAAO,QAAQ;CAEjB,IAAI,WAAW,UACb,MAAM,IAAI,MACR,qGACF;CAEF,OAAO;AACT"}
package/dist/tests.mjs ADDED
@@ -0,0 +1,119 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { abortIfCancelled, finishing, messageOf, outcomeOf, safeCloseServer } from "./browser.mjs";
5
+ import { waitForSettled } from "./settle.mjs";
6
+ import { acquirePage } from "./pages.mjs";
7
+ import { createBrowserSession } from "./session.mjs";
8
+ import { assertReachable, launchContextOf, planRun, serve } from "./pipeline.mjs";
9
+ import { engineOf } from "@storyshelf/core/adapter/capture-runner";
10
+ import { createPageLimiter, isTimeoutError } from "@storyshelf/core/utils";
11
+ import { StorybookAdapter } from "@storyshelf/core/capture";
12
+ // src/tests.ts
13
+ /** Storybook 10 `.test()` entries run as interaction tests in the iframe (ADR 0004, decision 10). */
14
+ /** How long one test may take (render plus body) when the project sets nothing. */
15
+ const DEFAULT_TEST_TIMEOUT_MS = 1e4;
16
+ /** A viewport for tests: they take no screenshot, so one size serves every entry. */
17
+ const TEST_VIEWPORT = {
18
+ name: "test",
19
+ width: 1280,
20
+ height: 720
21
+ };
22
+ /** Run every test entry once and report pass or fail for each; a failing test never throws. */
23
+ async function runTestEntries(rawInput, active, driver, defaultTarget) {
24
+ if (rawInput.tests.length === 0) return [];
25
+ const { input, name, remote } = planRun(rawInput, driver, defaultTarget);
26
+ assertReachable(name, remote, input.storybookUrl);
27
+ const session = createBrowserSession(await driver.launch(name, input.logger, launchContextOf(input, "tests", active.abort?.signal)));
28
+ active.session = session;
29
+ await abortIfCancelled(active, session);
30
+ let server;
31
+ return await finishing(async () => {
32
+ server = await serve(input, session);
33
+ return await runAll(session, server.url, input, active);
34
+ }, async (failed, error) => {
35
+ if (active.session === session) active.session = null;
36
+ await Promise.all([session.release(outcomeOf(active, failed), error), closeServer(server)]);
37
+ });
38
+ }
39
+ async function closeServer(server) {
40
+ if (server !== void 0) await safeCloseServer(server);
41
+ }
42
+ async function runAll(session, baseUrl, input, active) {
43
+ const outcomes = /* @__PURE__ */ new Map();
44
+ const pages = createPageLimiter({
45
+ concurrency: input.concurrency,
46
+ ...input.gate === void 0 ? {} : { gate: input.gate },
47
+ browser: engineOf(input.target),
48
+ logger: input.logger
49
+ });
50
+ try {
51
+ await pages.each(input.tests, async (test) => {
52
+ if (active.cancelled) throw new Error("Capture cancelled");
53
+ const outcome = await runTestEntry(session, baseUrl, test, input);
54
+ outcomes.set(test.id, outcome);
55
+ return outcome.timedOut === true;
56
+ });
57
+ } finally {
58
+ pages.dispose();
59
+ }
60
+ return input.tests.flatMap((test) => outcomes.get(test.id) ?? []);
61
+ }
62
+ /** One test: open its iframe URL, let Storybook render and run it, then read the verdict. */
63
+ async function runTestEntry(session, baseUrl, test, input) {
64
+ const started = performance.now();
65
+ const lease = await acquirePage({ session }, TEST_VIEWPORT);
66
+ let reusable = false;
67
+ try {
68
+ const report = await openAndWait(lease.page, baseUrl, test, input);
69
+ reusable = true;
70
+ const timeoutMs = input.playTimeoutMs ?? 1e4;
71
+ return verdictOf(test.id, report, timeoutMs, elapsedSince(started));
72
+ } catch (error) {
73
+ return crashed(test.id, error, elapsedSince(started));
74
+ } finally {
75
+ await lease.release(reusable);
76
+ }
77
+ }
78
+ function elapsedSince(started) {
79
+ return Math.round(performance.now() - started);
80
+ }
81
+ /** The page or navigation failed before Storybook could say anything: the test failed. */
82
+ function crashed(storyId, error, durationMs) {
83
+ return {
84
+ storyId,
85
+ status: "failed",
86
+ error: messageOf(error),
87
+ durationMs,
88
+ timedOut: isTimeoutError(error)
89
+ };
90
+ }
91
+ async function openAndWait(page, baseUrl, test, input) {
92
+ if (input.navigationTimeoutMs) page.setDefaultTimeout(input.navigationTimeoutMs);
93
+ const adapter = input.adapter ?? new StorybookAdapter();
94
+ await page.goto(adapter.buildUrl(baseUrl, test.id), { waitUntil: "load" });
95
+ return await waitForSettled(page, input.playTimeoutMs ?? 1e4, true);
96
+ }
97
+ /** Read Storybook's own account of the run: a thrown play body or render error fails the test. */
98
+ function verdictOf(storyId, report, timeoutMs, durationMs) {
99
+ const failed = (error, timedOut = false) => ({
100
+ storyId,
101
+ status: "failed",
102
+ error,
103
+ durationMs,
104
+ ...timedOut ? { timedOut } : {}
105
+ });
106
+ if (!report.hooked) return failed("Storybook's preview did not start (no channel to listen to)");
107
+ if (report.renderError !== void 0) return failed(`render failed: ${report.renderError}`);
108
+ if (report.playError !== void 0) return failed(report.playError);
109
+ if (report.unhandled?.[0] !== void 0) return failed(report.unhandled[0]);
110
+ if (!report.settled) return failed(`test did not finish within ${timeoutMs}ms (phase: ${report.phase ?? "unknown"})`, true);
111
+ return {
112
+ storyId,
113
+ status: "passed",
114
+ durationMs
115
+ };
116
+ }
117
+ export { DEFAULT_TEST_TIMEOUT_MS, runTestEntries, verdictOf };
118
+
119
+ //# sourceMappingURL=tests.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tests.mjs","names":[],"sources":["../src/tests.ts"],"sourcesContent":["/** Storybook 10 `.test()` entries run as interaction tests in the iframe (ADR 0004, decision 10). */\n\nimport {\n engineOf,\n type CaptureTarget,\n type StoryEntry,\n type TestOutcome,\n} from \"@storyshelf/core/adapter/capture-runner\";\nimport { StorybookAdapter } from \"@storyshelf/core/capture\";\nimport { createPageLimiter, isTimeoutError, type SessionGate } from \"@storyshelf/core/utils\";\nimport type { Page } from \"puppeteer-core\";\nimport { abortIfCancelled, finishing, messageOf, outcomeOf, safeCloseServer } from \"./browser.ts\";\nimport { acquirePage } from \"./pages.ts\";\nimport { assertReachable, launchContextOf, planRun, serve } from \"./pipeline.ts\";\nimport { createBrowserSession, type BrowserSession } from \"./session.ts\";\nimport { waitForSettled, type SettleReport } from \"./settle.ts\";\nimport type { ActiveRun, PuppeteerTestsInput, TargetDriver } from \"./types.ts\";\n\n/** How long one test may take (render plus body) when the project sets nothing. */\nexport const DEFAULT_TEST_TIMEOUT_MS = 10_000;\n\n/** A viewport for tests: they take no screenshot, so one size serves every entry. */\nconst TEST_VIEWPORT = { name: \"test\", width: 1280, height: 720 };\n\n/** Run every test entry once and report pass or fail for each; a failing test never throws. */\nexport async function runTestEntries(\n rawInput: PuppeteerTestsInput,\n active: ActiveRun,\n driver: TargetDriver,\n defaultTarget: CaptureTarget,\n): Promise<TestOutcome[]> {\n if (rawInput.tests.length === 0) return [];\n const { input, name, remote } = planRun(rawInput, driver, defaultTarget);\n assertReachable(name, remote, input.storybookUrl);\n const session = createBrowserSession(\n await driver.launch(name, input.logger, launchContextOf(input, \"tests\", active.abort?.signal)),\n );\n active.session = session;\n await abortIfCancelled(active, session);\n let server: { url: string; close(): Promise<void> } | undefined;\n return await finishing(\n async () => {\n server = await serve(input, session);\n return await runAll(session, server.url, input, active);\n },\n async (failed, error) => {\n if (active.session === session) active.session = null;\n await Promise.all([session.release(outcomeOf(active, failed), error), closeServer(server)]);\n },\n );\n}\n\nasync function closeServer(server: { close(): Promise<void> } | undefined): Promise<void> {\n if (server !== undefined) await safeCloseServer(server);\n}\n\nasync function runAll(\n session: BrowserSession,\n baseUrl: string,\n input: PuppeteerTestsInput & { target: CaptureTarget; gate?: SessionGate | undefined },\n active: ActiveRun,\n): Promise<TestOutcome[]> {\n const outcomes = new Map<string, TestOutcome>();\n const pages = createPageLimiter({\n concurrency: input.concurrency,\n ...(input.gate === undefined ? {} : { gate: input.gate }),\n browser: engineOf(input.target),\n logger: input.logger,\n });\n try {\n await pages.each(input.tests, async (test) => {\n if (active.cancelled) throw new Error(\"Capture cancelled\");\n const outcome = await runTestEntry(session, baseUrl, test, input);\n outcomes.set(test.id, outcome);\n return outcome.timedOut === true;\n });\n } finally {\n pages.dispose();\n }\n return input.tests.flatMap((test) => outcomes.get(test.id) ?? []);\n}\n\n/** One test: open its iframe URL, let Storybook render and run it, then read the verdict. */\nasync function runTestEntry(\n session: BrowserSession,\n baseUrl: string,\n test: StoryEntry,\n input: PuppeteerTestsInput,\n): Promise<TestOutcome & { timedOut?: boolean }> {\n const started = performance.now();\n const lease = await acquirePage({ session }, TEST_VIEWPORT);\n let reusable = false;\n try {\n const report = await openAndWait(lease.page, baseUrl, test, input);\n reusable = true;\n const timeoutMs = input.playTimeoutMs ?? DEFAULT_TEST_TIMEOUT_MS;\n return verdictOf(test.id, report, timeoutMs, elapsedSince(started));\n } catch (error) {\n return crashed(test.id, error, elapsedSince(started));\n } finally {\n await lease.release(reusable);\n }\n}\n\nfunction elapsedSince(started: number): number {\n return Math.round(performance.now() - started);\n}\n\n/** The page or navigation failed before Storybook could say anything: the test failed. */\nfunction crashed(\n storyId: string,\n error: unknown,\n durationMs: number,\n): TestOutcome & { timedOut?: boolean } {\n return {\n storyId,\n status: \"failed\",\n error: messageOf(error),\n durationMs,\n timedOut: isTimeoutError(error),\n };\n}\n\nasync function openAndWait(\n page: Page,\n baseUrl: string,\n test: StoryEntry,\n input: PuppeteerTestsInput,\n): Promise<SettleReport> {\n if (input.navigationTimeoutMs) page.setDefaultTimeout(input.navigationTimeoutMs);\n const adapter = input.adapter ?? new StorybookAdapter();\n await page.goto(adapter.buildUrl(baseUrl, test.id), { waitUntil: \"load\" });\n return await waitForSettled(page, input.playTimeoutMs ?? DEFAULT_TEST_TIMEOUT_MS, true);\n}\n\n/** Read Storybook's own account of the run: a thrown play body or render error fails the test. */\nexport function verdictOf(\n storyId: string,\n report: SettleReport,\n timeoutMs: number,\n durationMs: number,\n): TestOutcome & { timedOut?: boolean } {\n const failed = (error: string, timedOut = false): TestOutcome & { timedOut?: boolean } => ({\n storyId,\n status: \"failed\",\n error,\n durationMs,\n ...(timedOut ? { timedOut } : {}),\n });\n if (!report.hooked) return failed(\"Storybook's preview did not start (no channel to listen to)\");\n if (report.renderError !== undefined) return failed(`render failed: ${report.renderError}`);\n if (report.playError !== undefined) return failed(report.playError);\n if (report.unhandled?.[0] !== undefined) return failed(report.unhandled[0]);\n if (!report.settled) {\n return failed(\n `test did not finish within ${timeoutMs}ms (phase: ${report.phase ?? \"unknown\"})`,\n true,\n );\n }\n return { storyId, status: \"passed\", durationMs };\n}\n"],"mappings":"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"}
package/dist/types.d.mts CHANGED
@@ -1,6 +1,8 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
+ import "./session.mjs";
4
+ import "./pages.mjs";
5
+ import "@storyshelf/core/adapter/capture-runner";
6
+ import "@storyshelf/core/utils";
3
7
  import "@storyshelf/core/capture";
4
- import "puppeteer-core";
5
- import { BrowserName } from "@storyshelf/core/adapter/capture-runner";
6
8
  import "@storyshelf/core/logger";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storyshelf/runner-puppeteer",
3
- "version": "0.6.2",
3
+ "version": "0.8.0",
4
4
  "description": "Puppeteer CaptureRunner for StoryShelf (Chromium via chrome-headless-shell, smaller image).",
5
5
  "keywords": [
6
6
  "puppeteer",
@@ -27,9 +27,6 @@
27
27
  "dist"
28
28
  ],
29
29
  "type": "module",
30
- "engines": {
31
- "node": ">=24"
32
- },
33
30
  "sideEffects": false,
34
31
  "types": "./dist/index.d.mts",
35
32
  "exports": {
@@ -37,11 +34,16 @@
37
34
  "source": "./src/index.ts",
38
35
  "default": "./dist/index.mjs"
39
36
  },
37
+ "./remote": {
38
+ "source": "./src/remote/index.ts",
39
+ "default": "./dist/remote/index.mjs"
40
+ },
40
41
  "./package.json": "./package.json"
41
42
  },
42
43
  "publishConfig": {
43
44
  "exports": {
44
45
  ".": "./dist/index.mjs",
46
+ "./remote": "./dist/remote/index.mjs",
45
47
  "./package.json": "./package.json"
46
48
  },
47
49
  "access": "public"
@@ -52,25 +54,30 @@
52
54
  "fmt": "oxfmt ./src",
53
55
  "lint": "oxlint --type-aware --type-check ./src",
54
56
  "test": "vitest run",
55
- "prepublishOnly": "nub run build"
57
+ "test:integration": "RUN_INTEGRATION=1 vitest run a11y.integration.test.ts settle.integration.test.ts remote.integration.test.ts ignore-regions.integration.test.ts",
58
+ "prepublishOnly": "nub run build",
59
+ "test:flake": "vitest run --repeats=20 --maxWorkers=1 --no-file-parallelism src/capture-runner.test.ts src/settle.test.ts src/screenshot.test.ts"
56
60
  },
57
61
  "dependencies": {
58
- "@storyshelf/core": "0.6.2",
62
+ "@storyshelf/core": "0.8.0",
59
63
  "puppeteer-core": "^24.0.0"
60
64
  },
61
65
  "devDependencies": {
62
- "@storyshelf/app": "0.6.2",
63
- "@storyshelf/db-sqlite": "0.6.2",
64
- "@storyshelf/storage-local": "0.6.2",
66
+ "@storyshelf/app": "0.8.0",
67
+ "@storyshelf/db-sqlite": "0.8.0",
68
+ "@storyshelf/storage-local": "0.8.0",
65
69
  "@types/node": "^26.0.0",
66
70
  "@vitest/coverage-v8": "^4.0.0",
67
- "adm-zip": "0.6.0",
71
+ "adm-zip": "0.6.1",
68
72
  "oxfmt": "^0.65.0",
69
73
  "oxlint": "^1.80.0",
70
74
  "oxlint-tsgolint": "^7.0.2001",
71
75
  "tsdown": "^0.22.14",
72
76
  "typescript": "^7.0.2",
73
- "vitest": "^5.0.0"
77
+ "vitest": "^5.0.5"
78
+ },
79
+ "engines": {
80
+ "node": ">=24"
74
81
  },
75
82
  "jsr": {
76
83
  "runtimeCompat": {