@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.
Files changed (112) hide show
  1. package/dist/api.d.ts +3 -1
  2. package/dist/assert/asymmetric.d.ts +29 -0
  3. package/dist/assert/asymmetric.js +166 -0
  4. package/dist/assert/element-probes.d.ts +20 -0
  5. package/dist/assert/element-probes.js +45 -0
  6. package/dist/assert/expect.d.ts +20 -9
  7. package/dist/assert/expect.js +12 -4
  8. package/dist/assert/locator-assertion-base.d.ts +52 -0
  9. package/dist/assert/locator-assertion-base.js +78 -0
  10. package/dist/assert/locator-assertions.d.ts +26 -49
  11. package/dist/assert/locator-assertions.js +35 -78
  12. package/dist/assert/more-locator-assertions.d.ts +30 -0
  13. package/dist/assert/more-locator-assertions.js +86 -0
  14. package/dist/assert/retry-assertions.d.ts +28 -0
  15. package/dist/assert/retry-assertions.js +113 -0
  16. package/dist/assert/text-match.d.ts +2 -0
  17. package/dist/assert/text-match.js +8 -0
  18. package/dist/assert/value-assertions.d.ts +29 -1
  19. package/dist/assert/value-assertions.js +84 -5
  20. package/dist/assert/value-matchers.d.ts +19 -0
  21. package/dist/assert/value-matchers.js +53 -0
  22. package/dist/browser/browser.d.ts +9 -0
  23. package/dist/browser/browser.js +25 -5
  24. package/dist/browser/cdp.d.ts +11 -0
  25. package/dist/browser/cdp.js +28 -0
  26. package/dist/browser/chrome-launch.d.ts +4 -0
  27. package/dist/browser/chrome-launch.js +2 -2
  28. package/dist/browser/index.d.ts +5 -0
  29. package/dist/browser/index.js +4 -0
  30. package/dist/browser/launch-firefox.js +1 -1
  31. package/dist/browser/launch-flags.d.ts +4 -1
  32. package/dist/browser/launch-flags.js +18 -5
  33. package/dist/browser/navigation-error.d.ts +5 -1
  34. package/dist/browser/navigation-error.js +10 -2
  35. package/dist/browser/page-content.d.ts +5 -0
  36. package/dist/browser/page-content.js +43 -0
  37. package/dist/browser/page-evaluate.d.ts +9 -0
  38. package/dist/browser/page-evaluate.js +20 -0
  39. package/dist/browser/page-locators.d.ts +1 -1
  40. package/dist/browser/page-locators.js +1 -1
  41. package/dist/browser/page-navigation.d.ts +18 -0
  42. package/dist/browser/page-navigation.js +52 -2
  43. package/dist/browser/page-settings.d.ts +1 -1
  44. package/dist/browser/page.d.ts +52 -1
  45. package/dist/browser/page.js +66 -2
  46. package/dist/browser/session-capabilities.d.ts +10 -0
  47. package/dist/browser/session-capabilities.js +11 -0
  48. package/dist/browser/session.d.ts +2 -1
  49. package/dist/browser/session.js +3 -3
  50. package/dist/browser/storage-state.d.ts +6 -0
  51. package/dist/browser/storage-state.js +37 -0
  52. package/dist/browser/use-applier.d.ts +27 -0
  53. package/dist/browser/use-applier.js +94 -0
  54. package/dist/browser/viewport.d.ts +3 -2
  55. package/dist/browser/viewport.js +15 -2
  56. package/dist/config/config.js +2 -0
  57. package/dist/config/use-options.d.ts +16 -0
  58. package/dist/config/use-options.js +128 -0
  59. package/dist/locator/element-access.d.ts +2 -0
  60. package/dist/locator/element-reads.d.ts +17 -0
  61. package/dist/locator/element-reads.js +31 -0
  62. package/dist/locator/evaluate.d.ts +12 -0
  63. package/dist/locator/evaluate.js +36 -0
  64. package/dist/locator/locator.d.ts +23 -0
  65. package/dist/locator/locator.js +36 -1
  66. package/dist/logger/warn-once.d.ts +4 -0
  67. package/dist/logger/warn-once.js +13 -0
  68. package/dist/runner/attachments.d.ts +16 -0
  69. package/dist/runner/attachments.js +60 -0
  70. package/dist/runner/fixture-args.d.ts +15 -0
  71. package/dist/runner/fixture-args.js +95 -0
  72. package/dist/runner/fixture-defs.d.ts +22 -0
  73. package/dist/runner/fixture-defs.js +103 -0
  74. package/dist/runner/fixture-scope.d.ts +33 -0
  75. package/dist/runner/fixture-scope.js +156 -0
  76. package/dist/runner/fixtures.d.ts +34 -0
  77. package/dist/runner/fixtures.js +39 -0
  78. package/dist/runner/hooks.d.ts +6 -1
  79. package/dist/runner/hooks.js +15 -3
  80. package/dist/runner/registry.d.ts +3 -39
  81. package/dist/runner/registry.js +93 -19
  82. package/dist/runner/reporter.d.ts +7 -1
  83. package/dist/runner/reporter.js +3 -0
  84. package/dist/runner/run-test.d.ts +1 -3
  85. package/dist/runner/run-test.js +92 -34
  86. package/dist/runner/steps.d.ts +13 -0
  87. package/dist/runner/steps.js +67 -0
  88. package/dist/runner/suite.d.ts +5 -0
  89. package/dist/runner/suite.js +8 -0
  90. package/dist/runner/test-annotations.d.ts +16 -0
  91. package/dist/runner/test-annotations.js +47 -0
  92. package/dist/runner/test-function.d.ts +103 -0
  93. package/dist/runner/test-function.js +1 -0
  94. package/dist/runner/test-info.d.ts +104 -0
  95. package/dist/runner/test-info.js +169 -0
  96. package/dist/runner/test-runner.js +2 -1
  97. package/dist/runner/test-timeout.d.ts +22 -0
  98. package/dist/runner/test-timeout.js +45 -0
  99. package/dist/runner/use-options.d.ts +7 -0
  100. package/dist/runner/use-options.js +18 -0
  101. package/dist/runner/worker-session.js +2 -1
  102. package/dist/script/call-function.d.ts +7 -0
  103. package/dist/script/call-function.js +21 -3
  104. package/dist/script/element-handle.d.ts +1 -1
  105. package/dist/script/element-handle.js +1 -1
  106. package/dist/types/config.d.ts +3 -2
  107. package/dist/types/extend.d.ts +30 -0
  108. package/dist/types/test.d.ts +120 -0
  109. package/dist/types/use.d.ts +47 -0
  110. package/dist/wait/wait-until.d.ts +3 -1
  111. package/dist/wait/wait-until.js +7 -2
  112. package/package.json +1 -1
