@itmetsam/samurai-framework 0.21.2 → 0.21.4

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.
Files changed (98) hide show
  1. package/README.md +1 -1
  2. package/dist/assert/asymmetric.d.ts +29 -0
  3. package/dist/assert/asymmetric.js +166 -0
  4. package/dist/assert/element-probes.d.ts +20 -0
  5. package/dist/assert/element-probes.js +45 -0
  6. package/dist/assert/expect.d.ts +20 -9
  7. package/dist/assert/expect.js +12 -4
  8. package/dist/assert/locator-assertion-base.d.ts +52 -0
  9. package/dist/assert/locator-assertion-base.js +78 -0
  10. package/dist/assert/locator-assertions.d.ts +30 -49
  11. package/dist/assert/locator-assertions.js +44 -79
  12. package/dist/assert/more-locator-assertions.d.ts +30 -0
  13. package/dist/assert/more-locator-assertions.js +86 -0
  14. package/dist/assert/retry-assertions.d.ts +28 -0
  15. package/dist/assert/retry-assertions.js +113 -0
  16. package/dist/assert/text-match.d.ts +4 -2
  17. package/dist/assert/text-match.js +10 -2
  18. package/dist/assert/value-assertions.d.ts +29 -1
  19. package/dist/assert/value-assertions.js +84 -5
  20. package/dist/assert/value-matchers.d.ts +19 -0
  21. package/dist/assert/value-matchers.js +53 -0
  22. package/dist/browser/dialog.js +4 -1
  23. package/dist/browser/init-script.d.ts +11 -0
  24. package/dist/browser/init-script.js +29 -0
  25. package/dist/browser/navigation-error.d.ts +5 -1
  26. package/dist/browser/navigation-error.js +10 -2
  27. package/dist/browser/page-content.d.ts +5 -0
  28. package/dist/browser/page-content.js +43 -0
  29. package/dist/browser/page-evaluate.d.ts +9 -0
  30. package/dist/browser/page-evaluate.js +20 -0
  31. package/dist/browser/page-locators.d.ts +18 -0
  32. package/dist/browser/page-locators.js +19 -0
  33. package/dist/browser/page-navigation.d.ts +18 -0
  34. package/dist/browser/page-navigation.js +52 -2
  35. package/dist/browser/page-permissions.d.ts +6 -0
  36. package/dist/browser/page-permissions.js +15 -0
  37. package/dist/browser/page-settings.d.ts +3 -0
  38. package/dist/browser/page-settings.js +4 -0
  39. package/dist/browser/page.d.ts +76 -13
  40. package/dist/browser/page.js +103 -45
  41. package/dist/config/variables.js +2 -1
  42. package/dist/locator/accessible-name.d.ts +22 -0
  43. package/dist/locator/accessible-name.js +128 -0
  44. package/dist/locator/action-timeout-error.js +5 -1
  45. package/dist/locator/actionability.d.ts +3 -2
  46. package/dist/locator/actionability.js +4 -4
  47. package/dist/locator/attribute-locator.d.ts +13 -0
  48. package/dist/locator/attribute-locator.js +27 -0
  49. package/dist/locator/chain-resolver.d.ts +12 -3
  50. package/dist/locator/chain-resolver.js +85 -15
  51. package/dist/locator/check-actions.d.ts +15 -0
  52. package/dist/locator/check-actions.js +44 -0
  53. package/dist/locator/combine-step.d.ts +5 -0
  54. package/dist/locator/combine-step.js +15 -0
  55. package/dist/locator/control-probe.d.ts +10 -0
  56. package/dist/locator/control-probe.js +50 -0
  57. package/dist/locator/element-access.d.ts +26 -0
  58. package/dist/locator/element-access.js +53 -0
  59. package/dist/locator/element-actions.d.ts +17 -0
  60. package/dist/locator/element-actions.js +51 -0
  61. package/dist/locator/element-reads.d.ts +46 -4
  62. package/dist/locator/element-reads.js +114 -2
  63. package/dist/locator/element-state.d.ts +5 -0
  64. package/dist/locator/element-state.js +2 -0
  65. package/dist/locator/evaluate.d.ts +12 -0
  66. package/dist/locator/evaluate.js +36 -0
  67. package/dist/locator/file-actions.d.ts +2 -2
  68. package/dist/locator/filter-step.d.ts +24 -0
  69. package/dist/locator/filter-step.js +72 -0
  70. package/dist/locator/label-locator.d.ts +1 -1
  71. package/dist/locator/locator.d.ts +118 -30
  72. package/dist/locator/locator.js +218 -151
  73. package/dist/locator/page-scripts.d.ts +2 -0
  74. package/dist/locator/page-scripts.js +6 -0
  75. package/dist/locator/page-search.d.ts +11 -2
  76. package/dist/locator/page-search.js +15 -3
  77. package/dist/locator/page-step.d.ts +16 -0
  78. package/dist/locator/page-step.js +35 -0
  79. package/dist/locator/role-locator.d.ts +65 -0
  80. package/dist/locator/role-locator.js +125 -0
  81. package/dist/locator/role-table.d.ts +18 -0
  82. package/dist/locator/role-table.js +190 -0
  83. package/dist/locator/select-actions.d.ts +13 -0
  84. package/dist/locator/select-actions.js +73 -0
  85. package/dist/locator/selector-string.d.ts +7 -0
  86. package/dist/locator/selector-string.js +27 -0
  87. package/dist/locator/selector.d.ts +47 -15
  88. package/dist/locator/selector.js +83 -12
  89. package/dist/locator/strict-mode.d.ts +6 -0
  90. package/dist/locator/strict-mode.js +6 -0
  91. package/dist/locator/text-locator.d.ts +1 -1
  92. package/dist/network/url-match.js +3 -1
  93. package/dist/script/call-function.d.ts +7 -0
  94. package/dist/script/call-function.js +21 -3
  95. package/dist/types/config.d.ts +7 -0
  96. package/dist/wait/wait-until.d.ts +3 -1
  97. package/dist/wait/wait-until.js +7 -2
  98. package/package.json +1 -1
