@itmetsam/samurai-framework 0.1.0 → 0.2.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 +129 -0
- package/bin/samurai.mjs +31 -0
- package/dist/api.js +3 -0
- package/dist/assert/assertion-error.d.ts +23 -0
- package/dist/assert/assertion-error.js +45 -0
- package/dist/assert/expect.d.ts +55 -0
- package/dist/assert/expect.js +188 -0
- package/dist/browser/browser-context.d.ts +91 -0
- package/dist/browser/browser-context.js +146 -0
- package/dist/browser/browser-finder.d.ts +2 -0
- package/dist/browser/browser-finder.js +95 -0
- package/dist/browser/browser.d.ts +95 -0
- package/dist/browser/browser.js +372 -0
- package/dist/browser/context-tree.d.ts +23 -0
- package/dist/browser/context-tree.js +60 -0
- package/dist/browser/dialog.d.ts +37 -0
- package/dist/browser/dialog.js +61 -0
- package/dist/browser/download.d.ts +71 -0
- package/dist/browser/download.js +137 -0
- package/dist/browser/downloads-dir.d.ts +8 -0
- package/dist/browser/downloads-dir.js +38 -0
- package/dist/browser/emulation.d.ts +61 -0
- package/dist/browser/emulation.js +144 -0
- package/dist/browser/file-chooser.d.ts +39 -0
- package/dist/browser/file-chooser.js +86 -0
- package/dist/browser/file-paths.d.ts +4 -0
- package/dist/browser/file-paths.js +18 -0
- package/dist/browser/index.d.ts +4 -0
- package/dist/browser/index.js +4 -0
- package/dist/browser/navigation-error.d.ts +13 -0
- package/dist/browser/navigation-error.js +19 -0
- package/dist/browser/page-logs.d.ts +86 -0
- package/dist/browser/page-logs.js +149 -0
- package/dist/browser/page-wait.d.ts +9 -0
- package/dist/browser/page-wait.js +17 -0
- package/dist/browser/page.d.ts +370 -0
- package/dist/browser/page.js +1038 -0
- package/dist/browser/pdf.d.ts +72 -0
- package/dist/browser/pdf.js +37 -0
- package/dist/browser/screenshot.d.ts +29 -0
- package/dist/browser/screenshot.js +42 -0
- package/dist/cli/args.d.ts +35 -0
- package/dist/cli/args.js +69 -0
- package/dist/cli/cli.cli-test.d.ts +1 -0
- package/dist/cli/cli.cli-test.js +170 -0
- package/dist/cli/console-reporter.d.ts +15 -0
- package/dist/cli/console-reporter.js +52 -0
- package/dist/cli/init.d.ts +7 -0
- package/dist/cli/init.js +61 -0
- package/dist/cli/main.d.ts +12 -0
- package/dist/cli/main.js +156 -0
- package/dist/config/config.d.ts +24 -0
- package/dist/config/config.js +42 -0
- package/dist/config/default.d.ts +2 -0
- package/dist/config/default.js +3 -0
- package/dist/config/mask.d.ts +11 -0
- package/dist/config/mask.js +85 -0
- package/dist/config/run-settings.d.ts +28 -0
- package/dist/config/run-settings.js +61 -0
- package/dist/config/secrets.d.ts +10 -0
- package/dist/config/secrets.js +67 -0
- package/dist/config/variables.d.ts +11 -0
- package/dist/config/variables.js +26 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +15 -0
- package/dist/locator/action-timeout-error.d.ts +25 -0
- package/dist/locator/action-timeout-error.js +35 -0
- package/dist/locator/element-state.d.ts +44 -0
- package/dist/locator/element-state.js +109 -0
- package/dist/locator/label-locator.d.ts +13 -0
- package/dist/locator/label-locator.js +49 -0
- package/dist/locator/locator.d.ts +168 -0
- package/dist/locator/locator.js +591 -0
- package/dist/locator/selector-errors.d.ts +9 -0
- package/dist/locator/selector-errors.js +15 -0
- package/dist/locator/selector.d.ts +60 -0
- package/dist/locator/selector.js +113 -0
- package/dist/locator/text-locator.d.ts +13 -0
- package/dist/logger/index.d.ts +2 -0
- package/dist/logger/index.js +30 -0
- package/dist/network/data-collector.d.ts +26 -0
- package/dist/network/data-collector.js +77 -0
- package/dist/network/network-tracker.d.ts +94 -0
- package/dist/network/network-tracker.js +198 -0
- package/dist/network/network-wait.d.ts +16 -0
- package/dist/network/network-wait.js +24 -0
- package/dist/network/response.d.ts +22 -0
- package/dist/network/response.js +42 -0
- package/dist/network/router.d.ts +120 -0
- package/dist/network/router.js +343 -0
- package/dist/network/url-match.d.ts +8 -0
- package/dist/network/url-match.js +38 -0
- package/dist/recorder/apply.d.ts +3 -0
- package/dist/recorder/apply.js +10 -0
- package/dist/recorder/capture.d.ts +18 -0
- package/dist/recorder/capture.js +198 -0
- package/dist/recorder/index.d.ts +3 -0
- package/dist/recorder/index.js +3 -0
- package/dist/recorder/normalise.d.ts +68 -0
- package/dist/recorder/normalise.js +104 -0
- package/dist/recorder/ranking.d.ts +9 -0
- package/dist/recorder/ranking.js +21 -0
- package/dist/recorder/recorder.d.ts +52 -0
- package/dist/recorder/recorder.js +143 -0
- package/dist/recorder/session.d.ts +49 -0
- package/dist/recorder/session.js +113 -0
- package/dist/runner/module-type.d.ts +6 -0
- package/dist/runner/module-type.js +29 -0
- package/dist/runner/page-logs-report.d.ts +40 -0
- package/dist/runner/page-logs-report.js +44 -0
- package/dist/runner/prepare-run.d.ts +14 -0
- package/dist/runner/prepare-run.js +20 -0
- package/dist/runner/project.d.ts +17 -0
- package/dist/runner/project.js +33 -0
- package/dist/runner/registry.d.ts +7 -0
- package/dist/runner/registry.js +40 -0
- package/dist/runner/reporter.d.ts +43 -0
- package/dist/runner/reporter.js +83 -0
- package/dist/runner/run.d.ts +27 -0
- package/dist/runner/run.js +34 -0
- package/dist/runner/test-error.d.ts +3 -0
- package/dist/runner/test-error.js +18 -0
- package/dist/runner/test-runner.d.ts +44 -0
- package/dist/runner/test-runner.js +205 -0
- package/dist/script/call-function.d.ts +34 -0
- package/dist/script/call-function.js +71 -0
- package/dist/script/element-handle.d.ts +10 -0
- package/{src/script/element-handle.ts → dist/script/element-handle.js} +4 -4
- package/dist/script/helpers.d.ts +28 -0
- package/dist/script/helpers.js +50 -0
- package/dist/script/preload.d.ts +31 -0
- package/dist/script/preload.js +71 -0
- package/dist/script/serialize.d.ts +29 -0
- package/dist/script/serialize.js +207 -0
- package/dist/steps/build.d.ts +5 -0
- package/dist/steps/build.js +38 -0
- package/dist/steps/edit.d.ts +30 -0
- package/dist/steps/edit.js +80 -0
- package/dist/steps/emit.d.ts +6 -0
- package/dist/steps/emit.js +73 -0
- package/dist/steps/index.d.ts +5 -0
- package/dist/steps/index.js +4 -0
- package/dist/steps/model.d.ts +103 -0
- package/dist/steps/model.js +1 -0
- package/dist/steps/parse.d.ts +3 -0
- package/dist/steps/parse.js +272 -0
- package/dist/transport/bidi-connection.d.ts +46 -0
- package/dist/transport/bidi-connection.js +176 -0
- package/dist/transport/bidi-error.d.ts +10 -0
- package/dist/transport/bidi-error.js +10 -0
- package/dist/transport/subscriptions.d.ts +36 -0
- package/dist/transport/subscriptions.js +81 -0
- package/{src → dist}/types/test.d.ts +10 -4
- package/dist/wait/wait-until.d.ts +20 -0
- package/dist/wait/wait-until.js +63 -0
- package/package.json +44 -7
- package/.claude/settings.local.json +0 -7
- package/.env.example +0 -3
- package/.github/actions/setup/action.yml +0 -16
- package/.github/workflows/ci.yml +0 -71
- package/.trunk/configs/.markdownlint.yaml +0 -2
- package/.trunk/configs/.yamllint.yaml +0 -7
- package/.trunk/configs/svgo.config.mjs +0 -14
- package/.trunk/trunk.yaml +0 -34
- package/docs/superpowers/bidi-roadmap.md +0 -60
- package/docs/superpowers/plans/2026-09-29-auto-wait.md +0 -1827
- package/docs/superpowers/plans/2026-09-29-expect.md +0 -1350
- package/docs/superpowers/plans/2026-10-01-ci.md +0 -350
- package/docs/superpowers/plans/2026-10-01-test-api-alignment.md +0 -1821
- package/docs/superpowers/recorder-roadmap.md +0 -52
- package/docs/superpowers/specs/2026-09-29-auto-wait-design.md +0 -185
- package/docs/superpowers/specs/2026-09-29-bidi-foundation-design.md +0 -130
- package/docs/superpowers/specs/2026-09-29-contexts-and-storage-design.md +0 -88
- package/docs/superpowers/specs/2026-09-29-downloads-and-file-dialogs-design.md +0 -94
- package/docs/superpowers/specs/2026-09-29-emulation-design.md +0 -88
- package/docs/superpowers/specs/2026-09-29-expect-design.md +0 -130
- package/docs/superpowers/specs/2026-09-29-locate-nodes-design.md +0 -90
- package/docs/superpowers/specs/2026-09-29-logs-and-errors-design.md +0 -87
- package/docs/superpowers/specs/2026-09-29-navigation-waits-design.md +0 -83
- package/docs/superpowers/specs/2026-09-29-network-data-design.md +0 -78
- package/docs/superpowers/specs/2026-09-29-network-interception-design.md +0 -91
- package/docs/superpowers/specs/2026-09-29-network-tracking-design.md +0 -86
- package/docs/superpowers/specs/2026-09-29-page-api-design.md +0 -90
- package/docs/superpowers/specs/2026-09-29-preload-scripts-design.md +0 -82
- package/docs/superpowers/specs/2026-09-29-script-call-function-design.md +0 -107
- package/docs/superpowers/specs/2026-09-29-set-files-design.md +0 -62
- package/docs/superpowers/specs/2026-09-29-user-prompts-design.md +0 -75
- package/docs/superpowers/specs/2026-10-01-ci-design.md +0 -106
- package/docs/superpowers/specs/2026-10-01-test-api-alignment-design.md +0 -241
- package/public/logo-dark.svg +0 -236
- package/public/logo-light.svg +0 -262
- package/samurai.config.ts +0 -7
- package/src/assert/assertion-error.test.ts +0 -55
- package/src/assert/assertion-error.ts +0 -62
- package/src/assert/expect-locator.test.ts +0 -217
- package/src/assert/expect.test.ts +0 -73
- package/src/assert/expect.ts +0 -299
- package/src/browser/browser-context.browser-test.ts +0 -201
- package/src/browser/browser-context.test.ts +0 -407
- package/src/browser/browser-context.ts +0 -208
- package/src/browser/browser-finder.ts +0 -110
- package/src/browser/browser.test.ts +0 -88
- package/src/browser/browser.ts +0 -515
- package/src/browser/context-tree.test.ts +0 -86
- package/src/browser/context-tree.ts +0 -70
- package/src/browser/dialog.browser-test.ts +0 -144
- package/src/browser/dialog.test.ts +0 -322
- package/src/browser/dialog.ts +0 -83
- package/src/browser/download.test.ts +0 -161
- package/src/browser/download.ts +0 -172
- package/src/browser/downloads-dir.test.ts +0 -75
- package/src/browser/downloads-dir.ts +0 -39
- package/src/browser/emulation.browser-test.ts +0 -186
- package/src/browser/emulation.test.ts +0 -200
- package/src/browser/emulation.ts +0 -204
- package/src/browser/file-chooser.test.ts +0 -142
- package/src/browser/file-chooser.ts +0 -99
- package/src/browser/file-paths.ts +0 -18
- package/src/browser/goto.browser-test.ts +0 -44
- package/src/browser/navigation-error.ts +0 -26
- package/src/browser/navigation.browser-test.ts +0 -172
- package/src/browser/page-api.browser-test.ts +0 -228
- package/src/browser/page-logs.browser-test.ts +0 -119
- package/src/browser/page-logs.test.ts +0 -212
- package/src/browser/page-logs.ts +0 -208
- package/src/browser/page-wait.ts +0 -19
- package/src/browser/page.test.ts +0 -1567
- package/src/browser/page.ts +0 -1176
- package/src/browser/pdf.ts +0 -62
- package/src/browser/screenshot.ts +0 -65
- package/src/browser/transfers.browser-test.ts +0 -180
- package/src/config/config.ts +0 -33
- package/src/config/default.ts +0 -5
- package/src/config/fixtures.test.ts +0 -137
- package/src/config/mask.test.ts +0 -143
- package/src/config/mask.ts +0 -87
- package/src/config/run-settings.test.ts +0 -163
- package/src/config/run-settings.ts +0 -114
- package/src/config/secrets.ts +0 -70
- package/src/config/variables.ts +0 -31
- package/src/index.ts +0 -10
- package/src/locator/action-timeout-error.test.ts +0 -83
- package/src/locator/action-timeout-error.ts +0 -72
- package/src/locator/element-state.test.ts +0 -181
- package/src/locator/element-state.ts +0 -159
- package/src/locator/locate-nodes.browser-test.ts +0 -169
- package/src/locator/locator-actions.test.ts +0 -355
- package/src/locator/locator-chain.test.ts +0 -292
- package/src/locator/locator.browser-test.ts +0 -137
- package/src/locator/locator.test.ts +0 -179
- package/src/locator/locator.ts +0 -656
- package/src/locator/selector-errors.ts +0 -18
- package/src/locator/selector.test.ts +0 -124
- package/src/locator/selector.ts +0 -107
- package/src/locator/set-files.browser-test.ts +0 -141
- package/src/locator/set-files.test.ts +0 -191
- package/src/locator/text-locator.test.ts +0 -118
- package/src/logger/index.test.ts +0 -28
- package/src/logger/index.ts +0 -40
- package/src/network/data-collector.test.ts +0 -111
- package/src/network/data-collector.ts +0 -92
- package/src/network/network-data.browser-test.ts +0 -192
- package/src/network/network-tracker.browser-test.ts +0 -151
- package/src/network/network-tracker.test.ts +0 -307
- package/src/network/network-tracker.ts +0 -286
- package/src/network/network-wait.ts +0 -28
- package/src/network/response.test.ts +0 -93
- package/src/network/response.ts +0 -58
- package/src/network/router.browser-test.ts +0 -260
- package/src/network/router.test.ts +0 -528
- package/src/network/router.ts +0 -420
- package/src/network/url-match.test.ts +0 -69
- package/src/network/url-match.ts +0 -41
- package/src/runner/page-logs-report.test.ts +0 -144
- package/src/runner/page-logs-report.ts +0 -82
- package/src/runner/prepare-run.test.ts +0 -33
- package/src/runner/prepare-run.ts +0 -36
- package/src/runner/registry.test.ts +0 -59
- package/src/runner/registry.ts +0 -46
- package/src/runner/reporter.test.ts +0 -73
- package/src/runner/reporter.ts +0 -109
- package/src/runner/test-error.test.ts +0 -39
- package/src/runner/test-error.ts +0 -20
- package/src/runner/test-runner.ts +0 -184
- package/src/script/call-function.browser-test.ts +0 -112
- package/src/script/call-function.test.ts +0 -174
- package/src/script/call-function.ts +0 -113
- package/src/script/helpers.test.ts +0 -116
- package/src/script/helpers.ts +0 -68
- package/src/script/preload.browser-test.ts +0 -144
- package/src/script/preload.test.ts +0 -180
- package/src/script/preload.ts +0 -102
- package/src/script/serialize.test.ts +0 -182
- package/src/script/serialize.ts +0 -203
- package/src/testing/browser-fixture.ts +0 -18
- package/src/testing/fake-websocket.ts +0 -72
- package/src/testing/stub-connector.ts +0 -154
- package/src/tests/index.spec.ts +0 -30
- package/src/transport/bidi-connection.test.ts +0 -155
- package/src/transport/bidi-connection.ts +0 -256
- package/src/transport/bidi-error.ts +0 -23
- package/src/transport/foundation.browser-test.ts +0 -73
- package/src/transport/subscriptions.test.ts +0 -140
- package/src/transport/subscriptions.ts +0 -110
- package/src/types/bidi-commands.test-d.ts +0 -55
- package/src/wait/wait-until.test.ts +0 -117
- package/src/wait/wait-until.ts +0 -86
- package/tsconfig.json +0 -44
- /package/{src/api.ts → dist/api.d.ts} +0 -0
- /package/{src/locator/text-locator.ts → dist/locator/text-locator.js} +0 -0
- /package/{src → dist}/types/bidi-modules/browser.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/browsing-context.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/emulation.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/input.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/log.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/network.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/permissions.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/script.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/session.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/storage.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/web-extensions.d.ts +0 -0
- /package/{src → dist}/types/bidi-protocols/command.d.ts +0 -0
- /package/{src → dist}/types/bidi-protocols/error.d.ts +0 -0
- /package/{src → dist}/types/bidi-protocols/event.d.ts +0 -0
- /package/{src → dist}/types/bidi.d.ts +0 -0
- /package/{src → dist}/types/browser.d.ts +0 -0
- /package/{src → dist}/types/config.d.ts +0 -0
- /package/{src → dist}/types/env.d.ts +0 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Browser } from "../browser/browser.js";
|
|
4
|
+
import { projectDir } from "../config/config.js";
|
|
5
|
+
import { withProject } from "../runner/project.js";
|
|
6
|
+
import { parseSpec } from "../steps/parse.js";
|
|
7
|
+
import { applyRecorderEvent } from "./apply.js";
|
|
8
|
+
const NEW_SPEC_HEADER = `import { expect, test } from "@itmetsam/samurai-framework";\n\n`;
|
|
9
|
+
function addTest(source, title) {
|
|
10
|
+
const header = source.trim() === ""
|
|
11
|
+
? NEW_SPEC_HEADER
|
|
12
|
+
: `${source.replace(/\s*$/, "")}\n\n`;
|
|
13
|
+
return `${header}test(${JSON.stringify(title)}, async ({ page }) => {\n});\n`;
|
|
14
|
+
}
|
|
15
|
+
/** The index of the test `selector` names (see `RecordSpecOptions.test`) */
|
|
16
|
+
export function pickTest(tests, selector) {
|
|
17
|
+
if (tests.length === 0)
|
|
18
|
+
throw new Error("The file has no tests; give a title with --new to add one");
|
|
19
|
+
if (selector === undefined) {
|
|
20
|
+
if (tests.length === 1)
|
|
21
|
+
return 0;
|
|
22
|
+
throw new Error(`The file has ${tests.length} tests; choose one with --test:\n${tests.map((t) => ` ${t.name}`).join("\n")}`);
|
|
23
|
+
}
|
|
24
|
+
if (typeof selector === "number") {
|
|
25
|
+
if (!Number.isInteger(selector) ||
|
|
26
|
+
selector < 0 ||
|
|
27
|
+
selector >= tests.length) {
|
|
28
|
+
throw new Error(`There is no test ${selector}; the file has ${tests.length}`);
|
|
29
|
+
}
|
|
30
|
+
return selector;
|
|
31
|
+
}
|
|
32
|
+
const exact = tests.findIndex((t) => t.name === selector);
|
|
33
|
+
if (exact !== -1)
|
|
34
|
+
return exact;
|
|
35
|
+
const partial = tests.flatMap((t, index) => t.name.includes(selector) ? [index] : []);
|
|
36
|
+
if (partial.length === 1)
|
|
37
|
+
return partial[0];
|
|
38
|
+
throw new Error(partial.length === 0
|
|
39
|
+
? `No test matches "${selector}". Tests:\n${tests.map((t) => ` ${t.name}`).join("\n")}`
|
|
40
|
+
: `"${selector}" matches ${partial.length} tests; use the full name`);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Opens a browser window and records what the person does into a spec file, step by step, until `signal`
|
|
44
|
+
* aborts or the window is closed. Every step is written to the file as it happens, as a minimal edit.
|
|
45
|
+
* @example
|
|
46
|
+
* await recordSpec({ file: "tests/login.spec.ts", create: "signs in", environment: "staging" });
|
|
47
|
+
*/
|
|
48
|
+
export async function recordSpec(options) {
|
|
49
|
+
return withProject(options, async ({ settings }) => {
|
|
50
|
+
const file = path.resolve(projectDir(), options.file);
|
|
51
|
+
let source = existsSync(file) ? readFileSync(file, "utf8") : undefined;
|
|
52
|
+
if (source === undefined && options.create === undefined) {
|
|
53
|
+
throw new Error(`${options.file} does not exist; give a title with --new to create it`);
|
|
54
|
+
}
|
|
55
|
+
source ??= "";
|
|
56
|
+
if (options.create !== undefined &&
|
|
57
|
+
!parseSpec(source).some((t) => t.name === options.create)) {
|
|
58
|
+
source = addTest(source, options.create);
|
|
59
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
60
|
+
writeFileSync(file, source);
|
|
61
|
+
}
|
|
62
|
+
const tests = parseSpec(source);
|
|
63
|
+
const testIndex = pickTest(tests, options.create ?? options.test);
|
|
64
|
+
const test = tests[testIndex];
|
|
65
|
+
const at = options.at ?? test.steps.length;
|
|
66
|
+
if (!Number.isInteger(at) || at < 0 || at > test.steps.length) {
|
|
67
|
+
throw new RangeError(`--at must be between 0 and ${test.steps.length} (the test has ${test.steps.length} steps)`);
|
|
68
|
+
}
|
|
69
|
+
const { browser, page } = await Browser.launch("firefox", { port: options.port ?? 9223, headless: false }, options.signal);
|
|
70
|
+
const events = [];
|
|
71
|
+
try {
|
|
72
|
+
const start = options.url ?? settings.baseURL;
|
|
73
|
+
if (options.url !== undefined)
|
|
74
|
+
await page.goto(options.url);
|
|
75
|
+
else if (settings.baseURL !== undefined)
|
|
76
|
+
await page.navigateTo(settings.baseURL);
|
|
77
|
+
const recorder = await page.record({
|
|
78
|
+
at,
|
|
79
|
+
initialGoto: at === 0 && start !== undefined,
|
|
80
|
+
...(settings.baseURL !== undefined && { baseURL: settings.baseURL }),
|
|
81
|
+
onEvent: (event) => {
|
|
82
|
+
source = applyRecorderEvent(source, testIndex, event);
|
|
83
|
+
writeFileSync(file, source);
|
|
84
|
+
events.push(event);
|
|
85
|
+
options.onEvent?.(event, source);
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
await options.onStarted?.({ page, browser });
|
|
89
|
+
let windowClosed = false;
|
|
90
|
+
void browser.exited.then(() => (windowClosed = true));
|
|
91
|
+
await new Promise((resolve) => {
|
|
92
|
+
if (options.signal?.aborted)
|
|
93
|
+
return resolve();
|
|
94
|
+
options.signal?.addEventListener("abort", () => resolve(), {
|
|
95
|
+
once: true,
|
|
96
|
+
});
|
|
97
|
+
void browser.exited.then(resolve);
|
|
98
|
+
});
|
|
99
|
+
// With the window gone there is nothing to ask: what was reported before is in the file
|
|
100
|
+
if (!windowClosed)
|
|
101
|
+
await recorder.stop().catch(() => undefined);
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
await browser.close().catch(() => undefined);
|
|
105
|
+
}
|
|
106
|
+
const secrets = [
|
|
107
|
+
...new Set(events.flatMap(({ step }) => step.kind === "fill" && step.value.kind === "secret"
|
|
108
|
+
? [step.value.name]
|
|
109
|
+
: [])),
|
|
110
|
+
];
|
|
111
|
+
return { file, test: test.name, events, secrets };
|
|
112
|
+
});
|
|
113
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Specs must load as ES modules. A spec in a project whose package.json lacks `"type": "module"` is loaded as
|
|
3
|
+
* CommonJS, which gives it second copies of the framework's modules: `expect(locator)` then no longer
|
|
4
|
+
* recognises a locator, and its tests land in a registry the runner never reads. Fail early instead.
|
|
5
|
+
*/
|
|
6
|
+
export declare function assertModuleProject(specFile: string): void;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Specs must load as ES modules. A spec in a project whose package.json lacks `"type": "module"` is loaded as
|
|
5
|
+
* CommonJS, which gives it second copies of the framework's modules: `expect(locator)` then no longer
|
|
6
|
+
* recognises a locator, and its tests land in a registry the runner never reads. Fail early instead.
|
|
7
|
+
*/
|
|
8
|
+
export function assertModuleProject(specFile) {
|
|
9
|
+
if (specFile.endsWith(".mts"))
|
|
10
|
+
return;
|
|
11
|
+
for (let dir = path.dirname(specFile);; dir = path.dirname(dir)) {
|
|
12
|
+
const manifest = path.join(dir, "package.json");
|
|
13
|
+
if (existsSync(manifest)) {
|
|
14
|
+
let type;
|
|
15
|
+
try {
|
|
16
|
+
type = JSON.parse(readFileSync(manifest, "utf8")).type;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
throw new Error(`Cannot read ${manifest}`);
|
|
20
|
+
}
|
|
21
|
+
if (type === "module")
|
|
22
|
+
return;
|
|
23
|
+
throw new Error(`${manifest} must set "type": "module": specs are ES modules (needed to run ${path.basename(specFile)})`);
|
|
24
|
+
}
|
|
25
|
+
if (path.dirname(dir) === dir)
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
throw new Error(`No package.json with "type": "module" found above ${specFile}: specs are ES modules`);
|
|
29
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { LogRecord, PageError } from "../browser/page-logs.js";
|
|
2
|
+
import type { SamuraiTestConfig } from "../types/config.js";
|
|
3
|
+
import type { TestError } from "../types/test.js";
|
|
4
|
+
/** Config `logs` */
|
|
5
|
+
export type LogsConfig = NonNullable<SamuraiTestConfig["logs"]>;
|
|
6
|
+
/** The page threw exceptions nobody caught, and config `logs.failOnPageError` says that fails the test */
|
|
7
|
+
export declare class PageErrorsError extends Error {
|
|
8
|
+
readonly errors: readonly PageError[];
|
|
9
|
+
constructor(errors: readonly PageError[]);
|
|
10
|
+
}
|
|
11
|
+
/** A route handler threw: the test's mocks did not do what the test meant */
|
|
12
|
+
export declare class RouteErrorsError extends Error {
|
|
13
|
+
readonly errors: readonly Error[];
|
|
14
|
+
constructor(errors: readonly Error[]);
|
|
15
|
+
}
|
|
16
|
+
export interface PageLogsInput {
|
|
17
|
+
config: LogsConfig | undefined;
|
|
18
|
+
entries: readonly LogRecord[];
|
|
19
|
+
errors: readonly PageError[];
|
|
20
|
+
/** How many entries the page's buffer dropped */
|
|
21
|
+
dropped: number;
|
|
22
|
+
/** The test body already failed (or timed out) */
|
|
23
|
+
testFailed: boolean;
|
|
24
|
+
/** The test called `page.allowPageErrors()` */
|
|
25
|
+
allowPageErrors: boolean;
|
|
26
|
+
/** Errors thrown by `page.route` handlers; they fail a passing test */
|
|
27
|
+
routeErrors?: readonly Error[];
|
|
28
|
+
}
|
|
29
|
+
export interface PageLogsVerdict {
|
|
30
|
+
/** Set when a test that passed has to fail because of page errors */
|
|
31
|
+
error?: TestError;
|
|
32
|
+
/** The entries to put in the report, per `logs.capture` */
|
|
33
|
+
logs?: LogRecord[];
|
|
34
|
+
logsDropped?: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Decides, once a test body is done, whether page errors fail it and which log entries go in the report:
|
|
38
|
+
* `capture` "failures" (default) attaches them to failed tests, "all" always, "off" never.
|
|
39
|
+
*/
|
|
40
|
+
export declare function judgePageLogs(input: PageLogsInput): PageLogsVerdict;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { toTestError } from "./test-error.js";
|
|
2
|
+
/** The page threw exceptions nobody caught, and config `logs.failOnPageError` says that fails the test */
|
|
3
|
+
export class PageErrorsError extends Error {
|
|
4
|
+
constructor(errors) {
|
|
5
|
+
super(`${errors.length} uncaught page error(s): ${errors.map((error) => error.message).join("\n")}`);
|
|
6
|
+
this.name = "PageErrorsError";
|
|
7
|
+
this.errors = errors;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/** A route handler threw: the test's mocks did not do what the test meant */
|
|
11
|
+
export class RouteErrorsError extends Error {
|
|
12
|
+
constructor(errors) {
|
|
13
|
+
super(`${errors.length} route handler error(s): ${errors.map((error) => error.message).join("\n")}`);
|
|
14
|
+
this.name = "RouteErrorsError";
|
|
15
|
+
this.errors = errors;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function isIgnored(error, ignore) {
|
|
19
|
+
return ignore.some((pattern) => typeof pattern === "string" ? error.message.includes(pattern) : pattern.test(error.message));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Decides, once a test body is done, whether page errors fail it and which log entries go in the report:
|
|
23
|
+
* `capture` "failures" (default) attaches them to failed tests, "all" always, "off" never.
|
|
24
|
+
*/
|
|
25
|
+
export function judgePageLogs(input) {
|
|
26
|
+
const { config, entries, errors, dropped, testFailed, allowPageErrors, routeErrors = [] } = input;
|
|
27
|
+
const capture = config?.capture ?? "failures";
|
|
28
|
+
let error;
|
|
29
|
+
if (routeErrors.length > 0 && !testFailed) {
|
|
30
|
+
error = toTestError(new RouteErrorsError(routeErrors));
|
|
31
|
+
}
|
|
32
|
+
else if (config?.failOnPageError && !testFailed && !allowPageErrors) {
|
|
33
|
+
const counted = errors.filter((pageError) => !isIgnored(pageError, config.ignoreErrors ?? []));
|
|
34
|
+
if (counted.length > 0)
|
|
35
|
+
error = toTestError(new PageErrorsError(counted));
|
|
36
|
+
}
|
|
37
|
+
const failed = testFailed || error !== undefined;
|
|
38
|
+
const attach = capture === "all" || (capture === "failures" && failed);
|
|
39
|
+
return {
|
|
40
|
+
...(error && { error }),
|
|
41
|
+
...(attach && { logs: [...entries] }),
|
|
42
|
+
...(attach && dropped > 0 && { logsDropped: dropped }),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type RunOverrides, type RunSettings } from "../config/run-settings.js";
|
|
2
|
+
import type { SamuraiTestConfig } from "../types/config.js";
|
|
3
|
+
export interface PreparedRun {
|
|
4
|
+
settings: RunSettings;
|
|
5
|
+
secrets: ReadonlyMap<string, string>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Starts a run: picks the environment, loads its secrets and registers every value for masking
|
|
9
|
+
* (all of them, not only the ones a test reads), then makes the settings active.
|
|
10
|
+
*/
|
|
11
|
+
export declare function prepareRun(config: SamuraiTestConfig, overrides: RunOverrides, options?: {
|
|
12
|
+
cwd?: string;
|
|
13
|
+
env?: NodeJS.ProcessEnv;
|
|
14
|
+
}): PreparedRun;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { isShortSecret, registerSecret } from "../config/mask.js";
|
|
2
|
+
import { resolveRunSettings, setRunSettings, } from "../config/run-settings.js";
|
|
3
|
+
import { loadSecrets } from "../config/secrets.js";
|
|
4
|
+
import logger from "../logger/index.js";
|
|
5
|
+
/**
|
|
6
|
+
* Starts a run: picks the environment, loads its secrets and registers every value for masking
|
|
7
|
+
* (all of them, not only the ones a test reads), then makes the settings active.
|
|
8
|
+
*/
|
|
9
|
+
export function prepareRun(config, overrides, options = {}) {
|
|
10
|
+
const settings = resolveRunSettings(config, overrides);
|
|
11
|
+
const secrets = loadSecrets(settings.environment, options);
|
|
12
|
+
for (const [name, value] of secrets) {
|
|
13
|
+
registerSecret(name, value);
|
|
14
|
+
if (isShortSecret(value)) {
|
|
15
|
+
logger.warn(`Secret "${name}" is short; masking it may hide unrelated text in logs`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
setRunSettings(settings);
|
|
19
|
+
return { settings, secrets };
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type RunOverrides } from "../config/run-settings.js";
|
|
2
|
+
import type { SamuraiTestConfig } from "../types/config.js";
|
|
3
|
+
import { type PreparedRun } from "./prepare-run.js";
|
|
4
|
+
/** Which project an operation works on, and in which environment */
|
|
5
|
+
export interface ProjectOptions extends RunOverrides {
|
|
6
|
+
/** The project folder: `samurai.config.ts`, `.env.<environment>`, specs and `result/`. @default the working directory */
|
|
7
|
+
projectDir?: string;
|
|
8
|
+
/** Use this config instead of reading `samurai.config.ts` */
|
|
9
|
+
config?: SamuraiTestConfig;
|
|
10
|
+
/** Where the framework keeps browser profiles. @default `SAMURAI_DATA_DIR`, else `browsers/` in the project folder */
|
|
11
|
+
dataDir?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Runs `work` with the project's config, folders, environment and secrets in place, and cleans up afterwards.
|
|
15
|
+
* One at a time per process: these are process-wide, so a second call throws.
|
|
16
|
+
*/
|
|
17
|
+
export declare function withProject<T>(options: ProjectOptions, work: (run: PreparedRun) => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { loadConfig, projectDir, setActiveProject } from "../config/config.js";
|
|
2
|
+
import { resetSecrets } from "../config/mask.js";
|
|
3
|
+
import { setRunSettings } from "../config/run-settings.js";
|
|
4
|
+
import { prepareRun } from "./prepare-run.js";
|
|
5
|
+
let active = false;
|
|
6
|
+
/**
|
|
7
|
+
* Runs `work` with the project's config, folders, environment and secrets in place, and cleans up afterwards.
|
|
8
|
+
* One at a time per process: these are process-wide, so a second call throws.
|
|
9
|
+
*/
|
|
10
|
+
export async function withProject(options, work) {
|
|
11
|
+
if (active)
|
|
12
|
+
throw new Error("A test run is already active in this process");
|
|
13
|
+
active = true;
|
|
14
|
+
const { projectDir: dir, config, dataDir, environment, timeout, expectTimeout, } = options;
|
|
15
|
+
const previousDir = projectDir();
|
|
16
|
+
try {
|
|
17
|
+
setActiveProject({ dir: dir ?? previousDir, config, dataDir });
|
|
18
|
+
const loaded = config ?? (await loadConfig());
|
|
19
|
+
setActiveProject({ dir: dir ?? previousDir, config: loaded, dataDir });
|
|
20
|
+
resetSecrets();
|
|
21
|
+
const run = prepareRun(loaded, {
|
|
22
|
+
...(environment !== undefined && { environment }),
|
|
23
|
+
...(timeout !== undefined && { timeout }),
|
|
24
|
+
...(expectTimeout !== undefined && { expectTimeout }),
|
|
25
|
+
});
|
|
26
|
+
return await work(run);
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
setRunSettings(undefined);
|
|
30
|
+
setActiveProject(undefined);
|
|
31
|
+
active = false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RegisteredTestCase, TestCaseBase } from "../types/test.js";
|
|
2
|
+
/** Groups the tests registered inside `fn` under `title`. `fn` runs immediately and must be synchronous. */
|
|
3
|
+
export declare function describe(title: string, fn: () => void): void;
|
|
4
|
+
export declare function test(title: string, fn: TestCaseBase["function"]): void;
|
|
5
|
+
export declare function registeredTests(): readonly RegisteredTestCase[];
|
|
6
|
+
/** Forgets every registered test (tests only) */
|
|
7
|
+
export declare function clearRegistry(): void;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import logger from "../logger/index.js";
|
|
3
|
+
const registered = [];
|
|
4
|
+
/** Titles of the describe blocks being registered, outermost first */
|
|
5
|
+
const titleStack = [];
|
|
6
|
+
/** Groups the tests registered inside `fn` under `title`. `fn` runs immediately and must be synchronous. */
|
|
7
|
+
export function describe(title, fn) {
|
|
8
|
+
titleStack.push(title);
|
|
9
|
+
try {
|
|
10
|
+
const result = fn();
|
|
11
|
+
if (result instanceof Promise) {
|
|
12
|
+
result.catch(() => { });
|
|
13
|
+
throw new Error("describe() callback must be synchronous; it only registers tests");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
finally {
|
|
17
|
+
titleStack.pop();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function test(title, fn) {
|
|
21
|
+
const titlePath = [...titleStack, title];
|
|
22
|
+
const name = titlePath.join(" > ");
|
|
23
|
+
registered.push({ name, titlePath, function: fn, file: callerFile() });
|
|
24
|
+
logger.debug("Registered test %s", name);
|
|
25
|
+
}
|
|
26
|
+
export function registeredTests() {
|
|
27
|
+
return registered;
|
|
28
|
+
}
|
|
29
|
+
/** Forgets every registered test (tests only) */
|
|
30
|
+
export function clearRegistry() {
|
|
31
|
+
registered.length = 0;
|
|
32
|
+
titleStack.length = 0;
|
|
33
|
+
}
|
|
34
|
+
/** File of the code that called test(): stack lines are Error, callerFile, test, caller */
|
|
35
|
+
function callerFile() {
|
|
36
|
+
const caller = new Error().stack?.split("\n")[3] ?? "";
|
|
37
|
+
const match = caller.match(/\((.+?):\d+:\d+\)$/) ?? caller.match(/at (.+?):\d+:\d+$/);
|
|
38
|
+
const location = match?.[1] ?? "";
|
|
39
|
+
return location.startsWith("file://") ? fileURLToPath(location) : location;
|
|
40
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { RegisteredTestCase, TestError, TestLogEntry, TestResult, TestSummary } from "../types/test.js";
|
|
2
|
+
/** What a run reports while it happens */
|
|
3
|
+
export type RunEvent = {
|
|
4
|
+
type: "run-start";
|
|
5
|
+
environment: string;
|
|
6
|
+
total: number;
|
|
7
|
+
} | {
|
|
8
|
+
type: "test-start";
|
|
9
|
+
name: string;
|
|
10
|
+
file: string;
|
|
11
|
+
} | {
|
|
12
|
+
type: "test-end";
|
|
13
|
+
name: string;
|
|
14
|
+
file: string;
|
|
15
|
+
result: TestResult;
|
|
16
|
+
} | {
|
|
17
|
+
type: "run-end";
|
|
18
|
+
summary: TestSummary;
|
|
19
|
+
};
|
|
20
|
+
export interface ReporterOptions {
|
|
21
|
+
/** The environment the run uses, recorded in the report */
|
|
22
|
+
environment: string;
|
|
23
|
+
/** Where the report is written. @default <cwd>/result/report.json */
|
|
24
|
+
output?: string | false;
|
|
25
|
+
/** Called with every event, in order. A throwing listener is ignored */
|
|
26
|
+
onEvent?: (event: RunEvent) => void;
|
|
27
|
+
}
|
|
28
|
+
export default class TestReporter {
|
|
29
|
+
private testResults;
|
|
30
|
+
private summary;
|
|
31
|
+
private environment;
|
|
32
|
+
private output;
|
|
33
|
+
private listener;
|
|
34
|
+
constructor(options: ReporterOptions);
|
|
35
|
+
private emit;
|
|
36
|
+
onStart(total?: number): void;
|
|
37
|
+
onEnd(): Promise<TestSummary>;
|
|
38
|
+
onTestStart(test: RegisteredTestCase): void;
|
|
39
|
+
onTestEnd(test: RegisteredTestCase, error?: TestError, logs?: {
|
|
40
|
+
logs?: TestLogEntry[];
|
|
41
|
+
logsDropped?: number;
|
|
42
|
+
}): void;
|
|
43
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { projectDir } from "../config/config.js";
|
|
4
|
+
import { maskDeep } from "../config/mask.js";
|
|
5
|
+
/** Results are keyed by file and full title, so equal titles in different files or describe blocks stay apart */
|
|
6
|
+
function keyOf(test) {
|
|
7
|
+
return `${test.file}::${test.name}`;
|
|
8
|
+
}
|
|
9
|
+
export default class TestReporter {
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.testResults = new Map();
|
|
12
|
+
this.environment = options.environment;
|
|
13
|
+
this.output =
|
|
14
|
+
options.output ?? path.join(projectDir(), "result/report.json");
|
|
15
|
+
this.listener = options.onEvent;
|
|
16
|
+
}
|
|
17
|
+
emit(event) {
|
|
18
|
+
try {
|
|
19
|
+
this.listener?.(event);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// A listener must not break the run
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
onStart(total = 0) {
|
|
26
|
+
const t = performance.mark(`group-start`);
|
|
27
|
+
this.summary = {
|
|
28
|
+
duration: 0,
|
|
29
|
+
status: "failed",
|
|
30
|
+
startTime: t.startTime,
|
|
31
|
+
environment: this.environment,
|
|
32
|
+
tests: [],
|
|
33
|
+
};
|
|
34
|
+
this.emit({ type: "run-start", environment: this.environment, total });
|
|
35
|
+
}
|
|
36
|
+
async onEnd() {
|
|
37
|
+
performance.mark(`group-finish`);
|
|
38
|
+
const duration = performance.measure("test-duration", `group-start`, `group-finish`).duration;
|
|
39
|
+
this.summary = {
|
|
40
|
+
...this.summary,
|
|
41
|
+
duration,
|
|
42
|
+
environment: this.environment,
|
|
43
|
+
tests: Array.from(this.testResults.values()),
|
|
44
|
+
status: Array.from(this.testResults.values()).every((r) => r.status === "success")
|
|
45
|
+
? "success"
|
|
46
|
+
: "failed",
|
|
47
|
+
};
|
|
48
|
+
if (this.output !== false) {
|
|
49
|
+
await mkdir(path.dirname(this.output), { recursive: true });
|
|
50
|
+
await writeFile(this.output, JSON.stringify(this.summary, null, 2), {
|
|
51
|
+
encoding: "utf8",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
this.emit({ type: "run-end", summary: this.summary });
|
|
55
|
+
return this.summary;
|
|
56
|
+
}
|
|
57
|
+
onTestStart(test) {
|
|
58
|
+
const key = keyOf(test);
|
|
59
|
+
const t = performance.mark(`${key}-start`);
|
|
60
|
+
this.testResults.set(key, {
|
|
61
|
+
name: test.name,
|
|
62
|
+
file: test.file,
|
|
63
|
+
status: "started",
|
|
64
|
+
startTime: t.startTime,
|
|
65
|
+
});
|
|
66
|
+
this.emit({ type: "test-start", name: test.name, file: test.file });
|
|
67
|
+
}
|
|
68
|
+
onTestEnd(test, error, logs) {
|
|
69
|
+
const key = keyOf(test);
|
|
70
|
+
performance.mark(`${key}-finish`);
|
|
71
|
+
const duration = performance.measure("test-duration", `${key}-start`, `${key}-finish`).duration;
|
|
72
|
+
const result = maskDeep({
|
|
73
|
+
...this.testResults.get(key),
|
|
74
|
+
duration,
|
|
75
|
+
status: error ? "failed" : "success",
|
|
76
|
+
...(error ? error : undefined),
|
|
77
|
+
...(logs?.logs && { logs: logs.logs }),
|
|
78
|
+
...(logs?.logsDropped && { logsDropped: logs.logsDropped }),
|
|
79
|
+
});
|
|
80
|
+
this.testResults.set(key, result);
|
|
81
|
+
this.emit({ type: "test-end", name: test.name, file: test.file, result });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { TestSummary } from "../types/test.js";
|
|
2
|
+
import { type ProjectOptions } from "./project.js";
|
|
3
|
+
import { type RunnerOptions } from "./test-runner.js";
|
|
4
|
+
export interface RunTestsOptions extends RunnerOptions, ProjectOptions {
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Runs the project's tests, one after the other, and resolves with the summary (also written to the report).
|
|
8
|
+
* Spec files are TypeScript: the process must be able to import them (run under `tsx`, or with Node's own
|
|
9
|
+
* type stripping). One run at a time per process: config, secrets and the test registry are shared state;
|
|
10
|
+
* use a child process per run for more.
|
|
11
|
+
* @example
|
|
12
|
+
* const summary = await runTests({ projectDir: "/work/shop", environment: "staging", headless: true,
|
|
13
|
+
* onEvent: (event) => console.log(event.type) });
|
|
14
|
+
*/
|
|
15
|
+
export declare function runTests(options?: RunTestsOptions): Promise<TestSummary>;
|
|
16
|
+
/** A test found in a spec file */
|
|
17
|
+
export interface FoundTest {
|
|
18
|
+
/** Full name: `describe` titles and the test title joined with " > " */
|
|
19
|
+
name: string;
|
|
20
|
+
file: string;
|
|
21
|
+
}
|
|
22
|
+
/** Imports the project's specs without running anything and lists the tests the options select */
|
|
23
|
+
export declare function listTests(options?: RunTestsOptions): Promise<FoundTest[]>;
|
|
24
|
+
export type { RunEvent } from "./reporter.js";
|
|
25
|
+
export type { TestError, TestResult, TestSummary } from "../types/test.js";
|
|
26
|
+
export type { RunnerOptions } from "./test-runner.js";
|
|
27
|
+
export type { ProjectOptions } from "./project.js";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { withProject } from "./project.js";
|
|
2
|
+
import TestRunner, {} from "./test-runner.js";
|
|
3
|
+
/**
|
|
4
|
+
* Runs the project's tests, one after the other, and resolves with the summary (also written to the report).
|
|
5
|
+
* Spec files are TypeScript: the process must be able to import them (run under `tsx`, or with Node's own
|
|
6
|
+
* type stripping). One run at a time per process: config, secrets and the test registry are shared state;
|
|
7
|
+
* use a child process per run for more.
|
|
8
|
+
* @example
|
|
9
|
+
* const summary = await runTests({ projectDir: "/work/shop", environment: "staging", headless: true,
|
|
10
|
+
* onEvent: (event) => console.log(event.type) });
|
|
11
|
+
*/
|
|
12
|
+
export async function runTests(options = {}) {
|
|
13
|
+
const { projectDir, config, dataDir, environment, timeout, expectTimeout, ...runner } = options;
|
|
14
|
+
return withProject({
|
|
15
|
+
projectDir,
|
|
16
|
+
config,
|
|
17
|
+
dataDir,
|
|
18
|
+
environment,
|
|
19
|
+
timeout,
|
|
20
|
+
expectTimeout,
|
|
21
|
+
}, async (run) => (await TestRunner.init(run, undefined, runner)).start());
|
|
22
|
+
}
|
|
23
|
+
/** Imports the project's specs without running anything and lists the tests the options select */
|
|
24
|
+
export async function listTests(options = {}) {
|
|
25
|
+
const { projectDir, config, dataDir, environment, timeout, expectTimeout, ...runner } = options;
|
|
26
|
+
return withProject({
|
|
27
|
+
projectDir,
|
|
28
|
+
config,
|
|
29
|
+
dataDir,
|
|
30
|
+
environment,
|
|
31
|
+
timeout,
|
|
32
|
+
expectTimeout,
|
|
33
|
+
}, async (run) => (await (await TestRunner.init(run, undefined, runner)).register()).map(({ name, file }) => ({ name, file })));
|
|
34
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { AssertionError, toReportValue } from "../assert/assertion-error.js";
|
|
2
|
+
/** Maps anything a test threw to the error stored in the report */
|
|
3
|
+
export function toTestError(err) {
|
|
4
|
+
if (err instanceof AssertionError) {
|
|
5
|
+
return {
|
|
6
|
+
message: err.message,
|
|
7
|
+
stack: err.stack,
|
|
8
|
+
type: "assertion",
|
|
9
|
+
expected: toReportValue(err.expected),
|
|
10
|
+
actual: toReportValue(err.actual),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
message: err instanceof Error ? err.message : String(err),
|
|
15
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
16
|
+
type: "error",
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { RegisteredTestCase } from "../types/test.js";
|
|
2
|
+
import type { PreparedRun } from "./prepare-run.js";
|
|
3
|
+
import { type RunEvent } from "./reporter.js";
|
|
4
|
+
import type { SamuraiGroup } from "../types/config.js";
|
|
5
|
+
/** What decides which tests of the found files run, and how the browser starts */
|
|
6
|
+
export interface RunnerOptions {
|
|
7
|
+
/** Spec files to run, absolute or relative to the project folder. Default: every `*.spec.ts` under `srcDir` */
|
|
8
|
+
files?: string[];
|
|
9
|
+
/** Only tests whose full name matches (a string must be contained in it) */
|
|
10
|
+
grep?: string | RegExp;
|
|
11
|
+
/** Only tests with exactly one of these full names (`describe` titles joined with " > ") */
|
|
12
|
+
testNames?: string[];
|
|
13
|
+
/** Run the browser without a window. @default false */
|
|
14
|
+
headless?: boolean;
|
|
15
|
+
/** Remote debugging port of the browser. @default 9223 */
|
|
16
|
+
port?: number;
|
|
17
|
+
/** Where the report is written; `false` writes none. @default `<project>/result/report.json` */
|
|
18
|
+
reportPath?: string | false;
|
|
19
|
+
/** Receives start and end of the run and of every test, as they happen */
|
|
20
|
+
onEvent?: (event: RunEvent) => void;
|
|
21
|
+
/** Aborting stops the running test (its browser is closed), skips the rest and reports the run as failed */
|
|
22
|
+
signal?: AbortSignal;
|
|
23
|
+
}
|
|
24
|
+
/** Whether a test with this full name is selected by the options */
|
|
25
|
+
export declare function isSelected(name: string, { grep, testNames }: Pick<RunnerOptions, "grep" | "testNames">): boolean;
|
|
26
|
+
export default class TestRunner {
|
|
27
|
+
private testFiles;
|
|
28
|
+
private run;
|
|
29
|
+
private group?;
|
|
30
|
+
private options;
|
|
31
|
+
constructor(testFiles: string[], run: PreparedRun, group?: SamuraiGroup, options?: RunnerOptions);
|
|
32
|
+
static init(run: PreparedRun, group?: SamuraiGroup, options?: RunnerOptions): Promise<TestRunner>;
|
|
33
|
+
/** Runs the selected tests and returns the summary that was also written to the report */
|
|
34
|
+
/**
|
|
35
|
+
* Imports the spec files and returns the tests the options select. A fresh registry and a fresh import of each
|
|
36
|
+
* file, so a second run in this process registers its tests again (and sees edited specs, for files loaded as
|
|
37
|
+
* ES modules; CommonJS files stay cached until the process ends)
|
|
38
|
+
*/
|
|
39
|
+
register(): Promise<RegisteredTestCase[]>;
|
|
40
|
+
/** Runs the selected tests and returns the summary that was also written to the report */
|
|
41
|
+
start(): Promise<import("../types/test.js").TestSummary>;
|
|
42
|
+
private executeTestCase;
|
|
43
|
+
}
|
|
44
|
+
export { describe, test } from "./registry.js";
|