@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
@@ -0,0 +1,53 @@
1
+ import { format } from "./assertion-error.js";
2
+ /** Calls `fn` and says whether it threw */
3
+ export function callAndCatch(fn) {
4
+ try {
5
+ fn();
6
+ return { threw: false };
7
+ }
8
+ catch (error) {
9
+ return { threw: true, error };
10
+ }
11
+ }
12
+ function messageOf(error) {
13
+ if (error instanceof Error)
14
+ return error.message;
15
+ if (typeof error === "object" && error !== null && "message" in error)
16
+ return String(error.message);
17
+ return String(error);
18
+ }
19
+ /** Whether what was thrown fits `expected`; with no expectation, throwing at all fits */
20
+ export function thrownMatches(outcome, expected) {
21
+ if (!outcome.threw)
22
+ return false;
23
+ if (expected === undefined)
24
+ return true;
25
+ if (typeof expected === "string")
26
+ return messageOf(outcome.error).includes(expected);
27
+ if (expected instanceof RegExp)
28
+ return messageOf(outcome.error).search(expected) !== -1;
29
+ if (expected instanceof Error)
30
+ return messageOf(outcome.error) === expected.message;
31
+ return outcome.error instanceof expected;
32
+ }
33
+ /** What a `toThrow` failure shows as the received value */
34
+ export function describeOutcome(outcome) {
35
+ return outcome.threw ? `threw ${format(outcome.error)}` : "did not throw";
36
+ }
37
+ /** Jest's rule: the difference is under half a unit of the last digit asked for */
38
+ export function isCloseTo(actual, expected, digits) {
39
+ if (actual === expected)
40
+ return true;
41
+ return Math.abs(expected - actual) < Math.pow(10, -digits) / 2;
42
+ }
43
+ /** The `length` of a string, array or array-like; undefined for anything else */
44
+ export function lengthOf(value) {
45
+ if (typeof value === "string")
46
+ return value.length;
47
+ if (typeof value === "object" && value !== null && "length" in value) {
48
+ const length = value.length;
49
+ if (typeof length === "number")
50
+ return length;
51
+ }
52
+ return undefined;
53
+ }
@@ -30,7 +30,10 @@ export class Dialog {
30
30
  }
31
31
  /** Clicks OK; `promptText` is typed into a `prompt` first */
32
32
  accept(promptText) {
33
- return this.answer({ accept: true, ...(promptText !== undefined && { userText: promptText }) });
33
+ return this.answer({
34
+ accept: true,
35
+ ...(promptText !== undefined && { userText: promptText }),
36
+ });
34
37
  }
35
38
  /** Clicks Cancel */
