@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,124 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import {
|
|
4
|
-
cssSelector,
|
|
5
|
-
describeChain,
|
|
6
|
-
describeSelector,
|
|
7
|
-
normalizeXpath,
|
|
8
|
-
roleSelector,
|
|
9
|
-
textSelector,
|
|
10
|
-
toBiDiLocator,
|
|
11
|
-
xpathSelector,
|
|
12
|
-
type Selector,
|
|
13
|
-
} from "./selector.js";
|
|
14
|
-
|
|
15
|
-
describe("normalizeXpath", () => {
|
|
16
|
-
it("prefixes a relative xpath with //, keeping absolute, grouped and context paths", () => {
|
|
17
|
-
assert.equal(normalizeXpath("h1", false), "//h1");
|
|
18
|
-
assert.equal(normalizeXpath("//div", false), "//div");
|
|
19
|
-
assert.equal(normalizeXpath("a[text()='Start']", false), "//a[text()='Start']");
|
|
20
|
-
assert.equal(normalizeXpath("/html/body", false), "/html/body");
|
|
21
|
-
assert.equal(normalizeXpath("(//li)[2]", false), "(//li)[2]");
|
|
22
|
-
assert.equal(normalizeXpath(".//span", false), ".//span");
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("scopes a chained xpath to its parent: relative and // paths become .//", () => {
|
|
26
|
-
assert.equal(normalizeXpath("input", true), ".//input");
|
|
27
|
-
assert.equal(normalizeXpath("//input", true), ".//input");
|
|
28
|
-
assert.equal(normalizeXpath(".//input", true), ".//input");
|
|
29
|
-
assert.equal(normalizeXpath("./input", true), "./input");
|
|
30
|
-
assert.equal(normalizeXpath("/html/body", true), "/html/body");
|
|
31
|
-
assert.equal(normalizeXpath("(//li)[2]", true), "(//li)[2]");
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
describe("selector constructors", () => {
|
|
36
|
-
it("build selectors with defaults", () => {
|
|
37
|
-
assert.deepEqual(xpathSelector("h1"), { kind: "xpath", value: "h1" });
|
|
38
|
-
assert.deepEqual(cssSelector("form input"), { kind: "css", value: "form input" });
|
|
39
|
-
assert.deepEqual(textSelector("Sign in"), {
|
|
40
|
-
kind: "text",
|
|
41
|
-
value: "Sign in",
|
|
42
|
-
match: "full",
|
|
43
|
-
ignoreCase: false,
|
|
44
|
-
});
|
|
45
|
-
assert.deepEqual(textSelector("in", { match: "partial", ignoreCase: true }), {
|
|
46
|
-
kind: "text",
|
|
47
|
-
value: "in",
|
|
48
|
-
match: "partial",
|
|
49
|
-
ignoreCase: true,
|
|
50
|
-
});
|
|
51
|
-
assert.deepEqual(roleSelector("button", { name: "Submit" }), {
|
|
52
|
-
kind: "role",
|
|
53
|
-
role: "button",
|
|
54
|
-
name: "Submit",
|
|
55
|
-
});
|
|
56
|
-
assert.deepEqual(roleSelector("button"), { kind: "role", role: "button" });
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("reject empty input", () => {
|
|
60
|
-
assert.throws(() => xpathSelector(""), TypeError);
|
|
61
|
-
assert.throws(() => cssSelector(" "), TypeError);
|
|
62
|
-
assert.throws(() => textSelector(""), TypeError);
|
|
63
|
-
assert.throws(() => roleSelector(""), TypeError);
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
describe("toBiDiLocator", () => {
|
|
68
|
-
it("maps every kind", () => {
|
|
69
|
-
assert.deepEqual(toBiDiLocator(xpathSelector("h1"), false), { type: "xpath", value: "//h1" });
|
|
70
|
-
assert.deepEqual(toBiDiLocator(xpathSelector("h1"), true), { type: "xpath", value: ".//h1" });
|
|
71
|
-
assert.deepEqual(toBiDiLocator(cssSelector("a.b"), false), { type: "css", value: "a.b" });
|
|
72
|
-
assert.deepEqual(toBiDiLocator(textSelector("Hi", { match: "partial", ignoreCase: true }), false), {
|
|
73
|
-
type: "innerText",
|
|
74
|
-
value: "Hi",
|
|
75
|
-
matchType: "partial",
|
|
76
|
-
ignoreCase: true,
|
|
77
|
-
});
|
|
78
|
-
assert.deepEqual(toBiDiLocator(roleSelector("button", { name: "Go" }), false), {
|
|
79
|
-
type: "accessibility",
|
|
80
|
-
value: { role: "button", name: "Go" },
|
|
81
|
-
});
|
|
82
|
-
assert.deepEqual(toBiDiLocator(roleSelector("link"), false), {
|
|
83
|
-
type: "accessibility",
|
|
84
|
-
value: { role: "link" },
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("refuses nth, which the browser does not know", () => {
|
|
89
|
-
assert.throws(() => toBiDiLocator({ kind: "nth", index: 0 }, false), /nth/);
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
describe("describeSelector", () => {
|
|
94
|
-
it("prints a name for every kind", () => {
|
|
95
|
-
assert.equal(describeSelector(xpathSelector("h1"), false), "//h1");
|
|
96
|
-
assert.equal(describeSelector(xpathSelector("h1"), true), ".//h1");
|
|
97
|
-
assert.equal(describeSelector(cssSelector("form input"), false), "css=form input");
|
|
98
|
-
assert.equal(describeSelector(textSelector("Sign in"), false), 'text="Sign in"');
|
|
99
|
-
assert.equal(
|
|
100
|
-
describeSelector(textSelector("in", { match: "partial" }), false),
|
|
101
|
-
'text="in" (partial)',
|
|
102
|
-
);
|
|
103
|
-
assert.equal(
|
|
104
|
-
describeSelector(textSelector("in", { match: "partial", ignoreCase: true }), false),
|
|
105
|
-
'text="in" (partial, ignoring case)',
|
|
106
|
-
);
|
|
107
|
-
assert.equal(
|
|
108
|
-
describeSelector(textSelector("in", { ignoreCase: true }), false),
|
|
109
|
-
'text="in" (ignoring case)',
|
|
110
|
-
);
|
|
111
|
-
assert.equal(
|
|
112
|
-
describeSelector(roleSelector("button", { name: "Submit" }), false),
|
|
113
|
-
'role=button[name="Submit"]',
|
|
114
|
-
);
|
|
115
|
-
assert.equal(describeSelector(roleSelector("button"), false), "role=button");
|
|
116
|
-
assert.equal(describeSelector({ kind: "nth", index: 2 }, false), "nth=2");
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("joins a chain with >> and scopes chained xpaths", () => {
|
|
120
|
-
const chain: Selector[] = [xpathSelector("//form"), xpathSelector("input"), cssSelector(".x")];
|
|
121
|
-
assert.equal(describeChain(chain), "//form >> .//input >> css=.x");
|
|
122
|
-
assert.equal(describeChain([xpathSelector("h1")]), "//h1");
|
|
123
|
-
});
|
|
124
|
-
});
|
package/src/locator/selector.ts
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import type { NodeLocator } from "../types/bidi-modules/browsing-context.js";
|
|
2
|
-
|
|
3
|
-
/** One step of a locator. A locator is a chain of them, each searching inside the previous step's matches */
|
|
4
|
-
export type Selector =
|
|
5
|
-
| { kind: "xpath"; value: string }
|
|
6
|
-
| { kind: "css"; value: string }
|
|
7
|
-
| { kind: "text"; value: string; match: "full" | "partial"; ignoreCase: boolean }
|
|
8
|
-
| { kind: "role"; role?: string; name?: string }
|
|
9
|
-
/** Picks the n-th (0-based) of everything the chain so far matches; `all()` builds these */
|
|
10
|
-
| { kind: "nth"; index: number };
|
|
11
|
-
|
|
12
|
-
export interface TextOptions {
|
|
13
|
-
/** @default "full" */
|
|
14
|
-
match?: "full" | "partial";
|
|
15
|
-
/** @default false */
|
|
16
|
-
ignoreCase?: boolean;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function nonEmpty(value: string, what: string): string {
|
|
20
|
-
if (value.trim() === "") throw new TypeError(`${what} must not be empty`);
|
|
21
|
-
return value;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function xpathSelector(value: string): Selector {
|
|
25
|
-
return { kind: "xpath", value: nonEmpty(value, "an xpath") };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function cssSelector(value: string): Selector {
|
|
29
|
-
return { kind: "css", value: nonEmpty(value, "a css selector") };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function textSelector(value: string, { match = "full", ignoreCase = false }: TextOptions = {}): Selector {
|
|
33
|
-
return { kind: "text", value: nonEmpty(value, "the text"), match, ignoreCase };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function roleSelector(role: string, { name }: { name?: string } = {}): Selector {
|
|
37
|
-
nonEmpty(role, "the role");
|
|
38
|
-
return name === undefined ? { kind: "role", role } : { kind: "role", role, name };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Normalises an xpath. Relative xpaths get a leading `//` (`.//` when `scoped`, i.e. searching inside
|
|
43
|
-
* a parent match). Absolute (`/…`), grouped (`(…)`) and context (`./…`) paths are kept; in a scoped
|
|
44
|
-
* position `//…` is also made relative, so `parent.locator("//x")` means "x inside parent".
|
|
45
|
-
*/
|
|
46
|
-
export function normalizeXpath(xpath: string, scoped: boolean): string {
|
|
47
|
-
if (scoped) {
|
|
48
|
-
if (xpath.startsWith("//")) return "." + xpath;
|
|
49
|
-
return /^[/(.]/.test(xpath) ? xpath : ".//" + xpath;
|
|
50
|
-
}
|
|
51
|
-
return /^[/(.]/.test(xpath) ? xpath : "//" + xpath;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** The BiDi locator for a step. `scoped` is true for every step after the first */
|
|
55
|
-
export function toBiDiLocator(selector: Selector, scoped: boolean): NodeLocator {
|
|
56
|
-
switch (selector.kind) {
|
|
57
|
-
case "xpath":
|
|
58
|
-
return { type: "xpath", value: normalizeXpath(selector.value, scoped) };
|
|
59
|
-
case "css":
|
|
60
|
-
return { type: "css", value: selector.value };
|
|
61
|
-
case "text":
|
|
62
|
-
return {
|
|
63
|
-
type: "innerText",
|
|
64
|
-
value: selector.value,
|
|
65
|
-
matchType: selector.match,
|
|
66
|
-
ignoreCase: selector.ignoreCase,
|
|
67
|
-
};
|
|
68
|
-
case "role":
|
|
69
|
-
return {
|
|
70
|
-
type: "accessibility",
|
|
71
|
-
value: {
|
|
72
|
-
...(selector.role !== undefined && { role: selector.role }),
|
|
73
|
-
...(selector.name !== undefined && { name: selector.name }),
|
|
74
|
-
},
|
|
75
|
-
};
|
|
76
|
-
case "nth":
|
|
77
|
-
throw new Error("nth is resolved by the framework, not by the browser");
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** A short name for error messages */
|
|
82
|
-
export function describeSelector(selector: Selector, scoped: boolean): string {
|
|
83
|
-
switch (selector.kind) {
|
|
84
|
-
case "xpath":
|
|
85
|
-
return normalizeXpath(selector.value, scoped);
|
|
86
|
-
case "css":
|
|
87
|
-
return `css=${selector.value}`;
|
|
88
|
-
case "text": {
|
|
89
|
-
const flags = [
|
|
90
|
-
selector.match === "partial" && "partial",
|
|
91
|
-
selector.ignoreCase && "ignoring case",
|
|
92
|
-
].filter(Boolean);
|
|
93
|
-
return `text=${JSON.stringify(selector.value)}${flags.length ? ` (${flags.join(", ")})` : ""}`;
|
|
94
|
-
}
|
|
95
|
-
case "role":
|
|
96
|
-
return (
|
|
97
|
-
`role=${selector.role ?? ""}` +
|
|
98
|
-
(selector.name === undefined ? "" : `[name=${JSON.stringify(selector.name)}]`)
|
|
99
|
-
);
|
|
100
|
-
case "nth":
|
|
101
|
-
return `nth=${selector.index}`;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function describeChain(chain: readonly Selector[]): string {
|
|
106
|
-
return chain.map((selector, index) => describeSelector(selector, index > 0)).join(" >> ");
|
|
107
|
-
}
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { after, before, test } from "node:test";
|
|
6
|
-
import { Browser } from "../browser/browser.js";
|
|
7
|
-
import type Page from "../browser/page.js";
|
|
8
|
-
import { setContent } from "../testing/browser-fixture.js";
|
|
9
|
-
import { ActionTimeoutError } from "./action-timeout-error.js";
|
|
10
|
-
|
|
11
|
-
let browser: Browser;
|
|
12
|
-
let page: Page;
|
|
13
|
-
const dir = mkdtempSync(path.join(tmpdir(), "samurai-upload-"));
|
|
14
|
-
|
|
15
|
-
before(async () => {
|
|
16
|
-
({ browser, page } = await Browser.launch("firefox", { port: 9236, headless: true }));
|
|
17
|
-
await page.navigateTo("about:blank", "complete");
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
after(async () => {
|
|
21
|
-
await page.dispose();
|
|
22
|
-
await browser?.close();
|
|
23
|
-
rmSync(dir, { recursive: true, force: true });
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
const OPTIONS = { timeout: 20000 };
|
|
27
|
-
function file(name: string, content = name) {
|
|
28
|
-
const full = path.join(dir, name);
|
|
29
|
-
writeFileSync(full, content);
|
|
30
|
-
return full;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Records the chosen files and how often `input` and `change` fired */
|
|
34
|
-
const RECORD = `
|
|
35
|
-
const input = document.getElementById("f");
|
|
36
|
-
const out = document.getElementById("out");
|
|
37
|
-
const counts = { input: 0, change: 0 };
|
|
38
|
-
const render = () => {
|
|
39
|
-
out.textContent = JSON.stringify({ names: [...input.files].map((f) => f.name), ...counts });
|
|
40
|
-
};
|
|
41
|
-
input.addEventListener("input", () => { counts.input++; render(); });
|
|
42
|
-
input.addEventListener("change", () => { counts.change++; render(); });`;
|
|
43
|
-
const result = async () => JSON.parse((await page.locator("div[@id='out']").textContent()) ?? "null");
|
|
44
|
-
|
|
45
|
-
test("uploads one file and the browser fires input and change once", OPTIONS, async () => {
|
|
46
|
-
await setContent(page, `<input type="file" id="f"><div id="out"></div>`, RECORD);
|
|
47
|
-
await page.locator("input[@id='f']").setInputFiles(file("avatar.png"), { timeout: 5000 });
|
|
48
|
-
assert.deepEqual(await result(), { names: ["avatar.png"], input: 1, change: 1 });
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("uploads several files to a multiple input", OPTIONS, async () => {
|
|
52
|
-
await setContent(page, `<input type="file" id="f" multiple><div id="out"></div>`, RECORD);
|
|
53
|
-
await page.locator("input[@id='f']").setInputFiles([file("a.csv"), file("b.csv")], { timeout: 5000 });
|
|
54
|
-
assert.deepEqual((await result()).names, ["a.csv", "b.csv"]);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("the browser reads the file content", OPTIONS, async () => {
|
|
58
|
-
await setContent(
|
|
59
|
-
page,
|
|
60
|
-
`<input type="file" id="f"><div id="out"></div>`,
|
|
61
|
-
`document.getElementById("f").addEventListener("change", async (e) => {
|
|
62
|
-
document.getElementById("out").textContent = await e.target.files[0].text();
|
|
63
|
-
});`,
|
|
64
|
-
);
|
|
65
|
-
await page.locator("input[@id='f']").setInputFiles(file("hello.txt", "hello upload"), { timeout: 5000 });
|
|
66
|
-
await page.locator("div[@id='out']").waitFor({ state: "visible", timeout: 3000 });
|
|
67
|
-
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
68
|
-
assert.equal(await page.locator("div[@id='out']").textContent(), "hello upload");
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("an empty list clears the selection", OPTIONS, async () => {
|
|
72
|
-
await setContent(page, `<input type="file" id="f"><div id="out"></div>`, RECORD);
|
|
73
|
-
const input = page.locator("input[@id='f']");
|
|
74
|
-
await input.setInputFiles(file("first.txt"), { timeout: 5000 });
|
|
75
|
-
await input.setInputFiles([], { timeout: 5000 });
|
|
76
|
-
const state = await result();
|
|
77
|
-
assert.deepEqual(state.names, []);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test("works on a hidden input behind a styled label", OPTIONS, async () => {
|
|
81
|
-
await setContent(
|
|
82
|
-
page,
|
|
83
|
-
`<label for="f" style="display:inline-block;padding:8px">Choose</label>
|
|
84
|
-
<input type="file" id="f" style="display:none"><div id="out"></div>`,
|
|
85
|
-
RECORD,
|
|
86
|
-
);
|
|
87
|
-
const input = page.locator("input[@id='f']");
|
|
88
|
-
assert.equal(await input.isVisible(), false);
|
|
89
|
-
await input.setInputFiles(file("hidden.txt"), { timeout: 5000 });
|
|
90
|
-
assert.deepEqual((await result()).names, ["hidden.txt"]);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test("waits for an input that appears later", OPTIONS, async () => {
|
|
94
|
-
await setContent(
|
|
95
|
-
page,
|
|
96
|
-
`<div id="out"></div>`,
|
|
97
|
-
`setTimeout(() => {
|
|
98
|
-
document.body.insertAdjacentHTML("afterbegin", '<input type="file" id="f">');
|
|
99
|
-
${RECORD}
|
|
100
|
-
}, 300);`,
|
|
101
|
-
);
|
|
102
|
-
await page.locator("input[@id='f']").setInputFiles(file("late.txt"), { timeout: 5000 });
|
|
103
|
-
assert.deepEqual((await result()).names, ["late.txt"]);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test("refuses two files for an input without multiple", OPTIONS, async () => {
|
|
107
|
-
await setContent(page, `<input type="file" id="f"><div id="out"></div>`, RECORD);
|
|
108
|
-
await assert.rejects(
|
|
109
|
-
page.locator("input[@id='f']").setInputFiles([file("x.txt"), file("y.txt")], { timeout: 5000 }),
|
|
110
|
-
/does not accept multiple files/,
|
|
111
|
-
);
|
|
112
|
-
assert.deepEqual((await result().catch(() => null)), null, "nothing was set, so no event rendered");
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test("refuses an element that is not a file input", OPTIONS, async () => {
|
|
116
|
-
await setContent(page, `<input type="text" id="t"><div id="d"></div>`);
|
|
117
|
-
await assert.rejects(
|
|
118
|
-
page.locator("input[@id='t']").setInputFiles(file("t.txt"), { timeout: 5000 }),
|
|
119
|
-
/is not an <input type=file> \(found <input type="text">\)/,
|
|
120
|
-
);
|
|
121
|
-
await assert.rejects(
|
|
122
|
-
page.locator("div[@id='d']").setInputFiles(file("t.txt"), { timeout: 5000 }),
|
|
123
|
-
/found <div>/,
|
|
124
|
-
);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
test("a disabled input times out naming the check", OPTIONS, async () => {
|
|
128
|
-
await setContent(page, `<input type="file" id="f" disabled>`);
|
|
129
|
-
await assert.rejects(
|
|
130
|
-
page.locator("input[@id='f']").setInputFiles(file("d.txt"), { timeout: 400 }),
|
|
131
|
-
(err) => err instanceof ActionTimeoutError && /attached ✓ enabled ✗/.test(err.message),
|
|
132
|
-
);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test("a missing file fails before touching the page", OPTIONS, async () => {
|
|
136
|
-
await setContent(page, `<input type="file" id="f">`);
|
|
137
|
-
await assert.rejects(
|
|
138
|
-
page.locator("input[@id='f']").setInputFiles(path.join(dir, "missing.txt")),
|
|
139
|
-
/file not found/,
|
|
140
|
-
);
|
|
141
|
-
});
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { after, describe, it } from "node:test";
|
|
6
|
-
import { remote, stubConnector, type StubResponse } from "../testing/stub-connector.js";
|
|
7
|
-
import { BiDiError } from "../transport/bidi-error.js";
|
|
8
|
-
import { ActionTimeoutError } from "./action-timeout-error.js";
|
|
9
|
-
import Locator from "./locator.js";
|
|
10
|
-
|
|
11
|
-
const dir = mkdtempSync(path.join(tmpdir(), "samurai-set-files-"));
|
|
12
|
-
after(() => rmSync(dir, { recursive: true, force: true }));
|
|
13
|
-
function file(name: string) {
|
|
14
|
-
const full = path.join(dir, name);
|
|
15
|
-
writeFileSync(full, name);
|
|
16
|
-
return full;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const state = (overrides: object = {}): StubResponse =>
|
|
20
|
-
remote({
|
|
21
|
-
attached: true,
|
|
22
|
-
visible: true,
|
|
23
|
-
enabled: true,
|
|
24
|
-
editable: true,
|
|
25
|
-
box: { x: 0, y: 0, width: 10, height: 10 },
|
|
26
|
-
hitTarget: null,
|
|
27
|
-
...overrides,
|
|
28
|
-
});
|
|
29
|
-
const info = (overrides: object = {}): StubResponse =>
|
|
30
|
-
remote({ tag: "input", type: "file", multiple: false, ...overrides });
|
|
31
|
-
|
|
32
|
-
function setup(...responses: StubResponse[]) {
|
|
33
|
-
const stub = stubConnector(...responses);
|
|
34
|
-
return { locator: new Locator("input[@type='file']", stub.connector, "ctx"), ...stub };
|
|
35
|
-
}
|
|
36
|
-
const methods = (sent: Array<{ method: string }>) => sent.map(({ method }) => method);
|
|
37
|
-
const setFilesParams = (sent: Array<{ method: string; params: unknown }>) =>
|
|
38
|
-
sent.filter(({ method }) => method === "input.setFiles").map(({ params }) => params);
|
|
39
|
-
|
|
40
|
-
describe("setInputFiles", () => {
|
|
41
|
-
it("checks the files, waits for the element, verifies it, then sets the files", async () => {
|
|
42
|
-
const a = file("a.txt");
|
|
43
|
-
const { locator, sent } = setup(state(), info());
|
|
44
|
-
await locator.setInputFiles(a, { timeout: 1000 });
|
|
45
|
-
assert.deepEqual(methods(sent), [
|
|
46
|
-
"browsingContext.locateNodes", // the wait
|
|
47
|
-
"script.callFunction", // its probe
|
|
48
|
-
"browsingContext.locateNodes", // the element to use
|
|
49
|
-
"script.callFunction", // what kind of element it is
|
|
50
|
-
"input.setFiles",
|
|
51
|
-
]);
|
|
52
|
-
assert.deepEqual(setFilesParams(sent), [
|
|
53
|
-
{ context: "ctx", element: { sharedId: "stub-node-0" }, files: [a] },
|
|
54
|
-
]);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("waits only for attached and enabled: no scrolling, no hit test, hidden inputs are fine", async () => {
|
|
58
|
-
const { locator, calls } = setup(state({ visible: false, box: null }), info());
|
|
59
|
-
await locator.setInputFiles(file("hidden.txt"), { timeout: 1000 });
|
|
60
|
-
assert.deepEqual(calls[0]!.args[1], { scroll: false, hitTest: false });
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("resolves relative paths against the working directory", async () => {
|
|
64
|
-
const a = file("relative.txt");
|
|
65
|
-
const { locator, sent } = setup(state(), info());
|
|
66
|
-
await locator.setInputFiles(path.relative(process.cwd(), a), { timeout: 1000 });
|
|
67
|
-
assert.deepEqual((setFilesParams(sent)[0] as { files: string[] }).files, [a]);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("sends several files in order to an input that accepts them", async () => {
|
|
71
|
-
const [a, b] = [file("m1.txt"), file("m2.txt")];
|
|
72
|
-
const { locator, sent } = setup(state(), info({ multiple: true }));
|
|
73
|
-
await locator.setInputFiles([a, b], { timeout: 1000 });
|
|
74
|
-
assert.deepEqual((setFilesParams(sent)[0] as { files: string[] }).files, [a, b]);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("refuses several files for an input without multiple, and sends nothing", async () => {
|
|
78
|
-
const { locator, sent } = setup(state(), info({ multiple: false }));
|
|
79
|
-
await assert.rejects(
|
|
80
|
-
locator.setInputFiles([file("x1.txt"), file("x2.txt")], { timeout: 1000 }),
|
|
81
|
-
/setInputFiles\(\): \/\/input\[@type='file'\] does not accept multiple files/,
|
|
82
|
-
);
|
|
83
|
-
assert.deepEqual(setFilesParams(sent), []);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("an empty list clears the selection, also on an input without multiple, without touching the disk", async () => {
|
|
87
|
-
const { locator, sent } = setup(state(), info({ multiple: false }));
|
|
88
|
-
await locator.setInputFiles([], { timeout: 1000 });
|
|
89
|
-
assert.deepEqual((setFilesParams(sent)[0] as { files: string[] }).files, []);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("fails on a missing file before any command is sent", async () => {
|
|
93
|
-
const missing = path.join(dir, "nope.txt");
|
|
94
|
-
const { locator, sent } = setup(state(), info());
|
|
95
|
-
await assert.rejects(locator.setInputFiles(missing), new Error(`setInputFiles(): file not found: ${missing}`));
|
|
96
|
-
assert.equal(sent.length, 0);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("fails on a directory before any command is sent", async () => {
|
|
100
|
-
const folder = path.join(dir, "folder");
|
|
101
|
-
mkdirSync(folder);
|
|
102
|
-
const { locator, sent } = setup(state(), info());
|
|
103
|
-
await assert.rejects(locator.setInputFiles(folder), new Error(`setInputFiles(): not a file: ${folder}`));
|
|
104
|
-
assert.equal(sent.length, 0);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("names the first bad file when several are given", async () => {
|
|
108
|
-
const good = file("good.txt");
|
|
109
|
-
const bad = path.join(dir, "bad.txt");
|
|
110
|
-
const { locator } = setup(state(), info());
|
|
111
|
-
await assert.rejects(locator.setInputFiles([good, bad]), /file not found: .*bad\.txt/);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("rejects an element that is not a file input, describing what it found", async () => {
|
|
115
|
-
const a = file("wrong.txt");
|
|
116
|
-
const div = setup(state(), info({ tag: "div", type: "" }));
|
|
117
|
-
await assert.rejects(
|
|
118
|
-
div.locator.setInputFiles(a, { timeout: 1000 }),
|
|
119
|
-
/is not an <input type=file> \(found <div>\)/,
|
|
120
|
-
);
|
|
121
|
-
const text = setup(state(), info({ tag: "input", type: "text" }));
|
|
122
|
-
await assert.rejects(
|
|
123
|
-
text.locator.setInputFiles(a, { timeout: 1000 }),
|
|
124
|
-
/is not an <input type=file> \(found <input type="text">\)/,
|
|
125
|
-
);
|
|
126
|
-
assert.deepEqual(setFilesParams(div.sent), []);
|
|
127
|
-
assert.deepEqual(setFilesParams(text.sent), []);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it("times out naming the check that failed", async () => {
|
|
131
|
-
const { locator, sent } = setup(state({ enabled: false }));
|
|
132
|
-
await assert.rejects(locator.setInputFiles(file("t.txt"), { timeout: 250 }), (err) => {
|
|
133
|
-
assert.ok(err instanceof ActionTimeoutError);
|
|
134
|
-
assert.equal(err.action, "setInputFiles");
|
|
135
|
-
assert.equal(
|
|
136
|
-
err.message,
|
|
137
|
-
"setInputFiles(): //input[@type='file'] was not actionable within 250ms\n" +
|
|
138
|
-
" attached ✓ enabled ✗",
|
|
139
|
-
);
|
|
140
|
-
return true;
|
|
141
|
-
});
|
|
142
|
-
assert.deepEqual(setFilesParams(sent), []);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it("times out when the input never appears", async () => {
|
|
146
|
-
const { locator, nodeCounts } = setup(state());
|
|
147
|
-
nodeCounts(0);
|
|
148
|
-
await assert.rejects(
|
|
149
|
-
locator.setInputFiles(file("n.txt"), { timeout: 150 }),
|
|
150
|
-
/setInputFiles\(\): \/\/input\[@type='file'\] was not attached within 150ms/,
|
|
151
|
-
);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it("with timeout 0 it looks once", async () => {
|
|
155
|
-
const { locator, calls } = setup(state({ enabled: false }));
|
|
156
|
-
await assert.rejects(locator.setInputFiles(file("z.txt"), { timeout: 0 }), ActionTimeoutError);
|
|
157
|
-
assert.equal(calls.length, 1);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("waits until the input becomes enabled", async () => {
|
|
161
|
-
const { locator, sent } = setup(state({ enabled: false }), state({ enabled: false }), state(), info());
|
|
162
|
-
await locator.setInputFiles(file("w.txt"), { timeout: 3000 });
|
|
163
|
-
assert.equal(setFilesParams(sent).length, 1);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it("an input that vanished after the wait is reported as not found", async () => {
|
|
167
|
-
const { locator, nodeCounts, sent } = setup(state(), info());
|
|
168
|
-
nodeCounts(1, 0);
|
|
169
|
-
await assert.rejects(
|
|
170
|
-
locator.setInputFiles(file("v.txt"), { timeout: 1000 }),
|
|
171
|
-
/Failed to locate element \/\/input\[@type='file'\]/,
|
|
172
|
-
);
|
|
173
|
-
assert.deepEqual(setFilesParams(sent), []);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it("rethrows what the browser answers", async () => {
|
|
177
|
-
const { locator, failCommand } = setup(state(), info());
|
|
178
|
-
failCommand("input.setFiles", new BiDiError("input.setFiles", "unable to set file input", "read-only"));
|
|
179
|
-
await assert.rejects(locator.setInputFiles(file("e.txt"), { timeout: 1000 }), {
|
|
180
|
-
name: "BiDiError",
|
|
181
|
-
code: "unable to set file input",
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it("works through a chained locator", async () => {
|
|
186
|
-
const stub = stubConnector(state(), info());
|
|
187
|
-
const chained = new Locator("//form", stub.connector, "ctx").getByCss("input[type=file]");
|
|
188
|
-
await chained.setInputFiles(file("c.txt"), { timeout: 1000 });
|
|
189
|
-
assert.equal(setFilesParams(stub.sent).length, 1);
|
|
190
|
-
});
|
|
191
|
-
});
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import { TEXT_LOCATE } from "./text-locator.js";
|
|
4
|
-
|
|
5
|
-
/** A tiny DOM: elements with text and children, enough to run TEXT_LOCATE in Node */
|
|
6
|
-
class FakeElement {
|
|
7
|
-
parentElement: FakeElement | null = null;
|
|
8
|
-
children: FakeElement[] = [];
|
|
9
|
-
constructor(
|
|
10
|
-
public tagName: string,
|
|
11
|
-
private ownText = "",
|
|
12
|
-
) {}
|
|
13
|
-
add(...kids: FakeElement[]) {
|
|
14
|
-
for (const kid of kids) {
|
|
15
|
-
kid.parentElement = this;
|
|
16
|
-
this.children.push(kid);
|
|
17
|
-
}
|
|
18
|
-
return this;
|
|
19
|
-
}
|
|
20
|
-
get innerText(): string {
|
|
21
|
-
return [this.ownText, ...this.children.map((kid) => kid.innerText)].filter(Boolean).join(" ");
|
|
22
|
-
}
|
|
23
|
-
get textContent(): string {
|
|
24
|
-
return this.innerText;
|
|
25
|
-
}
|
|
26
|
-
descendants(): FakeElement[] {
|
|
27
|
-
return this.children.flatMap((kid) => [kid, ...kid.descendants()]);
|
|
28
|
-
}
|
|
29
|
-
querySelectorAll() {
|
|
30
|
-
return this.descendants();
|
|
31
|
-
}
|
|
32
|
-
compareDocumentPosition(other: FakeElement) {
|
|
33
|
-
const order = all(root());
|
|
34
|
-
return order.indexOf(other) > order.indexOf(this) ? 4 : 2;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
let documentRoot: FakeElement;
|
|
38
|
-
const root = () => documentRoot;
|
|
39
|
-
const all = (from: FakeElement): FakeElement[] => [from, ...from.descendants()];
|
|
40
|
-
|
|
41
|
-
function run(start: FakeElement[] | null, value: string, match: string, ignoreCase: boolean) {
|
|
42
|
-
const fn = new Function(
|
|
43
|
-
"document",
|
|
44
|
-
`return (${TEXT_LOCATE})`,
|
|
45
|
-
)({ documentElement: documentRoot, querySelectorAll: () => documentRoot.descendants() }) as (
|
|
46
|
-
starts: unknown[],
|
|
47
|
-
value: string,
|
|
48
|
-
match: string,
|
|
49
|
-
ignoreCase: boolean,
|
|
50
|
-
) => FakeElement[];
|
|
51
|
-
return fn(start ?? [], value, match, ignoreCase);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
describe("TEXT_LOCATE", () => {
|
|
55
|
-
it("compiles as a function declaration", () => {
|
|
56
|
-
assert.doesNotThrow(() => new Function(`return (${TEXT_LOCATE})`));
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("finds the innermost element with the full text", () => {
|
|
60
|
-
const span = new FakeElement("SPAN", "Go");
|
|
61
|
-
const div = new FakeElement("DIV").add(span);
|
|
62
|
-
documentRoot = new FakeElement("HTML").add(new FakeElement("BODY").add(div));
|
|
63
|
-
assert.deepEqual(run(null, "Go", "full", false), [span]);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("normalises whitespace on both sides", () => {
|
|
67
|
-
const p = new FakeElement("P", " Hello \n world ");
|
|
68
|
-
documentRoot = new FakeElement("HTML").add(p);
|
|
69
|
-
assert.deepEqual(run(null, "Hello world", "full", false), [p]);
|
|
70
|
-
assert.deepEqual(run(null, " Hello world", "full", false), [p]);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("full needs the whole text, partial a substring", () => {
|
|
74
|
-
const p = new FakeElement("P", "Sign in now");
|
|
75
|
-
documentRoot = new FakeElement("HTML").add(p);
|
|
76
|
-
assert.deepEqual(run(null, "Sign in", "full", false), []);
|
|
77
|
-
assert.deepEqual(run(null, "Sign in", "partial", false), [p]);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("ignoreCase compares lower-cased", () => {
|
|
81
|
-
const p = new FakeElement("P", "Sign IN");
|
|
82
|
-
documentRoot = new FakeElement("HTML").add(p);
|
|
83
|
-
assert.deepEqual(run(null, "sign in", "full", false), []);
|
|
84
|
-
assert.deepEqual(run(null, "sign in", "full", true), [p]);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("returns the parent when the text is only complete across its children", () => {
|
|
88
|
-
const b = new FakeElement("B", "Hel");
|
|
89
|
-
const p = new FakeElement("P", "lo").add(b);
|
|
90
|
-
documentRoot = new FakeElement("HTML").add(p);
|
|
91
|
-
// innerText is "lo Hel" here, so use text that only the parent contains in full
|
|
92
|
-
assert.deepEqual(run(null, "lo Hel", "full", false), [p]);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("returns every innermost match in document order", () => {
|
|
96
|
-
const a = new FakeElement("LI", "x");
|
|
97
|
-
const b = new FakeElement("LI", "x");
|
|
98
|
-
documentRoot = new FakeElement("HTML").add(new FakeElement("UL").add(a, b));
|
|
99
|
-
assert.deepEqual(run(null, "x", "full", false), [a, b]);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("searches only inside the start nodes, without repeating a match found from two of them", () => {
|
|
103
|
-
const inside = new FakeElement("SPAN", "hit");
|
|
104
|
-
const nested = new FakeElement("DIV").add(inside);
|
|
105
|
-
const outside = new FakeElement("SPAN", "hit");
|
|
106
|
-
const form = new FakeElement("FORM").add(nested);
|
|
107
|
-
documentRoot = new FakeElement("HTML").add(form, outside);
|
|
108
|
-
assert.deepEqual(run([form], "hit", "full", false), [inside]);
|
|
109
|
-
assert.deepEqual(run([form, nested], "hit", "full", false), [inside]);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("skips non-rendered elements", () => {
|
|
113
|
-
const script = new FakeElement("SCRIPT", "Go");
|
|
114
|
-
const p = new FakeElement("P", "Go");
|
|
115
|
-
documentRoot = new FakeElement("HTML").add(new FakeElement("HEAD").add(script), p);
|
|
116
|
-
assert.deepEqual(run(null, "Go", "full", false), [p]);
|
|
117
|
-
});
|
|
118
|
-
});
|