@@ -1,51 +1,57 @@
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 { takeScreenshot, } from "../browser/screenshot.js";
5
- import { probeState, waitForActionable, waitForState, } from "./actionability.js";
2
+ import { waitForActionable, waitForState, } from "./actionability.js";
3
+ import { setChecked } from "./check-actions.js";
6
4
  import { ChainResolver } from "./chain-resolver.js";
7
- import { dropFiles, setInputFiles, } from "./file-actions.js";
8
- import { describeType, } from "./element-state.js";
9
- import { bindOutput } from "../wait/output-sink.js";
10
- import { cssPropertyName, IS_CHECKED_BODY } from "./element-reads.js";
11
- import { clickRect, DEFAULT_PAUSE_MS, NAMED_KEYS, pause, pressKey, typeText, } from "./input-actions.js";
12
- import { CLICK_CHECKS, FILL_CHECKS, PRESS_CHECKS, SCREENSHOT_CHECKS, ATTACHED_ONLY, } from "./page-scripts.js";
13
- import { cssSelector, describeChain, describeSelector, labelSelector, roleSelector, testIdSelector, textSelector, xpathSelector, } from "./selector.js";
5
+ import { callInPage, noElementError, requireElement, } from "./element-access.js";
6
+ import { clickElement, fillElement, focusElement, pressOnElement, screenshotElement, } from "./element-actions.js";
7
+ import { readAccessibleDescription, readAccessibleName, readAllInnerTexts, readAllTextContents, readAttribute, readBoundingBox, readBoundingClientRect, readClassAttribute, readCssValue, readEnabledState, readInnerHTML, readInnerText, readInputValue, readIsChecked, readIsEditable, readIsEnabled, readIsVisible, readTextContent, } from "./element-reads.js";
8
+ import { evaluateOnAll, evaluateOnElement } from "./evaluate.js";
9
+ import { dropFiles, setInputFiles } from "./file-actions.js";
10
+ import { DEFAULT_PAUSE_MS, pause } from "./input-actions.js";
11
+ import { NOT_STRICT, strictViolation, } from "./strict-mode.js";
12
+ import { attributeSelector, cssSelector, describeChain, describeSelector, labelSelector, nthSelector, roleSelector, testIdSelector, textSelector, } from "./selector.js";
13
+ import { selectOptions } from "./select-actions.js";
14
+ import { filterSelector } from "./filter-step.js";
15
+ import { selectorFromString } from "./selector-string.js";
14
16
  export default class Locator {
15
17
  /**
16
18
  * `selector` is an xpath, or a chain of selector steps that each search inside the previous step's matches.
17
19
  * `helpers` is the page's helper realm. With it the actionability probe is installed once per
18
20
  * document and called by name; without it (a locator built by hand) the probe source is sent on every poll.
19
21
  * `alternatives` are fallback locators tried in order whenever this chain matches nothing (see `withFallbacks`).
22
+ * `strictness` says whether actions and reads throw when the locator matches several elements (see `strict`); default no.
20
23
  */
21
- constructor(selector, biDiConnector, contextId, helpers, alternatives = []) {
24
+ constructor(selector, biDiConnector, contextId, helpers, alternatives = [], strictness = NOT_STRICT) {
22
25
  this.chain =
23
- typeof selector === "string" ? [xpathSelector(selector)] : selector;
26
+ typeof selector === "string" ? [selectorFromString(selector)] : selector;
24
27
  this.biDiConnector = biDiConnector;
25
28
  this.contextId = contextId;
26
29
  this.helpers = helpers;
27
30
  this.alternatives = alternatives;
31
+ this.strictness = strictness;
28
32
  const locator = this;
29
33
  this.actionability = {
30
- resolveOne: () => this.resolveOne(),
31
- call: (fn, args, sandbox) => this.call(fn, args, sandbox),
34
+ resolveOne: (method) => this.resolveOne(method),
35
+ call: (fn, args, sandbox) => this.access.call(fn, args, sandbox),
32
36
  get selector() {
33
37
  return locator.selector;
34
38
  },
35
39
  helpers,
36
40
  };
37
- this.elementContext = {
41
+ this.access = {
38
42
  connector: biDiConnector,
39
43
  contextId,
40
44
  get selector() {
41
45
  return locator.selector;
42
46
  },
43
- call: (fn, args, sandbox) => this.call(fn, args, sandbox),
47
+ resolveOne: (method) => this.resolveOne(method),
48
+ resolveAll: () => this.resolve(),
49
+ call: (fn, args, sandbox) => callInPage(this.access, fn, args, sandbox),
44
50
  waitForActionable: (action, checks, probe, options) => this.waitForActionable(action, checks, probe, options),
45
- requireElement: () => this.requireElement(),
46
- noElementError: () => this.noElementError(),
51
+ requireElement: () => requireElement(this.access),
52
+ noElementError: () => noElementError(this.access.selector),
47
53
  };
48
- this.resolver = new ChainResolver(biDiConnector, contextId, (fn, args, sandbox) => this.call(fn, args, sandbox));
54
+ this.resolver = new ChainResolver(biDiConnector, contextId, (fn, args, sandbox) => this.access.call(fn, args, sandbox));
49
55
  }
50
56
  /**
51
57
  * The description of this locator used in error messages, e.g. `//form >> role=button[name="Save"]`;
@@ -59,7 +65,7 @@ export default class Locator {
59
65
  }
60
66
  /** Steps that keep the fallbacks: each fallback gets the same step, so "inside A or B" stays meaningful */
61
67
  child(step) {
62
- return new Locator([...this.chain, step], this.biDiConnector, this.contextId, this.helpers, this.alternatives.map((alt) => alt.child(step)));
68
+ return new Locator([...this.chain, step], this.biDiConnector, this.contextId, this.helpers, this.alternatives.map((alt) => alt.child(step)), this.strictness);
63
69
  }
64
70
  /**
65
71
  * A locator that tries this one first and, whenever it matches nothing, each of `fallbacks` in order;
@@ -70,7 +76,17 @@ export default class Locator {
70
76
  * page.getByTestId("save").withFallbacks(page.getByRole("button", { name: "Save" }), page.getByText("Save"));
71
77
  */
72
78
  withFallbacks(...fallbacks) {
73
- return new Locator(this.chain, this.biDiConnector, this.contextId, this.helpers, [...this.alternatives, ...fallbacks]);
79
+ return new Locator(this.chain, this.biDiConnector, this.contextId, this.helpers, [...this.alternatives, ...fallbacks], this.strictness);
80
+ }
81
+ /**
82
+ * This locator, strict or not whatever the page and the config say: a strict locator makes actions and reads
83
+ * (`click`, `fill`, `textContent`, ...) throw `name(): strict mode violation, N elements match ...` when it
84
+ * matches several elements, instead of using the first. Steps added to it stay strict. See config `use.strictSelectors`
85
+ * @example
86
+ * await page.locator("li").strict().click(); // throws when there is more than one li
87
+ */
88
+ strict(enabled = true) {
89
+ return new Locator(this.chain, this.biDiConnector, this.contextId, this.helpers, this.alternatives, () => Promise.resolve(enabled));
74
90
  }
75
91
  /**
76
92
  * Which locator found elements the last time this one searched: index 0 is the primary, 1 the first
@@ -79,30 +95,96 @@ export default class Locator {
79
95
  get matchedBy() {
80
96
  return this.matched;
81
97
  }
82
- /** Elements matching the label text (`<label>`, `aria-labelledby`, `aria-label`), inside this locator's matches */
98
+ /** Elements matching the label text (`<label>`, `aria-labelledby`, `aria-label`), inside this locator's matches. See `TextOptions` for `exact` and RegExp */
83
99
  getByLabel(text, options) {
84
100
  return this.child(labelSelector(text, options));
85
101
  }
102
+ /** Elements whose `placeholder` matches, inside this locator's matches. See `TextOptions` for `exact` and RegExp */
103
+ getByPlaceholder(text, options) {
104
+ return this.child(attributeSelector("placeholder", text, options));
105
+ }
106
+ /** Elements whose `alt` text matches (images, areas, image inputs), inside this locator's matches. See `TextOptions` */
107
+ getByAltText(text, options) {
108
+ return this.child(attributeSelector("alt", text, options));
109
+ }
110
+ /** Elements whose `title` attribute matches, inside this locator's matches. See `TextOptions` */
111
+ getByTitle(text, options) {
112
+ return this.child(attributeSelector("title", text, options));
113
+ }
86
114
  /** Elements whose `data-testid` attribute equals `testId`, inside this locator's matches */
87
115
  getByTestId(testId) {
88
116
  return this.child(testIdSelector(testId));
89
117
  }
90
- /** Elements matching `xpath` inside this locator's matches (relative and `//` paths search inside them) */
91
- locator(xpath) {
92
- return this.child(xpathSelector(xpath));
118
+ /**
119
+ * Elements matching `selector` inside this locator's matches. An xpath (`//x`, `./x`, `xpath=…`, or an older
120
+ * relative one such as `a[@id='x']`) searches inside them; anything else is css (`css=` is accepted).
121
+ * `options` filter the matches, as `filter()` does
122
+ */
123
+ locator(selector, options) {
124
+ const found = this.child(selectorFromString(selector));
125
+ return options === undefined ? found : found.filter(options);
93
126
  }
94
127
  /** Elements matching the CSS selector inside this locator's matches */
95
128
  getByCss(css) {
96
129
  return this.child(cssSelector(css));
97
130
  }
98
- /** Elements whose text matches, inside this locator's matches. Exact match unless `match: "partial"` */
131
+ /** Elements whose text matches, inside this locator's matches. Exact match unless `match: "partial"` or `exact: false`; a RegExp is tested as written */
99
132
  getByText(text, options) {
100
133
  return this.child(textSelector(text, options));
101
134
  }
102
- /** Elements with the ARIA role (and accessible name, when given), inside this locator's matches */
135
+ /** Elements with the ARIA role (and accessible name and state, when given), inside this locator's matches. See `RoleOptions` */
103
136
  getByRole(role, options) {
104
137
  return this.child(roleSelector(role, options));
105
138
  }
139
+ /** 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 */
140
+ nth(index) {
141
+ return this.child(nthSelector(index));
142
+ }
143
+ /**
144
+ * The matches that pass the tests, in their order. `hasText` keeps elements whose text contains the string
145
+ * (case-insensitive, whitespace collapsed; this is Playwright's rule, the `getBy*` text matching is unchanged)
146
+ * or matches the RegExp; `has` keeps elements containing a match of the locator, searched inside them.
147
+ * @example
148
+ * await page.locator("li").filter({ hasText: "paid", has: page.getByRole("button") }).first().click();
149
+ */
150
+ filter(options) {
151
+ return this.child(filterSelector(options, (inner, option) => inner.chainFor("filter()", option)));
152
+ }
153
+ /** The elements both this locator and `other` match, in this locator's order */
154
+ and(other) {
155
+ return this.combined("and", other);
156
+ }
157
+ /** The elements either this locator or `other` matches, in document order, each once */
158
+ or(other) {
159
+ return this.combined("or", other);
160
+ }
161
+ combined(kind, other) {
162
+ if (other.contextId !== this.contextId) {
163
+ throw new TypeError(`${kind}(): the locators belong to different pages`);
164
+ }
165
+ return new Locator([
166
+ {
167
+ kind,
168
+ left: this.chainFor(`${kind}()`, "an operand"),
169
+ right: other.chainFor(`${kind}()`, "an operand"),
170
+ },
171
+ ], this.biDiConnector, this.contextId, this.helpers, [], this.strictness);
172
+ }
173
+ /** The chain of a locator used inside another (`has`); a locator with fallbacks cannot be */
174
+ chainFor(method, role) {
175
+ if (this.alternatives.length > 0) {
176
+ throw new TypeError(`${method}: a locator with fallbacks (withFallbacks) cannot be used as ${role}`);
177
+ }
178
+ return this.chain;
179
+ }
180
+ /** The first match, in document order */
181
+ first() {
182
+ return this.nth(0);
183
+ }
184
+ /** The last match, in document order */
185
+ last() {
186
+ return this.nth(-1);
187
+ }
106
188
  /** One locator per element currently matching, in document order */
107
189
  async all() {
108
190
  if (this.alternatives.length > 0) {
@@ -114,7 +196,7 @@ export default class Locator {
114
196
  const only = this.chain.length === 1 ? this.chain[0] : undefined;
115
197
  return Array.from({ length: count }, (_, index) => only?.kind === "xpath"
116
198
  ? // 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)
199
+ new Locator(`(${describeSelector(only, false)})[${index + 1}]`, this.biDiConnector, this.contextId, this.helpers, [], this.strictness)
118
200
  : this.child({ kind: "nth", index }));
119
201
  }
120
202
  /**
@@ -150,73 +232,58 @@ export default class Locator {
150
232
  return undefined;
151
233
  }
152
234
  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
- }
235
+ return new Locator(this.chain, this.biDiConnector, this.contextId, this.helpers, [], this.strictness);
172
236
  }
173
237
  /**
174
- * Calls a function on the first matching element: `body` sees it as `el`, followed by `params`.
175
- * Answers `whenMissing` when nothing matches, or the element disappeared meanwhile.
238
+ * The element the locator matches first. With a `method` name and strict selectors on, several matches
239
+ * throw instead (`method` is the public method that asked, for the message)
176
240
  */
177
- async callOnElement(whenMissing, body, params = [], args = []) {
178
- const el = await this.resolveOne();
179
- if (!el)
180
- return whenMissing;
181
- try {
182
- return await this.call(`(el${params.map((param) => `, ${param}`).join("")}) => { ${body} }`, [el, ...args]);
241
+ async resolveOne(method) {
242
+ if (method === undefined ||
243
+ !(await (this.strictAnswer ??= this.strictness()))) {
244
+ return (await this.resolve(1))[0] ?? null;
183
245
  }
184
- catch (err) {
185
- if (err instanceof BiDiError && err.code === "no such node")
186
- return whenMissing;
187
- throw err;
246
+ const found = await this.resolve();
247
+ if (found.length > 1) {
248
+ throw strictViolation(method, found.length, this.selector);
188
249
  }
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);
250
+ return found[0] ?? null;
211
251
  }
212
252
  waitForActionable(action, checks, probe, options) {
213
253
  return waitForActionable(this.actionability, action, checks, probe, options);
214
254
  }
215
255
  /** Waits until the element is attached, visible, stable, enabled and not covered, then clicks its centre with real pointer input */
216
256
  async click(options) {
217
- const state = await this.waitForActionable("click", CLICK_CHECKS, { scroll: true, hitTest: true }, options);
218
- // "attached" is always required, so the box is present
219
- await clickRect(this.biDiConnector, this.contextId, state.box);
257
+ await clickElement(this.access, options);
258
+ }
259
+ /**
260
+ * Checks a checkbox, radio or `role=checkbox|radio|switch` element; does nothing when it is already checked.
261
+ * Waits like `click`, but a hidden input counts as reachable through its `<label>`, which is what gets clicked.
262
+ * `trial` waits without clicking. Throws when the click did not change the state.
263
+ * @example
264
+ * await page.getByLabel("I accept the terms").check();
265
+ */
266
+ async check(options) {
267
+ await setChecked(this.access, "check", true, options);
268
+ }
269
+ /** Unchecks a checkbox or `role=checkbox|switch` element; does nothing when it is already unchecked. A radio button cannot be unchecked: throws */
270
+ async uncheck(options) {
271
+ await setChecked(this.access, "uncheck", false, options);
272
+ }
273
+ /** `check()` or `uncheck()`, whichever brings the element to `checked` */
274
+ async setChecked(checked, options) {
275
+ await setChecked(this.access, "setChecked", checked, options);
276
+ }
277
+ /**
278
+ * Selects options of a `<select>` or `<select multiple>` (which replaces its selection) and fires `input` and
279
+ * `change`. A string matches an option's value or its label; `{ value, label, index }` must match every field
280
+ * given. Waits until the select is visible and enabled and every option exists. Returns the values now selected.
281
+ * @example
282
+ * await page.getByLabel("Country").selectOption("Netherlands");
283
+ * await page.locator("select").selectOption([{ index: 0 }, { value: "b" }]);
284
+ */
285
+ async selectOption(values, options) {
286
+ return selectOptions(this.access, values, options);
220
287
  }
221
288
  /**
222
289
  * Sets the files of an `<input type=file>`; `[]` clears it. Paths are relative to the project folder (absolute
@@ -227,7 +294,7 @@ export default class Locator {
227
294
  * await page.getByLabel("Upload").setInputFiles(generatedFile({ name: "huge.pdf", size: "60 MB" }));
228
295
  */
229
296
  async setInputFiles(files, options) {
230
- await setInputFiles(this.elementContext, files, options);
297
+ await setInputFiles(this.access, files, options);
231
298
  }
232
299
  /**
233
300
  * Drops files on the element, as dragging them from the desktop does: `dragenter`, `dragover` and `drop`
@@ -238,7 +305,7 @@ export default class Locator {
238
305
  * await page.getByTestId("dropzone").dropFiles(["files/a.png", "files/b.png"]);
239
306
  */
240
307
  async dropFiles(files, options) {
241
- await dropFiles(this.elementContext, files, options);
308
+ await dropFiles(this.access, files, options);
242
309
  }
243
310
  /**
244
311
  * Takes a screenshot of just this element, after scrolling it into view and waiting until it is attached and
@@ -247,17 +314,11 @@ export default class Locator {
247
314
  * await page.locator("div[@id='chart']").screenshot({ path: "out/chart.png" });
248
315
  */
249
316
  async screenshot(options = {}) {
250
- const { timeout, ...capture } = options;
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
- });
317
+ return screenshotElement(this.access, options);
256
318
  }
257
319
  /** Waits until the element is attached, then focuses it */
258
320
  async focus(options) {
259
- await this.waitForActionable("focus", ATTACHED_ONLY, { scroll: false, hitTest: false }, options);
260
- await this.callOnExistingElement(`el.focus();`);
321
+ await focusElement(this.access, options);
261
322
  }
262
323
  /** Waits until the element reaches `state` (default "visible"); does not scroll */
263
324
  async waitFor(options = {}) {
@@ -265,31 +326,60 @@ export default class Locator {
265
326
  }
266
327
  /** Returns the browser's getBoundingClientRect result for the given element */
267
328
  async getBoundingClientRect() {
268
- const rect = await this.callOnExistingElement(`
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;
329
+ return readBoundingClientRect(this.access);
276
330
  }
277
331
  /** Whether the element exists, has a non-empty box and is not hidden by CSS */
278
332
  async isVisible() {
279
- const state = await this.probeState({ scroll: false, hitTest: false });
280
- return state.attached && state.visible;
333
+ return readIsVisible(this.actionability);
334
+ }
335
+ /** The opposite of `isVisible`: true for a hidden element and for one that is not there */
336
+ async isHidden() {
337
+ return !(await this.isVisible());
281
338
  }
282
339
  /** Trimmed text content, or null when the element is missing */
283
340
  async textContent() {
284
- return stringOrNull(await this.callOnElement(null, `return el.textContent.trim();`));
341
+ return readTextContent(this.access);
342
+ }
343
+ /** Rendered text of the element (as a person would copy it, hidden text left out), untrimmed, or null when the element is missing */
344
+ async innerText() {
345
+ return readInnerText(this.access);
346
+ }
347
+ /** The markup inside the element, or null when the element is missing */
348
+ async innerHTML() {
349
+ return readInnerHTML(this.access);
350
+ }
351
+ /** `textContent` of every match, as the page has it (untrimmed, unlike `textContent()`); `[]` when none match. Does not wait */
352
+ async allTextContents() {
353
+ return readAllTextContents(this.access);
354
+ }
355
+ /** `innerText` of every match; `[]` when none match. Does not wait */
356
+ async allInnerTexts() {
357
+ return readAllInnerTexts(this.access);
358
+ }
359
+ /** The element's box in CSS pixels from the viewport's top left, or null when it is missing or not rendered (`display: none`). Does not wait or scroll */
360
+ async boundingBox() {
361
+ return readBoundingBox(this.access);
362
+ }
363
+ /**
364
+ * Waits until the element is attached (hidden is fine), then calls `fn(element, arg)` in the page and returns the result
365
+ * (see `page.evaluate` for what comes back). `fn` is a function or the source of one; a strict locator throws when several match.
366
+ * @example
367
+ * const id = await page.getByTestId("row").evaluate((el) => el.dataset.id);
368
+ */
369
+ async evaluate(fn, arg, options) {
370
+ return evaluateOnElement(this.access, fn, arg, options);
371
+ }
372
+ /** Calls `fn(elements, arg)` in the page with an array of every match (empty when none; does not wait) and returns the result */
373
+ async evaluateAll(fn, arg) {
374
+ return evaluateOnAll(this.access, fn, arg);
285
375
  }
286
376
  /** Value of an input/textarea/select, or null when missing */
287
377
  async inputValue() {
288
- return stringOrNull(await this.callOnElement(null, `return typeof el.value === "string" ? el.value : null;`));
378
+ return readInputValue(this.access);
289
379
  }
290
380
  /** Attribute value, or null when the element or attribute is missing */
291
381
  async getAttribute(name) {
292
- return stringOrNull(await this.callOnElement(null, `return el.getAttribute(name);`, ["name"], [name]));
382
+ return readAttribute(this.access, name);
293
383
  }
294
384
  /** References to the elements currently matching, in document order. Internal: for tooling that identifies elements */
295
385
  async elements() {
@@ -301,30 +391,39 @@ export default class Locator {
301
391
  }
302
392
  /** Whether the element exists and is not disabled; does not wait */
303
393
  async isEnabled() {
304
- const state = await this.probeState({ scroll: false, hitTest: false });
305
- return state.attached && state.enabled;
394
+ return readIsEnabled(this.actionability);
395
+ }
396
+ /** The opposite of `isEnabled`: true for a disabled element and for one that is not there; does not wait */
397
+ async isDisabled() {
398
+ return !(await this.isEnabled());
306
399
  }
307
400
  /** Like `isEnabled`, but null when the element is missing, so callers can tell "disabled" from "not there"; does not wait */
308
401
  async enabledState() {
309
- const state = await this.probeState({ scroll: false, hitTest: false });
310
- return state.attached ? state.enabled : null;
402
+ return readEnabledState(this.actionability);
311
403
  }
312
404
  /** Whether a checkbox or radio input is checked or the element has `aria-checked="true"`; null when the element is missing */
313
405
  async isChecked() {
314
- return booleanOrNull(await this.callOnElement(null, IS_CHECKED_BODY));
406
+ return readIsChecked(this.access);
407
+ }
408
+ /** The accessible name (what `getByRole`'s `name` is matched against), "" when there is none, or null when the element is missing */
409
+ async accessibleName() {
410
+ return readAccessibleName(this.access);
411
+ }
412
+ /** The accessible description (`aria-describedby`, `aria-description`, else `title` when it is not the name), or null when the element is missing */
413
+ async accessibleDescription() {
414
+ return readAccessibleDescription(this.access);
315
415
  }
316
416
  /** The class attribute as written ("" when the element has none), or null when the element is missing */
317
417
  async classAttribute() {
318
- return stringOrNull(await this.callOnElement(null, `return el.getAttribute("class") ?? "";`));
418
+ return readClassAttribute(this.access);
319
419
  }
320
420
  /** Computed value of a CSS property (camelCase names become kebab-case, `--custom` is kept), or null when the element is missing */
321
421
  async cssValue(name) {
322
- return stringOrNull(await this.callOnElement(null, `return getComputedStyle(el).getPropertyValue(name);`, ["name"], [cssPropertyName(name)]));
422
+ return readCssValue(this.access, name);
323
423
  }
324
424
  /** Whether the element exists and accepts typing (enabled, not readonly, a text field); does not wait */
325
425
  async isEditable() {
326
- const state = await this.probeState({ scroll: false, hitTest: false });
327
- return state.attached && state.editable;
426
+ return readIsEditable(this.actionability);
328
427
  }
329
428
  /** Wait for certain time (ms). Default: 300ms */
330
429
  async wait(duration = DEFAULT_PAUSE_MS) {
@@ -338,42 +437,10 @@ export default class Locator {
338
437
  * await page.getByLabel("New todo").press("Enter");
339
438
  */
340
439
  async press(key, options) {
341
- const code = NAMED_KEYS[key] ?? (Array.from(key).length === 1 ? key : "");
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);
440
+ await pressOnElement(this.access, key, options);
348
441
  }
349
442
  /** Waits until the field is editable and not covered, then replaces its value by typing `value`; an empty string clears it */
350
443
  async fill(value, options) {
351
- // Taken when the call starts, so a fill that outlives its test saves into that test's closed sink
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);
444
+ await fillElement(this.access, value, options);
364
445
  }
365
446
  }
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
- /** `normalise` collapses whitespace and trims (lower-casing with `ignoreCase`); `textMatches` compares a text with `value` by `match` */
6
- export declare const MATCHING_JS = "\n const normalise = (text) => {\n const collapsed = String(text).replace(/\\s+/g, \" \").trim();\n return ignoreCase ? collapsed.toLowerCase() : collapsed;\n };\n const wanted = normalise(value);\n const textMatches = (text) => {\n const normal = normalise(text);\n return match === \"full\" ? normal === wanted : normal.includes(wanted);\n };";
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
- /** `normalise` collapses whitespace and trims (lower-casing with `ignoreCase`); `textMatches` compares a text with `value` by `match` */
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 = String(text).replace(/\\s+/g, " ").trim();
13
+ const collapsed = collapse(text);
9
14
  return ignoreCase ? collapsed.toLowerCase() : collapsed;
10
15
  };
11
- const wanted = normalise(value);
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;