@itmetsam/samurai-framework 0.21.3 → 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 (44) hide show
  1. package/dist/assert/asymmetric.d.ts +29 -0
  2. package/dist/assert/asymmetric.js +166 -0
  3. package/dist/assert/element-probes.d.ts +20 -0
  4. package/dist/assert/element-probes.js +45 -0
  5. package/dist/assert/expect.d.ts +20 -9
  6. package/dist/assert/expect.js +12 -4
  7. package/dist/assert/locator-assertion-base.d.ts +52 -0
  8. package/dist/assert/locator-assertion-base.js +78 -0
  9. package/dist/assert/locator-assertions.d.ts +26 -49
  10. package/dist/assert/locator-assertions.js +35 -78
  11. package/dist/assert/more-locator-assertions.d.ts +30 -0
  12. package/dist/assert/more-locator-assertions.js +86 -0
  13. package/dist/assert/retry-assertions.d.ts +28 -0
  14. package/dist/assert/retry-assertions.js +113 -0
  15. package/dist/assert/text-match.d.ts +2 -0
  16. package/dist/assert/text-match.js +8 -0
  17. package/dist/assert/value-assertions.d.ts +29 -1
  18. package/dist/assert/value-assertions.js +84 -5
  19. package/dist/assert/value-matchers.d.ts +19 -0
  20. package/dist/assert/value-matchers.js +53 -0
  21. package/dist/browser/navigation-error.d.ts +5 -1
  22. package/dist/browser/navigation-error.js +10 -2
  23. package/dist/browser/page-content.d.ts +5 -0
  24. package/dist/browser/page-content.js +43 -0
  25. package/dist/browser/page-evaluate.d.ts +9 -0
  26. package/dist/browser/page-evaluate.js +20 -0
  27. package/dist/browser/page-locators.d.ts +1 -1
  28. package/dist/browser/page-locators.js +1 -1
  29. package/dist/browser/page-navigation.d.ts +18 -0
  30. package/dist/browser/page-navigation.js +52 -2
  31. package/dist/browser/page.d.ts +48 -1
  32. package/dist/browser/page.js +58 -1
  33. package/dist/locator/element-access.d.ts +2 -0
  34. package/dist/locator/element-reads.d.ts +17 -0
  35. package/dist/locator/element-reads.js +31 -0
  36. package/dist/locator/evaluate.d.ts +12 -0
  37. package/dist/locator/evaluate.js +36 -0
  38. package/dist/locator/locator.d.ts +23 -0
  39. package/dist/locator/locator.js +36 -1
  40. package/dist/script/call-function.d.ts +7 -0
  41. package/dist/script/call-function.js +21 -3
  42. package/dist/wait/wait-until.d.ts +3 -1
  43. package/dist/wait/wait-until.js +7 -2
  44. 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
+ }
@@ -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
+ }
@@ -13,6 +13,6 @@ export declare class PageLocators {
13
13
  private strictOverride;
14
14
  constructor(biDiConnector: BiDiConnector, id: BrowsingContext, helperRealm: HelperRealm, pageConfig: PageConfig, assertOpen: () => void);
15
15
  locate(selector: ConstructorParameters<typeof Locator>[0]): Locator;
16
- /** `undefined` goes back to config `use.strictSelectors`; applies to locators already made, too */
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
17
  setStrictSelectors(enabled: boolean | undefined): void;
18
18
  }
@@ -12,7 +12,7 @@ export class PageLocators {
12
12
  this.assertOpen();
13
13
  return new Locator(selector, this.biDiConnector, this.id, this.helperRealm, [], () => this.pageConfig.strictSelectors(this.strictOverride));
14
14
  }
15
- /** `undefined` goes back to config `use.strictSelectors`; applies to locators already made, too */
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
16
  setStrictSelectors(enabled) {
17
17
  this.strictOverride = enabled;
18
18
  }
@@ -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.
@@ -25,6 +25,7 @@ export type { PageSettings, ReadinessState };
25
25
  import { type Viewport } from "./viewport.js";
26
26
  import type { PreloadHandle } from "../script/preload.js";
27
27
  import type { BiDiConnector } from "../transport/bidi-connection.js";
28
+ import { type PageFunction } from "../script/call-function.js";
28
29
  import type { BrowsingContext } from "../types/bidi-modules/browsing-context.js";
29
30
  export type { LoadState, NavigateOptions, NavigationResult };
