@itmetsam/samurai-framework 0.22.9 → 0.22.11

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.
@@ -1,22 +1,15 @@
1
1
  import { ElementHandle } from "../script/element-handle.js";
2
- import { waitForActionable, waitForState, } from "./actionability.js";
3
- import { setChecked } from "./check-actions.js";
2
+ import { waitForActionable, } from "./actionability.js";
4
3
  import { ChainResolver } from "./chain-resolver.js";
5
4
  import { callInPage, noElementError, requireElement, } from "./element-access.js";
6
- import { clearElement, fillElement, focusElement, pressOnElement, pressSequentially, screenshotElement, } from "./element-actions.js";
7
5
  import { dragElement } from "./drag-actions.js";
8
- import { blurElement, dispatchEventOn } from "./event-actions.js";
9
- import { clickElement, hoverElement, tapElement, } from "./pointer-element-actions.js";
10
- import { readAccessibleDescription, readAccessibleName, readAllInnerTexts, readAllTextContents, readAttribute, readBoundingBox, readBoundingClientRect, readClassAttribute, readCssValue, readEnabledState, readInnerHTML, readInnerText, readInputValue, readIsChecked, readIsEditable, readIsEnabled, readIsVisible, readTextContent, } from "./element-reads.js";
11
- import { evaluateOnAll, evaluateOnElement } from "./evaluate.js";
12
- import { dropFiles, setInputFiles } from "./file-actions.js";
13
6
  import { DEFAULT_PAUSE_MS, pause } from "./input-actions.js";
14
7
  import { NOT_STRICT, strictViolation, } from "./strict-mode.js";
15
8
  import { attributeSelector, cssSelector, describeChain, describeSelector, labelSelector, nthSelector, roleSelector, testIdSelector, textSelector, } from "./selector.js";
16
- import { selectOptions } from "./select-actions.js";
17
9
  import { filterSelector } from "./filter-step.js";
18
10
  import { selectorFromString } from "./selector-string.js";
