@cronn/playwright-utils 0.2.0 → 0.4.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.
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Normalizer } from "@cronn/playwright-file-snapshots";
2
- import { APIRequestContext, APIResponse, Locator, Page, Route } from "@playwright/test";
2
+ import { APIRequestContext, APIResponse, ConsoleMessage, Locator, Page, Request, Response, Route } from "@playwright/test";
3
3
  //#region src/environment.d.ts
4
4
  /**
5
5
  * Whether the current process runs in a CI environment, based on the `CI`
@@ -23,6 +23,151 @@ type FetchAPI = typeof fetch;
23
23
  */
24
24
  declare function createFetchAdapter(requestContext: APIRequestContext): FetchAPI;
25
25
  //#endregion
26
+ //#region src/capturing/console-captor.d.ts
27
+ /**
28
+ * Filter used to select the messages collected by a {@link ConsoleCaptor}.
29
+ */
30
+ type ConsoleMessageFilter = (message: ConsoleMessage) => boolean;
31
+ /**
32
+ * The log level of a {@link ConsoleMessage}, e.g. `log`, `warning` or `error`.
33
+ */
34
+ type ConsoleMessageLevel = ReturnType<ConsoleMessage["type"]>;
35
+ /**
36
+ * Collects the console messages of a page.
37
+ *
38
+ * Playwright reports the console output of a page through its `console` event,
39
+ * which has to be registered and removed again by hand. A captor bundles these
40
+ * steps, optionally filters the messages by level or a custom predicate and can
41
+ * limit the capturing to a single action.
42
+ *
43
+ * Several captors can run on the same page at the same time, each with its own
44
+ * filter, and every captor receives all matching messages independently.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * const logs = captureConsole(page);
49
+ *
50
+ * await logs.during(async () => {
51
+ * await page.goto("/users");
52
+ * await page.getByRole("button", { name: "Only enabled" }).click();
53
+ * });
54
+ *
55
+ * expect(logs.messages.map((message) => message.text())).toEqual([
56
+ * "filter changed: enabled",
57
+ * ]);
58
+ * ```
59
+ */
60
+ declare class ConsoleCaptor {
61
+ private readonly page;
62
+ private readonly consoleListener;
63
+ /**
64
+ * The captured messages, in the order in which the page reported them.
65
+ *
66
+ * The array is filled while the captor is running, so it can also be
67
+ * inspected inside the action passed to {@link during}.
68
+ */
69
+ readonly messages: Array<ConsoleMessage>;
70
+ /**
71
+ * Create a captor for the console messages of a page.
72
+ *
73
+ * The captor does not collect anything until it is started, either by
74
+ * {@link startCapture} or by {@link during}.
75
+ *
76
+ * @param page - The page to capture console messages of
77
+ * @param filter - Optional. Decides which messages are captured, captures every message if omitted
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * const captor = captureConsole(page, (message) =>
82
+ * message.location().url.endsWith("/analytics.js"),
83
+ * );
84
+ * ```
85
+ */
86
+ constructor(page: Page, filter?: ConsoleMessageFilter);
87
+ /**
88
+ * Start collecting the console messages of the page.
89
+ *
90
+ * A captor can be started and stopped repeatedly; the collected messages are
91
+ * kept across restarts.
92
+ *
93
+ * @example
94
+ * ```ts
95
+ * export const test = baseTest.extend<{ consoleErrors: ConsoleCaptor }>({
96
+ * consoleErrors: async ({ page }, use) => {
97
+ * const captor = captureConsole(page, "error");
98
+ * captor.start();
99
+ *
100
+ * await use(captor);
101
+ *
102
+ * captor.stop();
103
+ * expect(captor.messages.map((message) => message.text())).toEqual([]);
104
+ * },
105
+ * });
106
+ * ```
107
+ */
108
+ startCapture(): void;
109
+ /**
110
+ * Stop collecting the console messages of the page.
111
+ *
112
+ * The already captured {@link messages} are kept.
113
+ */
114
+ stopCapture(): void;
115
+ /**
116
+ * Collect the console messages reported while the callback runs.
117
+ *
118
+ * The capturing is stopped once the action has finished, even if it throws.
119
+ * A promise returned by the action is awaited before the captor stops, no
120
+ * matter when it is awaited by the caller.
121
+ *
122
+ * Only the returned value is awaited: asynchronous work which the action
123
+ * starts without returning it is not covered by the capturing.
124
+ *
125
+ * @param action - The action to capture console messages during
126
+ * @returns The value of the action
127
+ *
128
+ * @example
129
+ * ```ts
130
+ * const captor = captureConsole(page, "error");
131
+ *
132
+ * await captor.during(async () => {
133
+ * await page.getByRole("button", { name: "Create user" }).click();
134
+ * await expect(page.getByRole("alert")).toBeVisible();
135
+ * });
136
+ *
137
+ * expect(captor.messages).toHaveLength(1);
138
+ * ```
139
+ */
140
+ during<T>(action: () => Promise<T>): Promise<T>;
141
+ }
142
+ /**
143
+ * Factory method to create a {@link ConsoleCaptor} with the provided filter.
144
+ *
145
+ * The filter can be the log-level or a custom function on the message.
146
+ *
147
+ * @param page - The page to capture logs for
148
+ * @param levelOrFilter - The level or filter to filter out messages
149
+ * @returns ConsoleCaptor
150
+ *
151
+ * @example
152
+ * ```ts
153
+ * const errorCaptor = captureConsole(page, "error");
154
+ * const cspCaptor = captureConsole(page, message =>
155
+ * hasLogLevel(message, "warning", "error") && message.text().includes("Content Security Policy")
156
+ * );
157
+ * ```
158
+ */
159
+ declare function captureConsole(page: Page, levelOrFilter?: ConsoleMessageLevel | Array<ConsoleMessageLevel> | ConsoleMessageFilter): ConsoleCaptor;
160
+ /**
161
+ * Tests whether the given console message has one of the specified log levels.
162
+ *
163
+ * This can be useful to define a filter with {@link captureConsole}.
164
+ *
165
+ * @param message - The console message to check
166
+ * @param level - The log level
167
+ * @returns True if the message has one of the given log levels
168
+ */
169
+ declare function hasLogLevel(message: ConsoleMessage, ...level: Array<ConsoleMessageLevel>): boolean;
170
+ //#endregion
26
171
  //#region src/api/route-interceptor/filter.d.ts
