@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
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import { test } from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import {
|
|
4
|
-
allPass,
|
|
5
|
-
evaluateChecks,
|
|
6
|
-
parseElementState,
|
|
7
|
-
PROBE_ELEMENT,
|
|
8
|
-
type CheckName,
|
|
9
|
-
type ElementState,
|
|
10
|
-
} from "./element-state.js";
|
|
11
|
-
|
|
12
|
-
const READY: ElementState = {
|
|
13
|
-
attached: true,
|
|
14
|
-
visible: true,
|
|
15
|
-
enabled: true,
|
|
16
|
-
editable: true,
|
|
17
|
-
box: { x: 10, y: 20, width: 100, height: 40 },
|
|
18
|
-
hitTarget: "self",
|
|
19
|
-
};
|
|
20
|
-
const ALL: CheckName[] = ["attached", "visible", "stable", "enabled", "editable", "hit target"];
|
|
21
|
-
|
|
22
|
-
test("the probe is a function declaration that compiles", () => {
|
|
23
|
-
assert.doesNotThrow(() => new Function(`return (${PROBE_ELEMENT})`), PROBE_ELEMENT);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test("the probe takes the located element and the options", () => {
|
|
27
|
-
assert.match(PROBE_ELEMENT, /^\(el, options\) =>/);
|
|
28
|
-
assert.ok(!PROBE_ELEMENT.includes("document.evaluate"), "the element is found by the browser's locate command");
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test("scrolling and hit testing are switched by the options argument", () => {
|
|
32
|
-
assert.match(PROBE_ELEMENT, /if \(options\.scroll\)/);
|
|
33
|
-
assert.match(PROBE_ELEMENT, /if \(options\.hitTest\)/);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test("parseElementState accepts a complete state", () => {
|
|
37
|
-
assert.deepEqual(parseElementState({ ...READY }), READY);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test("parseElementState rejects a non-object result", () => {
|
|
41
|
-
assert.throws(
|
|
42
|
-
() => parseElementState(null),
|
|
43
|
-
/Expected an element state object but received "null"/,
|
|
44
|
-
);
|
|
45
|
-
assert.throws(
|
|
46
|
-
() => parseElementState("string"),
|
|
47
|
-
/Expected an element state object but received "string"/,
|
|
48
|
-
);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("parseElementState rejects malformed state", () => {
|
|
52
|
-
assert.throws(() => parseElementState({ attached: true }), /Malformed element state/);
|
|
53
|
-
assert.throws(
|
|
54
|
-
() => parseElementState({ ...READY, visible: "yes" }),
|
|
55
|
-
/Malformed element state/,
|
|
56
|
-
);
|
|
57
|
-
const { box: _box, ...withoutBox } = READY;
|
|
58
|
-
assert.throws(() => parseElementState(withoutBox), /Malformed element state/);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("evaluateChecks passes an actionable element whose box did not move", () => {
|
|
62
|
-
const results = evaluateChecks(ALL, READY, READY);
|
|
63
|
-
assert.deepEqual(results, {
|
|
64
|
-
attached: "pass",
|
|
65
|
-
visible: "pass",
|
|
66
|
-
stable: "pass",
|
|
67
|
-
enabled: "pass",
|
|
68
|
-
editable: "pass",
|
|
69
|
-
"hit target": "pass",
|
|
70
|
-
});
|
|
71
|
-
assert.equal(allPass(results), true);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test("the first failing check marks later checks pending", () => {
|
|
75
|
-
const results = evaluateChecks(ALL, { ...READY, visible: false }, READY);
|
|
76
|
-
assert.deepEqual(results, {
|
|
77
|
-
attached: "pass",
|
|
78
|
-
visible: "fail",
|
|
79
|
-
stable: "pending",
|
|
80
|
-
enabled: "pending",
|
|
81
|
-
editable: "pending",
|
|
82
|
-
"hit target": "pending",
|
|
83
|
-
});
|
|
84
|
-
assert.equal(allPass(results), false);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test("stable needs a previous probe with the same box", () => {
|
|
88
|
-
const stable = (previous: ElementState | undefined) =>
|
|
89
|
-
evaluateChecks(["stable"], READY, previous).stable;
|
|
90
|
-
assert.equal(stable(undefined), "fail");
|
|
91
|
-
assert.equal(stable({ ...READY, box: { ...READY.box!, x: 11 } }), "fail");
|
|
92
|
-
assert.equal(stable({ ...READY, box: null }), "fail");
|
|
93
|
-
assert.equal(stable(READY), "pass");
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test("hit target passes only when the element itself is hit", () => {
|
|
97
|
-
const hit = (hitTarget: string | null) =>
|
|
98
|
-
evaluateChecks(["hit target"], { ...READY, hitTarget }, READY)["hit target"];
|
|
99
|
-
assert.equal(hit("self"), "pass");
|
|
100
|
-
assert.equal(hit("div#overlay"), "fail");
|
|
101
|
-
assert.equal(hit(null), "fail");
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test("only the required checks are evaluated, in the given order", () => {
|
|
105
|
-
assert.deepEqual(Object.keys(evaluateChecks(["attached", "enabled"], READY, undefined)), [
|
|
106
|
-
"attached",
|
|
107
|
-
"enabled",
|
|
108
|
-
]);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
/** Runs the probe against fake DOM objects, the way the page would call it */
|
|
112
|
-
function runProbe(element: object | null, options = { scroll: true, hitTest: true }) {
|
|
113
|
-
const fakeDocument = {
|
|
114
|
-
elementFromPoint: () => element,
|
|
115
|
-
};
|
|
116
|
-
const fakeWindow = { innerWidth: 1000, innerHeight: 800 };
|
|
117
|
-
const fakeComputedStyle = () => ({ display: "block", visibility: "visible", opacity: "1" });
|
|
118
|
-
const probe = new Function(
|
|
119
|
-
"document",
|
|
120
|
-
"XPathResult",
|
|
121
|
-
"getComputedStyle",
|
|
122
|
-
"window",
|
|
123
|
-
`return (${PROBE_ELEMENT})`,
|
|
124
|
-
)(fakeDocument, { FIRST_ORDERED_NODE_TYPE: 9 }, fakeComputedStyle, fakeWindow) as (
|
|
125
|
-
el: object | null,
|
|
126
|
-
options: { scroll: boolean; hitTest: boolean },
|
|
127
|
-
) => unknown;
|
|
128
|
-
return parseElementState(probe(element, options));
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
test("editable is a boolean even for non-HTML elements without isContentEditable", () => {
|
|
132
|
-
const svg = {
|
|
133
|
-
isConnected: true,
|
|
134
|
-
tagName: "svg",
|
|
135
|
-
id: "",
|
|
136
|
-
classList: [],
|
|
137
|
-
matches: () => false,
|
|
138
|
-
getAttribute: () => null,
|
|
139
|
-
contains: () => false,
|
|
140
|
-
getBoundingClientRect: () => ({ left: 0, top: 0, right: 10, bottom: 10, width: 10, height: 10 }),
|
|
141
|
-
};
|
|
142
|
-
const state = runProbe(svg);
|
|
143
|
-
assert.equal(state.attached, true);
|
|
144
|
-
assert.equal(state.editable, false);
|
|
145
|
-
assert.equal(typeof state.editable, "boolean");
|
|
146
|
-
assert.equal(state.hitTarget, "self");
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
test("the probe reports a missing element as not attached", () => {
|
|
150
|
-
const state = runProbe(null);
|
|
151
|
-
assert.equal(state.attached, false);
|
|
152
|
-
assert.equal(state.box, null);
|
|
153
|
-
assert.equal(state.hitTarget, null);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
test("a node that was removed from the document is not attached, though the browser still resolves it", () => {
|
|
157
|
-
const removed = {
|
|
158
|
-
isConnected: false,
|
|
159
|
-
getBoundingClientRect: () => {
|
|
160
|
-
throw new Error("must not be measured");
|
|
161
|
-
},
|
|
162
|
-
};
|
|
163
|
-
const state = runProbe(removed);
|
|
164
|
-
assert.equal(state.attached, false);
|
|
165
|
-
assert.equal(state.box, null);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
test("the probe skips the hit test unless asked", () => {
|
|
169
|
-
const el = {
|
|
170
|
-
isConnected: true,
|
|
171
|
-
tagName: "button",
|
|
172
|
-
id: "",
|
|
173
|
-
classList: [],
|
|
174
|
-
matches: () => false,
|
|
175
|
-
getAttribute: () => null,
|
|
176
|
-
contains: () => false,
|
|
177
|
-
getBoundingClientRect: () => ({ left: 0, top: 0, right: 10, bottom: 10, width: 10, height: 10 }),
|
|
178
|
-
};
|
|
179
|
-
assert.equal(runProbe(el, { scroll: false, hitTest: false }).hitTarget, null);
|
|
180
|
-
assert.equal(runProbe(el, { scroll: false, hitTest: true }).hitTarget, "self");
|
|
181
|
-
});
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
export interface ElementBox {
|
|
2
|
-
x: number;
|
|
3
|
-
y: number;
|
|
4
|
-
width: number;
|
|
5
|
-
height: number;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface ElementState {
|
|
9
|
-
attached: boolean;
|
|
10
|
-
visible: boolean;
|
|
11
|
-
enabled: boolean;
|
|
12
|
-
editable: boolean;
|
|
13
|
-
/** Viewport coordinates after any scroll; null when not attached */
|
|
14
|
-
box: ElementBox | null;
|
|
15
|
-
/**
|
|
16
|
-
* "self" when a click at the element's centre reaches it, otherwise a description of
|
|
17
|
-
* the covering element ("nothing" when no element is there); null when not checked
|
|
18
|
-
*/
|
|
19
|
-
hitTarget: string | null;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface ProbeOptions {
|
|
23
|
-
/** Scroll the element into view first when it is not fully inside the viewport */
|
|
24
|
-
scroll: boolean;
|
|
25
|
-
/** Check which element receives a click at the element's centre */
|
|
26
|
-
hitTest: boolean;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export type WaitForState = "attached" | "detached" | "visible" | "hidden";
|
|
30
|
-
export type CheckName = "attached" | "visible" | "stable" | "enabled" | "editable" | "hit target";
|
|
31
|
-
export type CheckResult = "pass" | "fail" | "pending";
|
|
32
|
-
/** Results keyed by check name, in the order the checks were evaluated */
|
|
33
|
-
export type CheckResults = Partial<Record<CheckName, CheckResult>>;
|
|
34
|
-
|
|
35
|
-
const TEXT_INPUT_TYPES = ["text", "search", "email", "url", "tel", "password", "number"];
|
|
36
|
-
|
|
37
|
-
/** State of an element that is not in the document (never matched, or gone) */
|
|
38
|
-
export const DETACHED_STATE: ElementState = {
|
|
39
|
-
attached: false,
|
|
40
|
-
visible: false,
|
|
41
|
-
enabled: false,
|
|
42
|
-
editable: false,
|
|
43
|
-
box: null,
|
|
44
|
-
hitTarget: null,
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Function declaration run in the page through `script.callFunction` as `(el, options)`, where `el`
|
|
49
|
-
* is the located element. Returns its state as a plain object in one round trip. Kept as a string so
|
|
50
|
-
* the transpiler cannot inject helpers (`__name`) that do not exist in the page.
|
|
51
|
-
* A node that was removed from the document stays resolvable in the browser, hence `isConnected`.
|
|
52
|
-
*/
|
|
53
|
-
export const PROBE_ELEMENT = `(el, options) => {
|
|
54
|
-
if (!el || !el.isConnected) {
|
|
55
|
-
return { attached: false, visible: false, enabled: false, editable: false, box: null, hitTarget: null };
|
|
56
|
-
}
|
|
57
|
-
if (options.scroll) {
|
|
58
|
-
const before = el.getBoundingClientRect();
|
|
59
|
-
const inViewport = before.top >= 0 && before.left >= 0 &&
|
|
60
|
-
before.bottom <= window.innerHeight && before.right <= window.innerWidth;
|
|
61
|
-
if (!inViewport) el.scrollIntoView({ block: "center", inline: "center" });
|
|
62
|
-
}
|
|
63
|
-
const rect = el.getBoundingClientRect();
|
|
64
|
-
const style = getComputedStyle(el);
|
|
65
|
-
const visible = rect.width > 0 && rect.height > 0 &&
|
|
66
|
-
style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0";
|
|
67
|
-
const enabled = !el.matches(":disabled") && el.getAttribute("aria-disabled") !== "true";
|
|
68
|
-
const tag = el.tagName.toLowerCase();
|
|
69
|
-
const typeable = Boolean(
|
|
70
|
-
(tag === "input" && ${JSON.stringify(TEXT_INPUT_TYPES)}.includes((el.getAttribute("type") || "text").toLowerCase())) ||
|
|
71
|
-
tag === "textarea" || el.isContentEditable);
|
|
72
|
-
const editable = Boolean(enabled && !el.readOnly && typeable);
|
|
73
|
-
let hitTarget = null;
|
|
74
|
-
if (options.hitTest) {
|
|
75
|
-
const top = document.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2);
|
|
76
|
-
hitTarget = !top ? "nothing"
|
|
77
|
-
: top === el || el.contains(top) ? "self"
|
|
78
|
-
: top.tagName.toLowerCase() + (top.id ? "#" + top.id : "") +
|
|
79
|
-
Array.from(top.classList, (name) => "." + name).join("");
|
|
80
|
-
}
|
|
81
|
-
return {
|
|
82
|
-
attached: true, visible, enabled, editable,
|
|
83
|
-
box: { x: rect.left, y: rect.top, width: rect.width, height: rect.height },
|
|
84
|
-
hitTarget,
|
|
85
|
-
};
|
|
86
|
-
}`;
|
|
87
|
-
|
|
88
|
-
/** Checks the probe result is a complete state; throws on anything else */
|
|
89
|
-
export function parseElementState(value: unknown): ElementState {
|
|
90
|
-
if (typeof value !== "object" || value === null) {
|
|
91
|
-
throw new Error(`Expected an element state object but received "${describeType(value)}"`);
|
|
92
|
-
}
|
|
93
|
-
const state = value as Partial<ElementState>;
|
|
94
|
-
const flags = ["attached", "visible", "enabled", "editable"] as const;
|
|
95
|
-
if (
|
|
96
|
-
flags.some((flag) => typeof state[flag] !== "boolean") ||
|
|
97
|
-
!("box" in state) ||
|
|
98
|
-
!("hitTarget" in state)
|
|
99
|
-
) {
|
|
100
|
-
throw new Error(`Malformed element state: ${JSON.stringify(value)}`);
|
|
101
|
-
}
|
|
102
|
-
return state as ElementState;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function describeType(value: unknown): string {
|
|
106
|
-
return value === null ? "null" : typeof value;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function sameBox(a: ElementBox | null, b: ElementBox | null) {
|
|
110
|
-
return (
|
|
111
|
-
a !== null &&
|
|
112
|
-
b !== null &&
|
|
113
|
-
a.x === b.x &&
|
|
114
|
-
a.y === b.y &&
|
|
115
|
-
a.width === b.width &&
|
|
116
|
-
a.height === b.height
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function checkPasses(name: CheckName, current: ElementState, previous: ElementState | undefined) {
|
|
121
|
-
switch (name) {
|
|
122
|
-
case "attached":
|
|
123
|
-
return current.attached;
|
|
124
|
-
case "visible":
|
|
125
|
-
return current.visible;
|
|
126
|
-
case "stable":
|
|
127
|
-
return sameBox(current.box, previous?.box ?? null);
|
|
128
|
-
case "enabled":
|
|
129
|
-
return current.enabled;
|
|
130
|
-
case "editable":
|
|
131
|
-
return current.editable;
|
|
132
|
-
case "hit target":
|
|
133
|
-
return current.hitTarget === "self";
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** Evaluates the required checks in order; after the first failure the rest are pending */
|
|
138
|
-
export function evaluateChecks(
|
|
139
|
-
required: readonly CheckName[],
|
|
140
|
-
current: ElementState,
|
|
141
|
-
previous: ElementState | undefined,
|
|
142
|
-
): CheckResults {
|
|
143
|
-
const results: CheckResults = {};
|
|
144
|
-
let failed = false;
|
|
145
|
-
for (const name of required) {
|
|
146
|
-
if (failed) {
|
|
147
|
-
results[name] = "pending";
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
const pass = checkPasses(name, current, previous);
|
|
151
|
-
results[name] = pass ? "pass" : "fail";
|
|
152
|
-
failed = !pass;
|
|
153
|
-
}
|
|
154
|
-
return results;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export function allPass(results: CheckResults): boolean {
|
|
158
|
-
return Object.values(results).every((result) => result === "pass");
|
|
159
|
-
}
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { after, before, test } from "node:test";
|
|
3
|
-
import { Browser } from "../browser/browser.js";
|
|
4
|
-
import type Page from "../browser/page.js";
|
|
5
|
-
import { setContent } from "../testing/browser-fixture.js";
|
|
6
|
-
import { InvalidSelectorError } from "./locator.js";
|
|
7
|
-
|
|
8
|
-
let browser: Browser;
|
|
9
|
-
let page: Page;
|
|
10
|
-
|
|
11
|
-
before(async () => {
|
|
12
|
-
({ browser, page } = await Browser.launch("firefox", { port: 9235, headless: true }));
|
|
13
|
-
await page.navigateTo("about:blank", "complete");
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
after(async () => {
|
|
17
|
-
await page.dispose();
|
|
18
|
-
await browser?.close();
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
const OPTIONS = { timeout: 20000 };
|
|
22
|
-
|
|
23
|
-
const FORMS = `
|
|
24
|
-
<form id="login"><h2>Login</h2>
|
|
25
|
-
<input type="text" name="user" placeholder="user">
|
|
26
|
-
<button type="submit">Sign in</button>
|
|
27
|
-
</form>
|
|
28
|
-
<form id="signup"><h2>Sign up</h2>
|
|
29
|
-
<input type="text" name="user" placeholder="new user">
|
|
30
|
-
<button type="submit">Create account</button>
|
|
31
|
-
</form>`;
|
|
32
|
-
|
|
33
|
-
test("css locators find elements and count them", OPTIONS, async () => {
|
|
34
|
-
await setContent(page, FORMS);
|
|
35
|
-
assert.equal(await page.getByCss("form input[name=user]").count(), 2);
|
|
36
|
-
assert.equal(await page.getByCss("#signup button").textContent(), "Create account");
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("xpath locators behave as before", OPTIONS, async () => {
|
|
40
|
-
await setContent(page, FORMS);
|
|
41
|
-
assert.equal(await page.locator("button[text()='Sign in']").count(), 1);
|
|
42
|
-
assert.equal(await page.locator("//form[@id='login']//input").getAttribute("placeholder"), "user");
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test("text locators match fully by default, partially and case-insensitively on request", OPTIONS, async () => {
|
|
46
|
-
await setContent(page, FORMS);
|
|
47
|
-
assert.equal(await page.getByText("Sign in").count(), 1);
|
|
48
|
-
assert.equal(await page.getByText("Sign").count(), 0, "full match by default");
|
|
49
|
-
assert.equal(await page.getByText("Sign", { match: "partial" }).count(), 2, "Sign in + Sign up");
|
|
50
|
-
assert.equal(await page.getByText("sign in").count(), 0);
|
|
51
|
-
assert.equal(await page.getByText("sign in", { ignoreCase: true }).count(), 1);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test("a chained locator is scoped to its parent", OPTIONS, async () => {
|
|
55
|
-
await setContent(page, FORMS);
|
|
56
|
-
const signup = page.locator("//form[@id='signup']");
|
|
57
|
-
assert.equal(await signup.getByCss("input").getAttribute("placeholder"), "new user");
|
|
58
|
-
assert.equal(await signup.locator("input").getAttribute("placeholder"), "new user");
|
|
59
|
-
assert.equal(await signup.locator("//input").getAttribute("placeholder"), "new user", "// is relative to the parent");
|
|
60
|
-
assert.equal(await signup.getByText("Sign in").count(), 0, "the login button is outside the signup form");
|
|
61
|
-
assert.equal(await page.getByCss("form").getByCss("button").count(), 2, "several parents");
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test("actions work through chained locators", OPTIONS, async () => {
|
|
65
|
-
await setContent(
|
|
66
|
-
page,
|
|
67
|
-
FORMS,
|
|
68
|
-
`document.querySelectorAll("button").forEach((b) =>
|
|
69
|
-
b.addEventListener("click", (e) => { e.preventDefault(); b.dataset.clicked = "yes"; }));`,
|
|
70
|
-
);
|
|
71
|
-
await page.locator("//form[@id='signup']").getByCss("button").click({ timeout: 5000 });
|
|
72
|
-
assert.equal(await page.getByCss("#signup button").getAttribute("data-clicked"), "yes");
|
|
73
|
-
assert.equal(await page.getByCss("#login button").getAttribute("data-clicked"), null);
|
|
74
|
-
await page.locator("//form[@id='login']").getByCss("input").fill("sam", { timeout: 5000 });
|
|
75
|
-
assert.equal(await page.getByCss("#login input").inputValue(), "sam");
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test("all() on a css locator gives locators for each element", OPTIONS, async () => {
|
|
79
|
-
await setContent(page, `<ul><li>a</li><li>b</li><li>c</li></ul>`);
|
|
80
|
-
const items = await page.getByCss("li").all();
|
|
81
|
-
assert.equal(items.length, 3);
|
|
82
|
-
assert.deepEqual(
|
|
83
|
-
await Promise.all(items.map((item) => item.textContent())),
|
|
84
|
-
["a", "b", "c"],
|
|
85
|
-
);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("the element is re-resolved every poll: a replaced element is found and clicked", OPTIONS, async () => {
|
|
89
|
-
await setContent(
|
|
90
|
-
page,
|
|
91
|
-
`<button id="b" disabled>old</button>`,
|
|
92
|
-
`setTimeout(() => {
|
|
93
|
-
document.body.innerHTML = '<button id="b">new</button>';
|
|
94
|
-
document.getElementById("b").addEventListener("click", (e) => { e.target.dataset.clicked = "yes"; });
|
|
95
|
-
}, 300);`,
|
|
96
|
-
);
|
|
97
|
-
const button = page.getByCss("#b");
|
|
98
|
-
await button.click({ timeout: 5000 });
|
|
99
|
-
assert.equal(await button.textContent(), "new");
|
|
100
|
-
assert.equal(await button.getAttribute("data-clicked"), "yes");
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test("a removed element is not attached even though the browser can still resolve it", OPTIONS, async () => {
|
|
104
|
-
await setContent(page, `<p id="p">x</p>`);
|
|
105
|
-
const paragraph = page.getByCss("#p");
|
|
106
|
-
await paragraph.waitFor({ state: "visible", timeout: 2000 });
|
|
107
|
-
await setContent(page, ``);
|
|
108
|
-
await paragraph.waitFor({ state: "detached", timeout: 2000 });
|
|
109
|
-
assert.equal(await paragraph.isVisible(), false);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test("the first match is re-evaluated: a new earlier element becomes the target", OPTIONS, async () => {
|
|
113
|
-
await setContent(
|
|
114
|
-
page,
|
|
115
|
-
`<p class="x">second</p>`,
|
|
116
|
-
`setTimeout(() => document.body.insertAdjacentHTML("afterbegin", '<p class="x">first</p>'), 300);`,
|
|
117
|
-
);
|
|
118
|
-
const first = page.getByCss("p.x");
|
|
119
|
-
assert.equal(await first.textContent(), "second");
|
|
120
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
121
|
-
assert.equal(await first.textContent(), "first");
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test("an invalid css selector fails at once with the selector in the message", OPTIONS, async () => {
|
|
125
|
-
await setContent(page, FORMS);
|
|
126
|
-
const started = Date.now();
|
|
127
|
-
await assert.rejects(page.getByCss("form[").click({ timeout: 5000 }), (err) => {
|
|
128
|
-
assert.ok(err instanceof InvalidSelectorError, String(err));
|
|
129
|
-
assert.equal(err.selector, "css=form[");
|
|
130
|
-
return true;
|
|
131
|
-
});
|
|
132
|
-
assert.ok(Date.now() - started < 3000, "not retried until the timeout");
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test("role locators use the browser's accessibility tree", OPTIONS, async () => {
|
|
136
|
-
await setContent(
|
|
137
|
-
page,
|
|
138
|
-
FORMS + `<a href="/x">Docs</a><div role="button" aria-label="Custom">x</div>`,
|
|
139
|
-
);
|
|
140
|
-
assert.equal(await page.getByRole("button", { name: "Sign in" }).count(), 1);
|
|
141
|
-
assert.equal(await page.getByRole("button").count(), 3, "two submit buttons and the role=button div");
|
|
142
|
-
assert.equal(await page.getByRole("link", { name: "Docs" }).getAttribute("href"), "/x");
|
|
143
|
-
assert.equal(await page.getByRole("button", { name: "Custom" }).count(), 1, "aria-label is the name");
|
|
144
|
-
assert.equal(
|
|
145
|
-
await page.locator("//form[@id='signup']").getByRole("button").textContent(),
|
|
146
|
-
"Create account",
|
|
147
|
-
);
|
|
148
|
-
assert.equal(await page.getByRole("textbox").count(), 2);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test("text search finds the innermost element, also without the browser's innerText locator", OPTIONS, async () => {
|
|
152
|
-
await setContent(page, `<div id="wrap"><span id="inner"> Save draft </span></div><p>Save</p>`);
|
|
153
|
-
const found = page.getByText("Save draft");
|
|
154
|
-
assert.equal(await found.count(), 1);
|
|
155
|
-
assert.equal(await found.getAttribute("id"), "inner", "not the wrapping div");
|
|
156
|
-
assert.equal(await page.getByText("Save").count(), 1, "full match: only the paragraph");
|
|
157
|
-
assert.equal(await page.getByText("save", { match: "partial", ignoreCase: true }).count(), 2);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
test("text locators are re-evaluated on every poll", OPTIONS, async () => {
|
|
161
|
-
await setContent(
|
|
162
|
-
page,
|
|
163
|
-
`<p>Loading</p>`,
|
|
164
|
-
`setTimeout(() => { document.body.innerHTML = '<button id="b">Done</button>'; }, 300);`,
|
|
165
|
-
);
|
|
166
|
-
const done = page.getByText("Done");
|
|
167
|
-
await done.waitFor({ state: "visible", timeout: 5000 });
|
|
168
|
-
assert.equal(await done.getAttribute("id"), "b");
|
|
169
|
-
});
|