@cronn/playwright-utils 0.3.0 → 0.5.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, Request, Response, 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,166 @@ 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
+ * Remove all captured {@link messages}.
117
+ *
118
+ * @example
119
+ * ```ts
120
+ * captor.startCapture();
121
+ *
122
+ * await page.goto("/users");
123
+ * captor.clearMessages();
124
+ *
125
+ * await page.getByRole("button", { name: "Only enabled" }).click();
126
+ * expect(captor.messages).toHaveLength(1);
127
+ * ```
128
+ */
129
+ clearMessages(): void;
130
+ /**
131
+ * Collect the console messages reported while the callback runs.
132
+ *
133
+ * The capturing is stopped once the action has finished, even if it throws.
134
+ * A promise returned by the action is awaited before the captor stops, no
135
+ * matter when it is awaited by the caller.
136
+ *
137
+ * Only the returned value is awaited: asynchronous work which the action
138
+ * starts without returning it is not covered by the capturing.
139
+ *
140
+ * @param action - The action to capture console messages during
141
+ * @returns The value of the action
142
+ *
143
+ * @example
144
+ * ```ts
145
+ * const captor = captureConsole(page, "error");
146
+ *
147
+ * await captor.during(async () => {
148
+ * await page.getByRole("button", { name: "Create user" }).click();
149
+ * await expect(page.getByRole("alert")).toBeVisible();
150
+ * });
151
+ *
152
+ * expect(captor.messages).toHaveLength(1);
153
+ * ```
154
+ */
155
+ during<T>(action: () => Promise<T>): Promise<T>;
156
+ }
157
+ /**
158
+ * Factory method to create a {@link ConsoleCaptor} with the provided filter.
159
+ *
160
+ * The filter can be the log-level or a custom function on the message.
161
+ *
162
+ * @param page - The page to capture logs for
163
+ * @param levelOrFilter - The level or filter to filter out messages
164
+ * @returns ConsoleCaptor
165
+ *
166
+ * @example
167
+ * ```ts
168
+ * const errorCaptor = captureConsole(page, "error");
169
+ * const cspCaptor = captureConsole(page, message =>
170
+ * hasLogLevel(message, "warning", "error") && message.text().includes("Content Security Policy")
171
+ * );
172
+ * ```
173
+ */
174
+ declare function captureConsole(page: Page, levelOrFilter?: ConsoleMessageLevel | Array<ConsoleMessageLevel> | ConsoleMessageFilter): ConsoleCaptor;
175
+ /**
176
+ * Tests whether the given console message has one of the specified log levels.
177
+ *
178
+ * This can be useful to define a filter with {@link captureConsole}.
179
+ *
180
+ * @param message - The console message to check
181
+ * @param level - The log level
182
+ * @returns True if the message has one of the given log levels
183
+ */
184
+ declare function hasLogLevel(message: ConsoleMessage, ...level: Array<ConsoleMessageLevel>): boolean;
185
+ //#endregion
26
186
  //#region src/api/route-interceptor/filter.d.ts
27
187
  /**
28
188
  * HTTP Methods used by {@link RouteFilter}
@@ -334,4 +494,4 @@ declare function maskedValueWithIndex(name: string): (index: number) => string;
334
494
  */
335
495
  type PlaywrightTarget = Page | Locator;
336
496
  //#endregion