27
172
  /**
28
173
  * HTTP Methods used by {@link RouteFilter}
@@ -178,6 +323,20 @@ declare class RouteInterceptor {
178
323
  * ```
179
324
  */
180
325
  modifyResponse(modifier: ResponseHandler): ExecutableRouteInterceptor;
326
+ /**
327
+ * Wait until the target route was called and return the client request.
328
+ *
329
+ * @param options - Options for {@link Page#waitForRequest}
330
+ * @returns Response - The server response
331
+ */
332
+ waitForRequest(options?: Parameters<Page["waitForRequest"]>[1]): Promise<Request>;
333
+ /**
334
+ * Wait until the target route was called and return the server response.
335
+ *
336
+ * @param options - Options for {@link Page#waitForResponse}
337
+ * @returns Response - The server response
338
+ */
339
+ waitForResponse(options?: Parameters<Page["waitForResponse"]>[1]): Promise<Response>;
181
340
  }
182
341
  /**
183
342
  * Stub returned by {@link RouteInterceptor} used to intercept routes during a block.
@@ -320,4 +479,4 @@ declare function maskedValueWithIndex(name: string): (index: number) => string;
320
479
  */
321
480
  type PlaywrightTarget = Page | Locator;
322
481
  //#endregion
323
- export { type HttpMethod, type PlaywrightTarget, type ResponseHandler, type RouteFilter, RouteInterceptor, RouteInterceptorFixture, createFetchAdapter, interceptRoute, isCI, maskBaseURL, maskedValue, maskedValueWithIndex, matchPath, modifyJsonBody, modifyTextBody, resolveFromPackageRoot };
482
+ export { ConsoleCaptor, type ConsoleMessageFilter, type ConsoleMessageLevel, type HttpMethod, type PlaywrightTarget, type ResponseHandler, type RouteFilter, RouteInterceptor, RouteInterceptorFixture, captureConsole, createFetchAdapter, hasLogLevel, interceptRoute, isCI, maskBaseURL, maskedValue, maskedValueWithIndex, matchPath, modifyJsonBody, modifyTextBody, resolveFromPackageRoot };
package/dist/index.mjs CHANGED
@@ -54,6 +54,169 @@ function mapToRequestBody(body) {
54
54
  return { data: body };
55
55
  }
56
56
  //#endregion