@@ -0,0 +1,53 @@
1
+ import { format } from "./assertion-error.js";
2
+ /** Calls `fn` and says whether it threw */
3
+ export function callAndCatch(fn) {
4
+ try {
5
+ fn();
6
+ return { threw: false };
7
+ }
8
+ catch (error) {
9
+ return { threw: true, error };
10
+ }
11
+ }
12
+ function messageOf(error) {
13
+ if (error instanceof Error)
14
+ return error.message;
15
+ if (typeof error === "object" && error !== null && "message" in error)
16
+ return String(error.message);
17
+ return String(error);
18
+ }
19
+ /** Whether what was thrown fits `expected`; with no expectation, throwing at all fits */
20
+ export function thrownMatches(outcome, expected) {
21
+ if (!outcome.threw)
22
+ return false;
23
+ if (expected === undefined)
24
+ return true;
25
+ if (typeof expected === "string")
26
+ return messageOf(outcome.error).includes(expected);
27
+ if (expected instanceof RegExp)
28
+ return messageOf(outcome.error).search(expected) !== -1;
29
+ if (expected instanceof Error)
30
+ return messageOf(outcome.error) === expected.message;
31
+ return outcome.error instanceof expected;
32
+ }
33
+ /** What a `toThrow` failure shows as the received value */
34
+ export function describeOutcome(outcome) {
35
+ return outcome.threw ? `threw ${format(outcome.error)}` : "did not throw";
36
+ }
37
+ /** Jest's rule: the difference is under half a unit of the last digit asked for */
38
+ export function isCloseTo(actual, expected, digits) {
39
+ if (actual === expected)
40
+ return true;
41
+ return Math.abs(expected - actual) < Math.pow(10, -digits) / 2;
42
+ }
43
+ /** The `length` of a string, array or array-like; undefined for anything else */
44
+ export function lengthOf(value) {
45
+ if (typeof value === "string")
46
+ return value.length;
47
+ if (typeof value === "object" && value !== null && "length" in value) {
48
+ const length = value.length;
49
+ if (typeof length === "number")
50
+ return length;
51
+ }
52
+ return undefined;
53
+ }
@@ -5,6 +5,8 @@ import { type BrowserProcess } from "./chrome-launch.js";
5
5
  import { BrowserContext } from "./browser-context.js";
6
6
  import type { UserPromptHandler } from "../types/bidi-modules/session.js";
7
7
  import { type EmulationOptions } from "./emulation.js";
8
+ import { UseApplier } from "./use-applier.js";
9
+ import type { UseOptions } from "../types/use.js";
8
10
  import { ChromeDownloadFolders } from "./download-folders.js";
9
11
  import { type BrowserLaunchOptions } from "./launch-flags.js";
10
12
  import type { BiDiCommands } from "../types/bidi.js";
