@itmetsam/samurai-framework 0.22.0 → 0.22.1
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 -3
- package/dist/browser/page-content.d.ts +4 -0
- package/dist/browser/page-content.js +12 -0
- package/dist/browser/page-events.d.ts +11 -0
- package/dist/browser/page-events.js +23 -0
- package/dist/browser/page-logging.d.ts +24 -0
- package/dist/browser/page-logging.js +43 -0
- package/dist/browser/page-settings.d.ts +1 -1
- package/dist/browser/page-viewport.d.ts +21 -0
- package/dist/browser/page-viewport.js +33 -0
- package/dist/browser/page.d.ts +6 -9
- package/dist/browser/page.js +31 -63
- package/dist/browser/screenshot.d.ts +6 -0
- package/dist/browser/screenshot.js +14 -0
- package/dist/config/config.js +2 -1
- package/dist/config/fixture-use.d.ts +19 -0
- package/dist/config/fixture-use.js +44 -0
- package/dist/config/group-use.d.ts +4 -2
- package/dist/config/group-use.js +30 -20
- package/dist/config/use-options.js +5 -1
- package/dist/runner/fixture-defs.d.ts +2 -1
- package/dist/runner/fixture-defs.js +51 -18
- package/dist/runner/fixture-meta.d.ts +20 -0
- package/dist/runner/fixture-meta.js +109 -0
- package/dist/runner/fixture-options.d.ts +8 -0
- package/dist/runner/fixture-options.js +35 -0
- package/dist/runner/fixture-outputs.d.ts +11 -0
- package/dist/runner/fixture-outputs.js +28 -0
- package/dist/runner/fixture-registry.d.ts +35 -0
- package/dist/runner/fixture-registry.js +73 -0
- package/dist/runner/fixture-scope.d.ts +39 -9
- package/dist/runner/fixture-scope.js +139 -70
- package/dist/runner/fixture-start.d.ts +27 -0
- package/dist/runner/fixture-start.js +76 -0
- package/dist/runner/fixture-use.d.ts +23 -0
- package/dist/runner/fixture-use.js +64 -0
- package/dist/runner/fixture-worker.d.ts +26 -0
- package/dist/runner/fixture-worker.js +46 -0
- package/dist/runner/prepare-run.js +5 -0
- package/dist/runner/registry.js +15 -5
- package/dist/runner/reporter.d.ts +3 -1
- package/dist/runner/reporter.js +1 -0
- package/dist/runner/run-test.d.ts +3 -0
- package/dist/runner/run-test.js +16 -1
- package/dist/runner/run.d.ts +7 -0
- package/dist/runner/run.js +9 -0
- package/dist/runner/test-function.d.ts +2 -2
- package/dist/runner/test-runner.js +6 -1
- package/dist/runner/use-options.d.ts +16 -2
- package/dist/runner/use-options.js +28 -8
- package/dist/runner/worker-session.js +6 -0
- package/dist/types/config.d.ts +35 -22
- package/dist/types/extend.d.ts +54 -2
- package/dist/types/test.d.ts +24 -0
- package/dist/types/use.d.ts +14 -0
- package/package.json +1 -1
package/dist/api.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { expect } from "./assert/expect.js";
|
|
|
7
7
|
export { defineConfig } from "./config/config.js";
|
|
8
8
|
export type { GlobalHookConfig, WebServerConfig } from "./types/config.js";
|
|
9
9
|
export { defineStep, type StepDefinition, type StepResult, type StepRunInput, } from "./flows/steps/index.js";
|
|
10
|
-
export type { AllHookFixtures, TestAnnotation, TestAttachment, TestDetails, TestFixtures, TestInfo, TestOptions, TestOutputs, TestStepEntry, } from "./types/test.js";
|
|
11
|
-
export type { FixtureOptions } from "./types/extend.js";
|
|
12
|
-
export type { ColorScheme, StorageState, StorageStateCookie, RuntimeUseOptions, UseOptions, } from "./types/use.js";
|
|
10
|
+
export type { AllHookFixtures, TestAnnotation, TestAttachment, TestDetails, TestFixtures, TestInfo, TestOptions, TestOutputs, TestFixtureEntry, TestStepEntry, } from "./types/test.js";
|
|
11
|
+
export type { FixtureField, FixtureOptions, FixtureOutput, } from "./types/extend.js";
|
|
12
|
+
export type { ColorScheme, StorageState, StorageStateCookie, RuntimeUseOptions, UseOptions, FixtureUse, } from "./types/use.js";
|
|
13
13
|
export { generatedFile, type GeneratedFile, type FileInput, } from "./browser/generated-files.js";
|
|
@@ -3,3 +3,7 @@ import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
|
3
3
|
export declare function readContent(connector: BiDiConnector, context: string): Promise<string>;
|
|
4
4
|
/** Writes `html` as the page's document; true when the document reached `state` within `timeout` ms */
|
|
5
5
|
export declare function writeContent(connector: BiDiConnector, context: string, html: string, state: "load" | "domcontentloaded", timeout: number): Promise<boolean>;
|
|
6
|
+
/** The URL of the document now */
|
|
7
|
+
export declare function readUrl(connector: BiDiConnector, context: string): Promise<string>;
|
|
8
|
+
/** The title of the document now */
|
|
9
|
+
export declare function readTitle(connector: BiDiConnector, context: string): Promise<string>;
|
|
@@ -41,3 +41,15 @@ export function writeContent(connector, context, html, state, timeout) {
|
|
|
41
41
|
timeout,
|
|
42
42
|
]);
|
|
43
43
|
}
|
|
44
|
+
/** The URL of the document now */
|
|
45
|
+
export function readUrl(connector, context) {
|
|
46
|
+
return callFunction(connector, context, "() => location.href", [], {
|
|
47
|
+
awaitPromise: false,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/** The title of the document now */
|
|
51
|
+
export function readTitle(connector, context) {
|
|
52
|
+
return callFunction(connector, context, "() => document.title", [], {
|
|
53
|
+
awaitPromise: false,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -42,3 +42,14 @@ export declare function sourcesFor(event: PageEventName): EventSource[];
|
|
|
42
42
|
* wait that ran out after `timeout` ms.
|
|
43
43
|
*/
|
|
44
44
|
export declare function waitForPageEvent<E extends PageEventName>(emitter: EventEmitter, event: E, matches: (item: PageEvents[E][0]) => boolean, options: Pick<WaitOptions, "trigger">, timeout: number, onTimeout: (timeout: number) => Error): Promise<PageEvents[E][0]>;
|
|
45
|
+
/** The waits `waitForEvent` hands its event to; `download` and `filechooser` take the predicate too */
|
|
46
|
+
export interface EventWaiters {
|
|
47
|
+
request(accepts: (item: unknown) => boolean, options: WaitOptions): Promise<unknown>;
|
|
48
|
+
response(accepts: (item: unknown) => boolean, options: WaitOptions): Promise<unknown>;
|
|
49
|
+
download(options: WaitOptions, accepts: (item: unknown) => boolean): Promise<unknown>;
|
|
50
|
+
filechooser(options: WaitOptions, accepts: (item: unknown) => boolean): Promise<unknown>;
|
|
51
|
+
}
|
|
52
|
+
/** `page.waitForEvent`: picks the named wait for `event`, with the predicate given as a function or in the options */
|
|
53
|
+
export declare function waitForEventOn<E extends WaitableEventName>(waiters: EventWaiters, event: E, optionsOrPredicate: ((item: PageEvents[E][0]) => boolean) | WaitForEventOptions<PageEvents[E][0]>): Promise<PageEvents[E][0]>;
|
|
54
|
+
/** Starts the parts `event` needs; a failure is reported to `onError`, not thrown */
|
|
55
|
+
export declare function startSources(event: PageEventName, start: Record<EventSource, () => Promise<unknown>>, onError: (err: unknown) => void): void;
|
|
@@ -56,3 +56,26 @@ export function waitForPageEvent(emitter, event, matches, options, timeout, onTi
|
|
|
56
56
|
}
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
|
+
/** `page.waitForEvent`: picks the named wait for `event`, with the predicate given as a function or in the options */
|
|
60
|
+
export async function waitForEventOn(waiters, event, optionsOrPredicate) {
|
|
61
|
+
const { predicate, ...options } = typeof optionsOrPredicate === "function"
|
|
62
|
+
? { predicate: optionsOrPredicate }
|
|
63
|
+
: optionsOrPredicate;
|
|
64
|
+
const accepts = (predicate ?? (() => true));
|
|
65
|
+
switch (event) {
|
|
66
|
+
case "request":
|
|
67
|
+
return waiters.request(accepts, options);
|
|
68
|
+
case "response":
|
|
69
|
+
return waiters.response(accepts, options);
|
|
70
|
+
case "download":
|
|
71
|
+
return waiters.download(options, accepts);
|
|
72
|
+
case "filechooser":
|
|
73
|
+
return waiters.filechooser(options, accepts);
|
|
74
|
+
default:
|
|
75
|
+
throw new Error(`waitForEvent(): "${String(event)}" is not supported; use page.on()`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Starts the parts `event` needs; a failure is reported to `onError`, not thrown */
|
|
79
|
+
export function startSources(event, start, onError) {
|
|
80
|
+
Promise.all(sourcesFor(event).map((source) => start[source]())).catch(onError);
|
|
81
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type EventEmitter from "node:events";
|
|
2
|
+
import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
3
|
+
import type { BrowsingContext } from "../types/bidi-modules/browsing-context.js";
|
|
4
|
+
import type { ContextTree } from "./context-tree.js";
|
|
5
|
+
import { PageLogs, type LogRecord, type PageError } from "./page-logs.js";
|
|
6
|
+
/** A page's console and error collection: started on demand, read at once, and whether the runner may fail on its errors */
|
|
7
|
+
export declare class PageLogging {
|
|
8
|
+
private readonly connector;
|
|
9
|
+
private readonly contextId;
|
|
10
|
+
private readonly tree;
|
|
11
|
+
private readonly emitter;
|
|
12
|
+
private lazyLogs;
|
|
13
|
+
private errorsAllowed;
|
|
14
|
+
constructor(connector: BiDiConnector, contextId: BrowsingContext, tree: () => Promise<ContextTree>, emitter: EventEmitter);
|
|
15
|
+
start(): Promise<void>;
|
|
16
|
+
/** Forgets the collection and hands back its start, for the caller to dispose; undefined when it never started */
|
|
17
|
+
take(): Promise<PageLogs> | undefined;
|
|
18
|
+
entries(): readonly LogRecord[];
|
|
19
|
+
errors(): readonly PageError[];
|
|
20
|
+
get dropped(): number;
|
|
21
|
+
clear(): void;
|
|
22
|
+
allowErrors(): void;
|
|
23
|
+
get errorsAreAllowed(): boolean;
|
|
24
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Lazy } from "./lazy.js";
|
|
2
|
+
import { PageLogs } from "./page-logs.js";
|
|
3
|
+
/** A page's console and error collection: started on demand, read at once, and whether the runner may fail on its errors */
|
|
4
|
+
export class PageLogging {
|
|
5
|
+
constructor(connector, contextId, tree, emitter) {
|
|
6
|
+
this.connector = connector;
|
|
7
|
+
this.contextId = contextId;
|
|
8
|
+
this.tree = tree;
|
|
9
|
+
this.emitter = emitter;
|
|
10
|
+
this.lazyLogs = new Lazy();
|
|
11
|
+
this.errorsAllowed = false;
|
|
12
|
+
}
|
|
13
|
+
async start() {
|
|
14
|
+
await this.lazyLogs.get(async () => {
|
|
15
|
+
const logs = await PageLogs.start(this.connector, await this.tree(), this.contextId);
|
|
16
|
+
logs.on("console", (message) => this.emitter.emit("console", message));
|
|
17
|
+
logs.on("pageerror", (error) => this.emitter.emit("pageerror", error));
|
|
18
|
+
return logs;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/** Forgets the collection and hands back its start, for the caller to dispose; undefined when it never started */
|
|
22
|
+
take() {
|
|
23
|
+
return this.lazyLogs.take();
|
|
24
|
+
}
|
|
25
|
+
entries() {
|
|
26
|
+
return this.lazyLogs.peek()?.entries ?? [];
|
|
27
|
+
}
|
|
28
|
+
errors() {
|
|
29
|
+
return this.lazyLogs.peek()?.errors ?? [];
|
|
30
|
+
}
|
|
31
|
+
get dropped() {
|
|
32
|
+
return this.lazyLogs.peek()?.dropped ?? 0;
|
|
33
|
+
}
|
|
34
|
+
clear() {
|
|
35
|
+
this.lazyLogs.peek()?.clear();
|
|
36
|
+
}
|
|
37
|
+
allowErrors() {
|
|
38
|
+
this.errorsAllowed = true;
|
|
39
|
+
}
|
|
40
|
+
get errorsAreAllowed() {
|
|
41
|
+
return this.errorsAllowed;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -19,7 +19,7 @@ export declare class PageConfig {
|
|
|
19
19
|
timeout?: number;
|
|
20
20
|
waitUntil?: "none" | "interactive" | "complete";
|
|
21
21
|
}>;
|
|
22
|
-
use(): Promise<import("../api.js").UseOptions & {
|
|
22
|
+
use(): Promise<import("../api.js").UseOptions & import("../api.js").FixtureUse & {
|
|
23
23
|
viewport?: {
|
|
24
24
|
width: number;
|
|
25
25
|
height: number;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
2
|
+
import type { BrowsingContext } from "../types/bidi-modules/browsing-context.js";
|
|
3
|
+
import type { PageConfig } from "./page-settings.js";
|
|
4
|
+
import { type Viewport } from "./viewport.js";
|
|
5
|
+
/** A page's viewport: the size it was last given and whether Chrome lays it out like a phone */
|
|
6
|
+
export declare class PageViewport {
|
|
7
|
+
private readonly connector;
|
|
8
|
+
private readonly contextId;
|
|
9
|
+
private readonly config;
|
|
10
|
+
private known;
|
|
11
|
+
/** Chrome's phone layout is on (`setMobileLayout`): viewport changes keep it */
|
|
12
|
+
private mobile;
|
|
13
|
+
constructor(connector: BiDiConnector, contextId: BrowsingContext, config: PageConfig);
|
|
14
|
+
set(viewport: Viewport | null): Promise<void>;
|
|
15
|
+
setMobileLayout(): Promise<void>;
|
|
16
|
+
size(): {
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
} | null;
|
|
20
|
+
applyDefault(): Promise<void>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { DEFAULT_VIEWPORT } from "../config/defaults.js";
|
|
2
|
+
import { applyViewport } from "./viewport.js";
|
|
3
|
+
/** A page's viewport: the size it was last given and whether Chrome lays it out like a phone */
|
|
4
|
+
export class PageViewport {
|
|
5
|
+
constructor(connector, contextId, config) {
|
|
6
|
+
this.connector = connector;
|
|
7
|
+
this.contextId = contextId;
|
|
8
|
+
this.config = config;
|
|
9
|
+
this.known = null;
|
|
10
|
+
/** Chrome's phone layout is on (`setMobileLayout`): viewport changes keep it */
|
|
11
|
+
this.mobile = false;
|
|
12
|
+
}
|
|
13
|
+
async set(viewport) {
|
|
14
|
+
await applyViewport(this.connector, this.contextId, viewport, this.mobile);
|
|
15
|
+
this.known = viewport && {
|
|
16
|
+
width: viewport.width,
|
|
17
|
+
height: viewport.height,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
async setMobileLayout() {
|
|
21
|
+
this.mobile = true;
|
|
22
|
+
await this.set(this.known);
|
|
23
|
+
}
|
|
24
|
+
size() {
|
|
25
|
+
return this.known && { ...this.known };
|
|
26
|
+
}
|
|
27
|
+
async applyDefault() {
|
|
28
|
+
const { viewport } = await this.config.use();
|
|
29
|
+
if (viewport === null)
|
|
30
|
+
return;
|
|
31
|
+
await this.set(viewport ?? DEFAULT_VIEWPORT);
|
|
32
|
+
}
|
|
33
|
+
}
|
package/dist/browser/page.d.ts
CHANGED
|
@@ -12,8 +12,8 @@ import type { RuntimeUseOptions } from "../types/use.js";
|
|
|
12
12
|
import type { Download } from "./download.js";
|
|
13
13
|
import type { FileChooser } from "./file-chooser.js";
|
|
14
14
|
import { type ScreenshotOptions } from "./screenshot.js";
|
|
15
|
-
import { Evidence } from "./evidence.js";
|
|
16
|
-
import {
|
|
15
|
+
import type { Evidence } from "./evidence.js";
|
|
16
|
+
import type { LogRecord, PageError } from "./page-logs.js";
|
|
17
17
|
import type { RouteHandler } from "../network/router.js";
|
|
18
18
|
import { Response } from "../network/response.js";
|
|
19
19
|
import type { NetworkMatch } from "../network/network-wait.js";
|
|
@@ -25,10 +25,10 @@ import { type PageEventName, type PageEvents, type WaitableEventName, type WaitF
|
|
|
25
25
|
export type { PageEventName, WaitableEventName, WaitForEventOptions, WaitOptions, };
|
|
26
26
|
import { type PageSettings, type ReadinessState } from "./page-settings.js";
|
|
27
27
|
export type { PageSettings, ReadinessState };
|
|
28
|
-
import {
|
|
28
|
+
import type { Viewport } from "./viewport.js";
|
|
29
29
|
import type { PreloadHandle } from "../script/preload.js";
|
|
30
30
|
import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
31
|
-
import {
|
|
31
|
+
import type { PageFunction } from "../script/call-function.js";
|
|
32
32
|
import type { BrowsingContext } from "../types/bidi-modules/browsing-context.js";
|
|
33
33
|
export type { LoadState, NavigateOptions, NavigationResult };
|
|
34
34
|
export default class Page {
|
|
@@ -48,15 +48,12 @@ export default class Page {
|
|
|
48
48
|
private nav;
|
|
49
49
|
private net;
|
|
50
50
|
private dialogs;
|
|
51
|
-
private
|
|
52
|
-
private errorsAllowed;
|
|
51
|
+
private logging;
|
|
53
52
|
private isClosed;
|
|
54
53
|
private browserContext;
|
|
55
54
|
private transfers;
|
|
56
55
|
private evidence;
|
|
57
|
-
private
|
|
58
|
-
/** Chrome's phone layout is on (`setMobileLayout`): viewport changes keep it */
|
|
59
|
-
private mobile;
|
|
56
|
+
private viewport;
|
|
60
57
|
/** `settings` override what `samurai.config.ts` says; without them the config is read when needed */
|
|
61
58
|
constructor(biDiConnector: BiDiConnector, contextId: string, settings?: PageSettings);
|
|
62
59
|
/** The BiDi browsing context id of this page's top-level context */
|
package/dist/browser/page.js
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import Locator from "../locator/locator.js";
|
|
2
2
|
import { attributeSelector, cssSelector, labelSelector, roleSelector, testIdSelector, textSelector, } from "../locator/selector.js";
|
|
3
3
|
import EventEmitter from "node:events";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { projectDir } from "../config/config.js";
|
|
6
|
-
import { DEFAULT_VIEWPORT } from "../config/defaults.js";
|
|
7
4
|
import logger from "../logger/index.js";
|
|
8
5
|
import { ContextTree } from "./context-tree.js";
|
|
9
6
|
import { printPdf } from "./pdf.js";
|
|
10
7
|
import { applyUse } from "./use-commands.js";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { PageLogs } from "./page-logs.js";
|
|
8
|
+
import { screenshotWithEvidence, } from "./screenshot.js";
|
|
9
|
+
import { PageLogging } from "./page-logging.js";
|
|
14
10
|
import { Lazy } from "./lazy.js";
|
|
15
11
|
import { Response } from "../network/response.js";
|
|
16
12
|
import { traverseHistory } from "./history.js";
|
|
@@ -24,13 +20,12 @@ import { grantPermission } from "./page-permissions.js";
|
|
|
24
20
|
import { PageLocators } from "./page-locators.js";
|
|
25
21
|
import Keyboard from "./keyboard.js";
|
|
26
22
|
import Mouse from "./mouse.js";
|
|
27
|
-
import {
|
|
23
|
+
import { startSources, waitForEventOn, waitForPageEvent, } from "./page-events.js";
|
|
28
24
|
import { PageConfig, } from "./page-settings.js";
|
|
29
|
-
import {
|
|
25
|
+
import { PageViewport } from "./page-viewport.js";
|
|
30
26
|
import { HelperRealm } from "../locator/helper-realm.js";
|
|
31
27
|
import { BiDiError } from "../transport/bidi-error.js";
|
|
32
|
-
import {
|
|
33
|
-
import { readContent } from "./page-content.js";
|
|
28
|
+
import { readContent, readTitle, readUrl } from "./page-content.js";
|
|
34
29
|
import { evaluateInPage } from "./page-evaluate.js";
|
|
35
30
|
export default class Page {
|
|
36
31
|
/** `settings` override what `samurai.config.ts` says; without them the config is read when needed */
|
|
@@ -38,12 +33,7 @@ export default class Page {
|
|
|
38
33
|
this.lazyTree = new Lazy();
|
|
39
34
|
this.initScripts = new InitScripts();
|
|
40
35
|
this.pageEvents = new EventEmitter();
|
|
41
|
-
this.lazyLogs = new Lazy();
|
|
42
|
-
this.errorsAllowed = false;
|
|
43
36
|
this.isClosed = false;
|
|
44
|
-
this.knownViewport = null;
|
|
45
|
-
/** Chrome's phone layout is on (`setMobileLayout`): viewport changes keep it */
|
|
46
|
-
this.mobile = false;
|
|
47
37
|
this.biDiConnector = biDiConnector;
|
|
48
38
|
this.id = contextId;
|
|
49
39
|
this.pageConfig = new PageConfig(settings);
|
|
@@ -52,6 +42,8 @@ export default class Page {
|
|
|
52
42
|
this.keyboard = new Keyboard(biDiConnector, contextId, () => this.assertOpen());
|
|
53
43
|
this.mouse = new Mouse(biDiConnector, contextId, () => this.assertOpen());
|
|
54
44
|
this.documents = new DocumentWatcher(biDiConnector, contextId);
|
|
45
|
+
this.viewport = new PageViewport(biDiConnector, contextId, this.pageConfig);
|
|
46
|
+
this.logging = new PageLogging(biDiConnector, contextId, () => this.tree(), this.pageEvents);
|
|
55
47
|
this.net = new PageNetwork(biDiConnector, contextId, {
|
|
56
48
|
config: this.pageConfig,
|
|
57
49
|
tree: { ensure: () => this.tree(), peek: () => this.lazyTree.peek() },
|
|
@@ -166,7 +158,7 @@ export default class Page {
|
|
|
166
158
|
this.net.disposeTracker(),
|
|
167
159
|
]);
|
|
168
160
|
const tree = this.lazyTree.take();
|
|
169
|
-
const pageLogs = this.
|
|
161
|
+
const pageLogs = this.logging.take();
|
|
170
162
|
results.push(...(await Promise.allSettled([
|
|
171
163
|
tree?.then((resolved) => resolved.dispose()),
|
|
172
164
|
this.documents.dispose(),
|
|
@@ -228,7 +220,7 @@ export default class Page {
|
|
|
228
220
|
/** The URL of the page now */
|
|
229
221
|
async url() {
|
|
230
222
|
this.assertOpen();
|
|
231
|
-
return
|
|
223
|
+
return readUrl(this.biDiConnector, this.id);
|
|
232
224
|
}
|
|
233
225
|
/**
|
|
234
226
|
* Waits until the URL matches `url` (a string equals it, or with `*` / `**` is a glob; a RegExp is tested on it;
|
|
@@ -257,7 +249,7 @@ export default class Page {
|
|
|
257
249
|
/** The title of the document now */
|
|
258
250
|
async title() {
|
|
259
251
|
this.assertOpen();
|
|
260
|
-
return
|
|
252
|
+
return readTitle(this.biDiConnector, this.id);
|
|
261
253
|
}
|
|
262
254
|
/**
|
|
263
255
|
* Runs `fn` in the page, with `arg` as its only argument, and returns the result (a promise is awaited). `fn` is a function,
|
|
@@ -280,16 +272,11 @@ export default class Page {
|
|
|
280
272
|
*/
|
|
281
273
|
async setViewport(viewport) {
|
|
282
274
|
this.assertOpen();
|
|
283
|
-
await
|
|
284
|
-
this.knownViewport = viewport && {
|
|
285
|
-
width: viewport.width,
|
|
286
|
-
height: viewport.height,
|
|
287
|
-
};
|
|
275
|
+
await this.viewport.set(viewport);
|
|
288
276
|
}
|
|
289
277
|
/** @internal Chrome: lays this page out like a phone from now on (`use.isMobile`), including the size it has now */
|
|
290
278
|
async setMobileLayout() {
|
|
291
|
-
this.
|
|
292
|
-
await this.setViewport(this.knownViewport);
|
|
279
|
+
await this.viewport.setMobileLayout();
|
|
293
280
|
}
|
|
294
281
|
/** Playwright's name for `setViewport` */
|
|
295
282
|
setViewportSize(size) {
|
|
@@ -300,14 +287,11 @@ export default class Page {
|
|
|
300
287
|
* browser opened it), read at once; null before one is known, after `setViewport(null)` and when config turns the viewport off.
|
|
301
288
|
*/
|
|
302
289
|
viewportSize() {
|
|
303
|
-
return this.
|
|
290
|
+
return this.viewport.size();
|
|
304
291
|
}
|
|
305
292
|
/** Gives the page the size from config `use.viewport` (1280x720 by default); the browser does this when it opens a page */
|
|
306
293
|
async applyDefaultViewport() {
|
|
307
|
-
|
|
308
|
-
if (viewport === null)
|
|
309
|
-
return;
|
|
310
|
-
await this.setViewport(viewport ?? DEFAULT_VIEWPORT);
|
|
294
|
+
await this.viewport.applyDefault();
|
|
311
295
|
}
|
|
312
296
|
/**
|
|
313
297
|
* What the browser sets up on every page it opens: the default size, then everything that has to be listening
|
|
@@ -328,12 +312,7 @@ export default class Page {
|
|
|
328
312
|
*/
|
|
329
313
|
async screenshot(options = {}) {
|
|
330
314
|
this.assertOpen();
|
|
331
|
-
|
|
332
|
-
if (options.name !== undefined) {
|
|
333
|
-
const evidence = this.evidence ?? new Evidence(path.join(projectDir(), "result"));
|
|
334
|
-
await writeOutput(evidence.fileFor(options.name, options.type).file, image);
|
|
335
|
-
}
|
|
336
|
-
return image;
|
|
315
|
+
return screenshotWithEvidence(this.biDiConnector, this.id, options, this.evidence);
|
|
337
316
|
}
|
|
338
317
|
/** @internal The runner gives each test its own evidence folder */
|
|
339
318
|
setEvidence(evidence) {
|
|
@@ -474,7 +453,7 @@ export default class Page {
|
|
|
474
453
|
network: () => this.startNetworkTracking(),
|
|
475
454
|
bodies: () => this.startBodyCollection(),
|
|
476
455
|
};
|
|
477
|
-
|
|
456
|
+
startSources(event, start, (err) => logger.error("Could not start listening for %s", event, { err }));
|
|
478
457
|
}
|
|
479
458
|
/** Starts following downloads; the browser does this when it opens a page, `page.on("download")` and `waitForDownload` when needed */
|
|
480
459
|
startDownloadTracking() {
|
|
@@ -515,24 +494,18 @@ export default class Page {
|
|
|
515
494
|
* await chooser.setFiles("./myfile.pdf");
|
|
516
495
|
*/
|
|
517
496
|
async waitForEvent(event, optionsOrPredicate = {}) {
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
:
|
|
521
|
-
|
|
522
|
-
switch (event) {
|
|
523
|
-
case "request":
|
|
524
|
-
return this.waitForRequest(accepts, options);
|
|
525
|
-
case "response":
|
|
526
|
-
return this.waitForResponse(accepts, options);
|
|
527
|
-
case "download":
|
|
497
|
+
return waitForEventOn({
|
|
498
|
+
request: (accepts, options) => this.waitForRequest(accepts, options),
|
|
499
|
+
response: (accepts, options) => this.waitForResponse(accepts, options),
|
|
500
|
+
download: (options, accepts) => {
|
|
528
501
|
this.assertOpen();
|
|
529
502
|
return this.transfers.waitForDownload(options, accepts);
|
|
530
|
-
|
|
503
|
+
},
|
|
504
|
+
filechooser: (options, accepts) => {
|
|
531
505
|
this.assertOpen();
|
|
532
506
|
return this.transfers.waitForFileChooser(options, accepts);
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
}
|
|
507
|
+
},
|
|
508
|
+
}, event, optionsOrPredicate);
|
|
536
509
|
}
|
|
537
510
|
/**
|
|
538
511
|
* Starts keeping response bodies, which is what makes `response.body()` possible: the browser only keeps a body
|
|
@@ -597,27 +570,22 @@ export default class Page {
|
|
|
597
570
|
* early output is kept; `page.on("console" | "pageerror")` starts it when needed.
|
|
598
571
|
*/
|
|
599
572
|
async startLogging() {
|
|
600
|
-
await this.
|
|
601
|
-
const logs = await PageLogs.start(this.biDiConnector, await this.tree(), this.id);
|
|
602
|
-
logs.on("console", (message) => this.pageEvents.emit("console", message));
|
|
603
|
-
logs.on("pageerror", (error) => this.pageEvents.emit("pageerror", error));
|
|
604
|
-
return logs;
|
|
605
|
-
});
|
|
573
|
+
await this.logging.start();
|
|
606
574
|
}
|
|
607
575
|
/** The newest console and error entries of this page (empty until logging started), oldest first */
|
|
608
576
|
getLogs() {
|
|
609
|
-
return this.
|
|
577
|
+
return this.logging.entries();
|
|
610
578
|
}
|
|
611
579
|
/** Uncaught exceptions of the page since logging started or `clearLogs()` */
|
|
612
580
|
pageErrors() {
|
|
613
|
-
return this.
|
|
581
|
+
return this.logging.errors();
|
|
614
582
|
}
|
|
615
583
|
/** How many entries fell out of the page's log buffer */
|
|
616
584
|
get logsDropped() {
|
|
617
|
-
return this.
|
|
585
|
+
return this.logging.dropped;
|
|
618
586
|
}
|
|
619
587
|
clearLogs() {
|
|
620
|
-
this.
|
|
588
|
+
this.logging.clear();
|
|
621
589
|
}
|
|
622
590
|
/** Resolves once everything the page logged before the call has arrived: events and replies share one ordered connection */
|
|
623
591
|
async syncLogs() {
|
|
@@ -625,10 +593,10 @@ export default class Page {
|
|
|
625
593
|
}
|
|
626
594
|
/** Tells the runner not to fail this test on uncaught page exceptions (config `logs.failOnPageError`) */
|
|
627
595
|
allowPageErrors() {
|
|
628
|
-
this.
|
|
596
|
+
this.logging.allowErrors();
|
|
629
597
|
}
|
|
630
598
|
get pageErrorsAllowed() {
|
|
631
|
-
return this.
|
|
599
|
+
return this.logging.errorsAreAllowed;
|
|
632
600
|
}
|
|
633
601
|
/** Starts watching for dialogs; the browser does this when it opens a page, `page.on("dialog")` when needed */
|
|
634
602
|
startDialogHandling() {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
2
|
+
import { Evidence } from "./evidence.js";
|
|
2
3
|
export interface ScreenshotOptions {
|
|
3
4
|
/** Also write the image here (folders are created) */
|
|
4
5
|
path?: string;
|
|
@@ -29,3 +30,8 @@ export declare function takeScreenshot(connector: BiDiConnector, context: string
|
|
|
29
30
|
}): Promise<Buffer>;
|
|
30
31
|
/** Writes `data` to `file`, creating the folders; nothing happens without a path */
|
|
31
32
|
export declare function writeOutput(file: string | undefined, data: Buffer): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* `page.screenshot`: `takeScreenshot`, then, with `options.name`, a copy in the evidence folder (the test's own,
|
|
35
|
+
* or `result/` of the project when the page has none)
|
|
36
|
+
*/
|
|
37
|
+
export declare function screenshotWithEvidence(connector: BiDiConnector, context: string, options: ScreenshotOptions, evidence: Evidence | undefined): Promise<Buffer>;
|
|
@@ -2,6 +2,8 @@ import { mkdir, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { callFunction } from "../script/call-function.js";
|
|
4
4
|
import { ElementHandle } from "../script/element-handle.js";
|
|
5
|
+
import { projectDir } from "../config/config.js";
|
|
6
|
+
import { Evidence } from "./evidence.js";
|
|
5
7
|
const MIME = { png: "image/png", jpeg: "image/jpeg" };
|
|
6
8
|
/** The `format` of a capture command; validates the type/quality combination */
|
|
7
9
|
function imageFormat({ type, quality, }) {
|
|
@@ -57,3 +59,15 @@ export async function writeOutput(file, data) {
|
|
|
57
59
|
await mkdir(path.dirname(file), { recursive: true });
|
|
58
60
|
await writeFile(file, data);
|
|
59
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* `page.screenshot`: `takeScreenshot`, then, with `options.name`, a copy in the evidence folder (the test's own,
|
|
64
|
+
* or `result/` of the project when the page has none)
|
|
65
|
+
*/
|
|
66
|
+
export async function screenshotWithEvidence(connector, context, options, evidence) {
|
|
67
|
+
const image = await takeScreenshot(connector, context, options);
|
|
68
|
+
if (options.name !== undefined) {
|
|
69
|
+
const folder = evidence ?? new Evidence(path.join(projectDir(), "result"));
|
|
70
|
+
await writeOutput(folder.fileFor(options.name, options.type).file, image);
|
|
71
|
+
}
|
|
72
|
+
return image;
|
|
73
|
+
}
|
package/dist/config/config.js
CHANGED
|
@@ -2,7 +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 { validateGroupUse } from "./group-use.js";
|
|
5
|
+
import { validateEnvironmentUse, validateGroupUse } from "./group-use.js";
|
|
6
6
|
import { validateUseConfig } from "./use-options.js";
|
|
7
7
|
import { validateForbidOnlyConfig } from "./forbid-only.js";
|
|
8
8
|
import { validateRetriesConfig } from "./retries.js";
|
|
@@ -37,6 +37,7 @@ export function validateConfig(config) {
|
|
|
37
37
|
validateFullyParallelConfig(config);
|
|
38
38
|
validateUseConfig(config);
|
|
39
39
|
validateGroupUse(config);
|
|
40
|
+
validateEnvironmentUse(config);
|
|
40
41
|
validateWebServerConfig(config);
|
|
41
42
|
}
|
|
42
43
|
/** The whole `samurai.config.ts` of the project (the working directory unless a project is active) */
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** The key of `use` that attaches fixtures by name (`true`) or removes them (`false`) */
|
|
2
|
+
export declare const FIXTURES_KEY = "fixtures";
|
|
3
|
+
/** Whether `key` of a `use` object is about fixtures: `fixtures`, or an option fixture (`orderOptions`) */
|
|
4
|
+
export declare function isFixtureUseKey(key: string): boolean;
|
|
5
|
+
/** The fixture keys of one `use` layer */
|
|
6
|
+
export declare function pickFixtureUse(layer: object | undefined): Record<string, unknown>;
|
|
7
|
+
/** The layer without its fixture keys: what is left is for the browser */
|
|
8
|
+
export declare function omitFixtureUse<T extends object>(layer: T | undefined): T;
|
|
9
|
+
/** Checks the fixture keys of one `use` layer; throws `use(): what is wrong` */
|
|
10
|
+
export declare function checkFixtureUse(options: Record<string, unknown>): void;
|
|
11
|
+
/** What the fixture keys of the layers come to, later layers winning key by key */
|
|
12
|
+
export interface MergedFixtureUse {
|
|
13
|
+
/** Fixture name to attached (`true`) or removed (`false`) */
|
|
14
|
+
fixtures: Record<string, boolean>;
|
|
15
|
+
/** Option fixture name to its values from `use`, over the fixture's own default */
|
|
16
|
+
options: Record<string, Record<string, unknown>>;
|
|
17
|
+
}
|
|
18
|
+
/** Merges the fixture keys of the `use` layers in order (config, environment, group, `test.use()`) */
|
|
19
|
+
export declare function mergeFixtureUse(layers: readonly object[]): MergedFixtureUse;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** The key of `use` that attaches fixtures by name (`true`) or removes them (`false`) */
|
|
2
|
+
export const FIXTURES_KEY = "fixtures";
|
|
3
|
+
const OPTION_FIXTURE_KEY = /^[a-z][A-Za-z0-9]*Options$/;
|
|
4
|
+
/** Whether `key` of a `use` object is about fixtures: `fixtures`, or an option fixture (`orderOptions`) */
|
|
5
|
+
export function isFixtureUseKey(key) {
|
|
6
|
+
return key === FIXTURES_KEY || OPTION_FIXTURE_KEY.test(key);
|
|
7
|
+
}
|
|
8
|
+
/** The fixture keys of one `use` layer */
|
|
9
|
+
export function pickFixtureUse(layer) {
|
|
10
|
+
return Object.fromEntries(Object.entries(layer ?? {}).filter(([key]) => isFixtureUseKey(key)));
|
|
11
|
+
}
|
|
12
|
+
/** The layer without its fixture keys: what is left is for the browser */
|
|
13
|
+
export function omitFixtureUse(layer) {
|
|
14
|
+
return Object.fromEntries(Object.entries(layer ?? {}).filter(([key]) => !isFixtureUseKey(key)));
|
|
15
|
+
}
|
|
16
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17
|
+
/** Checks the fixture keys of one `use` layer; throws `use(): what is wrong` */
|
|
18
|
+
export function checkFixtureUse(options) {
|
|
19
|
+
const attached = options[FIXTURES_KEY];
|
|
20
|
+
if (attached !== undefined) {
|
|
21
|
+
if (!isObject(attached))
|
|
22
|
+
throw new TypeError("use(): fixtures must be an object of fixture names and true or false");
|
|
23
|
+
for (const [name, on] of Object.entries(attached))
|
|
24
|
+
if (typeof on !== "boolean")
|
|
25
|
+
throw new TypeError(`use(): fixtures.${name} must be true or false, got ${JSON.stringify(on)}`);
|
|
26
|
+
}
|
|
27
|
+
for (const [key, value] of Object.entries(options))
|
|
28
|
+
if (key !== FIXTURES_KEY && isFixtureUseKey(key) && !isObject(value))
|
|
29
|
+
throw new TypeError(`use(): ${key} must be an object`);
|
|
30
|
+
}
|
|
31
|
+
/** Merges the fixture keys of the `use` layers in order (config, environment, group, `test.use()`) */
|
|
32
|
+
export function mergeFixtureUse(layers) {
|
|
33
|
+
const merged = { fixtures: {}, options: {} };
|
|
34
|
+
for (const layer of layers)
|
|
35
|
+
for (const [key, value] of Object.entries(pickFixtureUse(layer)))
|
|
36
|
+
if (key === FIXTURES_KEY)
|
|
37
|
+
Object.assign(merged.fixtures, value);
|
|
38
|
+
else
|
|
39
|
+
merged.options[key] = {
|
|
40
|
+
...merged.options[key],
|
|
41
|
+
...value,
|
|
42
|
+
};
|
|
43
|
+
return merged;
|
|
44
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { GroupUse, SamuraiGroup, SamuraiTestConfig } from "../types/config.js";
|
|
2
2
|
/**
|
|
3
|
-
* Checks the `use` of every group of the config: the options of config `use` (`test.use()` ones, and `viewport`)
|
|
4
|
-
* Throws `config.groups[1].use.locale: what is wrong`.
|
|
3
|
+
* Checks the `use` of every group of the config: the options of config `use` (`test.use()` ones, and `viewport`),
|
|
4
|
+
* and the fixture keys. Throws `config.groups[1].use.locale: what is wrong`.
|
|
5
5
|
*/
|
|
6
6
|
export declare function validateGroupUse(config: SamuraiTestConfig): void;
|
|
7
|
+
/** Checks the `use` of every environment the same way: `config.environments.production.use.locale: what is wrong` */
|
|
8
|
+
export declare function validateEnvironmentUse(config: SamuraiTestConfig): void;
|
|
7
9
|
/** The `use` of the group called `name`; nothing when there is no such group or it has none */
|
|
8
10
|
export declare function groupUseOf(groups: readonly SamuraiGroup[] | undefined, name: string): GroupUse;
|