57
+ //#region src/capturing/console-captor.ts
58
+ function defaultFilter(_message) {
59
+ return true;
60
+ }
61
+ /**
62
+ * Collects the console messages of a page.
63
+ *
64
+ * Playwright reports the console output of a page through its `console` event,
65
+ * which has to be registered and removed again by hand. A captor bundles these
66
+ * steps, optionally filters the messages by level or a custom predicate and can
67
+ * limit the capturing to a single action.
68
+ *
69
+ * Several captors can run on the same page at the same time, each with its own
70
+ * filter, and every captor receives all matching messages independently.
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * const logs = captureConsole(page);
75
+ *
76
+ * await logs.during(async () => {
77
+ * await page.goto("/users");
78
+ * await page.getByRole("button", { name: "Only enabled" }).click();
79
+ * });
80
+ *
81
+ * expect(logs.messages.map((message) => message.text())).toEqual([
82
+ * "filter changed: enabled",
83
+ * ]);
84
+ * ```
85
+ */
86
+ var ConsoleCaptor = class {
87
+ page;
88
+ consoleListener;
89
+ /**
90
+ * The captured messages, in the order in which the page reported them.
91
+ *
92
+ * The array is filled while the captor is running, so it can also be
93
+ * inspected inside the action passed to {@link during}.
94
+ */
95
+ messages = [];
96
+ /**
97
+ * Create a captor for the console messages of a page.
98
+ *
99
+ * The captor does not collect anything until it is started, either by
100
+ * {@link startCapture} or by {@link during}.
101
+ *
102
+ * @param page - The page to capture console messages of
103
+ * @param filter - Optional. Decides which messages are captured, captures every message if omitted
104
+ *
105
+ * @example
106
+ * ```ts
107
+ * const captor = captureConsole(page, (message) =>
108
+ * message.location().url.endsWith("/analytics.js"),
109
+ * );
110
+ * ```
111
+ */
112
+ constructor(page, filter = defaultFilter) {
113
+ this.page = page;
114
+ this.consoleListener = (event) => {
115
+ if (filter(event)) this.messages.push(event);
116
+ };
117
+ }
118
+ /**
119
+ * Start collecting the console messages of the page.
120
+ *
121
+ * A captor can be started and stopped repeatedly; the collected messages are
122
+ * kept across restarts.
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * export const test = baseTest.extend<{ consoleErrors: ConsoleCaptor }>({
127
+ * consoleErrors: async ({ page }, use) => {
128
+ * const captor = captureConsole(page, "error");
129
+ * captor.start();
130
+ *
131
+ * await use(captor);
132
+ *
133
+ * captor.stop();
134
+ * expect(captor.messages.map((message) => message.text())).toEqual([]);
135
+ * },
136
+ * });
137
+ * ```
138
+ */
139
+ startCapture() {
140
+ this.page.on("console", this.consoleListener);
141
+ }
142
+ /**
143
+ * Stop collecting the console messages of the page.
144
+ *
145
+ * The already captured {@link messages} are kept.
146
+ */
147
+ stopCapture() {
148
+ this.page.off("console", this.consoleListener);
149
+ }
150
+ /**
151
+ * Collect the console messages reported while the callback runs.
152
+ *
153
+ * The capturing is stopped once the action has finished, even if it throws.
154
+ * A promise returned by the action is awaited before the captor stops, no
155
+ * matter when it is awaited by the caller.
156
+ *
157
+ * Only the returned value is awaited: asynchronous work which the action
158
+ * starts without returning it is not covered by the capturing.
159
+ *
160
+ * @param action - The action to capture console messages during
161
+ * @returns The value of the action
162
+ *
163
+ * @example
164
+ * ```ts
165
+ * const captor = captureConsole(page, "error");
166
+ *
167
+ * await captor.during(async () => {
168
+ * await page.getByRole("button", { name: "Create user" }).click();
169
+ * await expect(page.getByRole("alert")).toBeVisible();
170
+ * });
171
+ *
172
+ * expect(captor.messages).toHaveLength(1);
173
+ * ```
174
+ */
175
+ async during(action) {
176
+ this.startCapture();
177
+ try {
178
+ return await action();
179
+ } finally {
180
+ this.stopCapture();
181
+ }
182
+ }
183
+ };
184
+ /**
185
+ * Factory method to create a {@link ConsoleCaptor} with the provided filter.
186
+ *
187
+ * The filter can be the log-level or a custom function on the message.
188
+ *
189
+ * @param page - The page to capture logs for
190
+ * @param levelOrFilter - The level or filter to filter out messages
191
+ * @returns ConsoleCaptor
192
+ *
193
+ * @example
194
+ * ```ts
195
+ * const errorCaptor = captureConsole(page, "error");
196
+ * const cspCaptor = captureConsole(page, message =>
197
+ * hasLogLevel(message, "warning", "error") && message.text().includes("Content Security Policy")
198
+ * );
199
+ * ```
200
+ */
201
+ function captureConsole(page, levelOrFilter) {
202
+ if (levelOrFilter === void 0) return new ConsoleCaptor(page);
203
+ if (typeof levelOrFilter === "function") return new ConsoleCaptor(page, levelOrFilter);
204
+ if (Array.isArray(levelOrFilter)) return new ConsoleCaptor(page, (message) => hasLogLevel(message, ...levelOrFilter));
205
+ return new ConsoleCaptor(page, (message) => hasLogLevel(message, levelOrFilter));
206
+ }
207
+ /**
208
+ * Tests whether the given console message has one of the specified log levels.
209
+ *
210
+ * This can be useful to define a filter with {@link captureConsole}.
211
+ *
212
+ * @param message - The console message to check
213
+ * @param level - The log level
214
+ * @returns True if the message has one of the given log levels
215
+ */
216
+ function hasLogLevel(message, ...level) {
217
+ return level.includes(message.type());
218
+ }
219
+ //#endregion
57
220
  //#region src/api/route-interceptor/interceptor.ts