@@ -27,6 +29,8 @@ export declare class Browser {
27
29
  readonly downloadsDir: string | undefined;
28
30
  private exitedPromise;
29
31
  private downloadFolders;
32
+ /** The `use` options of the run, once `setUse` has been called: what every context and page gets */
33
+ private use;
30
34
  constructor({ browserProc, biDiConnector, downloadsDir, downloadFolders, }: {
31
35
  browserProc: BrowserProcess;
32
36
  biDiConnector: BiDiConnector;
@@ -43,6 +47,11 @@ export declare class Browser {
43
47
  }>;
44
48
  /** Sets up a browser whose BiDi session is open: downloads folder, the first page and what it tracks */
45
49
  private static attach;
50
+ /**
51
+ * @internal The runner calls this after launch: the `use` options now apply to the default context and its pages,
52
+ * and to every context and page created later. Options passed to `newContext` win over them.
53
+ */
54
+ setUse(options: UseOptions, projectDir: string): Promise<UseApplier>;
46
55
  /** Closes the browser via BiDi browser.close, then makes sure the process has exited */
47
56
  close(): Promise<void>;
48
57
  /** Alias of close() */
@@ -6,6 +6,7 @@ import Page from "./page.js";
6
6
  import { launchChrome } from "./chrome-launch.js";
7
7
  import { BrowserContext } from "./browser-context.js";
8
8
  import { validateEmulation } from "./emulation.js";
9
+ import { UseApplier } from "./use-applier.js";
9
10
  import { prepareDownloadsDir, removeIfEmpty } from "./downloads-dir.js";
10
11
  import { ChromeDownloadFolders } from "./download-folders.js";
11
12
  import { launchFirefox } from "./launch-firefox.js";
@@ -46,7 +47,7 @@ export class Browser {
46
47
  const resolved = await projectBrowser(browserName);
47
48
  logger.debug("Found browser install location at: %s", resolved.executablePath);
48
49
  ensureBrowserProfile(browserName, launchOptions.profileDir &&
49
- path.join(launchOptions.profileDir, "user.js"));
50
+ path.join(launchOptions.profileDir, "user.js"), launchOptions.use);
50
51
  const commandTimeout = await readCommandTimeout();
51
52
  // Both launchers hand back an open BiDi session: Firefox after session.new, Chrome with the one
52
53
  // its chromedriver created
@@ -58,6 +59,7 @@ export class Browser {
58
59
  defaultLaunchOptions("chrome").profileDir,
59
60
  headless: launchOptions.headless ?? defaultLaunchOptions("chrome").headless,
60
61
  commandTimeout,
62
+ use: launchOptions.use,
61
63
  signal,
62
64
  })
63
65
  : await launchFirefox({
@@ -122,6 +124,19 @@ export class Browser {
122
124
  await page.startTracking();
123
125
  return { browser, page };
124
126
  }
127
+ /**
128
+ * @internal The runner calls this after launch: the `use` options now apply to the default context and its pages,
129
+ * and to every context and page created later. Options passed to `newContext` win over them.
130
+ */
131
+ async setUse(options, projectDir) {
132
+ const name = this.biDiConnector.browserName ?? "firefox";
133
+ const applier = new UseApplier(this.biDiConnector, name, options, projectDir);
134
+ this.use = applier;
135
+ await applier.context(this.defaultContext);
136
+ for (const page of this.defaultContext.pages())
137
+ await applier.page(page);
138
+ return applier;
139
+ }
125
140
  /** Closes the browser via BiDi browser.close, then makes sure the process has exited */
126
141
  async close() {
127
142
  await this.downloadFolders?.dispose();
@@ -163,6 +178,7 @@ export class Browser {
163
178
  const page = new Page(this.biDiConnector, createBrowserContext.context);
164
179
  this.contextFor(userContext).register(page);
165
180
  await page.startTracking();
181
+ await this.use?.page(page);
166
182
  return page;
167
183
  }
168
184
  /**
@@ -173,18 +189,22 @@ export class Browser {
173
189
  * const page = await alice.newPage();
174
190
  */
175
191
  async newContext(options = {}) {
176
- const { acceptInsecureCerts, unhandledPromptBehavior, ...emulation } = options;
192
+ const { unhandledPromptBehavior, ...emulation } = options;
193
+ const insecure = options.acceptInsecureCerts ?? this.use?.acceptInsecureCerts;
194
+ delete emulation.acceptInsecureCerts;
177
195
  // A wrong value must not leave a context behind, so it is refused before one is created
178
196
  validateEmulation(emulation);
179
197
  const { userContext } = await this.biDiConnector.send("browser.createUserContext", {
180
- ...(acceptInsecureCerts !== undefined && { acceptInsecureCerts }),
198
+ ...(insecure !== undefined && { acceptInsecureCerts: insecure }),
181
199
  ...(unhandledPromptBehavior && { unhandledPromptBehavior }),
182
200
  });
183
201
  const context = new BrowserContext(this.host, userContext, false);
184
202
  this.userContexts.set(userContext, context);
185
- if (Object.keys(emulation).length > 0) {
203
+ if (this.use || Object.keys(emulation).length > 0) {
186
204
  try {
187
- await context.emulate(emulation);
205
+ await this.use?.context(context);
206
+ if (Object.keys(emulation).length > 0)
207
+ await context.emulate(emulation);
188
208
  }
189
209
  catch (err) {
190
210
  // Firefox keeps a context in its profile until it is removed
@@ -0,0 +1,11 @@
1
+ import type { BiDiConnector } from "../transport/bidi-connection.js";
2
+ /**
3
+ * Sends a command the generated BiDi types do not list: `network.setExtraHeaders` (both browsers answer it) and
4
+ * the `goog:cdp.*` commands of Chrome
5
+ */
6
+ export declare function sendRaw(connector: BiDiConnector, method: string, params: object): Promise<any>;
7
+ /**
8
+ * Runs a Chrome DevTools Protocol command on the page `context` through the BiDi connection (`goog:cdp`, Chrome
9
+ * only). Use it where BiDi has no command yet; CDP state belongs to one page, so it is set per page.
10
+ */
11
+ export declare function cdpSend(connector: BiDiConnector, context: string, method: string, params: object): Promise<void>;
@@ -0,0 +1,28 @@
1
+ /** The CDP session of each page, per connection: `goog:cdp.getSession` attaches on every call */
2
+ const sessions = new WeakMap();
3
+ /**
4
+ * Sends a command the generated BiDi types do not list: `network.setExtraHeaders` (both browsers answer it) and
5
+ * the `goog:cdp.*` commands of Chrome
6
+ */
7
+ export function sendRaw(connector, method, params) {
8
+ return connector.send(method, params);
9
+ }
10
+ /**
11
+ * Runs a Chrome DevTools Protocol command on the page `context` through the BiDi connection (`goog:cdp`, Chrome
12
+ * only). Use it where BiDi has no command yet; CDP state belongs to one page, so it is set per page.
13
+ */
14
+ export async function cdpSend(connector, context, method, params) {
15
+ let known = sessions.get(connector);
16
+ if (!known)
17
+ sessions.set(connector, (known = new Map()));
18
+ let session = known.get(context);
19
+ if (session === undefined) {
20
+ const reply = await sendRaw(connector, "goog:cdp.getSession", { context });
21
+ known.set(context, (session = reply.session));
22
+ }
23
+ await sendRaw(connector, "goog:cdp.sendCommand", {
24
+ method,
25
+ params,
26
+ session,
27
+ });
28
+ }
@@ -1,6 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import EventEmitter from "node:events";
3
3
  import { BiDiConnector } from "../transport/bidi-connection.js";
4
+ import type { UseOptions } from "../types/use.js";
4
5
  /** What `Browser` needs of the thing it launched: the browser process, or for Chrome the chromedriver in front of it */
5
6
  export interface BrowserProcess {
6
7
  readonly exitCode: number | null;
@@ -14,6 +15,8 @@ interface ChromeLaunch {
14
15
  profileDir: string;
15
16
  headless: boolean;
16
17
  commandTimeout: number | undefined;
18
+ /** The `use` options that are fixed when the browser starts */
19
+ use?: UseOptions | undefined;
17
20
  signal?: AbortSignal | undefined;
18
21
  }
19
22
  export declare const chromeFlags: ({ headless, profileDir, }: Pick<ChromeLaunch, "headless" | "profileDir">) => string[];
@@ -25,6 +28,7 @@ export declare const chromeCapabilities: (launch: ChromeLaunch) => {
25
28
  binary: string;
26
29
  args: string[];
27
30
  };
31
+ acceptInsecureCerts?: boolean | undefined;
28
32
  unhandledPromptBehavior: {
29
33
  default: string;
30
34
  };
@@ -5,7 +5,7 @@ import os from "node:os";
5
5
  import path from "node:path";
6
6
  import logger from "../logger/index.js";
7
7
  import { BiDiConnector } from "../transport/bidi-connection.js";
8
- import { SESSION_CAPABILITIES } from "./session-capabilities.js";
8
+ import { sessionCapabilities } from "./session-capabilities.js";
9
9
  export const chromeFlags = ({ headless, profileDir, }) => [
10
10
  "--no-sandbox",
11
11
  "--no-first-run",
@@ -21,7 +21,7 @@ export const chromeCapabilities = (launch) => ({
21
21
  alwaysMatch: {
22
22
  browserName: "chrome",
23
23
  webSocketUrl: true,
24
- ...SESSION_CAPABILITIES.alwaysMatch,
24
+ ...sessionCapabilities(launch.use).alwaysMatch,
25
25
  "goog:chromeOptions": {
26
26
  binary: launch.executablePath,
27
27
  args: chromeFlags(launch),
@@ -2,5 +2,10 @@ export { Browser, type BrowserLaunchOptions, type ContextOptions, } from "./brow
2
2
  export { BrowserContext } from "./browser-context.js";
3
3
  export { default as Page } from "./page.js";
4
4
  export { DownloadTimeoutError } from "./download.js";
5
+ export { UrlTimeoutError } from "./navigation-error.js";
6
+ export { ScriptError, ScriptSerializationError, type PageFunction, } from "../script/call-function.js";
7
+ export { ElementHandle } from "../script/element-handle.js";
8
+ export { RemoteObject } from "../script/serialize.js";
9
+ export type { BoundingBox } from "../locator/element-reads.js";
5
10
  export { EmulationUnsupportedError } from "./emulation.js";
6
11
  export { findBrowser } from "./browser-finder.js";
@@ -2,5 +2,9 @@ export { Browser, } from "./browser.js";
2
2
  export { BrowserContext } from "./browser-context.js";
3
3
  export { default as Page } from "./page.js";
4
4
  export { DownloadTimeoutError } from "./download.js";
5
+ export { UrlTimeoutError } from "./navigation-error.js";
6
+ export { ScriptError, ScriptSerializationError, } from "../script/call-function.js";
7
+ export { ElementHandle } from "../script/element-handle.js";
8
+ export { RemoteObject } from "../script/serialize.js";
5
9
  export { EmulationUnsupportedError } from "./emulation.js";
6
10
  export { findBrowser } from "./browser-finder.js";
@@ -51,7 +51,7 @@ export function launchFirefox(launch) {
51
51
  connector = await BiDiConnector.connect(url, { commandTimeout });
52
52
  const status = await connector.send("session.status", {});
53
53
  logger.verbose("Session status", status);
54
- await startSession(connector, label, commandTimeout);
54
+ await startSession(connector, label, commandTimeout, launchOptions.use);
55
55
  logger.verbose("Session started successfully");
56
56
  settled();
57
57
  resolve({ proc, connector });
@@ -1,4 +1,5 @@
1
1
  import type { SupportedBrowser } from "../types/browser.js";
2
+ import type { UseOptions } from "../types/use.js";
2
3
  export interface BrowserLaunchOptions {
3
4
  /** Which port to use for the WebDeveloper tools */
4
5
  port: number;
@@ -6,6 +7,8 @@ export interface BrowserLaunchOptions {
6
7
  profileDir: string;
7
8
  /** Start the browser in headless mode or in UI mode */
8
9
  headless: boolean;
10
+ /** The `use` options that can only be set when the browser starts (certificates, and Firefox's JavaScript and colour scheme) */
11
+ use?: UseOptions;
9
12
  }
10
13
  /** The profile folder of a worker: worker 0 keeps the browser's own folder, worker N gets `<browser>-wN` beside it */
11
14
  export declare const workerProfileDir: (browserName: SupportedBrowser, workerIndex: number) => string;
@@ -15,4 +18,4 @@ export declare const browserLaunchFlag: (browserName: SupportedBrowser, launchOp
15
18
  /** Env added to a browser's process */
16
19
  export declare const browserLaunchEnv: (browserName: SupportedBrowser, appDataPath?: string) => Record<string, string>;
17
20
  /** Writes the browser's user.js unless it already holds the current prefs; an outdated one is replaced */
18
- export declare function ensureBrowserProfile(browserName: SupportedBrowser, profilePath?: string | undefined): void;
21
+ export declare function ensureBrowserProfile(browserName: SupportedBrowser, profilePath?: string | undefined, use?: UseOptions): void;
@@ -71,15 +71,28 @@ const browserProfiles = {
71
71
  user_pref("browser.newtabpage.enabled", false);
72
72
  `,
73
73
  };
74
+ /**
75
+ * The prefs a Firefox run chooses. Always written, with the browser's own values when nothing is chosen: Firefox
76
+ * keeps a pref a user.js set in the profile, so a run without `use` would otherwise inherit the one before it.
77
+ */
78
+ function usePrefs(use) {
79
+ const scheme = { light: 1, dark: 0, "no-preference": 2 }[use?.colorScheme ?? "no-preference"];
80
+ return `
81
+ user_pref("javascript.enabled", ${use?.javaScriptEnabled !== false});
82
+ user_pref("layout.css.prefers-color-scheme.content-override", ${scheme});
83
+ `;
84
+ }
85
+ const profileText = (browserName, use) => browserProfiles[browserName] +
86
+ (browserName === "firefox" ? usePrefs(use) : "");
74
87
  /** Writes the browser's user.js unless it already holds the current prefs; an outdated one is replaced */
75
- export function ensureBrowserProfile(browserName, profilePath = browserProfilePath(browserName)) {
88
+ export function ensureBrowserProfile(browserName, profilePath = browserProfilePath(browserName), use) {
76
89
  logger.verbose("Checking browser profile for: %s", browserName);
77
90
  if (!profilePath) {
78
91
  logger.verbose("No browser profile path declared, skipping...");
79
92
  return;
80
93
  }
81
94
  if (existsSync(profilePath)) {
82
- if (readFileSync(profilePath, "utf8") === browserProfiles[browserName]) {
95
+ if (readFileSync(profilePath, "utf8") === profileText(browserName, use)) {
83
96
  logger.verbose("Browser profile is up to date at: %s", profilePath);
84
97
  return;
85
98
  }
@@ -88,12 +101,12 @@ export function ensureBrowserProfile(browserName, profilePath = browserProfilePa
88
101
  else {
89
102
  logger.verbose("No browser profile created yet for %s", browserName);
90
103
  }
91
- createBrowserProfile(browserName, profilePath);
104
+ createBrowserProfile(browserName, profilePath, use);
92
105
  }
93
- function createBrowserProfile(browserName, profilePath) {
106
+ function createBrowserProfile(browserName, profilePath, use) {
94
107
  logger.verbose("Creating browser profile for %s at %s", browserName, profilePath);
95
108
  mkdirSync(path.dirname(profilePath), { recursive: true });
96
- writeFileSync(profilePath, browserProfiles[browserName], {
109
+ writeFileSync(profilePath, profileText(browserName, use), {
97
110
  encoding: "utf8",
98
111
  });
99
112
  logger.verbose("Browser profile created");
@@ -9,5 +9,9 @@ export declare class NavigationError extends Error {
9
9
  }
10
10
  /** The page did not reach the awaited load state in time */
11
11
  export declare class LoadStateTimeoutError extends WaitTimeoutError<undefined> {
12
- constructor(state: string, timeout: number);
12
+ constructor(state: string, timeout: number, operation?: string);
13
+ }
14
+ /** The page's URL did not match in time */
15
+ export declare class UrlTimeoutError extends WaitTimeoutError<string> {
16
+ constructor(expected: string, timeout: number, lastUrl: string | undefined);
13
17
  }
@@ -11,9 +11,17 @@ export class NavigationError extends Error {
11
11
  }
12
12
  /** The page did not reach the awaited load state in time */
13
13
  export class LoadStateTimeoutError extends WaitTimeoutError {
14
- constructor(state, timeout) {
14
+ constructor(state, timeout, operation = "waitForLoadState") {
15
15
  super(timeout, undefined);
16
16
  this.name = "LoadStateTimeoutError";
17
- this.message = `waitForLoadState("${state}") did not finish within ${timeout}ms`;
17
+ this.message = `${operation}("${state}") did not finish within ${timeout}ms`;
18
+ }
19
+ }
20
+ /** The page's URL did not match in time */
21
+ export class UrlTimeoutError extends WaitTimeoutError {
22
+ constructor(expected, timeout, lastUrl) {
23
+ super(timeout, lastUrl);
24
+ this.name = "UrlTimeoutError";
25
+ this.message = `waitForURL(): the URL did not match ${expected} within ${timeout}ms (last URL: ${lastUrl ?? "unknown"})`;
18
26
  }
19
27
  }
@@ -0,0 +1,5 @@
1
+ import type { BiDiConnector } from "../transport/bidi-connection.js";
2
+ /** The page's markup now, with its doctype */
3
+ export declare function readContent(connector: BiDiConnector, context: string): Promise<string>;
4
+ /** Writes `html` as the page's document; true when the document reached `state` within `timeout` ms */
5
+ export declare function writeContent(connector: BiDiConnector, context: string, html: string, state: "load" | "domcontentloaded", timeout: number): Promise<boolean>;
@@ -0,0 +1,43 @@
1
+ import { callFunction } from "../script/call-function.js";
2
+ /** The document as markup: the doctype (when there is one) followed by the document element */
3
+ const READ_CONTENT = `() => {
4
+ const type = document.doctype;
5
+ const doctype = type
6
+ ? "<!DOCTYPE " + type.name +
7
+ (type.publicId ? ' PUBLIC "' + type.publicId + '"' : "") +
8
+ (!type.publicId && type.systemId ? " SYSTEM" : "") +
9
+ (type.systemId ? ' "' + type.systemId + '"' : "") + ">"
10
+ : "";
11
+ return doctype + document.documentElement.outerHTML;
12
+ }`;
13
+ /**
14
+ * Replaces the document in place with `document.open()`, `write()` and `close()`: scripts and styles in `html` run and
15
+ * apply, and the page keeps its URL and origin. The listeners are added after `open()` because it removes the old ones.
16
+ * Resolves true once the `load` (or `DOMContentLoaded`) event fired, false when `timeout` ms passed first.
17
+ */
18
+ const WRITE_CONTENT = `async (html, state, timeout) => {
19
+ document.open();
20
+ const reached = new Promise((resolve) => {
21
+ const done = () => resolve(true);
22
+ if (state === "domcontentloaded") document.addEventListener("DOMContentLoaded", done, { once: true });
23
+ else window.addEventListener("load", done, { once: true });
24
+ setTimeout(() => resolve(false), timeout);
25
+ });
26
+ document.write(html);
27
+ document.close();
28
+ return reached;
29
+ }`;
30
+ /** The page's markup now, with its doctype */
31
+ export function readContent(connector, context) {
32
+ return callFunction(connector, context, READ_CONTENT, [], {
33
+ awaitPromise: false,
34
+ });
35
+ }
36
+ /** Writes `html` as the page's document; true when the document reached `state` within `timeout` ms */
37
+ export function writeContent(connector, context, html, state, timeout) {
38
+ return callFunction(connector, context, WRITE_CONTENT, [
39
+ html,
40
+ state,
41
+ timeout,
42
+ ]);
43
+ }
@@ -0,0 +1,9 @@
1
+ import { type PageFunction } from "../script/call-function.js";
2
+ import type { BiDiConnector } from "../transport/bidi-connection.js";
3
+ /** Whether a string is the source of a function (called with the argument) rather than an expression (evaluated) */
4
+ export declare function isFunctionSource(source: string): boolean;
5
+ /**
6
+ * Runs a function, or a string, in the page and returns the result as plain JavaScript (see `fromRemoteValue`).
7
+ * A function (or a string holding a function's source) is called with `arg`; any other string is an expression.
8
+ */
9
+ export declare function evaluateInPage<R, A>(connector: BiDiConnector, context: string, fn: PageFunction<[A], R>, arg?: A): Promise<R>;
@@ -0,0 +1,20 @@
1
+ import { callFunction, evaluateExpression, } from "../script/call-function.js";
2
+ /** A function (or arrow function, async or not) written out as source, as opposed to an expression */
3
+ const FUNCTION_SOURCE = /^\s*(?:async\s+)?(?:function\b|(?:\((?:[^()]|\([^()]*\))*\)|[\w$]+)\s*=>)/;
4
+ /** Whether a string is the source of a function (called with the argument) rather than an expression (evaluated) */
5
+ export function isFunctionSource(source) {
6
+ return FUNCTION_SOURCE.test(source);
7
+ }
8
+ /**
9
+ * Runs a function, or a string, in the page and returns the result as plain JavaScript (see `fromRemoteValue`).
10
+ * A function (or a string holding a function's source) is called with `arg`; any other string is an expression.
11
+ */
12
+ export async function evaluateInPage(connector, context, fn, arg) {
13
+ if (typeof fn === "string" && !isFunctionSource(fn)) {
14
+ if (arg !== undefined) {
15
+ throw new TypeError("evaluate(): an expression takes no argument; pass a function to use one");
16
+ }
17
+ return evaluateExpression(connector, context, fn);
18
+ }
19
+ return callFunction(connector, context, fn, arg === undefined ? [] : [arg]);
20
+ }
@@ -13,6 +13,6 @@ export declare class PageLocators {
13
13
  private strictOverride;
14
14
  constructor(biDiConnector: BiDiConnector, id: BrowsingContext, helperRealm: HelperRealm, pageConfig: PageConfig, assertOpen: () => void);
15
15
  locate(selector: ConstructorParameters<typeof Locator>[0]): Locator;
16
- /** `undefined` goes back to config `use.strictSelectors`; applies to locators already made, too */
16
+ /** `undefined` goes back to config `use.strictSelectors`; a locator asks once, at its first action or read, so earlier ones that already ran keep their answer */
17
17
  setStrictSelectors(enabled: boolean | undefined): void;
18
18
  }
@@ -12,7 +12,7 @@ export class PageLocators {
12
12
  this.assertOpen();
13
13
  return new Locator(selector, this.biDiConnector, this.id, this.helperRealm, [], () => this.pageConfig.strictSelectors(this.strictOverride));
14
14
  }
15
- /** `undefined` goes back to config `use.strictSelectors`; applies to locators already made, too */
15
+ /** `undefined` goes back to config `use.strictSelectors`; a locator asks once, at its first action or read, so earlier ones that already ran keep their answer */
16
16
  setStrictSelectors(enabled) {
17
17
  this.strictOverride = enabled;
18
18
  }
@@ -1,4 +1,5 @@
1
1
  import type { BiDiConnector } from "../transport/bidi-connection.js";
2
+ import { type UrlPattern } from "../network/url-match.js";
2
3
  import type { DocumentWatcher } from "./document-watcher.js";
3
4
  import type { PageConfig, ReadinessState } from "./page-settings.js";
4
5
  export type LoadState = "load" | "domcontentloaded" | "networkidle";
@@ -54,6 +55,23 @@ export declare class PageNavigation {
54
55
  waitForLoadState(state?: LoadState, options?: {
55
56
  timeout?: number;
56
57
  }): Promise<void>;
58
+ /**
59
+ * Waits until the page's URL matches `url` (a string equals it, or with `*` / `**` is a glob; a RegExp is tested on it;
60
+ * a function gets it as a `URL`), then waits for `waitUntil` (default "load"). A string starting with "/", "./" or
61
+ * "../" resolves against the base URL, as in `goto`. `timeout` applies to the URL, then again to the load state.
62
+ */
63
+ waitForURL(url: UrlPattern | ((url: URL) => boolean), options?: {
64
+ timeout?: number;
65
+ waitUntil?: LoadState;
66
+ }): Promise<void>;
67
+ /**
68
+ * Replaces the document with `html` (see `writeContent`) and waits for `waitUntil` (default "load"); "networkidle" is
69
+ * "load" plus a quiet network. The page keeps its URL.
70
+ */
71
+ setContent(html: string, options?: {
72
+ timeout?: number;
73
+ waitUntil?: LoadState;
74
+ }): Promise<void>;
57
75
  /**
58
76
  * Runs a navigation command and settles what waiting for it means: failures become NavigationErrors, and a
59
77
  * navigation that did not wait for the load is watched so `waitForLoadState` can tell it from the old page.
@@ -1,8 +1,11 @@
1
+ import logger from "../logger/index.js";
1
2
  import { peekRunSettings } from "../config/run-settings.js";
2
3
  import { BiDiError } from "../transport/bidi-error.js";
3
4
  import { UnsupportedOperationError } from "../transport/unsupported.js";
4
- import { WaitTimeoutError } from "../wait/wait-until.js";
5
- import { LoadStateTimeoutError, NavigationError } from "./navigation-error.js";
5
+ import { matchUrl } from "../network/url-match.js";
6
+ import { waitUntil, WaitTimeoutError } from "../wait/wait-until.js";
7
+ import { LoadStateTimeoutError, NavigationError, UrlTimeoutError, } from "./navigation-error.js";
8
+ import { writeContent } from "./page-content.js";
6
9
  /** A URL starting with "/", "./" or "../" */
7
10
  const RELATIVE_URL = /^\.{0,2}\//;
8
11
  /** Resolves a slash-led `url` against `base`; any other URL is returned as given */
@@ -88,6 +91,53 @@ export class PageNavigation {
88
91
  if (state === "networkidle")
89
92
  await this.deps.waitForNetworkIdle({ timeout });
90
93
  }
94
+ /**
95
+ * Waits until the page's URL matches `url` (a string equals it, or with `*` / `**` is a glob; a RegExp is tested on it;
96
+ * a function gets it as a `URL`), then waits for `waitUntil` (default "load"). A string starting with "/", "./" or
97
+ * "../" resolves against the base URL, as in `goto`. `timeout` applies to the URL, then again to the load state.
98
+ */
99
+ async waitForURL(url, options = {}) {
100
+ this.deps.assertOpen();
101
+ const timeout = await this.deps.config.navigationTimeout(options.timeout);
102
+ const pattern = typeof url === "string" ? this.resolveURL(url, "waitForURL") : url;
103
+ const matches = typeof pattern === "function"
104
+ ? (current) => pattern(new URL(current))
105
+ : (current) => matchUrl(pattern, current);
106
+ // A read that fails while the page navigates counts as no URL yet
107
+ const read = () => this.deps.url().catch((err) => {
108
+ this.deps.assertOpen();
109
+ logger.debug("waitForURL could not read the URL", { err });
110
+ return undefined;
111
+ });
112
+ try {
113
+ await waitUntil(read, (current) => current !== undefined && matches(current), { timeout });
114
+ }
115
+ catch (err) {
116
+ if (!(err instanceof WaitTimeoutError))
117
+ throw err;
118
+ const wanted = typeof pattern === "function"
119
+ ? "the predicate"
120
+ : typeof pattern === "string"
121
+ ? `"${pattern}"`
122
+ : String(pattern);
123
+ throw new UrlTimeoutError(wanted, timeout, err.last);
124
+ }
125
+ await this.waitForLoadState(options.waitUntil, { timeout });
126
+ }
127
+ /**
128
+ * Replaces the document with `html` (see `writeContent`) and waits for `waitUntil` (default "load"); "networkidle" is
129
+ * "load" plus a quiet network. The page keeps its URL.
130
+ */
131
+ async setContent(html, options = {}) {
132
+ this.deps.assertOpen();
133
+ const timeout = await this.deps.config.navigationTimeout(options.timeout);
134
+ const state = options.waitUntil ?? "load";
135
+ const reached = await writeContent(this.connector, this.contextId, html, state === "domcontentloaded" ? "domcontentloaded" : "load", timeout);
136
+ if (!reached)
137
+ throw new LoadStateTimeoutError(state, timeout, "setContent");
138
+ if (state === "networkidle")
139
+ await this.deps.waitForNetworkIdle({ timeout });
140
+ }
91
141
  /**
92
142
  * Runs a navigation command and settles what waiting for it means: failures become NavigationErrors, and a
93
143
  * navigation that did not wait for the load is watched so `waitForLoadState` can tell it from the old page.
@@ -19,7 +19,7 @@ export declare class PageConfig {
19
19
  timeout?: number;
20
20
  waitUntil?: "none" | "interactive" | "complete";
21
21
  }>;
22
- use(): Promise<{
22
+ use(): Promise<import("../api.js").UseOptions & {
23
23
  viewport?: {
24
24
  width: number;
25
25
  height: number;