@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.
Files changed (70) hide show
  1. package/README.md +8 -3
  2. package/dist/assert/locator-assertions.d.ts +4 -0
  3. package/dist/assert/locator-assertions.js +10 -2
  4. package/dist/assert/text-match.d.ts +2 -2
  5. package/dist/assert/text-match.js +2 -2
  6. package/dist/browser/dialog.js +4 -1
  7. package/dist/browser/init-script.d.ts +11 -0
  8. package/dist/browser/init-script.js +29 -0
  9. package/dist/browser/page-locators.d.ts +18 -0
  10. package/dist/browser/page-locators.js +19 -0
  11. package/dist/browser/page-permissions.d.ts +6 -0
  12. package/dist/browser/page-permissions.js +15 -0
  13. package/dist/browser/page-settings.d.ts +3 -0
  14. package/dist/browser/page-settings.js +4 -0
  15. package/dist/browser/page.d.ts +29 -13
  16. package/dist/browser/page.js +46 -45
  17. package/dist/config/variables.js +2 -1
  18. package/dist/locator/accessible-name.d.ts +22 -0
  19. package/dist/locator/accessible-name.js +128 -0
  20. package/dist/locator/action-timeout-error.js +5 -1
  21. package/dist/locator/actionability.d.ts +3 -2
  22. package/dist/locator/actionability.js +4 -4
  23. package/dist/locator/attribute-locator.d.ts +13 -0
  24. package/dist/locator/attribute-locator.js +27 -0
  25. package/dist/locator/chain-resolver.d.ts +12 -3
  26. package/dist/locator/chain-resolver.js +85 -15
  27. package/dist/locator/check-actions.d.ts +15 -0
  28. package/dist/locator/check-actions.js +44 -0
  29. package/dist/locator/combine-step.d.ts +5 -0
  30. package/dist/locator/combine-step.js +15 -0
  31. package/dist/locator/control-probe.d.ts +10 -0
  32. package/dist/locator/control-probe.js +50 -0
  33. package/dist/locator/element-access.d.ts +24 -0
  34. package/dist/locator/element-access.js +53 -0
  35. package/dist/locator/element-actions.d.ts +17 -0
  36. package/dist/locator/element-actions.js +51 -0
  37. package/dist/locator/element-reads.d.ts +29 -4
  38. package/dist/locator/element-reads.js +83 -2
  39. package/dist/locator/element-state.d.ts +5 -0
  40. package/dist/locator/element-state.js +2 -0
  41. package/dist/locator/file-actions.d.ts +2 -2
  42. package/dist/locator/filter-step.d.ts +24 -0
  43. package/dist/locator/filter-step.js +72 -0
  44. package/dist/locator/label-locator.d.ts +1 -1
  45. package/dist/locator/locator.d.ts +94 -29
  46. package/dist/locator/locator.js +183 -151
  47. package/dist/locator/page-scripts.d.ts +2 -0
  48. package/dist/locator/page-scripts.js +6 -0
  49. package/dist/locator/page-search.d.ts +11 -2
  50. package/dist/locator/page-search.js +15 -3
  51. package/dist/locator/page-step.d.ts +16 -0
  52. package/dist/locator/page-step.js +35 -0
  53. package/dist/locator/role-locator.d.ts +65 -0
  54. package/dist/locator/role-locator.js +125 -0
  55. package/dist/locator/role-table.d.ts +18 -0
  56. package/dist/locator/role-table.js +190 -0
  57. package/dist/locator/select-actions.d.ts +13 -0
  58. package/dist/locator/select-actions.js +73 -0
  59. package/dist/locator/selector-string.d.ts +7 -0
  60. package/dist/locator/selector-string.js +27 -0
  61. package/dist/locator/selector.d.ts +47 -15
  62. package/dist/locator/selector.js +83 -12
  63. package/dist/locator/strict-mode.d.ts +6 -0
  64. package/dist/locator/strict-mode.js +6 -0
  65. package/dist/locator/text-locator.d.ts +1 -1
  66. package/dist/network/network-tracker.d.ts +17 -1
  67. package/dist/network/network-tracker.js +63 -18
  68. package/dist/network/url-match.js +3 -1
  69. package/dist/types/config.d.ts +7 -0
  70. package/package.json +1 -1
