@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.
- package/dist/assert/asymmetric.d.ts +29 -0
- package/dist/assert/asymmetric.js +166 -0
- package/dist/assert/element-probes.d.ts +20 -0
- package/dist/assert/element-probes.js +45 -0
- package/dist/assert/expect.d.ts +20 -9
- package/dist/assert/expect.js +12 -4
- package/dist/assert/locator-assertion-base.d.ts +52 -0
- package/dist/assert/locator-assertion-base.js +78 -0
- package/dist/assert/locator-assertions.d.ts +26 -49
- package/dist/assert/locator-assertions.js +35 -78
- package/dist/assert/more-locator-assertions.d.ts +30 -0
- package/dist/assert/more-locator-assertions.js +86 -0
- package/dist/assert/retry-assertions.d.ts +28 -0
- package/dist/assert/retry-assertions.js +113 -0
- package/dist/assert/text-match.d.ts +2 -0
- package/dist/assert/text-match.js +8 -0
- package/dist/assert/value-assertions.d.ts +29 -1
- package/dist/assert/value-assertions.js +84 -5
- package/dist/assert/value-matchers.d.ts +19 -0
- package/dist/assert/value-matchers.js +53 -0
- package/dist/browser/navigation-error.d.ts +5 -1
- package/dist/browser/navigation-error.js +10 -2
- package/dist/browser/page-content.d.ts +5 -0
- package/dist/browser/page-content.js +43 -0
- package/dist/browser/page-evaluate.d.ts +9 -0
- package/dist/browser/page-evaluate.js +20 -0
- package/dist/browser/page-locators.d.ts +1 -1
- package/dist/browser/page-locators.js +1 -1
- package/dist/browser/page-navigation.d.ts +18 -0
- package/dist/browser/page-navigation.js +52 -2
- package/dist/browser/page.d.ts +48 -1
- package/dist/browser/page.js +58 -1
- package/dist/locator/element-access.d.ts +2 -0
- package/dist/locator/element-reads.d.ts +17 -0
- package/dist/locator/element-reads.js +31 -0
- package/dist/locator/evaluate.d.ts +12 -0
- package/dist/locator/evaluate.js +36 -0
- package/dist/locator/locator.d.ts +23 -0
- package/dist/locator/locator.js +36 -1
- package/dist/script/call-function.d.ts +7 -0
- package/dist/script/call-function.js +21 -3
- package/dist/wait/wait-until.d.ts +3 -1
- package/dist/wait/wait-until.js +7 -2
- 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 =
|
|
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`;
|
|
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`;
|
|
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 {
|
|
5
|
-
import {
|
|
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.
|
package/dist/browser/page.d.ts
CHANGED
|
@@ -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
|
|
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
|
/**
|
package/dist/browser/page.js
CHANGED
|
@@ -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
|
|
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 */
|