58
221
  function isFiltered(filter, request) {
59
222
  if (!filter.url(new URL(request.url()))) return false;
@@ -101,7 +264,7 @@ async function withSuspendedRoute(page, filter, action) {
101
264
  await route.fallback();
102
265
  return;
103
266
  }
104
- const suspendedRoute = barrier.promise.then(() => route.continue());
267
+ const suspendedRoute = barrier.promise.then(() => route.fallback());
105
268
  suspendedRoutes.add(suspendedRoute);
106
269
  try {
107
270
  await suspendedRoute;
@@ -224,6 +387,24 @@ var RouteInterceptor = class {
224
387
  modifyResponse(modifier) {
225
388
  return new ExecutableRouteInterceptor((action) => withModifiedResponse(this.page, this.filter, modifier, action));
226
389
  }
390
+ /**
391
+ * Wait until the target route was called and return the client request.
392
+ *
393
+ * @param options - Options for {@link Page#waitForRequest}
394
+ * @returns Response - The server response
395
+ */
396
+ waitForRequest(options) {
397
+ return this.page.waitForRequest((request) => isFiltered(this.filter, request), options);
398
+ }
399
+ /**
400
+ * Wait until the target route was called and return the server response.
401
+ *
402
+ * @param options - Options for {@link Page#waitForResponse}
403
+ * @returns Response - The server response
404
+ */
405
+ waitForResponse(options) {
406
+ return this.page.waitForResponse((response) => isFiltered(this.filter, response.request()), options);
407
+ }
227
408
  };
228
409
  /**
229
410
  * Stub returned by {@link RouteInterceptor} used to intercept routes during a block.
@@ -446,4 +627,4 @@ function identity(value) {
446
627
  return value;
447
628
  }
448
629
  //#endregion
449
- export { RouteInterceptor, RouteInterceptorFixture, createFetchAdapter, interceptRoute, isCI, maskBaseURL, maskedValue, maskedValueWithIndex, matchPath, modifyJsonBody, modifyTextBody, resolveFromPackageRoot };
630
+ export { ConsoleCaptor, RouteInterceptor, RouteInterceptorFixture, captureConsole, createFetchAdapter, hasLogLevel, interceptRoute, isCI, maskBaseURL, maskedValue, maskedValueWithIndex, matchPath, modifyJsonBody, modifyTextBody, resolveFromPackageRoot };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cronn/playwright-utils",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Utilities for writing tests with Playwright",
5
5
  "keywords": [
6
6
  "playwright",
@@ -29,30 +29,30 @@
29
29
  }
30
30
  },
31
31
  "dependencies": {
32
- "package-directory": "8.2.0"
32
+ "package-directory": "^8.2.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@arethetypeswrong/core": "0.18.5",
36
36
  "@changesets/cli": "3.0.1",
37
- "@cronn/element-snapshot": "0.27.0",
37
+ "@cronn/element-snapshot": "0.28.0",
38
38
  "@cronn/playwright-file-snapshots": "2.2.1",
39
39
  "@playwright/test": "1.62.1",
40
40
  "@types/node": "24.13.3",
41
41
  "@typescript/native": "npm:typescript@^7.0.2",
42
42
  "@vitest/coverage-v8": "4.1.11",
43
- "eslint": "10.9.0",
43
+ "eslint": "10.9.1",
44
44
  "eslint-config-prettier": "10.1.8",
45
45
  "eslint-plugin-check-file": "3.3.2",
46
46
  "eslint-plugin-unused-imports": "4.4.1",
47
- "knip": "6.32.2",
48
- "oxc-minify": "0.146.0",
49
- "oxfmt": "0.64.0",
47
+ "knip": "6.33.0",
48
+ "oxc-minify": "0.147.0",
49
+ "oxfmt": "0.65.0",
50
50
  "publint": "0.3.24",
51
51
  "rimraf": "6.1.3",
52
52
  "tsdown": "0.22.14",
53
- "turbo": "2.10.11",
53
+ "turbo": "2.10.12",
54
54
  "typescript": "npm:@typescript/typescript6@^6.0.2",
55
- "typescript-eslint": "8.67.0",
55
+ "typescript-eslint": "8.68.0",
56
56
  "vitepress": "2.0.0-alpha.19",
57
57
  "vitepress-plugin-group-icons": "1.7.6",
58
58
  "vitest": "4.1.11",