@itmetsam/samurai-framework 0.21.4 → 0.22.0

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 (178) hide show
  1. package/dist/api.d.ts +8 -1
  2. package/dist/api.js +1 -0
  3. package/dist/browser/browser-context.d.ts +18 -6
  4. package/dist/browser/browser-context.js +25 -7
  5. package/dist/browser/browser.d.ts +11 -2
  6. package/dist/browser/browser.js +31 -8
  7. package/dist/browser/cdp.d.ts +11 -0
  8. package/dist/browser/cdp.js +28 -0
  9. package/dist/browser/chrome-launch.d.ts +4 -0
  10. package/dist/browser/chrome-launch.js +11 -3
  11. package/dist/browser/context-cookie-store.d.ts +17 -0
  12. package/dist/browser/context-cookie-store.js +90 -0
  13. package/dist/browser/index.d.ts +8 -1
  14. package/dist/browser/index.js +5 -1
  15. package/dist/browser/keyboard.d.ts +35 -0
  16. package/dist/browser/keyboard.js +83 -0
  17. package/dist/browser/launch-firefox.js +1 -1
  18. package/dist/browser/launch-flags.d.ts +4 -1
  19. package/dist/browser/launch-flags.js +18 -5
  20. package/dist/browser/mouse.d.ts +42 -0
  21. package/dist/browser/mouse.js +66 -0
  22. package/dist/browser/page-navigation.d.ts +0 -2
  23. package/dist/browser/page-navigation.js +1 -6
  24. package/dist/browser/page-settings.d.ts +1 -1
  25. package/dist/browser/page.d.ts +20 -6
  26. package/dist/browser/page.js +24 -8
  27. package/dist/browser/session-capabilities.d.ts +10 -0
  28. package/dist/browser/session-capabilities.js +11 -0
  29. package/dist/browser/session.d.ts +2 -1
  30. package/dist/browser/session.js +3 -3
  31. package/dist/browser/storage-state.d.ts +4 -0
  32. package/dist/browser/storage-state.js +17 -0
  33. package/dist/browser/use-applier.d.ts +38 -0
  34. package/dist/browser/use-applier.js +121 -0
  35. package/dist/browser/use-commands.d.ts +36 -0
  36. package/dist/browser/use-commands.js +120 -0
  37. package/dist/browser/viewport.d.ts +3 -2
  38. package/dist/browser/viewport.js +15 -2
  39. package/dist/cli/args.d.ts +8 -4
  40. package/dist/cli/args.js +8 -5
  41. package/dist/cli/console-reporter.js +5 -1
  42. package/dist/cli/main.js +1 -0
  43. package/dist/cli/usage.d.ts +1 -1
  44. package/dist/cli/usage.js +4 -1
  45. package/dist/config/base-url.d.ts +4 -0
  46. package/dist/config/base-url.js +6 -0
  47. package/dist/config/config.js +8 -0
  48. package/dist/config/fully-parallel.d.ts +3 -0
  49. package/dist/config/fully-parallel.js +6 -0
  50. package/dist/config/group-use.d.ts +8 -0
  51. package/dist/config/group-use.js +41 -0
  52. package/dist/config/resolved-config.d.ts +2 -0
  53. package/dist/config/resolved-config.js +3 -0
  54. package/dist/config/storage-state-file.d.ts +3 -0
  55. package/dist/config/storage-state-file.js +20 -0
  56. package/dist/config/use-options.d.ts +18 -0
  57. package/dist/config/use-options.js +184 -0
  58. package/dist/config/web-server-options.d.ts +7 -0
  59. package/dist/config/web-server-options.js +111 -0
  60. package/dist/locator/check-actions.js +1 -1
  61. package/dist/locator/drag-actions.d.ts +17 -0
  62. package/dist/locator/drag-actions.js +80 -0
  63. package/dist/locator/element-actions.d.ts +10 -4
  64. package/dist/locator/element-actions.js +23 -16
  65. package/dist/locator/element-state.d.ts +5 -0
  66. package/dist/locator/element-state.js +2 -1
  67. package/dist/locator/event-actions.d.ts +10 -0
  68. package/dist/locator/event-actions.js +81 -0
  69. package/dist/locator/input-actions.d.ts +2 -11
  70. package/dist/locator/input-actions.js +3 -56
  71. package/dist/locator/key-actions.d.ts +11 -0
  72. package/dist/locator/key-actions.js +52 -0
  73. package/dist/locator/key-map.d.ts +16 -0
  74. package/dist/locator/key-map.js +76 -0
  75. package/dist/locator/locator.d.ts +35 -5
  76. package/dist/locator/locator.js +70 -16
  77. package/dist/locator/page-scripts.d.ts +2 -0
  78. package/dist/locator/page-scripts.js +7 -0
  79. package/dist/locator/pointer-actions.d.ts +44 -0
  80. package/dist/locator/pointer-actions.js +117 -0
  81. package/dist/locator/pointer-element-actions.d.ts +21 -0
  82. package/dist/locator/pointer-element-actions.js +42 -0
  83. package/dist/logger/warn-once.d.ts +4 -0
  84. package/dist/logger/warn-once.js +13 -0
  85. package/dist/request/api-request-context.d.ts +65 -0
  86. package/dist/request/api-request-context.js +198 -0
  87. package/dist/request/api-response.d.ts +34 -0
  88. package/dist/request/api-response.js +57 -0
  89. package/dist/request/cookie-jar.d.ts +48 -0
  90. package/dist/request/cookie-jar.js +164 -0
  91. package/dist/request/http-transport.d.ts +31 -0
  92. package/dist/request/http-transport.js +113 -0
  93. package/dist/request/redirect.d.ts +10 -0
  94. package/dist/request/redirect.js +21 -0
  95. package/dist/request/request-body.d.ts +27 -0
  96. package/dist/request/request-body.js +71 -0
  97. package/dist/request/request.d.ts +16 -0
  98. package/dist/request/request.js +20 -0
  99. package/dist/runner/all-hooks.d.ts +7 -1
  100. package/dist/runner/all-hooks.js +17 -1
  101. package/dist/runner/attachments.d.ts +16 -0
  102. package/dist/runner/attachments.js +60 -0
  103. package/dist/runner/fixture-args.d.ts +15 -0
  104. package/dist/runner/fixture-args.js +95 -0
  105. package/dist/runner/fixture-defs.d.ts +22 -0
  106. package/dist/runner/fixture-defs.js +104 -0
  107. package/dist/runner/fixture-scope.d.ts +33 -0
  108. package/dist/runner/fixture-scope.js +156 -0
  109. package/dist/runner/fixtures.d.ts +37 -0
  110. package/dist/runner/fixtures.js +40 -0
  111. package/dist/runner/global-hooks.d.ts +13 -0
  112. package/dist/runner/global-hooks.js +66 -0
  113. package/dist/runner/hooks.d.ts +6 -1
  114. package/dist/runner/hooks.js +15 -3
  115. package/dist/runner/options.d.ts +13 -2
  116. package/dist/runner/output-tail.d.ts +14 -0
  117. package/dist/runner/output-tail.js +30 -0
  118. package/dist/runner/pool-schedule.d.ts +22 -0
  119. package/dist/runner/pool-schedule.js +55 -0
  120. package/dist/runner/pool-units.d.ts +34 -0
  121. package/dist/runner/pool-units.js +85 -0
  122. package/dist/runner/pooled-run.d.ts +7 -5
  123. package/dist/runner/pooled-run.js +19 -9
  124. package/dist/runner/registry.d.ts +3 -39
  125. package/dist/runner/registry.js +99 -21
  126. package/dist/runner/reporter.d.ts +17 -1
  127. package/dist/runner/reporter.js +15 -2
  128. package/dist/runner/request-fixture.d.ts +14 -0
  129. package/dist/runner/request-fixture.js +25 -0
  130. package/dist/runner/run-hooks.d.ts +16 -0
  131. package/dist/runner/run-hooks.js +52 -0
  132. package/dist/runner/run-test.d.ts +1 -3
  133. package/dist/runner/run-test.js +98 -35
  134. package/dist/runner/run.d.ts +1 -0
  135. package/dist/runner/run.js +1 -0
  136. package/dist/runner/select.d.ts +15 -4
  137. package/dist/runner/select.js +38 -12
  138. package/dist/runner/serial-run.d.ts +23 -0
  139. package/dist/runner/serial-run.js +88 -0
  140. package/dist/runner/serial.d.ts +34 -0
  141. package/dist/runner/serial.js +63 -0
  142. package/dist/runner/shard.d.ts +22 -0
  143. package/dist/runner/shard.js +73 -0
  144. package/dist/runner/steps.d.ts +13 -0
  145. package/dist/runner/steps.js +67 -0
  146. package/dist/runner/suite.d.ts +8 -3
  147. package/dist/runner/suite.js +12 -3
  148. package/dist/runner/test-annotations.d.ts +16 -0
  149. package/dist/runner/test-annotations.js +47 -0
  150. package/dist/runner/test-function.d.ts +115 -0
  151. package/dist/runner/test-function.js +1 -0
  152. package/dist/runner/test-info.d.ts +104 -0
  153. package/dist/runner/test-info.js +170 -0
  154. package/dist/runner/test-runner.d.ts +1 -0
  155. package/dist/runner/test-runner.js +64 -19
  156. package/dist/runner/test-timeout.d.ts +22 -0
  157. package/dist/runner/test-timeout.js +45 -0
  158. package/dist/runner/use-options.d.ts +7 -0
  159. package/dist/runner/use-options.js +25 -0
  160. package/dist/runner/wait-for-url.d.ts +20 -0
  161. package/dist/runner/wait-for-url.js +65 -0
  162. package/dist/runner/web-server.d.ts +16 -0
  163. package/dist/runner/web-server.js +165 -0
  164. package/dist/runner/worker-pool.d.ts +13 -5
  165. package/dist/runner/worker-pool.js +85 -51
  166. package/dist/runner/worker-protocol.d.ts +8 -0
  167. package/dist/runner/worker-session.js +5 -2
  168. package/dist/runner/worker.d.ts +2 -0
  169. package/dist/runner/worker.js +7 -1
  170. package/dist/script/element-handle.d.ts +1 -1
  171. package/dist/script/element-handle.js +1 -1
  172. package/dist/types/config.d.ts +86 -2
  173. package/dist/types/extend.d.ts +30 -0
  174. package/dist/types/test.d.ts +138 -0
  175. package/dist/types/use.d.ts +85 -0
  176. package/package.json +1 -1
  177. package/dist/browser/emulation.d.ts +0 -61
  178. package/dist/browser/emulation.js +0 -180
