@cronn/playwright-utils 0.6.0 → 0.7.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
+ import { APIRequestContext, APIResponse, ConsoleMessage, Fixtures, Locator, Page, PlaywrightTestArgs, PlaywrightTestOptions, Request, Response, Route } from "@playwright/test";
1
2
  import { Normalizer } from "@cronn/playwright-file-snapshots";
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`
@@ -166,9 +166,6 @@ export declare class ConsoleCaptor {
166
166
  * @example
167
167
  * ```ts
168
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
169
  * ```
173
170
  */
174
171
  export declare function captureConsole(page: Page, levelOrFilter?: ConsoleMessageLevel | Array<ConsoleMessageLevel> | ConsoleMessageFilter): ConsoleCaptor;
@@ -183,6 +180,153 @@ export declare function captureConsole(page: Page, levelOrFilter?: ConsoleMessag
183
180
  */
184
181
  export declare function hasLogLevel(message: ConsoleMessage, ...level: Array<ConsoleMessageLevel>): boolean;
185
182
  //#endregion
183
+ //#region src/capturing/csp-captor.d.ts
184
+ /**
185
+ * A Content Security Policy violation, as reported by the browser's
186
+ * [`securitypolicyviolation`](https://developer.mozilla.org/en-US/docs/Web/API/Document/securitypolicyviolation_event) event.
187
+ */
188
+ type CspViolation = SecurityPolicyViolationEventInit;
189
+ /**
190
+ * Collects the Content Security Policy violations of a page.
191
+ *
192
+ * Browsers report CSP violations through the `securitypolicyviolation` DOM event,
193
+ * which is not otherwise exposed by Playwright. A captor registers a listener for
194
+ * this event on every document of the page and collects the reported violations,
195
+ * and can limit the capturing to a single action.
196
+ *
197
+ * Several captors can run on the same page at the same time, and every captor
198
+ * receives all violations independently.
199
+ *
200
+ * @example
201
+ * ```ts
202
+ * const violations = captureCspViolations(page);
203
+ *
204
+ * await violations.during(() => page.goto("/users"));
205
+ *
206
+ * expect(violations.violations).toEqual([]);
207
+ * ```
208
+ */
209
+ export declare class CspCaptor {
210
+ private readonly page;
211
+ private readonly bindingName;
212
+ private initialized;
213
+ private capturing;
214
+ /**
215
+ * The captured violations, in the order in which the page reported them.
216
+ *
217
+ * The array is filled while the captor is running, so it can also be
218
+ * inspected inside the action passed to {@link during}.
219
+ */
220
+ readonly violations: Array<CspViolation>;
221
+ /**
222
+ * Create a captor for the CSP violations of a page.
223
+ *
224
+ * The captor does not collect anything until it is started, either by
225
+ * {@link startCapture} or by {@link during}.
226
+ *
227
+ * @param page - The page to capture CSP violations of
228
+ */
229
+ constructor(page: Page);
230
+ private ensureInitialized;
231
+ /**
232
+ * Start collecting the CSP violations of the page.
233
+ *
234
+ * A captor can be started and stopped repeatedly; the collected violations are
235
+ * kept across restarts.
236
+ *
237
+ * Because violations are captured through an init script, {@link startCapture}
238
+ * has to be called before the page navigates for violations of the initial
239
+ * load to be captured.
240
+ */
241
+ startCapture(): Promise<void>;
242
+ /**
243
+ * Stop collecting the CSP violations of the page.
244
+ *
245
+ * The already captured {@link violations} are kept.
246
+ */
247
+ stopCapture(): void;
248
+ /**
249
+ * Remove all captured {@link violations}.
250
+ */
251
+ clearViolations(): void;
252
+ /**
253
+ * Collect the CSP violations reported while the callback runs.
254
+ *
255
+ * The capturing is stopped once the action has finished, even if it throws.
256
+ * A promise returned by the action is awaited before the captor stops, no
257
+ * matter when it is awaited by the caller.
258
+ *
259
+ * @param action - The action to capture CSP violations during
260
+ * @returns The value of the action
261
+ *
262
+ * @example
263
+ * ```ts
264
+ * const captor = captureCspViolations(page);
265
+ *
266
+ * await captor.during(() => page.goto("/users"));
267
+ *
268
+ * expect(captor.violations).toEqual([]);
269
+ * ```
270
+ */
271
+ during<T>(action: () => Promise<T>): Promise<T>;
272
+ }
273
+ /**
274
+ * Factory method to create a {@link CspCaptor}.
275
+ *
276
+ * @param page - The page to capture CSP violations for
277
+ * @returns CspCaptor
278
+ *
279
+ * @example
280
+ * ```ts
281
+ * const captor = captureCspViolations(page);
282
+ * ```
283
+ */
284
+ export declare function captureCspViolations(page: Page): CspCaptor;
285
+ interface CspFixtures {
286
+ /**
287
+ * A {@link CspCaptor} capturing every CSP violation of the page, available for
288
+ * tests that want to trigger and assert on a specific violation.
289
+ */
290
+ cspCaptor: CspCaptor;
291
+ /**
292
+ * Option fixture controlling whether {@link cspFixtures} skips failing
293
+ * a test that produced unexpected CSP violations.
294
+ *
295
+ * @default false
296
+ */
297
+ ignoreCspViolations: boolean;
298
+ }
299
+ /**
300
+ * Playwright fixtures reporting Content Security Policy violations for every test.
301
+ *
302
+ * `cspCaptor` is installed automatically on the page fixture and, by default, fails
303
+ * a test if it produced any CSP violation. Disable this for an individual test with
304
+ * `test.use({ ignoreCspViolations: true })`, and inspect `cspCaptor.violations`
305
+ * manually for tests targeting CSP behavior specifically.
306
+ *
307
+ * @example
308
+ * ```ts
309
+ * import { cspFixtures, type CspFixtures } from "@cronn/playwright-utils";
310
+ * import { test as base } from "@playwright/test";
311
+ *
312
+ * export const test = base.extend<CspFixtures>({
313
+ * ...cspFixtures,
314
+ * // ...other fixtures
315
+ * });
316
+ * ```
317
+ *
318
+ * @example
319
+ * ```ts
320
+ * test.use({ ignoreCspViolations: true });
321
+ *
322
+ * test("shows a warning banner on CSP violation", async ({ page, cspCaptor }) => {
323
+ * await page.getByRole("button", { name: "Load untrusted script" }).click();
324
+ * expect(cspCaptor.violations).toHaveLength(1);
325
+ * });
326
+ * ```
327
+ */
328
+ export declare const cspFixtures: Fixtures<CspFixtures, object, PlaywrightTestArgs & PlaywrightTestOptions>;
329
+ //#endregion
186
330
  //#region src/api/route-interceptor/filter.d.ts
187
331
  /**
188
332
  * HTTP Methods used by {@link RouteFilter}
@@ -529,4 +673,4 @@ export declare function maskedValueWithIndex(name: string): (index: number) => s
529
673
  */
530
674
  type PlaywrightTarget = Page | Locator;
531
675
  //#endregion
532
- export type { ConsoleMessageFilter, ConsoleMessageLevel, HttpMethod, PlaywrightTarget, ResponseHandler, RoleLocators, RouteFilter };
676
+ export type { ConsoleMessageFilter, ConsoleMessageLevel, CspFixtures, CspViolation, HttpMethod, PlaywrightTarget, ResponseHandler, RoleLocators, RouteFilter };
package/dist/index.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { expect } from "@playwright/test";
1
2
  import path from "node:path";
2
3
  import { packageDirectorySync } from "package-directory";
3
4
  import { maskString, stringNormalizer } from "@cronn/playwright-file-snapshots";
@@ -210,9 +211,6 @@ var ConsoleCaptor = class {
210
211
  * @example
211
212
  * ```ts
