@itmetsam/samurai-framework 0.22.0 → 0.22.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/api.d.ts +3 -3
  2. package/dist/browser/page-content.d.ts +4 -0
  3. package/dist/browser/page-content.js +12 -0
  4. package/dist/browser/page-events.d.ts +11 -0
  5. package/dist/browser/page-events.js +23 -0
  6. package/dist/browser/page-logging.d.ts +24 -0
  7. package/dist/browser/page-logging.js +43 -0
  8. package/dist/browser/page-settings.d.ts +1 -1
  9. package/dist/browser/page-viewport.d.ts +21 -0
  10. package/dist/browser/page-viewport.js +33 -0
  11. package/dist/browser/page.d.ts +6 -9
  12. package/dist/browser/page.js +31 -63
  13. package/dist/browser/screenshot.d.ts +6 -0
  14. package/dist/browser/screenshot.js +14 -0
  15. package/dist/capabilities.d.ts +16 -2
  16. package/dist/capabilities.js +16 -2
  17. package/dist/config/config.js +2 -1
  18. package/dist/config/fixture-use.d.ts +19 -0
  19. package/dist/config/fixture-use.js +44 -0
  20. package/dist/config/group-use.d.ts +4 -2
  21. package/dist/config/group-use.js +30 -20
  22. package/dist/config/use-options.js +5 -1
  23. package/dist/recorder/capture.d.ts +3 -0
  24. package/dist/recorder/capture.js +18 -1
  25. package/dist/recorder/normalise.d.ts +12 -1
  26. package/dist/recorder/normalise.js +29 -1
  27. package/dist/recorder/page-messages.d.ts +6 -2
  28. package/dist/recorder/page-messages.js +29 -21
  29. package/dist/recorder/toolbar/inspect.js +10 -3
  30. package/dist/runner/fixture-defs.d.ts +2 -1
  31. package/dist/runner/fixture-defs.js +51 -18
  32. package/dist/runner/fixture-meta.d.ts +20 -0
  33. package/dist/runner/fixture-meta.js +109 -0
  34. package/dist/runner/fixture-options.d.ts +8 -0
  35. package/dist/runner/fixture-options.js +35 -0
  36. package/dist/runner/fixture-outputs.d.ts +11 -0
  37. package/dist/runner/fixture-outputs.js +28 -0
  38. package/dist/runner/fixture-registry.d.ts +35 -0
  39. package/dist/runner/fixture-registry.js +73 -0
  40. package/dist/runner/fixture-scope.d.ts +39 -9
  41. package/dist/runner/fixture-scope.js +139 -70
  42. package/dist/runner/fixture-start.d.ts +27 -0
  43. package/dist/runner/fixture-start.js +76 -0
  44. package/dist/runner/fixture-use.d.ts +23 -0
  45. package/dist/runner/fixture-use.js +64 -0
  46. package/dist/runner/fixture-worker.d.ts +26 -0
  47. package/dist/runner/fixture-worker.js +46 -0
  48. package/dist/runner/prepare-run.js +5 -0
  49. package/dist/runner/registry.js +15 -5
  50. package/dist/runner/reporter.d.ts +3 -1
  51. package/dist/runner/reporter.js +1 -0
  52. package/dist/runner/run-test.d.ts +3 -0
  53. package/dist/runner/run-test.js +16 -1
  54. package/dist/runner/run.d.ts +7 -0
  55. package/dist/runner/run.js +9 -0
  56. package/dist/runner/test-function.d.ts +2 -2
  57. package/dist/runner/test-runner.js +6 -1
  58. package/dist/runner/use-options.d.ts +16 -2
  59. package/dist/runner/use-options.js +28 -8
  60. package/dist/runner/worker-session.js +6 -0
  61. package/dist/steps/build.js +49 -25
  62. package/dist/steps/emit.js +64 -7
  63. package/dist/steps/index.d.ts +1 -1
  64. package/dist/steps/model.d.ts +60 -10
  65. package/dist/steps/parse/call.js +125 -39
  66. package/dist/steps/parse/expectation.d.ts +2 -1
  67. package/dist/steps/parse/expectation.js +30 -8
  68. package/dist/steps/parse/literals.d.ts +9 -0
  69. package/dist/steps/parse/literals.js +42 -0
  70. package/dist/steps/parse/step.js +28 -1
  71. package/dist/types/config.d.ts +35 -22
  72. package/dist/types/extend.d.ts +54 -2
  73. package/dist/types/test.d.ts +24 -0
  74. package/dist/types/use.d.ts +14 -0
  75. 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