package/dist/api.d.ts CHANGED
@@ -1,6 +1,13 @@
1
1
  export { test, describe, beforeEach, afterEach, beforeAll, afterAll, } from "./runner/registry.js";
2
+ export { request, type NewRequestContextOptions } from "./request/request.js";
3
+ export type { APIRequestContext, RequestOptions, } from "./request/api-request-context.js";
4
+ export type { APIResponse } from "./request/api-response.js";
5
+ export type { FilePayload } from "./request/request-body.js";
2
6
  export { expect } from "./assert/expect.js";
3
7
  export { defineConfig } from "./config/config.js";
8
+ export type { GlobalHookConfig, WebServerConfig } from "./types/config.js";
4
9
  export { defineStep, type StepDefinition, type StepResult, type StepRunInput, } from "./flows/steps/index.js";
5
- export type { AllHookFixtures, TestDetails, TestFixtures, TestOptions, TestOutputs, } from "./types/test.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";
6
13
  export { generatedFile, type GeneratedFile, type FileInput, } from "./browser/generated-files.js";
package/dist/api.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { test, describe, beforeEach, afterEach, beforeAll, afterAll, } from "./runner/registry.js";
2
+ export { request } from "./request/request.js";
2
3
  export { expect } from "./assert/expect.js";
3
4
  export { defineConfig } from "./config/config.js";