212
213
  * const errorCaptor = captureConsole(page, "error");
213
- * const cspCaptor = captureConsole(page, message =>
214
- * hasLogLevel(message, "warning", "error") && message.text().includes("Content Security Policy")
215
- * );
216
214
  * ```
217
215
  */
218
216
  function captureConsole(page, levelOrFilter) {
@@ -234,6 +232,188 @@ function hasLogLevel(message, ...level) {
234
232
  return level.includes(message.type());
235
233
  }
236
234
  //#endregion
235
+ //#region src/capturing/csp-captor.ts
236
+ let nextBindingId = 0;
237
+ /**
238
+ * Collects the Content Security Policy violations of a page.
239
+ *
240
+ * Browsers report CSP violations through the `securitypolicyviolation` DOM event,
241
+ * which is not otherwise exposed by Playwright. A captor registers a listener for
242
+ * this event on every document of the page and collects the reported violations,
243
+ * and can limit the capturing to a single action.
244
+ *
245
+ * Several captors can run on the same page at the same time, and every captor
246
+ * receives all violations independently.
247
+ *
248
+ * @example
249
+ * ```ts
250
+ * const violations = captureCspViolations(page);
251
+ *
252
+ * await violations.during(() => page.goto("/users"));
253
+ *
254
+ * expect(violations.violations).toEqual([]);
255
+ * ```
256
+ */
257
+ var CspCaptor = class {
258
+ page;
259
+ bindingName = `__playwrightUtilsCspCaptor${nextBindingId++}__`;
260
+ initialized = false;
261
+ capturing = false;
262
+ /**
263
+ * The captured violations, in the order in which the page reported them.
264
+ *
265
+ * The array is filled while the captor is running, so it can also be
266
+ * inspected inside the action passed to {@link during}.
267
+ */
268
+ violations = [];
269
+ /**
270
+ * Create a captor for the CSP violations of a page.
271
+ *
272
+ * The captor does not collect anything until it is started, either by
273
+ * {@link startCapture} or by {@link during}.
274
+ *
275
+ * @param page - The page to capture CSP violations of
276
+ */
277
+ constructor(page) {
278
+ this.page = page;
279
+ }
280
+ async ensureInitialized() {
281
+ if (this.initialized) return;
282
+ this.initialized = true;
283
+ await this.page.exposeFunction(this.bindingName, (violation) => {
284
+ if (this.capturing) this.violations.push(violation);
285
+ });
286
+ await this.page.addInitScript((bindingName) => {
287
+ document.addEventListener("securitypolicyviolation", (event) => {
288
+ const report = window[bindingName];
289
+ report({
290
+ blockedURI: event.blockedURI,
291
+ disposition: event.disposition,
292
+ documentURI: event.documentURI,
293
+ effectiveDirective: event.effectiveDirective,
294
+ lineNumber: event.lineNumber,
295
+ columnNumber: event.columnNumber,
296
+ originalPolicy: event.originalPolicy,
297
+ referrer: event.referrer,
298
+ sample: event.sample,
299
+ sourceFile: event.sourceFile,
300
+ statusCode: event.statusCode,
301
+ violatedDirective: event.violatedDirective
302
+ });
303
+ });
304
+ }, this.bindingName);
305
+ }
306
+ /**
307
+ * Start collecting the CSP violations of the page.
308
+ *
309
+ * A captor can be started and stopped repeatedly; the collected violations are
310
+ * kept across restarts.
311
+ *
312
+ * Because violations are captured through an init script, {@link startCapture}
313
+ * has to be called before the page navigates for violations of the initial
314
+ * load to be captured.
315
+ */
316
+ async startCapture() {
317
+ await this.ensureInitialized();
318
+ this.capturing = true;
319
+ }
320
+ /**
321
+ * Stop collecting the CSP violations of the page.
322
+ *
323
+ * The already captured {@link violations} are kept.
324
+ */
325
+ stopCapture() {
326
+ this.capturing = false;
327
+ }
328
+ /**
329
+ * Remove all captured {@link violations}.
330
+ */
331
+ clearViolations() {
332
+ this.violations.length = 0;
333
+ }
334
+ /**
335
+ * Collect the CSP violations reported while the callback runs.
336
+ *
337
+ * The capturing is stopped once the action has finished, even if it throws.
338
+ * A promise returned by the action is awaited before the captor stops, no
339
+ * matter when it is awaited by the caller.
340
+ *
341
+ * @param action - The action to capture CSP violations during
342
+ * @returns The value of the action
343
+ *
344
+ * @example
345
+ * ```ts
346
+ * const captor = captureCspViolations(page);
347
+ *
348
+ * await captor.during(() => page.goto("/users"));
349
+ *
350
+ * expect(captor.violations).toEqual([]);
351
+ * ```
352
+ */
353
+ async during(action) {
354
+ await this.startCapture();
355
+ try {
356
+ return await action();
357
+ } finally {
358
+ this.stopCapture();
359
+ }
360
+ }
361
+ };
362
+ /**
363
+ * Factory method to create a {@link CspCaptor}.
364
+ *
365
+ * @param page - The page to capture CSP violations for
366
+ * @returns CspCaptor
367
+ *
368
+ * @example
369
+ * ```ts
370
+ * const captor = captureCspViolations(page);
371
+ * ```
372
+ */
373
+ function captureCspViolations(page) {
374
+ return new CspCaptor(page);
375
+ }
376
+ async function provideCspCaptor({ page, ignoreCspViolations }, use) {
377
+ const captor = captureCspViolations(page);
378
+ await captor.startCapture();
379
+ await use(captor);
380
+ captor.stopCapture();
381
+ if (!ignoreCspViolations) expect(captor.violations, "Unexpected CSP violations").toEqual([]);
382
+ }
383
+ /**
384
+ * Playwright fixtures reporting Content Security Policy violations for every test.
385
+ *
386
+ * `cspCaptor` is installed automatically on the page fixture and, by default, fails
387
+ * a test if it produced any CSP violation. Disable this for an individual test with
388
+ * `test.use({ ignoreCspViolations: true })`, and inspect `cspCaptor.violations`
389
+ * manually for tests targeting CSP behavior specifically.
390
+ *
391
+ * @example
392
+ * ```ts
393
+ * import { cspFixtures, type CspFixtures } from "@cronn/playwright-utils";
394
+ * import { test as base } from "@playwright/test";
395
+ *
396
+ * export const test = base.extend<CspFixtures>({
397
+ * ...cspFixtures,
398
+ * // ...other fixtures
399
+ * });
400
+ * ```
401
+ *
402
+ * @example
403
+ * ```ts
404
+ * test.use({ ignoreCspViolations: true });
405
+ *
406
+ * test("shows a warning banner on CSP violation", async ({ page, cspCaptor }) => {
407
+ * await page.getByRole("button", { name: "Load untrusted script" }).click();
408
+ * expect(cspCaptor.violations).toHaveLength(1);
409
+ * });
410
+ * ```
411
+ */
412
+ const cspFixtures = {
413
+ ignoreCspViolations: [false, { option: true }],
414
+ cspCaptor: [provideCspCaptor, { auto: true }]
415
+ };
416
+ //#endregion
237
417
  //#region src/api/route-interceptor/interceptor.ts
238
418
  function isFiltered(filter, request) {
239
419
  if (!filter.url(new URL(request.url()))) return false;
@@ -771,4 +951,4 @@ function identity(value) {
771
951
  return value;
772
952
  }
773
953
  //#endregion
774
- export { ConsoleCaptor, RouteInterceptor, RouteInterceptorFixture, captureConsole, createFetchAdapter, hasLogLevel, interceptRoute, isCI, maskBaseURL, maskedValue, maskedValueWithIndex, matchPath, modifyJsonBody, modifyTextBody, resolveFromPackageRoot, roleLocators };
954
+ export { ConsoleCaptor, CspCaptor, RouteInterceptor, RouteInterceptorFixture, captureConsole, captureCspViolations, createFetchAdapter, cspFixtures, hasLogLevel, interceptRoute, isCI, maskBaseURL, maskedValue, maskedValueWithIndex, matchPath, modifyJsonBody, modifyTextBody, resolveFromPackageRoot, roleLocators };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cronn/playwright-utils",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Utilities for writing tests with Playwright",
5
5
  "keywords": [
6
6
  "playwright",