@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,72 @@
|
|
|
1
|
+
import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
2
|
+
/** Paper sizes in centimetres, as the browser wants them */
|
|
3
|
+
declare const PAPER: {
|
|
4
|
+
readonly Letter: {
|
|
5
|
+
readonly width: 21.59;
|
|
6
|
+
readonly height: 27.94;
|
|
7
|
+
};
|
|
8
|
+
readonly Legal: {
|
|
9
|
+
readonly width: 21.59;
|
|
10
|
+
readonly height: 35.56;
|
|
11
|
+
};
|
|
12
|
+
readonly Tabloid: {
|
|
13
|
+
readonly width: 27.94;
|
|
14
|
+
readonly height: 43.18;
|
|
15
|
+
};
|
|
16
|
+
readonly Ledger: {
|
|
17
|
+
readonly width: 43.18;
|
|
18
|
+
readonly height: 27.94;
|
|
19
|
+
};
|
|
20
|
+
readonly A0: {
|
|
21
|
+
readonly width: 84.1;
|
|
22
|
+
readonly height: 118.9;
|
|
23
|
+
};
|
|
24
|
+
readonly A1: {
|
|
25
|
+
readonly width: 59.4;
|
|
26
|
+
readonly height: 84.1;
|
|
27
|
+
};
|
|
28
|
+
readonly A2: {
|
|
29
|
+
readonly width: 42;
|
|
30
|
+
readonly height: 59.4;
|
|
31
|
+
};
|
|
32
|
+
readonly A3: {
|
|
33
|
+
readonly width: 29.7;
|
|
34
|
+
readonly height: 42;
|
|
35
|
+
};
|
|
36
|
+
readonly A4: {
|
|
37
|
+
readonly width: 21;
|
|
38
|
+
readonly height: 29.7;
|
|
39
|
+
};
|
|
40
|
+
readonly A5: {
|
|
41
|
+
readonly width: 14.8;
|
|
42
|
+
readonly height: 21;
|
|
43
|
+
};
|
|
44
|
+
readonly A6: {
|
|
45
|
+
readonly width: 10.5;
|
|
46
|
+
readonly height: 14.8;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
export type PaperFormat = keyof typeof PAPER;
|
|
50
|
+
export interface PdfOptions {
|
|
51
|
+
/** Also write the PDF here (folders are created) */
|
|
52
|
+
path?: string;
|
|
53
|
+
landscape?: boolean;
|
|
54
|
+
/** 0.1 to 2 */
|
|
55
|
+
scale?: number;
|
|
56
|
+
/** Pages to print, e.g. `["1-3", 5]` */
|
|
57
|
+
pageRanges?: (string | number)[];
|
|
58
|
+
/** Page margins in centimetres */
|
|
59
|
+
margin?: {
|
|
60
|
+
top?: number;
|
|
61
|
+
bottom?: number;
|
|
62
|
+
left?: number;
|
|
63
|
+
right?: number;
|
|
64
|
+
};
|
|
65
|
+
/** Print background colours and images */
|
|
66
|
+
printBackground?: boolean;
|
|
67
|
+
/** Paper size. Without one the browser's default is used */
|
|
68
|
+
format?: PaperFormat;
|
|
69
|
+
}
|
|
70
|
+
/** Renders the page as a PDF and returns it (also written to `options.path`) */
|
|
71
|
+
export declare function printPdf(connector: BiDiConnector, context: string, options?: PdfOptions): Promise<Buffer>;
|
|
72
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { writeOutput } from "./screenshot.js";
|
|
2
|
+
/** Paper sizes in centimetres, as the browser wants them */
|
|
3
|
+
const PAPER = {
|
|
4
|
+
Letter: { width: 21.59, height: 27.94 },
|
|
5
|
+
Legal: { width: 21.59, height: 35.56 },
|
|
6
|
+
Tabloid: { width: 27.94, height: 43.18 },
|
|
7
|
+
Ledger: { width: 43.18, height: 27.94 },
|
|
8
|
+
A0: { width: 84.1, height: 118.9 },
|
|
9
|
+
A1: { width: 59.4, height: 84.1 },
|
|
10
|
+
A2: { width: 42, height: 59.4 },
|
|
11
|
+
A3: { width: 29.7, height: 42 },
|
|
12
|
+
A4: { width: 21, height: 29.7 },
|
|
13
|
+
A5: { width: 14.8, height: 21 },
|
|
14
|
+
A6: { width: 10.5, height: 14.8 },
|
|
15
|
+
};
|
|
16
|
+
/** Renders the page as a PDF and returns it (also written to `options.path`) */
|
|
17
|
+
export async function printPdf(connector, context, options = {}) {
|
|
18
|
+
const { scale, format } = options;
|
|
19
|
+
if (scale !== undefined && !(scale >= 0.1 && scale <= 2)) {
|
|
20
|
+
throw new RangeError("pdf(): scale must be between 0.1 and 2");
|
|
21
|
+
}
|
|
22
|
+
if (format !== undefined && !(format in PAPER)) {
|
|
23
|
+
throw new RangeError(`pdf(): unknown paper format "${format}"; use one of ${Object.keys(PAPER).join(", ")}`);
|
|
24
|
+
}
|
|
25
|
+
const { data } = await connector.send("browsingContext.print", {
|
|
26
|
+
context,
|
|
27
|
+
...(options.landscape && { orientation: "landscape" }),
|
|
28
|
+
...(scale !== undefined && { scale }),
|
|
29
|
+
...(options.pageRanges && { pageRanges: options.pageRanges }),
|
|
30
|
+
...(options.margin && { margin: options.margin }),
|
|
31
|
+
...(options.printBackground && { background: true }),
|
|
32
|
+
...(format && { page: { ...PAPER[format] } }),
|
|
33
|
+
});
|
|
34
|
+
const pdf = Buffer.from(data, "base64");
|
|
35
|
+
await writeOutput(options.path, pdf);
|
|
36
|
+
return pdf;
|
|
37
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
2
|
+
export interface ScreenshotOptions {
|
|
3
|
+
/** Also write the image here (folders are created) */
|
|
4
|
+
path?: string;
|
|
5
|
+
/** The whole document instead of the viewport */
|
|
6
|
+
fullPage?: boolean;
|
|
7
|
+
/** Only this part: in viewport coordinates, or document coordinates with `fullPage` */
|
|
8
|
+
clip?: {
|
|
9
|
+
x: number;
|
|
10
|
+
y: number;
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
};
|
|
14
|
+
/** @default "png" */
|
|
15
|
+
type?: "png" | "jpeg";
|
|
16
|
+
/** 0 to 100, jpeg only */
|
|
17
|
+
quality?: number;
|
|
18
|
+
}
|
|
19
|
+
/** The options a single element's screenshot takes: the image format and where to write it */
|
|
20
|
+
export type ElementScreenshotOptions = Pick<ScreenshotOptions, "path" | "type" | "quality">;
|
|
21
|
+
/**
|
|
22
|
+
* Captures the page (or, with `element`, that element) and returns the image. Writes it to `options.path` too.
|
|
23
|
+
* The browser answers Base64.
|
|
24
|
+
*/
|
|
25
|
+
export declare function takeScreenshot(connector: BiDiConnector, context: string, options?: ScreenshotOptions, element?: {
|
|
26
|
+
sharedId: string;
|
|
27
|
+
}): Promise<Buffer>;
|
|
28
|
+
/** Writes `data` to `file`, creating the folders; nothing happens without a path */
|
|
29
|
+
export declare function writeOutput(file: string | undefined, data: Buffer): Promise<void>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const MIME = { png: "image/png", jpeg: "image/jpeg" };
|
|
4
|
+
/** The `format` of a capture command; validates the type/quality combination */
|
|
5
|
+
function imageFormat({ type, quality }) {
|
|
6
|
+
if (quality !== undefined) {
|
|
7
|
+
if (type !== "jpeg")
|
|
8
|
+
throw new RangeError("screenshot(): quality only applies to jpeg");
|
|
9
|
+
if (!Number.isFinite(quality) || quality < 0 || quality > 100) {
|
|
10
|
+
throw new RangeError("screenshot(): quality must be between 0 and 100");
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
if (type === undefined)
|
|
14
|
+
return undefined;
|
|
15
|
+
return { type: MIME[type], ...(quality !== undefined && { quality: quality / 100 }) };
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Captures the page (or, with `element`, that element) and returns the image. Writes it to `options.path` too.
|
|
19
|
+
* The browser answers Base64.
|
|
20
|
+
*/
|
|
21
|
+
export async function takeScreenshot(connector, context, options = {}, element) {
|
|
22
|
+
const format = imageFormat(options);
|
|
23
|
+
const clip = element
|
|
24
|
+
? { type: "element", element: { sharedId: element.sharedId } }
|
|
25
|
+
: options.clip && { type: "box", ...options.clip };
|
|
26
|
+
const { data } = await connector.send("browsingContext.captureScreenshot", {
|
|
27
|
+
context,
|
|
28
|
+
...(options.fullPage && { origin: "document" }),
|
|
29
|
+
...(format && { format }),
|
|
30
|
+
...(clip && { clip }),
|
|
31
|
+
});
|
|
32
|
+
const image = Buffer.from(data, "base64");
|
|
33
|
+
await writeOutput(options.path, image);
|
|
34
|
+
return image;
|
|
35
|
+
}
|
|
36
|
+
/** Writes `data` to `file`, creating the folders; nothing happens without a path */
|
|
37
|
+
export async function writeOutput(file, data) {
|
|
38
|
+
if (!file)
|
|
39
|
+
return;
|
|
40
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
41
|
+
await writeFile(file, data);
|
|
42
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ProjectOptions } from "../runner/run.js";
|
|
2
|
+
/** The values a command line can override for one run */
|
|
3
|
+
type RunOverrides = Pick<ProjectOptions, "environment" | "timeout" | "expectTimeout">;
|
|
4
|
+
/** Flags and positional arguments of a command line; an unknown flag is an error */
|
|
5
|
+
export declare function parseCommandLine(argv: string[]): {
|
|
6
|
+
values: {
|
|
7
|
+
env?: string | undefined;
|
|
8
|
+
timeout?: string | undefined;
|
|
9
|
+
"expect-timeout"?: string | undefined;
|
|
10
|
+
headless?: boolean | undefined;
|
|
11
|
+
port?: string | undefined;
|
|
12
|
+
grep?: string | undefined;
|
|
13
|
+
file?: string[] | undefined;
|
|
14
|
+
json?: boolean | undefined;
|
|
15
|
+
test?: string | undefined;
|
|
16
|
+
at?: string | undefined;
|
|
17
|
+
new?: string | undefined;
|
|
18
|
+
url?: string | undefined;
|
|
19
|
+
help?: boolean | undefined;
|
|
20
|
+
version?: boolean | undefined;
|
|
21
|
+
};
|
|
22
|
+
positionals: string[];
|
|
23
|
+
};
|
|
24
|
+
/** How the command line says tests are selected and the browser starts */
|
|
25
|
+
export interface RunnerFlags {
|
|
26
|
+
headless?: boolean;
|
|
27
|
+
port?: number;
|
|
28
|
+
grep?: string;
|
|
29
|
+
files?: string[];
|
|
30
|
+
}
|
|
31
|
+
/** Reads `--headless`, `--port <n>`, `--grep <text>` and `--file <spec>` (repeatable) from `argv` */
|
|
32
|
+
export declare function parseRunnerFlags(argv: string[]): RunnerFlags;
|
|
33
|
+
/** Reads `--env`, `--timeout` and `--expect-timeout` from `argv`; the environment falls back to SAMURAI_ENV */
|
|
34
|
+
export declare function parseRunOverrides(argv: string[], env?: Partial<NodeJS.ProcessEnv>): RunOverrides;
|
|
35
|
+
export {};
|
package/dist/cli/args.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
const FLAGS = {
|
|
3
|
+
env: { type: "string" },
|
|
4
|
+
timeout: { type: "string" },
|
|
5
|
+
"expect-timeout": { type: "string" },
|
|
6
|
+
headless: { type: "boolean" },
|
|
7
|
+
port: { type: "string" },
|
|
8
|
+
grep: { type: "string" },
|
|
9
|
+
file: { type: "string", multiple: true },
|
|
10
|
+
json: { type: "boolean" },
|
|
11
|
+
test: { type: "string" },
|
|
12
|
+
at: { type: "string" },
|
|
13
|
+
new: { type: "string" },
|
|
14
|
+
url: { type: "string" },
|
|
15
|
+
help: { type: "boolean", short: "h" },
|
|
16
|
+
version: { type: "boolean", short: "v" },
|
|
17
|
+
};
|
|
18
|
+
/** Flags and positional arguments of a command line; an unknown flag is an error */
|
|
19
|
+
export function parseCommandLine(argv) {
|
|
20
|
+
return parseArgs({
|
|
21
|
+
args: argv,
|
|
22
|
+
options: FLAGS,
|
|
23
|
+
strict: true,
|
|
24
|
+
allowPositionals: true,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function parseFlags(argv) {
|
|
28
|
+
return parseCommandLine(argv).values;
|
|
29
|
+
}
|
|
30
|
+
/** Reads `--headless`, `--port <n>`, `--grep <text>` and `--file <spec>` (repeatable) from `argv` */
|
|
31
|
+
export function parseRunnerFlags(argv) {
|
|
32
|
+
const values = parseFlags(argv);
|
|
33
|
+
const flags = {};
|
|
34
|
+
if (values.headless)
|
|
35
|
+
flags.headless = true;
|
|
36
|
+
if (values.port !== undefined) {
|
|
37
|
+
const port = Number(values.port);
|
|
38
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
39
|
+
throw new Error(`--port must be a port number between 1 and 65535, got "${values.port}"`);
|
|
40
|
+
}
|
|
41
|
+
flags.port = port;
|
|
42
|
+
}
|
|
43
|
+
if (values.grep !== undefined)
|
|
44
|
+
flags.grep = values.grep;
|
|
45
|
+
if (values.file?.length)
|
|
46
|
+
flags.files = values.file;
|
|
47
|
+
return flags;
|
|
48
|
+
}
|
|
49
|
+
/** Reads `--env`, `--timeout` and `--expect-timeout` from `argv`; the environment falls back to SAMURAI_ENV */
|
|
50
|
+
export function parseRunOverrides(argv, env = {}) {
|
|
51
|
+
const values = parseFlags(argv);
|
|
52
|
+
const overrides = {};
|
|
53
|
+
const environment = values.env ?? env.SAMURAI_ENV;
|
|
54
|
+
if (environment)
|
|
55
|
+
overrides.environment = environment;
|
|
56
|
+
if (values.timeout !== undefined)
|
|
57
|
+
overrides.timeout = milliseconds("--timeout", values.timeout);
|
|
58
|
+
if (values["expect-timeout"] !== undefined) {
|
|
59
|
+
overrides.expectTimeout = milliseconds("--expect-timeout", values["expect-timeout"]);
|
|
60
|
+
}
|
|
61
|
+
return overrides;
|
|
62
|
+
}
|
|
63
|
+
function milliseconds(flag, raw) {
|
|
64
|
+
const value = Number(raw);
|
|
65
|
+
if (raw.trim() === "" || !Number.isInteger(value) || value < 0) {
|
|
66
|
+
throw new Error(`${flag} must be a whole number of milliseconds, got "${raw}"`);
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { describe, it } from "node:test";
|
|
6
|
+
import { ConsoleReporter } from "./console-reporter.js";
|
|
7
|
+
import { initProject, scaffold } from "./init.js";
|
|
8
|
+
import { main, USAGE } from "./main.js";
|
|
9
|
+
function io() {
|
|
10
|
+
const out = [];
|
|
11
|
+
const err = [];
|
|
12
|
+
const target = {
|
|
13
|
+
out: (t) => out.push(t),
|
|
14
|
+
err: (t) => err.push(t),
|
|
15
|
+
color: false,
|
|
16
|
+
env: {},
|
|
17
|
+
};
|
|
18
|
+
return { target, out: () => out.join(""), err: () => err.join("") };
|
|
19
|
+
}
|
|
20
|
+
describe("main", () => {
|
|
21
|
+
it("prints the usage for --help, -h and help", async () => {
|
|
22
|
+
for (const argv of [["--help"], ["-h"], ["help"], ["run", "--help"]]) {
|
|
23
|
+
const { target, out } = io();
|
|
24
|
+
assert.equal(await main(argv, target), 0);
|
|
25
|
+
assert.equal(out(), USAGE);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
it("prints the version", async () => {
|
|
29
|
+
const { target, out } = io();
|
|
30
|
+
assert.equal(await main(["--version"], target), 0);
|
|
31
|
+
assert.match(out(), /^\d+\.\d+\.\d+/);
|
|
32
|
+
});
|
|
33
|
+
it("exits 2 for an unknown command, with the usage", async () => {
|
|
34
|
+
const { target, err } = io();
|
|
35
|
+
assert.equal(await main(["bogus"], target), 2);
|
|
36
|
+
assert.match(err(), /Unknown command "bogus"/);
|
|
37
|
+
assert.match(err(), /Usage:/);
|
|
38
|
+
});
|
|
39
|
+
it("exits 2 for an unknown flag or a bad value, with a message", async () => {
|
|
40
|
+
const flag = io();
|
|
41
|
+
assert.equal(await main(["run", "--nope"], flag.target), 2);
|
|
42
|
+
assert.match(flag.err(), /nope/);
|
|
43
|
+
const port = io();
|
|
44
|
+
assert.equal(await main(["run", "--port", "abc"], port.target), 2);
|
|
45
|
+
assert.match(port.err(), /--port/);
|
|
46
|
+
});
|
|
47
|
+
it("exits 2 when the project has no config", async () => {
|
|
48
|
+
const dir = mkdtempSync(path.join(tmpdir(), "samurai-cli-"));
|
|
49
|
+
const previous = process.cwd();
|
|
50
|
+
process.chdir(dir);
|
|
51
|
+
try {
|
|
52
|
+
const { target, err } = io();
|
|
53
|
+
assert.equal(await main(["list"], target), 2);
|
|
54
|
+
assert.ok(err().length > 0);
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
process.chdir(previous);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
it("record needs a spec and a sensible --at", async () => {
|
|
61
|
+
const none = io();
|
|
62
|
+
assert.equal(await main(["record"], none.target), 2);
|
|
63
|
+
assert.match(none.err(), /Which spec/);
|
|
64
|
+
const bad = io();
|
|
65
|
+
assert.equal(await main(["record", "a.spec.ts", "--at", "x"], bad.target), 2);
|
|
66
|
+
assert.match(bad.err(), /--at/);
|
|
67
|
+
});
|
|
68
|
+
it("record refuses a spec that does not exist unless --new is given", async () => {
|
|
69
|
+
const dir = mkdtempSync(path.join(tmpdir(), "samurai-cli-"));
|
|
70
|
+
writeFileSync(path.join(dir, "samurai.config.ts"), "export default {};");
|
|
71
|
+
const previous = process.cwd();
|
|
72
|
+
process.chdir(dir);
|
|
73
|
+
try {
|
|
74
|
+
const { target, err } = io();
|
|
75
|
+
assert.equal(await main(["record", "tests/none.spec.ts"], target), 2);
|
|
76
|
+
assert.match(err(), /does not exist/);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
process.chdir(previous);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
it("init scaffolds a project and never overwrites", async () => {
|
|
83
|
+
const dir = mkdtempSync(path.join(tmpdir(), "samurai-init-"));
|
|
84
|
+
writeFileSync(path.join(dir, "package.json"), '{"name":"mine"}');
|
|
85
|
+
const { target, out } = io();
|
|
86
|
+
assert.equal(await main(["init", dir], target), 0);
|
|
87
|
+
assert.match(out(), /kept\s+package\.json/);
|
|
88
|
+
assert.match(out(), /created samurai\.config\.ts/);
|
|
89
|
+
assert.equal(readFileSync(path.join(dir, "package.json"), "utf8"), '{"name":"mine"}');
|
|
90
|
+
for (const file of [
|
|
91
|
+
"samurai.config.ts",
|
|
92
|
+
"tests/example.spec.ts",
|
|
93
|
+
".env.example",
|
|
94
|
+
".gitignore",
|
|
95
|
+
]) {
|
|
96
|
+
assert.ok(existsSync(path.join(dir, file)), file);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
describe("scaffold", () => {
|
|
101
|
+
it("is an ES module project with a test script", () => {
|
|
102
|
+
const manifest = JSON.parse(scaffold("shop")["package.json"]);
|
|
103
|
+
assert.equal(manifest.type, "module");
|
|
104
|
+
assert.equal(manifest.scripts.test, "samurai run");
|
|
105
|
+
assert.equal(manifest.name, "shop");
|
|
106
|
+
});
|
|
107
|
+
it("names the package after the folder, safely", () => {
|
|
108
|
+
const dir = mkdtempSync(path.join(tmpdir(), "My Shop!-"));
|
|
109
|
+
initProject(dir);
|
|
110
|
+
const manifest = JSON.parse(readFileSync(path.join(dir, "package.json"), "utf8"));
|
|
111
|
+
assert.match(manifest.name, /^[a-z0-9-]+$/);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
describe("ConsoleReporter", () => {
|
|
115
|
+
const summary = {
|
|
116
|
+
status: "failed",
|
|
117
|
+
duration: 2300,
|
|
118
|
+
startTime: 0,
|
|
119
|
+
environment: "staging",
|
|
120
|
+
tests: [
|
|
121
|
+
{
|
|
122
|
+
name: "a",
|
|
123
|
+
file: "/x/a.spec.ts",
|
|
124
|
+
startTime: 0,
|
|
125
|
+
status: "success",
|
|
126
|
+
duration: 1000,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: "b > c",
|
|
130
|
+
file: "/x/a.spec.ts",
|
|
131
|
+
startTime: 0,
|
|
132
|
+
status: "failed",
|
|
133
|
+
duration: 1300,
|
|
134
|
+
type: "error",
|
|
135
|
+
message: "boom\nsecond line",
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
};
|
|
139
|
+
it("prints a line per test, the failures in full and a count", () => {
|
|
140
|
+
let text = "";
|
|
141
|
+
const reporter = new ConsoleReporter({
|
|
142
|
+
write: (t) => (text += t),
|
|
143
|
+
color: false,
|
|
144
|
+
});
|
|
145
|
+
reporter.handle({ type: "run-start", environment: "staging", total: 2 });
|
|
146
|
+
reporter.handle({ type: "test-start", name: "a", file: "/x/a.spec.ts" });
|
|
147
|
+
for (const result of summary.tests)
|
|
148
|
+
reporter.handle({
|
|
149
|
+
type: "test-end",
|
|
150
|
+
name: "name" in result ? result.name : "",
|
|
151
|
+
file: "/x/a.spec.ts",
|
|
152
|
+
result,
|
|
153
|
+
});
|
|
154
|
+
reporter.handle({ type: "run-end", summary });
|
|
155
|
+
assert.match(text, /Running 2 tests against staging/);
|
|
156
|
+
assert.match(text, /✔ a \(1\.0s\)/);
|
|
157
|
+
assert.match(text, /✖ b > c \(1\.3s\)/);
|
|
158
|
+
assert.match(text, /\n {4}boom\n {4}second line\n/);
|
|
159
|
+
assert.match(text, /1 failed, 1 passed \(2\.3s\)/);
|
|
160
|
+
assert.ok(!text.includes("\u001b"), "no colour codes when colour is off");
|
|
161
|
+
});
|
|
162
|
+
it("colours when asked to", () => {
|
|
163
|
+
let text = "";
|
|
164
|
+
new ConsoleReporter({ write: (t) => (text += t), color: true }).handle({
|
|
165
|
+
type: "run-end",
|
|
166
|
+
summary,
|
|
167
|
+
});
|
|
168
|
+
assert.ok(text.includes("\u001b[31m"));
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RunEvent } from "../runner/run.js";
|
|
2
|
+
export interface ConsoleReporterOptions {
|
|
3
|
+
write: (text: string) => void;
|
|
4
|
+
/** Colour with ANSI codes */
|
|
5
|
+
color: boolean;
|
|
6
|
+
}
|
|
7
|
+
/** Prints a run for a person: one line per test as it ends, then every failure in full and a count */
|
|
8
|
+
export declare class ConsoleReporter {
|
|
9
|
+
private readonly write;
|
|
10
|
+
private readonly paint;
|
|
11
|
+
constructor({ write, color }: ConsoleReporterOptions);
|
|
12
|
+
handle(event: RunEvent): void;
|
|
13
|
+
private line;
|
|
14
|
+
private summary;
|
|
15
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const seconds = (ms) => `${(ms / 1000).toFixed(1)}s`;
|
|
2
|
+
/** Prints a run for a person: one line per test as it ends, then every failure in full and a count */
|
|
3
|
+
export class ConsoleReporter {
|
|
4
|
+
constructor({ write, color }) {
|
|
5
|
+
this.write = write;
|
|
6
|
+
this.paint = (code, text) => color ? `\u001b[${code}m${text}\u001b[0m` : text;
|
|
7
|
+
}
|
|
8
|
+
handle(event) {
|
|
9
|
+
switch (event.type) {
|
|
10
|
+
case "run-start":
|
|
11
|
+
this.write(`Running ${event.total} ${event.total === 1 ? "test" : "tests"} against ${event.environment}\n`);
|
|
12
|
+
return;
|
|
13
|
+
case "test-end":
|
|
14
|
+
this.write(` ${this.line(event.name, event.result)}\n`);
|
|
15
|
+
return;
|
|
16
|
+
case "run-end":
|
|
17
|
+
this.summary(event.summary);
|
|
18
|
+
return;
|
|
19
|
+
case "test-start":
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
line(name, result) {
|
|
24
|
+
const took = "duration" in result
|
|
25
|
+
? ` ${this.paint(90, `(${seconds(result.duration)})`)}`
|
|
26
|
+
: "";
|
|
27
|
+
return result.status === "success"
|
|
28
|
+
? `${this.paint(32, "✔")} ${name}${took}`
|
|
29
|
+
: `${this.paint(31, "✖")} ${name}${took}`;
|
|
30
|
+
}
|
|
31
|
+
summary(summary) {
|
|
32
|
+
const failed = summary.tests.filter((test) => test.status !== "success");
|
|
33
|
+
for (const test of failed) {
|
|
34
|
+
if (!("name" in test))
|
|
35
|
+
continue;
|
|
36
|
+
const message = "message" in test ? test.message : "did not finish";
|
|
37
|
+
this.write(`\n${this.paint(31, `${test.name}`)}\n`);
|
|
38
|
+
this.write(`${message
|
|
39
|
+
.split("\n")
|
|
40
|
+
.map((line) => ` ${line}`)
|
|
41
|
+
.join("\n")}\n`);
|
|
42
|
+
if ("file" in test)
|
|
43
|
+
this.write(` ${this.paint(90, test.file)}\n`);
|
|
44
|
+
}
|
|
45
|
+
const passed = summary.tests.length - failed.length;
|
|
46
|
+
const parts = [
|
|
47
|
+
failed.length > 0 && this.paint(31, `${failed.length} failed`),
|
|
48
|
+
this.paint(32, `${passed} passed`),
|
|
49
|
+
].filter(Boolean);
|
|
50
|
+
this.write(`\n${parts.join(", ")} ${this.paint(90, `(${seconds(summary.duration)})`)}\n`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** The files `samurai init` writes, by path relative to the project folder */
|
|
2
|
+
export declare function scaffold(name: string): Record<string, string>;
|
|
3
|
+
/** Writes the scaffold into `dir`, never overwriting a file. Returns what was written and what was left alone */
|
|
4
|
+
export declare function initProject(dir: string): {
|
|
5
|
+
written: string[];
|
|
6
|
+
skipped: string[];
|
|
7
|
+
};
|
package/dist/cli/init.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const PACKAGE = "@itmetsam/samurai-framework";
|
|
4
|
+
/** The files `samurai init` writes, by path relative to the project folder */
|
|
5
|
+
export function scaffold(name) {
|
|
6
|
+
return {
|
|
7
|
+
"package.json": JSON.stringify({
|
|
8
|
+
name,
|
|
9
|
+
version: "0.1.0",
|
|
10
|
+
private: true,
|
|
11
|
+
type: "module",
|
|
12
|
+
scripts: { test: "samurai run" },
|
|
13
|
+
dependencies: { [PACKAGE]: "^0.1.0" },
|
|
14
|
+
devDependencies: { tsx: "^4.21.0" },
|
|
15
|
+
}, null, 2) + "\n",
|
|
16
|
+
"samurai.config.ts": `import { defineConfig } from "${PACKAGE}";
|
|
17
|
+
|
|
18
|
+
export default defineConfig({
|
|
19
|
+
srcDir: "./tests",
|
|
20
|
+
browser: "firefox",
|
|
21
|
+
timeout: 30000,
|
|
22
|
+
environments: {
|
|
23
|
+
local: { baseURL: "http://localhost:3000" },
|
|
24
|
+
},
|
|
25
|
+
defaultEnvironment: "local",
|
|
26
|
+
});
|
|
27
|
+
`,
|
|
28
|
+
"tests/example.spec.ts": `import { expect, test } from "${PACKAGE}";
|
|
29
|
+
|
|
30
|
+
test("example.com has its heading", async ({ page }) => {
|
|
31
|
+
await page.goto("https://example.com");
|
|
32
|
+
await expect(page.getByRole("heading", { name: "Example Domain" })).toBeVisible();
|
|
33
|
+
});
|
|
34
|
+
`,
|
|
35
|
+
".env.example": `# Copy to .env.<environment>, e.g. .env.local. Never commit real values.
|
|
36
|
+
# Tests read these as secrets.TEST_PASSWORD; real environment variables win over this file.
|
|
37
|
+
SAMURAI_SECRET_TEST_PASSWORD=
|
|
38
|
+
`,
|
|
39
|
+
".gitignore": "node_modules\nresult\nlogs\nbrowsers\n.env\n.env.*\n!.env.example\n",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** Writes the scaffold into `dir`, never overwriting a file. Returns what was written and what was left alone */
|
|
43
|
+
export function initProject(dir) {
|
|
44
|
+
const root = path.resolve(dir);
|
|
45
|
+
const written = [];
|
|
46
|
+
const skipped = [];
|
|
47
|
+
for (const [file, content] of Object.entries(scaffold(path
|
|
48
|
+
.basename(root)
|
|
49
|
+
.toLowerCase()
|
|
50
|
+
.replace(/[^a-z0-9-]+/g, "-") || "samurai-tests"))) {
|
|
51
|
+
const target = path.join(root, file);
|
|
52
|
+
if (existsSync(target)) {
|
|
53
|
+
skipped.push(file);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
57
|
+
writeFileSync(target, content);
|
|
58
|
+
written.push(file);
|
|
59
|
+
}
|
|
60
|
+
return { written, skipped };
|
|
61
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const USAGE = "samurai: browser tests over WebDriver BiDi\n\nUsage:\n samurai run [options] Run the tests (the default command)\n samurai list [options] List the tests without running them\n samurai record <spec> Open a browser and record what you do into a spec file\n samurai init [folder] Scaffold a project: config, an example test, .env.example\n\nOptions for run and list:\n --env <name> Environment to run against (or SAMURAI_ENV)\n --file <spec> Only this spec file; repeatable\n --grep <text> Only tests whose full name contains the text\n --json Machine-readable output: JSON lines for run, one array for list\n\nOptions for run:\n --timeout <ms> Time one test may take\n --expect-timeout <ms> Time actions and assertions retry\n --headless Run the browser without a window\n --port <n> Browser debugging port (default 9223)\n\nOptions for record (also --env, --port, --json):\n --new <title> Add a test with this title (creates the spec file if needed)\n --test <name|index> Record into this test; not needed when the file has one\n --at <n> Insert from step n (default: after the last step)\n --url <url> Page to start on (default: the environment's baseURL)\n Stop with Ctrl+C or by closing the browser window.\n\n -h, --help Show this help\n -v, --version Show the version\n\nExit code: 0 when every test passed, 1 when a test failed, 2 for a usage or configuration error.\nRun it in the project folder, the one holding samurai.config.ts.\n";
|
|
2
|
+
export interface Io {
|
|
3
|
+
out: (text: string) => void;
|
|
4
|
+
err: (text: string) => void;
|
|
5
|
+
/** Whether output goes to a terminal that shows colours */
|
|
6
|
+
color: boolean;
|
|
7
|
+
env: Partial<NodeJS.ProcessEnv>;
|
|
8
|
+
/** Aborts a recording (Ctrl+C) */
|
|
9
|
+
signal?: AbortSignal;
|
|
10
|
+
}
|
|
11
|
+
/** The command line as `samurai` runs it. Returns the exit code */
|
|
12
|
+
export declare function main(argv: string[], io: Io): Promise<number>;
|