@itmetsam/samurai-framework 0.21.1 → 0.21.3
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 +8 -3
- package/dist/assert/locator-assertions.d.ts +4 -0
- package/dist/assert/locator-assertions.js +10 -2
- package/dist/assert/text-match.d.ts +2 -2
- package/dist/assert/text-match.js +2 -2
- package/dist/browser/dialog.js +4 -1
- package/dist/browser/init-script.d.ts +11 -0
- package/dist/browser/init-script.js +29 -0
- package/dist/browser/page-locators.d.ts +18 -0
- package/dist/browser/page-locators.js +19 -0
- package/dist/browser/page-permissions.d.ts +6 -0
- package/dist/browser/page-permissions.js +15 -0
- package/dist/browser/page-settings.d.ts +3 -0
- package/dist/browser/page-settings.js +4 -0
- package/dist/browser/page.d.ts +29 -13
- package/dist/browser/page.js +46 -45
- package/dist/config/variables.js +2 -1
- package/dist/locator/accessible-name.d.ts +22 -0
- package/dist/locator/accessible-name.js +128 -0
- package/dist/locator/action-timeout-error.js +5 -1
- package/dist/locator/actionability.d.ts +3 -2
- package/dist/locator/actionability.js +4 -4
- package/dist/locator/attribute-locator.d.ts +13 -0
- package/dist/locator/attribute-locator.js +27 -0
- package/dist/locator/chain-resolver.d.ts +12 -3
- package/dist/locator/chain-resolver.js +85 -15
- package/dist/locator/check-actions.d.ts +15 -0
- package/dist/locator/check-actions.js +44 -0
- package/dist/locator/combine-step.d.ts +5 -0
- package/dist/locator/combine-step.js +15 -0
- package/dist/locator/control-probe.d.ts +10 -0
- package/dist/locator/control-probe.js +50 -0
- package/dist/locator/element-access.d.ts +24 -0
- package/dist/locator/element-access.js +53 -0
- package/dist/locator/element-actions.d.ts +17 -0
- package/dist/locator/element-actions.js +51 -0
- package/dist/locator/element-reads.d.ts +29 -4
- package/dist/locator/element-reads.js +83 -2
- package/dist/locator/element-state.d.ts +5 -0
- package/dist/locator/element-state.js +2 -0
- package/dist/locator/file-actions.d.ts +2 -2
- package/dist/locator/filter-step.d.ts +24 -0
- package/dist/locator/filter-step.js +72 -0
- package/dist/locator/label-locator.d.ts +1 -1
- package/dist/locator/locator.d.ts +94 -29
- package/dist/locator/locator.js +183 -151
- package/dist/locator/page-scripts.d.ts +2 -0
- package/dist/locator/page-scripts.js +6 -0
- package/dist/locator/page-search.d.ts +11 -2
- package/dist/locator/page-search.js +15 -3
- package/dist/locator/page-step.d.ts +16 -0
- package/dist/locator/page-step.js +35 -0
- package/dist/locator/role-locator.d.ts +65 -0
- package/dist/locator/role-locator.js +125 -0
- package/dist/locator/role-table.d.ts +18 -0
- package/dist/locator/role-table.js +190 -0
- package/dist/locator/select-actions.d.ts +13 -0
- package/dist/locator/select-actions.js +73 -0
- package/dist/locator/selector-string.d.ts +7 -0
- package/dist/locator/selector-string.js +27 -0
- package/dist/locator/selector.d.ts +47 -15
- package/dist/locator/selector.js +83 -12
- package/dist/locator/strict-mode.d.ts +6 -0
- package/dist/locator/strict-mode.js +6 -0
- package/dist/locator/text-locator.d.ts +1 -1
- package/dist/network/network-tracker.d.ts +17 -1
- package/dist/network/network-tracker.js +63 -18
- package/dist/network/url-match.js +3 -1
- package/dist/types/config.d.ts +7 -0
- package/package.json +1 -1
package/dist/locator/locator.js
CHANGED
|
@@ -1,51 +1,55 @@
|
|
|
1
|
-
import { BiDiError } from "../transport/bidi-error.js";
|
|
2
|
-
import { callFunction, ScriptError } from "../script/call-function.js";
|
|
3
1
|
import { ElementHandle } from "../script/element-handle.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { waitForActionable, waitForState, } from "./actionability.js";
|
|
3
|
+
import { setChecked } from "./check-actions.js";
|
|
6
4
|
import { ChainResolver } from "./chain-resolver.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { cssSelector, describeChain, describeSelector, labelSelector, roleSelector, testIdSelector, textSelector,
|
|
5
|
+
import { callInPage, noElementError, requireElement, } from "./element-access.js";
|
|
6
|
+
import { clickElement, fillElement, focusElement, pressOnElement, screenshotElement, } from "./element-actions.js";
|
|
7
|
+
import { readAccessibleDescription, readAccessibleName, readAttribute, readBoundingClientRect, readClassAttribute, readCssValue, readEnabledState, readInputValue, readIsChecked, readIsEditable, readIsEnabled, readIsVisible, readTextContent, } from "./element-reads.js";
|
|
8
|
+
import { dropFiles, setInputFiles } from "./file-actions.js";
|
|
9
|
+
import { DEFAULT_PAUSE_MS, pause } from "./input-actions.js";
|
|
10
|
+
import { NOT_STRICT, strictViolation, } from "./strict-mode.js";
|
|
11
|
+
import { attributeSelector, cssSelector, describeChain, describeSelector, labelSelector, nthSelector, roleSelector, testIdSelector, textSelector, } from "./selector.js";
|
|
12
|
+
import { selectOptions } from "./select-actions.js";
|
|
13
|
+
import { filterSelector } from "./filter-step.js";
|
|
14
|
+
import { selectorFromString } from "./selector-string.js";
|
|
14
15
|
export default class Locator {
|
|
15
16
|
/**
|
|
16
17
|
* `selector` is an xpath, or a chain of selector steps that each search inside the previous step's matches.
|
|
17
18
|
* `helpers` is the page's helper realm. With it the actionability probe is installed once per
|
|
18
19
|
* document and called by name; without it (a locator built by hand) the probe source is sent on every poll.
|
|
19
20
|
* `alternatives` are fallback locators tried in order whenever this chain matches nothing (see `withFallbacks`).
|
|
21
|
+
* `strictness` says whether actions and reads throw when the locator matches several elements (see `strict`); default no.
|
|
20
22
|
*/
|
|
21
|
-
constructor(selector, biDiConnector, contextId, helpers, alternatives = []) {
|
|
23
|
+
constructor(selector, biDiConnector, contextId, helpers, alternatives = [], strictness = NOT_STRICT) {
|
|
22
24
|
this.chain =
|
|
23
|
-
typeof selector === "string" ? [
|
|
25
|
+
typeof selector === "string" ? [selectorFromString(selector)] : selector;
|
|
24
26
|
this.biDiConnector = biDiConnector;
|
|
25
27
|
this.contextId = contextId;
|
|
26
28
|
this.helpers = helpers;
|
|
27
29
|
this.alternatives = alternatives;
|
|
30
|
+
this.strictness = strictness;
|
|
28
31
|
const locator = this;
|
|
29
32
|
this.actionability = {
|
|
30
|
-
resolveOne: () => this.resolveOne(),
|
|
31
|
-
call: (fn, args, sandbox) => this.call(fn, args, sandbox),
|
|
33
|
+
resolveOne: (method) => this.resolveOne(method),
|
|
34
|
+
call: (fn, args, sandbox) => this.access.call(fn, args, sandbox),
|
|
32
35
|
get selector() {
|
|
33
36
|
return locator.selector;
|
|
34
37
|
},
|
|
35
38
|
helpers,
|
|
36
39
|
};
|
|
37
|
-
this.
|
|
40
|
+
this.access = {
|
|
38
41
|
connector: biDiConnector,
|
|
39
42
|
contextId,
|
|
40
43
|
get selector() {
|
|
41
44
|
return locator.selector;
|
|
42
45
|
},
|
|
43
|
-
|
|
46
|
+
resolveOne: (method) => this.resolveOne(method),
|
|
47
|
+
call: (fn, args, sandbox) => callInPage(this.access, fn, args, sandbox),
|
|
44
48
|
waitForActionable: (action, checks, probe, options) => this.waitForActionable(action, checks, probe, options),
|
|
45
|
-
requireElement: () => this.
|
|
46
|
-
noElementError: () => this.
|
|
49
|
+
requireElement: () => requireElement(this.access),
|
|
50
|
+
noElementError: () => noElementError(this.access.selector),
|
|
47
51
|
};
|
|
48
|
-
this.resolver = new ChainResolver(biDiConnector, contextId, (fn, args, sandbox) => this.call(fn, args, sandbox));
|
|
52
|
+
this.resolver = new ChainResolver(biDiConnector, contextId, (fn, args, sandbox) => this.access.call(fn, args, sandbox));
|
|
49
53
|
}
|
|
50
54
|
/**
|
|
51
55
|
* The description of this locator used in error messages, e.g. `//form >> role=button[name="Save"]`;
|
|
@@ -59,7 +63,7 @@ export default class Locator {
|
|
|
59
63
|
}
|
|
60
64
|
/** Steps that keep the fallbacks: each fallback gets the same step, so "inside A or B" stays meaningful */
|
|
61
65
|
child(step) {
|
|
62
|
-
return new Locator([...this.chain, step], this.biDiConnector, this.contextId, this.helpers, this.alternatives.map((alt) => alt.child(step)));
|
|
66
|
+
return new Locator([...this.chain, step], this.biDiConnector, this.contextId, this.helpers, this.alternatives.map((alt) => alt.child(step)), this.strictness);
|
|
63
67
|
}
|
|
64
68
|
/**
|
|
65
69
|
* A locator that tries this one first and, whenever it matches nothing, each of `fallbacks` in order;
|
|
@@ -70,7 +74,17 @@ export default class Locator {
|
|
|
70
74
|
* page.getByTestId("save").withFallbacks(page.getByRole("button", { name: "Save" }), page.getByText("Save"));
|
|
71
75
|
*/
|
|
72
76
|
withFallbacks(...fallbacks) {
|
|
73
|
-
return new Locator(this.chain, this.biDiConnector, this.contextId, this.helpers, [...this.alternatives, ...fallbacks]);
|
|
77
|
+
return new Locator(this.chain, this.biDiConnector, this.contextId, this.helpers, [...this.alternatives, ...fallbacks], this.strictness);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* This locator, strict or not whatever the page and the config say: a strict locator makes actions and reads
|
|
81
|
+
* (`click`, `fill`, `textContent`, ...) throw `name(): strict mode violation, N elements match ...` when it
|
|
82
|
+
* matches several elements, instead of using the first. Steps added to it stay strict. See config `use.strictSelectors`
|
|
83
|
+
* @example
|
|
84
|
+
* await page.locator("li").strict().click(); // throws when there is more than one li
|
|
85
|
+
*/
|
|
86
|
+
strict(enabled = true) {
|
|
87
|
+
return new Locator(this.chain, this.biDiConnector, this.contextId, this.helpers, this.alternatives, () => Promise.resolve(enabled));
|
|
74
88
|
}
|
|
75
89
|
/**
|
|
76
90
|
* Which locator found elements the last time this one searched: index 0 is the primary, 1 the first
|
|
@@ -79,30 +93,96 @@ export default class Locator {
|
|
|
79
93
|
get matchedBy() {
|
|
80
94
|
return this.matched;
|
|
81
95
|
}
|
|
82
|
-
/** Elements matching the label text (`<label>`, `aria-labelledby`, `aria-label`), inside this locator's matches */
|
|
96
|
+
/** Elements matching the label text (`<label>`, `aria-labelledby`, `aria-label`), inside this locator's matches. See `TextOptions` for `exact` and RegExp */
|
|
83
97
|
getByLabel(text, options) {
|
|
84
98
|
return this.child(labelSelector(text, options));
|
|
85
99
|
}
|
|
100
|
+
/** Elements whose `placeholder` matches, inside this locator's matches. See `TextOptions` for `exact` and RegExp */
|
|
101
|
+
getByPlaceholder(text, options) {
|
|
102
|
+
return this.child(attributeSelector("placeholder", text, options));
|
|
103
|
+
}
|
|
104
|
+
/** Elements whose `alt` text matches (images, areas, image inputs), inside this locator's matches. See `TextOptions` */
|
|
105
|
+
getByAltText(text, options) {
|
|
106
|
+
return this.child(attributeSelector("alt", text, options));
|
|
107
|
+
}
|
|
108
|
+
/** Elements whose `title` attribute matches, inside this locator's matches. See `TextOptions` */
|
|
109
|
+
getByTitle(text, options) {
|
|
110
|
+
return this.child(attributeSelector("title", text, options));
|
|
111
|
+
}
|
|
86
112
|
/** Elements whose `data-testid` attribute equals `testId`, inside this locator's matches */
|
|
87
113
|
getByTestId(testId) {
|
|
88
114
|
return this.child(testIdSelector(testId));
|
|
89
115
|
}
|
|
90
|
-
/**
|
|
91
|
-
|
|
92
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Elements matching `selector` inside this locator's matches. An xpath (`//x`, `./x`, `xpath=…`, or an older
|
|
118
|
+
* relative one such as `a[@id='x']`) searches inside them; anything else is css (`css=` is accepted).
|
|
119
|
+
* `options` filter the matches, as `filter()` does
|
|
120
|
+
*/
|
|
121
|
+
locator(selector, options) {
|
|
122
|
+
const found = this.child(selectorFromString(selector));
|
|
123
|
+
return options === undefined ? found : found.filter(options);
|
|
93
124
|
}
|
|
94
125
|
/** Elements matching the CSS selector inside this locator's matches */
|
|
95
126
|
getByCss(css) {
|
|
96
127
|
return this.child(cssSelector(css));
|
|
97
128
|
}
|
|
98
|
-
/** Elements whose text matches, inside this locator's matches. Exact match unless `match: "partial"` */
|
|
129
|
+
/** Elements whose text matches, inside this locator's matches. Exact match unless `match: "partial"` or `exact: false`; a RegExp is tested as written */
|
|
99
130
|
getByText(text, options) {
|
|
100
131
|
return this.child(textSelector(text, options));
|
|
101
132
|
}
|
|
102
|
-
/** Elements with the ARIA role (and accessible name, when given), inside this locator's matches */
|
|
133
|
+
/** Elements with the ARIA role (and accessible name and state, when given), inside this locator's matches. See `RoleOptions` */
|
|
103
134
|
getByRole(role, options) {
|
|
104
135
|
return this.child(roleSelector(role, options));
|
|
105
136
|
}
|
|
137
|
+
/** The n-th (0-based) match, in document order; a negative index counts from the end (`-1` is the last). Matches nothing when out of range */
|
|
138
|
+
nth(index) {
|
|
139
|
+
return this.child(nthSelector(index));
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The matches that pass the tests, in their order. `hasText` keeps elements whose text contains the string
|
|
143
|
+
* (case-insensitive, whitespace collapsed; this is Playwright's rule, the `getBy*` text matching is unchanged)
|
|
144
|
+
* or matches the RegExp; `has` keeps elements containing a match of the locator, searched inside them.
|
|
145
|
+
* @example
|
|
146
|
+
* await page.locator("li").filter({ hasText: "paid", has: page.getByRole("button") }).first().click();
|
|
147
|
+
*/
|
|
148
|
+
filter(options) {
|
|
149
|
+
return this.child(filterSelector(options, (inner, option) => inner.chainFor("filter()", option)));
|
|
150
|
+
}
|
|
151
|
+
/** The elements both this locator and `other` match, in this locator's order */
|
|
152
|
+
and(other) {
|
|
153
|
+
return this.combined("and", other);
|
|
154
|
+
}
|
|
155
|
+
/** The elements either this locator or `other` matches, in document order, each once */
|
|
156
|
+
or(other) {
|
|
157
|
+
return this.combined("or", other);
|
|
158
|
+
}
|
|
159
|
+
combined(kind, other) {
|
|
160
|
+
if (other.contextId !== this.contextId) {
|
|
161
|
+
throw new TypeError(`${kind}(): the locators belong to different pages`);
|
|
162
|
+
}
|
|
163
|
+
return new Locator([
|
|
164
|
+
{
|
|
165
|
+
kind,
|
|
166
|
+
left: this.chainFor(`${kind}()`, "an operand"),
|
|
167
|
+
right: other.chainFor(`${kind}()`, "an operand"),
|
|
168
|
+
},
|
|
169
|
+
], this.biDiConnector, this.contextId, this.helpers, [], this.strictness);
|
|
170
|
+
}
|
|
171
|
+
/** The chain of a locator used inside another (`has`); a locator with fallbacks cannot be */
|
|
172
|
+
chainFor(method, role) {
|
|
173
|
+
if (this.alternatives.length > 0) {
|
|
174
|
+
throw new TypeError(`${method}: a locator with fallbacks (withFallbacks) cannot be used as ${role}`);
|
|
175
|
+
}
|
|
176
|
+
return this.chain;
|
|
177
|
+
}
|
|
178
|
+
/** The first match, in document order */
|
|
179
|
+
first() {
|
|
180
|
+
return this.nth(0);
|
|
181
|
+
}
|
|
182
|
+
/** The last match, in document order */
|
|
183
|
+
last() {
|
|
184
|
+
return this.nth(-1);
|
|
185
|
+
}
|
|
106
186
|
/** One locator per element currently matching, in document order */
|
|
107
187
|
async all() {
|
|
108
188
|
if (this.alternatives.length > 0) {
|
|
@@ -114,7 +194,7 @@ export default class Locator {
|
|
|
114
194
|
const only = this.chain.length === 1 ? this.chain[0] : undefined;
|
|
115
195
|
return Array.from({ length: count }, (_, index) => only?.kind === "xpath"
|
|
116
196
|
? // A positional xpath keeps working without a second search for the whole chain
|
|
117
|
-
new Locator(`(${describeSelector(only, false)})[${index + 1}]`, this.biDiConnector, this.contextId, this.helpers)
|
|
197
|
+
new Locator(`(${describeSelector(only, false)})[${index + 1}]`, this.biDiConnector, this.contextId, this.helpers, [], this.strictness)
|
|
118
198
|
: this.child({ kind: "nth", index }));
|
|
119
199
|
}
|
|
120
200
|
/**
|
|
@@ -150,73 +230,58 @@ export default class Locator {
|
|
|
150
230
|
return undefined;
|
|
151
231
|
}
|
|
152
232
|
withoutFallbacks() {
|
|
153
|
-
return new Locator(this.chain, this.biDiConnector, this.contextId, this.helpers);
|
|
154
|
-
}
|
|
155
|
-
async resolveOne() {
|
|
156
|
-
return (await this.resolve(1))[0] ?? null;
|
|
157
|
-
}
|
|
158
|
-
/** Calls `fn` in the page; a throwing function becomes a "Failed to locate element" error */
|
|
159
|
-
async call(fn, args, sandbox) {
|
|
160
|
-
try {
|
|
161
|
-
return await callFunction(this.biDiConnector, this.contextId, fn, args, {
|
|
162
|
-
awaitPromise: false,
|
|
163
|
-
...(sandbox !== undefined && { sandbox }),
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
catch (err) {
|
|
167
|
-
if (err instanceof ScriptError) {
|
|
168
|
-
throw new Error(`Failed to locate element ${this.selector}. Details: ${err.text}`);
|
|
169
|
-
}
|
|
170
|
-
throw err;
|
|
171
|
-
}
|
|
233
|
+
return new Locator(this.chain, this.biDiConnector, this.contextId, this.helpers, [], this.strictness);
|
|
172
234
|
}
|
|
173
235
|
/**
|
|
174
|
-
*
|
|
175
|
-
*
|
|
236
|
+
* The element the locator matches first. With a `method` name and strict selectors on, several matches
|
|
237
|
+
* throw instead (`method` is the public method that asked, for the message)
|
|
176
238
|
*/
|
|
177
|
-
async
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
return
|
|
181
|
-
try {
|
|
182
|
-
return await this.call(`(el${params.map((param) => `, ${param}`).join("")}) => { ${body} }`, [el, ...args]);
|
|
239
|
+
async resolveOne(method) {
|
|
240
|
+
if (method === undefined ||
|
|
241
|
+
!(await (this.strictAnswer ??= this.strictness()))) {
|
|
242
|
+
return (await this.resolve(1))[0] ?? null;
|
|
183
243
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
throw err;
|
|
244
|
+
const found = await this.resolve();
|
|
245
|
+
if (found.length > 1) {
|
|
246
|
+
throw strictViolation(method, found.length, this.selector);
|
|
188
247
|
}
|
|
189
|
-
|
|
190
|
-
/** Like `callOnElement`, for operations that cannot go on without the element */
|
|
191
|
-
async callOnExistingElement(body) {
|
|
192
|
-
const missing = Symbol("missing");
|
|
193
|
-
const result = await this.callOnElement(missing, body);
|
|
194
|
-
if (result === missing)
|
|
195
|
-
throw this.noElementError();
|
|
196
|
-
return result;
|
|
197
|
-
}
|
|
198
|
-
/** The error for an operation that needs an element when none matches */
|
|
199
|
-
noElementError() {
|
|
200
|
-
return new Error(`Failed to locate element ${this.selector}. Details: no element matches`);
|
|
201
|
-
}
|
|
202
|
-
/** The element the locator matches now; throws when none does */
|
|
203
|
-
async requireElement() {
|
|
204
|
-
const el = await this.resolveOne();
|
|
205
|
-
if (!el)
|
|
206
|
-
throw this.noElementError();
|
|
207
|
-
return el;
|
|
208
|
-
}
|
|
209
|
-
probeState(options) {
|
|
210
|
-
return probeState(this.actionability, options);
|
|
248
|
+
return found[0] ?? null;
|
|
211
249
|
}
|
|
212
250
|
waitForActionable(action, checks, probe, options) {
|
|
213
251
|
return waitForActionable(this.actionability, action, checks, probe, options);
|
|
214
252
|
}
|
|
215
253
|
/** Waits until the element is attached, visible, stable, enabled and not covered, then clicks its centre with real pointer input */
|
|
216
254
|
async click(options) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
255
|
+
await clickElement(this.access, options);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Checks a checkbox, radio or `role=checkbox|radio|switch` element; does nothing when it is already checked.
|
|
259
|
+
* Waits like `click`, but a hidden input counts as reachable through its `<label>`, which is what gets clicked.
|
|
260
|
+
* `trial` waits without clicking. Throws when the click did not change the state.
|
|
261
|
+
* @example
|
|
262
|
+
* await page.getByLabel("I accept the terms").check();
|
|
263
|
+
*/
|
|
264
|
+
async check(options) {
|
|
265
|
+
await setChecked(this.access, "check", true, options);
|
|
266
|
+
}
|
|
267
|
+
/** Unchecks a checkbox or `role=checkbox|switch` element; does nothing when it is already unchecked. A radio button cannot be unchecked: throws */
|
|
268
|
+
async uncheck(options) {
|
|
269
|
+
await setChecked(this.access, "uncheck", false, options);
|
|
270
|
+
}
|
|
271
|
+
/** `check()` or `uncheck()`, whichever brings the element to `checked` */
|
|
272
|
+
async setChecked(checked, options) {
|
|
273
|
+
await setChecked(this.access, "setChecked", checked, options);
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Selects options of a `<select>` or `<select multiple>` (which replaces its selection) and fires `input` and
|
|
277
|
+
* `change`. A string matches an option's value or its label; `{ value, label, index }` must match every field
|
|
278
|
+
* given. Waits until the select is visible and enabled and every option exists. Returns the values now selected.
|
|
279
|
+
* @example
|
|
280
|
+
* await page.getByLabel("Country").selectOption("Netherlands");
|
|
281
|
+
* await page.locator("select").selectOption([{ index: 0 }, { value: "b" }]);
|
|
282
|
+
*/
|
|
283
|
+
async selectOption(values, options) {
|
|
284
|
+
return selectOptions(this.access, values, options);
|
|
220
285
|
}
|
|
221
286
|
/**
|
|
222
287
|
* Sets the files of an `<input type=file>`; `[]` clears it. Paths are relative to the project folder (absolute
|
|
@@ -227,7 +292,7 @@ export default class Locator {
|
|
|
227
292
|
* await page.getByLabel("Upload").setInputFiles(generatedFile({ name: "huge.pdf", size: "60 MB" }));
|
|
228
293
|
*/
|
|
229
294
|
async setInputFiles(files, options) {
|
|
230
|
-
await setInputFiles(this.
|
|
295
|
+
await setInputFiles(this.access, files, options);
|
|
231
296
|
}
|
|
232
297
|
/**
|
|
233
298
|
* Drops files on the element, as dragging them from the desktop does: `dragenter`, `dragover` and `drop`
|
|
@@ -238,7 +303,7 @@ export default class Locator {
|
|
|
238
303
|
* await page.getByTestId("dropzone").dropFiles(["files/a.png", "files/b.png"]);
|
|
239
304
|
*/
|
|
240
305
|
async dropFiles(files, options) {
|
|
241
|
-
await dropFiles(this.
|
|
306
|
+
await dropFiles(this.access, files, options);
|
|
242
307
|
}
|
|
243
308
|
/**
|
|
244
309
|
* Takes a screenshot of just this element, after scrolling it into view and waiting until it is attached and
|
|
@@ -247,17 +312,11 @@ export default class Locator {
|
|
|
247
312
|
* await page.locator("div[@id='chart']").screenshot({ path: "out/chart.png" });
|
|
248
313
|
*/
|
|
249
314
|
async screenshot(options = {}) {
|
|
250
|
-
|
|
251
|
-
await this.waitForActionable("screenshot", SCREENSHOT_CHECKS, { scroll: true, hitTest: false }, timeout === undefined ? undefined : { timeout });
|
|
252
|
-
const el = await this.requireElement();
|
|
253
|
-
return takeScreenshot(this.biDiConnector, this.contextId, capture, {
|
|
254
|
-
sharedId: el.sharedId,
|
|
255
|
-
});
|
|
315
|
+
return screenshotElement(this.access, options);
|
|
256
316
|
}
|
|
257
317
|
/** Waits until the element is attached, then focuses it */
|
|
258
318
|
async focus(options) {
|
|
259
|
-
await this.
|
|
260
|
-
await this.callOnExistingElement(`el.focus();`);
|
|
319
|
+
await focusElement(this.access, options);
|
|
261
320
|
}
|
|
262
321
|
/** Waits until the element reaches `state` (default "visible"); does not scroll */
|
|
263
322
|
async waitFor(options = {}) {
|
|
@@ -265,31 +324,27 @@ export default class Locator {
|
|
|
265
324
|
}
|
|
266
325
|
/** Returns the browser's getBoundingClientRect result for the given element */
|
|
267
326
|
async getBoundingClientRect() {
|
|
268
|
-
|
|
269
|
-
const { x, y, width, height, top, right, bottom, left } = el.getBoundingClientRect();
|
|
270
|
-
return { x, y, width, height, top, right, bottom, left };
|
|
271
|
-
`);
|
|
272
|
-
if (typeof rect !== "object" || rect === null) {
|
|
273
|
-
throw new Error(`Expected to receive an object but instead received "${describeType(rect)}"`);
|
|
274
|
-
}
|
|
275
|
-
return rect;
|
|
327
|
+
return readBoundingClientRect(this.access);
|
|
276
328
|
}
|
|
277
329
|
/** Whether the element exists, has a non-empty box and is not hidden by CSS */
|
|
278
330
|
async isVisible() {
|
|
279
|
-
|
|
280
|
-
|
|
331
|
+
return readIsVisible(this.actionability);
|
|
332
|
+
}
|
|
333
|
+
/** The opposite of `isVisible`: true for a hidden element and for one that is not there */
|
|
334
|
+
async isHidden() {
|
|
335
|
+
return !(await this.isVisible());
|
|
281
336
|
}
|
|
282
337
|
/** Trimmed text content, or null when the element is missing */
|
|
283
338
|
async textContent() {
|
|
284
|
-
return
|
|
339
|
+
return readTextContent(this.access);
|
|
285
340
|
}
|
|
286
341
|
/** Value of an input/textarea/select, or null when missing */
|
|
287
342
|
async inputValue() {
|
|
288
|
-
return
|
|
343
|
+
return readInputValue(this.access);
|
|
289
344
|
}
|
|
290
345
|
/** Attribute value, or null when the element or attribute is missing */
|
|
291
346
|
async getAttribute(name) {
|
|
292
|
-
return
|
|
347
|
+
return readAttribute(this.access, name);
|
|
293
348
|
}
|
|
294
349
|
/** References to the elements currently matching, in document order. Internal: for tooling that identifies elements */
|
|
295
350
|
async elements() {
|
|
@@ -301,30 +356,39 @@ export default class Locator {
|
|
|
301
356
|
}
|
|
302
357
|
/** Whether the element exists and is not disabled; does not wait */
|
|
303
358
|
async isEnabled() {
|
|
304
|
-
|
|
305
|
-
|
|
359
|
+
return readIsEnabled(this.actionability);
|
|
360
|
+
}
|
|
361
|
+
/** The opposite of `isEnabled`: true for a disabled element and for one that is not there; does not wait */
|
|
362
|
+
async isDisabled() {
|
|
363
|
+
return !(await this.isEnabled());
|
|
306
364
|
}
|
|
307
365
|
/** Like `isEnabled`, but null when the element is missing, so callers can tell "disabled" from "not there"; does not wait */
|
|
308
366
|
async enabledState() {
|
|
309
|
-
|
|
310
|
-
return state.attached ? state.enabled : null;
|
|
367
|
+
return readEnabledState(this.actionability);
|
|
311
368
|
}
|
|
312
369
|
/** Whether a checkbox or radio input is checked or the element has `aria-checked="true"`; null when the element is missing */
|
|
313
370
|
async isChecked() {
|
|
314
|
-
return
|
|
371
|
+
return readIsChecked(this.access);
|
|
372
|
+
}
|
|
373
|
+
/** The accessible name (what `getByRole`'s `name` is matched against), "" when there is none, or null when the element is missing */
|
|
374
|
+
async accessibleName() {
|
|
375
|
+
return readAccessibleName(this.access);
|
|
376
|
+
}
|
|
377
|
+
/** The accessible description (`aria-describedby`, `aria-description`, else `title` when it is not the name), or null when the element is missing */
|
|
378
|
+
async accessibleDescription() {
|
|
379
|
+
return readAccessibleDescription(this.access);
|
|
315
380
|
}
|
|
316
381
|
/** The class attribute as written ("" when the element has none), or null when the element is missing */
|
|
317
382
|
async classAttribute() {
|
|
318
|
-
return
|
|
383
|
+
return readClassAttribute(this.access);
|
|
319
384
|
}
|
|
320
385
|
/** Computed value of a CSS property (camelCase names become kebab-case, `--custom` is kept), or null when the element is missing */
|
|
321
386
|
async cssValue(name) {
|
|
322
|
-
return
|
|
387
|
+
return readCssValue(this.access, name);
|
|
323
388
|
}
|
|
324
389
|
/** Whether the element exists and accepts typing (enabled, not readonly, a text field); does not wait */
|
|
325
390
|
async isEditable() {
|
|
326
|
-
|
|
327
|
-
return state.attached && state.editable;
|
|
391
|
+
return readIsEditable(this.actionability);
|
|
328
392
|
}
|
|
329
393
|
/** Wait for certain time (ms). Default: 300ms */
|
|
330
394
|
async wait(duration = DEFAULT_PAUSE_MS) {
|
|
@@ -338,42 +402,10 @@ export default class Locator {
|
|
|
338
402
|
* await page.getByLabel("New todo").press("Enter");
|
|
339
403
|
*/
|
|
340
404
|
async press(key, options) {
|
|
341
|
-
|
|
342
|
-
if (code === "") {
|
|
343
|
-
throw new Error(`press(): unknown key "${key}". Use one of ${Object.keys(NAMED_KEYS).join(", ")} or a single character`);
|
|
344
|
-
}
|
|
345
|
-
await this.waitForActionable("press", PRESS_CHECKS, { scroll: true, hitTest: false }, options);
|
|
346
|
-
await this.callOnElement(undefined, `el.focus();`);
|
|
347
|
-
await pressKey(this.biDiConnector, this.contextId, code);
|
|
405
|
+
await pressOnElement(this.access, key, options);
|
|
348
406
|
}
|
|
349
407
|
/** Waits until the field is editable and not covered, then replaces its value by typing `value`; an empty string clears it */
|
|
350
408
|
async fill(value, options) {
|
|
351
|
-
|
|
352
|
-
const save = options?.output === undefined
|
|
353
|
-
? undefined
|
|
354
|
-
: bindOutput("fill()", options.output);
|
|
355
|
-
const state = await this.waitForActionable("fill", FILL_CHECKS, { scroll: true, hitTest: true }, options);
|
|
356
|
-
// Typing over the selected existing value replaces it; Backspace clears it for an empty value
|
|
357
|
-
const keys = value === "" ? NAMED_KEYS.Backspace : value;
|
|
358
|
-
await clickRect(this.biDiConnector, this.contextId, state.box);
|
|
359
|
-
// Lets the focus the click caused (and the page's focus handlers) settle before the text is selected and typed over
|
|
360
|
-
await this.wait();
|
|
361
|
-
await this.callOnElement(undefined, `if (typeof el.select === "function") el.select();`);
|
|
362
|
-
await typeText(this.biDiConnector, this.contextId, keys);
|
|
363
|
-
save?.(value);
|
|
409
|
+
await fillElement(this.access, value, options);
|
|
364
410
|
}
|
|
365
411
|
}
|
|
366
|
-
function booleanOrNull(value) {
|
|
367
|
-
if (typeof value === "boolean")
|
|
368
|
-
return value;
|
|
369
|
-
if (value === null)
|
|
370
|
-
return null;
|
|
371
|
-
throw new Error(`Expected a boolean or null but received "${describeType(value)}"`);
|
|
372
|
-
}
|
|
373
|
-
function stringOrNull(value) {
|
|
374
|
-
if (typeof value === "string")
|
|
375
|
-
return value;
|
|
376
|
-
if (value === null)
|
|
377
|
-
return null;
|
|
378
|
-
throw new Error(`Expected a string or null but received "${describeType(value)}"`);
|
|
379
|
-
}
|
|
@@ -11,6 +11,8 @@ export declare const DROP_INPUT = "() => {\n const input = document.createEleme
|
|
|
11
11
|
export declare const DROP_EVENTS = "(el, types) => {\n const input = globalThis[Symbol.for(\"samurai.drop\")];\n delete globalThis[Symbol.for(\"samurai.drop\")];\n const data = new DataTransfer();\n Array.from(input.files).forEach((file, i) => {\n data.items.add(types[i] ? new File([file], file.name, { type: types[i], lastModified: file.lastModified }) : file);\n });\n const box = el.getBoundingClientRect();\n const at = { clientX: box.left + box.width / 2, clientY: box.top + box.height / 2 };\n for (const type of [\"dragenter\", \"dragover\", \"drop\"]) {\n el.dispatchEvent(new DragEvent(type, Object.assign({ bubbles: true, cancelable: true, composed: true, dataTransfer: data }, at)));\n }\n}";
|
|
12
12
|
export declare const SCREENSHOT_CHECKS: readonly CheckName[];
|
|
13
13
|
export declare const CLICK_CHECKS: readonly CheckName[];
|
|
14
|
+
/** `selectOption`: a select is not clicked, so it need not be stable or uncovered */
|
|
15
|
+
export declare const SELECT_CHECKS: readonly CheckName[];
|
|
14
16
|
export declare const FILL_CHECKS: readonly CheckName[];
|
|
15
17
|
export declare const PRESS_CHECKS: readonly CheckName[];
|
|
16
18
|
export declare const ATTACHED_ONLY: readonly CheckName[];
|
|
@@ -39,6 +39,12 @@ export const CLICK_CHECKS = [
|
|
|
39
39
|
"enabled",
|
|
40
40
|
"hit target",
|
|
41
41
|
];
|
|
42
|
+
/** `selectOption`: a select is not clicked, so it need not be stable or uncovered */
|
|
43
|
+
export const SELECT_CHECKS = [
|
|
44
|
+
"attached",
|
|
45
|
+
"visible",
|
|
46
|
+
"enabled",
|
|
47
|
+
];
|
|
42
48
|
export const FILL_CHECKS = [
|
|
43
49
|
"attached",
|
|
44
50
|
"visible",
|
|
@@ -2,9 +2,18 @@
|
|
|
2
2
|
* Page-side pieces the text and label searches share. They are strings, glued into the function declarations
|
|
3
3
|
* of `TEXT_LOCATE` and `LABEL_LOCATE`, which run in the page with `(starts, value, match, ignoreCase)` in scope.
|
|
4
4
|
*/
|
|
5
|
-
/**
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* `normalise` collapses whitespace and trims (lower-casing with `ignoreCase`); `textMatches` compares a text with `value` by
|
|
7
|
+
* `match`. A RegExp `value` is tested on the collapsed text as written (flags kept, `g` and `y` dropped); `match` and
|
|
8
|
+
* `ignoreCase` then do not apply.
|
|
9
|
+
*/
|
|
10
|
+
export declare const MATCHING_JS = "\n const collapse = (text) => String(text).replace(/\\s+/g, \" \").trim();\n const normalise = (text) => {\n const collapsed = collapse(text);\n return ignoreCase ? collapsed.toLowerCase() : collapsed;\n };\n const pattern = typeof value === \"string\" ? null : new RegExp(value.source, value.flags.replace(/[gy]/g, \"\"));\n const wanted = pattern ? \"\" : normalise(value);\n const textMatches = (text) => {\n if (pattern) return pattern.test(collapse(text));\n const normal = normalise(text);\n return match === \"full\" ? normal === wanted : normal.includes(wanted);\n };";
|
|
7
11
|
/** The nodes to search in: the start nodes, else the whole document */
|
|
8
12
|
export declare const ROOTS_JS = "const roots = starts.length > 0 ? starts : [document];";
|
|
9
13
|
/** A sort callback putting elements in document order */
|
|
10
14
|
export declare const DOCUMENT_ORDER_JS = "(a, b) => (a === b ? 0 : a.compareDocumentPosition(b) & 4 ? -1 : 1)";
|
|
15
|
+
/**
|
|
16
|
+
* Function declaration run in the page as `(elements)`: the same elements in document order. A search with
|
|
17
|
+
* several start nodes answers per start node, so its results are not in document order.
|
|
18
|
+
*/
|
|
19
|
+
export declare const SORT_IN_DOCUMENT_ORDER = "(elements) => elements.slice().sort((a, b) => (a === b ? 0 : a.compareDocumentPosition(b) & 4 ? -1 : 1))";
|
|
@@ -2,14 +2,21 @@
|
|
|
2
2
|
* Page-side pieces the text and label searches share. They are strings, glued into the function declarations
|
|
3
3
|
* of `TEXT_LOCATE` and `LABEL_LOCATE`, which run in the page with `(starts, value, match, ignoreCase)` in scope.
|
|
4
4
|
*/
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* `normalise` collapses whitespace and trims (lower-casing with `ignoreCase`); `textMatches` compares a text with `value` by
|
|
7
|
+
* `match`. A RegExp `value` is tested on the collapsed text as written (flags kept, `g` and `y` dropped); `match` and
|
|
8
|
+
* `ignoreCase` then do not apply.
|
|
9
|
+
*/
|
|
6
10
|
export const MATCHING_JS = `
|
|
11
|
+
const collapse = (text) => String(text).replace(/\\s+/g, " ").trim();
|
|
7
12
|
const normalise = (text) => {
|
|
8
|
-
const collapsed =
|
|
13
|
+
const collapsed = collapse(text);
|
|
9
14
|
return ignoreCase ? collapsed.toLowerCase() : collapsed;
|
|
10
15
|
};
|
|
11
|
-
const
|
|
16
|
+
const pattern = typeof value === "string" ? null : new RegExp(value.source, value.flags.replace(/[gy]/g, ""));
|
|
17
|
+
const wanted = pattern ? "" : normalise(value);
|
|
12
18
|
const textMatches = (text) => {
|
|
19
|
+
if (pattern) return pattern.test(collapse(text));
|
|
13
20
|
const normal = normalise(text);
|
|
14
21
|
return match === "full" ? normal === wanted : normal.includes(wanted);
|
|
15
22
|
};`;
|
|
@@ -17,3 +24,8 @@ export const MATCHING_JS = `
|
|
|
17
24
|
export const ROOTS_JS = `const roots = starts.length > 0 ? starts : [document];`;
|
|
18
25
|
/** A sort callback putting elements in document order */
|
|
19
26
|
export const DOCUMENT_ORDER_JS = `(a, b) => (a === b ? 0 : a.compareDocumentPosition(b) & 4 ? -1 : 1)`;
|
|
27
|
+
/**
|
|
28
|
+
* Function declaration run in the page as `(elements)`: the same elements in document order. A search with
|
|
29
|
+
* several start nodes answers per start node, so its results are not in document order.
|
|
30
|
+
*/
|
|
31
|
+
export const SORT_IN_DOCUMENT_ORDER = `(elements) => elements.slice().sort(${DOCUMENT_ORDER_JS})`;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Arg } from "../script/serialize.js";
|
|
2
|
+
import type { Selector } from "./selector.js";
|
|
3
|
+
/** A step the framework searches in the page: the function (run as `(starts, ...args)`) and its arguments after the start nodes */
|
|
4
|
+
export interface PageSearch {
|
|
5
|
+
fn: string;
|
|
6
|
+
args: Arg[];
|
|
7
|
+
}
|
|
8
|
+
/** The text search of a `text` step, which the browser's own `innerText` locator cannot do (or does not exist) */
|
|
9
|
+
export declare function textSearch(selector: Extract<Selector, {
|
|
10
|
+
kind: "text";
|
|
11
|
+
}>): PageSearch;
|
|
12
|
+
/**
|
|
13
|
+
* The page-side search for a step the browser cannot locate by itself: labels, attributes, RegExp texts and roles with a RegExp name or options.
|
|
14
|
+
* `undefined` when the browser's locator does the job
|
|
15
|
+
*/
|
|
16
|
+
export declare function pageSearch(selector: Selector): PageSearch | undefined;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ATTRIBUTE_LOCATE } from "./attribute-locator.js";
|
|
2
|
+
import { LABEL_LOCATE } from "./label-locator.js";
|
|
3
|
+
import { isNativeRole, ROLE_LOCATE, roleSearchArgs } from "./role-locator.js";
|
|
4
|
+
import { TEXT_LOCATE } from "./text-locator.js";
|
|
5
|
+
/** The arguments of the text-matching searches: `(value, match, ignoreCase)`, and the attribute for `attribute` steps */
|
|
6
|
+
function textArgs(selector) {
|
|
7
|
+
const args = [selector.value, selector.match, selector.ignoreCase];
|
|
8
|
+
return selector.kind === "attribute" ? [...args, selector.attribute] : args;
|
|
9
|
+
}
|
|
10
|
+
/** The text search of a `text` step, which the browser's own `innerText` locator cannot do (or does not exist) */
|
|
11
|
+
export function textSearch(selector) {
|
|
12
|
+
return { fn: TEXT_LOCATE, args: textArgs(selector) };
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The page-side search for a step the browser cannot locate by itself: labels, attributes, RegExp texts and roles with a RegExp name or options.
|
|
16
|
+
* `undefined` when the browser's locator does the job
|
|
17
|
+
*/
|
|
18
|
+
export function pageSearch(selector) {
|
|
19
|
+
switch (selector.kind) {
|
|
20
|
+
case "label":
|
|
21
|
+
return { fn: LABEL_LOCATE, args: textArgs(selector) };
|
|
22
|
+
case "attribute":
|
|
23
|
+
return { fn: ATTRIBUTE_LOCATE, args: textArgs(selector) };
|
|
24
|
+
case "text":
|
|
25
|
+
return typeof selector.value === "string"
|
|
26
|
+
? undefined
|
|
27
|
+
: textSearch(selector);
|
|
28
|
+
case "role":
|
|
29
|
+
return isNativeRole(selector)
|
|
30
|
+
? undefined
|
|
31
|
+
: { fn: ROLE_LOCATE, args: [...roleSearchArgs(selector)] };
|
|
32
|
+
default:
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|