@itmetsam/samurai-framework 0.22.10 → 0.22.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/page-actions.d.ts +151 -0
- package/dist/browser/page-actions.js +197 -0
- package/dist/browser/page-core.d.ts +80 -0
- package/dist/browser/page-core.js +150 -0
- package/dist/browser/page-locating.d.ts +37 -0
- package/dist/browser/page-locating.js +58 -0
- package/dist/browser/page-observing.d.ts +120 -0
- package/dist/browser/page-observing.js +194 -0
- package/dist/browser/page.d.ts +7 -358
- package/dist/browser/page.js +2 -582
- package/dist/locator/locator-element-methods.d.ts +169 -0
- package/dist/locator/locator-element-methods.js +228 -0
- package/dist/locator/locator.d.ts +7 -161
- package/dist/locator/locator.js +4 -223
- package/package.json +1 -1
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { type PdfOptions } from "./pdf.js";
|
|
2
|
+
import type { RuntimeUseOptions } from "../types/use.js";
|
|
3
|
+
import { type ScreenshotOptions } from "./screenshot.js";
|
|
4
|
+
import type { UrlPattern } from "../network/url-match.js";
|
|
5
|
+
import type { LoadState, NavigateOptions, NavigationResult } from "./page-navigation.js";
|
|
6
|
+
import { type ReadinessState } from "./page-settings.js";
|
|
7
|
+
import type { Viewport } from "./viewport.js";
|
|
8
|
+
import type { PreloadHandle } from "../script/preload.js";
|
|
9
|
+
import type { PageFunction } from "../script/call-function.js";
|
|
10
|
+
import { PageCore } from "./page-core.js";
|
|
11
|
+
/** @internal Navigation, content, viewport and the other things a page does */
|
|
12
|
+
export declare abstract class PageActions extends PageCore {
|
|
13
|
+
/**
|
|
14
|
+
* Runs `script` before any page script in every document this page loads from now on, and once in
|
|
15
|
+
* the document that is loaded now. It runs in the page's own realm, so the page sees what it sets.
|
|
16
|
+
* A function receives `arg` (which must be JSON-serializable) as its only argument;
|
|
17
|
+
* a string is script source (statements) and takes no argument.
|
|
18
|
+
* @example
|
|
19
|
+
* await page.addInitScript(() => { window.__testMode = true; });
|
|
20
|
+
* await page.addInitScript((flags) => { window.flags = flags; }, { beta: true });
|
|
21
|
+
*/
|
|
22
|
+
addInitScript(script: string | ((arg?: any) => void), arg?: unknown): Promise<PreloadHandle>;
|
|
23
|
+
/** Sets the base that relative `goto` URLs resolve against; `undefined` falls back to the run's baseURL */
|
|
24
|
+
setBaseURL(url: string | undefined): void;
|
|
25
|
+
/** @internal The full URL `goto` would navigate to: a URL starting with "/", "./" or "../" resolves against the base URL, others stay as given. `method` names the caller in the error */
|
|
26
|
+
resolveURL(url: string, method?: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Navigates like `navigateTo`, but a URL starting with "/", "./" or "../" resolves against the base URL
|
|
29
|
+
* (`setBaseURL`, else the environment's `baseURL`).
|
|
30
|
+
* @example
|
|
31
|
+
* await page.goto("/products");
|
|
32
|
+
* await page.goto("https://example.com", { wait: "interactive" });
|
|
33
|
+
*/
|
|
34
|
+
goto(url: string, options?: NavigateOptions): Promise<NavigationResult>;
|
|
35
|
+
/**
|
|
36
|
+
* Navigates to `url` and resolves when the page reached `wait` (default "complete", the load event).
|
|
37
|
+
* A URL without a scheme gets `https://`. Throws NavigationError when the navigation fails or takes too long.
|
|
38
|
+
* The positional form `navigateTo(url, wait, protocol)` still works.
|
|
39
|
+
* @example
|
|
40
|
+
* await page.navigateTo("example.com");
|
|
41
|
+
* await page.navigateTo("localhost:3000", { wait: "interactive", protocol: "http" });
|
|
42
|
+
*/
|
|
43
|
+
navigateTo(url: string, options?: NavigateOptions | ReadinessState, protocol?: "http" | "https"): Promise<NavigationResult>;
|
|
44
|
+
/**
|
|
45
|
+
* Reloads the page and resolves when it reached `wait` (default like `navigateTo`). Firefox does not support
|
|
46
|
+
* `ignoreCache`; asking for it throws UnsupportedOperationError.
|
|
47
|
+
*/
|
|
48
|
+
reload(options?: {
|
|
49
|
+
wait?: ReadinessState;
|
|
50
|
+
timeout?: number;
|
|
51
|
+
ignoreCache?: boolean;
|
|
52
|
+
}): Promise<NavigationResult>;
|
|
53
|
+
/** Goes back one entry in the history. False when there is none to go back to */
|
|
54
|
+
goBack(): Promise<boolean>;
|
|
55
|
+
/** Goes forward one entry in the history. False when there is none */
|
|
56
|
+
goForward(): Promise<boolean>;
|
|
57
|
+
/** The URL of the page now */
|
|
58
|
+
url(): Promise<string>;
|
|
59
|
+
/**
|
|
60
|
+
* Waits until the URL matches `url` (a string equals it, or with `*` / `**` is a glob; a RegExp is tested on it;
|
|
61
|
+
* a function gets it as a `URL`), then until the page reached `waitUntil` (default "load"). Also sees `pushState`.
|
|
62
|
+
* @example
|
|
63
|
+
* await page.getByRole("button", { name: "Sign in" }).click();
|
|
64
|
+
* await page.waitForURL("**\/dashboard");
|
|
65
|
+
*/
|
|
66
|
+
waitForURL(url: UrlPattern | ((url: URL) => boolean), options?: {
|
|
67
|
+
timeout?: number;
|
|
68
|
+
waitUntil?: LoadState;
|
|
69
|
+
}): Promise<void>;
|
|
70
|
+
/** The markup of the document now, doctype included */
|
|
71
|
+
content(): Promise<string>;
|
|
72
|
+
/**
|
|
73
|
+
* Replaces the document with `html` and waits for `waitUntil` (default "load"). Scripts and styles in it run and apply;
|
|
74
|
+
* the page keeps its URL and origin (it is not a `data:` page).
|
|
75
|
+
* @example
|
|
76
|
+
* await page.setContent("<button>Go</button><script>document.title = 'x'</script>");
|
|
77
|
+
*/
|
|
78
|
+
setContent(html: string, options?: {
|
|
79
|
+
timeout?: number;
|
|
80
|
+
waitUntil?: LoadState;
|
|
81
|
+
}): Promise<void>;
|
|
82
|
+
/** The title of the document now */
|
|
83
|
+
title(): Promise<string>;
|
|
84
|
+
/**
|
|
85
|
+
* Runs `fn` in the page, with `arg` as its only argument, and returns the result (a promise is awaited). `fn` is a function,
|
|
86
|
+
* the source of one (`"async () => ..."`), or any other string, which is an expression (`"document.title"`; it takes no argument).
|
|
87
|
+
* Values come back as plain JavaScript: Date, Map, Set, RegExp, BigInt, NaN, undefined, nested objects and arrays;
|
|
88
|
+
* an element as an `ElementHandle`; what cannot be copied (a function, window, error, promise) as a `RemoteObject` with its `type`.
|
|
89
|
+
* The page's own error message is kept (`ScriptError`). A function written under tsx must not contain named inner functions
|
|
90
|
+
* (the transpiler adds a `__name` helper the page lacks); a string always works.
|
|
91
|
+
* @example
|
|
92
|
+
* const count = await page.evaluate((min) => document.querySelectorAll("li").length > min, 3);
|
|
93
|
+
*/
|
|
94
|
+
evaluate<R = unknown, A = unknown>(fn: PageFunction<[A], R>, arg?: A): Promise<R>;
|
|
95
|
+
/**
|
|
96
|
+
* Sets the size of the viewport (and, optionally, the device pixel ratio); `null` puts the browser's own size back.
|
|
97
|
+
* @example
|
|
98
|
+
* await page.setViewport({ width: 390, height: 844, devicePixelRatio: 3 });
|
|
99
|
+
*/
|
|
100
|
+
setViewport(viewport: Viewport | null): Promise<void>;
|
|
101
|
+
/** @internal Chrome: lays this page out like a phone from now on (`use.isMobile`), including the size it has now */
|
|
102
|
+
setMobileLayout(): Promise<void>;
|
|
103
|
+
/** Playwright's name for `setViewport` */
|
|
104
|
+
setViewportSize(size: Viewport): Promise<void>;
|
|
105
|
+
/**
|
|
106
|
+
* The size this page was last given (`setViewport`, `setViewportSize`, or the default from config `use.viewport` when the
|
|
107
|
+
* browser opened it), read at once; null before one is known, after `setViewport(null)` and when config turns the viewport off.
|
|
108
|
+
*/
|
|
109
|
+
viewportSize(): {
|
|
110
|
+
width: number;
|
|
111
|
+
height: number;
|
|
112
|
+
} | null;
|
|
113
|
+
/** Gives the page the size from config `use.viewport` (1280x720 by default); the browser does this when it opens a page */
|
|
114
|
+
applyDefaultViewport(): Promise<void>;
|
|
115
|
+
/**
|
|
116
|
+
* Takes a screenshot and returns the image (also written to `options.path`).
|
|
117
|
+
* @example
|
|
118
|
+
* await page.screenshot({ path: "out/home.png", fullPage: true });
|
|
119
|
+
*/
|
|
120
|
+
screenshot(options?: ScreenshotOptions): Promise<Buffer>;
|
|
121
|
+
/** Waits `ms` milliseconds. Prefer waiting for something (an assertion, `waitForNetworkIdle`) when you can */
|
|
122
|
+
waitForTimeout(ms: number): Promise<void>;
|
|
123
|
+
/** Renders the page as a PDF and returns it (also written to `options.path`) */
|
|
124
|
+
pdf(options?: PdfOptions): Promise<Buffer>;
|
|
125
|
+
/**
|
|
126
|
+
* Changes the `use` options that can change while the browser runs for this page only: locale, timezone, user
|
|
127
|
+
* agent, geolocation, offline, orientation, screen, hasTouch, javaScriptEnabled. It wins over what the page's
|
|
128
|
+
* context says; `null` puts the real (or the context's) value back. Same options as `test.use()`, applied now.
|
|
129
|
+
* @example
|
|
130
|
+
* await page.use({ locale: "nl-NL", offline: true });
|
|
131
|
+
*/
|
|
132
|
+
use(options: RuntimeUseOptions): Promise<void>;
|
|
133
|
+
/**
|
|
134
|
+
* Grants a browser permission ("geolocation", "notifications", "camera", …) to the origin this page is on,
|
|
135
|
+
* or to `options.origin`. A geolocation override only answers once this is granted.
|
|
136
|
+
*/
|
|
137
|
+
grantPermission(name: string, options?: {
|
|
138
|
+
origin?: string;
|
|
139
|
+
}): Promise<void>;
|
|
140
|
+
/**
|
|
141
|
+
* Waits until the page's document reached `state` (default "load"): "domcontentloaded" and "load" follow the
|
|
142
|
+
* document, "networkidle" is "load" plus a quiet network (see `waitForNetworkIdle`). Resolves at once when it
|
|
143
|
+
* is already there. Call it once the navigation has started; a document that is still the old one counts.
|
|
144
|
+
* @example
|
|
145
|
+
* await page.locator("a[@id='next']").click();
|
|
146
|
+
* await page.waitForLoadState("domcontentloaded");
|
|
147
|
+
*/
|
|
148
|
+
waitForLoadState(state?: LoadState, options?: {
|
|
149
|
+
timeout?: number;
|
|
150
|
+
}): Promise<void>;
|
|
151
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { printPdf } from "./pdf.js";
|
|
2
|
+
import { applyUse } from "./use-commands.js";
|
|
3
|
+
import { screenshotWithEvidence, } from "./screenshot.js";
|
|
4
|
+
import { traverseHistory } from "./history.js";
|
|
5
|
+
import { grantPermission } from "./page-permissions.js";
|
|
6
|
+
import {} from "./page-settings.js";
|
|
7
|
+
import { readContent, readTitle, readUrl } from "./page-content.js";
|
|
8
|
+
import { evaluateInPage } from "./page-evaluate.js";
|
|
9
|
+
import { PageCore } from "./page-core.js";
|
|
10
|
+
/** @internal Navigation, content, viewport and the other things a page does */
|
|
11
|
+
export class PageActions extends PageCore {
|
|
12
|
+
/**
|
|
13
|
+
* Runs `script` before any page script in every document this page loads from now on, and once in
|
|
14
|
+
* the document that is loaded now. It runs in the page's own realm, so the page sees what it sets.
|
|
15
|
+
* A function receives `arg` (which must be JSON-serializable) as its only argument;
|
|
16
|
+
* a string is script source (statements) and takes no argument.
|
|
17
|
+
* @example
|
|
18
|
+
* await page.addInitScript(() => { window.__testMode = true; });
|
|
19
|
+
* await page.addInitScript((flags) => { window.flags = flags; }, { beta: true });
|
|
20
|
+
*/
|
|
21
|
+
async addInitScript(script, arg) {
|
|
22
|
+
this.assertOpen();
|
|
23
|
+
return this.initScripts.add(this.biDiConnector, this.id, script, arg);
|
|
24
|
+
}
|
|
25
|
+
/** Sets the base that relative `goto` URLs resolve against; `undefined` falls back to the run's baseURL */
|
|
26
|
+
setBaseURL(url) {
|
|
27
|
+
this.nav.setBaseURL(url);
|
|
28
|
+
}
|
|
29
|
+
/** @internal The full URL `goto` would navigate to: a URL starting with "/", "./" or "../" resolves against the base URL, others stay as given. `method` names the caller in the error */
|
|
30
|
+
resolveURL(url, method) {
|
|
31
|
+
return this.nav.resolveURL(url, method);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Navigates like `navigateTo`, but a URL starting with "/", "./" or "../" resolves against the base URL
|
|
35
|
+
* (`setBaseURL`, else the environment's `baseURL`).
|
|
36
|
+
* @example
|
|
37
|
+
* await page.goto("/products");
|
|
38
|
+
* await page.goto("https://example.com", { wait: "interactive" });
|
|
39
|
+
*/
|
|
40
|
+
goto(url, options) {
|
|
41
|
+
return this.nav.goto(url, options);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Navigates to `url` and resolves when the page reached `wait` (default "complete", the load event).
|
|
45
|
+
* A URL without a scheme gets `https://`. Throws NavigationError when the navigation fails or takes too long.
|
|
46
|
+
* The positional form `navigateTo(url, wait, protocol)` still works.
|
|
47
|
+
* @example
|
|
48
|
+
* await page.navigateTo("example.com");
|
|
49
|
+
* await page.navigateTo("localhost:3000", { wait: "interactive", protocol: "http" });
|
|
50
|
+
*/
|
|
51
|
+
navigateTo(url, options, protocol) {
|
|
52
|
+
return this.nav.navigateTo(url, options, protocol);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Reloads the page and resolves when it reached `wait` (default like `navigateTo`). Firefox does not support
|
|
56
|
+
* `ignoreCache`; asking for it throws UnsupportedOperationError.
|
|
57
|
+
*/
|
|
58
|
+
reload(options = {}) {
|
|
59
|
+
return this.nav.reload(options);
|
|
60
|
+
}
|
|
61
|
+
/** Goes back one entry in the history. False when there is none to go back to */
|
|
62
|
+
async goBack() {
|
|
63
|
+
this.assertOpen();
|
|
64
|
+
return traverseHistory(this.biDiConnector, this.id, -1);
|
|
65
|
+
}
|
|
66
|
+
/** Goes forward one entry in the history. False when there is none */
|
|
67
|
+
async goForward() {
|
|
68
|
+
this.assertOpen();
|
|
69
|
+
return traverseHistory(this.biDiConnector, this.id, 1);
|
|
70
|
+
}
|
|
71
|
+
/** The URL of the page now */
|
|
72
|
+
async url() {
|
|
73
|
+
this.assertOpen();
|
|
74
|
+
return readUrl(this.biDiConnector, this.id);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Waits until the URL matches `url` (a string equals it, or with `*` / `**` is a glob; a RegExp is tested on it;
|
|
78
|
+
* a function gets it as a `URL`), then until the page reached `waitUntil` (default "load"). Also sees `pushState`.
|
|
79
|
+
* @example
|
|
80
|
+
* await page.getByRole("button", { name: "Sign in" }).click();
|
|
81
|
+
* await page.waitForURL("**\/dashboard");
|
|
82
|
+
*/
|
|
83
|
+
waitForURL(url, options) {
|
|
84
|
+
return this.nav.waitForURL(url, options);
|
|
85
|
+
}
|
|
86
|
+
/** The markup of the document now, doctype included */
|
|
87
|
+
async content() {
|
|
88
|
+
this.assertOpen();
|
|
89
|
+
return readContent(this.biDiConnector, this.id);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Replaces the document with `html` and waits for `waitUntil` (default "load"). Scripts and styles in it run and apply;
|
|
93
|
+
* the page keeps its URL and origin (it is not a `data:` page).
|
|
94
|
+
* @example
|
|
95
|
+
* await page.setContent("<button>Go</button><script>document.title = 'x'</script>");
|
|
96
|
+
*/
|
|
97
|
+
async setContent(html, options) {
|
|
98
|
+
return this.nav.setContent(html, options);
|
|
99
|
+
}
|
|
100
|
+
/** The title of the document now */
|
|
101
|
+
async title() {
|
|
102
|
+
this.assertOpen();
|
|
103
|
+
return readTitle(this.biDiConnector, this.id);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Runs `fn` in the page, with `arg` as its only argument, and returns the result (a promise is awaited). `fn` is a function,
|
|
107
|
+
* the source of one (`"async () => ..."`), or any other string, which is an expression (`"document.title"`; it takes no argument).
|
|
108
|
+
* Values come back as plain JavaScript: Date, Map, Set, RegExp, BigInt, NaN, undefined, nested objects and arrays;
|
|
109
|
+
* an element as an `ElementHandle`; what cannot be copied (a function, window, error, promise) as a `RemoteObject` with its `type`.
|
|
110
|
+
* The page's own error message is kept (`ScriptError`). A function written under tsx must not contain named inner functions
|
|
111
|
+
* (the transpiler adds a `__name` helper the page lacks); a string always works.
|
|
112
|
+
* @example
|
|
113
|
+
* const count = await page.evaluate((min) => document.querySelectorAll("li").length > min, 3);
|
|
114
|
+
*/
|
|
115
|
+
async evaluate(fn, arg) {
|
|
116
|
+
this.assertOpen();
|
|
117
|
+
return evaluateInPage(this.biDiConnector, this.id, fn, arg);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Sets the size of the viewport (and, optionally, the device pixel ratio); `null` puts the browser's own size back.
|
|
121
|
+
* @example
|
|
122
|
+
* await page.setViewport({ width: 390, height: 844, devicePixelRatio: 3 });
|
|
123
|
+
*/
|
|
124
|
+
async setViewport(viewport) {
|
|
125
|
+
this.assertOpen();
|
|
126
|
+
await this.viewport.set(viewport);
|
|
127
|
+
}
|
|
128
|
+
/** @internal Chrome: lays this page out like a phone from now on (`use.isMobile`), including the size it has now */
|
|
129
|
+
async setMobileLayout() {
|
|
130
|
+
await this.viewport.setMobileLayout();
|
|
131
|
+
}
|
|
132
|
+
/** Playwright's name for `setViewport` */
|
|
133
|
+
setViewportSize(size) {
|
|
134
|
+
return this.setViewport(size);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The size this page was last given (`setViewport`, `setViewportSize`, or the default from config `use.viewport` when the
|
|
138
|
+
* browser opened it), read at once; null before one is known, after `setViewport(null)` and when config turns the viewport off.
|
|
139
|
+
*/
|
|
140
|
+
viewportSize() {
|
|
141
|
+
return this.viewport.size();
|
|
142
|
+
}
|
|
143
|
+
/** Gives the page the size from config `use.viewport` (1280x720 by default); the browser does this when it opens a page */
|
|
144
|
+
async applyDefaultViewport() {
|
|
145
|
+
await this.viewport.applyDefault();
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Takes a screenshot and returns the image (also written to `options.path`).
|
|
149
|
+
* @example
|
|
150
|
+
* await page.screenshot({ path: "out/home.png", fullPage: true });
|
|
151
|
+
*/
|
|
152
|
+
async screenshot(options = {}) {
|
|
153
|
+
this.assertOpen();
|
|
154
|
+
return screenshotWithEvidence(this.biDiConnector, this.id, options, this.evidence);
|
|
155
|
+
}
|
|
156
|
+
/** Waits `ms` milliseconds. Prefer waiting for something (an assertion, `waitForNetworkIdle`) when you can */
|
|
157
|
+
async waitForTimeout(ms) {
|
|
158
|
+
if (!Number.isFinite(ms) || ms < 0)
|
|
159
|
+
throw new RangeError(`waitForTimeout(): ms must be 0 or more, got ${ms}`);
|
|
160
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
161
|
+
}
|
|
162
|
+
/** Renders the page as a PDF and returns it (also written to `options.path`) */
|
|
163
|
+
async pdf(options = {}) {
|
|
164
|
+
this.assertOpen();
|
|
165
|
+
return printPdf(this.biDiConnector, this.id, options);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Changes the `use` options that can change while the browser runs for this page only: locale, timezone, user
|
|
169
|
+
* agent, geolocation, offline, orientation, screen, hasTouch, javaScriptEnabled. It wins over what the page's
|
|
170
|
+
* context says; `null` puts the real (or the context's) value back. Same options as `test.use()`, applied now.
|
|
171
|
+
* @example
|
|
172
|
+
* await page.use({ locale: "nl-NL", offline: true });
|
|
173
|
+
*/
|
|
174
|
+
async use(options) {
|
|
175
|
+
this.assertOpen();
|
|
176
|
+
await applyUse(this.biDiConnector, { contexts: [this.id] }, options);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Grants a browser permission ("geolocation", "notifications", "camera", …) to the origin this page is on,
|
|
180
|
+
* or to `options.origin`. A geolocation override only answers once this is granted.
|
|
181
|
+
*/
|
|
182
|
+
async grantPermission(name, options = {}) {
|
|
183
|
+
this.assertOpen();
|
|
184
|
+
await grantPermission(this.biDiConnector, () => this.url(), () => this.browserContext, name, options);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Waits until the page's document reached `state` (default "load"): "domcontentloaded" and "load" follow the
|
|
188
|
+
* document, "networkidle" is "load" plus a quiet network (see `waitForNetworkIdle`). Resolves at once when it
|
|
189
|
+
* is already there. Call it once the navigation has started; a document that is still the old one counts.
|
|
190
|
+
* @example
|
|
191
|
+
* await page.locator("a[@id='next']").click();
|
|
192
|
+
* await page.waitForLoadState("domcontentloaded");
|
|
193
|
+
*/
|
|
194
|
+
waitForLoadState(state = "load", options = {}) {
|
|
195
|
+
return this.nav.waitForLoadState(state, options);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { APIRequestContext } from "../request/api-request-context.js";
|
|
2
|
+
import EventEmitter from "node:events";
|
|
3
|
+
import { ContextTree } from "./context-tree.js";
|
|
4
|
+
import type { BrowserContext } from "./browser-context.js";
|
|
5
|
+
import type { Evidence } from "./evidence.js";
|
|
6
|
+
import { PageLogging } from "./page-logging.js";
|
|
7
|
+
import { Lazy } from "./lazy.js";
|
|
8
|
+
import { DialogHandling } from "./dialog-handling.js";
|
|
9
|
+
import { DocumentWatcher } from "./document-watcher.js";
|
|
10
|
+
import { PageNavigation } from "./page-navigation.js";
|
|
11
|
+
import { PageNetwork } from "./page-network.js";
|
|
12
|
+
import { PageTransfers } from "./page-transfers.js";
|
|
13
|
+
import { InitScripts } from "./init-script.js";
|
|
14
|
+
import { PageLocators } from "./page-locators.js";
|
|
15
|
+
import Keyboard from "./keyboard.js";
|
|
16
|
+
import Mouse from "./mouse.js";
|
|
17
|
+
import { PageConfig, type PageSettings } from "./page-settings.js";
|
|
18
|
+
import { PageViewport } from "./page-viewport.js";
|
|
19
|
+
import { HelperRealm } from "../locator/helper-realm.js";
|
|
20
|
+
import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
21
|
+
import type { BrowsingContext } from "../types/bidi-modules/browsing-context.js";
|
|
22
|
+
/** @internal Page state and its wiring: the layers of `Page` build on it (see page.ts) */
|
|
23
|
+
export declare abstract class PageCore {
|
|
24
|
+
protected biDiConnector: BiDiConnector;
|
|
25
|
+
protected id: BrowsingContext;
|
|
26
|
+
protected lazyTree: Lazy<ContextTree>;
|
|
27
|
+
protected helperRealm: HelperRealm;
|
|
28
|
+
protected initScripts: InitScripts;
|
|
29
|
+
protected pageConfig: PageConfig;
|
|
30
|
+
protected locators: PageLocators;
|
|
31
|
+
/** Real key input to whatever has focus: `press` (also chords such as `Control+a`), `down`, `up`, `type`, `insertText` */
|
|
32
|
+
readonly keyboard: Keyboard;
|
|
33
|
+
/** Real pointer input at viewport coordinates: `move`, `down`, `up`, `click`, `dblclick`, `wheel` */
|
|
34
|
+
readonly mouse: Mouse;
|
|
35
|
+
protected pageEvents: EventEmitter<any>;
|
|
36
|
+
protected documents: DocumentWatcher;
|
|
37
|
+
protected nav: PageNavigation;
|
|
38
|
+
protected net: PageNetwork;
|
|
39
|
+
protected dialogs: DialogHandling;
|
|
40
|
+
protected logging: PageLogging;
|
|
41
|
+
protected isClosed: boolean;
|
|
42
|
+
protected browserContext: BrowserContext | undefined;
|
|
43
|
+
protected transfers: PageTransfers;
|
|
44
|
+
protected evidence: Evidence | undefined;
|
|
45
|
+
protected viewport: PageViewport;
|
|
46
|
+
/** `settings` override what `samurai.config.ts` says; without them the config is read when needed */
|
|
47
|
+
constructor(biDiConnector: BiDiConnector, contextId: string, settings?: PageSettings);
|
|
48
|
+
/** The BiDi browsing context id of this page's top-level context */
|
|
49
|
+
get contextId(): BrowsingContext;
|
|
50
|
+
/** The context tree, created on first use and shared by everything that needs to attribute frame events to this page */
|
|
51
|
+
tree(): Promise<ContextTree>;
|
|
52
|
+
/** Removes everything the page registered in the browser: init scripts, framework helpers and event subscriptions */
|
|
53
|
+
dispose(): Promise<void>;
|
|
54
|
+
/** @internal The runner gives each test its own evidence folder */
|
|
55
|
+
setEvidence(evidence: Evidence | undefined): void;
|
|
56
|
+
/** The browser context (cookies, storage) this page belongs to */
|
|
57
|
+
context(): BrowserContext;
|
|
58
|
+
/** Makes HTTP requests from the test with this page's cookies; the same as `page.context().request` */
|
|
59
|
+
get request(): APIRequestContext;
|
|
60
|
+
/** @internal Called when the page is opened in a context */
|
|
61
|
+
setContext(context: BrowserContext): void;
|
|
62
|
+
/** @internal Marks the page closed after its tab is gone (its context was removed) and releases what it registered */
|
|
63
|
+
detach(): Promise<void>;
|
|
64
|
+
/** True once `close()` has run */
|
|
65
|
+
get closed(): boolean;
|
|
66
|
+
protected assertOpen(): void;
|
|
67
|
+
/**
|
|
68
|
+
* Closes the tab and releases everything the page registered. Later calls on the page throw "page closed".
|
|
69
|
+
* With `runBeforeUnload` the page's `beforeunload` handlers run first. Closing the browser's last tab may end the browser.
|
|
70
|
+
*/
|
|
71
|
+
close(options?: {
|
|
72
|
+
runBeforeUnload?: boolean;
|
|
73
|
+
}): Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* Resolves with the first `event` `matches` accepts. Listens before `trigger` runs, so what the trigger causes cannot
|
|
76
|
+
* be missed; a trigger that throws rejects the wait with its error. `onTimeout` builds the error for a wait that ran out.
|
|
77
|
+
*/
|
|
78
|
+
private waitForPageEvent;
|
|
79
|
+
abstract url(): Promise<string>;
|
|
80
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import EventEmitter from "node:events";
|
|
2
|
+
import logger from "../logger/index.js";
|
|
3
|
+
import { ContextTree } from "./context-tree.js";
|
|
4
|
+
import { PageLogging } from "./page-logging.js";
|
|
5
|
+
import { Lazy } from "./lazy.js";
|
|
6
|
+
import { DialogHandling } from "./dialog-handling.js";
|
|
7
|
+
import { DocumentWatcher } from "./document-watcher.js";
|
|
8
|
+
import { PageNavigation } from "./page-navigation.js";
|
|
9
|
+
import { PageNetwork } from "./page-network.js";
|
|
10
|
+
import { PageTransfers } from "./page-transfers.js";
|
|
11
|
+
import { InitScripts } from "./init-script.js";
|
|
12
|
+
import { PageLocators } from "./page-locators.js";
|
|
13
|
+
import Keyboard from "./keyboard.js";
|
|
14
|
+
import Mouse from "./mouse.js";
|
|
15
|
+
import { waitForPageEvent, } from "./page-events.js";
|
|
16
|
+
import { PageConfig } from "./page-settings.js";
|
|
17
|
+
import { PageViewport } from "./page-viewport.js";
|
|
18
|
+
import { HelperRealm } from "../locator/helper-realm.js";
|
|
19
|
+
import { BiDiError } from "../transport/bidi-error.js";
|
|
20
|
+
/** @internal Page state and its wiring: the layers of `Page` build on it (see page.ts) */
|
|
21
|
+
export class PageCore {
|
|
22
|
+
/** `settings` override what `samurai.config.ts` says; without them the config is read when needed */
|
|
23
|
+
constructor(biDiConnector, contextId, settings) {
|
|
24
|
+
this.lazyTree = new Lazy();
|
|
25
|
+
this.initScripts = new InitScripts();
|
|
26
|
+
this.pageEvents = new EventEmitter();
|
|
27
|
+
this.isClosed = false;
|
|
28
|
+
this.biDiConnector = biDiConnector;
|
|
29
|
+
this.id = contextId;
|
|
30
|
+
this.pageConfig = new PageConfig(settings);
|
|
31
|
+
this.helperRealm = new HelperRealm(biDiConnector, contextId);
|
|
32
|
+
this.locators = new PageLocators(biDiConnector, contextId, this.helperRealm, this.pageConfig, () => this.assertOpen());
|
|
33
|
+
this.keyboard = new Keyboard(biDiConnector, contextId, () => this.assertOpen());
|
|
34
|
+
this.mouse = new Mouse(biDiConnector, contextId, () => this.assertOpen());
|
|
35
|
+
this.documents = new DocumentWatcher(biDiConnector, contextId);
|
|
36
|
+
this.viewport = new PageViewport(biDiConnector, contextId, this.pageConfig);
|
|
37
|
+
this.logging = new PageLogging(biDiConnector, contextId, () => this.tree(), this.pageEvents);
|
|
38
|
+
this.net = new PageNetwork(biDiConnector, contextId, {
|
|
39
|
+
config: this.pageConfig,
|
|
40
|
+
tree: { ensure: () => this.tree(), peek: () => this.lazyTree.peek() },
|
|
41
|
+
emitter: this.pageEvents,
|
|
42
|
+
waitForEvent: (event, matches, options, onTimeout) => this.waitForPageEvent(event, matches, options, onTimeout),
|
|
43
|
+
});
|
|
44
|
+
this.nav = new PageNavigation(biDiConnector, contextId, {
|
|
45
|
+
config: this.pageConfig,
|
|
46
|
+
documents: this.documents,
|
|
47
|
+
assertOpen: () => this.assertOpen(),
|
|
48
|
+
url: () => this.url(),
|
|
49
|
+
waitForNetworkIdle: (options) => this.net.waitForIdle(options),
|
|
50
|
+
});
|
|
51
|
+
this.transfers = new PageTransfers(biDiConnector, contextId, {
|
|
52
|
+
config: this.pageConfig,
|
|
53
|
+
tree: { ensure: () => this.tree(), peek: () => this.lazyTree.peek() },
|
|
54
|
+
emitter: this.pageEvents,
|
|
55
|
+
waitForEvent: (event, matches, options, onTimeout) => this.waitForPageEvent(event, matches, options, onTimeout),
|
|
56
|
+
});
|
|
57
|
+
this.dialogs = new DialogHandling(biDiConnector, contextId, { ensure: () => this.tree(), peek: () => this.lazyTree.peek() }, this.pageEvents);
|
|
58
|
+
}
|
|
59
|
+
/** The BiDi browsing context id of this page's top-level context */
|
|
60
|
+
get contextId() {
|
|
61
|
+
return this.id;
|
|
62
|
+
}
|
|
63
|
+
/** The context tree, created on first use and shared by everything that needs to attribute frame events to this page */
|
|
64
|
+
tree() {
|
|
65
|
+
return this.lazyTree.get(() => ContextTree.create(this.biDiConnector));
|
|
66
|
+
}
|
|
67
|
+
/** Removes everything the page registered in the browser: init scripts, framework helpers and event subscriptions */
|
|
68
|
+
async dispose() {
|
|
69
|
+
const scripts = this.initScripts.all();
|
|
70
|
+
const results = await Promise.allSettled([
|
|
71
|
+
...scripts.map((handle) => handle.dispose()),
|
|
72
|
+
this.helperRealm.dispose(),
|
|
73
|
+
// The tracker goes before the tree it reads
|
|
74
|
+
this.net.disposeTracker(),
|
|
75
|
+
]);
|
|
76
|
+
const tree = this.lazyTree.take();
|
|
77
|
+
const pageLogs = this.logging.take();
|
|
78
|
+
results.push(...(await Promise.allSettled([
|
|
79
|
+
tree?.then((resolved) => resolved.dispose()),
|
|
80
|
+
this.documents.dispose(),
|
|
81
|
+
this.dialogs.dispose(),
|
|
82
|
+
pageLogs?.then((logs) => logs.dispose()),
|
|
83
|
+
...this.net.disposeRest(),
|
|
84
|
+
...this.transfers.dispose(),
|
|
85
|
+
])));
|
|
86
|
+
const failed = results.find((result) => result.status === "rejected");
|
|
87
|
+
if (failed)
|
|
88
|
+
throw failed.reason;
|
|
89
|
+
}
|
|
90
|
+
/** @internal The runner gives each test its own evidence folder */
|
|
91
|
+
setEvidence(evidence) {
|
|
92
|
+
this.evidence = evidence;
|
|
93
|
+
}
|
|
94
|
+
/** The browser context (cookies, storage) this page belongs to */
|
|
95
|
+
context() {
|
|
96
|
+
if (!this.browserContext)
|
|
97
|
+
throw new Error("this page has no browser context: open it with browser.newPage() or context.newPage()");
|
|
98
|
+
return this.browserContext;
|
|
99
|
+
}
|
|
100
|
+
/** Makes HTTP requests from the test with this page's cookies; the same as `page.context().request` */
|
|
101
|
+
get request() {
|
|
102
|
+
return this.context().request;
|
|
103
|
+
}
|
|
104
|
+
/** @internal Called when the page is opened in a context */
|
|
105
|
+
setContext(context) {
|
|
106
|
+
this.browserContext = context;
|
|
107
|
+
}
|
|
108
|
+
/** @internal Marks the page closed after its tab is gone (its context was removed) and releases what it registered */
|
|
109
|
+
async detach() {
|
|
110
|
+
this.isClosed = true;
|
|
111
|
+
await this.dispose().catch((err) => logger.debug("Releasing a detached page failed", { err }));
|
|
112
|
+
}
|
|
113
|
+
/** True once `close()` has run */
|
|
114
|
+
get closed() {
|
|
115
|
+
return this.isClosed;
|
|
116
|
+
}
|
|
117
|
+
assertOpen() {
|
|
118
|
+
if (this.isClosed)
|
|
119
|
+
throw new Error("page closed");
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Closes the tab and releases everything the page registered. Later calls on the page throw "page closed".
|
|
123
|
+
* With `runBeforeUnload` the page's `beforeunload` handlers run first. Closing the browser's last tab may end the browser.
|
|
124
|
+
*/
|
|
125
|
+
async close(options = {}) {
|
|
126
|
+
if (this.isClosed)
|
|
127
|
+
return;
|
|
128
|
+
try {
|
|
129
|
+
await this.biDiConnector.send("browsingContext.close", {
|
|
130
|
+
context: this.id,
|
|
131
|
+
...(options.runBeforeUnload && { promptUnload: true }),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
// Already gone (closed by the page, the browser or another call): the goal is met
|
|
136
|
+
if (!(err instanceof BiDiError && err.code === "no such frame"))
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
this.isClosed = true;
|
|
140
|
+
await this.dispose().catch((err) => logger.debug("Releasing a closed page failed", { err }));
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Resolves with the first `event` `matches` accepts. Listens before `trigger` runs, so what the trigger causes cannot
|
|
144
|
+
* be missed; a trigger that throws rejects the wait with its error. `onTimeout` builds the error for a wait that ran out.
|
|
145
|
+
*/
|
|
146
|
+
async waitForPageEvent(event, matches, options, onTimeout) {
|
|
147
|
+
const timeout = await this.pageConfig.navigationTimeout(options.timeout);
|
|
148
|
+
return waitForPageEvent(this.pageEvents, event, matches, options, timeout, onTimeout);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import Locator from "../locator/locator.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";
|
|
5
|
+
import { PageActions } from "./page-actions.js";
|
|
6
|
+
/** @internal The ways to locate elements */
|
|
7
|
+
export declare abstract class PageLocating extends PageActions {
|
|
8
|
+
/**
|
|
9
|
+
* Locates elements on the page. An xpath (`//x`, `/html/body`, `(//x)[2]`, `./x`, `xpath=…`, or an older
|
|
10
|
+
* relative one such as `a[@id='x']`) or a css selector (`input[name='x']`, `a, button`, `css=…`).
|
|
11
|
+
* `options` filter the matches, as `locator.filter()` does
|
|
12
|
+
*/
|
|
13
|
+
locator(selector: string, options?: FilterOptions<Locator>): Locator;
|
|
14
|
+
/**
|
|
15
|
+
* Makes this page's locators strict (or not): an action or read on a locator matching several elements then
|
|
16
|
+
* throws `name(): strict mode violation, N elements match ...` instead of using the first. `undefined` goes back
|
|
17
|
+
* to config `use.strictSelectors` (off unless set). Applies to locators made afterwards and to earlier ones that have not
|
|
18
|
+
* run an action or read yet: a locator keeps the answer it got at its first one
|
|
19
|
+
*/
|
|
20
|
+
setStrictSelectors(enabled: boolean | undefined): void;
|
|
21
|
+
/** Locates elements with a CSS selector */
|
|
22
|
+
getByCss(css: string): Locator;
|
|
23
|
+
/** Locates elements by their text: an exact match unless `match: "partial"` or `exact: false`; a RegExp is tested as written (see `TextOptions`) */
|
|
24
|
+
getByText(text: string | RegExp, options?: TextOptions): Locator;
|
|
25
|
+
/** Locates form controls by their label text: an exact match unless `match: "partial"` or `exact: false`; a RegExp is tested as written */
|
|
26
|
+
getByLabel(text: string | RegExp, options?: TextOptions): Locator;
|
|
27
|
+
/** Locates elements by their `placeholder` (see `TextOptions` for `exact` and RegExp) */
|
|
28
|
+
getByPlaceholder(text: string | RegExp, options?: ExactOption): Locator;
|
|
29
|
+
/** Locates elements by their `alt` text (images, areas, image inputs) */
|
|
30
|
+
getByAltText(text: string | RegExp, options?: ExactOption): Locator;
|
|
31
|
+
/** Locates elements by their `title` attribute */
|
|
32
|
+
getByTitle(text: string | RegExp, options?: ExactOption): Locator;
|
|
33
|
+
/** Locates elements by their `data-testid` attribute */
|
|
34
|
+
getByTestId(testId: string): Locator;
|
|
35
|
+
/** Locates elements by ARIA role and, optionally, accessible name and state (see `RoleOptions`) */
|
|
36
|
+
getByRole(role: string, options?: RoleOptions): Locator;
|
|
37
|
+
}
|