@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,591 @@
|
|
|
1
|
+
import { BiDiError } from "../transport/bidi-error.js";
|
|
2
|
+
import { callFunction, ScriptError } from "../script/call-function.js";
|
|
3
|
+
import { CALL_PROBE, HELPER_SANDBOX, HELPERS_MISSING, } from "../script/helpers.js";
|
|
4
|
+
import { ElementHandle } from "../script/element-handle.js";
|
|
5
|
+
import { takeScreenshot, } from "../browser/screenshot.js";
|
|
6
|
+
import { assertIsFile, resolveFiles } from "../browser/file-paths.js";
|
|
7
|
+
import { resolveTimeout, waitUntil, WaitTimeoutError, } from "../wait/wait-until.js";
|
|
8
|
+
import { ActionTimeoutError } from "./action-timeout-error.js";
|
|
9
|
+
import { allPass, DETACHED_STATE, evaluateChecks, parseElementState, PROBE_ELEMENT, } from "./element-state.js";
|
|
10
|
+
import { InvalidSelectorError, UnsupportedOperationError, } from "./selector-errors.js";
|
|
11
|
+
import { LABEL_LOCATE } from "./label-locator.js";
|
|
12
|
+
import { TEXT_LOCATE } from "./text-locator.js";
|
|
13
|
+
import { cssSelector, describeChain, describeSelector, labelSelector, roleSelector, testIdSelector, textSelector, toBiDiLocator, xpathSelector, } from "./selector.js";
|
|
14
|
+
export { InvalidSelectorError, UnsupportedOperationError, } from "./selector-errors.js";
|
|
15
|
+
/** Locator types a connection's browser rejected, so they are not tried again on every poll */
|
|
16
|
+
const unsupportedByConnector = new WeakMap();
|
|
17
|
+
function unsupportedTypes(connector) {
|
|
18
|
+
let types = unsupportedByConnector.get(connector);
|
|
19
|
+
if (!types)
|
|
20
|
+
unsupportedByConnector.set(connector, (types = new Set()));
|
|
21
|
+
return types;
|
|
22
|
+
}
|
|
23
|
+
/** What `setInputFiles` needs to know about the element before it sends files */
|
|
24
|
+
const FILE_INPUT_INFO = `(el) => ({
|
|
25
|
+
tag: el.tagName.toLowerCase(),
|
|
26
|
+
type: (el.getAttribute("type") || "").toLowerCase(),
|
|
27
|
+
multiple: Boolean(el.multiple),
|
|
28
|
+
})`;
|
|
29
|
+
const FILE_INPUT_CHECKS = ["attached", "enabled"];
|
|
30
|
+
const SCREENSHOT_CHECKS = ["attached", "visible"];
|
|
31
|
+
const CLICK_CHECKS = [
|
|
32
|
+
"attached",
|
|
33
|
+
"visible",
|
|
34
|
+
"stable",
|
|
35
|
+
"enabled",
|
|
36
|
+
"hit target",
|
|
37
|
+
];
|
|
38
|
+
const FILL_CHECKS = [
|
|
39
|
+
"attached",
|
|
40
|
+
"visible",
|
|
41
|
+
"enabled",
|
|
42
|
+
"editable",
|
|
43
|
+
"hit target",
|
|
44
|
+
];
|
|
45
|
+
const ATTACHED_ONLY = ["attached"];
|
|
46
|
+
/** Unicode code point WebDriver uses for the Backspace key */
|
|
47
|
+
const BACKSPACE = "";
|
|
48
|
+
export default class Locator {
|
|
49
|
+
/**
|
|
50
|
+
* `selector` is an xpath, or a chain of selector steps that each search inside the previous step's matches.
|
|
51
|
+
* `helpers` is the page's helper realm. With it the actionability probe is installed once per
|
|
52
|
+
* document and called by name; without it (a locator built by hand) the probe source is sent on every poll.
|
|
53
|
+
* `alternatives` are fallback locators tried in order whenever this chain matches nothing (see `withFallbacks`).
|
|
54
|
+
*/
|
|
55
|
+
constructor(selector, biDiConnector, contextId, helpers, alternatives = []) {
|
|
56
|
+
this.chain =
|
|
57
|
+
typeof selector === "string" ? [xpathSelector(selector)] : selector;
|
|
58
|
+
this.biDiConnector = biDiConnector;
|
|
59
|
+
this.contextId = contextId;
|
|
60
|
+
this.helpers = helpers;
|
|
61
|
+
this.alternatives = alternatives;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The description of this locator used in error messages, e.g. `//form >> role=button[name="Save"]`;
|
|
65
|
+
* fallbacks follow as `or …`
|
|
66
|
+
*/
|
|
67
|
+
get selector() {
|
|
68
|
+
return [
|
|
69
|
+
describeChain(this.chain),
|
|
70
|
+
...this.alternatives.map((alt) => alt.selector),
|
|
71
|
+
].join(" or ");
|
|
72
|
+
}
|
|
73
|
+
/** Steps that keep the fallbacks: each fallback gets the same step, so "inside A or B" stays meaningful */
|
|
74
|
+
child(step) {
|
|
75
|
+
return new Locator([...this.chain, step], this.biDiConnector, this.contextId, this.helpers, this.alternatives.map((alt) => alt.child(step)));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* A locator that tries this one first and, whenever it matches nothing, each of `fallbacks` in order;
|
|
79
|
+
* the first one with a match wins. For steps with several ways to find the same element (a test id,
|
|
80
|
+
* then role and name, then text), most stable first. `matchedBy` says which one matched last, so a
|
|
81
|
+
* self-healing layer can tell the primary locator has gone stale.
|
|
82
|
+
* @example
|
|
83
|
+
* page.getByTestId("save").withFallbacks(page.getByRole("button", { name: "Save" }), page.getByText("Save"));
|
|
84
|
+
*/
|
|
85
|
+
withFallbacks(...fallbacks) {
|
|
86
|
+
return new Locator(this.chain, this.biDiConnector, this.contextId, this.helpers, [...this.alternatives, ...fallbacks]);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Which locator found elements the last time this one searched: index 0 is the primary, 1 the first
|
|
90
|
+
* fallback, and so on. `undefined` before any search or when the last search found nothing.
|
|
91
|
+
*/
|
|
92
|
+
get matchedBy() {
|
|
93
|
+
return this.matched;
|
|
94
|
+
}
|
|
95
|
+
/** Elements matching the label text (`<label>`, `aria-labelledby`, `aria-label`), inside this locator's matches */
|
|
96
|
+
getByLabel(text, options) {
|
|
97
|
+
return this.child(labelSelector(text, options));
|
|
98
|
+
}
|
|
99
|
+
/** Elements whose `data-testid` attribute equals `testId`, inside this locator's matches */
|
|
100
|
+
getByTestId(testId) {
|
|
101
|
+
return this.child(testIdSelector(testId));
|
|
102
|
+
}
|
|
103
|
+
/** Elements matching `xpath` inside this locator's matches (relative and `//` paths search inside them) */
|
|
104
|
+
locator(xpath) {
|
|
105
|
+
return this.child(xpathSelector(xpath));
|
|
106
|
+
}
|
|
107
|
+
/** Elements matching the CSS selector inside this locator's matches */
|
|
108
|
+
getByCss(css) {
|
|
109
|
+
return this.child(cssSelector(css));
|
|
110
|
+
}
|
|
111
|
+
/** Elements whose text matches, inside this locator's matches. Exact match unless `match: "partial"` */
|
|
112
|
+
getByText(text, options) {
|
|
113
|
+
return this.child(textSelector(text, options));
|
|
114
|
+
}
|
|
115
|
+
/** Elements with the ARIA role (and accessible name, when given), inside this locator's matches */
|
|
116
|
+
getByRole(role, options) {
|
|
117
|
+
return this.child(roleSelector(role, options));
|
|
118
|
+
}
|
|
119
|
+
/** One locator per element currently matching, in document order */
|
|
120
|
+
async all() {
|
|
121
|
+
if (this.alternatives.length > 0) {
|
|
122
|
+
// Pin to whichever of the primary and the fallbacks matches, so each result is one element
|
|
123
|
+
const first = await this.firstMatching();
|
|
124
|
+
return first ? first.all() : [];
|
|
125
|
+
}
|
|
126
|
+
const count = await this.count();
|
|
127
|
+
const only = this.chain.length === 1 ? this.chain[0] : undefined;
|
|
128
|
+
return Array.from({ length: count }, (_, index) => only?.kind === "xpath"
|
|
129
|
+
? // A positional xpath keeps working without a second search for the whole chain
|
|
130
|
+
new Locator(`(${describeSelector(only, false)})[${index + 1}]`, this.biDiConnector, this.contextId, this.helpers)
|
|
131
|
+
: this.child({ kind: "nth", index }));
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Finds the elements the chain matches, in document order. Every step searches inside the previous
|
|
135
|
+
* step's matches; `max` limits the final result. Nothing is cached: the element that matches "first"
|
|
136
|
+
* can change between polls, and a removed node stays resolvable in the browser.
|
|
137
|
+
*/
|
|
138
|
+
async resolve(max) {
|
|
139
|
+
this.matched = undefined;
|
|
140
|
+
const own = await this.resolveChain(max);
|
|
141
|
+
if (own.length > 0) {
|
|
142
|
+
this.matched = { index: 0, selector: describeChain(this.chain) };
|
|
143
|
+
return own;
|
|
144
|
+
}
|
|
145
|
+
for (const [index, alternative] of this.alternatives.entries()) {
|
|
146
|
+
const found = await alternative.resolve(max);
|
|
147
|
+
if (found.length > 0) {
|
|
148
|
+
this.matched = { index: index + 1, selector: alternative.selector };
|
|
149
|
+
return found;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
/** The primary or the first fallback that currently matches, without its own fallbacks */
|
|
155
|
+
async firstMatching() {
|
|
156
|
+
if ((await this.resolveChain(1)).length > 0)
|
|
157
|
+
return this.withoutFallbacks();
|
|
158
|
+
for (const alternative of this.alternatives) {
|
|
159
|
+
const found = await alternative.firstMatching();
|
|
160
|
+
if (found)
|
|
161
|
+
return found;
|
|
162
|
+
}
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
withoutFallbacks() {
|
|
166
|
+
return new Locator(this.chain, this.biDiConnector, this.contextId, this.helpers);
|
|
167
|
+
}
|
|
168
|
+
async resolveChain(max) {
|
|
169
|
+
let nodes;
|
|
170
|
+
for (const [index, selector] of this.chain.entries()) {
|
|
171
|
+
if (selector.kind === "nth") {
|
|
172
|
+
const picked = nodes?.[selector.index];
|
|
173
|
+
nodes = picked ? [picked] : [];
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
nodes = await this.locateStep(selector, index > 0, nodes, index === this.chain.length - 1 ? max : undefined);
|
|
177
|
+
}
|
|
178
|
+
if (nodes.length === 0)
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
return nodes ?? [];
|
|
182
|
+
}
|
|
183
|
+
async resolveOne() {
|
|
184
|
+
return (await this.resolve(1))[0] ?? null;
|
|
185
|
+
}
|
|
186
|
+
async locateStep(selector, scoped, startNodes, max) {
|
|
187
|
+
if (selector.kind === "label")
|
|
188
|
+
return this.locateInPage(LABEL_LOCATE, selector, startNodes, max);
|
|
189
|
+
if (selector.kind === "text" &&
|
|
190
|
+
unsupportedTypes(this.biDiConnector).has("innerText")) {
|
|
191
|
+
return this.locateByText(selector, startNodes, max);
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
return await this.locateNatively(selector, scoped, startNodes, max);
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
if (err instanceof BiDiError && err.code === "invalid selector") {
|
|
198
|
+
throw new InvalidSelectorError(describeSelector(selector, scoped), err.message);
|
|
199
|
+
}
|
|
200
|
+
if (err instanceof BiDiError && err.code === "unsupported operation") {
|
|
201
|
+
if (selector.kind === "text") {
|
|
202
|
+
// The browser has no innerText locator (Firefox): remember that and search in the page instead
|
|
203
|
+
unsupportedTypes(this.biDiConnector).add("innerText");
|
|
204
|
+
return this.locateByText(selector, startNodes, max);
|
|
205
|
+
}
|
|
206
|
+
throw new UnsupportedOperationError(`${describeSelector(selector, scoped)}: this kind of locator is not supported by the browser (${err.message})`);
|
|
207
|
+
}
|
|
208
|
+
throw err;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async locateNatively(selector, scoped, startNodes, max) {
|
|
212
|
+
const { nodes } = await this.biDiConnector.send("browsingContext.locateNodes", {
|
|
213
|
+
context: this.contextId,
|
|
214
|
+
locator: toBiDiLocator(selector, scoped),
|
|
215
|
+
...(max !== undefined && { maxNodeCount: max }),
|
|
216
|
+
// Only the references are needed; skip serializing the matched subtrees
|
|
217
|
+
serializationOptions: { maxDomDepth: 0 },
|
|
218
|
+
...(startNodes && {
|
|
219
|
+
startNodes: startNodes.map(({ sharedId }) => ({ sharedId })),
|
|
220
|
+
}),
|
|
221
|
+
});
|
|
222
|
+
const found = new Map();
|
|
223
|
+
for (const node of nodes) {
|
|
224
|
+
if (node.sharedId !== undefined && !found.has(node.sharedId)) {
|
|
225
|
+
found.set(node.sharedId, new ElementHandle(node.sharedId, node.handle));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return [...found.values()];
|
|
229
|
+
}
|
|
230
|
+
/** Text search done in the page, for browsers without BiDi's innerText locator */
|
|
231
|
+
locateByText(selector, startNodes, max) {
|
|
232
|
+
return this.locateInPage(TEXT_LOCATE, selector, startNodes, max);
|
|
233
|
+
}
|
|
234
|
+
/** Runs a page-side search `(starts, value, match, ignoreCase) => elements` */
|
|
235
|
+
async locateInPage(fn, selector, startNodes, max) {
|
|
236
|
+
const found = await this.call(fn, [
|
|
237
|
+
startNodes ?? [],
|
|
238
|
+
selector.value,
|
|
239
|
+
selector.match,
|
|
240
|
+
selector.ignoreCase,
|
|
241
|
+
]);
|
|
242
|
+
return max === undefined ? found : found.slice(0, max);
|
|
243
|
+
}
|
|
244
|
+
/** Calls `fn` in the page; a throwing function becomes a "Failed to locate element" error */
|
|
245
|
+
async call(fn, args, sandbox) {
|
|
246
|
+
try {
|
|
247
|
+
return await callFunction(this.biDiConnector, this.contextId, fn, args, {
|
|
248
|
+
awaitPromise: false,
|
|
249
|
+
...(sandbox !== undefined && { sandbox }),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
if (err instanceof ScriptError) {
|
|
254
|
+
throw new Error(`Failed to locate element ${this.selector}. Details: ${err.text}`);
|
|
255
|
+
}
|
|
256
|
+
throw err;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Calls a function on the first matching element: `body` sees it as `el`, followed by `params`.
|
|
261
|
+
* Answers `whenMissing` when nothing matches, or the element disappeared meanwhile.
|
|
262
|
+
*/
|
|
263
|
+
async callOnElement(whenMissing, body, params = [], args = []) {
|
|
264
|
+
const el = await this.resolveOne();
|
|
265
|
+
if (!el)
|
|
266
|
+
return whenMissing;
|
|
267
|
+
try {
|
|
268
|
+
return await this.call(`(el${params.map((param) => `, ${param}`).join("")}) => { ${body} }`, [el, ...args]);
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
if (err instanceof BiDiError && err.code === "no such node")
|
|
272
|
+
return whenMissing;
|
|
273
|
+
throw err;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/** Like `callOnElement`, for operations that cannot go on without the element */
|
|
277
|
+
async callOnExistingElement(body) {
|
|
278
|
+
const missing = Symbol("missing");
|
|
279
|
+
const result = await this.callOnElement(missing, body);
|
|
280
|
+
if (result === missing) {
|
|
281
|
+
throw new Error(`Failed to locate element ${this.selector}. Details: no element matches`);
|
|
282
|
+
}
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
/** Reads the element's actionability state: one search, one probe */
|
|
286
|
+
async probeState(options) {
|
|
287
|
+
const el = await this.resolveOne();
|
|
288
|
+
if (!el)
|
|
289
|
+
return DETACHED_STATE;
|
|
290
|
+
try {
|
|
291
|
+
return parseElementState(await this.callProbe(el, options));
|
|
292
|
+
}
|
|
293
|
+
catch (err) {
|
|
294
|
+
// The document changed between the search and the probe
|
|
295
|
+
if (err instanceof BiDiError && err.code === "no such node")
|
|
296
|
+
return DETACHED_STATE;
|
|
297
|
+
throw err;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
async callProbe(el, options) {
|
|
301
|
+
const args = [el, { ...options }];
|
|
302
|
+
if (!this.helpers)
|
|
303
|
+
return this.call(PROBE_ELEMENT, args);
|
|
304
|
+
await this.helpers.ensureInstalled();
|
|
305
|
+
let result = await this.call(CALL_PROBE, args, HELPER_SANDBOX);
|
|
306
|
+
if (result === HELPERS_MISSING) {
|
|
307
|
+
// A document got past the registration (e.g. created while it was being set up): install again once
|
|
308
|
+
await this.helpers.reinstall();
|
|
309
|
+
result = await this.call(CALL_PROBE, args, HELPER_SANDBOX);
|
|
310
|
+
if (result === HELPERS_MISSING) {
|
|
311
|
+
throw new ScriptError("the samurai helpers are missing in the page after reinstalling", "probeElement");
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return result;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Probes until every required check passes and returns that state.
|
|
318
|
+
* Throws ActionTimeoutError describing the last probe on timeout.
|
|
319
|
+
*/
|
|
320
|
+
async waitForActionable(action, checks, probe, options) {
|
|
321
|
+
const timeout = await resolveTimeout(options?.timeout);
|
|
322
|
+
// Stability compares two probes; with timeout 0 there is only one
|
|
323
|
+
const required = options?.force
|
|
324
|
+
? ATTACHED_ONLY
|
|
325
|
+
: timeout === 0
|
|
326
|
+
? checks.filter((check) => check !== "stable")
|
|
327
|
+
: checks;
|
|
328
|
+
let lastChecks;
|
|
329
|
+
try {
|
|
330
|
+
return await waitUntil(() => this.probeState(probe), (current, previous) => {
|
|
331
|
+
lastChecks = evaluateChecks(required, current, previous);
|
|
332
|
+
return allPass(lastChecks);
|
|
333
|
+
}, { timeout });
|
|
334
|
+
}
|
|
335
|
+
catch (err) {
|
|
336
|
+
if (!(err instanceof WaitTimeoutError))
|
|
337
|
+
throw err;
|
|
338
|
+
const last = err.last;
|
|
339
|
+
throw new ActionTimeoutError({
|
|
340
|
+
action,
|
|
341
|
+
selector: this.selector,
|
|
342
|
+
timeout,
|
|
343
|
+
reason: !last
|
|
344
|
+
? "unreadable"
|
|
345
|
+
: last.attached
|
|
346
|
+
? "not-actionable"
|
|
347
|
+
: "not-attached",
|
|
348
|
+
checks: lastChecks,
|
|
349
|
+
coveredBy: lastChecks?.["hit target"] === "fail"
|
|
350
|
+
? (last?.hitTarget ?? undefined)
|
|
351
|
+
: undefined,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/** Waits until the element is attached, visible, stable, enabled and not covered, then clicks its centre with real pointer input */
|
|
356
|
+
async click(options) {
|
|
357
|
+
const state = await this.waitForActionable("click", CLICK_CHECKS, { scroll: true, hitTest: true }, options);
|
|
358
|
+
// "attached" is always required, so the box is present
|
|
359
|
+
await this.clickRect(state.box);
|
|
360
|
+
}
|
|
361
|
+
/** Simulate a click event on the center off a page element.
|
|
362
|
+
* This is mostly used internally to focus input elements when using the `fill()` method
|
|
363
|
+
* @example
|
|
364
|
+
* const container = page.locator("div[@id=['clickable-element']");
|
|
365
|
+
* const rect = await container.getBoundingClientRect();
|
|
366
|
+
* await container.clickRect(rect);
|
|
367
|
+
*/
|
|
368
|
+
async clickRect(rect) {
|
|
369
|
+
await this.biDiConnector.send("input.performActions", {
|
|
370
|
+
context: this.contextId,
|
|
371
|
+
actions: [
|
|
372
|
+
{
|
|
373
|
+
type: "pointer",
|
|
374
|
+
id: "samurai-pointer",
|
|
375
|
+
actions: [
|
|
376
|
+
{
|
|
377
|
+
type: "pointerMove",
|
|
378
|
+
x: rect.x + rect.width / 2,
|
|
379
|
+
y: rect.y + rect.height / 2,
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
type: "pointerDown",
|
|
383
|
+
button: 0,
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
type: "pointerUp",
|
|
387
|
+
button: 0,
|
|
388
|
+
},
|
|
389
|
+
],
|
|
390
|
+
},
|
|
391
|
+
],
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Sets the files of an `<input type=file>` (an empty list clears them); the browser fires `input` and `change`.
|
|
396
|
+
* Paths are resolved against the working directory and must exist on the machine the browser runs on.
|
|
397
|
+
* Waits for the input to be attached and enabled; it need not be visible, file inputs are often hidden behind a label.
|
|
398
|
+
* @example
|
|
399
|
+
* await page.locator("input[@type='file']").setInputFiles("./fixtures/avatar.png");
|
|
400
|
+
* await page.locator("input[@type='file']").setInputFiles([]);
|
|
401
|
+
*/
|
|
402
|
+
async setInputFiles(files, options) {
|
|
403
|
+
const paths = resolveFiles(files);
|
|
404
|
+
for (const file of paths)
|
|
405
|
+
await assertIsFile(file, "setInputFiles");
|
|
406
|
+
await this.waitForActionable("setInputFiles", FILE_INPUT_CHECKS, { scroll: false, hitTest: false }, options?.timeout === undefined ? undefined : { timeout: options.timeout });
|
|
407
|
+
const missing = () => new Error(`Failed to locate element ${this.selector}. Details: no element matches`);
|
|
408
|
+
const el = await this.resolveOne();
|
|
409
|
+
if (!el)
|
|
410
|
+
throw missing();
|
|
411
|
+
let info;
|
|
412
|
+
try {
|
|
413
|
+
info = await this.call(FILE_INPUT_INFO, [el]);
|
|
414
|
+
}
|
|
415
|
+
catch (err) {
|
|
416
|
+
if (err instanceof BiDiError && err.code === "no such node")
|
|
417
|
+
throw missing();
|
|
418
|
+
throw err;
|
|
419
|
+
}
|
|
420
|
+
if (info.tag !== "input" || info.type !== "file") {
|
|
421
|
+
const found = info.tag === "input"
|
|
422
|
+
? `<input type="${info.type || "text"}">`
|
|
423
|
+
: `<${info.tag}>`;
|
|
424
|
+
throw new Error(`setInputFiles(): ${this.selector} is not an <input type=file> (found ${found})`);
|
|
425
|
+
}
|
|
426
|
+
if (paths.length > 1 && !info.multiple) {
|
|
427
|
+
throw new Error(`setInputFiles(): ${this.selector} does not accept multiple files`);
|
|
428
|
+
}
|
|
429
|
+
await this.biDiConnector.send("input.setFiles", {
|
|
430
|
+
context: this.contextId,
|
|
431
|
+
element: { sharedId: el.sharedId },
|
|
432
|
+
files: paths,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Takes a screenshot of just this element, after scrolling it into view and waiting until it is attached and
|
|
437
|
+
* visible. Returns the image (also written to `options.path`).
|
|
438
|
+
* @example
|
|
439
|
+
* await page.locator("div[@id='chart']").screenshot({ path: "out/chart.png" });
|
|
440
|
+
*/
|
|
441
|
+
async screenshot(options = {}) {
|
|
442
|
+
const { timeout, ...capture } = options;
|
|
443
|
+
await this.waitForActionable("screenshot", SCREENSHOT_CHECKS, { scroll: true, hitTest: false }, timeout === undefined ? undefined : { timeout });
|
|
444
|
+
const el = await this.resolveOne();
|
|
445
|
+
if (!el) {
|
|
446
|
+
throw new Error(`Failed to locate element ${this.selector}. Details: no element matches`);
|
|
447
|
+
}
|
|
448
|
+
return takeScreenshot(this.biDiConnector, this.contextId, capture, {
|
|
449
|
+
sharedId: el.sharedId,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
/** Waits until the element is attached, then focuses it */
|
|
453
|
+
async focus(options) {
|
|
454
|
+
await this.waitForActionable("focus", ATTACHED_ONLY, { scroll: false, hitTest: false }, options);
|
|
455
|
+
await this.callOnExistingElement(`el.focus();`);
|
|
456
|
+
}
|
|
457
|
+
/** Waits until the element reaches `state` (default "visible"); does not scroll */
|
|
458
|
+
async waitFor({ state = "visible", timeout, } = {}) {
|
|
459
|
+
const resolved = await resolveTimeout(timeout);
|
|
460
|
+
const reached = {
|
|
461
|
+
attached: (current) => current.attached,
|
|
462
|
+
detached: (current) => !current.attached,
|
|
463
|
+
visible: (current) => current.attached && current.visible,
|
|
464
|
+
hidden: (current) => !current.attached || !current.visible,
|
|
465
|
+
};
|
|
466
|
+
try {
|
|
467
|
+
await waitUntil(() => this.probeState({ scroll: false, hitTest: false }), reached[state], {
|
|
468
|
+
timeout: resolved,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
catch (err) {
|
|
472
|
+
if (!(err instanceof WaitTimeoutError))
|
|
473
|
+
throw err;
|
|
474
|
+
throw new ActionTimeoutError({
|
|
475
|
+
action: "waitFor",
|
|
476
|
+
selector: this.selector,
|
|
477
|
+
timeout: resolved,
|
|
478
|
+
reason: err.last === undefined ? "unreadable" : "wrong-state",
|
|
479
|
+
state,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
/** Returns the browser's getBoundingClientRect result for the given element */
|
|
484
|
+
async getBoundingClientRect() {
|
|
485
|
+
const rect = await this.callOnExistingElement(`
|
|
486
|
+
const { x, y, width, height, top, right, bottom, left } = el.getBoundingClientRect();
|
|
487
|
+
return { x, y, width, height, top, right, bottom, left };
|
|
488
|
+
`);
|
|
489
|
+
if (typeof rect !== "object" || rect === null) {
|
|
490
|
+
throw new Error(`Expected to receive an object but instead received "${describeType(rect)}"`);
|
|
491
|
+
}
|
|
492
|
+
return rect;
|
|
493
|
+
}
|
|
494
|
+
stringOrNull(value) {
|
|
495
|
+
if (typeof value === "string")
|
|
496
|
+
return value;
|
|
497
|
+
if (value === null)
|
|
498
|
+
return null;
|
|
499
|
+
throw new Error(`Expected a string or null but received "${describeType(value)}"`);
|
|
500
|
+
}
|
|
501
|
+
/** Whether the element exists, has a non-empty box and is not hidden by CSS */
|
|
502
|
+
async isVisible() {
|
|
503
|
+
const result = await this.callOnElement(false, `
|
|
504
|
+
const rect = el.getBoundingClientRect();
|
|
505
|
+
const style = getComputedStyle(el);
|
|
506
|
+
return rect.width > 0 && rect.height > 0 &&
|
|
507
|
+
style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0";
|
|
508
|
+
`);
|
|
509
|
+
return result === true;
|
|
510
|
+
}
|
|
511
|
+
/** Trimmed text content, or null when the element is missing */
|
|
512
|
+
async textContent() {
|
|
513
|
+
return this.stringOrNull(await this.callOnElement(null, `return el.textContent.trim();`));
|
|
514
|
+
}
|
|
515
|
+
/** Value of an input/textarea/select, or null when missing */
|
|
516
|
+
async inputValue() {
|
|
517
|
+
return this.stringOrNull(await this.callOnElement(null, `return typeof el.value === "string" ? el.value : null;`));
|
|
518
|
+
}
|
|
519
|
+
/** Attribute value, or null when the element or attribute is missing */
|
|
520
|
+
async getAttribute(name) {
|
|
521
|
+
return this.stringOrNull(await this.callOnElement(null, `return el.getAttribute(name);`, ["name"], [name]));
|
|
522
|
+
}
|
|
523
|
+
/** References to the elements currently matching, in document order. Internal: for tooling that identifies elements */
|
|
524
|
+
async elements() {
|
|
525
|
+
return this.resolve();
|
|
526
|
+
}
|
|
527
|
+
/** Number of elements currently matching */
|
|
528
|
+
async count() {
|
|
529
|
+
return (await this.resolve()).length;
|
|
530
|
+
}
|
|
531
|
+
/** Whether the element exists and is not disabled; does not wait */
|
|
532
|
+
async isEnabled() {
|
|
533
|
+
const state = await this.probeState({ scroll: false, hitTest: false });
|
|
534
|
+
return state.attached && state.enabled;
|
|
535
|
+
}
|
|
536
|
+
/** Whether the element exists and accepts typing (enabled, not readonly, a text field); does not wait */
|
|
537
|
+
async isEditable() {
|
|
538
|
+
const state = await this.probeState({ scroll: false, hitTest: false });
|
|
539
|
+
return state.attached && state.editable;
|
|
540
|
+
}
|
|
541
|
+
/** Wait for certain time (ms). Default: 300ms */
|
|
542
|
+
async wait(duration = 300) {
|
|
543
|
+
await this.biDiConnector.send("input.performActions", {
|
|
544
|
+
context: this.contextId,
|
|
545
|
+
actions: [
|
|
546
|
+
{
|
|
547
|
+
type: "none",
|
|
548
|
+
id: "samurai-sleep",
|
|
549
|
+
actions: [
|
|
550
|
+
{
|
|
551
|
+
type: "pause",
|
|
552
|
+
duration,
|
|
553
|
+
},
|
|
554
|
+
],
|
|
555
|
+
},
|
|
556
|
+
],
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
/** Waits until the field is editable and not covered, then replaces its value by typing `value`; an empty string clears it */
|
|
560
|
+
async fill(value, options) {
|
|
561
|
+
const state = await this.waitForActionable("fill", FILL_CHECKS, { scroll: true, hitTest: true }, options);
|
|
562
|
+
// Typing over the selected existing value replaces it; Backspace clears it for an empty value
|
|
563
|
+
const keys = value === "" ? BACKSPACE : value;
|
|
564
|
+
const actions = Array.from(keys).flatMap((char) => [
|
|
565
|
+
{
|
|
566
|
+
type: "keyDown",
|
|
567
|
+
value: char,
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
type: "keyUp",
|
|
571
|
+
value: char,
|
|
572
|
+
},
|
|
573
|
+
]);
|
|
574
|
+
await this.clickRect(state.box);
|
|
575
|
+
await this.wait();
|
|
576
|
+
await this.callOnElement(undefined, `if (typeof el.select === "function") el.select();`);
|
|
577
|
+
await this.biDiConnector.send("input.performActions", {
|
|
578
|
+
context: this.contextId,
|
|
579
|
+
actions: [
|
|
580
|
+
{
|
|
581
|
+
actions,
|
|
582
|
+
type: "key",
|
|
583
|
+
id: "samurai-keyboard",
|
|
584
|
+
},
|
|
585
|
+
],
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
function describeType(value) {
|
|
590
|
+
return value === null ? "null" : typeof value;
|
|
591
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** The browser rejected a selector (bad css or xpath); retrying cannot help */
|
|
2
|
+
export declare class InvalidSelectorError extends Error {
|
|
3
|
+
readonly selector: string;
|
|
4
|
+
constructor(selector: string, reason: string);
|
|
5
|
+
}
|
|
6
|
+
/** The browser does not implement this kind of locator */
|
|
7
|
+
export declare class UnsupportedOperationError extends Error {
|
|
8
|
+
constructor(message: string);
|
|
9
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** The browser rejected a selector (bad css or xpath); retrying cannot help */
|
|
2
|
+
export class InvalidSelectorError extends Error {
|
|
3
|
+
constructor(selector, reason) {
|
|
4
|
+
super(`Invalid selector ${selector}: ${reason}`);
|
|
5
|
+
this.name = "InvalidSelectorError";
|
|
6
|
+
this.selector = selector;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
/** The browser does not implement this kind of locator */
|
|
10
|
+
export class UnsupportedOperationError extends Error {
|
|
11
|
+
constructor(message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "UnsupportedOperationError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { NodeLocator } from "../types/bidi-modules/browsing-context.js";
|
|
2
|
+
/** One step of a locator. A locator is a chain of them, each searching inside the previous step's matches */
|
|
3
|
+
export type Selector = {
|
|
4
|
+
kind: "xpath";
|
|
5
|
+
value: string;
|
|
6
|
+
} | {
|
|
7
|
+
kind: "css";
|
|
8
|
+
value: string;
|
|
9
|
+
} | {
|
|
10
|
+
kind: "text";
|
|
11
|
+
value: string;
|
|
12
|
+
match: "full" | "partial";
|
|
13
|
+
ignoreCase: boolean;
|
|
14
|
+
} | {
|
|
15
|
+
kind: "role";
|
|
16
|
+
role?: string;
|
|
17
|
+
name?: string;
|
|
18
|
+
}
|
|
19
|
+
/** Form controls named by a `<label>`, `aria-labelledby` or `aria-label` whose text matches */
|
|
20
|
+
| {
|
|
21
|
+
kind: "label";
|
|
22
|
+
value: string;
|
|
23
|
+
match: "full" | "partial";
|
|
24
|
+
ignoreCase: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** Elements whose `data-testid` attribute equals `value` */
|
|
27
|
+
| {
|
|
28
|
+
kind: "testid";
|
|
29
|
+
value: string;
|
|
30
|
+
}
|
|
31
|
+
/** Picks the n-th (0-based) of everything the chain so far matches; `all()` builds these */
|
|
32
|
+
| {
|
|
33
|
+
kind: "nth";
|
|
34
|
+
index: number;
|
|
35
|
+
};
|
|
36
|
+
export interface TextOptions {
|
|
37
|
+
/** @default "full" */
|
|
38
|
+
match?: "full" | "partial";
|
|
39
|
+
/** @default false */
|
|
40
|
+
ignoreCase?: boolean;
|
|
41
|
+
}
|
|
42
|
+
export declare function xpathSelector(value: string): Selector;
|
|
43
|
+
export declare function cssSelector(value: string): Selector;
|
|
44
|
+
export declare function textSelector(value: string, { match, ignoreCase }?: TextOptions): Selector;
|
|
45
|
+
export declare function labelSelector(value: string, { match, ignoreCase }?: TextOptions): Selector;
|
|
46
|
+
export declare function testIdSelector(value: string): Selector;
|
|
47
|
+
export declare function roleSelector(role: string, { name }?: {
|
|
48
|
+
name?: string;
|
|
49
|
+
}): Selector;
|
|
50
|
+
/**
|
|
51
|
+
* Normalises an xpath. Relative xpaths get a leading `//` (`.//` when `scoped`, i.e. searching inside
|
|
52
|
+
* a parent match). Absolute (`/…`), grouped (`(…)`) and context (`./…`) paths are kept; in a scoped
|
|
53
|
+
* position `//…` is also made relative, so `parent.locator("//x")` means "x inside parent".
|
|
54
|
+
*/
|
|
55
|
+
export declare function normalizeXpath(xpath: string, scoped: boolean): string;
|
|
56
|
+
/** The BiDi locator for a step. `scoped` is true for every step after the first */
|
|
57
|
+
export declare function toBiDiLocator(selector: Selector, scoped: boolean): NodeLocator;
|
|
58
|
+
/** A short name for error messages */
|
|
59
|
+
export declare function describeSelector(selector: Selector, scoped: boolean): string;
|
|
60
|
+
export declare function describeChain(chain: readonly Selector[]): string;
|