4
5
  export { defineStep, } from "./flows/steps/index.js";
@@ -1,6 +1,7 @@
1
1
  import type { BiDiConnector } from "../transport/bidi-connection.js";
2
2
  import type { SameSite } from "../types/bidi-modules/network.js";
3
- import { type EmulationOptions } from "./emulation.js";
3
+ import { APIRequestContext, type RequestContextOptions } from "../request/api-request-context.js";
4
+ import type { RuntimeUseOptions } from "../types/use.js";
4
5
  import type Page from "./page.js";
5
6
  /** A cookie as `context.cookies()` returns it */
6
7
  export interface Cookie {
@@ -54,6 +55,8 @@ export declare class BrowserContext {
54
55
  readonly isDefault: boolean;
55
56
  private opened;
56
57
  private isClosed;
58
+ private requestContext;
59
+ private readonly requestDefaults;
57
60
  constructor(host: ContextHost,
58
61
  /** The BiDi user context id; "default" for the browser's own */
59
62
  id: string, isDefault: boolean);
@@ -65,6 +68,14 @@ export declare class BrowserContext {
65
68
  pages(): Page[];
66
69
  /** @internal Called when a page is opened in this context */
67
70
  register(page: Page): void;
71
+ /**
72
+ * Makes HTTP requests from the test that share this context's cookies: they send the cookies the pages hold,
73
+ * and the cookies the responses set are in the pages' `document.cookie` afterwards. Its `extraHTTPHeaders`
74
+ * and `ignoreHTTPSErrors` follow the run's `use` options.
75
+ */
76
+ get request(): APIRequestContext;
77
+ /** @internal What `request` starts from: the run's `use.extraHTTPHeaders` and `acceptInsecureCerts` */
78
+ setRequestDefaults(defaults: RequestContextOptions): void;
68
79
  /** Adds cookies to this context's storage. Every cookie is checked before any is stored */
69
80
  addCookies(cookies: CookieInput[]): Promise<void>;
70
81
  /** The cookies of this context, optionally only those matching `filter` */
@@ -72,15 +83,16 @@ export declare class BrowserContext {
72
83
  /** Deletes all cookies of this context, or those matching `filter` */
73
84
  clearCookies(filter?: CookieFilter): Promise<void>;
74
85
  /**
75
- * Pretends things about every page of this context, including pages opened later: locale, timezone, user agent,
76
- * geolocation, offline, orientation, screen size. A page's own `emulate()` wins over the context's.
86
+ * Changes the `use` options that can change while the browser runs for every page of this context, including
87
+ * pages opened later: locale, timezone, user agent, geolocation, offline, orientation, screen, hasTouch,
88
+ * javaScriptEnabled. A page's own `use()` wins over the context's.
77
89
  * @example
78
- * await context.emulate({ locale: "nl-NL", timezone: "Europe/Amsterdam", offline: false });
90
+ * await context.use({ locale: "nl-NL", timezone: "Europe/Amsterdam", offline: false });
79
91
  */
80
- emulate(options: EmulationOptions): Promise<void>;
92
+ use(options: RuntimeUseOptions): Promise<void>;
81
93
  /**
82
94
  * Grants, denies or resets a browser permission ("geolocation", "notifications", "camera", …) for `origin`
83
- * in this context. Geolocation emulation only answers once the permission is granted.
95
+ * in this context. A geolocation override only answers once the permission is granted.
84
96
  */
85
97
  setPermission(name: string, state: "granted" | "denied" | "prompt", origin: string): Promise<void>;
86
98
  /** Removes the context; the browser closes its pages. The default context cannot be closed */
@@ -1,5 +1,7 @@
1
1
  import { BiDiError } from "../transport/bidi-error.js";
2
- import { applyEmulation } from "./emulation.js";
2
+ import { APIRequestContext, } from "../request/api-request-context.js";
3
+ import { ContextCookieStore } from "./context-cookie-store.js";
4
+ import { applyUse } from "./use-commands.js";
3
5
  /**
4
6
  * An isolated browsing session inside one browser: its own cookies, storage and cache. Pages opened in it
5
7
  * share them; pages of other contexts see none of it.
@@ -13,6 +15,7 @@ export class BrowserContext {
13
15
  this.isDefault = isDefault;
14
16
  this.opened = [];
15
17
  this.isClosed = false;
18
+ this.requestDefaults = {};
16
19
  }
17
20
  /** True once `close()` has run */
18
21
  get closed() {
@@ -36,6 +39,19 @@ export class BrowserContext {
36
39
  this.opened.push(page);
37
40
  page.setContext(this);
38
41
  }
42
+ /**
43
+ * Makes HTTP requests from the test that share this context's cookies: they send the cookies the pages hold,
44
+ * and the cookies the responses set are in the pages' `document.cookie` afterwards. Its `extraHTTPHeaders`
45
+ * and `ignoreHTTPSErrors` follow the run's `use` options.
46
+ */
47
+ get request() {
48
+ this.assertOpen();
49
+ return (this.requestContext ??= new APIRequestContext(this.requestDefaults, new ContextCookieStore(this)));
50
+ }
51
+ /** @internal What `request` starts from: the run's `use.extraHTTPHeaders` and `acceptInsecureCerts` */
52
+ setRequestDefaults(defaults) {
53
+ Object.assign(this.requestDefaults, defaults);
54
+ }
39
55
  /** Adds cookies to this context's storage. Every cookie is checked before any is stored */
40
56
  async addCookies(cookies) {
41
57
  this.assertOpen();
@@ -65,18 +81,19 @@ export class BrowserContext {
65
81
  });
66
82
  }
67
83
  /**
68
- * Pretends things about every page of this context, including pages opened later: locale, timezone, user agent,
69
- * geolocation, offline, orientation, screen size. A page's own `emulate()` wins over the context's.
84
+ * Changes the `use` options that can change while the browser runs for every page of this context, including
85
+ * pages opened later: locale, timezone, user agent, geolocation, offline, orientation, screen, hasTouch,
86
+ * javaScriptEnabled. A page's own `use()` wins over the context's.
70
87
  * @example
71
- * await context.emulate({ locale: "nl-NL", timezone: "Europe/Amsterdam", offline: false });
88
+ * await context.use({ locale: "nl-NL", timezone: "Europe/Amsterdam", offline: false });
72
89
  */
73
- async emulate(options) {
90
+ async use(options) {
74
91
  this.assertOpen();
75
- await applyEmulation(this.host.connector, { userContexts: [this.id] }, options);
92
+ await applyUse(this.host.connector, { userContexts: [this.id] }, options);
76
93
  }
77
94
  /**
78
95
  * Grants, denies or resets a browser permission ("geolocation", "notifications", "camera", …) for `origin`
79
- * in this context. Geolocation emulation only answers once the permission is granted.
96
+ * in this context. A geolocation override only answers once the permission is granted.
80
97
  */
81
98
  async setPermission(name, state, origin) {
82
99
  this.assertOpen();
@@ -104,6 +121,7 @@ export class BrowserContext {
104
121
  throw err;
105
122
  }
106
123
  this.isClosed = true;
124
+ await this.requestContext?.dispose();
107
125
  const pages = this.opened;
108
126
  this.opened = [];
109
127
  await Promise.all(pages.map((page) => page.detach()));
@@ -4,14 +4,16 @@ import Page from "./page.js";
4
4
  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
- import { type EmulationOptions } from "./emulation.js";
7
+ import type { RuntimeUseOptions } from "../types/use.js";
8
+ import { UseApplier } from "./use-applier.js";
9
+ import type { GroupUse } from "../types/config.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";
11
13
  import type { CookieFilter, PartialCookie, PartitionDescriptor } from "../types/bidi-modules/storage.js";
12
14
  export type { BrowserLaunchOptions } from "./launch-flags.js";
13
15
  /** Options of `browser.newContext()` */
14
- export interface ContextOptions extends EmulationOptions {
16
+ export interface ContextOptions extends RuntimeUseOptions {
15
17
  acceptInsecureCerts?: boolean;
16
18
  /** How this context's pages answer JavaScript dialogs; the session default is to leave them for the framework */
17
19
  unhandledPromptBehavior?: UserPromptHandler;
@@ -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: GroupUse, 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() */
@@ -5,7 +5,8 @@ import { projectBrowser } from "../browsers/resolve.js";
5
5
  import Page from "./page.js";
6
6
  import { launchChrome } from "./chrome-launch.js";
7
7
  import { BrowserContext } from "./browser-context.js";
8
- import { validateEmulation } from "./emulation.js";
8
+ import { validateRuntimeUse } from "./use-commands.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,27 +189,34 @@ 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, ...settings } = options;
193
+ const insecure = options.acceptInsecureCerts ?? this.use?.acceptInsecureCerts;
194
+ delete settings.acceptInsecureCerts;
177
195
  // A wrong value must not leave a context behind, so it is refused before one is created
178
- validateEmulation(emulation);
196
+ validateRuntimeUse(settings);
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(settings).length > 0) {
186
204
  try {
187
- await context.emulate(emulation);
205
+ await this.use?.context(context);
206
+ if (Object.keys(settings).length > 0)
207
+ await context.use(settings);
188
208
  }
189
209
  catch (err) {
190
210
  // Firefox keeps a context in its profile until it is removed
191
- await context.close().catch((closeErr) => logger.debug("Could not remove a context whose emulation failed", {
211
+ await context.close().catch((closeErr) => logger.debug("Could not remove a context whose settings failed", {
192
212
  closeErr,
193
213
  }));
194
214
  throw err;
195
215
  }
196
216
  }
217
+ // The option of this context wins over the run's `use.acceptInsecureCerts`
218
+ if (insecure !== undefined)
219
+ context.setRequestDefaults({ ignoreHTTPSErrors: insecure });
197
220
  return context;
198
221
  }
199
222
  /** The default context, then the contexts this browser created that are still open */
@@ -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),
@@ -118,18 +118,26 @@ export async function launchChrome(launch) {
118
118
  launch.signal?.removeEventListener("abort", onAbort);
119
119
  }
120
120
  }
121
+ /** How many of chromedriver's last lines the error carries when it exits before it is ready */
122
+ const OUTPUT_TAIL_LINES = 10;
121
123
  function driverPort(driver) {
122
124
  return new Promise((resolve, reject) => {
125
+ const tail = [];
123
126
  const onData = (data) => {
124
127
  logger.debug(`chromedriver: ${data}`);
125
128
  const match = String(data).match(/started successfully on port (\d+)/);
126
129
  if (match)
127
130
  resolve(Number(match[1]));
131
+ for (const line of String(data).split("\n"))
132
+ if (line.trim() !== "")
133
+ tail.push(line);
134
+ tail.splice(0, Math.max(0, tail.length - OUTPUT_TAIL_LINES));
128
135
  };
129
136
  driver.stdout?.on("data", onData);
130
137
  driver.stderr?.on("data", onData);
131
138
  driver.once("error", reject);
132
- driver.once("exit", (code) => reject(new Error(`chromedriver exited with code ${code} before it was ready`)));
139
+ driver.once("exit", (code, signal) => reject(new Error(`chromedriver exited with code ${code ?? signal} before it was ready` +
140
+ (tail.length > 0 ? `. Its last output:\n${tail.join("\n")}` : ""))));
133
141
  });
134
142
  }
135
143
  /** chromedriver as the process of a Chrome launch: ending it ends Chrome, and so does deleting the session */
@@ -0,0 +1,17 @@
1
+ import { type CookieStore } from "../request/cookie-jar.js";
2
+ import type { StorageStateCookie } from "../types/use.js";
3
+ import type { BrowserContext } from "./browser-context.js";
4
+ /**
5
+ * The cookies of a browser context as a request context's cookie store: requests send what the pages of the
6
+ * context hold, and the `Set-Cookie` headers of the responses go into the context, so both see one session.
7
+ */
8
+ export declare class ContextCookieStore implements CookieStore {
9
+ private readonly context;
10
+ constructor(context: BrowserContext);
11
+ cookieHeader(url: URL): Promise<string | undefined>;
12
+ store(url: URL, setCookies: string[]): Promise<void>;
13
+ list(): Promise<StorageStateCookie[]>;
14
+ private add;
15
+ /** The site deleted the cookie: the browser keeps it under the host or, for a domain cookie, the dotted domain */
16
+ private remove;
17
+ }
@@ -0,0 +1,90 @@
1
+ import logger from "../logger/index.js";
2
+ import { cookieHeaderOf, cookieMatches, isExpired, parseSetCookie, } from "../request/cookie-jar.js";
3
+ const CAPITAL = { strict: "Strict", lax: "Lax", none: "None" };
4
+ function stored(cookie) {
5
+ const sameSite = CAPITAL[cookie.sameSite];
6
+ return {
7
+ name: cookie.name,
8
+ value: cookie.value,
9
+ domain: cookie.domain.replace(/^\./, "").toLowerCase(),
10
+ hostOnly: !cookie.domain.startsWith("."),
11
+ path: cookie.path,
12
+ secure: cookie.secure,
13
+ httpOnly: cookie.httpOnly,
14
+ ...(sameSite !== undefined && { sameSite }),
15
+ ...(cookie.expiry !== undefined && { expires: cookie.expiry }),
16
+ };
17
+ }
18
+ /**
19
+ * The cookies of a browser context as a request context's cookie store: requests send what the pages of the
20
+ * context hold, and the `Set-Cookie` headers of the responses go into the context, so both see one session.
21
+ */
22
+ export class ContextCookieStore {
23
+ constructor(context) {
24
+ this.context = context;
25
+ }
26
+ async cookieHeader(url) {
27
+ const now = Date.now();
28
+ const matching = (await this.context.cookies())
29
+ .map(stored)
30
+ .filter((cookie) => cookieMatches(cookie, url, now));
31
+ return cookieHeaderOf(matching);
32
+ }
33
+ async store(url, setCookies) {
34
+ for (const header of setCookies) {
35
+ const cookie = parseSetCookie(header, url);
36
+ if (!cookie)
37
+ continue;
38
+ try {
39
+ if (isExpired(cookie))
40
+ await this.remove(cookie);
41
+ else
42
+ await this.add(cookie);
43
+ }
44
+ catch (err) {
45
+ // A browser refuses some cookies a server can send (Secure on a plain-http page, say): as a browser would
46
+ logger.debug("The browser did not take a Set-Cookie", {
47
+ name: cookie.name,
48
+ err,
49
+ });
50
+ }
51
+ }
52
+ }
53
+ async list() {
54
+ return (await this.context.cookies()).map((cookie) => ({
55
+ name: cookie.name,
56
+ value: cookie.value,
57
+ domain: cookie.domain,
58
+ path: cookie.path,
59
+ expires: cookie.expiry ?? -1,
60
+ httpOnly: cookie.httpOnly,
61
+ secure: cookie.secure,
62
+ sameSite: CAPITAL[cookie.sameSite] ?? "Lax",
63
+ }));
64
+ }
65
+ add(cookie) {
66
+ return this.context.addCookies([
67
+ {
68
+ name: cookie.name,
69
+ value: cookie.value,
70
+ domain: cookie.hostOnly ? cookie.domain : `.${cookie.domain}`,
71
+ path: cookie.path,
72
+ secure: cookie.secure,
73
+ httpOnly: cookie.httpOnly,
74
+ ...(cookie.sameSite !== undefined && {
75
+ sameSite: cookie.sameSite.toLowerCase(),
76
+ }),
77
+ ...(cookie.expires !== undefined && { expiry: cookie.expires }),
78
+ },
79
+ ]);
80
+ }
81
+ /** The site deleted the cookie: the browser keeps it under the host or, for a domain cookie, the dotted domain */
82
+ async remove(cookie) {
83
+ for (const domain of [cookie.domain, `.${cookie.domain}`])
84
+ await this.context.clearCookies({
85
+ name: cookie.name,
86
+ domain,
87
+ path: cookie.path,
88
+ });
89
+ }
90
+ }
@@ -1,6 +1,13 @@
1
1
  export { Browser, type BrowserLaunchOptions, type ContextOptions, } from "./browser.js";
2
2
  export { BrowserContext } from "./browser-context.js";
3
3
  export { default as Page } from "./page.js";
4
+ export type { default as Keyboard } from "./keyboard.js";
5
+ export type { default as Mouse } from "./mouse.js";
4
6
  export { DownloadTimeoutError } from "./download.js";
5
- export { EmulationUnsupportedError } from "./emulation.js";
7
+ export { UrlTimeoutError } from "./navigation-error.js";
8
+ export { ScriptError, ScriptSerializationError, type PageFunction, } from "../script/call-function.js";
9
+ export { ElementHandle } from "../script/element-handle.js";
10
+ export { RemoteObject } from "../script/serialize.js";
11
+ export type { BoundingBox } from "../locator/element-reads.js";
12
+ export { UseError, UseUnsupportedError } from "./use-commands.js";
6
13
  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 { EmulationUnsupportedError } from "./emulation.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";
9
+ export { UseError, UseUnsupportedError } from "./use-commands.js";
6
10
  export { findBrowser } from "./browser-finder.js";
@@ -0,0 +1,35 @@
1
+ import type { BiDiConnector } from "../transport/bidi-connection.js";
2
+ /**
3
+ * The page's keyboard: real key input to whatever has focus. BiDi remembers which keys are down between calls,
4
+ * so `down("Shift")`, other input, then `up("Shift")` works, and a held `Shift` capitalizes `press("a")`.
5
+ */
6
+ export default class Keyboard {
7
+ private connector;
8
+ private contextId;
9
+ private assertOpen;
10
+ constructor(connector: BiDiConnector, contextId: string, assertOpen: () => void);
11
+ /**
12
+ * Presses and releases a key, or a chord: modifiers (`Control`, `Shift`, `Alt`, `Meta`, `ControlOrMeta`) joined to
13
+ * the key by `+`. Named keys are `Enter`, `Tab`, `Escape`, `Backspace`, `Delete`, `Space`, `Insert`, the arrows, `Home`, `End`,
14
+ * `PageUp`, `PageDown` and `F1` to `F12`; any other key is a single character. `delay` waits (ms) between press and release.
15
+ * @example
16
+ * await page.keyboard.press("Control+a");
17
+ * await page.keyboard.press("Shift+Tab");
18
+ */
19
+ press(key: string, options?: {
20
+ delay?: number;
21
+ }): Promise<void>;
22
+ /** Presses a key and keeps it down until `up`; a modifier stays in effect for the keys and clicks in between */
23
+ down(key: string): Promise<void>;
24
+ /** Releases a key pressed with `down`; a key that is not down is ignored */
25
+ up(key: string): Promise<void>;
26
+ /** Presses and releases one key per character of `text`, `delay` ms apart; a line break is Enter */
27
+ type(text: string, options?: {
28
+ delay?: number;
29
+ }): Promise<void>;
30
+ /**
31
+ * Inserts `text` at the caret of the focused element in one step, as a paste does: `input` fires, `keydown` and `keyup`
32
+ * do not. Does nothing when no editable element has focus.
33
+ */
34
+ insertText(text: string): Promise<void>;
35
+ }
@@ -0,0 +1,83 @@
1
+ import { callFunction } from "../script/call-function.js";
2
+ import { chordSteps, performKeys, textSteps } from "../locator/key-actions.js";
3
+ import { parseKeyChord } from "../locator/key-map.js";
4
+ /** Types into whatever has focus, replacing the selection: an input, a textarea or an editable element, also in a same-origin frame */
5
+ const INSERT_TEXT = `(text) => {
6
+ let doc = document;
7
+ let el = doc.activeElement;
8
+ while (el && el.tagName === "IFRAME" && el.contentDocument && el.contentDocument.activeElement) {
9
+ doc = el.contentDocument;
10
+ el = doc.activeElement;
11
+ }
12
+ const selection = doc.getSelection();
13
+ // A freshly focused editable element can have no caret yet (Firefox): put it at the end
14
+ if (el && el.isContentEditable && selection && !el.contains(selection.anchorNode)) {
15
+ const range = doc.createRange();
16
+ range.selectNodeContents(el);
17
+ range.collapse(false);
18
+ selection.removeAllRanges();
19
+ selection.addRange(range);
20
+ }
21
+ doc.execCommand("insertText", false, text);
22
+ }`;
23
+ /** The key `down` and `up` take: one key, no chord, since a chord is several keys */
24
+ function singleKey(key, method) {
25
+ const chord = parseKeyChord(key, method);
26
+ if (chord.modifiers.length > 0) {
27
+ throw new Error(`${method}(): "${key}" is a chord. Press its keys one at a time`);
28
+ }
29
+ return chord.key;
30
+ }
31
+ /**
32
+ * The page's keyboard: real key input to whatever has focus. BiDi remembers which keys are down between calls,
33
+ * so `down("Shift")`, other input, then `up("Shift")` works, and a held `Shift` capitalizes `press("a")`.
34
+ */
35
+ export default class Keyboard {
36
+ constructor(connector, contextId, assertOpen) {
37
+ this.connector = connector;
38
+ this.contextId = contextId;
39
+ this.assertOpen = assertOpen;
40
+ }
41
+ /**
42
+ * Presses and releases a key, or a chord: modifiers (`Control`, `Shift`, `Alt`, `Meta`, `ControlOrMeta`) joined to
43
+ * the key by `+`. Named keys are `Enter`, `Tab`, `Escape`, `Backspace`, `Delete`, `Space`, `Insert`, the arrows, `Home`, `End`,
44
+ * `PageUp`, `PageDown` and `F1` to `F12`; any other key is a single character. `delay` waits (ms) between press and release.
45
+ * @example
46
+ * await page.keyboard.press("Control+a");
47
+ * await page.keyboard.press("Shift+Tab");
48
+ */
49
+ async press(key, options) {
50
+ this.assertOpen();
51
+ const chord = parseKeyChord(key, "keyboard.press");
52
+ await performKeys(this.connector, this.contextId, chordSteps(chord, options?.delay));
53
+ }
54
+ /** Presses a key and keeps it down until `up`; a modifier stays in effect for the keys and clicks in between */
55
+ async down(key) {
56
+ this.assertOpen();
57
+ await performKeys(this.connector, this.contextId, [
58
+ { type: "keyDown", value: singleKey(key, "keyboard.down") },
59
+ ]);
60
+ }
61
+ /** Releases a key pressed with `down`; a key that is not down is ignored */
62
+ async up(key) {
63
+ this.assertOpen();
64
+ await performKeys(this.connector, this.contextId, [
65
+ { type: "keyUp", value: singleKey(key, "keyboard.up") },
66
+ ]);
67
+ }
68
+ /** Presses and releases one key per character of `text`, `delay` ms apart; a line break is Enter */
69
+ async type(text, options) {
70
+ this.assertOpen();
71
+ await performKeys(this.connector, this.contextId, textSteps(text, options?.delay));
72
+ }
73
+ /**
74
+ * Inserts `text` at the caret of the focused element in one step, as a paste does: `input` fires, `keydown` and `keyup`
75
+ * do not. Does nothing when no editable element has focus.
76
+ */
77
+ async insertText(text) {
78
+ this.assertOpen();
79
+ await callFunction(this.connector, this.contextId, INSERT_TEXT, [text], {
80
+ awaitPromise: false,
81
+ });
82
+ }
83
+ }