@itmetsam/samurai-framework 0.21.5 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/dist/api.d.ts +8 -3
  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 +4 -4
  6. package/dist/browser/browser.js +11 -8
  7. package/dist/browser/chrome-launch.js +9 -1
  8. package/dist/browser/context-cookie-store.d.ts +17 -0
  9. package/dist/browser/context-cookie-store.js +90 -0
  10. package/dist/browser/index.d.ts +3 -1
  11. package/dist/browser/index.js +1 -1
  12. package/dist/browser/keyboard.d.ts +35 -0
  13. package/dist/browser/keyboard.js +83 -0
  14. package/dist/browser/mouse.d.ts +42 -0
  15. package/dist/browser/mouse.js +66 -0
  16. package/dist/browser/page-content.d.ts +4 -0
  17. package/dist/browser/page-content.js +12 -0
  18. package/dist/browser/page-events.d.ts +11 -0
  19. package/dist/browser/page-events.js +23 -0
  20. package/dist/browser/page-logging.d.ts +24 -0
  21. package/dist/browser/page-logging.js +43 -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-viewport.d.ts +21 -0
  26. package/dist/browser/page-viewport.js +33 -0
  27. package/dist/browser/page.d.ts +23 -16
  28. package/dist/browser/page.js +48 -71
  29. package/dist/browser/screenshot.d.ts +6 -0
  30. package/dist/browser/screenshot.js +14 -0
  31. package/dist/browser/storage-state.d.ts +0 -2
  32. package/dist/browser/storage-state.js +0 -20
  33. package/dist/browser/use-applier.d.ts +16 -5
  34. package/dist/browser/use-applier.js +44 -17
  35. package/dist/browser/use-commands.d.ts +36 -0
  36. package/dist/browser/use-commands.js +120 -0
  37. package/dist/cli/args.d.ts +8 -4
  38. package/dist/cli/args.js +8 -5
  39. package/dist/cli/console-reporter.js +5 -1
  40. package/dist/cli/main.js +1 -0
  41. package/dist/cli/usage.d.ts +1 -1
  42. package/dist/cli/usage.js +4 -1
  43. package/dist/config/base-url.d.ts +4 -0
  44. package/dist/config/base-url.js +6 -0
  45. package/dist/config/config.js +7 -0
  46. package/dist/config/fixture-use.d.ts +19 -0
  47. package/dist/config/fixture-use.js +44 -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 +10 -0
  51. package/dist/config/group-use.js +51 -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 +3 -1
  57. package/dist/config/use-options.js +74 -14
  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/request/api-request-context.d.ts +65 -0
  84. package/dist/request/api-request-context.js +198 -0
  85. package/dist/request/api-response.d.ts +34 -0
  86. package/dist/request/api-response.js +57 -0
  87. package/dist/request/cookie-jar.d.ts +48 -0
  88. package/dist/request/cookie-jar.js +164 -0
  89. package/dist/request/http-transport.d.ts +31 -0
  90. package/dist/request/http-transport.js +113 -0
  91. package/dist/request/redirect.d.ts +10 -0
  92. package/dist/request/redirect.js +21 -0
  93. package/dist/request/request-body.d.ts +27 -0
  94. package/dist/request/request-body.js +71 -0
  95. package/dist/request/request.d.ts +16 -0
  96. package/dist/request/request.js +20 -0
  97. package/dist/runner/all-hooks.d.ts +7 -1
  98. package/dist/runner/all-hooks.js +17 -1
  99. package/dist/runner/fixture-defs.d.ts +2 -1
  100. package/dist/runner/fixture-defs.js +52 -18
  101. package/dist/runner/fixture-meta.d.ts +20 -0
  102. package/dist/runner/fixture-meta.js +109 -0
  103. package/dist/runner/fixture-options.d.ts +8 -0
  104. package/dist/runner/fixture-options.js +35 -0
  105. package/dist/runner/fixture-outputs.d.ts +11 -0
  106. package/dist/runner/fixture-outputs.js +28 -0
  107. package/dist/runner/fixture-registry.d.ts +35 -0
  108. package/dist/runner/fixture-registry.js +73 -0
  109. package/dist/runner/fixture-scope.d.ts +39 -9
  110. package/dist/runner/fixture-scope.js +139 -70
  111. package/dist/runner/fixture-start.d.ts +27 -0
  112. package/dist/runner/fixture-start.js +76 -0
  113. package/dist/runner/fixture-use.d.ts +23 -0
  114. package/dist/runner/fixture-use.js +64 -0
  115. package/dist/runner/fixture-worker.d.ts +26 -0
  116. package/dist/runner/fixture-worker.js +46 -0
  117. package/dist/runner/fixtures.d.ts +4 -1
  118. package/dist/runner/fixtures.js +3 -2
  119. package/dist/runner/global-hooks.d.ts +13 -0
  120. package/dist/runner/global-hooks.js +66 -0
  121. package/dist/runner/options.d.ts +13 -2
  122. package/dist/runner/output-tail.d.ts +14 -0
  123. package/dist/runner/output-tail.js +30 -0
  124. package/dist/runner/pool-schedule.d.ts +22 -0
  125. package/dist/runner/pool-schedule.js +55 -0
  126. package/dist/runner/pool-units.d.ts +34 -0
  127. package/dist/runner/pool-units.js +85 -0
  128. package/dist/runner/pooled-run.d.ts +7 -5
  129. package/dist/runner/pooled-run.js +19 -9
  130. package/dist/runner/prepare-run.js +5 -0
  131. package/dist/runner/registry.js +21 -7
  132. package/dist/runner/reporter.d.ts +13 -1
  133. package/dist/runner/reporter.js +13 -2
  134. package/dist/runner/request-fixture.d.ts +14 -0
  135. package/dist/runner/request-fixture.js +25 -0
  136. package/dist/runner/run-hooks.d.ts +16 -0
  137. package/dist/runner/run-hooks.js +52 -0
  138. package/dist/runner/run-test.d.ts +3 -0
  139. package/dist/runner/run-test.js +23 -3
  140. package/dist/runner/run.d.ts +8 -0
  141. package/dist/runner/run.js +10 -0
  142. package/dist/runner/select.d.ts +15 -4
  143. package/dist/runner/select.js +38 -12
  144. package/dist/runner/serial-run.d.ts +23 -0
  145. package/dist/runner/serial-run.js +88 -0
  146. package/dist/runner/serial.d.ts +34 -0
  147. package/dist/runner/serial.js +63 -0
  148. package/dist/runner/shard.d.ts +22 -0
  149. package/dist/runner/shard.js +73 -0
  150. package/dist/runner/suite.d.ts +3 -3
  151. package/dist/runner/suite.js +4 -3
  152. package/dist/runner/test-function.d.ts +15 -3
  153. package/dist/runner/test-info.js +2 -1
  154. package/dist/runner/test-runner.d.ts +1 -0
  155. package/dist/runner/test-runner.js +67 -18
  156. package/dist/runner/use-options.d.ts +18 -4
  157. package/dist/runner/use-options.js +33 -6
  158. package/dist/runner/wait-for-url.d.ts +20 -0
  159. package/dist/runner/wait-for-url.js +65 -0
  160. package/dist/runner/web-server.d.ts +16 -0
  161. package/dist/runner/web-server.js +165 -0
  162. package/dist/runner/worker-pool.d.ts +13 -5
  163. package/dist/runner/worker-pool.js +85 -51
  164. package/dist/runner/worker-protocol.d.ts +8 -0
  165. package/dist/runner/worker-session.js +9 -1
  166. package/dist/runner/worker.d.ts +2 -0
  167. package/dist/runner/worker.js +7 -1
  168. package/dist/types/config.d.ts +117 -21
  169. package/dist/types/extend.d.ts +54 -2
  170. package/dist/types/test.d.ts +42 -0
  171. package/dist/types/use.d.ts +59 -7
  172. package/package.json +1 -1
  173. package/dist/browser/emulation.d.ts +0 -61
  174. package/dist/browser/emulation.js +0 -180