+ }
@@ -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 { type LogRecord, type PageError } from "./page-logs.js";
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 { type Viewport } from "./viewport.js";
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 { type PageFunction } from "../script/call-function.js";
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 lazyLogs;
52
- private errorsAllowed;
51
+ private logging;
53
52
  private isClosed;
54
53
  private browserContext;
55
54
  private transfers;
56
55
  private evidence;
57
- private knownViewport;
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 */
@@ -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 { takeScreenshot, writeOutput, } from "./screenshot.js";
12
- import { Evidence } from "./evidence.js";
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 { sourcesFor, waitForPageEvent, } from "./page-events.js";
23
+ import { startSources, waitForEventOn, waitForPageEvent, } from "./page-events.js";
28
24
  import { PageConfig, } from "./page-settings.js";
29
- import { applyViewport } from "./viewport.js";
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 { callFunction } from "../script/call-function.js";
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.lazyLogs.take();
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 callFunction(this.biDiConnector, this.id, "() => location.href", [], { awaitPromise: false });
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 callFunction(this.biDiConnector, this.id, "() => document.title", [], { awaitPromise: false });
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 applyViewport(this.biDiConnector, this.id, viewport, this.mobile);
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.mobile = true;
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.knownViewport && { ...this.knownViewport };
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
- const { viewport } = await this.pageConfig.use();
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
- const image = await takeScreenshot(this.biDiConnector, this.id, options);
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
- Promise.all(sourcesFor(event).map((source) => start[source]())).catch((err) => logger.error("Could not start listening for %s", event, { err }));
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
- const { predicate, ...options } = typeof optionsOrPredicate === "function"
519
- ? { predicate: optionsOrPredicate }
520
- : optionsOrPredicate;
521
- const accepts = (predicate ?? (() => true));
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
- case "filechooser":
503
+ },
504
+ filechooser: (options, accepts) => {
531
505
  this.assertOpen();
532
506
  return this.transfers.waitForFileChooser(options, accepts);
533
- default:
534
- throw new Error(`waitForEvent(): "${String(event)}" is not supported; use page.on()`);
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.lazyLogs.get(async () => {
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.lazyLogs.peek()?.entries ?? [];
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.lazyLogs.peek()?.errors ?? [];
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.lazyLogs.peek()?.dropped ?? 0;
585
+ return this.logging.dropped;
618
586
  }
619
587
  clearLogs() {
620
- this.lazyLogs.peek()?.clear();
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.errorsAllowed = true;
596
+ this.logging.allowErrors();
629
597
  }
630
598
  get pageErrorsAllowed() {
631
- return this.errorsAllowed;
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
+ }
@@ -38,8 +38,12 @@ export declare const CAPABILITIES: {
38
38
  readonly flowNodeFolders: 2;
39
39
  /** `nodeFolder(nodeId, browser)` from `./flows/run`: a flow node's folder under `outputDir` (version 2 takes the browser). */
40
40
  readonly nodeFolder: 2;
41
- /** Assertion matchers version 2: `toBeHidden`, `toBeEnabled`, `toBeChecked`, `toHaveClass` and `toHaveCSS`, in the framework and the step codec. */
42
- readonly assertionMatchers: 2;
41
+ /**
42
+ * Assertion matchers in the framework and the step codec. Version 2: `toBeHidden`, `toBeEnabled`, `toBeChecked`,
43
+ * `toHaveClass` and `toHaveCSS`. Version 3 adds `toBeFocused`, `toBeDisabled`, `toBeEditable`, `toBeAttached`,
44
+ * `toBeInViewport`, `toBeEmpty`, `toHaveId`, `toHaveAccessibleName` and `toHaveAccessibleDescription` to the codec.
45
+ */
46
+ readonly assertionMatchers: 3;
43
47
  /** `expect(page).toHaveURL` and `toHaveTitle`, and the `expectPage` step in the step codec. */
44
48
  readonly pageAssertions: true;
45
49
  /**
@@ -50,6 +54,16 @@ export declare const CAPABILITIES: {
50
54
  readonly testInputsOutputs: true;
51
55
  /** A flow runs once per browser: `browser` on every flow event, `flow-done`, `runFlowInProject` resolving to `FlowRunResult`. */
52
56
  readonly flowPerBrowser: true;
57
+ /** The codec's `check`, `uncheck`, `selectOption`, `hover`, `dblclick` and `clear` steps. */
58
+ readonly actionSteps: true;
59
+ /**
60
+ * The codec's locator forms: `exact` and RegExp text and name, `getByPlaceholder` / `getByAltText` / `getByTitle`, the
61
+ * `getByRole` options `level`, `checked`, `pressed`, `expanded`, `selected`, `disabled` and `includeHidden`, and the
62
+ * `nth`, `first`, `last` and `filter({ hasText })` calls. `LocatorCall.text` and `.name` can be a RegExp.
63
+ */
64
+ readonly locatorForms: true;
65
+ /** The recorder writes a `check` / `uncheck` for a checkbox or radio click and a `selectOption` for a `<select>` choice. */
66
+ readonly recorderActionSteps: true;
53
67
  };
54
68
  /** The shape of {@link CAPABILITIES}. */
55
69
  export type Capabilities = typeof CAPABILITIES;
@@ -38,8 +38,12 @@ export const CAPABILITIES = {
38
38
  flowNodeFolders: 2,
39
39
  /** `nodeFolder(nodeId, browser)` from `./flows/run`: a flow node's folder under `outputDir` (version 2 takes the browser). */
40
40
  nodeFolder: 2,
41
- /** Assertion matchers version 2: `toBeHidden`, `toBeEnabled`, `toBeChecked`, `toHaveClass` and `toHaveCSS`, in the framework and the step codec. */
42
- assertionMatchers: 2,
41
+ /**
42
+ * Assertion matchers in the framework and the step codec. Version 2: `toBeHidden`, `toBeEnabled`, `toBeChecked`,
43
+ * `toHaveClass` and `toHaveCSS`. Version 3 adds `toBeFocused`, `toBeDisabled`, `toBeEditable`, `toBeAttached`,
44
+ * `toBeInViewport`, `toBeEmpty`, `toHaveId`, `toHaveAccessibleName` and `toHaveAccessibleDescription` to the codec.
45
+ */
46
+ assertionMatchers: 3,
43
47
  /** `expect(page).toHaveURL` and `toHaveTitle`, and the `expectPage` step in the step codec. */
44
48
  pageAssertions: true,
45
49
  /**
@@ -50,4 +54,14 @@ export const CAPABILITIES = {
50
54
  testInputsOutputs: true,
51
55
  /** A flow runs once per browser: `browser` on every flow event, `flow-done`, `runFlowInProject` resolving to `FlowRunResult`. */
52
56
  flowPerBrowser: true,
57
+ /** The codec's `check`, `uncheck`, `selectOption`, `hover`, `dblclick` and `clear` steps. */
58
+ actionSteps: true,
59
+ /**
60
+ * The codec's locator forms: `exact` and RegExp text and name, `getByPlaceholder` / `getByAltText` / `getByTitle`, the
61
+ * `getByRole` options `level`, `checked`, `pressed`, `expanded`, `selected`, `disabled` and `includeHidden`, and the
62
+ * `nth`, `first`, `last` and `filter({ hasText })` calls. `LocatorCall.text` and `.name` can be a RegExp.
63
+ */
64
+ locatorForms: true,
65
+ /** The recorder writes a `check` / `uncheck` for a checkbox or radio click and a `selectOption` for a `<select>` choice. */
66
+ recorderActionSteps: true,
53
67
  };
@@ -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;