@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
package/src/browser/pdf.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
2
|
-
import { writeOutput } from "./screenshot.js";
|
|
3
|
-
|
|
4
|
-
/** Paper sizes in centimetres, as the browser wants them */
|
|
5
|
-
const PAPER = {
|
|
6
|
-
Letter: { width: 21.59, height: 27.94 },
|
|
7
|
-
Legal: { width: 21.59, height: 35.56 },
|
|
8
|
-
Tabloid: { width: 27.94, height: 43.18 },
|
|
9
|
-
Ledger: { width: 43.18, height: 27.94 },
|
|
10
|
-
A0: { width: 84.1, height: 118.9 },
|
|
11
|
-
A1: { width: 59.4, height: 84.1 },
|
|
12
|
-
A2: { width: 42, height: 59.4 },
|
|
13
|
-
A3: { width: 29.7, height: 42 },
|
|
14
|
-
A4: { width: 21, height: 29.7 },
|
|
15
|
-
A5: { width: 14.8, height: 21 },
|
|
16
|
-
A6: { width: 10.5, height: 14.8 },
|
|
17
|
-
} as const;
|
|
18
|
-
|
|
19
|
-
export type PaperFormat = keyof typeof PAPER;
|
|
20
|
-
|
|
21
|
-
export interface PdfOptions {
|
|
22
|
-
/** Also write the PDF here (folders are created) */
|
|
23
|
-
path?: string;
|
|
24
|
-
landscape?: boolean;
|
|
25
|
-
/** 0.1 to 2 */
|
|
26
|
-
scale?: number;
|
|
27
|
-
/** Pages to print, e.g. `["1-3", 5]` */
|
|
28
|
-
pageRanges?: (string | number)[];
|
|
29
|
-
/** Page margins in centimetres */
|
|
30
|
-
margin?: { top?: number; bottom?: number; left?: number; right?: number };
|
|
31
|
-
/** Print background colours and images */
|
|
32
|
-
printBackground?: boolean;
|
|
33
|
-
/** Paper size. Without one the browser's default is used */
|
|
34
|
-
format?: PaperFormat;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Renders the page as a PDF and returns it (also written to `options.path`) */
|
|
38
|
-
export async function printPdf(
|
|
39
|
-
connector: BiDiConnector,
|
|
40
|
-
context: string,
|
|
41
|
-
options: PdfOptions = {},
|
|
42
|
-
): Promise<Buffer> {
|
|
43
|
-
const { scale, format } = options;
|
|
44
|
-
if (scale !== undefined && !(scale >= 0.1 && scale <= 2)) {
|
|
45
|
-
throw new RangeError("pdf(): scale must be between 0.1 and 2");
|
|
46
|
-
}
|
|
47
|
-
if (format !== undefined && !(format in PAPER)) {
|
|
48
|
-
throw new RangeError(`pdf(): unknown paper format "${format}"; use one of ${Object.keys(PAPER).join(", ")}`);
|
|
49
|
-
}
|
|
50
|
-
const { data } = await connector.send("browsingContext.print", {
|
|
51
|
-
context,
|
|
52
|
-
...(options.landscape && { orientation: "landscape" as const }),
|
|
53
|
-
...(scale !== undefined && { scale }),
|
|
54
|
-
...(options.pageRanges && { pageRanges: options.pageRanges }),
|
|
55
|
-
...(options.margin && { margin: options.margin }),
|
|
56
|
-
...(options.printBackground && { background: true }),
|
|
57
|
-
...(format && { page: { ...PAPER[format] } }),
|
|
58
|
-
});
|
|
59
|
-
const pdf = Buffer.from(data, "base64");
|
|
60
|
-
await writeOutput(options.path, pdf);
|
|
61
|
-
return pdf;
|
|
62
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
4
|
-
|
|
5
|
-
export interface ScreenshotOptions {
|
|
6
|
-
/** Also write the image here (folders are created) */
|
|
7
|
-
path?: string;
|
|
8
|
-
/** The whole document instead of the viewport */
|
|
9
|
-
fullPage?: boolean;
|
|
10
|
-
/** Only this part: in viewport coordinates, or document coordinates with `fullPage` */
|
|
11
|
-
clip?: { x: number; y: number; width: number; height: number };
|
|
12
|
-
/** @default "png" */
|
|
13
|
-
type?: "png" | "jpeg";
|
|
14
|
-
/** 0 to 100, jpeg only */
|
|
15
|
-
quality?: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** The options a single element's screenshot takes: the image format and where to write it */
|
|
19
|
-
export type ElementScreenshotOptions = Pick<ScreenshotOptions, "path" | "type" | "quality">;
|
|
20
|
-
|
|
21
|
-
const MIME = { png: "image/png", jpeg: "image/jpeg" } as const;
|
|
22
|
-
|
|
23
|
-
/** The `format` of a capture command; validates the type/quality combination */
|
|
24
|
-
function imageFormat({ type, quality }: Pick<ScreenshotOptions, "type" | "quality">) {
|
|
25
|
-
if (quality !== undefined) {
|
|
26
|
-
if (type !== "jpeg") throw new RangeError("screenshot(): quality only applies to jpeg");
|
|
27
|
-
if (!Number.isFinite(quality) || quality < 0 || quality > 100) {
|
|
28
|
-
throw new RangeError("screenshot(): quality must be between 0 and 100");
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
if (type === undefined) return undefined;
|
|
32
|
-
return { type: MIME[type], ...(quality !== undefined && { quality: quality / 100 }) };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Captures the page (or, with `element`, that element) and returns the image. Writes it to `options.path` too.
|
|
37
|
-
* The browser answers Base64.
|
|
38
|
-
*/
|
|
39
|
-
export async function takeScreenshot(
|
|
40
|
-
connector: BiDiConnector,
|
|
41
|
-
context: string,
|
|
42
|
-
options: ScreenshotOptions = {},
|
|
43
|
-
element?: { sharedId: string },
|
|
44
|
-
): Promise<Buffer> {
|
|
45
|
-
const format = imageFormat(options);
|
|
46
|
-
const clip = element
|
|
47
|
-
? { type: "element" as const, element: { sharedId: element.sharedId } }
|
|
48
|
-
: options.clip && { type: "box" as const, ...options.clip };
|
|
49
|
-
const { data } = await connector.send("browsingContext.captureScreenshot", {
|
|
50
|
-
context,
|
|
51
|
-
...(options.fullPage && { origin: "document" as const }),
|
|
52
|
-
...(format && { format }),
|
|
53
|
-
...(clip && { clip }),
|
|
54
|
-
});
|
|
55
|
-
const image = Buffer.from(data, "base64");
|
|
56
|
-
await writeOutput(options.path, image);
|
|
57
|
-
return image;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** Writes `data` to `file`, creating the folders; nothing happens without a path */
|
|
61
|
-
export async function writeOutput(file: string | undefined, data: Buffer): Promise<void> {
|
|
62
|
-
if (!file) return;
|
|
63
|
-
await mkdir(path.dirname(file), { recursive: true });
|
|
64
|
-
await writeFile(file, data);
|
|
65
|
-
}
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { createServer, type Server } from "node:http";
|
|
4
|
-
import type { AddressInfo, Socket } from "node:net";
|
|
5
|
-
import { tmpdir } from "node:os";
|
|
6
|
-
import path from "node:path";
|
|
7
|
-
import { after, before, test } from "node:test";
|
|
8
|
-
import { callFunction } from "../script/call-function.js";
|
|
9
|
-
import { setContent } from "../testing/browser-fixture.js";
|
|
10
|
-
import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
11
|
-
import { Browser } from "./browser.js";
|
|
12
|
-
import { DownloadError } from "./download.js";
|
|
13
|
-
import type Page from "./page.js";
|
|
14
|
-
|
|
15
|
-
let browser: Browser;
|
|
16
|
-
let page: Page;
|
|
17
|
-
let server: Server;
|
|
18
|
-
let base: string;
|
|
19
|
-
let downloadsDir: string;
|
|
20
|
-
const sockets = new Set<Socket>();
|
|
21
|
-
const uploads = mkdtempSync(path.join(tmpdir(), "samurai-uploads-"));
|
|
22
|
-
|
|
23
|
-
before(async () => {
|
|
24
|
-
server = createServer((req, res) => {
|
|
25
|
-
const p = (req.url ?? "").split("?")[0];
|
|
26
|
-
if (p === "/file") {
|
|
27
|
-
res.writeHead(200, { "content-type": "text/csv", "content-disposition": 'attachment; filename="report.csv"' }).end("a,b\n1,2\n");
|
|
28
|
-
} else if (p === "/broken") {
|
|
29
|
-
res.writeHead(200, { "content-type": "application/octet-stream", "content-disposition": 'attachment; filename="broken.bin"', "content-length": "100000" });
|
|
30
|
-
res.write("partial");
|
|
31
|
-
setTimeout(() => res.destroy(), 150);
|
|
32
|
-
} else if (p === "/frame") {
|
|
33
|
-
res.setHeader("content-type", "text/html").end(`<body><a id="dl" href="/file">in frame</a></body>`);
|
|
34
|
-
} else {
|
|
35
|
-
res.setHeader("content-type", "text/html").end("<!doctype html><body>page</body>");
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
server.on("connection", (socket) => {
|
|
39
|
-
sockets.add(socket);
|
|
40
|
-
socket.on("close", () => sockets.delete(socket));
|
|
41
|
-
});
|
|
42
|
-
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
43
|
-
base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
|
|
44
|
-
({ browser, page } = await Browser.launch("firefox", { port: 9250, headless: true }));
|
|
45
|
-
downloadsDir = browser.downloadsDir!;
|
|
46
|
-
await page.navigateTo(`${base}/`);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
after(async () => {
|
|
50
|
-
await page.dispose();
|
|
51
|
-
await browser?.close();
|
|
52
|
-
sockets.forEach((socket) => socket.destroy());
|
|
53
|
-
await new Promise((resolve) => server.close(resolve));
|
|
54
|
-
rmSync(uploads, { recursive: true, force: true });
|
|
55
|
-
if (downloadsDir) rmSync(downloadsDir, { recursive: true, force: true });
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
const OPTIONS = { timeout: 40000 };
|
|
59
|
-
const connector = () => (page as unknown as { biDiConnector: BiDiConnector }).biDiConnector;
|
|
60
|
-
const evaluate = <T>(source: string) => callFunction<T>(connector(), page.contextId, source);
|
|
61
|
-
const upload = (name: string, content = name) => {
|
|
62
|
-
const full = path.join(uploads, name);
|
|
63
|
-
writeFileSync(full, content);
|
|
64
|
-
return full;
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
test("the browser saves downloads in the folder the framework made for it", OPTIONS, async () => {
|
|
68
|
-
assert.ok(downloadsDir, "the browser could be told where to save");
|
|
69
|
-
assert.ok(existsSync(downloadsDir));
|
|
70
|
-
assert.match(path.basename(downloadsDir), /^dl-/);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test("waitForDownload reports the download when it starts; path() gives the saved file", OPTIONS, async () => {
|
|
74
|
-
await setContent(page, `<a id="dl" href="${base}/file">download</a>`);
|
|
75
|
-
const download = await page.waitForDownload({ trigger: () => page.locator("a[@id='dl']").click({ timeout: 5000 }) });
|
|
76
|
-
assert.equal(download.suggestedFilename(), "report.csv");
|
|
77
|
-
assert.equal(download.url(), `${base}/file`);
|
|
78
|
-
const file = await download.path();
|
|
79
|
-
assert.equal(path.dirname(file), downloadsDir);
|
|
80
|
-
assert.equal(readFileSync(file, "utf8"), "a,b\n1,2\n");
|
|
81
|
-
assert.equal(await download.failure(), null);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test("a second download with the same name is saved under another name", OPTIONS, async () => {
|
|
85
|
-
await setContent(page, `<a id="dl" href="${base}/file?again">download</a>`);
|
|
86
|
-
const download = await page.waitForDownload({ trigger: () => page.locator("a[@id='dl']").click({ timeout: 5000 }) });
|
|
87
|
-
const file = await download.path();
|
|
88
|
-
assert.notEqual(path.basename(file), "report.csv");
|
|
89
|
-
assert.match(path.basename(file), /^report.*\.csv$/);
|
|
90
|
-
assert.ok(readdirSync(downloadsDir).length >= 2);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test("a download the server breaks off is cancelled", OPTIONS, async () => {
|
|
94
|
-
await setContent(page, `<a id="broken" href="${base}/broken">broken</a>`);
|
|
95
|
-
const download = await page.waitForDownload({ trigger: () => page.locator("a[@id='broken']").click({ timeout: 5000 }) });
|
|
96
|
-
assert.equal(download.suggestedFilename(), "broken.bin");
|
|
97
|
-
await assert.rejects(download.path(), (err: unknown) => {
|
|
98
|
-
assert.ok(err instanceof DownloadError);
|
|
99
|
-
assert.equal(err.reason, "canceled");
|
|
100
|
-
assert.equal(err.url, `${base}/broken`);
|
|
101
|
-
return true;
|
|
102
|
-
});
|
|
103
|
-
assert.equal(await download.failure(), "canceled");
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test("a download started from an iframe belongs to the page", OPTIONS, async () => {
|
|
107
|
-
await page.navigateTo(`${base}/?frames`);
|
|
108
|
-
await evaluate(`() => { const f = document.body.appendChild(document.createElement("iframe")); f.src = "/frame"; }`);
|
|
109
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
110
|
-
const seen: string[] = [];
|
|
111
|
-
page.on("download", (download) => seen.push(download.suggestedFilename()));
|
|
112
|
-
// click inside the frame through the script: pointer input is per top-level context
|
|
113
|
-
await evaluate(`() => { document.querySelector("iframe").contentDocument.getElementById("dl").click(); }`);
|
|
114
|
-
const deadline = Date.now() + 5000;
|
|
115
|
-
while (seen.length === 0 && Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, 50));
|
|
116
|
-
assert.equal(seen.length, 1);
|
|
117
|
-
assert.match(seen[0]!, /^report.*\.csv$/, "Firefox's suggested name already carries its (n) suffix when the name is taken");
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test("waitForDownload times out with a message of its own", OPTIONS, async () => {
|
|
121
|
-
await assert.rejects(page.waitForDownload({ timeout: 300 }), /waitForDownload\(\): no download started within 300ms/);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test("a file input's picker is reported, and setFiles fills the input", OPTIONS, async () => {
|
|
125
|
-
await setContent(
|
|
126
|
-
page,
|
|
127
|
-
`<input type="file" id="f"><div id="out"></div>`,
|
|
128
|
-
`const f = document.getElementById("f");
|
|
129
|
-
f.addEventListener("change", () => { document.getElementById("out").textContent = [...f.files].map((x) => x.name).join(","); });`,
|
|
130
|
-
);
|
|
131
|
-
const picture = upload("avatar.png");
|
|
132
|
-
const chooser = await page.waitForFileChooser({ trigger: () => page.locator("input[@id='f']").click({ timeout: 5000 }) });
|
|
133
|
-
assert.equal(chooser.isMultiple(), false);
|
|
134
|
-
await chooser.setFiles(picture);
|
|
135
|
-
assert.equal(await evaluate<string>(`() => document.getElementById("out").textContent`), "avatar.png");
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test("a picker that takes several files says so, and takes them", OPTIONS, async () => {
|
|
139
|
-
await setContent(page, `<input type="file" id="m" multiple>`);
|
|
140
|
-
const chooser = await page.waitForFileChooser({ trigger: () => page.locator("input[@id='m']").click({ timeout: 5000 }) });
|
|
141
|
-
assert.equal(chooser.isMultiple(), true);
|
|
142
|
-
await chooser.setFiles([upload("a.csv"), upload("b.csv")]);
|
|
143
|
-
assert.deepEqual(await evaluate<string[]>(`() => [...document.getElementById("m").files].map((f) => f.name)`), ["a.csv", "b.csv"]);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test("a single-file picker refuses two files", OPTIONS, async () => {
|
|
147
|
-
await setContent(page, `<input type="file" id="f">`);
|
|
148
|
-
const chooser = await page.waitForFileChooser({ trigger: () => page.locator("input[@id='f']").click({ timeout: 5000 }) });
|
|
149
|
-
await assert.rejects(chooser.setFiles([upload("x1.txt"), upload("x2.txt")]), /file chooser does not accept multiple files/);
|
|
150
|
-
await assert.rejects(chooser.setFiles(path.join(uploads, "missing.txt")), /file not found/);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
test("a click on the label of a hidden input opens the picker as well", OPTIONS, async () => {
|
|
154
|
-
await setContent(page, `<label id="lab" for="f" style="display:inline-block;padding:6px">Choose</label><input type="file" id="f" style="display:none">`);
|
|
155
|
-
const chooser = await page.waitForFileChooser({ trigger: () => page.locator("label[@id='lab']").click({ timeout: 5000 }) });
|
|
156
|
-
await chooser.setFiles(upload("hidden.txt"));
|
|
157
|
-
assert.deepEqual(await evaluate<string[]>(`() => [...document.getElementById("f").files].map((f) => f.name)`), ["hidden.txt"]);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
test("a picker nobody waits for does not hang the page", OPTIONS, async () => {
|
|
161
|
-
await setContent(page, `<input type="file" id="f"><button id="b">next</button>`);
|
|
162
|
-
await page.locator("input[@id='f']").click({ timeout: 5000 });
|
|
163
|
-
await page.locator("button[@id='b']").click({ timeout: 5000 });
|
|
164
|
-
assert.equal(await page.locator("button[@id='b']").textContent(), "next");
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
test("page.on(filechooser) sees every picker", OPTIONS, async () => {
|
|
168
|
-
await setContent(page, `<input type="file" id="f">`);
|
|
169
|
-
let count = 0;
|
|
170
|
-
page.on("filechooser", () => count++);
|
|
171
|
-
await page.locator("input[@id='f']").click({ timeout: 5000 });
|
|
172
|
-
await page.locator("input[@id='f']").click({ timeout: 5000 });
|
|
173
|
-
const deadline = Date.now() + 3000;
|
|
174
|
-
while (count < 2 && Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, 25));
|
|
175
|
-
assert.equal(count, 2);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test("waitForFileChooser times out with a message of its own", OPTIONS, async () => {
|
|
179
|
-
await assert.rejects(page.waitForFileChooser({ timeout: 300 }), /waitForFileChooser\(\): no file chooser opened within 300ms/);
|
|
180
|
-
});
|
package/src/config/config.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import type { SamuraiTestConfig } from "../types/config.js";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import logger from "../logger/index.js";
|
|
4
|
-
import { pathToFileURL } from "url";
|
|
5
|
-
|
|
6
|
-
/** The whole `samurai.config.ts` of the current working directory */
|
|
7
|
-
export async function loadConfig(): Promise<SamuraiTestConfig> {
|
|
8
|
-
const configUrl = path.join(process.cwd(), "samurai.config.ts");
|
|
9
|
-
logger.debug("Reading config from: %s", configUrl);
|
|
10
|
-
|
|
11
|
-
const config = (await import(pathToFileURL(configUrl).href)) as {
|
|
12
|
-
default?: SamuraiTestConfig;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
if (!config.default) {
|
|
16
|
-
throw new Error(
|
|
17
|
-
"samurai.config.ts does not export defineConfig as default!",
|
|
18
|
-
);
|
|
19
|
-
}
|
|
20
|
-
return config.default;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export async function readConfig<K extends keyof SamuraiTestConfig>(
|
|
24
|
-
key: K,
|
|
25
|
-
): Promise<SamuraiTestConfig[K]> {
|
|
26
|
-
const value = (await loadConfig())[key];
|
|
27
|
-
logger.debug("Resolved config value: ", { value });
|
|
28
|
-
return value;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function defineConfig(config: SamuraiTestConfig) {
|
|
32
|
-
return config;
|
|
33
|
-
}
|
package/src/config/default.ts
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import { after, describe, test } from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import { inspect } from "node:util";
|
|
7
|
-
import { createEnvFixture, UnknownVariableError } from "./variables.js";
|
|
8
|
-
import { createSecretsFixture, loadSecrets, MissingSecretError } from "./secrets.js";
|
|
9
|
-
|
|
10
|
-
describe("env fixture", () => {
|
|
11
|
-
const env = createEnvFixture("staging", { customerEmail: "sam@example.test", newCheckout: true });
|
|
12
|
-
|
|
13
|
-
test("reads variables", () => {
|
|
14
|
-
assert.equal(env.customerEmail, "sam@example.test");
|
|
15
|
-
const { newCheckout } = env;
|
|
16
|
-
assert.equal(newCheckout, true);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test("an unknown variable throws", () => {
|
|
20
|
-
assert.throws(
|
|
21
|
-
() => env.customerEmial,
|
|
22
|
-
(err) =>
|
|
23
|
-
err instanceof UnknownVariableError &&
|
|
24
|
-
err.message === 'Variable "customerEmial" is not defined in environment "staging"',
|
|
25
|
-
);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test("is read-only", () => {
|
|
29
|
-
assert.throws(() => {
|
|
30
|
-
(env as Record<string, unknown>).customerEmail = "x";
|
|
31
|
-
}, TypeError);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("behaves like a plain object for JSON, inspect and await", async () => {
|
|
35
|
-
assert.equal(JSON.stringify(env), '{"customerEmail":"sam@example.test","newCheckout":true}');
|
|
36
|
-
assert.match(inspect(env), /customerEmail/);
|
|
37
|
-
assert.equal(await env, env);
|
|
38
|
-
assert.equal("customerEmail" in env, true);
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
describe("loadSecrets", () => {
|
|
43
|
-
const dir = mkdtempSync(path.join(tmpdir(), "samurai-secrets-"));
|
|
44
|
-
after(() => rmSync(dir, { recursive: true, force: true }));
|
|
45
|
-
writeFileSync(
|
|
46
|
-
path.join(dir, ".env.staging"),
|
|
47
|
-
[
|
|
48
|
-
"# staging secrets",
|
|
49
|
-
"",
|
|
50
|
-
"SAMURAI_SECRET_TEST_CARD=4242424242424242",
|
|
51
|
-
'SAMURAI_SECRET_CUSTOMER_PW="pa ss # word"',
|
|
52
|
-
"SAMURAI_SECRET_FROM_FILE=file",
|
|
53
|
-
"OTHER_VALUE=ignored",
|
|
54
|
-
].join("\n"),
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
test("parses comments and quotes in .env files", () => {
|
|
58
|
-
const secrets = loadSecrets("staging", { cwd: dir, env: {} as unknown as NodeJS.ProcessEnv });
|
|
59
|
-
assert.deepEqual(Object.fromEntries(secrets), {
|
|
60
|
-
TEST_CARD: "4242424242424242",
|
|
61
|
-
CUSTOMER_PW: "pa ss # word",
|
|
62
|
-
FROM_FILE: "file",
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("env vars beat the file and non-prefixed keys are ignored", () => {
|
|
67
|
-
const secrets = loadSecrets("staging", {
|
|
68
|
-
cwd: dir,
|
|
69
|
-
env: { SAMURAI_SECRET_FROM_FILE: "env", SAMURAI_SECRET_ONLY_ENV: "x", PATH: "/bin" } as unknown as NodeJS.ProcessEnv,
|
|
70
|
-
});
|
|
71
|
-
assert.equal(secrets.get("FROM_FILE"), "env");
|
|
72
|
-
assert.equal(secrets.get("ONLY_ENV"), "x");
|
|
73
|
-
assert.equal(secrets.has("PATH"), false);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test("an empty value counts as unset and does not override a file value", () => {
|
|
77
|
-
const empty = mkdtempSync(path.join(tmpdir(), "samurai-secrets-empty-"));
|
|
78
|
-
try {
|
|
79
|
-
writeFileSync(path.join(empty, ".env.staging"), "SAMURAI_SECRET_A=\nSAMURAI_SECRET_B=fromfile\nSAMURAI_SECRET_C=\n");
|
|
80
|
-
const secrets = loadSecrets("staging", {
|
|
81
|
-
cwd: empty,
|
|
82
|
-
env: { SAMURAI_SECRET_B: "", SAMURAI_SECRET_C: "fromenv" } as unknown as NodeJS.ProcessEnv,
|
|
83
|
-
});
|
|
84
|
-
assert.deepEqual(Object.fromEntries(secrets), { B: "fromfile", C: "fromenv" });
|
|
85
|
-
assert.equal(secrets.has("A"), false);
|
|
86
|
-
} finally {
|
|
87
|
-
rmSync(empty, { recursive: true, force: true });
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test("a missing file is fine", () => {
|
|
92
|
-
assert.deepEqual(Object.fromEntries(loadSecrets("dev", { cwd: dir, env: { SAMURAI_SECRET_A: "1" } as unknown as NodeJS.ProcessEnv })), { A: "1" });
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test("names that are not UPPER_SNAKE_CASE are skipped", () => {
|
|
96
|
-
assert.equal(loadSecrets("dev", { cwd: dir, env: { SAMURAI_SECRET_lower: "x" } as unknown as NodeJS.ProcessEnv }).size, 0);
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
describe("secrets fixture", () => {
|
|
101
|
-
const secrets = createSecretsFixture("staging", new Map([["TEST_CARD", "4242424242424242"]]));
|
|
102
|
-
|
|
103
|
-
test("reads a secret", () => {
|
|
104
|
-
assert.equal(secrets.TEST_CARD, "4242424242424242");
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test("a missing secret throws", () => {
|
|
108
|
-
assert.throws(
|
|
109
|
-
() => secrets.CUSTOMER_PW,
|
|
110
|
-
(err) =>
|
|
111
|
-
err instanceof MissingSecretError &&
|
|
112
|
-
err.message === 'Secret "CUSTOMER_PW" is not set (expected env var SAMURAI_SECRET_CUSTOMER_PW or .env.staging)',
|
|
113
|
-
);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
test("an invalid name throws", () => {
|
|
117
|
-
assert.throws(() => secrets.testCard, { message: 'Invalid secret name "testCard" (use UPPER_SNAKE_CASE)' });
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test("JSON and inspect never show values", () => {
|
|
121
|
-
assert.equal(JSON.stringify(secrets), '{"TEST_CARD":"••••"}');
|
|
122
|
-
assert.equal(JSON.stringify({ secrets }), '{"secrets":{"TEST_CARD":"••••"}}');
|
|
123
|
-
assert.doesNotMatch(inspect(secrets), /4242/);
|
|
124
|
-
assert.match(inspect(secrets), /TEST_CARD/);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
test("is read-only", () => {
|
|
128
|
-
assert.throws(() => {
|
|
129
|
-
(secrets as Record<string, string>).TEST_CARD = "x";
|
|
130
|
-
}, TypeError);
|
|
131
|
-
assert.equal(secrets.TEST_CARD, "4242424242424242");
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
test("await does not treat it as a thenable", async () => {
|
|
135
|
-
assert.equal(await secrets, secrets);
|
|
136
|
-
});
|
|
137
|
-
});
|
package/src/config/mask.test.ts
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import { afterEach, test } from "node:test";
|
|
2
|
-
import { inspect } from "node:util";
|
|
3
|
-
import assert from "node:assert/strict";
|
|
4
|
-
import { isShortSecret, MASK, maskDeep, maskFormat, maskText, registerSecret, resetSecrets } from "./mask.js";
|
|
5
|
-
|
|
6
|
-
afterEach(() => resetSecrets());
|
|
7
|
-
|
|
8
|
-
test("nothing registered leaves text alone", () => {
|
|
9
|
-
assert.equal(maskText("card 4242"), "card 4242");
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
test("masks every occurrence of a long value", () => {
|
|
13
|
-
registerSecret("TEST_CARD", "4242424242424242");
|
|
14
|
-
assert.equal(maskText("fill 4242424242424242 then 4242424242424242x"), `fill ${MASK} then ${MASK}x`);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
test("escapes regex characters", () => {
|
|
18
|
-
registerSecret("PW", "p@ss.w*rd+(1)");
|
|
19
|
-
assert.equal(maskText("pw=p@ss.w*rd+(1)!"), `pw=${MASK}!`);
|
|
20
|
-
assert.equal(maskText("p@ssXw*rd+(1)"), "p@ssXw*rd+(1)");
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test("masks the longest secret first", () => {
|
|
24
|
-
registerSecret("SHORT", "secret");
|
|
25
|
-
registerSecret("LONG", "secret-and-more");
|
|
26
|
-
assert.equal(maskText("x secret-and-more y secret"), `x ${MASK} y ${MASK}`);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test("short values only match whole tokens", () => {
|
|
30
|
-
registerSecret("PIN", "12");
|
|
31
|
-
assert.equal(maskText("pin 12, step 12."), `pin ${MASK}, step ${MASK}.`);
|
|
32
|
-
assert.equal(maskText("2012 and a12b"), "2012 and a12b");
|
|
33
|
-
assert.equal(maskText("12"), MASK);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test("empty values are ignored", () => {
|
|
37
|
-
registerSecret("EMPTY", "");
|
|
38
|
-
assert.equal(maskText("anything"), "anything");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test("isShortSecret", () => {
|
|
42
|
-
assert.equal(isShortSecret("abc"), true);
|
|
43
|
-
assert.equal(isShortSecret("abcd"), false);
|
|
44
|
-
assert.equal(isShortSecret(""), false);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("maskDeep masks strings in nested objects and arrays and returns a copy", () => {
|
|
48
|
-
registerSecret("TEST_CARD", "4242424242424242");
|
|
49
|
-
const result = {
|
|
50
|
-
message: "expected 4242424242424242",
|
|
51
|
-
expected: "4242424242424242",
|
|
52
|
-
duration: 12,
|
|
53
|
-
logs: [{ text: "typed 4242424242424242", timestamp: 1 }],
|
|
54
|
-
};
|
|
55
|
-
const masked = maskDeep(result);
|
|
56
|
-
assert.deepEqual(masked, {
|
|
57
|
-
message: `expected ${MASK}`,
|
|
58
|
-
expected: MASK,
|
|
59
|
-
duration: 12,
|
|
60
|
-
logs: [{ text: `typed ${MASK}`, timestamp: 1 }],
|
|
61
|
-
});
|
|
62
|
-
assert.equal(result.expected, "4242424242424242");
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test("maskFormat masks the message and metadata of a log entry", () => {
|
|
66
|
-
registerSecret("TEST_CARD", "4242424242424242");
|
|
67
|
-
const info = maskFormat().transform({
|
|
68
|
-
level: "info",
|
|
69
|
-
message: "filled 4242424242424242",
|
|
70
|
-
meta: { value: "4242424242424242" },
|
|
71
|
-
}) as unknown as { message: string; meta: { value: string } };
|
|
72
|
-
assert.equal(info.message, `filled ${MASK}`);
|
|
73
|
-
assert.equal(info.meta.value, MASK);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
class Wrapper {
|
|
77
|
-
constructor(public v: string) {}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
test("maskDeep masks inside class instances, toJSON objects and null-prototype objects", () => {
|
|
81
|
-
registerSecret("TEST_CARD", "4242424242424242");
|
|
82
|
-
const input = new Wrapper("4242424242424242");
|
|
83
|
-
const out = maskDeep({ w: input, u: new URL("https://x.test/?c=4242424242424242") }) as unknown as {
|
|
84
|
-
w: { v: string };
|
|
85
|
-
u: string;
|
|
86
|
-
};
|
|
87
|
-
assert.deepEqual(out.w, { v: MASK });
|
|
88
|
-
assert.equal(out.u, `https://x.test/?c=${MASK}`);
|
|
89
|
-
assert.equal(input.v, "4242424242424242");
|
|
90
|
-
|
|
91
|
-
const bare = Object.create(null) as Record<string, string>;
|
|
92
|
-
bare.v = "4242424242424242";
|
|
93
|
-
assert.deepEqual(maskDeep({ bare }), { bare: { v: MASK } });
|
|
94
|
-
assert.equal(bare.v, "4242424242424242");
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test("maskDeep masks an Error's name, message and stack", () => {
|
|
98
|
-
registerSecret("TEST_CARD", "4242424242424242");
|
|
99
|
-
const err = new Error("bad 4242424242424242");
|
|
100
|
-
const out = maskDeep({ err }) as unknown as { err: { name: string; message: string; stack: string } };
|
|
101
|
-
assert.equal(out.err.message, `bad ${MASK}`);
|
|
102
|
-
assert.ok(!out.err.stack.includes("4242424242424242"));
|
|
103
|
-
assert.equal(err.message, "bad 4242424242424242");
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test("maskFormat masks class instances, URLs and errors in metadata", () => {
|
|
107
|
-
registerSecret("TEST_CARD", "4242424242424242");
|
|
108
|
-
const info = maskFormat().transform({
|
|
109
|
-
level: "info",
|
|
110
|
-
message: "m",
|
|
111
|
-
meta: { w: new Wrapper("4242424242424242"), u: new URL("https://x.test/4242424242424242"), e: new Error("4242424242424242") },
|
|
112
|
-
}) as unknown as { meta: unknown };
|
|
113
|
-
assert.ok(!JSON.stringify(info.meta).includes("4242424242424242"));
|
|
114
|
-
assert.ok(JSON.stringify(info.meta).includes(MASK));
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
test("circular structures do not throw and are masked elsewhere", () => {
|
|
118
|
-
registerSecret("TEST_CARD", "4242424242424242");
|
|
119
|
-
const circ: Record<string, unknown> = { card: "4242424242424242" };
|
|
120
|
-
circ.self = circ;
|
|
121
|
-
const out = maskDeep(circ) as { card: string; self: unknown };
|
|
122
|
-
assert.equal(out.card, MASK);
|
|
123
|
-
assert.equal(out.self, "[Circular]");
|
|
124
|
-
const info = maskFormat().transform({ level: "info", message: "m", meta: { circ } }) as unknown as {
|
|
125
|
-
meta: { circ: { card: string } };
|
|
126
|
-
};
|
|
127
|
-
assert.equal(info.meta.circ.card, MASK);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test("the same object twice (not circular) is masked both times", () => {
|
|
131
|
-
registerSecret("TEST_CARD", "4242424242424242");
|
|
132
|
-
const shared = { v: "4242424242424242" };
|
|
133
|
-
assert.deepEqual(maskDeep({ a: shared, b: shared }), { a: { v: MASK }, b: { v: MASK } });
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test("escaped and encoded copies of a secret are masked", () => {
|
|
137
|
-
const secret = 'ab\\cd"ef gh';
|
|
138
|
-
registerSecret("PW", secret);
|
|
139
|
-
assert.equal(maskText(JSON.stringify(secret)), `"${MASK}"`);
|
|
140
|
-
assert.equal(maskText(inspect(secret)), `'${MASK}'`);
|
|
141
|
-
assert.equal(maskText(`q=${encodeURIComponent(secret)}`), `q=${MASK}`);
|
|
142
|
-
assert.equal(maskText(secret), MASK);
|
|
143
|
-
});
|