@@ -0,0 +1,17 @@
1
+ import { type ElementScreenshotOptions } from "../browser/screenshot.js";
2
+ import type { ActionOptions } from "./actionability.js";
3
+ import { type ElementAccess } from "./element-access.js";
4
+ /** Waits until the element is attached, visible, stable, enabled and not covered, then clicks its centre with real pointer input */
5
+ export declare function clickElement(access: ElementAccess, options?: ActionOptions): Promise<void>;
6
+ /** Takes a screenshot of just the element, after scrolling it into view and waiting until it is attached and visible */
7
+ export declare function screenshotElement(access: ElementAccess, options?: ElementScreenshotOptions & {
8
+ timeout?: number;
9
+ }): Promise<Buffer>;
10
+ /** Waits until the element is attached, then focuses it */
11
+ export declare function focusElement(access: ElementAccess, options?: ActionOptions): Promise<void>;
12
+ /** Waits until the element is attached, visible and enabled, focuses it and presses `key` with real keyboard input */
13
+ export declare function pressOnElement(access: ElementAccess, key: string, options?: ActionOptions): Promise<void>;
14
+ /** Waits until the field is editable and not covered, then replaces its value by typing `value`; an empty string clears it */
15
+ export declare function fillElement(access: ElementAccess, value: string, options?: ActionOptions & {
16
+ output?: string;
17
+ }): Promise<void>;
@@ -0,0 +1,51 @@
1
+ import { bindOutput } from "../wait/output-sink.js";
2
+ import { takeScreenshot, } from "../browser/screenshot.js";
3
+ import { callOnElement, callOnExistingElement, requireElement, } from "./element-access.js";
4
+ import { clickRect, DEFAULT_PAUSE_MS, NAMED_KEYS, pause, pressKey, typeText, } from "./input-actions.js";
5
+ import { ATTACHED_ONLY, CLICK_CHECKS, FILL_CHECKS, PRESS_CHECKS, SCREENSHOT_CHECKS, } from "./page-scripts.js";
6
+ /** Waits until the element is attached, visible, stable, enabled and not covered, then clicks its centre with real pointer input */
7
+ export async function clickElement(access, options) {
8
+ const state = await access.waitForActionable("click", CLICK_CHECKS, { scroll: true, hitTest: true }, options);
9
+ // "attached" is always required, so the box is present
10
+ await clickRect(access.connector, access.contextId, state.box);
11
+ }
12
+ /** Takes a screenshot of just the element, after scrolling it into view and waiting until it is attached and visible */
13
+ export async function screenshotElement(access, options = {}) {
14
+ const { timeout, ...capture } = options;
15
+ await access.waitForActionable("screenshot", SCREENSHOT_CHECKS, { scroll: true, hitTest: false }, timeout === undefined ? undefined : { timeout });
16
+ const el = await requireElement(access);
17
+ return takeScreenshot(access.connector, access.contextId, capture, {
18
+ sharedId: el.sharedId,
19
+ });
20
+ }
21
+ /** Waits until the element is attached, then focuses it */
22
+ export async function focusElement(access, options) {
23
+ await access.waitForActionable("focus", ATTACHED_ONLY, { scroll: false, hitTest: false }, options);
24
+ await callOnExistingElement(access, `el.focus();`);
25
+ }
26
+ /** Waits until the element is attached, visible and enabled, focuses it and presses `key` with real keyboard input */
27
+ export async function pressOnElement(access, key, options) {
28
+ const code = NAMED_KEYS[key] ?? (Array.from(key).length === 1 ? key : "");
29
+ if (code === "") {
30
+ throw new Error(`press(): unknown key "${key}". Use one of ${Object.keys(NAMED_KEYS).join(", ")} or a single character`);
31
+ }
32
+ await access.waitForActionable("press", PRESS_CHECKS, { scroll: true, hitTest: false }, options);
33
+ await callOnElement(access, undefined, `el.focus();`);
34
+ await pressKey(access.connector, access.contextId, code);
35
+ }
36
+ /** Waits until the field is editable and not covered, then replaces its value by typing `value`; an empty string clears it */
37
+ export async function fillElement(access, value, options) {
38
+ // Taken when the call starts, so a fill that outlives its test saves into that test's closed sink
39
+ const save = options?.output === undefined
40
+ ? undefined
41
+ : bindOutput("fill()", options.output);
42
+ const state = await access.waitForActionable("fill", FILL_CHECKS, { scroll: true, hitTest: true }, options);
43
+ // Typing over the selected existing value replaces it; Backspace clears it for an empty value
44
+ const keys = value === "" ? NAMED_KEYS.Backspace : value;
45
+ await clickRect(access.connector, access.contextId, state.box);
46
+ // Lets the focus the click caused (and the page's focus handlers) settle before the text is selected and typed over
47
+ await pause(access.connector, access.contextId, DEFAULT_PAUSE_MS);
48
+ await callOnElement(access, undefined, `if (typeof el.select === "function") el.select();`);
49
+ await typeText(access.connector, access.contextId, keys);
50
+ save?.(value);
51
+ }
@@ -1,4 +1,29 @@
1
- /** Body run in the page as `(el) => { ... }`: a checked checkbox or radio input, or `aria-checked="true"` */
2
- export declare const IS_CHECKED_BODY = "\n const type = (el.getAttribute(\"type\") || \"\").toLowerCase();\n const input = el.tagName === \"INPUT\" && (type === \"checkbox\" || type === \"radio\");\n return (input && el.checked) || el.getAttribute(\"aria-checked\") === \"true\";";
3
- /** The CSS property name for `getPropertyValue`: camelCase becomes kebab-case (`backgroundColor` to `background-color`), `--custom` and kebab-case names are kept */
4
- export declare function cssPropertyName(name: string): string;
1
+ import type { ElementRectangle } from "../types/bidi-modules/browsing-context.js";
2
+ import { type ActionabilityContext } from "./actionability.js";
3
+ import { type ElementAccess } from "./element-access.js";
4
+ /** Trimmed text content, or null when the element is missing */
5
+ export declare function readTextContent(access: ElementAccess): Promise<string | null>;
6
+ /** The accessible name (see `ACCESSIBLE_NAME_JS`), "" when it has none, or null when the element is missing */
7
+ export declare function readAccessibleName(access: ElementAccess): Promise<string | null>;
8
+ /** The accessible description, "" when it has none, or null when the element is missing */
9
+ export declare function readAccessibleDescription(access: ElementAccess): Promise<string | null>;
10
+ /** Value of an input/textarea/select, or null when missing */
11
+ export declare function readInputValue(access: ElementAccess): Promise<string | null>;
12
+ /** Attribute value, or null when the element or attribute is missing */
13
+ export declare function readAttribute(access: ElementAccess, name: string): Promise<string | null>;
14
+ /** Whether a checkbox or radio input is checked or the element has `aria-checked="true"`; null when the element is missing */
15
+ export declare function readIsChecked(access: ElementAccess): Promise<boolean | null>;
16
+ /** The class attribute as written ("" when the element has none), or null when the element is missing */
17
+ export declare function readClassAttribute(access: ElementAccess): Promise<string | null>;
18
+ /** Computed value of a CSS property (camelCase names become kebab-case, `--custom` is kept), or null when the element is missing */
19
+ export declare function readCssValue(access: ElementAccess, name: string): Promise<string | null>;
20
+ /** The browser's getBoundingClientRect result for the element */
21
+ export declare function readBoundingClientRect(access: ElementAccess): Promise<ElementRectangle>;
22
+ /** Whether the element exists, has a non-empty box and is not hidden by CSS */
23
+ export declare function readIsVisible(ctx: ActionabilityContext): Promise<boolean>;
24
+ /** Whether the element exists and is not disabled; does not wait */
25
+ export declare function readIsEnabled(ctx: ActionabilityContext): Promise<boolean>;
26
+ /** Like `readIsEnabled`, but null when the element is missing, so callers can tell "disabled" from "not there"; does not wait */
27
+ export declare function readEnabledState(ctx: ActionabilityContext): Promise<boolean | null>;
28
+ /** Whether the element exists and accepts typing (enabled, not readonly, a text field); does not wait */
29
+ export declare function readIsEditable(ctx: ActionabilityContext): Promise<boolean>;
@@ -1,11 +1,92 @@
1
+ import { probeState } from "./actionability.js";
2
+ import { callOnElement, callOnExistingElement, } from "./element-access.js";
3
+ import { describeType } from "./element-state.js";
4
+ import { ACCESSIBLE_NAME_JS } from "./accessible-name.js";
1
5
  /** Body run in the page as `(el) => { ... }`: a checked checkbox or radio input, or `aria-checked="true"` */
