@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,207 @@
|
|
|
1
|
+
import { ElementHandle } from "./element-handle.js";
|
|
2
|
+
/** Deepest nesting `toLocalValue` accepts; a second guard next to cycle detection */
|
|
3
|
+
const MAX_DEPTH = 100;
|
|
4
|
+
/** A remote value that cannot be copied to plain JS (function, window, promise, …), kept by type and handle */
|
|
5
|
+
export class RemoteObject {
|
|
6
|
+
constructor(type, handle,
|
|
7
|
+
/** The deserialized value, when the browser sent one and handles were kept */
|
|
8
|
+
value) {
|
|
9
|
+
this.type = type;
|
|
10
|
+
this.handle = handle;
|
|
11
|
+
this.value = value;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A channel the page function can call as `send(value)`; the browser delivers each value as a
|
|
16
|
+
* `script.message` event. Only meaningful as a preload script or function argument.
|
|
17
|
+
*/
|
|
18
|
+
export class ChannelArg {
|
|
19
|
+
constructor(channel) {
|
|
20
|
+
this.channel = channel;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Serializes `value` for `script.callFunction` arguments; throws TypeError for what cannot cross the wire */
|
|
24
|
+
export function toLocalValue(value) {
|
|
25
|
+
return serialize(value, new Set(), 0);
|
|
26
|
+
}
|
|
27
|
+
function serialize(value, ancestors, depth) {
|
|
28
|
+
if (depth > MAX_DEPTH)
|
|
29
|
+
throw new TypeError("cannot serialize a value that is too deeply nested");
|
|
30
|
+
switch (typeof value) {
|
|
31
|
+
case "undefined":
|
|
32
|
+
return { type: "undefined" };
|
|
33
|
+
case "boolean":
|
|
34
|
+
return { type: "boolean", value };
|
|
35
|
+
case "string":
|
|
36
|
+
return { type: "string", value };
|
|
37
|
+
case "number":
|
|
38
|
+
return { type: "number", value: numberValue(value) };
|
|
39
|
+
case "bigint":
|
|
40
|
+
return { type: "bigint", value: value.toString() };
|
|
41
|
+
case "function":
|
|
42
|
+
case "symbol":
|
|
43
|
+
throw new TypeError(`cannot serialize ${typeof value}`);
|
|
44
|
+
}
|
|
45
|
+
if (value === null)
|
|
46
|
+
return { type: "null" };
|
|
47
|
+
if (value instanceof ChannelArg) {
|
|
48
|
+
// Nodes arrive as references only, which is all the recorder needs
|
|
49
|
+
return {
|
|
50
|
+
type: "channel",
|
|
51
|
+
value: {
|
|
52
|
+
channel: value.channel,
|
|
53
|
+
serializationOptions: { maxDomDepth: 0, maxObjectDepth: 4 },
|
|
54
|
+
ownership: "none",
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (value instanceof ElementHandle) {
|
|
59
|
+
return value.handle === undefined
|
|
60
|
+
? { sharedId: value.sharedId }
|
|
61
|
+
: { sharedId: value.sharedId, handle: value.handle };
|
|
62
|
+
}
|
|
63
|
+
if (value instanceof Date)
|
|
64
|
+
return { type: "date", value: value.toISOString() };
|
|
65
|
+
if (value instanceof RegExp) {
|
|
66
|
+
return {
|
|
67
|
+
type: "regexp",
|
|
68
|
+
value: { pattern: value.source, flags: value.flags },
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const object = value;
|
|
72
|
+
if (ancestors.has(object))
|
|
73
|
+
throw new TypeError("cannot serialize a circular structure");
|
|
74
|
+
ancestors.add(object);
|
|
75
|
+
try {
|
|
76
|
+
const next = (item) => serialize(item, ancestors, depth + 1);
|
|
77
|
+
if (Array.isArray(object))
|
|
78
|
+
return { type: "array", value: object.map(next) };
|
|
79
|
+
if (object instanceof Set)
|
|
80
|
+
return { type: "set", value: [...object].map(next) };
|
|
81
|
+
if (object instanceof Map) {
|
|
82
|
+
return {
|
|
83
|
+
type: "map",
|
|
84
|
+
value: [...object].map(([key, item]) => [
|
|
85
|
+
typeof key === "string" ? key : next(key),
|
|
86
|
+
next(item),
|
|
87
|
+
]),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const prototype = Object.getPrototypeOf(object);
|
|
91
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
92
|
+
throw new TypeError(`cannot serialize ${prototype.constructor?.name ?? "object"}`);
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
type: "object",
|
|
96
|
+
value: Object.entries(object).map(([key, item]) => [key, next(item)]),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
ancestors.delete(object);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function numberValue(value) {
|
|
104
|
+
if (Number.isNaN(value))
|
|
105
|
+
return "NaN";
|
|
106
|
+
if (value === Infinity)
|
|
107
|
+
return "Infinity";
|
|
108
|
+
if (value === -Infinity)
|
|
109
|
+
return "-Infinity";
|
|
110
|
+
if (Object.is(value, -0))
|
|
111
|
+
return "-0";
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
/** Converts a remote value to plain JS; what cannot be copied becomes a `RemoteObject` */
|
|
115
|
+
export function fromRemoteValue(value, options = {}) {
|
|
116
|
+
return deserialize(value, new Map(), options);
|
|
117
|
+
}
|
|
118
|
+
function deserialize(value, known, options) {
|
|
119
|
+
const loose = value;
|
|
120
|
+
switch (value.type) {
|
|
121
|
+
case "undefined":
|
|
122
|
+
return undefined;
|
|
123
|
+
case "null":
|
|
124
|
+
return null;
|
|
125
|
+
case "string":
|
|
126
|
+
case "boolean":
|
|
127
|
+
return value.value;
|
|
128
|
+
case "number":
|
|
129
|
+
return specialNumber(value.value);
|
|
130
|
+
case "bigint":
|
|
131
|
+
return BigInt(value.value);
|
|
132
|
+
case "date":
|
|
133
|
+
return new Date(value.value);
|
|
134
|
+
case "regexp": {
|
|
135
|
+
const { pattern, flags } = loose.value;
|
|
136
|
+
return new RegExp(pattern, flags);
|
|
137
|
+
}
|
|
138
|
+
case "node":
|
|
139
|
+
return new ElementHandle(loose.sharedId ?? "", loose.handle);
|
|
140
|
+
}
|
|
141
|
+
const nested = (item) => deserialize(item, known, options);
|
|
142
|
+
const remember = (result) => {
|
|
143
|
+
if (loose.internalId !== undefined)
|
|
144
|
+
known.set(loose.internalId, result);
|
|
145
|
+
return result;
|
|
146
|
+
};
|
|
147
|
+
const backReference = () => loose.internalId !== undefined && known.has(loose.internalId)
|
|
148
|
+
? { found: true, value: known.get(loose.internalId) }
|
|
149
|
+
: undefined;
|
|
150
|
+
let result;
|
|
151
|
+
switch (value.type) {
|
|
152
|
+
case "array":
|
|
153
|
+
case "set":
|
|
154
|
+
case "object":
|
|
155
|
+
case "map": {
|
|
156
|
+
const reference = backReference();
|
|
157
|
+
if (reference && loose.value === undefined)
|
|
158
|
+
return reference.value;
|
|
159
|
+
if (loose.value === undefined)
|
|
160
|
+
break; // cut off by the serialization depth
|
|
161
|
+
if (value.type === "array") {
|
|
162
|
+
const list = remember([]);
|
|
163
|
+
loose.value.forEach((item) => list.push(nested(item)));
|
|
164
|
+
result = list;
|
|
165
|
+
}
|
|
166
|
+
else if (value.type === "set") {
|
|
167
|
+
const set = remember(new Set());
|
|
168
|
+
loose.value.forEach((item) => set.add(nested(item)));
|
|
169
|
+
result = set;
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
const entries = loose.value;
|
|
173
|
+
const key = (raw) => typeof raw === "string" ? raw : nested(raw);
|
|
174
|
+
if (value.type === "map") {
|
|
175
|
+
const map = remember(new Map());
|
|
176
|
+
entries.forEach(([k, item]) => map.set(key(k), nested(item)));
|
|
177
|
+
result = map;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
const object = remember({});
|
|
181
|
+
entries.forEach(([k, item]) => {
|
|
182
|
+
object[String(key(k))] = nested(item);
|
|
183
|
+
});
|
|
184
|
+
result = object;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return options.keepHandles && loose.handle !== undefined
|
|
188
|
+
? new RemoteObject(value.type, loose.handle, result)
|
|
189
|
+
: result;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return new RemoteObject(value.type, loose.handle);
|
|
193
|
+
}
|
|
194
|
+
function specialNumber(value) {
|
|
195
|
+
switch (value) {
|
|
196
|
+
case "NaN":
|
|
197
|
+
return NaN;
|
|
198
|
+
case "Infinity":
|
|
199
|
+
return Infinity;
|
|
200
|
+
case "-Infinity":
|
|
201
|
+
return -Infinity;
|
|
202
|
+
case "-0":
|
|
203
|
+
return -0;
|
|
204
|
+
default:
|
|
205
|
+
return Number(value);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type Locator from "../locator/locator.js";
|
|
2
|
+
import type Page from "../browser/page.js";
|
|
3
|
+
import type { LocatorSpec } from "./model.js";
|
|
4
|
+
/** The locator a step's `LocatorSpec` stands for, with its fallbacks */
|
|
5
|
+
export declare function locatorFromSpec(page: Page, spec: LocatorSpec): Locator;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
function fromCalls(page, calls) {
|
|
2
|
+
const [first, ...rest] = calls;
|
|
3
|
+
if (!first)
|
|
4
|
+
throw new Error("A locator needs at least one call");
|
|
5
|
+
const apply = (target, call) => {
|
|
6
|
+
const text = {
|
|
7
|
+
...(call.method !== "locator" &&
|
|
8
|
+
call.method !== "getByCss" &&
|
|
9
|
+
call.method !== "getByTestId" &&
|
|
10
|
+
call.method !== "getByRole" && {
|
|
11
|
+
...(call.match !== undefined && { match: call.match }),
|
|
12
|
+
...(call.ignoreCase !== undefined && { ignoreCase: call.ignoreCase }),
|
|
13
|
+
}),
|
|
14
|
+
};
|
|
15
|
+
switch (call.method) {
|
|
16
|
+
case "locator":
|
|
17
|
+
return target.locator(call.xpath);
|
|
18
|
+
case "getByCss":
|
|
19
|
+
return target.getByCss(call.css);
|
|
20
|
+
case "getByText":
|
|
21
|
+
return target.getByText(call.text, text);
|
|
22
|
+
case "getByLabel":
|
|
23
|
+
return target.getByLabel(call.text, text);
|
|
24
|
+
case "getByRole":
|
|
25
|
+
return target.getByRole(call.role, call.name === undefined ? undefined : { name: call.name });
|
|
26
|
+
case "getByTestId":
|
|
27
|
+
return target.getByTestId(call.testId);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
return rest.reduce((locator, call) => apply(locator, call), apply(page, first));
|
|
31
|
+
}
|
|
32
|
+
/** The locator a step's `LocatorSpec` stands for, with its fallbacks */
|
|
33
|
+
export function locatorFromSpec(page, spec) {
|
|
34
|
+
const primary = fromCalls(page, spec.chain);
|
|
35
|
+
return spec.fallbacks.length === 0
|
|
36
|
+
? primary
|
|
37
|
+
: primary.withFallbacks(...spec.fallbacks.map((chain) => fromCalls(page, chain)));
|
|
38
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Step } from "./model.js";
|
|
2
|
+
export type StepEdit = {
|
|
3
|
+
op: "replace";
|
|
4
|
+
test: number;
|
|
5
|
+
index: number;
|
|
6
|
+
step: Step;
|
|
7
|
+
}
|
|
8
|
+
/** `index` may equal the number of steps, to append */
|
|
9
|
+
| {
|
|
10
|
+
op: "insert";
|
|
11
|
+
test: number;
|
|
12
|
+
index: number;
|
|
13
|
+
step: Step;
|
|
14
|
+
} | {
|
|
15
|
+
op: "remove";
|
|
16
|
+
test: number;
|
|
17
|
+
index: number;
|
|
18
|
+
}
|
|
19
|
+
/** Moves a step so it ends up at position `to` */
|
|
20
|
+
| {
|
|
21
|
+
op: "move";
|
|
22
|
+
test: number;
|
|
23
|
+
from: number;
|
|
24
|
+
to: number;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Applies one edit and returns the new source. Only the touched statement changes: everything else,
|
|
28
|
+
* formatting and comments included, stays byte for byte.
|
|
29
|
+
*/
|
|
30
|
+
export declare function applyEdit(source: string, edit: StepEdit): string;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { stepToSource } from "./emit.js";
|
|
2
|
+
import { parseSpec } from "./parse.js";
|
|
3
|
+
const lineStart = (source, offset) => source.lastIndexOf("\n", offset - 1) + 1;
|
|
4
|
+
function lineEnd(source, offset) {
|
|
5
|
+
const newline = source.indexOf("\n", offset);
|
|
6
|
+
return newline === -1 ? source.length : newline + 1;
|
|
7
|
+
}
|
|
8
|
+
const indentOf = (source, offset) => /^[ \t]*/.exec(source.slice(lineStart(source, offset)))[0];
|
|
9
|
+
function pickTest(source, test) {
|
|
10
|
+
const found = parseSpec(source)[test];
|
|
11
|
+
if (!found)
|
|
12
|
+
throw new RangeError(`There is no test ${test} in this file`);
|
|
13
|
+
return found;
|
|
14
|
+
}
|
|
15
|
+
function pickStep(parsed, index) {
|
|
16
|
+
const step = parsed.steps[index];
|
|
17
|
+
if (!step)
|
|
18
|
+
throw new RangeError(`Test "${parsed.name}" has no step ${index}`);
|
|
19
|
+
return step;
|
|
20
|
+
}
|
|
21
|
+
function splice(source, from, to, text) {
|
|
22
|
+
return source.slice(0, from) + text + source.slice(to);
|
|
23
|
+
}
|
|
24
|
+
/** Inserts statement text so it becomes step `index`, copying the indentation of the steps around it */
|
|
25
|
+
function insertCode(source, parsed, index, code) {
|
|
26
|
+
if (index < 0 || index > parsed.steps.length) {
|
|
27
|
+
throw new RangeError(`Cannot insert at step ${index}: test "${parsed.name}" has ${parsed.steps.length}`);
|
|
28
|
+
}
|
|
29
|
+
const before = parsed.steps[index];
|
|
30
|
+
if (before) {
|
|
31
|
+
// Goes above the step's own comments, which belong to it
|
|
32
|
+
const at = lineStart(source, before.anchor);
|
|
33
|
+
return splice(source, at, at, `${indentOf(source, before.start)}${code}\n`);
|
|
34
|
+
}
|
|
35
|
+
const last = parsed.steps[parsed.steps.length - 1];
|
|
36
|
+
if (last) {
|
|
37
|
+
const indent = indentOf(source, last.start);
|
|
38
|
+
const next = lineEnd(source, last.end);
|
|
39
|
+
// After the last statement's line, so a trailing comment stays with it
|
|
40
|
+
if (next <= parsed.bodyEnd && source[next - 1] === "\n")
|
|
41
|
+
return splice(source, next, next, `${indent}${code}\n`);
|
|
42
|
+
return splice(source, last.end, last.end, `\n${indent}${code}`);
|
|
43
|
+
}
|
|
44
|
+
// Empty body: indent one level deeper than the line that closes it
|
|
45
|
+
const closing = indentOf(source, parsed.bodyEnd);
|
|
46
|
+
const rest = source.slice(parsed.bodyStart, parsed.bodyEnd);
|
|
47
|
+
const tail = rest.includes("\n") ? "" : `\n${closing}`;
|
|
48
|
+
return splice(source, parsed.bodyStart, parsed.bodyStart, `\n${closing} ${code}${tail}`);
|
|
49
|
+
}
|
|
50
|
+
function removeStep(source, parsed, index) {
|
|
51
|
+
const { anchor, end } = pickStep(parsed, index);
|
|
52
|
+
return splice(source, lineStart(source, anchor), lineEnd(source, end), "");
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Applies one edit and returns the new source. Only the touched statement changes: everything else,
|
|
56
|
+
* formatting and comments included, stays byte for byte.
|
|
57
|
+
*/
|
|
58
|
+
export function applyEdit(source, edit) {
|
|
59
|
+
const parsed = pickTest(source, edit.test);
|
|
60
|
+
switch (edit.op) {
|
|
61
|
+
case "replace": {
|
|
62
|
+
const { start, end } = pickStep(parsed, edit.index);
|
|
63
|
+
return splice(source, start, end, stepToSource(edit.step));
|
|
64
|
+
}
|
|
65
|
+
case "insert":
|
|
66
|
+
return insertCode(source, parsed, edit.index, stepToSource(edit.step));
|
|
67
|
+
case "remove":
|
|
68
|
+
return removeStep(source, parsed, edit.index);
|
|
69
|
+
case "move": {
|
|
70
|
+
const { start, end } = pickStep(parsed, edit.from);
|
|
71
|
+
if (edit.to < 0 || edit.to >= parsed.steps.length)
|
|
72
|
+
throw new RangeError(`Cannot move to step ${edit.to}`);
|
|
73
|
+
if (edit.to === edit.from)
|
|
74
|
+
return source;
|
|
75
|
+
const code = source.slice(start, end);
|
|
76
|
+
const removed = removeStep(source, parsed, edit.from);
|
|
77
|
+
return insertCode(removed, pickTest(removed, edit.test), edit.to, code);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { LocatorCall, LocatorSpec, Step, TextValue } from "./model.js";
|
|
2
|
+
export declare function callToSource(call: LocatorCall): string;
|
|
3
|
+
export declare function locatorToSource({ chain, fallbacks }: LocatorSpec): string;
|
|
4
|
+
export declare function valueToSource(value: TextValue): string;
|
|
5
|
+
/** The statement for a step, on one line */
|
|
6
|
+
export declare function stepToSource(step: Step): string;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const str = (value) => JSON.stringify(value);
|
|
2
|
+
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
3
|
+
function textOptions(call) {
|
|
4
|
+
const parts = [
|
|
5
|
+
call.match !== undefined && `match: ${str(call.match)}`,
|
|
6
|
+
call.ignoreCase !== undefined && `ignoreCase: ${call.ignoreCase}`,
|
|
7
|
+
].filter(Boolean);
|
|
8
|
+
return parts.length ? `, { ${parts.join(", ")} }` : "";
|
|
9
|
+
}
|
|
10
|
+
export function callToSource(call) {
|
|
11
|
+
switch (call.method) {
|
|
12
|
+
case "locator":
|
|
13
|
+
return `locator(${str(call.xpath)})`;
|
|
14
|
+
case "getByCss":
|
|
15
|
+
return `getByCss(${str(call.css)})`;
|
|
16
|
+
case "getByText":
|
|
17
|
+
case "getByLabel":
|
|
18
|
+
return `${call.method}(${str(call.text)}${textOptions(call)})`;
|
|
19
|
+
case "getByRole":
|
|
20
|
+
return `getByRole(${str(call.role)}${call.name === undefined ? "" : `, { name: ${str(call.name)} }`})`;
|
|
21
|
+
case "getByTestId":
|
|
22
|
+
return `getByTestId(${str(call.testId)})`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const chainToSource = (chain) => ["page", ...chain.map(callToSource)].join(".");
|
|
26
|
+
export function locatorToSource({ chain, fallbacks }) {
|
|
27
|
+
const primary = chainToSource(chain);
|
|
28
|
+
return fallbacks.length === 0
|
|
29
|
+
? primary
|
|
30
|
+
: `${primary}.withFallbacks(${fallbacks.map(chainToSource).join(", ")})`;
|
|
31
|
+
}
|
|
32
|
+
export function valueToSource(value) {
|
|
33
|
+
if (value.kind === "literal")
|
|
34
|
+
return str(value.value);
|
|
35
|
+
const object = value.kind === "env" ? "env" : "secrets";
|
|
36
|
+
return IDENTIFIER.test(value.name)
|
|
37
|
+
? `${object}.${value.name}`
|
|
38
|
+
: `${object}[${str(value.name)}]`;
|
|
39
|
+
}
|
|
40
|
+
function matcherToSource(expected) {
|
|
41
|
+
return typeof expected === "string"
|
|
42
|
+
? str(expected)
|
|
43
|
+
: `/${expected.regex.source}/${expected.regex.flags}`;
|
|
44
|
+
}
|
|
45
|
+
function expectationToSource(expectation) {
|
|
46
|
+
switch (expectation.matcher) {
|
|
47
|
+
case "toBeVisible":
|
|
48
|
+
return "toBeVisible()";
|
|
49
|
+
case "toHaveAttribute":
|
|
50
|
+
return `toHaveAttribute(${str(expectation.name)}, ${matcherToSource(expectation.expected)})`;
|
|
51
|
+
case "toHaveCount":
|
|
52
|
+
return `toHaveCount(${expectation.expected})`;
|
|
53
|
+
default:
|
|
54
|
+
return `${expectation.matcher}(${matcherToSource(expectation.expected)})`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** The statement for a step, on one line */
|
|
58
|
+
export function stepToSource(step) {
|
|
59
|
+
switch (step.kind) {
|
|
60
|
+
case "goto":
|
|
61
|
+
return `await page.goto(${str(step.url)});`;
|
|
62
|
+
case "click":
|
|
63
|
+
return `await ${locatorToSource(step.locator)}.click();`;
|
|
64
|
+
case "fill":
|
|
65
|
+
return `await ${locatorToSource(step.locator)}.fill(${valueToSource(step.value)});`;
|
|
66
|
+
case "expect":
|
|
67
|
+
return `await expect(${locatorToSource(step.locator)})${step.not ? ".not" : ""}.${expectationToSource(step.expectation)};`;
|
|
68
|
+
case "waitForNetworkIdle":
|
|
69
|
+
return "await page.waitForNetworkIdle();";
|
|
70
|
+
case "custom":
|
|
71
|
+
return step.code;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { parseSpec } from "./parse.js";
|
|
2
|
+
export { applyEdit, type StepEdit } from "./edit.js";
|
|
3
|
+
export { stepToSource, locatorToSource, callToSource, valueToSource, } from "./emit.js";
|
|
4
|
+
export { locatorFromSpec } from "./build.js";
|
|
5
|
+
export type { Expectation, LocatorCall, LocatorSpec, ParsedStep, ParsedTest, Step, TextMatcher, TextValue, } from "./model.js";
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/** One call of a locator expression, as written in a spec: `page.getByRole("button", { name: "Save" })` */
|
|
2
|
+
export type LocatorCall = {
|
|
3
|
+
method: "locator";
|
|
4
|
+
xpath: string;
|
|
5
|
+
} | {
|
|
6
|
+
method: "getByCss";
|
|
7
|
+
css: string;
|
|
8
|
+
} | {
|
|
9
|
+
method: "getByText";
|
|
10
|
+
text: string;
|
|
11
|
+
match?: "full" | "partial";
|
|
12
|
+
ignoreCase?: boolean;
|
|
13
|
+
} | {
|
|
14
|
+
method: "getByLabel";
|
|
15
|
+
text: string;
|
|
16
|
+
match?: "full" | "partial";
|
|
17
|
+
ignoreCase?: boolean;
|
|
18
|
+
} | {
|
|
19
|
+
method: "getByRole";
|
|
20
|
+
role: string;
|
|
21
|
+
name?: string;
|
|
22
|
+
} | {
|
|
23
|
+
method: "getByTestId";
|
|
24
|
+
testId: string;
|
|
25
|
+
};
|
|
26
|
+
/** A chain of locator calls on `page`, and the locators `withFallbacks` falls back to, most preferred first */
|
|
27
|
+
export interface LocatorSpec {
|
|
28
|
+
chain: LocatorCall[];
|
|
29
|
+
fallbacks: LocatorCall[][];
|
|
30
|
+
}
|
|
31
|
+
/** A text to type or compare with */
|
|
32
|
+
export type TextValue = {
|
|
33
|
+
kind: "literal";
|
|
34
|
+
value: string;
|
|
35
|
+
} | {
|
|
36
|
+
kind: "env";
|
|
37
|
+
name: string;
|
|
38
|
+
} | {
|
|
39
|
+
kind: "secret";
|
|
40
|
+
name: string;
|
|
41
|
+
};
|
|
42
|
+
/** What an assertion expects of text: a string or a regular expression */
|
|
43
|
+
export type TextMatcher = string | {
|
|
44
|
+
regex: {
|
|
45
|
+
source: string;
|
|
46
|
+
flags: string;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
export type Expectation = {
|
|
50
|
+
matcher: "toBeVisible";
|
|
51
|
+
} | {
|
|
52
|
+
matcher: "toHaveText" | "toContainText" | "toHaveValue";
|
|
53
|
+
expected: TextMatcher;
|
|
54
|
+
} | {
|
|
55
|
+
matcher: "toHaveAttribute";
|
|
56
|
+
name: string;
|
|
57
|
+
expected: TextMatcher;
|
|
58
|
+
} | {
|
|
59
|
+
matcher: "toHaveCount";
|
|
60
|
+
expected: number;
|
|
61
|
+
};
|
|
62
|
+
/** The restricted set of statements the recorder writes. Anything else in a test is a `custom` step */
|
|
63
|
+
export type Step = {
|
|
64
|
+
kind: "goto";
|
|
65
|
+
url: string;
|
|
66
|
+
} | {
|
|
67
|
+
kind: "click";
|
|
68
|
+
locator: LocatorSpec;
|
|
69
|
+
} | {
|
|
70
|
+
kind: "fill";
|
|
71
|
+
locator: LocatorSpec;
|
|
72
|
+
value: TextValue;
|
|
73
|
+
} | {
|
|
74
|
+
kind: "expect";
|
|
75
|
+
locator: LocatorSpec;
|
|
76
|
+
not: boolean;
|
|
77
|
+
expectation: Expectation;
|
|
78
|
+
} | {
|
|
79
|
+
kind: "waitForNetworkIdle";
|
|
80
|
+
}
|
|
81
|
+
/** A statement the codec does not understand, kept verbatim */
|
|
82
|
+
| {
|
|
83
|
+
kind: "custom";
|
|
84
|
+
code: string;
|
|
85
|
+
};
|
|
86
|
+
/** A step and where its statement sits in the source */
|
|
87
|
+
export interface ParsedStep {
|
|
88
|
+
step: Step;
|
|
89
|
+
/** Offset of the statement's first character (after leading comments) */
|
|
90
|
+
start: number;
|
|
91
|
+
end: number;
|
|
92
|
+
/** Offset where the statement's own lines begin, leading comments included */
|
|
93
|
+
anchor: number;
|
|
94
|
+
}
|
|
95
|
+
export interface ParsedTest {
|
|
96
|
+
/** describe titles, then the test title */
|
|
97
|
+
titlePath: string[];
|
|
98
|
+
name: string;
|
|
99
|
+
steps: ParsedStep[];
|
|
100
|
+
/** Offset just after the `{` of the test body, and of its closing `}` */
|
|
101
|
+
bodyStart: number;
|
|
102
|
+
bodyEnd: number;
|
|
103
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|