@itmetsam/samurai-framework 0.21.3 → 0.21.5
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/api.d.ts +3 -1
- 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/browser.d.ts +9 -0
- package/dist/browser/browser.js +25 -5
- package/dist/browser/cdp.d.ts +11 -0
- package/dist/browser/cdp.js +28 -0
- package/dist/browser/chrome-launch.d.ts +4 -0
- package/dist/browser/chrome-launch.js +2 -2
- package/dist/browser/index.d.ts +5 -0
- package/dist/browser/index.js +4 -0
- package/dist/browser/launch-firefox.js +1 -1
- package/dist/browser/launch-flags.d.ts +4 -1
- package/dist/browser/launch-flags.js +18 -5
- 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-settings.d.ts +1 -1
- package/dist/browser/page.d.ts +52 -1
- package/dist/browser/page.js +66 -2
- package/dist/browser/session-capabilities.d.ts +10 -0
- package/dist/browser/session-capabilities.js +11 -0
- package/dist/browser/session.d.ts +2 -1
- package/dist/browser/session.js +3 -3
- package/dist/browser/storage-state.d.ts +6 -0
- package/dist/browser/storage-state.js +37 -0
- package/dist/browser/use-applier.d.ts +27 -0
- package/dist/browser/use-applier.js +94 -0
- package/dist/browser/viewport.d.ts +3 -2
- package/dist/browser/viewport.js +15 -2
- package/dist/config/config.js +2 -0
- package/dist/config/use-options.d.ts +16 -0
- package/dist/config/use-options.js +128 -0
- 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/logger/warn-once.d.ts +4 -0
- package/dist/logger/warn-once.js +13 -0
- package/dist/runner/attachments.d.ts +16 -0
- package/dist/runner/attachments.js +60 -0
- package/dist/runner/fixture-args.d.ts +15 -0
- package/dist/runner/fixture-args.js +95 -0
- package/dist/runner/fixture-defs.d.ts +22 -0
- package/dist/runner/fixture-defs.js +103 -0
- package/dist/runner/fixture-scope.d.ts +33 -0
- package/dist/runner/fixture-scope.js +156 -0
- package/dist/runner/fixtures.d.ts +34 -0
- package/dist/runner/fixtures.js +39 -0
- package/dist/runner/hooks.d.ts +6 -1
- package/dist/runner/hooks.js +15 -3
- package/dist/runner/registry.d.ts +3 -39
- package/dist/runner/registry.js +93 -19
- package/dist/runner/reporter.d.ts +7 -1
- package/dist/runner/reporter.js +3 -0
- package/dist/runner/run-test.d.ts +1 -3
- package/dist/runner/run-test.js +92 -34
- package/dist/runner/steps.d.ts +13 -0
- package/dist/runner/steps.js +67 -0
- package/dist/runner/suite.d.ts +5 -0
- package/dist/runner/suite.js +8 -0
- package/dist/runner/test-annotations.d.ts +16 -0
- package/dist/runner/test-annotations.js +47 -0
- package/dist/runner/test-function.d.ts +103 -0
- package/dist/runner/test-function.js +1 -0
- package/dist/runner/test-info.d.ts +104 -0
- package/dist/runner/test-info.js +169 -0
- package/dist/runner/test-runner.js +2 -1
- package/dist/runner/test-timeout.d.ts +22 -0
- package/dist/runner/test-timeout.js +45 -0
- package/dist/runner/use-options.d.ts +7 -0
- package/dist/runner/use-options.js +18 -0
- package/dist/runner/worker-session.js +2 -1
- package/dist/script/call-function.d.ts +7 -0
- package/dist/script/call-function.js +21 -3
- package/dist/script/element-handle.d.ts +1 -1
- package/dist/script/element-handle.js +1 -1
- package/dist/types/config.d.ts +3 -2
- package/dist/types/extend.d.ts +30 -0
- package/dist/types/test.d.ts +120 -0
- package/dist/types/use.d.ts +47 -0
- package/dist/wait/wait-until.d.ts +3 -1
- package/dist/wait/wait-until.js +7 -2
- package/package.json +1 -1
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,9 @@ export default class Page {
|
|
|
46
47
|
private browserContext;
|
|
47
48
|
private transfers;
|
|
48
49
|
private evidence;
|
|
50
|
+
private knownViewport;
|
|
51
|
+
/** Chrome's phone layout is on (`emulateMobile`): viewport changes keep it */
|
|
52
|
+
private mobile;
|
|
49
53
|
/** `settings` override what `samurai.config.ts` says; without them the config is read when needed */
|
|
50
54
|
constructor(biDiConnector: BiDiConnector, contextId: string, settings?: PageSettings);
|
|
51
55
|
/** The BiDi browsing context id of this page's top-level context */
|
|
@@ -61,7 +65,8 @@ export default class Page {
|
|
|
61
65
|
/**
|
|
62
66
|
* Makes this page's locators strict (or not): an action or read on a locator matching several elements then
|
|
63
67
|
* 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
|
|
68
|
+
* to config `use.strictSelectors` (off unless set). Applies to locators made afterwards and to earlier ones that have not
|
|
69
|
+
* run an action or read yet: a locator keeps the answer it got at its first one
|
|
65
70
|
*/
|
|
66
71
|
setStrictSelectors(enabled: boolean | undefined): void;
|
|
67
72
|
/** Locates elements with a CSS selector */
|
|
@@ -133,14 +138,60 @@ export default class Page {
|
|
|
133
138
|
goForward(): Promise<boolean>;
|
|
134
139
|
/** The URL of the page now */
|
|
135
140
|
url(): Promise<string>;
|
|
141
|
+
/**
|
|
142
|
+
* Waits until the URL matches `url` (a string equals it, or with `*` / `**` is a glob; a RegExp is tested on it;
|
|
143
|
+
* a function gets it as a `URL`), then until the page reached `waitUntil` (default "load"). Also sees `pushState`.
|
|
144
|
+
* @example
|
|
145
|
+
* await page.getByRole("button", { name: "Sign in" }).click();
|
|
146
|
+
* await page.waitForURL("**\/dashboard");
|
|
147
|
+
*/
|
|
148
|
+
waitForURL(url: UrlPattern | ((url: URL) => boolean), options?: {
|
|
149
|
+
timeout?: number;
|
|
150
|
+
waitUntil?: LoadState;
|
|
151
|
+
}): Promise<void>;
|
|
152
|
+
/** The markup of the document now, doctype included */
|
|
153
|
+
content(): Promise<string>;
|
|
154
|
+
/**
|
|
155
|
+
* Replaces the document with `html` and waits for `waitUntil` (default "load"). Scripts and styles in it run and apply;
|
|
156
|
+
* the page keeps its URL and origin (it is not a `data:` page).
|
|
157
|
+
* @example
|
|
158
|
+
* await page.setContent("<button>Go</button><script>document.title = 'x'</script>");
|
|
159
|
+
*/
|
|
160
|
+
setContent(html: string, options?: {
|
|
161
|
+
timeout?: number;
|
|
162
|
+
waitUntil?: LoadState;
|
|
163
|
+
}): Promise<void>;
|
|
136
164
|
/** The title of the document now */
|
|
137
165
|
title(): Promise<string>;
|
|
166
|
+
/**
|
|
167
|
+
* Runs `fn` in the page, with `arg` as its only argument, and returns the result (a promise is awaited). `fn` is a function,
|
|
168
|
+
* the source of one (`"async () => ..."`), or any other string, which is an expression (`"document.title"`; it takes no argument).
|
|
169
|
+
* Values come back as plain JavaScript: Date, Map, Set, RegExp, BigInt, NaN, undefined, nested objects and arrays;
|
|
170
|
+
* an element as an `ElementHandle`; what cannot be copied (a function, window, error, promise) as a `RemoteObject` with its `type`.
|
|
171
|
+
* The page's own error message is kept (`ScriptError`). A function written under tsx must not contain named inner functions
|
|
172
|
+
* (the transpiler adds a `__name` helper the page lacks); a string always works.
|
|
173
|
+
* @example
|
|
174
|
+
* const count = await page.evaluate((min) => document.querySelectorAll("li").length > min, 3);
|
|
175
|
+
*/
|
|
176
|
+
evaluate<R = unknown, A = unknown>(fn: PageFunction<[A], R>, arg?: A): Promise<R>;
|
|
138
177
|
/**
|
|
139
178
|
* Sets the size of the viewport (and, optionally, the device pixel ratio); `null` puts the browser's own size back.
|
|
140
179
|
* @example
|
|
141
180
|
* await page.setViewport({ width: 390, height: 844, devicePixelRatio: 3 });
|
|
142
181
|
*/
|
|
143
182
|
setViewport(viewport: Viewport | null): Promise<void>;
|
|
183
|
+
/** @internal Chrome: lays this page out like a phone from now on (`use.isMobile`), including the size it has now */
|
|
184
|
+
emulateMobile(): Promise<void>;
|
|
185
|
+
/** Playwright's name for `setViewport` */
|
|
186
|
+
setViewportSize(size: Viewport): Promise<void>;
|
|
187
|
+
/**
|
|
188
|
+
* The size this page was last given (`setViewport`, `setViewportSize`, or the default from config `use.viewport` when the
|
|
189
|
+
* browser opened it), read at once; null before one is known, after `setViewport(null)` and when config turns the viewport off.
|
|
190
|
+
*/
|
|
191
|
+
viewportSize(): {
|
|
192
|
+
width: number;
|
|
193
|
+
height: number;
|
|
194
|
+
} | null;
|
|
144
195
|
/** Gives the page the size from config `use.viewport` (1280x720 by default); the browser does this when it opens a page */
|
|
145
196
|
applyDefaultViewport(): Promise<void>;
|
|
146
197
|
/**
|
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,9 @@ export default class Page {
|
|
|
37
39
|
this.lazyLogs = new Lazy();
|
|
38
40
|
this.errorsAllowed = false;
|
|
39
41
|
this.isClosed = false;
|
|
42
|
+
this.knownViewport = null;
|
|
43
|
+
/** Chrome's phone layout is on (`emulateMobile`): viewport changes keep it */
|
|
44
|
+
this.mobile = false;
|
|
40
45
|
this.biDiConnector = biDiConnector;
|
|
41
46
|
this.id = contextId;
|
|
42
47
|
this.pageConfig = new PageConfig(settings);
|
|
@@ -84,7 +89,8 @@ export default class Page {
|
|
|
84
89
|
/**
|
|
85
90
|
* Makes this page's locators strict (or not): an action or read on a locator matching several elements then
|
|
86
91
|
* 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
|
|
92
|
+
* to config `use.strictSelectors` (off unless set). Applies to locators made afterwards and to earlier ones that have not
|
|
93
|
+
* run an action or read yet: a locator keeps the answer it got at its first one
|
|
88
94
|
*/
|
|
89
95
|
setStrictSelectors(enabled) {
|
|
90
96
|
this.locators.setStrictSelectors(enabled);
|
|
@@ -220,11 +226,49 @@ export default class Page {
|
|
|
220
226
|
this.assertOpen();
|
|
221
227
|
return callFunction(this.biDiConnector, this.id, "() => location.href", [], { awaitPromise: false });
|
|
222
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* Waits until the URL matches `url` (a string equals it, or with `*` / `**` is a glob; a RegExp is tested on it;
|
|
231
|
+
* a function gets it as a `URL`), then until the page reached `waitUntil` (default "load"). Also sees `pushState`.
|
|
232
|
+
* @example
|
|
233
|
+
* await page.getByRole("button", { name: "Sign in" }).click();
|
|
234
|
+
* await page.waitForURL("**\/dashboard");
|
|
235
|
+
*/
|
|
236
|
+
waitForURL(url, options) {
|
|
237
|
+
return this.nav.waitForURL(url, options);
|
|
238
|
+
}
|
|
239
|
+
/** The markup of the document now, doctype included */
|
|
240
|
+
async content() {
|
|
241
|
+
this.assertOpen();
|
|
242
|
+
return readContent(this.biDiConnector, this.id);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Replaces the document with `html` and waits for `waitUntil` (default "load"). Scripts and styles in it run and apply;
|
|
246
|
+
* the page keeps its URL and origin (it is not a `data:` page).
|
|
247
|
+
* @example
|
|
248
|
+
* await page.setContent("<button>Go</button><script>document.title = 'x'</script>");
|
|
249
|
+
*/
|
|
250
|
+
async setContent(html, options) {
|
|
251
|
+
return this.nav.setContent(html, options);
|
|
252
|
+
}
|
|
223
253
|
/** The title of the document now */
|
|
224
254
|
async title() {
|
|
225
255
|
this.assertOpen();
|
|
226
256
|
return callFunction(this.biDiConnector, this.id, "() => document.title", [], { awaitPromise: false });
|
|
227
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* Runs `fn` in the page, with `arg` as its only argument, and returns the result (a promise is awaited). `fn` is a function,
|
|
260
|
+
* the source of one (`"async () => ..."`), or any other string, which is an expression (`"document.title"`; it takes no argument).
|
|
261
|
+
* Values come back as plain JavaScript: Date, Map, Set, RegExp, BigInt, NaN, undefined, nested objects and arrays;
|
|
262
|
+
* an element as an `ElementHandle`; what cannot be copied (a function, window, error, promise) as a `RemoteObject` with its `type`.
|
|
263
|
+
* The page's own error message is kept (`ScriptError`). A function written under tsx must not contain named inner functions
|
|
264
|
+
* (the transpiler adds a `__name` helper the page lacks); a string always works.
|
|
265
|
+
* @example
|
|
266
|
+
* const count = await page.evaluate((min) => document.querySelectorAll("li").length > min, 3);
|
|
267
|
+
*/
|
|
268
|
+
async evaluate(fn, arg) {
|
|
269
|
+
this.assertOpen();
|
|
270
|
+
return evaluateInPage(this.biDiConnector, this.id, fn, arg);
|
|
271
|
+
}
|
|
228
272
|
/**
|
|
229
273
|
* Sets the size of the viewport (and, optionally, the device pixel ratio); `null` puts the browser's own size back.
|
|
230
274
|
* @example
|
|
@@ -232,7 +276,27 @@ export default class Page {
|
|
|
232
276
|
*/
|
|
233
277
|
async setViewport(viewport) {
|
|
234
278
|
this.assertOpen();
|
|
235
|
-
await applyViewport(this.biDiConnector, this.id, viewport);
|
|
279
|
+
await applyViewport(this.biDiConnector, this.id, viewport, this.mobile);
|
|
280
|
+
this.knownViewport = viewport && {
|
|
281
|
+
width: viewport.width,
|
|
282
|
+
height: viewport.height,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
/** @internal Chrome: lays this page out like a phone from now on (`use.isMobile`), including the size it has now */
|
|
286
|
+
async emulateMobile() {
|
|
287
|
+
this.mobile = true;
|
|
288
|
+
await this.setViewport(this.knownViewport);
|
|
289
|
+
}
|
|
290
|
+
/** Playwright's name for `setViewport` */
|
|
291
|
+
setViewportSize(size) {
|
|
292
|
+
return this.setViewport(size);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* The size this page was last given (`setViewport`, `setViewportSize`, or the default from config `use.viewport` when the
|
|
296
|
+
* browser opened it), read at once; null before one is known, after `setViewport(null)` and when config turns the viewport off.
|
|
297
|
+
*/
|
|
298
|
+
viewportSize() {
|
|
299
|
+
return this.knownViewport && { ...this.knownViewport };
|
|
236
300
|
}
|
|
237
301
|
/** Gives the page the size from config `use.viewport` (1280x720 by default); the browser does this when it opens a page */
|
|
238
302
|
async applyDefaultViewport() {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { UseOptions } from "../types/use.js";
|
|
1
2
|
/**
|
|
2
3
|
* Sent with session.new (Firefox) or as capabilities of the chromedriver session (Chrome). With "ignore" the browser
|
|
3
4
|
* leaves a JavaScript dialog open and reports it (`browsingContext.userPromptOpened`), so the framework decides how
|
|
@@ -10,3 +11,12 @@ export declare const SESSION_CAPABILITIES: {
|
|
|
10
11
|
};
|
|
11
12
|
};
|
|
12
13
|
};
|
|
14
|
+
/** The session capabilities for a launch: the ones above, plus the `use` option that has to be set when the browser starts */
|
|
15
|
+
export declare function sessionCapabilities(use?: Pick<UseOptions, "acceptInsecureCerts">): {
|
|
16
|
+
alwaysMatch: {
|
|
17
|
+
acceptInsecureCerts?: boolean | undefined;
|
|
18
|
+
unhandledPromptBehavior: {
|
|
19
|
+
default: string;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
};
|
|
@@ -6,3 +6,14 @@
|
|
|
6
6
|
export const SESSION_CAPABILITIES = {
|
|
7
7
|
alwaysMatch: { unhandledPromptBehavior: { default: "ignore" } },
|
|
8
8
|
};
|
|
9
|
+
/** The session capabilities for a launch: the ones above, plus the `use` option that has to be set when the browser starts */
|
|
10
|
+
export function sessionCapabilities(use) {
|
|
11
|
+
return {
|
|
12
|
+
alwaysMatch: {
|
|
13
|
+
...SESSION_CAPABILITIES.alwaysMatch,
|
|
14
|
+
...(use?.acceptInsecureCerts !== undefined && {
|
|
15
|
+
acceptInsecureCerts: use.acceptInsecureCerts,
|
|
16
|
+
}),
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
2
|
+
import type { UseOptions } from "../types/use.js";
|
|
2
3
|
/** session.new got no reply in time */
|
|
3
4
|
export declare class BrowserStartTimeoutError extends Error {
|
|
4
5
|
constructor(label: string, timeout: number);
|
|
5
6
|
}
|
|
6
7
|
/** Sends session.new with its own, longer timeout; `label` names the browser in the timeout error */
|
|
7
|
-
export declare function startSession(connector: Pick<BiDiConnector, "send">, label: string, commandTimeout: number | undefined): Promise<void>;
|
|
8
|
+
export declare function startSession(connector: Pick<BiDiConnector, "send">, label: string, commandTimeout: number | undefined, use?: UseOptions): Promise<void>;
|
|
8
9
|
/**
|
|
9
10
|
* Stops what a failed launch started. A browser left running holds the profile (the next launch
|
|
10
11
|
* fails with "already open") and keeps the test process alive.
|
package/dist/browser/session.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_COMMAND_TIMEOUT } from "../config/defaults.js";
|
|
2
2
|
import { BiDiError } from "../transport/bidi-error.js";
|
|
3
|
-
import {
|
|
3
|
+
import { sessionCapabilities } from "./session-capabilities.js";
|
|
4
4
|
/** session.new waits at least this long (ms): the first start after an install can take a while (macOS checks the new app) */
|
|
5
5
|
const SESSION_START_TIMEOUT = 120_000;
|
|
6
6
|
/** session.new got no reply in time */
|
|
@@ -11,10 +11,10 @@ export class BrowserStartTimeoutError extends Error {
|
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
/** Sends session.new with its own, longer timeout; `label` names the browser in the timeout error */
|
|
14
|
-
export async function startSession(connector, label, commandTimeout) {
|
|
14
|
+
export async function startSession(connector, label, commandTimeout, use) {
|
|
15
15
|
const timeout = Math.max(commandTimeout ?? DEFAULT_COMMAND_TIMEOUT, SESSION_START_TIMEOUT);
|
|
16
16
|
try {
|
|
17
|
-
await connector.send("session.new", { capabilities:
|
|
17
|
+
await connector.send("session.new", { capabilities: sessionCapabilities(use) }, { timeout });
|
|
18
18
|
}
|
|
19
19
|
catch (err) {
|
|
20
20
|
if (err instanceof BiDiError && err.code === "timeout")
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { StorageState } from "../types/use.js";
|
|
2
|
+
import type { CookieInput } from "./browser-context.js";
|
|
3
|
+
/** The state itself, or the JSON file (relative to `baseDir`) that holds it. Throws `use(): what is wrong`. */
|
|
4
|
+
export declare function loadStorageState(source: string | StorageState, baseDir: string): StorageState;
|
|
5
|
+
/** The state's cookies as `context.addCookies()` takes them; `expires` of -1 (or none) is a session cookie */
|
|
6
|
+
export declare function cookiesOf(state: StorageState): CookieInput[];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { checkStorageStateObject } from "../config/use-options.js";
|
|
4
|
+
/** The state itself, or the JSON file (relative to `baseDir`) that holds it. Throws `use(): what is wrong`. */
|
|
5
|
+
export function loadStorageState(source, baseDir) {
|
|
6
|
+
if (typeof source !== "string")
|
|
7
|
+
return source;
|
|
8
|
+
const file = path.resolve(baseDir, source);
|
|
9
|
+
let state;
|
|
10
|
+
try {
|
|
11
|
+
state = JSON.parse(readFileSync(file, "utf8"));
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
throw new Error(`use(): storageState file ${JSON.stringify(source)} could not be read: ${err instanceof Error ? err.message : String(err)}`);
|
|
15
|
+
}
|
|
16
|
+
if (typeof state !== "object" || state === null || Array.isArray(state))
|
|
17
|
+
throw new Error(`use(): storageState file ${JSON.stringify(source)} must hold an object`);
|
|
18
|
+
checkStorageStateObject(state);
|
|
19
|
+
return state;
|
|
20
|
+
}
|
|
21
|
+
const SAME_SITE = { Strict: "strict", Lax: "lax", None: "none" };
|
|
22
|
+
/** The state's cookies as `context.addCookies()` takes them; `expires` of -1 (or none) is a session cookie */
|
|
23
|
+
export function cookiesOf(state) {
|
|
24
|
+
return (state.cookies ?? []).map((cookie) => ({
|
|
25
|
+
name: cookie.name,
|
|
26
|
+
value: cookie.value,
|
|
27
|
+
domain: cookie.domain,
|
|
28
|
+
...(cookie.path !== undefined && { path: cookie.path }),
|
|
29
|
+
...(cookie.expires !== undefined &&
|
|
30
|
+
cookie.expires > 0 && { expiry: cookie.expires }),
|
|
31
|
+
...(cookie.httpOnly !== undefined && { httpOnly: cookie.httpOnly }),
|
|
32
|
+
...(cookie.secure !== undefined && { secure: cookie.secure }),
|
|
33
|
+
...(cookie.sameSite !== undefined && {
|
|
34
|
+
sameSite: SAME_SITE[cookie.sameSite],
|
|
35
|
+
}),
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { SupportedBrowser } from "../types/browser.js";
|
|
2
|
+
import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
3
|
+
import type { UseOptions } from "../types/use.js";
|
|
4
|
+
import type { BrowserContext } from "./browser-context.js";
|
|
5
|
+
import type Page from "./page.js";
|
|
6
|
+
/** The `use` options a browser applies to its contexts and pages, by what each browser can do (see the wiki's support matrix) */
|
|
7
|
+
export declare class UseApplier {
|
|
8
|
+
private readonly connector;
|
|
9
|
+
private readonly options;
|
|
10
|
+
private readonly state;
|
|
11
|
+
private readonly chrome;
|
|
12
|
+
constructor(connector: BiDiConnector, browserName: SupportedBrowser, options: UseOptions, projectDir: string);
|
|
13
|
+
/** What new contexts accept unless they are told otherwise */
|
|
14
|
+
get acceptInsecureCerts(): boolean | undefined;
|
|
15
|
+
/** Whether pages are laid out like a phone: Chrome only */
|
|
16
|
+
get isMobile(): boolean;
|
|
17
|
+
/** Whether pages have touch input: Chrome only */
|
|
18
|
+
get hasTouch(): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Sets up a context: locale, timezone, user agent, touch and JavaScript through BiDi emulation, the extra headers,
|
|
21
|
+
* then the storage state. Firefox gets JavaScript and the colour scheme when it starts (a profile setting).
|
|
22
|
+
*/
|
|
23
|
+
context(context: BrowserContext): Promise<void>;
|
|
24
|
+
/** Sets up a page of a context this applier set up: Chrome's phone layout and colour scheme (CDP, so per page) */
|
|
25
|
+
page(page: Page): Promise<void>;
|
|
26
|
+
private restore;
|
|
27
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { warnOnce } from "../logger/warn-once.js";
|
|
2
|
+
import { cdpSend, sendRaw } from "./cdp.js";
|
|
3
|
+
import { cookiesOf, loadStorageState } from "./storage-state.js";
|
|
4
|
+
/** The `use` options a browser applies to its contexts and pages, by what each browser can do (see the wiki's support matrix) */
|
|
5
|
+
export class UseApplier {
|
|
6
|
+
constructor(connector, browserName, options, projectDir) {
|
|
7
|
+
this.connector = connector;
|
|
8
|
+
this.options = options;
|
|
9
|
+
this.chrome = browserName === "chrome";
|
|
10
|
+
// Read now: a wrong path fails the test before it starts
|
|
11
|
+
if (options.storageState !== undefined)
|
|
12
|
+
this.state = loadStorageState(options.storageState, projectDir);
|
|
13
|
+
if (!this.chrome) {
|
|
14
|
+
if (options.isMobile)
|
|
15
|
+
warnOnce("use.isMobile", "use.isMobile only works on Chrome: it is ignored on Firefox");
|
|
16
|
+
if (options.hasTouch)
|
|
17
|
+
warnOnce("use.hasTouch", "use.hasTouch only works on Chrome: it is ignored on Firefox");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** What new contexts accept unless they are told otherwise */
|
|
21
|
+
get acceptInsecureCerts() {
|
|
22
|
+
return this.options.acceptInsecureCerts;
|
|
23
|
+
}
|
|
24
|
+
/** Whether pages are laid out like a phone: Chrome only */
|
|
25
|
+
get isMobile() {
|
|
26
|
+
return this.chrome && this.options.isMobile === true;
|
|
27
|
+
}
|
|
28
|
+
/** Whether pages have touch input: Chrome only */
|
|
29
|
+
get hasTouch() {
|
|
30
|
+
return this.chrome && this.options.hasTouch === true;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Sets up a context: locale, timezone, user agent, touch and JavaScript through BiDi emulation, the extra headers,
|
|
34
|
+
* then the storage state. Firefox gets JavaScript and the colour scheme when it starts (a profile setting).
|
|
35
|
+
*/
|
|
36
|
+
async context(context) {
|
|
37
|
+
const { options } = this;
|
|
38
|
+
const emulation = {
|
|
39
|
+
...(options.locale !== undefined && { locale: options.locale }),
|
|
40
|
+
...(options.timezoneId !== undefined && { timezone: options.timezoneId }),
|
|
41
|
+
...(options.userAgent !== undefined && { userAgent: options.userAgent }),
|
|
42
|
+
...(this.hasTouch && { touch: 1 }),
|
|
43
|
+
...(this.chrome &&
|
|
44
|
+
options.javaScriptEnabled === false && { javaScriptEnabled: false }),
|
|
45
|
+
};
|
|
46
|
+
if (Object.keys(emulation).length > 0)
|
|
47
|
+
await context.emulate(emulation);
|
|
48
|
+
if (options.extraHTTPHeaders)
|
|
49
|
+
await sendRaw(this.connector, "network.setExtraHeaders", {
|
|
50
|
+
headers: Object.entries(options.extraHTTPHeaders).map(([name, value]) => ({ name, value: { type: "string", value } })),
|
|
51
|
+
userContexts: [context.id],
|
|
52
|
+
});
|
|
53
|
+
if (this.state)
|
|
54
|
+
await this.restore(context, this.state);
|
|
55
|
+
}
|
|
56
|
+
/** Sets up a page of a context this applier set up: Chrome's phone layout and colour scheme (CDP, so per page) */
|
|
57
|
+
async page(page) {
|
|
58
|
+
if (!this.chrome)
|
|
59
|
+
return;
|
|
60
|
+
if (this.isMobile)
|
|
61
|
+
await page.emulateMobile();
|
|
62
|
+
if (this.options.colorScheme !== undefined)
|
|
63
|
+
await cdpSend(this.connector, page.contextId, "Emulation.setEmulatedMedia", {
|
|
64
|
+
features: [
|
|
65
|
+
{ name: "prefers-color-scheme", value: this.options.colorScheme },
|
|
66
|
+
],
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
async restore(context, state) {
|
|
70
|
+
const cookies = cookiesOf(state);
|
|
71
|
+
if (cookies.length > 0)
|
|
72
|
+
await context.addCookies(cookies);
|
|
73
|
+
for (const { origin, localStorage } of state.origins ?? []) {
|
|
74
|
+
if (localStorage.length === 0)
|
|
75
|
+
continue;
|
|
76
|
+
await writeLocalStorage(context, origin, localStorage);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Local storage belongs to an origin, so the browser has to be on it: a scratch tab opens the origin (the request is
|
|
82
|
+
* answered here, it never reaches the network), writes the entries and closes again.
|
|
83
|
+
*/
|
|
84
|
+
async function writeLocalStorage(context, origin, entries) {
|
|
85
|
+
const tab = await context.newPage();
|
|
86
|
+
try {
|
|
87
|
+
await tab.route(`${origin}/**`, (route) => route.fulfill({ contentType: "text/html", body: "<!doctype html>" }));
|
|
88
|
+
await tab.navigateTo(`${origin}/`);
|
|
89
|
+
await tab.evaluate((items) => items.forEach(({ name, value }) => localStorage.setItem(name, value)), entries);
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
await tab.close();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -9,6 +9,7 @@ export interface Viewport {
|
|
|
9
9
|
export declare function validateViewport({ width, height, devicePixelRatio, }: Viewport): void;
|
|
10
10
|
/**
|
|
11
11
|
* Gives the context this size (and optionally this device pixel ratio); `null` puts the browser's own size back
|
|
12
|
-
* and waits until the page has really got it.
|
|
12
|
+
* and waits until the page has really got it. A `mobile` page (Chrome's phone layout, `use.isMobile`) is sized
|
|
13
|
+
* through CDP, as BiDi's `setViewport` would turn the phone layout off again.
|
|
13
14
|
*/
|
|
14
|
-
export declare function applyViewport(connector: BiDiConnector, context: string, viewport: Viewport | null): Promise<void>;
|
|
15
|
+
export declare function applyViewport(connector: BiDiConnector, context: string, viewport: Viewport | null, mobile?: boolean): Promise<void>;
|
package/dist/browser/viewport.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { callFunction } from "../script/call-function.js";
|
|
2
|
+
import { cdpSend } from "./cdp.js";
|
|
2
3
|
/** Time (ms) without a `resize` event after which a viewport reset counts as done */
|
|
3
4
|
const RESIZE_QUIET_TIME = 50;
|
|
4
5
|
/** Time (ms) a viewport reset waits for a `resize` event that may never come (the size did not change) */
|
|
@@ -20,9 +21,21 @@ export function validateViewport({ width, height, devicePixelRatio, }) {
|
|
|
20
21
|
}
|
|
21
22
|
/**
|
|
22
23
|
* Gives the context this size (and optionally this device pixel ratio); `null` puts the browser's own size back
|
|
23
|
-
* and waits until the page has really got it.
|
|
24
|
+
* and waits until the page has really got it. A `mobile` page (Chrome's phone layout, `use.isMobile`) is sized
|
|
25
|
+
* through CDP, as BiDi's `setViewport` would turn the phone layout off again.
|
|
24
26
|
*/
|
|
25
|
-
export async function applyViewport(connector, context, viewport) {
|
|
27
|
+
export async function applyViewport(connector, context, viewport, mobile = false) {
|
|
28
|
+
if (mobile) {
|
|
29
|
+
if (viewport)
|
|
30
|
+
validateViewport(viewport);
|
|
31
|
+
await cdpSend(connector, context, "Emulation.setDeviceMetricsOverride", {
|
|
32
|
+
width: viewport?.width ?? 0,
|
|
33
|
+
height: viewport?.height ?? 0,
|
|
34
|
+
deviceScaleFactor: viewport ? (viewport.devicePixelRatio ?? 1) : 0,
|
|
35
|
+
mobile: true,
|
|
36
|
+
});
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
26
39
|
if (viewport === null) {
|
|
27
40
|
await untilResized(connector, context, () => connector.send("browsingContext.setViewport", {
|
|
28
41
|
context,
|
package/dist/config/config.js
CHANGED
|
@@ -2,6 +2,7 @@ import path from "path";
|
|
|
2
2
|
import logger from "../logger/index.js";
|
|
3
3
|
import { pathToFileURL } from "url";
|
|
4
4
|
import { validateBrowserConfig } from "./browsers.js";
|
|
5
|
+
import { validateUseConfig } from "./use-options.js";
|
|
5
6
|
import { validateForbidOnlyConfig } from "./forbid-only.js";
|
|
6
7
|
import { validateRetriesConfig } from "./retries.js";
|
|
7
8
|
import { validateWorkersConfig } from "./workers.js";
|
|
@@ -30,6 +31,7 @@ export function validateConfig(config) {
|
|
|
30
31
|
validateWorkersConfig(config);
|
|
31
32
|
validateForbidOnlyConfig(config);
|
|
32
33
|
validateRetriesConfig(config);
|
|
34
|
+
validateUseConfig(config);
|
|
33
35
|
}
|
|
34
36
|
/** The whole `samurai.config.ts` of the project (the working directory unless a project is active) */
|
|
35
37
|
export async function loadRawConfig() {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { SamuraiTestConfig } from "../types/config.js";
|
|
2
|
+
import type { UseOptions } from "../types/use.js";
|
|
3
|
+
/**
|
|
4
|
+
* Checks the options of `test.use()` or of the config's `use` and returns them. The config's `use` also holds
|
|
5
|
+
* options that are not these (`viewport`, `headless`, ...), so it passes `allowOthers`; `test.use()` refuses
|
|
6
|
+
* a key it does not know. Throws `use(): what is wrong`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function validateUseOptions(options: unknown, allowOthers?: boolean): UseOptions;
|
|
9
|
+
/** Checks a storage state object, whether it came from the options or from a file */
|
|
10
|
+
export declare function checkStorageStateObject(state: Record<string, unknown>): void;
|
|
11
|
+
/** The options after `layers` are applied in order: a later layer wins, key by key */
|
|
12
|
+
export declare function mergeUse(layers: readonly UseOptions[]): UseOptions;
|
|
13
|
+
/** Checks the new options of a config's `use` */
|
|
14
|
+
export declare function validateUseConfig(config: SamuraiTestConfig): void;
|
|
15
|
+
/** The options of `test.use()` that a config's `use` holds, without the rest (`viewport`, `headless`, ...) */
|
|
16
|
+
export declare function useOptionsOf(configUse: object | undefined): UseOptions;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const NAMES = [
|
|
2
|
+
"locale",
|
|
3
|
+
"timezoneId",
|
|
4
|
+
"userAgent",
|
|
5
|
+
"javaScriptEnabled",
|
|
6
|
+
"acceptInsecureCerts",
|
|
7
|
+
"hasTouch",
|
|
8
|
+
"isMobile",
|
|
9
|
+
"storageState",
|
|
10
|
+
"extraHTTPHeaders",
|
|
11
|
+
"colorScheme",
|
|
12
|
+
];
|
|
13
|
+
const BOOLEANS = [
|
|
14
|
+
"javaScriptEnabled",
|
|
15
|
+
"acceptInsecureCerts",
|
|
16
|
+
"hasTouch",
|
|
17
|
+
"isMobile",
|
|
18
|
+
];
|
|
19
|
+
const COLOR_SCHEMES = ["light", "dark", "no-preference"];
|
|
20
|
+
const HEADER_NAME = /^[\w!#$%&'*+.^`|~-]+$/;
|
|
21
|
+
const fail = (problem) => {
|
|
22
|
+
throw new TypeError(`use(): ${problem}`);
|
|
23
|
+
};
|
|
24
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
25
|
+
/**
|
|
26
|
+
* Checks the options of `test.use()` or of the config's `use` and returns them. The config's `use` also holds
|
|
27
|
+
* options that are not these (`viewport`, `headless`, ...), so it passes `allowOthers`; `test.use()` refuses
|
|
28
|
+
* a key it does not know. Throws `use(): what is wrong`.
|
|
29
|
+
*/
|
|
30
|
+
export function validateUseOptions(options, allowOthers = false) {
|
|
31
|
+
if (!isObject(options))
|
|
32
|
+
return fail("the options must be an object");
|
|
33
|
+
for (const key of Object.keys(options))
|
|
34
|
+
if (!allowOthers && !NAMES.includes(key))
|
|
35
|
+
fail(`unknown option "${key}"`);
|
|
36
|
+
const use = options;
|
|
37
|
+
for (const name of BOOLEANS)
|
|
38
|
+
if (use[name] !== undefined && typeof use[name] !== "boolean")
|
|
39
|
+
fail(`${name} must be true or false, got ${JSON.stringify(use[name])}`);
|
|
40
|
+
checkLocale(use.locale);
|
|
41
|
+
checkTimezone(use.timezoneId);
|
|
42
|
+
if (use.userAgent !== undefined && !nonEmpty(use.userAgent))
|
|
43
|
+
fail("userAgent must be a non-empty string");
|
|
44
|
+
if (use.colorScheme !== undefined && !COLOR_SCHEMES.includes(use.colorScheme))
|
|
45
|
+
fail(`colorScheme must be "light", "dark" or "no-preference", got ${JSON.stringify(use.colorScheme)}`);
|
|
46
|
+
checkHeaders(use.extraHTTPHeaders);
|
|
47
|
+
checkStorageState(use.storageState);
|
|
48
|
+
return use;
|
|
49
|
+
}
|
|
50
|
+
const nonEmpty = (value) => typeof value === "string" && value !== "";
|
|
51
|
+
function checkLocale(locale) {
|
|
52
|
+
if (locale === undefined)
|
|
53
|
+
return;
|
|
54
|
+
try {
|
|
55
|
+
if (!nonEmpty(locale) || Intl.getCanonicalLocales(locale).length !== 1)
|
|
56
|
+
throw new RangeError();
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
fail(`invalid locale ${JSON.stringify(locale)}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function checkTimezone(timezone) {
|
|
63
|
+
if (timezone === undefined)
|
|
64
|
+
return;
|
|
65
|
+
try {
|
|
66
|
+
if (!nonEmpty(timezone))
|
|
67
|
+
throw new RangeError();
|
|
68
|
+
new Intl.DateTimeFormat(undefined, { timeZone: timezone });
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
fail(`unknown timezoneId ${JSON.stringify(timezone)}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function checkHeaders(headers) {
|
|
75
|
+
if (headers === undefined)
|
|
76
|
+
return;
|
|
77
|
+
if (!isObject(headers))
|
|
78
|
+
fail("extraHTTPHeaders must be an object of header names and values");
|
|
79
|
+
for (const [name, value] of Object.entries(headers))
|
|
80
|
+
if (!HEADER_NAME.test(name))
|
|
81
|
+
fail(`extraHTTPHeaders has an invalid header name "${name}"`);
|
|
82
|
+
else if (typeof value !== "string")
|
|
83
|
+
fail(`extraHTTPHeaders "${name}" must be a string`);
|
|
84
|
+
}
|
|
85
|
+
function checkStorageState(state) {
|
|
86
|
+
if (state === undefined)
|
|
87
|
+
return;
|
|
88
|
+
if (typeof state === "string") {
|
|
89
|
+
if (state === "")
|
|
90
|
+
fail("storageState must not be an empty path");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (!isObject(state))
|
|
94
|
+
return fail("storageState must be a file path or an object");
|
|
95
|
+
checkStorageStateObject(state);
|
|
96
|
+
}
|
|
97
|
+
/** Checks a storage state object, whether it came from the options or from a file */
|
|
98
|
+
export function checkStorageStateObject(state) {
|
|
99
|
+
for (const key of ["cookies", "origins"])
|
|
100
|
+
if (state[key] !== undefined && !Array.isArray(state[key]))
|
|
101
|
+
fail(`storageState.${key} must be an array`);
|
|
102
|
+
for (const cookie of (state.cookies ?? [])) {
|
|
103
|
+
if (!isObject(cookie) ||
|
|
104
|
+
!nonEmpty(cookie.name) ||
|
|
105
|
+
typeof cookie.value !== "string" ||
|
|
106
|
+
!nonEmpty(cookie.domain))
|
|
107
|
+
fail("every storageState cookie needs a name, a value and a domain");
|
|
108
|
+
}
|
|
109
|
+
for (const origin of (state.origins ?? [])) {
|
|
110
|
+
if (!isObject(origin) ||
|
|
111
|
+
!nonEmpty(origin.origin) ||
|
|
112
|
+
!Array.isArray(origin.localStorage ?? []))
|
|
113
|
+
fail("every storageState origin needs an origin and a localStorage list");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** The options after `layers` are applied in order: a later layer wins, key by key */
|
|
117
|
+
export function mergeUse(layers) {
|
|
118
|
+
return Object.assign({}, ...layers);
|
|
119
|
+
}
|
|
120
|
+
/** Checks the new options of a config's `use` */
|
|
121
|
+
export function validateUseConfig(config) {
|
|
122
|
+
if (config.use !== undefined)
|
|
123
|
+
validateUseOptions(config.use, true);
|
|
124
|
+
}
|
|
125
|
+
/** The options of `test.use()` that a config's `use` holds, without the rest (`viewport`, `headless`, ...) */
|
|
126
|
+
export function useOptionsOf(configUse) {
|
|
127
|
+
return Object.fromEntries(Object.entries(configUse ?? {}).filter(([key]) => NAMES.includes(key)));
|
|
128
|
+
}
|
|
@@ -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>;
|