@itmetsam/samurai-framework 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +129 -0
- package/bin/samurai.mjs +31 -0
- package/dist/api.js +3 -0
- package/dist/assert/assertion-error.d.ts +23 -0
- package/dist/assert/assertion-error.js +45 -0
- package/dist/assert/expect.d.ts +55 -0
- package/dist/assert/expect.js +188 -0
- package/dist/browser/browser-context.d.ts +91 -0
- package/dist/browser/browser-context.js +146 -0
- package/dist/browser/browser-finder.d.ts +2 -0
- package/dist/browser/browser-finder.js +95 -0
- package/dist/browser/browser.d.ts +95 -0
- package/dist/browser/browser.js +372 -0
- package/dist/browser/context-tree.d.ts +23 -0
- package/dist/browser/context-tree.js +60 -0
- package/dist/browser/dialog.d.ts +37 -0
- package/dist/browser/dialog.js +61 -0
- package/dist/browser/download.d.ts +71 -0
- package/dist/browser/download.js +137 -0
- package/dist/browser/downloads-dir.d.ts +8 -0
- package/dist/browser/downloads-dir.js +38 -0
- package/dist/browser/emulation.d.ts +61 -0
- package/dist/browser/emulation.js +144 -0
- package/dist/browser/file-chooser.d.ts +39 -0
- package/dist/browser/file-chooser.js +86 -0
- package/dist/browser/file-paths.d.ts +4 -0
- package/dist/browser/file-paths.js +18 -0
- package/dist/browser/index.d.ts +4 -0
- package/dist/browser/index.js +4 -0
- package/dist/browser/navigation-error.d.ts +13 -0
- package/dist/browser/navigation-error.js +19 -0
- package/dist/browser/page-logs.d.ts +86 -0
- package/dist/browser/page-logs.js +149 -0
- package/dist/browser/page-wait.d.ts +9 -0
- package/dist/browser/page-wait.js +17 -0
- package/dist/browser/page.d.ts +370 -0
- package/dist/browser/page.js +1038 -0
- package/dist/browser/pdf.d.ts +72 -0
- package/dist/browser/pdf.js +37 -0
- package/dist/browser/screenshot.d.ts +29 -0
- package/dist/browser/screenshot.js +42 -0
- package/dist/cli/args.d.ts +35 -0
- package/dist/cli/args.js +69 -0
- package/dist/cli/cli.cli-test.d.ts +1 -0
- package/dist/cli/cli.cli-test.js +170 -0
- package/dist/cli/console-reporter.d.ts +15 -0
- package/dist/cli/console-reporter.js +52 -0
- package/dist/cli/init.d.ts +7 -0
- package/dist/cli/init.js +61 -0
- package/dist/cli/main.d.ts +12 -0
- package/dist/cli/main.js +156 -0
- package/dist/config/config.d.ts +24 -0
- package/dist/config/config.js +42 -0
- package/dist/config/default.d.ts +2 -0
- package/dist/config/default.js +3 -0
- package/dist/config/mask.d.ts +11 -0
- package/dist/config/mask.js +85 -0
- package/dist/config/run-settings.d.ts +28 -0
- package/dist/config/run-settings.js +61 -0
- package/dist/config/secrets.d.ts +10 -0
- package/dist/config/secrets.js +67 -0
- package/dist/config/variables.d.ts +11 -0
- package/dist/config/variables.js +26 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +15 -0
- package/dist/locator/action-timeout-error.d.ts +25 -0
- package/dist/locator/action-timeout-error.js +35 -0
- package/dist/locator/element-state.d.ts +44 -0
- package/dist/locator/element-state.js +109 -0
- package/dist/locator/label-locator.d.ts +13 -0
- package/dist/locator/label-locator.js +49 -0
- package/dist/locator/locator.d.ts +168 -0
- package/dist/locator/locator.js +591 -0
- package/dist/locator/selector-errors.d.ts +9 -0
- package/dist/locator/selector-errors.js +15 -0
- package/dist/locator/selector.d.ts +60 -0
- package/dist/locator/selector.js +113 -0
- package/dist/locator/text-locator.d.ts +13 -0
- package/dist/logger/index.d.ts +2 -0
- package/dist/logger/index.js +30 -0
- package/dist/network/data-collector.d.ts +26 -0
- package/dist/network/data-collector.js +77 -0
- package/dist/network/network-tracker.d.ts +94 -0
- package/dist/network/network-tracker.js +198 -0
- package/dist/network/network-wait.d.ts +16 -0
- package/dist/network/network-wait.js +24 -0
- package/dist/network/response.d.ts +22 -0
- package/dist/network/response.js +42 -0
- package/dist/network/router.d.ts +120 -0
- package/dist/network/router.js +343 -0
- package/dist/network/url-match.d.ts +8 -0
- package/dist/network/url-match.js +38 -0
- package/dist/recorder/apply.d.ts +3 -0
- package/dist/recorder/apply.js +10 -0
- package/dist/recorder/capture.d.ts +18 -0
- package/dist/recorder/capture.js +198 -0
- package/dist/recorder/index.d.ts +3 -0
- package/dist/recorder/index.js +3 -0
- package/dist/recorder/normalise.d.ts +68 -0
- package/dist/recorder/normalise.js +104 -0
- package/dist/recorder/ranking.d.ts +9 -0
- package/dist/recorder/ranking.js +21 -0
- package/dist/recorder/recorder.d.ts +52 -0
- package/dist/recorder/recorder.js +143 -0
- package/dist/recorder/session.d.ts +49 -0
- package/dist/recorder/session.js +113 -0
- package/dist/runner/module-type.d.ts +6 -0
- package/dist/runner/module-type.js +29 -0
- package/dist/runner/page-logs-report.d.ts +40 -0
- package/dist/runner/page-logs-report.js +44 -0
- package/dist/runner/prepare-run.d.ts +14 -0
- package/dist/runner/prepare-run.js +20 -0
- package/dist/runner/project.d.ts +17 -0
- package/dist/runner/project.js +33 -0
- package/dist/runner/registry.d.ts +7 -0
- package/dist/runner/registry.js +40 -0
- package/dist/runner/reporter.d.ts +43 -0
- package/dist/runner/reporter.js +83 -0
- package/dist/runner/run.d.ts +27 -0
- package/dist/runner/run.js +34 -0
- package/dist/runner/test-error.d.ts +3 -0
- package/dist/runner/test-error.js +18 -0
- package/dist/runner/test-runner.d.ts +44 -0
- package/dist/runner/test-runner.js +205 -0
- package/dist/script/call-function.d.ts +34 -0
- package/dist/script/call-function.js +71 -0
- package/dist/script/element-handle.d.ts +10 -0
- package/{src/script/element-handle.ts → dist/script/element-handle.js} +4 -4
- package/dist/script/helpers.d.ts +28 -0
- package/dist/script/helpers.js +50 -0
- package/dist/script/preload.d.ts +31 -0
- package/dist/script/preload.js +71 -0
- package/dist/script/serialize.d.ts +29 -0
- package/dist/script/serialize.js +207 -0
- package/dist/steps/build.d.ts +5 -0
- package/dist/steps/build.js +38 -0
- package/dist/steps/edit.d.ts +30 -0
- package/dist/steps/edit.js +80 -0
- package/dist/steps/emit.d.ts +6 -0
- package/dist/steps/emit.js +73 -0
- package/dist/steps/index.d.ts +5 -0
- package/dist/steps/index.js +4 -0
- package/dist/steps/model.d.ts +103 -0
- package/dist/steps/model.js +1 -0
- package/dist/steps/parse.d.ts +3 -0
- package/dist/steps/parse.js +272 -0
- package/dist/transport/bidi-connection.d.ts +46 -0
- package/dist/transport/bidi-connection.js +176 -0
- package/dist/transport/bidi-error.d.ts +10 -0
- package/dist/transport/bidi-error.js +10 -0
- package/dist/transport/subscriptions.d.ts +36 -0
- package/dist/transport/subscriptions.js +81 -0
- package/{src → dist}/types/test.d.ts +10 -4
- package/dist/wait/wait-until.d.ts +20 -0
- package/dist/wait/wait-until.js +63 -0
- package/package.json +44 -7
- package/.claude/settings.local.json +0 -7
- package/.env.example +0 -3
- package/.github/actions/setup/action.yml +0 -16
- package/.github/workflows/ci.yml +0 -71
- package/.trunk/configs/.markdownlint.yaml +0 -2
- package/.trunk/configs/.yamllint.yaml +0 -7
- package/.trunk/configs/svgo.config.mjs +0 -14
- package/.trunk/trunk.yaml +0 -34
- package/docs/superpowers/bidi-roadmap.md +0 -60
- package/docs/superpowers/plans/2026-09-29-auto-wait.md +0 -1827
- package/docs/superpowers/plans/2026-09-29-expect.md +0 -1350
- package/docs/superpowers/plans/2026-10-01-ci.md +0 -350
- package/docs/superpowers/plans/2026-10-01-test-api-alignment.md +0 -1821
- package/docs/superpowers/recorder-roadmap.md +0 -52
- package/docs/superpowers/specs/2026-09-29-auto-wait-design.md +0 -185
- package/docs/superpowers/specs/2026-09-29-bidi-foundation-design.md +0 -130
- package/docs/superpowers/specs/2026-09-29-contexts-and-storage-design.md +0 -88
- package/docs/superpowers/specs/2026-09-29-downloads-and-file-dialogs-design.md +0 -94
- package/docs/superpowers/specs/2026-09-29-emulation-design.md +0 -88
- package/docs/superpowers/specs/2026-09-29-expect-design.md +0 -130
- package/docs/superpowers/specs/2026-09-29-locate-nodes-design.md +0 -90
- package/docs/superpowers/specs/2026-09-29-logs-and-errors-design.md +0 -87
- package/docs/superpowers/specs/2026-09-29-navigation-waits-design.md +0 -83
- package/docs/superpowers/specs/2026-09-29-network-data-design.md +0 -78
- package/docs/superpowers/specs/2026-09-29-network-interception-design.md +0 -91
- package/docs/superpowers/specs/2026-09-29-network-tracking-design.md +0 -86
- package/docs/superpowers/specs/2026-09-29-page-api-design.md +0 -90
- package/docs/superpowers/specs/2026-09-29-preload-scripts-design.md +0 -82
- package/docs/superpowers/specs/2026-09-29-script-call-function-design.md +0 -107
- package/docs/superpowers/specs/2026-09-29-set-files-design.md +0 -62
- package/docs/superpowers/specs/2026-09-29-user-prompts-design.md +0 -75
- package/docs/superpowers/specs/2026-10-01-ci-design.md +0 -106
- package/docs/superpowers/specs/2026-10-01-test-api-alignment-design.md +0 -241
- package/public/logo-dark.svg +0 -236
- package/public/logo-light.svg +0 -262
- package/samurai.config.ts +0 -7
- package/src/assert/assertion-error.test.ts +0 -55
- package/src/assert/assertion-error.ts +0 -62
- package/src/assert/expect-locator.test.ts +0 -217
- package/src/assert/expect.test.ts +0 -73
- package/src/assert/expect.ts +0 -299
- package/src/browser/browser-context.browser-test.ts +0 -201
- package/src/browser/browser-context.test.ts +0 -407
- package/src/browser/browser-context.ts +0 -208
- package/src/browser/browser-finder.ts +0 -110
- package/src/browser/browser.test.ts +0 -88
- package/src/browser/browser.ts +0 -515
- package/src/browser/context-tree.test.ts +0 -86
- package/src/browser/context-tree.ts +0 -70
- package/src/browser/dialog.browser-test.ts +0 -144
- package/src/browser/dialog.test.ts +0 -322
- package/src/browser/dialog.ts +0 -83
- package/src/browser/download.test.ts +0 -161
- package/src/browser/download.ts +0 -172
- package/src/browser/downloads-dir.test.ts +0 -75
- package/src/browser/downloads-dir.ts +0 -39
- package/src/browser/emulation.browser-test.ts +0 -186
- package/src/browser/emulation.test.ts +0 -200
- package/src/browser/emulation.ts +0 -204
- package/src/browser/file-chooser.test.ts +0 -142
- package/src/browser/file-chooser.ts +0 -99
- package/src/browser/file-paths.ts +0 -18
- package/src/browser/goto.browser-test.ts +0 -44
- package/src/browser/navigation-error.ts +0 -26
- package/src/browser/navigation.browser-test.ts +0 -172
- package/src/browser/page-api.browser-test.ts +0 -228
- package/src/browser/page-logs.browser-test.ts +0 -119
- package/src/browser/page-logs.test.ts +0 -212
- package/src/browser/page-logs.ts +0 -208
- package/src/browser/page-wait.ts +0 -19
- package/src/browser/page.test.ts +0 -1567
- package/src/browser/page.ts +0 -1176
- package/src/browser/pdf.ts +0 -62
- package/src/browser/screenshot.ts +0 -65
- package/src/browser/transfers.browser-test.ts +0 -180
- package/src/config/config.ts +0 -33
- package/src/config/default.ts +0 -5
- package/src/config/fixtures.test.ts +0 -137
- package/src/config/mask.test.ts +0 -143
- package/src/config/mask.ts +0 -87
- package/src/config/run-settings.test.ts +0 -163
- package/src/config/run-settings.ts +0 -114
- package/src/config/secrets.ts +0 -70
- package/src/config/variables.ts +0 -31
- package/src/index.ts +0 -10
- package/src/locator/action-timeout-error.test.ts +0 -83
- package/src/locator/action-timeout-error.ts +0 -72
- package/src/locator/element-state.test.ts +0 -181
- package/src/locator/element-state.ts +0 -159
- package/src/locator/locate-nodes.browser-test.ts +0 -169
- package/src/locator/locator-actions.test.ts +0 -355
- package/src/locator/locator-chain.test.ts +0 -292
- package/src/locator/locator.browser-test.ts +0 -137
- package/src/locator/locator.test.ts +0 -179
- package/src/locator/locator.ts +0 -656
- package/src/locator/selector-errors.ts +0 -18
- package/src/locator/selector.test.ts +0 -124
- package/src/locator/selector.ts +0 -107
- package/src/locator/set-files.browser-test.ts +0 -141
- package/src/locator/set-files.test.ts +0 -191
- package/src/locator/text-locator.test.ts +0 -118
- package/src/logger/index.test.ts +0 -28
- package/src/logger/index.ts +0 -40
- package/src/network/data-collector.test.ts +0 -111
- package/src/network/data-collector.ts +0 -92
- package/src/network/network-data.browser-test.ts +0 -192
- package/src/network/network-tracker.browser-test.ts +0 -151
- package/src/network/network-tracker.test.ts +0 -307
- package/src/network/network-tracker.ts +0 -286
- package/src/network/network-wait.ts +0 -28
- package/src/network/response.test.ts +0 -93
- package/src/network/response.ts +0 -58
- package/src/network/router.browser-test.ts +0 -260
- package/src/network/router.test.ts +0 -528
- package/src/network/router.ts +0 -420
- package/src/network/url-match.test.ts +0 -69
- package/src/network/url-match.ts +0 -41
- package/src/runner/page-logs-report.test.ts +0 -144
- package/src/runner/page-logs-report.ts +0 -82
- package/src/runner/prepare-run.test.ts +0 -33
- package/src/runner/prepare-run.ts +0 -36
- package/src/runner/registry.test.ts +0 -59
- package/src/runner/registry.ts +0 -46
- package/src/runner/reporter.test.ts +0 -73
- package/src/runner/reporter.ts +0 -109
- package/src/runner/test-error.test.ts +0 -39
- package/src/runner/test-error.ts +0 -20
- package/src/runner/test-runner.ts +0 -184
- package/src/script/call-function.browser-test.ts +0 -112
- package/src/script/call-function.test.ts +0 -174
- package/src/script/call-function.ts +0 -113
- package/src/script/helpers.test.ts +0 -116
- package/src/script/helpers.ts +0 -68
- package/src/script/preload.browser-test.ts +0 -144
- package/src/script/preload.test.ts +0 -180
- package/src/script/preload.ts +0 -102
- package/src/script/serialize.test.ts +0 -182
- package/src/script/serialize.ts +0 -203
- package/src/testing/browser-fixture.ts +0 -18
- package/src/testing/fake-websocket.ts +0 -72
- package/src/testing/stub-connector.ts +0 -154
- package/src/tests/index.spec.ts +0 -30
- package/src/transport/bidi-connection.test.ts +0 -155
- package/src/transport/bidi-connection.ts +0 -256
- package/src/transport/bidi-error.ts +0 -23
- package/src/transport/foundation.browser-test.ts +0 -73
- package/src/transport/subscriptions.test.ts +0 -140
- package/src/transport/subscriptions.ts +0 -110
- package/src/types/bidi-commands.test-d.ts +0 -55
- package/src/wait/wait-until.test.ts +0 -117
- package/src/wait/wait-until.ts +0 -86
- package/tsconfig.json +0 -44
- /package/{src/api.ts → dist/api.d.ts} +0 -0
- /package/{src/locator/text-locator.ts → dist/locator/text-locator.js} +0 -0
- /package/{src → dist}/types/bidi-modules/browser.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/browsing-context.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/emulation.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/input.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/log.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/network.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/permissions.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/script.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/session.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/storage.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/web-extensions.d.ts +0 -0
- /package/{src → dist}/types/bidi-protocols/command.d.ts +0 -0
- /package/{src → dist}/types/bidi-protocols/error.d.ts +0 -0
- /package/{src → dist}/types/bidi-protocols/event.d.ts +0 -0
- /package/{src → dist}/types/bidi.d.ts +0 -0
- /package/{src → dist}/types/browser.d.ts +0 -0
- /package/{src → dist}/types/config.d.ts +0 -0
- /package/{src → dist}/types/env.d.ts +0 -0
package/src/locator/locator.ts
DELETED
|
@@ -1,656 +0,0 @@
|
|
|
1
|
-
import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
2
|
-
import { BiDiError } from "../transport/bidi-error.js";
|
|
3
|
-
import type {
|
|
4
|
-
BrowsingContext,
|
|
5
|
-
ElementRectangle,
|
|
6
|
-
} from "../types/bidi-modules/browsing-context.js";
|
|
7
|
-
import type {
|
|
8
|
-
KeyDownAction,
|
|
9
|
-
KeyUpAction,
|
|
10
|
-
} from "../types/bidi-modules/input.js";
|
|
11
|
-
import { callFunction, ScriptError } from "../script/call-function.js";
|
|
12
|
-
import {
|
|
13
|
-
CALL_PROBE,
|
|
14
|
-
HELPER_SANDBOX,
|
|
15
|
-
HELPERS_MISSING,
|
|
16
|
-
type HelperInstaller,
|
|
17
|
-
} from "../script/helpers.js";
|
|
18
|
-
import { ElementHandle } from "../script/element-handle.js";
|
|
19
|
-
import { takeScreenshot, type ElementScreenshotOptions } from "../browser/screenshot.js";
|
|
20
|
-
import { assertIsFile, resolveFiles } from "../browser/file-paths.js";
|
|
21
|
-
import type { Arg } from "../script/serialize.js";
|
|
22
|
-
import { resolveTimeout, waitUntil, WaitTimeoutError } from "../wait/wait-until.js";
|
|
23
|
-
import { ActionTimeoutError } from "./action-timeout-error.js";
|
|
24
|
-
import {
|
|
25
|
-
allPass,
|
|
26
|
-
DETACHED_STATE,
|
|
27
|
-
evaluateChecks,
|
|
28
|
-
parseElementState,
|
|
29
|
-
PROBE_ELEMENT,
|
|
30
|
-
type CheckName,
|
|
31
|
-
type CheckResults,
|
|
32
|
-
type ElementState,
|
|
33
|
-
type ProbeOptions,
|
|
34
|
-
type WaitForState,
|
|
35
|
-
} from "./element-state.js";
|
|
36
|
-
|
|
37
|
-
import { InvalidSelectorError, UnsupportedOperationError } from "./selector-errors.js";
|
|
38
|
-
import { TEXT_LOCATE } from "./text-locator.js";
|
|
39
|
-
import {
|
|
40
|
-
cssSelector,
|
|
41
|
-
describeChain,
|
|
42
|
-
describeSelector,
|
|
43
|
-
roleSelector,
|
|
44
|
-
textSelector,
|
|
45
|
-
toBiDiLocator,
|
|
46
|
-
xpathSelector,
|
|
47
|
-
type Selector,
|
|
48
|
-
type TextOptions,
|
|
49
|
-
} from "./selector.js";
|
|
50
|
-
|
|
51
|
-
export type { WaitForState } from "./element-state.js";
|
|
52
|
-
export { InvalidSelectorError, UnsupportedOperationError } from "./selector-errors.js";
|
|
53
|
-
export type { TextOptions } from "./selector.js";
|
|
54
|
-
|
|
55
|
-
export interface ActionOptions {
|
|
56
|
-
/**
|
|
57
|
-
* Time (ms) to wait for the element to become actionable. Defaults to config `expect.timeout`, then 5000.
|
|
58
|
-
* 0 checks once without waiting; the stability check is skipped because it needs two probes
|
|
59
|
-
*/
|
|
60
|
-
timeout?: number;
|
|
61
|
-
/** Skip every actionability check except "attached" */
|
|
62
|
-
force?: boolean;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Locator types a connection's browser rejected, so they are not tried again on every poll */
|
|
66
|
-
const unsupportedByConnector = new WeakMap<BiDiConnector, Set<string>>();
|
|
67
|
-
|
|
68
|
-
function unsupportedTypes(connector: BiDiConnector): Set<string> {
|
|
69
|
-
let types = unsupportedByConnector.get(connector);
|
|
70
|
-
if (!types) unsupportedByConnector.set(connector, (types = new Set()));
|
|
71
|
-
return types;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** What `setInputFiles` needs to know about the element before it sends files */
|
|
75
|
-
const FILE_INPUT_INFO = `(el) => ({
|
|
76
|
-
tag: el.tagName.toLowerCase(),
|
|
77
|
-
type: (el.getAttribute("type") || "").toLowerCase(),
|
|
78
|
-
multiple: Boolean(el.multiple),
|
|
79
|
-
})`;
|
|
80
|
-
|
|
81
|
-
const FILE_INPUT_CHECKS: readonly CheckName[] = ["attached", "enabled"];
|
|
82
|
-
const SCREENSHOT_CHECKS: readonly CheckName[] = ["attached", "visible"];
|
|
83
|
-
|
|
84
|
-
const CLICK_CHECKS: readonly CheckName[] = ["attached", "visible", "stable", "enabled", "hit target"];
|
|
85
|
-
const FILL_CHECKS: readonly CheckName[] = ["attached", "visible", "enabled", "editable", "hit target"];
|
|
86
|
-
const ATTACHED_ONLY: readonly CheckName[] = ["attached"];
|
|
87
|
-
|
|
88
|
-
/** Unicode code point WebDriver uses for the Backspace key */
|
|
89
|
-
const BACKSPACE = "";
|
|
90
|
-
|
|
91
|
-
export default class Locator {
|
|
92
|
-
private biDiConnector: BiDiConnector;
|
|
93
|
-
private contextId: BrowsingContext;
|
|
94
|
-
private chain: readonly Selector[];
|
|
95
|
-
private helpers: HelperInstaller | undefined;
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* `selector` is an xpath, or a chain of selector steps that each search inside the previous step's matches.
|
|
99
|
-
* `helpers` is the page's helper realm. With it the actionability probe is installed once per
|
|
100
|
-
* document and called by name; without it (a locator built by hand) the probe source is sent on every poll.
|
|
101
|
-
*/
|
|
102
|
-
constructor(
|
|
103
|
-
selector: string | readonly Selector[],
|
|
104
|
-
biDiConnector: BiDiConnector,
|
|
105
|
-
contextId: BrowsingContext,
|
|
106
|
-
helpers?: HelperInstaller,
|
|
107
|
-
) {
|
|
108
|
-
this.chain = typeof selector === "string" ? [xpathSelector(selector)] : selector;
|
|
109
|
-
this.biDiConnector = biDiConnector;
|
|
110
|
-
this.contextId = contextId;
|
|
111
|
-
this.helpers = helpers;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** The description of this locator used in error messages, e.g. `//form >> role=button[name="Save"]` */
|
|
115
|
-
public get selector(): string {
|
|
116
|
-
return describeChain(this.chain);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
private child(step: Selector): Locator {
|
|
120
|
-
return new Locator([...this.chain, step], this.biDiConnector, this.contextId, this.helpers);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** Elements matching `xpath` inside this locator's matches (relative and `//` paths search inside them) */
|
|
124
|
-
public locator(xpath: string): Locator {
|
|
125
|
-
return this.child(xpathSelector(xpath));
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** Elements matching the CSS selector inside this locator's matches */
|
|
129
|
-
public getByCss(css: string): Locator {
|
|
130
|
-
return this.child(cssSelector(css));
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/** Elements whose text matches, inside this locator's matches. Exact match unless `match: "partial"` */
|
|
134
|
-
public getByText(text: string, options?: TextOptions): Locator {
|
|
135
|
-
return this.child(textSelector(text, options));
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/** Elements with the ARIA role (and accessible name, when given), inside this locator's matches */
|
|
139
|
-
public getByRole(role: string, options?: { name?: string }): Locator {
|
|
140
|
-
return this.child(roleSelector(role, options));
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/** One locator per element currently matching, in document order */
|
|
144
|
-
public async all(): Promise<Locator[]> {
|
|
145
|
-
const count = await this.count();
|
|
146
|
-
const only = this.chain.length === 1 ? this.chain[0]! : undefined;
|
|
147
|
-
return Array.from({ length: count }, (_, index) =>
|
|
148
|
-
only?.kind === "xpath"
|
|
149
|
-
? // A positional xpath keeps working without a second search for the whole chain
|
|
150
|
-
new Locator(
|
|
151
|
-
`(${describeSelector(only, false)})[${index + 1}]`,
|
|
152
|
-
this.biDiConnector,
|
|
153
|
-
this.contextId,
|
|
154
|
-
this.helpers,
|
|
155
|
-
)
|
|
156
|
-
: this.child({ kind: "nth", index }),
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Finds the elements the chain matches, in document order. Every step searches inside the previous
|
|
162
|
-
* step's matches; `max` limits the final result. Nothing is cached: the element that matches "first"
|
|
163
|
-
* can change between polls, and a removed node stays resolvable in the browser.
|
|
164
|
-
*/
|
|
165
|
-
private async resolve(max?: number): Promise<ElementHandle[]> {
|
|
166
|
-
let nodes: ElementHandle[] | undefined;
|
|
167
|
-
for (const [index, selector] of this.chain.entries()) {
|
|
168
|
-
if (selector.kind === "nth") {
|
|
169
|
-
const picked = nodes?.[selector.index];
|
|
170
|
-
nodes = picked ? [picked] : [];
|
|
171
|
-
} else {
|
|
172
|
-
nodes = await this.locateStep(selector, index > 0, nodes, index === this.chain.length - 1 ? max : undefined);
|
|
173
|
-
}
|
|
174
|
-
if (nodes.length === 0) return [];
|
|
175
|
-
}
|
|
176
|
-
return nodes ?? [];
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
private async resolveOne(): Promise<ElementHandle | null> {
|
|
180
|
-
return (await this.resolve(1))[0] ?? null;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
private async locateStep(
|
|
184
|
-
selector: Selector,
|
|
185
|
-
scoped: boolean,
|
|
186
|
-
startNodes: ElementHandle[] | undefined,
|
|
187
|
-
max: number | undefined,
|
|
188
|
-
): Promise<ElementHandle[]> {
|
|
189
|
-
if (selector.kind === "text" && unsupportedTypes(this.biDiConnector).has("innerText")) {
|
|
190
|
-
return this.locateByText(selector, startNodes, max);
|
|
191
|
-
}
|
|
192
|
-
try {
|
|
193
|
-
return await this.locateNatively(selector, scoped, startNodes, max);
|
|
194
|
-
} catch (err) {
|
|
195
|
-
if (err instanceof BiDiError && err.code === "invalid selector") {
|
|
196
|
-
throw new InvalidSelectorError(describeSelector(selector, scoped), err.message);
|
|
197
|
-
}
|
|
198
|
-
if (err instanceof BiDiError && err.code === "unsupported operation") {
|
|
199
|
-
if (selector.kind === "text") {
|
|
200
|
-
// The browser has no innerText locator (Firefox): remember that and search in the page instead
|
|
201
|
-
unsupportedTypes(this.biDiConnector).add("innerText");
|
|
202
|
-
return this.locateByText(selector, startNodes, max);
|
|
203
|
-
}
|
|
204
|
-
throw new UnsupportedOperationError(
|
|
205
|
-
`${describeSelector(selector, scoped)}: this kind of locator is not supported by the browser (${err.message})`,
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
throw err;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
private async locateNatively(
|
|
213
|
-
selector: Selector,
|
|
214
|
-
scoped: boolean,
|
|
215
|
-
startNodes: ElementHandle[] | undefined,
|
|
216
|
-
max: number | undefined,
|
|
217
|
-
): Promise<ElementHandle[]> {
|
|
218
|
-
const { nodes } = await this.biDiConnector.send("browsingContext.locateNodes", {
|
|
219
|
-
context: this.contextId,
|
|
220
|
-
locator: toBiDiLocator(selector, scoped),
|
|
221
|
-
...(max !== undefined && { maxNodeCount: max }),
|
|
222
|
-
// Only the references are needed; skip serializing the matched subtrees
|
|
223
|
-
serializationOptions: { maxDomDepth: 0 },
|
|
224
|
-
...(startNodes && {
|
|
225
|
-
startNodes: startNodes.map(({ sharedId }) => ({ sharedId })),
|
|
226
|
-
}),
|
|
227
|
-
});
|
|
228
|
-
const found = new Map<string, ElementHandle>();
|
|
229
|
-
for (const node of nodes) {
|
|
230
|
-
if (node.sharedId !== undefined && !found.has(node.sharedId)) {
|
|
231
|
-
found.set(node.sharedId, new ElementHandle(node.sharedId, node.handle));
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
return [...found.values()];
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/** Text search done in the page, for browsers without BiDi's innerText locator */
|
|
238
|
-
private async locateByText(
|
|
239
|
-
selector: Extract<Selector, { kind: "text" }>,
|
|
240
|
-
startNodes: ElementHandle[] | undefined,
|
|
241
|
-
max: number | undefined,
|
|
242
|
-
): Promise<ElementHandle[]> {
|
|
243
|
-
const found = await this.call<ElementHandle[]>(TEXT_LOCATE, [
|
|
244
|
-
startNodes ?? [],
|
|
245
|
-
selector.value,
|
|
246
|
-
selector.match,
|
|
247
|
-
selector.ignoreCase,
|
|
248
|
-
]);
|
|
249
|
-
return max === undefined ? found : found.slice(0, max);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/** Calls `fn` in the page; a throwing function becomes a "Failed to locate element" error */
|
|
253
|
-
private async call<T>(fn: string, args: Arg[], sandbox?: string): Promise<T> {
|
|
254
|
-
try {
|
|
255
|
-
return await callFunction<T>(this.biDiConnector, this.contextId, fn, args, {
|
|
256
|
-
awaitPromise: false,
|
|
257
|
-
...(sandbox !== undefined && { sandbox }),
|
|
258
|
-
});
|
|
259
|
-
} catch (err) {
|
|
260
|
-
if (err instanceof ScriptError) {
|
|
261
|
-
throw new Error(
|
|
262
|
-
`Failed to locate element ${this.selector}. Details: ${err.text}`,
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
throw err;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Calls a function on the first matching element: `body` sees it as `el`, followed by `params`.
|
|
271
|
-
* Answers `whenMissing` when nothing matches, or the element disappeared meanwhile.
|
|
272
|
-
*/
|
|
273
|
-
private async callOnElement<T>(
|
|
274
|
-
whenMissing: T,
|
|
275
|
-
body: string,
|
|
276
|
-
params: string[] = [],
|
|
277
|
-
args: Arg[] = [],
|
|
278
|
-
): Promise<T> {
|
|
279
|
-
const el = await this.resolveOne();
|
|
280
|
-
if (!el) return whenMissing;
|
|
281
|
-
try {
|
|
282
|
-
return await this.call<T>(
|
|
283
|
-
`(el${params.map((param) => `, ${param}`).join("")}) => { ${body} }`,
|
|
284
|
-
[el, ...args],
|
|
285
|
-
);
|
|
286
|
-
} catch (err) {
|
|
287
|
-
if (err instanceof BiDiError && err.code === "no such node") return whenMissing;
|
|
288
|
-
throw err;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/** Like `callOnElement`, for operations that cannot go on without the element */
|
|
293
|
-
private async callOnExistingElement<T>(body: string): Promise<T> {
|
|
294
|
-
const missing = Symbol("missing");
|
|
295
|
-
const result = await this.callOnElement<T | typeof missing>(missing, body);
|
|
296
|
-
if (result === missing) {
|
|
297
|
-
throw new Error(`Failed to locate element ${this.selector}. Details: no element matches`);
|
|
298
|
-
}
|
|
299
|
-
return result;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/** Reads the element's actionability state: one search, one probe */
|
|
303
|
-
private async probeState(options: ProbeOptions): Promise<ElementState> {
|
|
304
|
-
const el = await this.resolveOne();
|
|
305
|
-
if (!el) return DETACHED_STATE;
|
|
306
|
-
try {
|
|
307
|
-
return parseElementState(await this.callProbe(el, options));
|
|
308
|
-
} catch (err) {
|
|
309
|
-
// The document changed between the search and the probe
|
|
310
|
-
if (err instanceof BiDiError && err.code === "no such node") return DETACHED_STATE;
|
|
311
|
-
throw err;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
private async callProbe(el: ElementHandle, options: ProbeOptions): Promise<unknown> {
|
|
316
|
-
const args = [el, { ...options }];
|
|
317
|
-
if (!this.helpers) return this.call(PROBE_ELEMENT, args);
|
|
318
|
-
|
|
319
|
-
await this.helpers.ensureInstalled();
|
|
320
|
-
let result = await this.call(CALL_PROBE, args, HELPER_SANDBOX);
|
|
321
|
-
if (result === HELPERS_MISSING) {
|
|
322
|
-
// A document got past the registration (e.g. created while it was being set up): install again once
|
|
323
|
-
await this.helpers.reinstall();
|
|
324
|
-
result = await this.call(CALL_PROBE, args, HELPER_SANDBOX);
|
|
325
|
-
if (result === HELPERS_MISSING) {
|
|
326
|
-
throw new ScriptError("the samurai helpers are missing in the page after reinstalling", "probeElement");
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
return result;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* Probes until every required check passes and returns that state.
|
|
334
|
-
* Throws ActionTimeoutError describing the last probe on timeout.
|
|
335
|
-
*/
|
|
336
|
-
private async waitForActionable(
|
|
337
|
-
action: string,
|
|
338
|
-
checks: readonly CheckName[],
|
|
339
|
-
probe: ProbeOptions,
|
|
340
|
-
options: ActionOptions | undefined,
|
|
341
|
-
): Promise<ElementState> {
|
|
342
|
-
const timeout = await resolveTimeout(options?.timeout);
|
|
343
|
-
// Stability compares two probes; with timeout 0 there is only one
|
|
344
|
-
const required = options?.force
|
|
345
|
-
? ATTACHED_ONLY
|
|
346
|
-
: timeout === 0
|
|
347
|
-
? checks.filter((check) => check !== "stable")
|
|
348
|
-
: checks;
|
|
349
|
-
let lastChecks: CheckResults | undefined;
|
|
350
|
-
try {
|
|
351
|
-
return await waitUntil(
|
|
352
|
-
() => this.probeState(probe),
|
|
353
|
-
(current, previous) => {
|
|
354
|
-
lastChecks = evaluateChecks(required, current, previous);
|
|
355
|
-
return allPass(lastChecks);
|
|
356
|
-
},
|
|
357
|
-
{ timeout },
|
|
358
|
-
);
|
|
359
|
-
} catch (err) {
|
|
360
|
-
if (!(err instanceof WaitTimeoutError)) throw err;
|
|
361
|
-
const last = err.last as ElementState | undefined;
|
|
362
|
-
throw new ActionTimeoutError({
|
|
363
|
-
action,
|
|
364
|
-
selector: this.selector,
|
|
365
|
-
timeout,
|
|
366
|
-
reason: !last ? "unreadable" : last.attached ? "not-actionable" : "not-attached",
|
|
367
|
-
checks: lastChecks,
|
|
368
|
-
coveredBy: lastChecks?.["hit target"] === "fail" ? (last?.hitTarget ?? undefined) : undefined,
|
|
369
|
-
});
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/** Waits until the element is attached, visible, stable, enabled and not covered, then clicks its centre with real pointer input */
|
|
374
|
-
public async click(options?: ActionOptions): Promise<void> {
|
|
375
|
-
const state = await this.waitForActionable(
|
|
376
|
-
"click",
|
|
377
|
-
CLICK_CHECKS,
|
|
378
|
-
{ scroll: true, hitTest: true },
|
|
379
|
-
options,
|
|
380
|
-
);
|
|
381
|
-
// "attached" is always required, so the box is present
|
|
382
|
-
await this.clickRect(state.box!);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
/** Simulate a click event on the center off a page element.
|
|
386
|
-
* This is mostly used internally to focus input elements when using the `fill()` method
|
|
387
|
-
* @example
|
|
388
|
-
* const container = page.locator("div[@id=['clickable-element']");
|
|
389
|
-
* const rect = await container.getBoundingClientRect();
|
|
390
|
-
* await container.clickRect(rect);
|
|
391
|
-
*/
|
|
392
|
-
public async clickRect(rect: Pick<ElementRectangle, "x" | "y" | "width" | "height">) {
|
|
393
|
-
await this.biDiConnector.send("input.performActions", {
|
|
394
|
-
context: this.contextId,
|
|
395
|
-
actions: [
|
|
396
|
-
{
|
|
397
|
-
type: "pointer",
|
|
398
|
-
id: "samurai-pointer",
|
|
399
|
-
actions: [
|
|
400
|
-
{
|
|
401
|
-
type: "pointerMove",
|
|
402
|
-
x: rect.x + rect.width / 2,
|
|
403
|
-
y: rect.y + rect.height / 2,
|
|
404
|
-
},
|
|
405
|
-
{
|
|
406
|
-
type: "pointerDown",
|
|
407
|
-
button: 0,
|
|
408
|
-
},
|
|
409
|
-
{
|
|
410
|
-
type: "pointerUp",
|
|
411
|
-
button: 0,
|
|
412
|
-
},
|
|
413
|
-
],
|
|
414
|
-
},
|
|
415
|
-
],
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* Sets the files of an `<input type=file>` (an empty list clears them); the browser fires `input` and `change`.
|
|
421
|
-
* Paths are resolved against the working directory and must exist on the machine the browser runs on.
|
|
422
|
-
* Waits for the input to be attached and enabled; it need not be visible, file inputs are often hidden behind a label.
|
|
423
|
-
* @example
|
|
424
|
-
* await page.locator("input[@type='file']").setInputFiles("./fixtures/avatar.png");
|
|
425
|
-
* await page.locator("input[@type='file']").setInputFiles([]);
|
|
426
|
-
*/
|
|
427
|
-
public async setInputFiles(files: string | string[], options?: { timeout?: number }): Promise<void> {
|
|
428
|
-
const paths = resolveFiles(files);
|
|
429
|
-
for (const file of paths) await assertIsFile(file, "setInputFiles");
|
|
430
|
-
|
|
431
|
-
await this.waitForActionable(
|
|
432
|
-
"setInputFiles",
|
|
433
|
-
FILE_INPUT_CHECKS,
|
|
434
|
-
{ scroll: false, hitTest: false },
|
|
435
|
-
options?.timeout === undefined ? undefined : { timeout: options.timeout },
|
|
436
|
-
);
|
|
437
|
-
|
|
438
|
-
const missing = () =>
|
|
439
|
-
new Error(`Failed to locate element ${this.selector}. Details: no element matches`);
|
|
440
|
-
const el = await this.resolveOne();
|
|
441
|
-
if (!el) throw missing();
|
|
442
|
-
let info: { tag: string; type: string; multiple: boolean };
|
|
443
|
-
try {
|
|
444
|
-
info = await this.call(FILE_INPUT_INFO, [el]);
|
|
445
|
-
} catch (err) {
|
|
446
|
-
if (err instanceof BiDiError && err.code === "no such node") throw missing();
|
|
447
|
-
throw err;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
if (info.tag !== "input" || info.type !== "file") {
|
|
451
|
-
const found = info.tag === "input" ? `<input type="${info.type || "text"}">` : `<${info.tag}>`;
|
|
452
|
-
throw new Error(`setInputFiles(): ${this.selector} is not an <input type=file> (found ${found})`);
|
|
453
|
-
}
|
|
454
|
-
if (paths.length > 1 && !info.multiple) {
|
|
455
|
-
throw new Error(`setInputFiles(): ${this.selector} does not accept multiple files`);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
await this.biDiConnector.send("input.setFiles", {
|
|
459
|
-
context: this.contextId,
|
|
460
|
-
element: { sharedId: el.sharedId },
|
|
461
|
-
files: paths,
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/**
|
|
466
|
-
* Takes a screenshot of just this element, after scrolling it into view and waiting until it is attached and
|
|
467
|
-
* visible. Returns the image (also written to `options.path`).
|
|
468
|
-
* @example
|
|
469
|
-
* await page.locator("div[@id='chart']").screenshot({ path: "out/chart.png" });
|
|
470
|
-
*/
|
|
471
|
-
public async screenshot(options: ElementScreenshotOptions & { timeout?: number } = {}): Promise<Buffer> {
|
|
472
|
-
const { timeout, ...capture } = options;
|
|
473
|
-
await this.waitForActionable(
|
|
474
|
-
"screenshot",
|
|
475
|
-
SCREENSHOT_CHECKS,
|
|
476
|
-
{ scroll: true, hitTest: false },
|
|
477
|
-
timeout === undefined ? undefined : { timeout },
|
|
478
|
-
);
|
|
479
|
-
const el = await this.resolveOne();
|
|
480
|
-
if (!el) {
|
|
481
|
-
throw new Error(`Failed to locate element ${this.selector}. Details: no element matches`);
|
|
482
|
-
}
|
|
483
|
-
return takeScreenshot(this.biDiConnector, this.contextId, capture, { sharedId: el.sharedId });
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
/** Waits until the element is attached, then focuses it */
|
|
487
|
-
public async focus(options?: ActionOptions): Promise<void> {
|
|
488
|
-
await this.waitForActionable("focus", ATTACHED_ONLY, { scroll: false, hitTest: false }, options);
|
|
489
|
-
await this.callOnExistingElement(`el.focus();`);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
/** Waits until the element reaches `state` (default "visible"); does not scroll */
|
|
493
|
-
public async waitFor({
|
|
494
|
-
state = "visible",
|
|
495
|
-
timeout,
|
|
496
|
-
}: { state?: WaitForState; timeout?: number } = {}): Promise<void> {
|
|
497
|
-
const resolved = await resolveTimeout(timeout);
|
|
498
|
-
const reached: Record<WaitForState, (current: ElementState) => boolean> = {
|
|
499
|
-
attached: (current) => current.attached,
|
|
500
|
-
detached: (current) => !current.attached,
|
|
501
|
-
visible: (current) => current.attached && current.visible,
|
|
502
|
-
hidden: (current) => !current.attached || !current.visible,
|
|
503
|
-
};
|
|
504
|
-
try {
|
|
505
|
-
await waitUntil(() => this.probeState({ scroll: false, hitTest: false }), reached[state], {
|
|
506
|
-
timeout: resolved,
|
|
507
|
-
});
|
|
508
|
-
} catch (err) {
|
|
509
|
-
if (!(err instanceof WaitTimeoutError)) throw err;
|
|
510
|
-
throw new ActionTimeoutError({
|
|
511
|
-
action: "waitFor",
|
|
512
|
-
selector: this.selector,
|
|
513
|
-
timeout: resolved,
|
|
514
|
-
reason: err.last === undefined ? "unreadable" : "wrong-state",
|
|
515
|
-
state,
|
|
516
|
-
});
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
/** Returns the browser's getBoundingClientRect result for the given element */
|
|
521
|
-
public async getBoundingClientRect(): Promise<ElementRectangle> {
|
|
522
|
-
const rect = await this.callOnExistingElement<unknown>(`
|
|
523
|
-
const { x, y, width, height, top, right, bottom, left } = el.getBoundingClientRect();
|
|
524
|
-
return { x, y, width, height, top, right, bottom, left };
|
|
525
|
-
`);
|
|
526
|
-
if (typeof rect !== "object" || rect === null) {
|
|
527
|
-
throw new Error(
|
|
528
|
-
`Expected to receive an object but instead received "${describeType(rect)}"`,
|
|
529
|
-
);
|
|
530
|
-
}
|
|
531
|
-
return rect as ElementRectangle;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
private stringOrNull(value: unknown): string | null {
|
|
535
|
-
if (typeof value === "string") return value;
|
|
536
|
-
if (value === null) return null;
|
|
537
|
-
throw new Error(`Expected a string or null but received "${describeType(value)}"`);
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
/** Whether the element exists, has a non-empty box and is not hidden by CSS */
|
|
541
|
-
public async isVisible(): Promise<boolean> {
|
|
542
|
-
const result = await this.callOnElement<unknown>(
|
|
543
|
-
false,
|
|
544
|
-
`
|
|
545
|
-
const rect = el.getBoundingClientRect();
|
|
546
|
-
const style = getComputedStyle(el);
|
|
547
|
-
return rect.width > 0 && rect.height > 0 &&
|
|
548
|
-
style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0";
|
|
549
|
-
`,
|
|
550
|
-
);
|
|
551
|
-
return result === true;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
/** Trimmed text content, or null when the element is missing */
|
|
555
|
-
public async textContent(): Promise<string | null> {
|
|
556
|
-
return this.stringOrNull(
|
|
557
|
-
await this.callOnElement<unknown>(null, `return el.textContent.trim();`),
|
|
558
|
-
);
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
/** Value of an input/textarea/select, or null when missing */
|
|
562
|
-
public async inputValue(): Promise<string | null> {
|
|
563
|
-
return this.stringOrNull(
|
|
564
|
-
await this.callOnElement<unknown>(null, `return typeof el.value === "string" ? el.value : null;`),
|
|
565
|
-
);
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
/** Attribute value, or null when the element or attribute is missing */
|
|
569
|
-
public async getAttribute(name: string): Promise<string | null> {
|
|
570
|
-
return this.stringOrNull(
|
|
571
|
-
await this.callOnElement<unknown>(null, `return el.getAttribute(name);`, ["name"], [name]),
|
|
572
|
-
);
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
/** Number of elements currently matching */
|
|
576
|
-
public async count(): Promise<number> {
|
|
577
|
-
return (await this.resolve()).length;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
/** Whether the element exists and is not disabled; does not wait */
|
|
581
|
-
public async isEnabled(): Promise<boolean> {
|
|
582
|
-
const state = await this.probeState({ scroll: false, hitTest: false });
|
|
583
|
-
return state.attached && state.enabled;
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
/** Whether the element exists and accepts typing (enabled, not readonly, a text field); does not wait */
|
|
587
|
-
public async isEditable(): Promise<boolean> {
|
|
588
|
-
const state = await this.probeState({ scroll: false, hitTest: false });
|
|
589
|
-
return state.attached && state.editable;
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
/** Wait for certain time (ms). Default: 300ms */
|
|
593
|
-
public async wait(duration: number = 300) {
|
|
594
|
-
await this.biDiConnector.send("input.performActions", {
|
|
595
|
-
context: this.contextId,
|
|
596
|
-
actions: [
|
|
597
|
-
{
|
|
598
|
-
type: "none",
|
|
599
|
-
id: "samurai-sleep",
|
|
600
|
-
actions: [
|
|
601
|
-
{
|
|
602
|
-
type: "pause",
|
|
603
|
-
duration,
|
|
604
|
-
},
|
|
605
|
-
],
|
|
606
|
-
},
|
|
607
|
-
],
|
|
608
|
-
});
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
/** Waits until the field is editable and not covered, then replaces its value by typing `value`; an empty string clears it */
|
|
612
|
-
public async fill(value: string, options?: ActionOptions): Promise<void> {
|
|
613
|
-
const state = await this.waitForActionable(
|
|
614
|
-
"fill",
|
|
615
|
-
FILL_CHECKS,
|
|
616
|
-
{ scroll: true, hitTest: true },
|
|
617
|
-
options,
|
|
618
|
-
);
|
|
619
|
-
|
|
620
|
-
// Typing over the selected existing value replaces it; Backspace clears it for an empty value
|
|
621
|
-
const keys = value === "" ? BACKSPACE : value;
|
|
622
|
-
const actions: Array<KeyDownAction | KeyUpAction> = Array.from(
|
|
623
|
-
keys,
|
|
624
|
-
).flatMap((char) => [
|
|
625
|
-
{
|
|
626
|
-
type: "keyDown",
|
|
627
|
-
value: char,
|
|
628
|
-
},
|
|
629
|
-
{
|
|
630
|
-
type: "keyUp",
|
|
631
|
-
value: char,
|
|
632
|
-
},
|
|
633
|
-
]);
|
|
634
|
-
|
|
635
|
-
await this.clickRect(state.box!);
|
|
636
|
-
|
|
637
|
-
await this.wait();
|
|
638
|
-
|
|
639
|
-
await this.callOnElement(undefined, `if (typeof el.select === "function") el.select();`);
|
|
640
|
-
|
|
641
|
-
await this.biDiConnector.send("input.performActions", {
|
|
642
|
-
context: this.contextId,
|
|
643
|
-
actions: [
|
|
644
|
-
{
|
|
645
|
-
actions,
|
|
646
|
-
type: "key",
|
|
647
|
-
id: "samurai-keyboard",
|
|
648
|
-
},
|
|
649
|
-
],
|
|
650
|
-
});
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
function describeType(value: unknown): string {
|
|
655
|
-
return value === null ? "null" : typeof value;
|
|
656
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/** The browser rejected a selector (bad css or xpath); retrying cannot help */
|
|
2
|
-
export class InvalidSelectorError extends Error {
|
|
3
|
-
public readonly selector: string;
|
|
4
|
-
|
|
5
|
-
constructor(selector: string, reason: string) {
|
|
6
|
-
super(`Invalid selector ${selector}: ${reason}`);
|
|
7
|
-
this.name = "InvalidSelectorError";
|
|
8
|
-
this.selector = selector;
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/** The browser does not implement this kind of locator */
|
|
13
|
-
export class UnsupportedOperationError extends Error {
|
|
14
|
-
constructor(message: string) {
|
|
15
|
-
super(message);
|
|
16
|
-
this.name = "UnsupportedOperationError";
|
|
17
|
-
}
|
|
18
|
-
}
|