19
- export default class Locator {
11
+ import { LocatorElementMethods } from "./locator-element-methods.js";
12
+ export default class Locator extends LocatorElementMethods {
20
13
  /**
21
14
  * `selector` is an xpath, or a chain of selector steps that each search inside the previous step's matches.
22
15
  * `helpers` is the page's helper realm. With it the actionability probe is installed once per
@@ -25,6 +18,7 @@ export default class Locator {
25
18
  * `strictness` says whether actions and reads throw when the locator matches several elements (see `strict`); default no.
26
19
  */
27
20
  constructor(selector, biDiConnector, contextId, helpers, alternatives = [], strictness = NOT_STRICT) {
21
+ super();
28
22
  this.chain =
29
23
  typeof selector === "string" ? [selectorFromString(selector)] : selector;
30
24
  this.biDiConnector = biDiConnector;
@@ -268,171 +262,10 @@ export default class Locator {
268
262
  waitForActionable(action, checks, probe, options) {
269
263
  return waitForActionable(this.actionability, action, checks, probe, options);
270
264
  }
271
- /**
272
- * Waits until the element is attached, visible, stable, enabled and not covered, then clicks it with real pointer
273
- * input: its centre, or `position` (pixels from its top left corner). `button`, `clickCount`, `delay` (ms between press
274
- * and release) and `modifiers` (keys held down) are as in Playwright
275
- */
276
- async click(options) {
277
- await clickElement(this.access, options);
278
- }
279
- /** `click` with `clickCount: 2` */
280
- async dblclick(options) {
281
- await clickElement(this.access, { ...options, clickCount: 2 }, "dblclick");
282
- }
283
- /** Waits until the element is attached, visible, stable and not covered, then moves the mouse onto it (`:hover`, `mouseover`, tooltips) */
284
- async hover(options) {
285
- await hoverElement(this.access, options);
286
- }
287
- /** Waits like `click`, then touches the element with a touch pointer: `touchstart`, `touchend`, then the mouse events a tap ends in */
288
- async tap(options) {
289
- await tapElement(this.access, options);
290
- }
291
265
  /** Drags this element onto `target` with real pointer input; `sourcePosition`, `targetPosition` and `steps` shape the gesture. Firefox needs synthetic drop events, see the wiki */
292
266
  async dragTo(target, options) {
293
267
  await dragElement(this.access, target.access, options);
294
268
  }
295
- /** Waits until the element is attached, then fires a synthetic event (`click`, `keydown`, ...; the class follows the type) at it, bubbling and composed */
296
- async dispatchEvent(type, eventInit, options) {
297
- await dispatchEventOn(this.access, type, eventInit, options);
298
- }
299
- /** Waits until the element is attached, then removes focus from it */
300
- async blur(options) {
301
- await blurElement(this.access, options);
302
- }
303
- /** Empties an input, textarea or editable element, as `fill("")` does */
304
- async clear(options) {
305
- await clearElement(this.access, options);
306
- }
307
- /** Focuses the element and types `text` into it one key press at a time, `delay` ms apart */
308
- async pressSequentially(text, options) {
309
- await pressSequentially(this.access, text, options);
310
- }
311
- /**
312
- * Checks a checkbox, radio or `role=checkbox|radio|switch` element; does nothing when it is already checked.
313
- * Waits like `click`, but a hidden input counts as reachable through its `<label>`, which is what gets clicked.
314
- * `trial` waits without clicking. Throws when the click did not change the state.
315
- * @example
316
- * await page.getByLabel("I accept the terms").check();
317
- */
318
- async check(options) {
319
- await setChecked(this.access, "check", true, options);
320
- }
321
- /** Unchecks a checkbox or `role=checkbox|switch` element; does nothing when it is already unchecked. A radio button cannot be unchecked: throws */
322
- async uncheck(options) {
323
- await setChecked(this.access, "uncheck", false, options);
324
- }
325
- /** `check()` or `uncheck()`, whichever brings the element to `checked` */
326
- async setChecked(checked, options) {
327
- await setChecked(this.access, "setChecked", checked, options);
328
- }
329
- /**
330
- * Selects options of a `<select>` or `<select multiple>` (which replaces its selection) and fires `input` and
331
- * `change`. A string matches an option's value or its label; `{ value, label, index }` must match every field
332
- * given. Waits until the select is visible and enabled and every option exists. Returns the values now selected.
333
- * @example
334
- * await page.getByLabel("Country").selectOption("Netherlands");
335
- * await page.locator("select").selectOption([{ index: 0 }, { value: "b" }]);
336
- */
337
- async selectOption(values, options) {
338
- return selectOptions(this.access, values, options);
339
- }
340
- /**
341
- * Sets the files of an `<input type=file>`; `[]` clears it. Paths are relative to the project folder (absolute
342
- * ones are used as they are); `generatedFile(…)` writes a file of a given size when the step runs.
343
- * Waits for the input to be attached and enabled; it need not be visible, file inputs are often hidden behind a label.
344
- * @example
345
- * await page.getByLabel("Avatar").setInputFiles("files/avatar.png");
346
- * await page.getByLabel("Upload").setInputFiles(generatedFile({ name: "huge.pdf", size: "60 MB" }));
347
- */
348
- async setInputFiles(files, options) {
349
- await setInputFiles(this.access, files, options);
350
- }
351
- /**
352
- * Drops files on the element, as dragging them from the desktop does: `dragenter`, `dragover` and `drop`
353
- * with a `DataTransfer` holding the files. Takes what `setInputFiles` takes. The events are synthetic
354
- * (`isTrusted` is false); a page that only accepts trusted drops ignores them.
355
- * Waits for the element to be attached and visible.
356
- * @example
357
- * await page.getByTestId("dropzone").dropFiles(["files/a.png", "files/b.png"]);
358
- */
359
- async dropFiles(files, options) {
360
- await dropFiles(this.access, files, options);
361
- }
362
- /**
363
- * Takes a screenshot of just this element, after scrolling it into view and waiting until it is attached and
364
- * visible. Returns the image (also written to `options.path`).
365
- * @example
366
- * await page.locator("div[@id='chart']").screenshot({ path: "out/chart.png" });
367
- */
368
- async screenshot(options = {}) {
369
- return screenshotElement(this.access, options);
370
- }
371
- /** Waits until the element is attached, then focuses it */
372
- async focus(options) {
373
- await focusElement(this.access, options);
374
- }
375
- /** Waits until the element reaches `state` (default "visible"); does not scroll */
376
- async waitFor(options = {}) {
377
- await waitForState(this.actionability, options);
378
- }
379
- /** Returns the browser's getBoundingClientRect result for the given element */
380
- async getBoundingClientRect() {
381
- return readBoundingClientRect(this.access);
382
- }
383
- /** Whether the element exists, has a non-empty box and is not hidden by CSS */
384
- async isVisible() {
385
- return readIsVisible(this.actionability);
386
- }
387
- /** The opposite of `isVisible`: true for a hidden element and for one that is not there */
388
- async isHidden() {
389
- return !(await this.isVisible());
390
- }
391
- /** Trimmed text content, or null when the element is missing */
392
- async textContent() {
393
- return readTextContent(this.access);
394
- }
395
- /** Rendered text of the element (as a person would copy it, hidden text left out), untrimmed, or null when the element is missing */
396
- async innerText() {
397
- return readInnerText(this.access);
398
- }
399
- /** The markup inside the element, or null when the element is missing */
400
- async innerHTML() {
401
- return readInnerHTML(this.access);
402
- }
403
- /** `textContent` of every match, as the page has it (untrimmed, unlike `textContent()`); `[]` when none match. Does not wait */
404
- async allTextContents() {
405
- return readAllTextContents(this.access);
406
- }
407
- /** `innerText` of every match; `[]` when none match. Does not wait */
408
- async allInnerTexts() {
409
- return readAllInnerTexts(this.access);
410
- }
411
- /** 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 */
412
- async boundingBox() {
413
- return readBoundingBox(this.access);
414
- }
415
- /**
416
- * Waits until the element is attached (hidden is fine), then calls `fn(element, arg)` in the page and returns the result
417
- * (see `page.evaluate` for what comes back). `fn` is a function or the source of one; a strict locator throws when several match.
418
- * @example
419
- * const id = await page.getByTestId("row").evaluate((el) => el.dataset.id);
420
- */
421
- async evaluate(fn, arg, options) {
422
- return evaluateOnElement(this.access, fn, arg, options);
423
- }
424
- /** Calls `fn(elements, arg)` in the page with an array of every match (empty when none; does not wait) and returns the result */
425
- async evaluateAll(fn, arg) {
426
- return evaluateOnAll(this.access, fn, arg);
427
- }
428
- /** Value of an input/textarea/select, or null when missing */
429
- async inputValue() {
430
- return readInputValue(this.access);
431
- }
432
- /** Attribute value, or null when the element or attribute is missing */
433
- async getAttribute(name) {
434
- return readAttribute(this.access, name);
435
- }
436
269
  /** References to the elements currently matching, in document order. Internal: for tooling that identifies elements */
437
270
  async elements() {
438
271
  return this.resolve();
@@ -441,60 +274,8 @@ export default class Locator {
441
274
  async count() {
442
275
  return (await this.resolve()).length;
443
276
  }
444
- /** Whether the element exists and is not disabled; does not wait */
445
- async isEnabled() {
446
- return readIsEnabled(this.actionability);
447
- }
448
- /** The opposite of `isEnabled`: true for a disabled element and for one that is not there; does not wait */
449
- async isDisabled() {
450
- return !(await this.isEnabled());
451
- }
452
- /** Like `isEnabled`, but null when the element is missing, so callers can tell "disabled" from "not there"; does not wait */
453
- async enabledState() {
454
- return readEnabledState(this.actionability);
455
- }
456
- /** Whether a checkbox or radio input is checked or the element has `aria-checked="true"`; null when the element is missing */
457
- async isChecked() {
458
- return readIsChecked(this.access);
459
- }
460
- /** The accessible name (what `getByRole`'s `name` is matched against), "" when there is none, or null when the element is missing */
461
- async accessibleName() {
462
- return readAccessibleName(this.access);
463
- }
464
- /** The accessible description (`aria-describedby`, `aria-description`, else `title` when it is not the name), or null when the element is missing */
465
- async accessibleDescription() {
466
- return readAccessibleDescription(this.access);
467
- }
468
- /** The class attribute as written ("" when the element has none), or null when the element is missing */
469
- async classAttribute() {
470
- return readClassAttribute(this.access);
471
- }
472
- /** Computed value of a CSS property (camelCase names become kebab-case, `--custom` is kept), or null when the element is missing */
473
- async cssValue(name) {
474
- return readCssValue(this.access, name);
475
- }
476
- /** Whether the element exists and accepts typing (enabled, not readonly, a text field); does not wait */
477
- async isEditable() {
478
- return readIsEditable(this.actionability);
479
- }
480
277
  /** Wait for certain time (ms). Default: 300ms */
481
278
  async wait(duration = DEFAULT_PAUSE_MS) {
482
279
  await pause(this.biDiConnector, this.contextId, duration);
483
280
  }
484
- /**
485
- * Waits until the element is attached, visible and enabled, focuses it and presses `key` with real keyboard input.
486
- * Named keys are `Enter`, `Escape`, `Tab`, `Backspace`, `Delete`, `Space`, `Insert`, `Home`, `End`, `PageUp`, `PageDown`,
487
- * `F1` to `F12` and the arrows; any other key must be a single character. A chord holds modifiers (`Control`, `Shift`,
488
- * `Alt`, `Meta`, `ControlOrMeta`) while the last key is pressed.
489
- * @example
490
- * await page.getByLabel("New todo").press("Enter");
491
- * await page.getByLabel("Name").press("Control+a");
492
- */
493
- async press(key, options) {
494
- await pressOnElement(this.access, key, options);
495
- }
496
- /** Waits until the field is editable and not covered, then replaces its value by typing `value`; an empty string clears it */
497
- async fill(value, options) {
498
- await fillElement(this.access, value, options);
499
- }
500
281
  }
@@ -90,7 +90,7 @@ export class APIRequestContext {
90
90
  if (cookie)
91
91
  sent.cookie = sent.cookie ? `${sent.cookie}; ${cookie}` : cookie;
92
92
  const remaining = deadline > 0 ? Math.max(1, deadline - Date.now()) : 0;
93
- const hop = await this.hop({ url, method, headers: sent, body }, options, remaining, abort.signal);
93
+ const hop = await this.hop({ url, method, headers: sent, body, budget: timeout }, options, remaining, abort.signal);
94
94
  if (hop.setCookies.length > 0)
95
95
  await this.cookies.store(url, hop.setCookies);
96
96
  const location = hop.headers.find((h) => h.name.toLowerCase() === "location")?.value;
@@ -9,6 +9,8 @@ export interface HopRequest {
9
9
  ignoreHTTPSErrors: boolean;
10
10
  /** Milliseconds this hop may take; 0 is no limit */
11
11
  timeout: number;
12
+ /** The timeout the caller set for the whole request, which the timeout message names (`timeout` is what is left of it) */
13
+ budget?: number | undefined;
12
14
  signal: AbortSignal;
13
15
  }
14
16
  export interface HopResponse {
@@ -104,7 +104,7 @@ export function sendHop(request) {
104
104
  request.timeout > 0
105
105
  ? setTimeout(() => {
106
106
  req.destroy();
107
- finish(new TransportError(`timed out after ${request.timeout}ms (${where})`, "ETIMEDOUT"));
107
+ finish(new TransportError(`timed out after ${request.budget ?? request.timeout}ms (${where})`, "ETIMEDOUT"));
108
108
  }, request.timeout)
109
109
  : undefined;
110
110
  req.on("error", (err) => finish(new TransportError(`${err.code ?? err.message} (${where})`, err.code)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itmetsam/samurai-framework",
3
- "version": "0.22.9",
3
+ "version": "0.22.11",
4
4
  "description": "Browser test framework over WebDriver BiDi: auto-waiting locators, environments and secrets, a recorder, and a command line.",
5
5
  "main": "dist/api.js",
6
6
  "exports": {