2
- export const IS_CHECKED_BODY = `
6
+ const IS_CHECKED_BODY = `
3
7
  const type = (el.getAttribute("type") || "").toLowerCase();
4
8
  const input = el.tagName === "INPUT" && (type === "checkbox" || type === "radio");
5
9
  return (input && el.checked) || el.getAttribute("aria-checked") === "true";`;
6
10
  /** The CSS property name for `getPropertyValue`: camelCase becomes kebab-case (`backgroundColor` to `background-color`), `--custom` and kebab-case names are kept */
7
- export function cssPropertyName(name) {
11
+ function cssPropertyName(name) {
8
12
  if (name.startsWith("--"))
9
13
  return name;
10
14
  return name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
11
15
  }
16
+ function booleanOrNull(value) {
17
+ if (typeof value === "boolean")
18
+ return value;
19
+ if (value === null)
20
+ return null;
21
+ throw new Error(`Expected a boolean or null but received "${describeType(value)}"`);
22
+ }
23
+ function stringOrNull(value) {
24
+ if (typeof value === "string")
25
+ return value;
26
+ if (value === null)
27
+ return null;
28
+ throw new Error(`Expected a string or null but received "${describeType(value)}"`);
29
+ }
30
+ /** Trimmed text content, or null when the element is missing */
31
+ export async function readTextContent(access) {
32
+ return stringOrNull(await callOnElement(access, null, `return el.textContent.trim();`, [], [], "textContent"));
33
+ }
34
+ /** The accessible name (see `ACCESSIBLE_NAME_JS`), "" when it has none, or null when the element is missing */
35
+ export async function readAccessibleName(access) {
36
+ return stringOrNull(await callOnElement(access, null, `${ACCESSIBLE_NAME_JS} return accessibleName(el);`, [], [], "accessibleName"));
37
+ }
38
+ /** The accessible description, "" when it has none, or null when the element is missing */
39
+ export async function readAccessibleDescription(access) {
40
+ return stringOrNull(await callOnElement(access, null, `${ACCESSIBLE_NAME_JS} return accessibleDescription(el);`, [], [], "accessibleDescription"));
41
+ }
42
+ /** Value of an input/textarea/select, or null when missing */
43
+ export async function readInputValue(access) {
44
+ return stringOrNull(await callOnElement(access, null, `return typeof el.value === "string" ? el.value : null;`, [], [], "inputValue"));
45
+ }
46
+ /** Attribute value, or null when the element or attribute is missing */
47
+ export async function readAttribute(access, name) {
48
+ return stringOrNull(await callOnElement(access, null, `return el.getAttribute(name);`, ["name"], [name], "getAttribute"));
49
+ }
50
+ /** Whether a checkbox or radio input is checked or the element has `aria-checked="true"`; null when the element is missing */
51
+ export async function readIsChecked(access) {
52
+ return booleanOrNull(await callOnElement(access, null, IS_CHECKED_BODY, [], [], "isChecked"));
53
+ }
54
+ /** The class attribute as written ("" when the element has none), or null when the element is missing */
55
+ export async function readClassAttribute(access) {
56
+ return stringOrNull(await callOnElement(access, null, `return el.getAttribute("class") ?? "";`, [], [], "classAttribute"));
57
+ }
58
+ /** Computed value of a CSS property (camelCase names become kebab-case, `--custom` is kept), or null when the element is missing */
59
+ export async function readCssValue(access, name) {
60
+ return stringOrNull(await callOnElement(access, null, `return getComputedStyle(el).getPropertyValue(name);`, ["name"], [cssPropertyName(name)], "cssValue"));
61
+ }
62
+ /** The browser's getBoundingClientRect result for the element */
63
+ export async function readBoundingClientRect(access) {
64
+ const rect = await callOnExistingElement(access, `
65
+ const { x, y, width, height, top, right, bottom, left } = el.getBoundingClientRect();
66
+ return { x, y, width, height, top, right, bottom, left };
67
+ `, "getBoundingClientRect");
68
+ if (typeof rect !== "object" || rect === null) {
69
+ throw new Error(`Expected to receive an object but instead received "${describeType(rect)}"`);
70
+ }
71
+ return rect;
72
+ }
73
+ /** Whether the element exists, has a non-empty box and is not hidden by CSS */
74
+ export async function readIsVisible(ctx) {
75
+ const state = await probeState(ctx, { scroll: false, hitTest: false }, "isVisible");
76
+ return state.attached && state.visible;
77
+ }
78
+ /** Whether the element exists and is not disabled; does not wait */
79
+ export async function readIsEnabled(ctx) {
80
+ const state = await probeState(ctx, { scroll: false, hitTest: false }, "isEnabled");
81
+ return state.attached && state.enabled;
82
+ }
83
+ /** Like `readIsEnabled`, but null when the element is missing, so callers can tell "disabled" from "not there"; does not wait */
84
+ export async function readEnabledState(ctx) {
85
+ const state = await probeState(ctx, { scroll: false, hitTest: false }, "enabledState");
86
+ return state.attached ? state.enabled : null;
87
+ }
88
+ /** Whether the element exists and accepts typing (enabled, not readonly, a text field); does not wait */
89
+ export async function readIsEditable(ctx) {
90
+ const state = await probeState(ctx, { scroll: false, hitTest: false }, "isEditable");
91
+ return state.attached && state.editable;
92
+ }
@@ -22,6 +22,11 @@ export interface ProbeOptions {
22
22
  scroll: boolean;
23
23
  /** Check which element receives a click at the element's centre */
24
24
  hitTest: boolean;
25
+ /**
26
+ * Probe a checkbox, radio or switch the way a user reaches it: opacity 0 does not hide it, and a click on
27
+ * one of its labels counts as hitting it (`control-probe.ts`). Only the check actions set this
28
+ */
29
+ control?: boolean;
25
30
  }
26
31
  export type WaitForState = "attached" | "detached" | "visible" | "hidden";
27
32
  export type CheckName = "attached" | "visible" | "stable" | "enabled" | "editable" | "hit target";
@@ -1,3 +1,4 @@
1
+ import { CONTROL_PROBE_BRANCH } from "./control-probe.js";
1
2
  const TEXT_INPUT_TYPES = [
2
3
  "text",
3
4
  "search",
@@ -26,6 +27,7 @@ export const PROBE_ELEMENT = `(el, options) => {
26
27
  if (!el || !el.isConnected) {
27
28
  return ${JSON.stringify(DETACHED_STATE)};
28
29
  }
30
+ ${CONTROL_PROBE_BRANCH}
29
31
  if (options.scroll) {
30
32
  const before = el.getBoundingClientRect();
31
33
  const inViewport = before.top >= 0 && before.left >= 0 &&
@@ -3,7 +3,7 @@ import type { BrowsingContext } from "../types/bidi-modules/browsing-context.js"
3
3
  import type { ElementHandle } from "../script/element-handle.js";
4
4
  import type { Arg } from "../script/serialize.js";
5
5
  import { type FileInput } from "../browser/generated-files.js";
6
- import type { CheckName, ProbeOptions } from "./element-state.js";
6
+ import type { CheckName, ElementState, ProbeOptions } from "./element-state.js";
7
7
  import type { ActionOptions } from "./actionability.js";
8
8
  /** What the file actions need of a locator */
9
9
  export interface ElementContext {
@@ -12,7 +12,7 @@ export interface ElementContext {
12
12
  /** The locator's description for error messages */
13
13
  readonly selector: string;
14
14
  call<T>(fn: string, args: Arg[], sandbox?: string): Promise<T>;
15
- waitForActionable(action: string, checks: readonly CheckName[], probe: ProbeOptions, options: ActionOptions | undefined): Promise<unknown>;
15
+ waitForActionable(action: string, checks: readonly CheckName[], probe: ProbeOptions, options: ActionOptions | undefined): Promise<ElementState>;
16
16
  /** The element the locator matches now; throws when none does */
17
17
  requireElement(): Promise<ElementHandle>;
18
18
  /** The error for an operation that needs an element when none matches */
@@ -0,0 +1,24 @@
1
+ import type { ElementHandle } from "../script/element-handle.js";
2
+ import type { CallInPage } from "./chain-resolver.js";
3
+ import type { FilterTests, Selector, TextCriterion } from "./selector.js";
4
+ /** What `filter()` and the options of `locator(selector, options)` take; `has` and `hasNot` are locators searched inside each candidate */
5
+ export interface FilterOptions<L = unknown> {
6
+ /** Keeps elements whose text contains this string (case-insensitive, whitespace collapsed), or matches this RegExp (tested on the text as it is) */
7
+ hasText?: TextCriterion;
8
+ /** Keeps elements whose text does not contain / match it */
9
+ hasNotText?: TextCriterion;
10
+ /** Keeps elements that contain a match of this locator, searched inside the element */
11
+ has?: L;
12
+ /** Keeps elements that contain no match of this locator */
13
+ hasNot?: L;
14
+ }
15
+ /** The `filter` step for the options; `chainOf` reads the chain of a `has` / `hasNot` locator (and rejects one with fallbacks) */
16
+ export declare function filterSelector<L>(options: FilterOptions<L>, chainOf: (locator: L, option: string) => readonly Selector[]): Selector;
17
+ /** What applying a filter needs: the page, and a search of a chain inside one element */
18
+ export interface FilterContext {
19
+ call: CallInPage;
20
+ /** The first match of `chain` inside `start` (its first step is scoped to it), or none */
21
+ findInside(chain: readonly Selector[], start: ElementHandle): Promise<ElementHandle[]>;
22
+ }
23
+ /** The candidates that pass every test of the filter, in their order */
24
+ export declare function applyFilter(candidates: readonly ElementHandle[], tests: FilterTests, { call, findInside }: FilterContext): Promise<ElementHandle[]>;
@@ -0,0 +1,72 @@
1
+ const OPTION_NAMES = ["hasText", "hasNotText", "has", "hasNot"];
2
+ function checkText(name, value) {
3
+ if (value !== undefined &&
4
+ typeof value !== "string" &&
5
+ !(value instanceof RegExp)) {
6
+ throw new TypeError(`filter(): ${name} must be a string or a RegExp`);
7
+ }
8
+ }
9
+ /** The `filter` step for the options; `chainOf` reads the chain of a `has` / `hasNot` locator (and rejects one with fallbacks) */
10
+ export function filterSelector(options, chainOf) {
11
+ if (typeof options !== "object" || options === null) {
12
+ throw new TypeError("filter(): options must be an object");
13
+ }
14
+ const given = OPTION_NAMES.filter((name) => options[name] !== undefined);
15
+ if (given.length === 0) {
16
+ throw new TypeError("filter(): give at least one of hasText, hasNotText, has or hasNot");
17
+ }
18
+ checkText("hasText", options.hasText);
19
+ checkText("hasNotText", options.hasNotText);
20
+ const tests = {};
21
+ if (options.hasText !== undefined)
22
+ tests.hasText = options.hasText;
23
+ if (options.hasNotText !== undefined)
24
+ tests.hasNotText = options.hasNotText;
25
+ if (options.has !== undefined)
26
+ tests.has = chainOf(options.has, "has");
27
+ if (options.hasNot !== undefined)
28
+ tests.hasNot = chainOf(options.hasNot, "hasNot");
29
+ return { kind: "filter", ...tests };
30
+ }
31
+ /**
32
+ * Function declaration run in the page as `(elements, hasText, hasNotText)`: the elements whose
33
+ * `textContent` passes both tests. A string test is a substring match with whitespace collapsed and case
34
+ * ignored (Playwright's `hasText`); a RegExp is tested on the text as it is, without its `g` and `y` flags.
35
+ */
36
+ const TEXT_FILTER = `(elements, hasText, hasNotText) => {
37
+ const collapse = (text) => String(text).replace(/\\s+/g, " ").trim().toLowerCase();
38
+ const found = (el, wanted) => {
39
+ const text = el.textContent ?? "";
40
+ return typeof wanted === "string"
41
+ ? collapse(text).includes(collapse(wanted))
42
+ : new RegExp(wanted.source, wanted.flags.replace(/[gy]/g, "")).test(text);
43
+ };
44
+ return elements.filter((el) =>
45
+ (hasText === undefined || found(el, hasText)) &&
46
+ (hasNotText === undefined || !found(el, hasNotText)));
47
+ }`;
48
+ /** The candidates that pass every test of the filter, in their order */
49
+ export async function applyFilter(candidates, tests, { call, findInside }) {
50
+ let kept = [...candidates];
51
+ if (tests.hasText !== undefined || tests.hasNotText !== undefined) {
52
+ kept = await call(TEXT_FILTER, [
53
+ kept,
54
+ tests.hasText,
55
+ tests.hasNotText,
56
+ ]);
57
+ }
58
+ for (const [chain, wanted] of [
59
+ [tests.has, true],
60
+ [tests.hasNot, false],
61
+ ]) {
62
+ if (!chain)
63
+ continue;
64
+ const passing = [];
65
+ for (const candidate of kept) {
66
+ if ((await findInside(chain, candidate)).length > 0 === wanted)
67
+ passing.push(candidate);
68
+ }
69
+ kept = passing;
70
+ }
71
+ return kept;
72
+ }
@@ -10,4 +10,4 @@
10
10
  *
11
11
  * A string, so the transpiler cannot inject helpers that do not exist in the page.
12
12
  */
13
- export declare const LABEL_LOCATE = "(starts, value, match, ignoreCase) => {\n \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 };\n const textOf = (node) => node.innerText ?? node.textContent ?? \"\";\n const labelsOf = (el) => {\n const texts = [];\n if (el.labels) for (const label of el.labels) texts.push(textOf(label));\n const by = el.getAttribute(\"aria-labelledby\");\n if (by) {\n for (const id of by.split(/\\s+/)) {\n const named = id && document.getElementById(id);\n if (named) texts.push(textOf(named));\n }\n }\n const own = el.getAttribute(\"aria-label\");\n if (own) texts.push(own);\n return texts;\n };\n const roots = starts.length > 0 ? starts : [document];\n const seen = new Set();\n const matches = [];\n for (const root of roots) {\n for (const el of root.querySelectorAll(\"*\")) {\n if (seen.has(el)) continue;\n seen.add(el);\n if (labelsOf(el).some(textMatches)) matches.push(el);\n }\n }\n return matches.sort((a, b) => (a === b ? 0 : a.compareDocumentPosition(b) & 4 ? -1 : 1));\n}";
13
+ export declare const LABEL_LOCATE = "(starts, value, match, ignoreCase) => {\n \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 };\n const textOf = (node) => node.innerText ?? node.textContent ?? \"\";\n const labelsOf = (el) => {\n const texts = [];\n if (el.labels) for (const label of el.labels) texts.push(textOf(label));\n const by = el.getAttribute(\"aria-labelledby\");\n if (by) {\n for (const id of by.split(/\\s+/)) {\n const named = id && document.getElementById(id);\n if (named) texts.push(textOf(named));\n }\n }\n const own = el.getAttribute(\"aria-label\");\n if (own) texts.push(own);\n return texts;\n };\n const roots = starts.length > 0 ? starts : [document];\n const seen = new Set();\n const matches = [];\n for (const root of roots) {\n for (const el of root.querySelectorAll(\"*\")) {\n if (seen.has(el)) continue;\n seen.add(el);\n if (labelsOf(el).some(textMatches)) matches.push(el);\n }\n }\n return matches.sort((a, b) => (a === b ? 0 : a.compareDocumentPosition(b) & 4 ? -1 : 1));\n}";
@@ -1,12 +1,17 @@
1
1
  import type { BiDiConnector } from "../transport/bidi-connection.js";
2
2
  import type { BrowsingContext, ElementRectangle } from "../types/bidi-modules/browsing-context.js";
3
3
  import { ElementHandle } from "../script/element-handle.js";
4
- import { type ElementScreenshotOptions } from "../browser/screenshot.js";
4
+ import type { ElementScreenshotOptions } from "../browser/screenshot.js";
5
5
  import type { FileInput } from "../browser/generated-files.js";
6
6
  import { type ActionOptions } from "./actionability.js";
7
- import { type WaitForState } from "./element-state.js";
7
+ import { type CheckOptions } from "./check-actions.js";
8
+ import type { WaitForState } from "./element-state.js";
8
9
  import type { HelperInstaller } from "./helper-realm.js";
9
- import { type Selector, type TextOptions } from "./selector.js";
10
+ import { type StrictSource } from "./strict-mode.js";
11
+ import type { RoleOptions } from "./role-locator.js";
12
+ import { type Selector, type ExactOption, type TextOptions } from "./selector.js";
13
+ import { type SelectOptionValue } from "./select-actions.js";
14
+ import { type FilterOptions } from "./filter-step.js";
10
15
  export type { ActionOptions } from "./actionability.js";
11
16
  export default class Locator {
12
17
  private biDiConnector;
@@ -14,19 +19,23 @@ export default class Locator {
14
19
  private chain;
15
20
  private helpers;
16
21
  private alternatives;
22
+ private strictness;
23
+ /** Asked once per locator: the answer comes from the config, which a wait would otherwise read on every poll */
24
+ private strictAnswer;
17
25
  private matched;
18
26
  private resolver;
19
27
  /** What the actionability waits use of this locator */
20
28
  private actionability;
21
- /** What the file actions use of this locator */
22
- private elementContext;
29
+ /** What the reads and actions use of this locator */
30
+ private access;
23
31
  /**
24
32
  * `selector` is an xpath, or a chain of selector steps that each search inside the previous step's matches.
25
33
  * `helpers` is the page's helper realm. With it the actionability probe is installed once per
26
34
  * document and called by name; without it (a locator built by hand) the probe source is sent on every poll.
27
35
  * `alternatives` are fallback locators tried in order whenever this chain matches nothing (see `withFallbacks`).
36
+ * `strictness` says whether actions and reads throw when the locator matches several elements (see `strict`); default no.
28
37
  */
29
- constructor(selector: string | readonly Selector[], biDiConnector: BiDiConnector, contextId: BrowsingContext, helpers?: HelperInstaller, alternatives?: readonly Locator[]);
38
+ constructor(selector: string | readonly Selector[], biDiConnector: BiDiConnector, contextId: BrowsingContext, helpers?: HelperInstaller, alternatives?: readonly Locator[], strictness?: StrictSource);
30
39
  /**
31
40
  * The description of this locator used in error messages, e.g. `//form >> role=button[name="Save"]`;
32
41
  * fallbacks follow as `or …`
@@ -43,6 +52,14 @@ export default class Locator {
43
52
  * page.getByTestId("save").withFallbacks(page.getByRole("button", { name: "Save" }), page.getByText("Save"));
44
53
  */
45
54
  withFallbacks(...fallbacks: Locator[]): Locator;
55
+ /**
56
+ * This locator, strict or not whatever the page and the config say: a strict locator makes actions and reads
57
+ * (`click`, `fill`, `textContent`, ...) throw `name(): strict mode violation, N elements match ...` when it
58
+ * matches several elements, instead of using the first. Steps added to it stay strict. See config `use.strictSelectors`
59
+ * @example
60
+ * await page.locator("li").strict().click(); // throws when there is more than one li
61
+ */
62
+ strict(enabled?: boolean): Locator;
46
63
  /**
47
64
  * Which locator found elements the last time this one searched: index 0 is the primary, 1 the first
48
65
  * fallback, and so on. `undefined` before any search or when the last search found nothing.
@@ -51,20 +68,49 @@ export default class Locator {
51
68
  index: number;
52
69
  selector: string;
53
70
  } | undefined;
54
- /** Elements matching the label text (`<label>`, `aria-labelledby`, `aria-label`), inside this locator's matches */
55
- getByLabel(text: string, options?: TextOptions): Locator;
71
+ /** Elements matching the label text (`<label>`, `aria-labelledby`, `aria-label`), inside this locator's matches. See `TextOptions` for `exact` and RegExp */
72
+ getByLabel(text: string | RegExp, options?: TextOptions): Locator;
73
+ /** Elements whose `placeholder` matches, inside this locator's matches. See `TextOptions` for `exact` and RegExp */
74
+ getByPlaceholder(text: string | RegExp, options?: ExactOption): Locator;
75
+ /** Elements whose `alt` text matches (images, areas, image inputs), inside this locator's matches. See `TextOptions` */
76
+ getByAltText(text: string | RegExp, options?: ExactOption): Locator;
77
+ /** Elements whose `title` attribute matches, inside this locator's matches. See `TextOptions` */
78
+ getByTitle(text: string | RegExp, options?: ExactOption): Locator;
56
79
  /** Elements whose `data-testid` attribute equals `testId`, inside this locator's matches */
57
80
  getByTestId(testId: string): Locator;
58
- /** Elements matching `xpath` inside this locator's matches (relative and `//` paths search inside them) */
59
- locator(xpath: string): Locator;
81
+ /**
82
+ * Elements matching `selector` inside this locator's matches. An xpath (`//x`, `./x`, `xpath=…`, or an older
83
+ * relative one such as `a[@id='x']`) searches inside them; anything else is css (`css=` is accepted).
84
+ * `options` filter the matches, as `filter()` does
85
+ */
86
+ locator(selector: string, options?: FilterOptions<Locator>): Locator;
60
87
  /** Elements matching the CSS selector inside this locator's matches */
61
88
  getByCss(css: string): Locator;
62
- /** Elements whose text matches, inside this locator's matches. Exact match unless `match: "partial"` */
63
- getByText(text: string, options?: TextOptions): Locator;
64
- /** Elements with the ARIA role (and accessible name, when given), inside this locator's matches */
65
- getByRole(role: string, options?: {
66
- name?: string;
67
- }): Locator;
89
+ /** Elements whose text matches, inside this locator's matches. Exact match unless `match: "partial"` or `exact: false`; a RegExp is tested as written */
90
+ getByText(text: string | RegExp, options?: TextOptions): Locator;
91
+ /** Elements with the ARIA role (and accessible name and state, when given), inside this locator's matches. See `RoleOptions` */
92
+ getByRole(role: string, options?: RoleOptions): Locator;
93
+ /** 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 */
94
+ nth(index: number): Locator;
95
+ /**
96
+ * The matches that pass the tests, in their order. `hasText` keeps elements whose text contains the string
97
+ * (case-insensitive, whitespace collapsed; this is Playwright's rule, the `getBy*` text matching is unchanged)
98
+ * or matches the RegExp; `has` keeps elements containing a match of the locator, searched inside them.
99
+ * @example
100
+ * await page.locator("li").filter({ hasText: "paid", has: page.getByRole("button") }).first().click();
101
+ */
102
+ filter(options: FilterOptions<Locator>): Locator;
103
+ /** The elements both this locator and `other` match, in this locator's order */
104
+ and(other: Locator): Locator;
105
+ /** The elements either this locator or `other` matches, in document order, each once */
106
+ or(other: Locator): Locator;
107
+ private combined;
108
+ /** The chain of a locator used inside another (`has`); a locator with fallbacks cannot be */
109
+ private chainFor;
110
+ /** The first match, in document order */
111
+ first(): Locator;
112
+ /** The last match, in document order */
113
+ last(): Locator;
68
114
  /** One locator per element currently matching, in document order */
69
115
  all(): Promise<Locator[]>;
70
116
  /**
@@ -76,24 +122,35 @@ export default class Locator {
76
122
  /** The primary or the first fallback that currently matches, without its own fallbacks */
77
123
  private firstMatching;
78
124
  private withoutFallbacks;
79
- private resolveOne;
80
- /** Calls `fn` in the page; a throwing function becomes a "Failed to locate element" error */
81
- private call;
82
125
  /**
83
- * Calls a function on the first matching element: `body` sees it as `el`, followed by `params`.
84
- * Answers `whenMissing` when nothing matches, or the element disappeared meanwhile.
126
+ * The element the locator matches first. With a `method` name and strict selectors on, several matches
127
+ * throw instead (`method` is the public method that asked, for the message)
85
128
  */
86
- private callOnElement;
87
- /** Like `callOnElement`, for operations that cannot go on without the element */
88
- private callOnExistingElement;
89
- /** The error for an operation that needs an element when none matches */
90
- private noElementError;
91
- /** The element the locator matches now; throws when none does */
92
- private requireElement;
93
- private probeState;
129
+ private resolveOne;
94
130
  private waitForActionable;
95
131
  /** Waits until the element is attached, visible, stable, enabled and not covered, then clicks its centre with real pointer input */
96
132
  click(options?: ActionOptions): Promise<void>;
133
+ /**
134
+ * Checks a checkbox, radio or `role=checkbox|radio|switch` element; does nothing when it is already checked.
135
+ * Waits like `click`, but a hidden input counts as reachable through its `<label>`, which is what gets clicked.
136
+ * `trial` waits without clicking. Throws when the click did not change the state.
137
+ * @example
138
+ * await page.getByLabel("I accept the terms").check();
139
+ */
140
+ check(options?: CheckOptions): Promise<void>;
141
+ /** Unchecks a checkbox or `role=checkbox|switch` element; does nothing when it is already unchecked. A radio button cannot be unchecked: throws */
142
+ uncheck(options?: CheckOptions): Promise<void>;
143
+ /** `check()` or `uncheck()`, whichever brings the element to `checked` */
144
+ setChecked(checked: boolean, options?: CheckOptions): Promise<void>;
145
+ /**
146
+ * Selects options of a `<select>` or `<select multiple>` (which replaces its selection) and fires `input` and
147
+ * `change`. A string matches an option's value or its label; `{ value, label, index }` must match every field
148
+ * given. Waits until the select is visible and enabled and every option exists. Returns the values now selected.
149
+ * @example
150
+ * await page.getByLabel("Country").selectOption("Netherlands");
151
+ * await page.locator("select").selectOption([{ index: 0 }, { value: "b" }]);
152
+ */
153
+ selectOption(values: SelectOptionValue | SelectOptionValue[], options?: ActionOptions): Promise<string[]>;
97
154
  /**
98
155
  * Sets the files of an `<input type=file>`; `[]` clears it. Paths are relative to the project folder (absolute
99
156
  * ones are used as they are); `generatedFile(…)` writes a file of a given size when the step runs.
@@ -136,6 +193,8 @@ export default class Locator {
136
193
  getBoundingClientRect(): Promise<ElementRectangle>;
137
194
  /** Whether the element exists, has a non-empty box and is not hidden by CSS */
138
195
  isVisible(): Promise<boolean>;
196
+ /** The opposite of `isVisible`: true for a hidden element and for one that is not there */
197
+ isHidden(): Promise<boolean>;
139
198
  /** Trimmed text content, or null when the element is missing */
140
199
  textContent(): Promise<string | null>;
141
200
  /** Value of an input/textarea/select, or null when missing */
@@ -148,10 +207,16 @@ export default class Locator {
148
207
  count(): Promise<number>;
149
208
  /** Whether the element exists and is not disabled; does not wait */
150
209
  isEnabled(): Promise<boolean>;
210
+ /** The opposite of `isEnabled`: true for a disabled element and for one that is not there; does not wait */
211
+ isDisabled(): Promise<boolean>;
151
212
  /** Like `isEnabled`, but null when the element is missing, so callers can tell "disabled" from "not there"; does not wait */
152
213
  enabledState(): Promise<boolean | null>;
153
214
  /** Whether a checkbox or radio input is checked or the element has `aria-checked="true"`; null when the element is missing */
154
215
  isChecked(): Promise<boolean | null>;
216
+ /** The accessible name (what `getByRole`'s `name` is matched against), "" when there is none, or null when the element is missing */
217
+ accessibleName(): Promise<string | null>;
218
+ /** The accessible description (`aria-describedby`, `aria-description`, else `title` when it is not the name), or null when the element is missing */
219
+ accessibleDescription(): Promise<string | null>;
155
220
  /** The class attribute as written ("" when the element has none), or null when the element is missing */
156
221
  classAttribute(): Promise<string | null>;
157
222
  /** Computed value of a CSS property (camelCase names become kebab-case, `--custom` is kept), or null when the element is missing */