30
31
  export default class Page {
@@ -46,6 +47,7 @@ export default class Page {
46
47
  private browserContext;
47
48
  private transfers;
48
49
  private evidence;
50
+ private knownViewport;
49
51
  /** `settings` override what `samurai.config.ts` says; without them the config is read when needed */
50
52
  constructor(biDiConnector: BiDiConnector, contextId: string, settings?: PageSettings);
51
53
  /** The BiDi browsing context id of this page's top-level context */
@@ -61,7 +63,8 @@ export default class Page {
61
63
  /**
62
64
  * Makes this page's locators strict (or not): an action or read on a locator matching several elements then
63
65
  * throws `name(): strict mode violation, N elements match ...` instead of using the first. `undefined` goes back
64
- * to config `use.strictSelectors` (off unless set). Applies to locators already made, too
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
65
68
  */
66
69
  setStrictSelectors(enabled: boolean | undefined): void;
67
70
  /** Locates elements with a CSS selector */
@@ -133,14 +136,58 @@ export default class Page {
133
136
  goForward(): Promise<boolean>;
134
137
  /** The URL of the page now */
135
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>;
136
162
  /** The title of the document now */
137
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>;
138
175
  /**
139
176
  * Sets the size of the viewport (and, optionally, the device pixel ratio); `null` puts the browser's own size back.
140
177
  * @example
141
178
  * await page.setViewport({ width: 390, height: 844, devicePixelRatio: 3 });
142
179
  */
143
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;
144
191
  /** Gives the page the size from config `use.viewport` (1280x720 by default); the browser does this when it opens a page */
145
192
  applyDefaultViewport(): Promise<void>;
146
193
  /**
@@ -28,6 +28,8 @@ import { applyViewport } from "./viewport.js";
28
28
  import { HelperRealm } from "../locator/helper-realm.js";
29
29
  import { BiDiError } from "../transport/bidi-error.js";
30
30
  import { callFunction } from "../script/call-function.js";
31
+ import { readContent } from "./page-content.js";
32
+ import { evaluateInPage } from "./page-evaluate.js";
31
33
  export default class Page {
32
34
  /** `settings` override what `samurai.config.ts` says; without them the config is read when needed */
33
35
  constructor(biDiConnector, contextId, settings) {
@@ -37,6 +39,7 @@ export default class Page {
37
39
  this.lazyLogs = new Lazy();
38
40
  this.errorsAllowed = false;
39
41
  this.isClosed = false;
42
+ this.knownViewport = null;
40
43
  this.biDiConnector = biDiConnector;
41
44
  this.id = contextId;
42
45
  this.pageConfig = new PageConfig(settings);
@@ -84,7 +87,8 @@ export default class Page {
84
87
  /**
85
88
  * Makes this page's locators strict (or not): an action or read on a locator matching several elements then
86
89
  * throws `name(): strict mode violation, N elements match ...` instead of using the first. `undefined` goes back
87
- * to config `use.strictSelectors` (off unless set). Applies to locators already made, too
90
+ * to config `use.strictSelectors` (off unless set). Applies to locators made afterwards and to earlier ones that have not
91
+ * run an action or read yet: a locator keeps the answer it got at its first one
88
92
  */
89
93
  setStrictSelectors(enabled) {
90
94
  this.locators.setStrictSelectors(enabled);
@@ -220,11 +224,49 @@ export default class Page {
220
224
  this.assertOpen();
221
225
  return callFunction(this.biDiConnector, this.id, "() => location.href", [], { awaitPromise: false });
222
226
  }
227
+ /**
228
+ * Waits until the URL matches `url` (a string equals it, or with `*` / `**` is a glob; a RegExp is tested on it;
229
+ * a function gets it as a `URL`), then until the page reached `waitUntil` (default "load"). Also sees `pushState`.
230
+ * @example
231
+ * await page.getByRole("button", { name: "Sign in" }).click();
232
+ * await page.waitForURL("**\/dashboard");
233
+ */
234
+ waitForURL(url, options) {
235
+ return this.nav.waitForURL(url, options);
236
+ }
237
+ /** The markup of the document now, doctype included */
238
+ async content() {
239
+ this.assertOpen();
240
+ return readContent(this.biDiConnector, this.id);
241
+ }
242
+ /**
243
+ * Replaces the document with `html` and waits for `waitUntil` (default "load"). Scripts and styles in it run and apply;
244
+ * the page keeps its URL and origin (it is not a `data:` page).
245
+ * @example
246
+ * await page.setContent("<button>Go</button><script>document.title = 'x'</script>");
247
+ */
248
+ async setContent(html, options) {
249
+ return this.nav.setContent(html, options);
250
+ }
223
251
  /** The title of the document now */
224
252
  async title() {
225
253
  this.assertOpen();
226
254
  return callFunction(this.biDiConnector, this.id, "() => document.title", [], { awaitPromise: false });
227
255
  }
256
+ /**
257
+ * Runs `fn` in the page, with `arg` as its only argument, and returns the result (a promise is awaited). `fn` is a function,
258
+ * the source of one (`"async () => ..."`), or any other string, which is an expression (`"document.title"`; it takes no argument).
259
+ * Values come back as plain JavaScript: Date, Map, Set, RegExp, BigInt, NaN, undefined, nested objects and arrays;
260
+ * an element as an `ElementHandle`; what cannot be copied (a function, window, error, promise) as a `RemoteObject` with its `type`.
261
+ * The page's own error message is kept (`ScriptError`). A function written under tsx must not contain named inner functions
262
+ * (the transpiler adds a `__name` helper the page lacks); a string always works.
263
+ * @example
264
+ * const count = await page.evaluate((min) => document.querySelectorAll("li").length > min, 3);
265
+ */
266
+ async evaluate(fn, arg) {
267
+ this.assertOpen();
268
+ return evaluateInPage(this.biDiConnector, this.id, fn, arg);
269
+ }
228
270
  /**
229
271
  * Sets the size of the viewport (and, optionally, the device pixel ratio); `null` puts the browser's own size back.
230
272
  * @example
@@ -233,6 +275,21 @@ export default class Page {
233
275
  async setViewport(viewport) {
234
276
  this.assertOpen();
235
277
  await applyViewport(this.biDiConnector, this.id, viewport);
278
+ this.knownViewport = viewport && {
279
+ width: viewport.width,
280
+ height: viewport.height,
281
+ };
282
+ }
283
+ /** Playwright's name for `setViewport` */
284
+ setViewportSize(size) {
285
+ return this.setViewport(size);
286
+ }
287
+ /**
288
+ * The size this page was last given (`setViewport`, `setViewportSize`, or the default from config `use.viewport` when the
289
+ * browser opened it), read at once; null before one is known, after `setViewport(null)` and when config turns the viewport off.
290
+ */
291
+ viewportSize() {
292
+ return this.knownViewport && { ...this.knownViewport };
236
293
  }
237
294
  /** Gives the page the size from config `use.viewport` (1280x720 by default); the browser does this when it opens a page */
238
295
  async applyDefaultViewport() {
@@ -8,6 +8,8 @@ export interface ElementAccess extends ElementContext {
8
8
  * throw instead (`method` is the public method that asked, for the message)
9
9
  */
10
10
  resolveOne(method?: string): Promise<ElementHandle | null>;
11
+ /** Every element the locator matches now, in document order; never strict, never waits */
12
+ resolveAll(): Promise<ElementHandle[]>;
11
13
  }
12
14
  /** Calls `fn` in the page; a throwing function becomes a "Failed to locate element" error */
13
15
  export declare function callInPage<T>(access: Pick<ElementAccess, "connector" | "contextId" | "selector">, fn: string, args: Arg[], sandbox?: string): Promise<T>;
@@ -3,6 +3,23 @@ import { type ActionabilityContext } from "./actionability.js";
3
3
  import { type ElementAccess } from "./element-access.js";
4
4
  /** Trimmed text content, or null when the element is missing */
5
5
  export declare function readTextContent(access: ElementAccess): Promise<string | null>;
6
+ /** Rendered text (what a person would copy: line breaks, no hidden text), untrimmed, or null when the element is missing */
7
+ export declare function readInnerText(access: ElementAccess): Promise<string | null>;
8
+ /** The markup inside the element, or null when the element is missing */
9
+ export declare function readInnerHTML(access: ElementAccess): Promise<string | null>;
10
+ /** `textContent` of every match, as the page has it (untrimmed); an empty array when none match */
11
+ export declare function readAllTextContents(access: ElementAccess): Promise<string[]>;
12
+ /** `innerText` of every match; an empty array when none match */
13
+ export declare function readAllInnerTexts(access: ElementAccess): Promise<string[]>;
14
+ /** The element's box in CSS pixels from the viewport's top left */
15
+ export interface BoundingBox {
16
+ x: number;
17
+ y: number;
18
+ width: number;
19
+ height: number;
20
+ }
21
+ /** The element's box, or null when it is missing or not rendered (`display: none`, detached): it has no boxes then */
22
+ export declare function readBoundingBox(access: ElementAccess): Promise<BoundingBox | null>;
6
23
  /** The accessible name (see `ACCESSIBLE_NAME_JS`), "" when it has none, or null when the element is missing */
7
24
  export declare function readAccessibleName(access: ElementAccess): Promise<string | null>;
8
25
  /** The accessible description, "" when it has none, or null when the element is missing */
@@ -2,6 +2,7 @@ import { probeState } from "./actionability.js";
2
2
  import { callOnElement, callOnExistingElement, } from "./element-access.js";
3
3
  import { describeType } from "./element-state.js";
4
4
  import { ACCESSIBLE_NAME_JS } from "./accessible-name.js";
5
+ import { evaluateOnAll } from "./evaluate.js";
5
6
  /** Body run in the page as `(el) => { ... }`: a checked checkbox or radio input, or `aria-checked="true"` */
6
7
  const IS_CHECKED_BODY = `
7
8
  const type = (el.getAttribute("type") || "").toLowerCase();
@@ -31,6 +32,36 @@ function stringOrNull(value) {
31
32
  export async function readTextContent(access) {
32
33
  return stringOrNull(await callOnElement(access, null, `return el.textContent.trim();`, [], [], "textContent"));
33
34
  }
35
+ /** Rendered text (what a person would copy: line breaks, no hidden text), untrimmed, or null when the element is missing */
36
+ export async function readInnerText(access) {
37
+ return stringOrNull(await callOnElement(access, null, `return el.innerText;`, [], [], "innerText"));
38
+ }
39
+ /** The markup inside the element, or null when the element is missing */
40
+ export async function readInnerHTML(access) {
41
+ return stringOrNull(await callOnElement(access, null, `return el.innerHTML;`, [], [], "innerHTML"));
42
+ }
43
+ /** `textContent` of every match, as the page has it (untrimmed); an empty array when none match */
44
+ export function readAllTextContents(access) {
45
+ return evaluateOnAll(access, `(els) => els.map((el) => el.textContent ?? "")`, undefined);
46
+ }
47
+ /** `innerText` of every match; an empty array when none match */
48
+ export function readAllInnerTexts(access) {
49
+ return evaluateOnAll(access, `(els) => els.map((el) => el.innerText ?? "")`, undefined);
50
+ }
51
+ /** The element's box, or null when it is missing or not rendered (`display: none`, detached): it has no boxes then */
52
+ export async function readBoundingBox(access) {
53
+ const box = await callOnElement(access, null, `
54
+ if (el.getClientRects().length === 0) return null;
55
+ const { x, y, width, height } = el.getBoundingClientRect();
56
+ return { x, y, width, height };
57
+ `, [], [], "boundingBox");
58
+ if (box === null)
59
+ return null;
60
+ if (typeof box !== "object") {
61
+ throw new Error(`Expected to receive an object but instead received "${describeType(box)}"`);
62
+ }
63
+ return box;
64
+ }
34
65
  /** The accessible name (see `ACCESSIBLE_NAME_JS`), "" when it has none, or null when the element is missing */
35
66
  export async function readAccessibleName(access) {
36
67
  return stringOrNull(await callOnElement(access, null, `${ACCESSIBLE_NAME_JS} return accessibleName(el);`, [], [], "accessibleName"));
@@ -0,0 +1,12 @@
1
+ import { type PageFunction } from "../script/call-function.js";
2
+ import type { ElementAccess } from "./element-access.js";
3
+ /**
4
+ * Waits until the element is attached (hidden is fine), then calls `fn(element, arg)` in the page.
5
+ * A missing element ends in the usual timeout error; a strict locator throws when several match.
6
+ * What the page function throws reaches the caller unchanged (a `ScriptError`).
7
+ */
8
+ export declare function evaluateOnElement<R, A>(access: ElementAccess, fn: PageFunction<[any, A], R>, arg: A | undefined, options?: {
9
+ timeout?: number;
10
+ }): Promise<R>;
11
+ /** Calls `fn(elements, arg)` in the page with every element the locator matches now (an empty array when none); does not wait */
12
+ export declare function evaluateOnAll<R, A>(access: ElementAccess, fn: PageFunction<[any[], A], R>, arg: A | undefined): Promise<R>;
@@ -0,0 +1,36 @@
1
+ import { BiDiError } from "../transport/bidi-error.js";
2
+ import { callFunction } from "../script/call-function.js";
3
+ import { ATTACHED_ONLY } from "./page-scripts.js";
4
+ /**
5
+ * Waits until the element is attached (hidden is fine), then calls `fn(element, arg)` in the page.
6
+ * A missing element ends in the usual timeout error; a strict locator throws when several match.
7
+ * What the page function throws reaches the caller unchanged (a `ScriptError`).
8
+ */
9
+ export async function evaluateOnElement(access, fn, arg, options) {
10
+ await access.waitForActionable("evaluate", ATTACHED_ONLY, { scroll: false, hitTest: false }, options?.timeout === undefined ? undefined : { timeout: options.timeout });
11
+ const el = await access.requireElement();
12
+ try {
13
+ return await callFunction(access.connector, access.contextId, fn, arg === undefined ? [el] : [el, arg]);
14
+ }
15
+ catch (err) {
16
+ // The element went away between the search and the call
17
+ if (err instanceof BiDiError && err.code === "no such node")
18
+ throw access.noElementError();
19
+ throw err;
20
+ }
21
+ }
22
+ /** Calls `fn(elements, arg)` in the page with every element the locator matches now (an empty array when none); does not wait */
23
+ export async function evaluateOnAll(access, fn, arg) {
24
+ // One more search when an element was removed between the search and the call
25
+ for (let attempt = 1;; attempt++) {
26
+ const elements = await access.resolveAll();
27
+ try {
28
+ return await callFunction(access.connector, access.contextId, fn, arg === undefined ? [elements] : [elements, arg]);
29
+ }
30
+ catch (err) {
31
+ if (attempt === 2 ||
32
+ !(err instanceof BiDiError && err.code === "no such node"))
33
+ throw err;
34
+ }
35
+ }
36
+ }
@@ -1,10 +1,12 @@
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 { PageFunction } from "../script/call-function.js";
4
5
  import type { ElementScreenshotOptions } from "../browser/screenshot.js";
5
6
  import type { FileInput } from "../browser/generated-files.js";
6
7
  import { type ActionOptions } from "./actionability.js";
7
8
  import { type CheckOptions } from "./check-actions.js";
9
+ import { type BoundingBox } from "./element-reads.js";
8
10
  import type { WaitForState } from "./element-state.js";
9
11
  import type { HelperInstaller } from "./helper-realm.js";
10
12
  import { type StrictSource } from "./strict-mode.js";
@@ -197,6 +199,27 @@ export default class Locator {
197
199
  isHidden(): Promise<boolean>;
198
200
  /** Trimmed text content, or null when the element is missing */
199
201
  textContent(): Promise<string | null>;
202
+ /** Rendered text of the element (as a person would copy it, hidden text left out), untrimmed, or null when the element is missing */
203
+ innerText(): Promise<string | null>;
204
+ /** The markup inside the element, or null when the element is missing */
205
+ innerHTML(): Promise<string | null>;
206
+ /** `textContent` of every match, as the page has it (untrimmed, unlike `textContent()`); `[]` when none match. Does not wait */
207
+ allTextContents(): Promise<string[]>;
208
+ /** `innerText` of every match; `[]` when none match. Does not wait */
209
+ allInnerTexts(): Promise<string[]>;
210
+ /** 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 */
211
+ boundingBox(): Promise<BoundingBox | null>;
212
+ /**
213
+ * Waits until the element is attached (hidden is fine), then calls `fn(element, arg)` in the page and returns the result
214
+ * (see `page.evaluate` for what comes back). `fn` is a function or the source of one; a strict locator throws when several match.
215
+ * @example
216
+ * const id = await page.getByTestId("row").evaluate((el) => el.dataset.id);
217
+ */
218
+ evaluate<R = unknown, A = unknown>(fn: PageFunction<[any, A], R>, arg?: A, options?: {
219
+ timeout?: number;
220
+ }): Promise<R>;
221
+ /** Calls `fn(elements, arg)` in the page with an array of every match (empty when none; does not wait) and returns the result */
222
+ evaluateAll<R = unknown, A = unknown>(fn: PageFunction<[any[], A], R>, arg?: A): Promise<R>;
200
223
  /** Value of an input/textarea/select, or null when missing */
201
224
  inputValue(): Promise<string | null>;
202
225
  /** Attribute value, or null when the element or attribute is missing */