36
39
  dismiss() {
@@ -1,2 +1,13 @@
1
+ import { type PreloadHandle } from "../script/preload.js";
2
+ import type { BiDiConnector } from "../transport/bidi-connection.js";
3
+ import type { BrowsingContext } from "../types/bidi-modules/browsing-context.js";
1
4
  /** Wraps an init script as one function declaration; the argument is embedded as JSON because preload scripts take no arguments */
2
5
  export declare function initScriptSource(script: string | ((arg?: any) => void), arg: unknown): string;
6
+ /** The init scripts a page registered, so they can be released with the page */
7
+ export declare class InitScripts {
8
+ private scripts;
9
+ /** Registers `script` for every document the context loads from now on, and runs it in the one loaded now */
10
+ add(biDiConnector: BiDiConnector, contextId: BrowsingContext, script: string | ((arg?: any) => void), arg?: unknown): Promise<PreloadHandle>;
11
+ /** The scripts still registered */
12
+ all(): PreloadHandle[];
13
+ }
@@ -1,4 +1,5 @@
1
1
  import { toFunctionDeclaration } from "../script/call-function.js";
2
+ import { addPreload } from "../script/preload.js";
2
3
  /** Wraps an init script as one function declaration; the argument is embedded as JSON because preload scripts take no arguments */
3
4
  export function initScriptSource(script, arg) {
4
5
  if (typeof script === "string") {
@@ -14,3 +15,31 @@ export function initScriptSource(script, arg) {
14
15
  }
15
16
  return `() => (${declaration})(${argument})`;
16
17
  }
18
+ /** The init scripts a page registered, so they can be released with the page */
19
+ export class InitScripts {
20
+ constructor() {
21
+ this.scripts = new Set();
22
+ }
23
+ /** Registers `script` for every document the context loads from now on, and runs it in the one loaded now */
24
+ async add(biDiConnector, contextId, script, arg) {
25
+ const source = initScriptSource(script, arg);
26
+ const handle = await addPreload(biDiConnector, {
27
+ source,
28
+ contexts: [contextId],
29
+ onRunError: "log",
30
+ });
31
+ const tracked = {
32
+ id: handle.id,
33
+ dispose: async () => {
34
+ this.scripts.delete(tracked);
35
+ await handle.dispose();
36
+ },
37
+ };
38
+ this.scripts.add(tracked);
39
+ return tracked;
40
+ }
41
+ /** The scripts still registered */
42
+ all() {
43
+ return [...this.scripts];
44
+ }
45
+ }
@@ -9,5 +9,9 @@ export declare class NavigationError extends Error {
9
9
  }
10
10
  /** The page did not reach the awaited load state in time */
11
11
  export declare class LoadStateTimeoutError extends WaitTimeoutError<undefined> {
12
- constructor(state: string, timeout: number);
12
+ constructor(state: string, timeout: number, operation?: string);
13
+ }
14
+ /** The page's URL did not match in time */
15
+ export declare class UrlTimeoutError extends WaitTimeoutError<string> {
16
+ constructor(expected: string, timeout: number, lastUrl: string | undefined);
13
17
  }
@@ -11,9 +11,17 @@ export class NavigationError extends Error {
11
11
  }
12
12
  /** The page did not reach the awaited load state in time */
13
13
  export class LoadStateTimeoutError extends WaitTimeoutError {
14
- constructor(state, timeout) {
14
+ constructor(state, timeout, operation = "waitForLoadState") {
15
15
  super(timeout, undefined);
16
16
  this.name = "LoadStateTimeoutError";
17
- this.message = `waitForLoadState("${state}") did not finish within ${timeout}ms`;
17
+ this.message = `${operation}("${state}") did not finish within ${timeout}ms`;
18
+ }
19
+ }
20
+ /** The page's URL did not match in time */
21
+ export class UrlTimeoutError extends WaitTimeoutError {
22
+ constructor(expected, timeout, lastUrl) {
23
+ super(timeout, lastUrl);
24
+ this.name = "UrlTimeoutError";
25
+ this.message = `waitForURL(): the URL did not match ${expected} within ${timeout}ms (last URL: ${lastUrl ?? "unknown"})`;
18
26
  }
19
27
  }
@@ -0,0 +1,5 @@
1
+ import type { BiDiConnector } from "../transport/bidi-connection.js";
2
+ /** The page's markup now, with its doctype */
3
+ export declare function readContent(connector: BiDiConnector, context: string): Promise<string>;
4
+ /** Writes `html` as the page's document; true when the document reached `state` within `timeout` ms */
5
+ export declare function writeContent(connector: BiDiConnector, context: string, html: string, state: "load" | "domcontentloaded", timeout: number): Promise<boolean>;
@@ -0,0 +1,43 @@
1
+ import { callFunction } from "../script/call-function.js";
2
+ /** The document as markup: the doctype (when there is one) followed by the document element */
3
+ const READ_CONTENT = `() => {
4
+ const type = document.doctype;
5
+ const doctype = type
6
+ ? "<!DOCTYPE " + type.name +
7
+ (type.publicId ? ' PUBLIC "' + type.publicId + '"' : "") +
8
+ (!type.publicId && type.systemId ? " SYSTEM" : "") +
9
+ (type.systemId ? ' "' + type.systemId + '"' : "") + ">"
10
+ : "";
11
+ return doctype + document.documentElement.outerHTML;
12
+ }`;
13
+ /**
14
+ * Replaces the document in place with `document.open()`, `write()` and `close()`: scripts and styles in `html` run and
15
+ * apply, and the page keeps its URL and origin. The listeners are added after `open()` because it removes the old ones.
16
+ * Resolves true once the `load` (or `DOMContentLoaded`) event fired, false when `timeout` ms passed first.
17
+ */
18
+ const WRITE_CONTENT = `async (html, state, timeout) => {
19
+ document.open();
20
+ const reached = new Promise((resolve) => {
21
+ const done = () => resolve(true);
22
+ if (state === "domcontentloaded") document.addEventListener("DOMContentLoaded", done, { once: true });
23
+ else window.addEventListener("load", done, { once: true });
24
+ setTimeout(() => resolve(false), timeout);
25
+ });
26
+ document.write(html);
27
+ document.close();
28
+ return reached;
29
+ }`;
30
+ /** The page's markup now, with its doctype */
31
+ export function readContent(connector, context) {
32
+ return callFunction(connector, context, READ_CONTENT, [], {
33
+ awaitPromise: false,
34
+ });
35
+ }
36
+ /** Writes `html` as the page's document; true when the document reached `state` within `timeout` ms */
37
+ export function writeContent(connector, context, html, state, timeout) {
38
+ return callFunction(connector, context, WRITE_CONTENT, [
39
+ html,
40
+ state,
41
+ timeout,
42
+ ]);
43
+ }
@@ -0,0 +1,9 @@
1
+ import { type PageFunction } from "../script/call-function.js";
2
+ import type { BiDiConnector } from "../transport/bidi-connection.js";
3
+ /** Whether a string is the source of a function (called with the argument) rather than an expression (evaluated) */
4
+ export declare function isFunctionSource(source: string): boolean;
5
+ /**
6
+ * Runs a function, or a string, in the page and returns the result as plain JavaScript (see `fromRemoteValue`).
7
+ * A function (or a string holding a function's source) is called with `arg`; any other string is an expression.
8
+ */
9
+ export declare function evaluateInPage<R, A>(connector: BiDiConnector, context: string, fn: PageFunction<[A], R>, arg?: A): Promise<R>;
@@ -0,0 +1,20 @@
1
+ import { callFunction, evaluateExpression, } from "../script/call-function.js";
2
+ /** A function (or arrow function, async or not) written out as source, as opposed to an expression */
3
+ const FUNCTION_SOURCE = /^\s*(?:async\s+)?(?:function\b|(?:\((?:[^()]|\([^()]*\))*\)|[\w$]+)\s*=>)/;
4
+ /** Whether a string is the source of a function (called with the argument) rather than an expression (evaluated) */
5
+ export function isFunctionSource(source) {
6
+ return FUNCTION_SOURCE.test(source);
7
+ }
8
+ /**
9
+ * Runs a function, or a string, in the page and returns the result as plain JavaScript (see `fromRemoteValue`).
10
+ * A function (or a string holding a function's source) is called with `arg`; any other string is an expression.
11
+ */
12
+ export async function evaluateInPage(connector, context, fn, arg) {
13
+ if (typeof fn === "string" && !isFunctionSource(fn)) {
14
+ if (arg !== undefined) {
15
+ throw new TypeError("evaluate(): an expression takes no argument; pass a function to use one");
16
+ }
17
+ return evaluateExpression(connector, context, fn);
18
+ }
19
+ return callFunction(connector, context, fn, arg === undefined ? [] : [arg]);
20
+ }
@@ -0,0 +1,18 @@
1
+ import Locator from "../locator/locator.js";
2
+ import type { HelperRealm } from "../locator/helper-realm.js";
3
+ import type { BiDiConnector } from "../transport/bidi-connection.js";
4
+ import type { BrowsingContext } from "../types/bidi-modules/browsing-context.js";
5
+ import type { PageConfig } from "./page-settings.js";
6
+ /** Makes the page's locators, and holds whether they are strict */
7
+ export declare class PageLocators {
8
+ private biDiConnector;
9
+ private id;
10
+ private helperRealm;
11
+ private pageConfig;
12
+ private assertOpen;
13
+ private strictOverride;
14
+ constructor(biDiConnector: BiDiConnector, id: BrowsingContext, helperRealm: HelperRealm, pageConfig: PageConfig, assertOpen: () => void);
15
+ locate(selector: ConstructorParameters<typeof Locator>[0]): Locator;
16
+ /** `undefined` goes back to config `use.strictSelectors`; a locator asks once, at its first action or read, so earlier ones that already ran keep their answer */
17
+ setStrictSelectors(enabled: boolean | undefined): void;
18
+ }
@@ -0,0 +1,19 @@
1
+ import Locator from "../locator/locator.js";
2
+ /** Makes the page's locators, and holds whether they are strict */
3
+ export class PageLocators {
4
+ constructor(biDiConnector, id, helperRealm, pageConfig, assertOpen) {
5
+ this.biDiConnector = biDiConnector;
6
+ this.id = id;
7
+ this.helperRealm = helperRealm;
8
+ this.pageConfig = pageConfig;
9
+ this.assertOpen = assertOpen;
10
+ }
11
+ locate(selector) {
12
+ this.assertOpen();
13
+ return new Locator(selector, this.biDiConnector, this.id, this.helperRealm, [], () => this.pageConfig.strictSelectors(this.strictOverride));
14
+ }
15
+ /** `undefined` goes back to config `use.strictSelectors`; a locator asks once, at its first action or read, so earlier ones that already ran keep their answer */
16
+ setStrictSelectors(enabled) {
17
+ this.strictOverride = enabled;
18
+ }
19
+ }
@@ -1,4 +1,5 @@
1
1
  import type { BiDiConnector } from "../transport/bidi-connection.js";
2
+ import { type UrlPattern } from "../network/url-match.js";
2
3
  import type { DocumentWatcher } from "./document-watcher.js";
3
4
  import type { PageConfig, ReadinessState } from "./page-settings.js";
4
5
  export type LoadState = "load" | "domcontentloaded" | "networkidle";
@@ -54,6 +55,23 @@ export declare class PageNavigation {
54
55
  waitForLoadState(state?: LoadState, options?: {
55
56
  timeout?: number;
56
57
  }): Promise<void>;
58
+ /**
59
+ * Waits until the page's URL matches `url` (a string equals it, or with `*` / `**` is a glob; a RegExp is tested on it;
60
+ * a function gets it as a `URL`), then waits for `waitUntil` (default "load"). A string starting with "/", "./" or
61
+ * "../" resolves against the base URL, as in `goto`. `timeout` applies to the URL, then again to the load state.
62
+ */
63
+ waitForURL(url: UrlPattern | ((url: URL) => boolean), options?: {
64
+ timeout?: number;
65
+ waitUntil?: LoadState;
66
+ }): Promise<void>;
67
+ /**
68
+ * Replaces the document with `html` (see `writeContent`) and waits for `waitUntil` (default "load"); "networkidle" is
69
+ * "load" plus a quiet network. The page keeps its URL.
70
+ */
71
+ setContent(html: string, options?: {
72
+ timeout?: number;
73
+ waitUntil?: LoadState;
74
+ }): Promise<void>;
57
75
  /**
58
76
  * Runs a navigation command and settles what waiting for it means: failures become NavigationErrors, and a
59
77
  * navigation that did not wait for the load is watched so `waitForLoadState` can tell it from the old page.
@@ -1,8 +1,11 @@
1
+ import logger from "../logger/index.js";
1
2
  import { peekRunSettings } from "../config/run-settings.js";
2
3
  import { BiDiError } from "../transport/bidi-error.js";
3
4
  import { UnsupportedOperationError } from "../transport/unsupported.js";
4
- import { WaitTimeoutError } from "../wait/wait-until.js";
5
- import { LoadStateTimeoutError, NavigationError } from "./navigation-error.js";
5
+ import { matchUrl } from "../network/url-match.js";
6
+ import { waitUntil, WaitTimeoutError } from "../wait/wait-until.js";
7
+ import { LoadStateTimeoutError, NavigationError, UrlTimeoutError, } from "./navigation-error.js";
8
+ import { writeContent } from "./page-content.js";
6
9
  /** A URL starting with "/", "./" or "../" */
7
10
  const RELATIVE_URL = /^\.{0,2}\//;
8
11
  /** Resolves a slash-led `url` against `base`; any other URL is returned as given */
@@ -88,6 +91,53 @@ export class PageNavigation {
88
91
  if (state === "networkidle")
89
92
  await this.deps.waitForNetworkIdle({ timeout });
90
93
  }
94
+ /**
95
+ * Waits until the page's URL matches `url` (a string equals it, or with `*` / `**` is a glob; a RegExp is tested on it;
96
+ * a function gets it as a `URL`), then waits for `waitUntil` (default "load"). A string starting with "/", "./" or
97
+ * "../" resolves against the base URL, as in `goto`. `timeout` applies to the URL, then again to the load state.
98
+ */
99
+ async waitForURL(url, options = {}) {
100
+ this.deps.assertOpen();
101
+ const timeout = await this.deps.config.navigationTimeout(options.timeout);
102
+ const pattern = typeof url === "string" ? this.resolveURL(url, "waitForURL") : url;
103
+ const matches = typeof pattern === "function"
104
+ ? (current) => pattern(new URL(current))
105
+ : (current) => matchUrl(pattern, current);
106
+ // A read that fails while the page navigates counts as no URL yet
107
+ const read = () => this.deps.url().catch((err) => {
108
+ this.deps.assertOpen();
109
+ logger.debug("waitForURL could not read the URL", { err });
110
+ return undefined;
111
+ });
112
+ try {
113
+ await waitUntil(read, (current) => current !== undefined && matches(current), { timeout });
114
+ }
115
+ catch (err) {
116
+ if (!(err instanceof WaitTimeoutError))
117
+ throw err;
118
+ const wanted = typeof pattern === "function"
119
+ ? "the predicate"
120
+ : typeof pattern === "string"
121
+ ? `"${pattern}"`
122
+ : String(pattern);
123
+ throw new UrlTimeoutError(wanted, timeout, err.last);
124
+ }
125
+ await this.waitForLoadState(options.waitUntil, { timeout });
126
+ }
127
+ /**
128
+ * Replaces the document with `html` (see `writeContent`) and waits for `waitUntil` (default "load"); "networkidle" is
129
+ * "load" plus a quiet network. The page keeps its URL.
130
+ */
131
+ async setContent(html, options = {}) {
132
+ this.deps.assertOpen();
133
+ const timeout = await this.deps.config.navigationTimeout(options.timeout);
134
+ const state = options.waitUntil ?? "load";
135
+ const reached = await writeContent(this.connector, this.contextId, html, state === "domcontentloaded" ? "domcontentloaded" : "load", timeout);
136
+ if (!reached)
137
+ throw new LoadStateTimeoutError(state, timeout, "setContent");
138
+ if (state === "networkidle")
139
+ await this.deps.waitForNetworkIdle({ timeout });
140
+ }
91
141
  /**
92
142
  * Runs a navigation command and settles what waiting for it means: failures become NavigationErrors, and a
93
143
  * navigation that did not wait for the load is watched so `waitForLoadState` can tell it from the old page.
@@ -0,0 +1,6 @@
1
+ import type { BiDiConnector } from "../transport/bidi-connection.js";
2
+ import type { BrowserContext } from "./browser-context.js";
3
+ /** Grants a browser permission to the origin of the page (`url`), or to `options.origin` */
4
+ export declare function grantPermission(biDiConnector: BiDiConnector, url: () => Promise<string>, context: () => BrowserContext | undefined, name: string, options?: {
5
+ origin?: string;
6
+ }): Promise<void>;
@@ -0,0 +1,15 @@
1
+ /** Grants a browser permission to the origin of the page (`url`), or to `options.origin` */
2
+ export async function grantPermission(biDiConnector, url, context, name, options = {}) {
3
+ const origin = options.origin ?? new URL(await url()).origin;
4
+ // about:blank and data: pages have no origin to grant to
5
+ if (origin === "null")
6
+ throw new Error("grantPermission(): needs a page on a web origin, or an explicit origin option");
7
+ const browserContext = context();
8
+ await biDiConnector.send("permissions.setPermission", {
9
+ descriptor: { name },
10
+ state: "granted",
11
+ origin,
12
+ ...(browserContext &&
13
+ !browserContext.isDefault && { userContext: browserContext.id }),
14
+ });
15
+ }
@@ -25,7 +25,10 @@ export declare class PageConfig {
25
25
  height: number;
26
26
  } | null;
27
27
  headless?: boolean;
28
+ strictSelectors?: boolean;
28
29
  }>;
30
+ /** Whether locators throw when they match several elements: the given override, else config `use.strictSelectors`, else no */
31
+ strictSelectors(override?: boolean): Promise<boolean>;
29
32
  /** The time (ms) a navigation or a wait may take: the call's, else config `navigation.timeout`, else 30 s */
30
33
  navigationTimeout(override?: number): Promise<number>;
31
34
  /** What a navigation waits for: the call's, else config `navigation.waitUntil`, else the load event */
@@ -31,6 +31,10 @@ export class PageConfig {
31
31
  ? Promise.resolve(this.given.use)
32
32
  : readSection("use");
33
33
  }
34
+ /** Whether locators throw when they match several elements: the given override, else config `use.strictSelectors`, else no */
35
+ async strictSelectors(override) {
36
+ return override ?? (await this.use()).strictSelectors ?? false;
37
+ }
34
38
  /** The time (ms) a navigation or a wait may take: the call's, else config `navigation.timeout`, else 30 s */
35
39
  async navigationTimeout(override) {
36
40
  return (override ??
@@ -1,5 +1,7 @@
1
1
  import Locator from "../locator/locator.js";
2
- import type { TextOptions } from "../locator/selector.js";
2
+ import type { FilterOptions } from "../locator/filter-step.js";
3
+ import type { RoleOptions } from "../locator/role-locator.js";
4
+ import type { ExactOption, TextOptions } from "../locator/selector.js";
3
5
  import type { NetworkRequest } from "../network/network-tracker.js";
4
6
  import type { Recorder, RecorderOptions } from "../recorder/recorder.js";
5
7
  import { ContextTree } from "./context-tree.js";
@@ -21,8 +23,9 @@ export type { PageEventName, WaitableEventName, WaitForEventOptions, WaitOptions
21
23
  import { type PageSettings, type ReadinessState } from "./page-settings.js";
22
24
  export type { PageSettings, ReadinessState };
23
25
  import { type Viewport } from "./viewport.js";
24
- import { type PreloadHandle } from "../script/preload.js";
26
+ import type { PreloadHandle } from "../script/preload.js";
25
27
  import type { BiDiConnector } from "../transport/bidi-connection.js";
28
+ import { type PageFunction } from "../script/call-function.js";
26
29
  import type { BrowsingContext } from "../types/bidi-modules/browsing-context.js";
27
30
  export type { LoadState, NavigateOptions, NavigationResult };
28
31
  export default class Page {
@@ -32,6 +35,7 @@ export default class Page {
32
35
  private helperRealm;
33
36
  private initScripts;
34
37
  private pageConfig;
38
+ private locators;
35
39
  private pageEvents;
36
40
  private documents;
37
41
  private nav;
@@ -43,27 +47,42 @@ export default class Page {
43
47
  private browserContext;
44
48
  private transfers;
45
49
  private evidence;
50
+ private knownViewport;
46
51
  /** `settings` override what `samurai.config.ts` says; without them the config is read when needed */
47
52
  constructor(biDiConnector: BiDiConnector, contextId: string, settings?: PageSettings);
48
53
  /** The BiDi browsing context id of this page's top-level context */
49
54
  get contextId(): BrowsingContext;
50
55
  /** The context tree, created on first use and shared by everything that needs to attribute frame events to this page */
51
56
  tree(): Promise<ContextTree>;
52
- /** Locates an element on the page and returns an interactable object */
53
- locator(xpath: string): Locator;
54
- private locate;
57
+ /**
58
+ * Locates elements on the page. An xpath (`//x`, `/html/body`, `(//x)[2]`, `./x`, `xpath=…`, or an older
59
+ * relative one such as `a[@id='x']`) or a css selector (`input[name='x']`, `a, button`, `css=…`).
60
+ * `options` filter the matches, as `locator.filter()` does
61
+ */
62
+ locator(selector: string, options?: FilterOptions<Locator>): Locator;
63
+ /**
64
+ * Makes this page's locators strict (or not): an action or read on a locator matching several elements then
65
+ * throws `name(): strict mode violation, N elements match ...` instead of using the first. `undefined` goes back
66
+ * to config `use.strictSelectors` (off unless set). Applies to locators made afterwards and to earlier ones that have not
67
+ * run an action or read yet: a locator keeps the answer it got at its first one
68
+ */
69
+ setStrictSelectors(enabled: boolean | undefined): void;
55
70
  /** Locates elements with a CSS selector */
56
71
  getByCss(css: string): Locator;
57
- /** Locates elements by their text: an exact match unless `match: "partial"` */
58
- getByText(text: string, options?: TextOptions): Locator;
59
- /** Locates form controls by their label text: an exact match unless `match: "partial"` */
60
- getByLabel(text: string, options?: TextOptions): Locator;
72
+ /** Locates elements by their text: an exact match unless `match: "partial"` or `exact: false`; a RegExp is tested as written (see `TextOptions`) */
73
+ getByText(text: string | RegExp, options?: TextOptions): Locator;
74
+ /** Locates form controls by their label text: an exact match unless `match: "partial"` or `exact: false`; a RegExp is tested as written */
75
+ getByLabel(text: string | RegExp, options?: TextOptions): Locator;
76
+ /** Locates elements by their `placeholder` (see `TextOptions` for `exact` and RegExp) */
77
+ getByPlaceholder(text: string | RegExp, options?: ExactOption): Locator;
78
+ /** Locates elements by their `alt` text (images, areas, image inputs) */
79
+ getByAltText(text: string | RegExp, options?: ExactOption): Locator;
80
+ /** Locates elements by their `title` attribute */
81
+ getByTitle(text: string | RegExp, options?: ExactOption): Locator;
61
82
  /** Locates elements by their `data-testid` attribute */
62
83
  getByTestId(testId: string): Locator;
63
- /** Locates elements by ARIA role and, optionally, accessible name */
64
- getByRole(role: string, options?: {
65
- name?: string;
66
- }): Locator;
84
+ /** Locates elements by ARIA role and, optionally, accessible name and state (see `RoleOptions`) */
85
+ getByRole(role: string, options?: RoleOptions): Locator;
67
86
  /**
68
87
  * Runs `script` before any page script in every document this page loads from now on, and once in
69
88
  * the document that is loaded now. It runs in the page's own realm, so the page sees what it sets.
@@ -117,14 +136,58 @@ export default class Page {
117
136
  goForward(): Promise<boolean>;
118
137
  /** The URL of the page now */
119
138
  url(): Promise<string>;
139
+ /**
140
+ * Waits until the URL matches `url` (a string equals it, or with `*` / `**` is a glob; a RegExp is tested on it;
141
+ * a function gets it as a `URL`), then until the page reached `waitUntil` (default "load"). Also sees `pushState`.
142
+ * @example
143
+ * await page.getByRole("button", { name: "Sign in" }).click();
144
+ * await page.waitForURL("**\/dashboard");
145
+ */
146
+ waitForURL(url: UrlPattern | ((url: URL) => boolean), options?: {
147
+ timeout?: number;
148
+ waitUntil?: LoadState;
149
+ }): Promise<void>;
150
+ /** The markup of the document now, doctype included */
151
+ content(): Promise<string>;
152
+ /**
153
+ * Replaces the document with `html` and waits for `waitUntil` (default "load"). Scripts and styles in it run and apply;
154
+ * the page keeps its URL and origin (it is not a `data:` page).
155
+ * @example
156
+ * await page.setContent("<button>Go</button><script>document.title = 'x'</script>");
157
+ */
158
+ setContent(html: string, options?: {
159
+ timeout?: number;
160
+ waitUntil?: LoadState;
161
+ }): Promise<void>;
120
162
  /** The title of the document now */
121
163
  title(): Promise<string>;
164
+ /**
165
+ * Runs `fn` in the page, with `arg` as its only argument, and returns the result (a promise is awaited). `fn` is a function,
166
+ * the source of one (`"async () => ..."`), or any other string, which is an expression (`"document.title"`; it takes no argument).
167
+ * Values come back as plain JavaScript: Date, Map, Set, RegExp, BigInt, NaN, undefined, nested objects and arrays;
168
+ * an element as an `ElementHandle`; what cannot be copied (a function, window, error, promise) as a `RemoteObject` with its `type`.
169
+ * The page's own error message is kept (`ScriptError`). A function written under tsx must not contain named inner functions
170
+ * (the transpiler adds a `__name` helper the page lacks); a string always works.
171
+ * @example
172
+ * const count = await page.evaluate((min) => document.querySelectorAll("li").length > min, 3);
173
+ */
174
+ evaluate<R = unknown, A = unknown>(fn: PageFunction<[A], R>, arg?: A): Promise<R>;
122
175
  /**
123
176
  * Sets the size of the viewport (and, optionally, the device pixel ratio); `null` puts the browser's own size back.
124
177
  * @example
125
178
  * await page.setViewport({ width: 390, height: 844, devicePixelRatio: 3 });
126
179
  */
127
180
  setViewport(viewport: Viewport | null): Promise<void>;
181
+ /** Playwright's name for `setViewport` */
182
+ setViewportSize(size: Viewport): Promise<void>;
183
+ /**
184
+ * The size this page was last given (`setViewport`, `setViewportSize`, or the default from config `use.viewport` when the
185
+ * browser opened it), read at once; null before one is known, after `setViewport(null)` and when config turns the viewport off.
186
+ */
187
+ viewportSize(): {
188
+ width: number;
189
+ height: number;
190
+ } | null;
128
191
  /** Gives the page the size from config `use.viewport` (1280x720 by default); the browser does this when it opens a page */
129
192
  applyDefaultViewport(): Promise<void>;
130
193
  /**