337
- export { type HttpMethod, type PlaywrightTarget, type ResponseHandler, type RouteFilter, RouteInterceptor, RouteInterceptorFixture, createFetchAdapter, interceptRoute, isCI, maskBaseURL, maskedValue, maskedValueWithIndex, matchPath, modifyJsonBody, modifyTextBody, resolveFromPackageRoot };
497
+ 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,186 @@ 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
+ * Remove all captured {@link messages}.
152
+ *
153
+ * @example
154
+ * ```ts
155
+ * captor.startCapture();
156
+ *
157
+ * await page.goto("/users");
158
+ * captor.clearMessages();
159
+ *
160
+ * await page.getByRole("button", { name: "Only enabled" }).click();
161
+ * expect(captor.messages).toHaveLength(1);
162
+ * ```
163
+ */
164
+ clearMessages() {
165
+ this.messages.length = 0;
166
+ }
167
+ /**
168
+ * Collect the console messages reported while the callback runs.
169
+ *
170
+ * The capturing is stopped once the action has finished, even if it throws.
171
+ * A promise returned by the action is awaited before the captor stops, no
172
+ * matter when it is awaited by the caller.
173
+ *
174
+ * Only the returned value is awaited: asynchronous work which the action
175
+ * starts without returning it is not covered by the capturing.
176
+ *
177
+ * @param action - The action to capture console messages during
178
+ * @returns The value of the action
179
+ *
180
+ * @example
181
+ * ```ts
182
+ * const captor = captureConsole(page, "error");
183
+ *
184
+ * await captor.during(async () => {
185
+ * await page.getByRole("button", { name: "Create user" }).click();
186
+ * await expect(page.getByRole("alert")).toBeVisible();
187
+ * });
188
+ *
189
+ * expect(captor.messages).toHaveLength(1);
190
+ * ```
191
+ */
192
+ async during(action) {
193
+ this.startCapture();
194
+ try {
195
+ return await action();
196
+ } finally {
197
+ this.stopCapture();
198
+ }
199
+ }
200
+ };
201
+ /**
202
+ * Factory method to create a {@link ConsoleCaptor} with the provided filter.
203
+ *
204
+ * The filter can be the log-level or a custom function on the message.
205
+ *
206
+ * @param page - The page to capture logs for
207
+ * @param levelOrFilter - The level or filter to filter out messages
208
+ * @returns ConsoleCaptor
209
+ *
210
+ * @example
211
+ * ```ts
212
+ * const errorCaptor = captureConsole(page, "error");
213
+ * const cspCaptor = captureConsole(page, message =>
214
+ * hasLogLevel(message, "warning", "error") && message.text().includes("Content Security Policy")
215
+ * );
216
+ * ```
217
+ */
218
+ function captureConsole(page, levelOrFilter) {
219
+ if (levelOrFilter === void 0) return new ConsoleCaptor(page);
220
+ if (typeof levelOrFilter === "function") return new ConsoleCaptor(page, levelOrFilter);
221
+ if (Array.isArray(levelOrFilter)) return new ConsoleCaptor(page, (message) => hasLogLevel(message, ...levelOrFilter));
222
+ return new ConsoleCaptor(page, (message) => hasLogLevel(message, levelOrFilter));
223
+ }
224
+ /**
225
+ * Tests whether the given console message has one of the specified log levels.
226
+ *
227
+ * This can be useful to define a filter with {@link captureConsole}.
228
+ *
229
+ * @param message - The console message to check
230
+ * @param level - The log level
231
+ * @returns True if the message has one of the given log levels
232
+ */
233
+ function hasLogLevel(message, ...level) {
234
+ return level.includes(message.type());
235
+ }
236
+ //#endregion
57
237
  //#region src/api/route-interceptor/interceptor.ts
58
238
  function isFiltered(filter, request) {
59
239
  if (!filter.url(new URL(request.url()))) return false;
@@ -101,7 +281,7 @@ async function withSuspendedRoute(page, filter, action) {
101
281
  await route.fallback();
102
282
  return;
103
283
  }
104
- const suspendedRoute = barrier.promise.then(() => route.continue());
284
+ const suspendedRoute = barrier.promise.then(() => route.fallback());
105
285
  suspendedRoutes.add(suspendedRoute);
106
286
  try {
107
287
  await suspendedRoute;
@@ -464,4 +644,4 @@ function identity(value) {
464
644
  return value;
465
645
  }
466
646
  //#endregion
467
- export { RouteInterceptor, RouteInterceptorFixture, createFetchAdapter, interceptRoute, isCI, maskBaseURL, maskedValue, maskedValueWithIndex, matchPath, modifyJsonBody, modifyTextBody, resolveFromPackageRoot };
647
+ 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.3.0",
3
+ "version": "0.5.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",