@storyshelf/runner-playwright 0.6.2 → 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 -31
- 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 +3 -1
- package/dist/version.mjs +16 -0
- package/dist/version.mjs.map +1 -0
- package/package.json +17 -11
package/dist/targets.mjs
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { isBrowserName, isRemoteBrowser, remoteTargetsOf } from "@storyshelf/core/adapter/capture-runner";
|
|
5
|
+
//#region src/targets.ts
|
|
6
|
+
/** The runner's `browsers` option: what an operator lists, and the local targets it flattens to. */
|
|
7
|
+
/** A local engine with options, for `browsers`: `local("chrome", { channel: "chrome-beta" })`. */
|
|
8
|
+
function local(engine, options = {}) {
|
|
9
|
+
if (!isBrowserName(engine)) throw new Error(`local(): unknown engine "${String(engine)}"`);
|
|
10
|
+
if (options.channel !== void 0 && engine !== "chromium" && engine !== "chrome") throw new Error(`local("${engine}"): a channel applies only to chromium and chrome`);
|
|
11
|
+
return {
|
|
12
|
+
kind: "local",
|
|
13
|
+
engine,
|
|
14
|
+
options
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Flatten the `browsers` list into targets, first = default. Throws on an empty list, an unknown
|
|
19
|
+
* engine, or two entries producing one target, naming both, so a mistake fails when the runner is
|
|
20
|
+
* created and not when the first build runs.
|
|
21
|
+
*/
|
|
22
|
+
function flattenBrowsers(entries) {
|
|
23
|
+
if (entries.length === 0) throw new Error("browsers must list at least one target");
|
|
24
|
+
const seen = /* @__PURE__ */ new Map();
|
|
25
|
+
const flat = {
|
|
26
|
+
declared: [],
|
|
27
|
+
locals: [],
|
|
28
|
+
remotes: []
|
|
29
|
+
};
|
|
30
|
+
for (const [index, entry] of entries.entries()) for (const target of isRemoteBrowser(entry) ? remoteTargetsOf(entry) : [localOf(entry)]) {
|
|
31
|
+
claim(seen, target.target, index);
|
|
32
|
+
add(flat, target);
|
|
33
|
+
}
|
|
34
|
+
return flat;
|
|
35
|
+
}
|
|
36
|
+
/** Record that entry `index` produces `target`, refusing a second entry that does. */
|
|
37
|
+
function claim(seen, target, index) {
|
|
38
|
+
const first = seen.get(target);
|
|
39
|
+
if (first !== void 0) throw new Error(`browsers entries ${first + 1} and ${index + 1} both produce the target "${target}"`);
|
|
40
|
+
seen.set(target, index);
|
|
41
|
+
}
|
|
42
|
+
function localOf(entry) {
|
|
43
|
+
const { engine, options } = typeof entry === "string" ? local(entry) : entry;
|
|
44
|
+
return {
|
|
45
|
+
target: engine,
|
|
46
|
+
channel: channelOf(engine, options)
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function add(flat, target) {
|
|
50
|
+
if ("kind" in target) {
|
|
51
|
+
flat.remotes.push(target);
|
|
52
|
+
flat.declared.push({
|
|
53
|
+
target: target.target,
|
|
54
|
+
reach: target.reach,
|
|
55
|
+
status: "ready"
|
|
56
|
+
});
|
|
57
|
+
} else {
|
|
58
|
+
flat.locals.push(target);
|
|
59
|
+
flat.declared.push({
|
|
60
|
+
target: target.target,
|
|
61
|
+
reach: "loopback",
|
|
62
|
+
status: "ready"
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Bare `chrome` means Chrome, not the bundled Chromium (ADR 0007, decision 7). */
|
|
67
|
+
function channelOf(engine, options) {
|
|
68
|
+
return options.channel ?? (engine === "chrome" ? "chrome" : void 0);
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
export { flattenBrowsers, local };
|
|
72
|
+
|
|
73
|
+
//# 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 /** Playwright channel (`chrome`, `chrome-beta`, `msedge`, …); only for `chromium` and `chrome`. */\n channel?: 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 service from\n * `@storyshelf/runner-playwright/remote` (`playwrightServer(...)`, `remote(...)`, ...), which\n * 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 Playwright channel to launch with, when one applies. */\n channel: string | undefined;\n}\n\n/** A local engine with options, for `browsers`: `local(\"chrome\", { channel: \"chrome-beta\" })`. */\nexport function local(engine: BrowserName, options: LocalOptions = {}): LocalBrowser {\n if (!isBrowserName(engine)) {\n throw new Error(`local(): unknown engine \"${String(engine)}\"`);\n }\n if (options.channel !== undefined && engine !== \"chromium\" && engine !== \"chrome\") {\n throw new Error(`local(\"${engine}\"): a channel applies only to chromium and chrome`);\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 unknown\n * engine, or two entries producing one target, naming both, so a mistake fails when the runner is\n * created and not when the first build runs.\n */\nexport function flattenBrowsers(entries: readonly BrowserEntry[]): 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 for (const target of isRemoteBrowser(entry) ? remoteTargetsOf(entry) : [localOf(entry)]) {\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): LocalTarget {\n const { engine, options } = typeof entry === \"string\" ? local(entry) : entry;\n return { target: engine, channel: channelOf(engine, options) };\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\n/** Bare `chrome` means Chrome, not the bundled Chromium (ADR 0007, decision 7). */\nfunction channelOf(engine: BrowserName, options: LocalOptions): string | undefined {\n return options.channel ?? (engine === \"chrome\" ? \"chrome\" : undefined);\n}\n"],"mappings":";;;;;;;AAwCA,SAAgB,MAAM,QAAqB,UAAwB,CAAC,GAAiB;CACnF,IAAI,CAAC,cAAc,MAAM,GACvB,MAAM,IAAI,MAAM,4BAA4B,OAAO,MAAM,EAAE,EAAE;CAE/D,IAAI,QAAQ,YAAY,KAAA,KAAa,WAAW,cAAc,WAAW,UACvE,MAAM,IAAI,MAAM,UAAU,OAAO,kDAAkD;CAErF,OAAO;EAAE,MAAM;EAAS;EAAQ;CAAQ;AAC1C;;;;;;AAeA,SAAgB,gBAAgB,SAA+C;CAC7E,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,GAC3C,KAAK,MAAM,UAAU,gBAAgB,KAAK,IAAI,gBAAgB,KAAK,IAAI,CAAC,QAAQ,KAAK,CAAC,GAAG;EACvF,MAAM,MAAM,OAAO,QAAQ,KAAK;EAChC,IAAI,MAAM,MAAM;CAClB;CAEF,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,OAAgD;CAC/D,MAAM,EAAE,QAAQ,YAAY,OAAO,UAAU,WAAW,MAAM,KAAK,IAAI;CACvE,OAAO;EAAE,QAAQ;EAAQ,SAAS,UAAU,QAAQ,OAAO;CAAE;AAC/D;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;;AAGA,SAAS,UAAU,QAAqB,SAA2C;CACjF,OAAO,QAAQ,YAAY,WAAW,WAAW,WAAW,KAAA;AAC9D"}
|
package/dist/tests.mjs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { messageOf, safeCloseBrowser, safeCloseServer } from "./browser.mjs";
|
|
5
|
+
import { waitForSettled } from "./settle.mjs";
|
|
6
|
+
import { acquirePage } from "./pages.mjs";
|
|
7
|
+
import { assertReachable, planRun, serve } from "./pipeline.mjs";
|
|
8
|
+
import { engineOf } from "@storyshelf/core/adapter/capture-runner";
|
|
9
|
+
import { createPageLimiter, isTimeoutError } from "@storyshelf/core/utils";
|
|
10
|
+
import { StorybookAdapter } from "@storyshelf/core/capture";
|
|
11
|
+
//#region src/tests.ts
|
|
12
|
+
/** Storybook 10 `.test()` entries run as interaction tests in the iframe (ADR 0004, decision 10). */
|
|
13
|
+
/** How long one test may take (render plus body) when the project sets nothing. */
|
|
14
|
+
const DEFAULT_TEST_TIMEOUT_MS = 1e4;
|
|
15
|
+
/** A viewport for tests: they take no screenshot, so one size serves every entry. */
|
|
16
|
+
const TEST_VIEWPORT = {
|
|
17
|
+
name: "test",
|
|
18
|
+
width: 1280,
|
|
19
|
+
height: 720
|
|
20
|
+
};
|
|
21
|
+
/** Run every test entry once and report pass or fail for each; a failing test never throws. */
|
|
22
|
+
async function runTestEntries(rawInput, active, driver, defaultTarget) {
|
|
23
|
+
if (rawInput.tests.length === 0) return [];
|
|
24
|
+
const { input, name, remote } = planRun(rawInput, driver, defaultTarget);
|
|
25
|
+
assertReachable(name, remote, input.storybookUrl);
|
|
26
|
+
const browser = await driver.launch(name, input.logger);
|
|
27
|
+
const server = await serve(input, browser);
|
|
28
|
+
active.browser = browser;
|
|
29
|
+
try {
|
|
30
|
+
return await runAll(browser, server.url, input, active);
|
|
31
|
+
} finally {
|
|
32
|
+
active.browser = null;
|
|
33
|
+
await Promise.all([safeCloseBrowser(browser), safeCloseServer(server)]);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function runAll(browser, baseUrl, input, active) {
|
|
37
|
+
const outcomes = /* @__PURE__ */ new Map();
|
|
38
|
+
const pages = createPageLimiter({
|
|
39
|
+
concurrency: input.concurrency,
|
|
40
|
+
...input.gate === void 0 ? {} : { gate: input.gate },
|
|
41
|
+
browser: engineOf(input.target),
|
|
42
|
+
logger: input.logger
|
|
43
|
+
});
|
|
44
|
+
try {
|
|
45
|
+
await pages.each(input.tests, async (test) => {
|
|
46
|
+
if (active.cancelled) throw new Error("Capture cancelled");
|
|
47
|
+
const outcome = await runTestEntry(browser, baseUrl, test, input);
|
|
48
|
+
outcomes.set(test.id, outcome);
|
|
49
|
+
return outcome.timedOut === true;
|
|
50
|
+
});
|
|
51
|
+
} finally {
|
|
52
|
+
pages.dispose();
|
|
53
|
+
}
|
|
54
|
+
return input.tests.flatMap((test) => outcomes.get(test.id) ?? []);
|
|
55
|
+
}
|
|
56
|
+
/** One test: open its iframe URL, let Storybook render and run it, then read the verdict. */
|
|
57
|
+
async function runTestEntry(browser, baseUrl, test, input) {
|
|
58
|
+
const started = performance.now();
|
|
59
|
+
const lease = await acquirePage({ browser }, TEST_VIEWPORT);
|
|
60
|
+
let reusable = false;
|
|
61
|
+
try {
|
|
62
|
+
const report = await openAndWait(lease.page, baseUrl, test, input);
|
|
63
|
+
reusable = true;
|
|
64
|
+
const timeoutMs = input.playTimeoutMs ?? 1e4;
|
|
65
|
+
return verdictOf(test.id, report, timeoutMs, elapsedSince(started));
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return crashed(test.id, error, elapsedSince(started));
|
|
68
|
+
} finally {
|
|
69
|
+
await lease.release(reusable);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function elapsedSince(started) {
|
|
73
|
+
return Math.round(performance.now() - started);
|
|
74
|
+
}
|
|
75
|
+
/** The page or navigation failed before Storybook could say anything: the test failed. */
|
|
76
|
+
function crashed(storyId, error, durationMs) {
|
|
77
|
+
return {
|
|
78
|
+
storyId,
|
|
79
|
+
status: "failed",
|
|
80
|
+
error: messageOf(error),
|
|
81
|
+
durationMs,
|
|
82
|
+
timedOut: isTimeoutError(error)
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
async function openAndWait(page, baseUrl, test, input) {
|
|
86
|
+
if (input.navigationTimeoutMs) page.setDefaultTimeout(input.navigationTimeoutMs);
|
|
87
|
+
const adapter = input.adapter ?? new StorybookAdapter();
|
|
88
|
+
await page.goto(adapter.buildUrl(baseUrl, test.id), { waitUntil: "load" });
|
|
89
|
+
return await waitForSettled(page, input.playTimeoutMs ?? 1e4);
|
|
90
|
+
}
|
|
91
|
+
/** Read Storybook's own account of the run: a thrown play body or render error fails the test. */
|
|
92
|
+
function verdictOf(storyId, report, timeoutMs, durationMs) {
|
|
93
|
+
const failed = (error, timedOut = false) => ({
|
|
94
|
+
storyId,
|
|
95
|
+
status: "failed",
|
|
96
|
+
error,
|
|
97
|
+
durationMs,
|
|
98
|
+
...timedOut ? { timedOut } : {}
|
|
99
|
+
});
|
|
100
|
+
if (!report.hooked) return failed("Storybook's preview did not start (no channel to listen to)");
|
|
101
|
+
if (report.renderError !== void 0) return failed(`render failed: ${report.renderError}`);
|
|
102
|
+
if (report.playError !== void 0) return failed(report.playError);
|
|
103
|
+
if (report.unhandled?.[0] !== void 0) return failed(report.unhandled[0]);
|
|
104
|
+
if (!report.settled) return failed(`test did not finish within ${timeoutMs}ms (phase: ${report.phase ?? "unknown"})`, true);
|
|
105
|
+
return {
|
|
106
|
+
storyId,
|
|
107
|
+
status: "passed",
|
|
108
|
+
durationMs
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
export { DEFAULT_TEST_TIMEOUT_MS, runTestEntries, verdictOf };
|
|
113
|
+
|
|
114
|
+
//# 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 { Browser, Page } from \"playwright-core\";\nimport { messageOf, safeCloseBrowser, safeCloseServer } from \"./browser.ts\";\nimport { acquirePage } from \"./pages.ts\";\nimport { assertReachable, planRun, serve } from \"./pipeline.ts\";\nimport { waitForSettled, type SettleReport } from \"./settle.ts\";\nimport type { ActiveRun, PlaywrightTestsInput, 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: PlaywrightTestsInput,\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 browser = await driver.launch(name, input.logger);\n const server = await serve(input, browser);\n active.browser = browser;\n try {\n return await runAll(browser, server.url, input, active);\n } finally {\n active.browser = null;\n await Promise.all([safeCloseBrowser(browser), safeCloseServer(server)]);\n }\n}\n\nasync function runAll(\n browser: Browser,\n baseUrl: string,\n input: PlaywrightTestsInput & { 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(browser, 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 browser: Browser,\n baseUrl: string,\n test: StoryEntry,\n input: PlaywrightTestsInput,\n): Promise<TestOutcome & { timedOut?: boolean }> {\n const started = performance.now();\n const lease = await acquirePage({ browser }, 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: PlaywrightTestsInput,\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);\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
|
|
3
|
+
import "./pages.mjs";
|
|
4
|
+
import "@storyshelf/core/adapter/capture-runner";
|
|
5
|
+
import "@storyshelf/core/utils";
|
|
4
6
|
import "@storyshelf/core/capture";
|
|
5
7
|
import "@storyshelf/core/logger";
|
|
6
8
|
import "playwright-core";
|
package/dist/version.mjs
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
//#region src/version.ts
|
|
6
|
+
/** The `playwright-core` this runner drives browsers with. */
|
|
7
|
+
let cached;
|
|
8
|
+
/** Its version, for the connect-time mismatch error and for services that ask the client's version. */
|
|
9
|
+
function playwrightVersion() {
|
|
10
|
+
cached ??= createRequire(import.meta.url)("playwright-core/package.json").version;
|
|
11
|
+
return cached;
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { playwrightVersion };
|
|
15
|
+
|
|
16
|
+
//# sourceMappingURL=version.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.mjs","names":[],"sources":["../src/version.ts"],"sourcesContent":["/** The `playwright-core` this runner drives browsers with. */\nimport { createRequire } from \"node:module\";\n\nlet cached: string | undefined;\n\n/** Its version, for the connect-time mismatch error and for services that ask the client's version. */\nexport function playwrightVersion(): string {\n cached ??= (createRequire(import.meta.url)(\"playwright-core/package.json\") as { version: string })\n .version;\n return cached;\n}\n"],"mappings":";;;;;;AAGA,IAAI;;AAGJ,SAAgB,oBAA4B;CAC1C,WAAY,cAAc,YAAY,GAAG,CAAC,CAAC,8BAA8B,CAAC,CACvE;CACH,OAAO;AACT"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@storyshelf/runner-playwright",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Pure Playwright CaptureRunner implementation for StoryShelf: renders Storybook screenshots in-process (no db/storage coupling).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"playwright",
|
|
@@ -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,19 +54,20 @@
|
|
|
52
54
|
"fmt": "oxfmt ./src",
|
|
53
55
|
"lint": "oxlint --type-aware --type-check ./src",
|
|
54
56
|
"test": "vitest run",
|
|
55
|
-
"test:integration": "RUN_INTEGRATION=1 vitest run integration.test.ts passkey.integration.test.ts",
|
|
56
|
-
"prepublishOnly": "nub run build"
|
|
57
|
+
"test:integration": "RUN_INTEGRATION=1 vitest run integration.test.ts passkey.integration.test.ts remote.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"
|
|
57
60
|
},
|
|
58
61
|
"dependencies": {
|
|
59
|
-
"@storyshelf/core": "0.
|
|
62
|
+
"@storyshelf/core": "0.7.0",
|
|
60
63
|
"playwright-core": "1.63.0"
|
|
61
64
|
},
|
|
62
65
|
"devDependencies": {
|
|
63
66
|
"@hono/node-server": "^1.17.0",
|
|
64
|
-
"@storyshelf/app": "0.
|
|
65
|
-
"@storyshelf/auth": "0.
|
|
66
|
-
"@storyshelf/db-sqlite": "0.
|
|
67
|
-
"@storyshelf/storage-local": "0.
|
|
67
|
+
"@storyshelf/app": "0.7.0",
|
|
68
|
+
"@storyshelf/auth": "0.7.0",
|
|
69
|
+
"@storyshelf/db-sqlite": "0.7.0",
|
|
70
|
+
"@storyshelf/storage-local": "0.7.0",
|
|
68
71
|
"@types/node": "^26.0.0",
|
|
69
72
|
"@vitest/coverage-v8": "^4.0.0",
|
|
70
73
|
"adm-zip": "0.6.0",
|
|
@@ -75,6 +78,9 @@
|
|
|
75
78
|
"typescript": "^7.0.2",
|
|
76
79
|
"vitest": "^5.0.0"
|
|
77
80
|
},
|
|
81
|
+
"engines": {
|
|
82
|
+
"node": ">=24"
|
|
83
|
+
},
|
|
78
84
|
"jsr": {
|
|
79
85
|
"runtimeCompat": {
|
|
80
86
|
"node": true,
|