package/dist/api.d.ts CHANGED
@@ -1,8 +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, TestAnnotation, TestAttachment, TestDetails, TestFixtures, TestInfo, TestOptions, TestOutputs, TestStepEntry, } from "./types/test.js";
6
- export type { FixtureOptions } from "./types/extend.js";
7
- export type { ColorScheme, StorageState, StorageStateCookie, 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";
8
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,16 +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
8
  import { UseApplier } from "./use-applier.js";
9
- import type { UseOptions } from "../types/use.js";
9
+ import type { GroupUse } from "../types/config.js";
10
10
  import { ChromeDownloadFolders } from "./download-folders.js";
11
11
  import { type BrowserLaunchOptions } from "./launch-flags.js";
12
12
  import type { BiDiCommands } from "../types/bidi.js";
13
13
  import type { CookieFilter, PartialCookie, PartitionDescriptor } from "../types/bidi-modules/storage.js";
14
14
  export type { BrowserLaunchOptions } from "./launch-flags.js";
15
15
  /** Options of `browser.newContext()` */
16
- export interface ContextOptions extends EmulationOptions {
16
+ export interface ContextOptions extends RuntimeUseOptions {
17
17
  acceptInsecureCerts?: boolean;
18
18
  /** How this context's pages answer JavaScript dialogs; the session default is to leave them for the framework */
19
19
  unhandledPromptBehavior?: UserPromptHandler;
@@ -51,7 +51,7 @@ export declare class Browser {
51
51
  * @internal The runner calls this after launch: the `use` options now apply to the default context and its pages,
52
52
  * and to every context and page created later. Options passed to `newContext` win over them.
53
53
  */
54
- setUse(options: UseOptions, projectDir: string): Promise<UseApplier>;
54
+ setUse(options: GroupUse, projectDir: string): Promise<UseApplier>;
55
55
  /** Closes the browser via BiDi browser.close, then makes sure the process has exited */
56
56
  close(): Promise<void>;
57
57
  /** Alias of close() */
@@ -5,7 +5,7 @@ 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
9
  import { UseApplier } from "./use-applier.js";
10
10
  import { prepareDownloadsDir, removeIfEmpty } from "./downloads-dir.js";
11
11
  import { ChromeDownloadFolders } from "./download-folders.js";
@@ -189,31 +189,34 @@ export class Browser {
189
189
  * const page = await alice.newPage();
190
190
  */
191
191
  async newContext(options = {}) {
192
- const { unhandledPromptBehavior, ...emulation } = options;
192
+ const { unhandledPromptBehavior, ...settings } = options;
193
193
  const insecure = options.acceptInsecureCerts ?? this.use?.acceptInsecureCerts;
194
- delete emulation.acceptInsecureCerts;
194
+ delete settings.acceptInsecureCerts;
195
195
  // A wrong value must not leave a context behind, so it is refused before one is created
196
- validateEmulation(emulation);
196
+ validateRuntimeUse(settings);
197
197
  const { userContext } = await this.biDiConnector.send("browser.createUserContext", {
198
198
  ...(insecure !== undefined && { acceptInsecureCerts: insecure }),
199
199
  ...(unhandledPromptBehavior && { unhandledPromptBehavior }),
200
200
  });
201
201
  const context = new BrowserContext(this.host, userContext, false);
202
202
  this.userContexts.set(userContext, context);
203
- if (this.use || Object.keys(emulation).length > 0) {
203
+ if (this.use || Object.keys(settings).length > 0) {
204
204
  try {
205
205
  await this.use?.context(context);
206
- if (Object.keys(emulation).length > 0)
207
- await context.emulate(emulation);
206
+ if (Object.keys(settings).length > 0)
207
+ await context.use(settings);
208
208
  }
209
209
  catch (err) {
210
210
  // Firefox keeps a context in its profile until it is removed
211
- 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", {
212
212
  closeErr,
213
213
  }));
214
214
  throw err;
215
215
  }
216
216
  }
217
+ // The option of this context wins over the run's `use.acceptInsecureCerts`
218
+ if (insecure !== undefined)
219
+ context.setRequestDefaults({ ignoreHTTPSErrors: insecure });
217
220
  return context;
218
221
  }
219
222
  /** The default context, then the contexts this browser created that are still open */
@@ -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,11 +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
7
  export { UrlTimeoutError } from "./navigation-error.js";
6
8
  export { ScriptError, ScriptSerializationError, type PageFunction, } from "../script/call-function.js";
7
9
  export { ElementHandle } from "../script/element-handle.js";
8
10
  export { RemoteObject } from "../script/serialize.js";
9
11
  export type { BoundingBox } from "../locator/element-reads.js";
10
- export { EmulationUnsupportedError } from "./emulation.js";
12
+ export { UseError, UseUnsupportedError } from "./use-commands.js";
11
13
  export { findBrowser } from "./browser-finder.js";
@@ -6,5 +6,5 @@ export { UrlTimeoutError } from "./navigation-error.js";
6
6
  export { ScriptError, ScriptSerializationError, } from "../script/call-function.js";
7
7
  export { ElementHandle } from "../script/element-handle.js";
8
8
  export { RemoteObject } from "../script/serialize.js";
9
- export { EmulationUnsupportedError } from "./emulation.js";
9
+ export { UseError, UseUnsupportedError } from "./use-commands.js";
10
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
+ }
@@ -0,0 +1,42 @@
1
+ import type { BiDiConnector } from "../transport/bidi-connection.js";
2
+ import { type MouseButton } from "../locator/pointer-actions.js";
3
+ /**
4
+ * The page's mouse: real pointer input at viewport coordinates (CSS pixels from the top left of the page's
5
+ * visible area). The pointer starts at 0, 0; `move` with `steps` runs from where the mouse last was through this object.
6
+ */
7
+ export default class Mouse {
8
+ private connector;
9
+ private contextId;
10
+ private assertOpen;
11
+ private position;
12
+ constructor(connector: BiDiConnector, contextId: string, assertOpen: () => void);
13
+ /** Moves the pointer to `x`, `y` in a straight line of `steps` moves (default 1) */
14
+ move(x: number, y: number, options?: {
15
+ steps?: number;
16
+ }): Promise<void>;
17
+ /** Presses a button (default left) where the pointer is, and keeps it down until `up` */
18
+ down(options?: {
19
+ button?: MouseButton;
20
+ }): Promise<void>;
21
+ /** Releases a button pressed with `down` */
22
+ up(options?: {
23
+ button?: MouseButton;
24
+ }): Promise<void>;
25
+ /**
26
+ * Moves to `x`, `y` and clicks. `clickCount` presses in a row (2 is a double click), `delay` ms between each press and release.
27
+ * @example
28
+ * await page.mouse.click(120, 80, { button: "right" });
29
+ */
30
+ click(x: number, y: number, options?: {
31
+ button?: MouseButton;
32
+ clickCount?: number;
33
+ delay?: number;
34
+ }): Promise<void>;
35
+ /** `click` with two presses */
36
+ dblclick(x: number, y: number, options?: {
37
+ button?: MouseButton;
38
+ delay?: number;
39
+ }): Promise<void>;
40
+ /** Turns the wheel where the pointer is by `deltaX` and `deltaY` pixels (positive is right and down) */
41
+ wheel(deltaX: number, deltaY: number): Promise<void>;
42
+ }
@@ -0,0 +1,66 @@
1
+ import { performPointer, wheelScroll, buttonCode, checkClick, clickSteps, moveSteps, } from "../locator/pointer-actions.js";
2
+ /** Throws `mouse.method(): ...` unless every value is a finite number */
3
+ function numbers(method, what, ...values) {
4
+ if (!values.every(Number.isFinite)) {
5
+ throw new Error(`mouse.${method}(): ${what} must be numbers`);
6
+ }
7
+ }
8
+ /**
9
+ * The page's mouse: real pointer input at viewport coordinates (CSS pixels from the top left of the page's
10
+ * visible area). The pointer starts at 0, 0; `move` with `steps` runs from where the mouse last was through this object.
11
+ */
12
+ export default class Mouse {
13
+ constructor(connector, contextId, assertOpen) {
14
+ this.connector = connector;
15
+ this.contextId = contextId;
16
+ this.assertOpen = assertOpen;
17
+ this.position = { x: 0, y: 0 };
18
+ }
19
+ /** Moves the pointer to `x`, `y` in a straight line of `steps` moves (default 1) */
20
+ async move(x, y, options) {
21
+ this.assertOpen();
22
+ numbers("move", "x and y", x, y);
23
+ const to = { x, y };
24
+ await performPointer(this.connector, this.contextId, moveSteps(this.position, to, options?.steps));
25
+ this.position = to;
26
+ }
27
+ /** Presses a button (default left) where the pointer is, and keeps it down until `up` */
28
+ async down(options) {
29
+ this.assertOpen();
30
+ await performPointer(this.connector, this.contextId, [
31
+ {
32
+ type: "pointerDown",
33
+ button: buttonCode(options?.button, "mouse.down"),
34
+ },
35
+ ]);
36
+ }
37
+ /** Releases a button pressed with `down` */
38
+ async up(options) {
39
+ this.assertOpen();
40
+ await performPointer(this.connector, this.contextId, [
41
+ { type: "pointerUp", button: buttonCode(options?.button, "mouse.up") },
42
+ ]);
43
+ }
44
+ /**
45
+ * Moves to `x`, `y` and clicks. `clickCount` presses in a row (2 is a double click), `delay` ms between each press and release.
46
+ * @example
47
+ * await page.mouse.click(120, 80, { button: "right" });
48
+ */
49
+ async click(x, y, options) {
50
+ this.assertOpen();
51
+ numbers("click", "x and y", x, y);
52
+ checkClick(options ?? {}, "mouse.click");
53
+ await performPointer(this.connector, this.contextId, clickSteps({ x, y }, options, "mouse.click"));
54
+ this.position = { x, y };
55
+ }
56
+ /** `click` with two presses */
57
+ async dblclick(x, y, options) {
58
+ await this.click(x, y, { ...options, clickCount: 2 });
59
+ }
60
+ /** Turns the wheel where the pointer is by `deltaX` and `deltaY` pixels (positive is right and down) */
61
+ async wheel(deltaX, deltaY) {
62
+ this.assertOpen();
63
+ numbers("wheel", "deltaX and deltaY", deltaX, deltaY);
64
+ await wheelScroll(this.connector, this.contextId, this.position, deltaX, deltaY);
65
+ }
66
+ }
@@ -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
+ }