@cronn/playwright-utils 0.5.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 +199 -20
- package/dist/index.mjs +311 -4
- package/package.json +17 -18
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`
|
|
@@ -7,7 +7,7 @@ import { APIRequestContext, APIResponse, ConsoleMessage, Locator, Page, Request,
|
|
|
7
7
|
*
|
|
8
8
|
* Evaluated once when the module is loaded.
|
|
9
9
|
*/
|
|
10
|
-
declare const isCI: boolean;
|
|
10
|
+
export declare const isCI: boolean;
|
|
11
11
|
//#endregion
|
|
12
12
|
//#region src/api/fetch-adapter.d.ts
|
|
13
13
|
type FetchAPI = typeof fetch;
|
|
@@ -21,7 +21,7 @@ type FetchAPI = typeof fetch;
|
|
|
21
21
|
* @param requestContext - Playwright request context to send requests with
|
|
22
22
|
* @returns A function with the signature of the global `fetch`
|
|
23
23
|
*/
|
|
24
|
-
declare function createFetchAdapter(requestContext: APIRequestContext): FetchAPI;
|
|
24
|
+
export declare function createFetchAdapter(requestContext: APIRequestContext): FetchAPI;
|
|
25
25
|
//#endregion
|
|
26
26
|
//#region src/capturing/console-captor.d.ts
|
|
27
27
|
/**
|
|
@@ -57,7 +57,7 @@ type ConsoleMessageLevel = ReturnType<ConsoleMessage["type"]>;
|
|
|
57
57
|
* ]);
|
|
58
58
|
* ```
|
|
59
59
|
*/
|
|
60
|
-
declare class ConsoleCaptor {
|
|
60
|
+
export declare class ConsoleCaptor {
|
|
61
61
|
private readonly page;
|
|
62
62
|
private readonly consoleListener;
|
|
63
63
|
/**
|
|
@@ -166,12 +166,9 @@ 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
|
-
declare function captureConsole(page: Page, levelOrFilter?: ConsoleMessageLevel | Array<ConsoleMessageLevel> | ConsoleMessageFilter): ConsoleCaptor;
|
|
171
|
+
export declare function captureConsole(page: Page, levelOrFilter?: ConsoleMessageLevel | Array<ConsoleMessageLevel> | ConsoleMessageFilter): ConsoleCaptor;
|
|
175
172
|
/**
|
|
176
173
|
* Tests whether the given console message has one of the specified log levels.
|
|
177
174
|
*
|
|
@@ -181,7 +178,154 @@ declare function captureConsole(page: Page, levelOrFilter?: ConsoleMessageLevel
|
|
|
181
178
|
* @param level - The log level
|
|
182
179
|
* @returns True if the message has one of the given log levels
|
|
183
180
|
*/
|
|
184
|
-
declare function hasLogLevel(message: ConsoleMessage, ...level: Array<ConsoleMessageLevel>): boolean;
|
|
181
|
+
export declare function hasLogLevel(message: ConsoleMessage, ...level: Array<ConsoleMessageLevel>): boolean;
|
|
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>;
|
|
185
329
|
//#endregion
|
|
186
330
|
//#region src/api/route-interceptor/filter.d.ts
|
|
187
331
|
/**
|
|
@@ -211,7 +355,7 @@ interface RouteFilter {
|
|
|
211
355
|
* });
|
|
212
356
|
* ```
|
|
213
357
|
*/
|
|
214
|
-
declare function matchPath(pattern: RegExp | string): RouteFilter;
|
|
358
|
+
export declare function matchPath(pattern: RegExp | string): RouteFilter;
|
|
215
359
|
//#endregion
|
|
216
360
|
//#region src/api/route-interceptor/response-handler.d.ts
|
|
217
361
|
/**
|
|
@@ -236,7 +380,7 @@ type ResponseHandler = (response: APIResponse) => Promise<FulfillOptions> | Fulf
|
|
|
236
380
|
* });
|
|
237
381
|
* ```
|
|
238
382
|
*/
|
|
239
|
-
declare function modifyJsonBody<T>(modifier: (body: T) => T): ResponseHandler;
|
|
383
|
+
export declare function modifyJsonBody<T>(modifier: (body: T) => T): ResponseHandler;
|
|
240
384
|
/**
|
|
241
385
|
* Helper to create a {@link ResponseHandler} that modifies the response body as a plaintext string.
|
|
242
386
|
*
|
|
@@ -252,7 +396,7 @@ declare function modifyJsonBody<T>(modifier: (body: T) => T): ResponseHandler;
|
|
|
252
396
|
* });
|
|
253
397
|
* ```
|
|
254
398
|
*/
|
|
255
|
-
declare function modifyTextBody(modifier: (body: string) => string): ResponseHandler;
|
|
399
|
+
export declare function modifyTextBody(modifier: (body: string) => string): ResponseHandler;
|
|
256
400
|
//#endregion
|
|
257
401
|
//#region src/api/route-interceptor/interceptor.d.ts
|
|
258
402
|
type Action<T> = () => Promise<T> | T;
|
|
@@ -260,7 +404,7 @@ type FulfillOptions = Parameters<Route["fulfill"]>[0];
|
|
|
260
404
|
/**
|
|
261
405
|
* Entrypoint to create various network interceptors using the {@link Page#route} API.
|
|
262
406
|
*/
|
|
263
|
-
declare class RouteInterceptor {
|
|
407
|
+
export declare class RouteInterceptor {
|
|
264
408
|
readonly page: Page;
|
|
265
409
|
readonly filter: RouteFilter;
|
|
266
410
|
constructor(page: Page, filter: RouteFilter);
|
|
@@ -398,7 +542,7 @@ declare class ExecutableRouteInterceptor {
|
|
|
398
542
|
* });
|
|
399
543
|
* ```
|
|
400
544
|
*/
|
|
401
|
-
declare function interceptRoute(page: Page, filter: RouteFilter): RouteInterceptor;
|
|
545
|
+
export declare function interceptRoute(page: Page, filter: RouteFilter): RouteInterceptor;
|
|
402
546
|
/**
|
|
403
547
|
* Base class to build re-usable route interceptors.
|
|
404
548
|
*
|
|
@@ -428,7 +572,7 @@ declare function interceptRoute(page: Page, filter: RouteFilter): RouteIntercept
|
|
|
428
572
|
* });
|
|
429
573
|
* ```
|
|
430
574
|
*/
|
|
431
|
-
declare class RouteInterceptorFixture {
|
|
575
|
+
export declare class RouteInterceptorFixture {
|
|
432
576
|
private readonly page;
|
|
433
577
|
constructor(page: Page);
|
|
434
578
|
/**
|
|
@@ -460,7 +604,42 @@ declare class RouteInterceptorFixture {
|
|
|
460
604
|
* @returns The absolute path to the resolved location
|
|
461
605
|
* @throws If no package root can be found
|
|
462
606
|
*/
|
|
463
|
-
declare function resolveFromPackageRoot(...paths: Array<string>): string;
|
|
607
|
+
export declare function resolveFromPackageRoot(...paths: Array<string>): string;
|
|
608
|
+
//#endregion
|
|
609
|
+
//#region src/page-object-model/locator-extensions.d.ts
|
|
610
|
+
type RoleLocators = Record<Role, RoleLocator>;
|
|
611
|
+
interface RoleLocator {
|
|
612
|
+
(options?: RoleLocatorOptions): Locator;
|
|
613
|
+
(name: string | RegExp, options?: RoleLocatorOptions): Locator;
|
|
614
|
+
}
|
|
615
|
+
type RoleLocatorOptions = Omit<GetByRoleOptions, "name">;
|
|
616
|
+
type Role = Parameters<Locator["getByRole"]>[0];
|
|
617
|
+
type GetByRoleOptions = Parameters<Locator["getByRole"]>[1];
|
|
618
|
+
/**
|
|
619
|
+
* Creates a set of locator functions for every ARIA role, so tests don't
|
|
620
|
+
* have to repeat `getByRole` and its role string throughout a page object.
|
|
621
|
+
*
|
|
622
|
+
* Every generated locator applies `exact: true` by default, keeping name
|
|
623
|
+
* matches from accidentally matching unrelated elements with a similar
|
|
624
|
+
* accessible name. This can be overridden per call through `options`, or
|
|
625
|
+
* replaced entirely for every locator by passing a custom `defaultOptions`.
|
|
626
|
+
*
|
|
627
|
+
* The accessible name can be omitted when an element should be located by
|
|
628
|
+
* role alone, in which case `options` can be passed as the first argument.
|
|
629
|
+
*
|
|
630
|
+
* @param locator - The page or locator to scope the role locators to
|
|
631
|
+
* @param defaultOptions - Optional. Options applied to every generated locator, overridden by per-call options; defaults to `{ exact: true }`
|
|
632
|
+
* @returns A `RoleLocators` object with one locator function per ARIA role
|
|
633
|
+
*
|
|
634
|
+
* @example
|
|
635
|
+
* ```ts
|
|
636
|
+
* const { button, textbox } = roleLocators(page);
|
|
637
|
+
*
|
|
638
|
+
* await textbox("Email").fill("user@example.com");
|
|
639
|
+
* await button("Submit").click();
|
|
640
|
+
* ```
|
|
641
|
+
*/
|
|
642
|
+
export declare function roleLocators(locator: Pick<Locator, "getByRole">, defaultOptions?: GetByRoleOptions): RoleLocators;
|
|
464
643
|
//#endregion
|
|
465
644
|
//#region src/normalizers/mask-base-url.d.ts
|
|
466
645
|
/**
|
|
@@ -469,7 +648,7 @@ declare function resolveFromPackageRoot(...paths: Array<string>): string;
|
|
|
469
648
|
*
|
|
470
649
|
* @param baseURL - Base URL to mask; if `undefined`, values are left unchanged
|
|
471
650
|
*/
|
|
472
|
-
declare function maskBaseURL<TValue>(baseURL: string | undefined): Normalizer<TValue>;
|
|
651
|
+
export declare function maskBaseURL<TValue>(baseURL: string | undefined): Normalizer<TValue>;
|
|
473
652
|
//#endregion
|
|
474
653
|
//#region src/normalizers/masked-value.d.ts
|
|
475
654
|
/**
|
|
@@ -478,14 +657,14 @@ declare function maskBaseURL<TValue>(baseURL: string | undefined): Normalizer<TV
|
|
|
478
657
|
*
|
|
479
658
|
* @param name - Name of the masked value
|
|
480
659
|
*/
|
|
481
|
-
declare function maskedValue(name: string): string;
|
|
660
|
+
export declare function maskedValue(name: string): string;
|
|
482
661
|
/**
|
|
483
662
|
* Creates a factory for indexed masked placeholders, e.g. `[USER_1]`.
|
|
484
663
|
*
|
|
485
664
|
* @param name - Name of the masked value
|
|
486
665
|
* @returns A function mapping an index to a masked placeholder
|
|
487
666
|
*/
|
|
488
|
-
declare function maskedValueWithIndex(name: string): (index: number) => string;
|
|
667
|
+
export declare function maskedValueWithIndex(name: string): (index: number) => string;
|
|
489
668
|
//#endregion
|
|
490
669
|
//#region src/types/playwright.d.ts
|
|
491
670
|
/**
|
|
@@ -494,4 +673,4 @@ declare function maskedValueWithIndex(name: string): (index: number) => string;
|
|
|
494
673
|
*/
|
|
495
674
|
type PlaywrightTarget = Page | Locator;
|
|
496
675
|
//#endregion
|
|
497
|
-
export {
|
|
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;
|
|
@@ -609,6 +789,133 @@ function resolveFromPackageRoot(...paths) {
|
|
|
609
789
|
return path.resolve(packageDir, ...paths);
|
|
610
790
|
}
|
|
611
791
|
//#endregion
|
|
792
|
+
//#region src/page-object-model/locator-extensions.ts
|
|
793
|
+
const ROLES = [
|
|
794
|
+
"alert",
|
|
795
|
+
"alertdialog",
|
|
796
|
+
"application",
|
|
797
|
+
"article",
|
|
798
|
+
"banner",
|
|
799
|
+
"blockquote",
|
|
800
|
+
"button",
|
|
801
|
+
"caption",
|
|
802
|
+
"cell",
|
|
803
|
+
"checkbox",
|
|
804
|
+
"code",
|
|
805
|
+
"columnheader",
|
|
806
|
+
"combobox",
|
|
807
|
+
"complementary",
|
|
808
|
+
"contentinfo",
|
|
809
|
+
"definition",
|
|
810
|
+
"deletion",
|
|
811
|
+
"dialog",
|
|
812
|
+
"directory",
|
|
813
|
+
"document",
|
|
814
|
+
"emphasis",
|
|
815
|
+
"feed",
|
|
816
|
+
"figure",
|
|
817
|
+
"form",
|
|
818
|
+
"generic",
|
|
819
|
+
"grid",
|
|
820
|
+
"gridcell",
|
|
821
|
+
"group",
|
|
822
|
+
"heading",
|
|
823
|
+
"img",
|
|
824
|
+
"insertion",
|
|
825
|
+
"link",
|
|
826
|
+
"list",
|
|
827
|
+
"listbox",
|
|
828
|
+
"listitem",
|
|
829
|
+
"log",
|
|
830
|
+
"main",
|
|
831
|
+
"marquee",
|
|
832
|
+
"math",
|
|
833
|
+
"meter",
|
|
834
|
+
"menu",
|
|
835
|
+
"menubar",
|
|
836
|
+
"menuitem",
|
|
837
|
+
"menuitemcheckbox",
|
|
838
|
+
"menuitemradio",
|
|
839
|
+
"navigation",
|
|
840
|
+
"none",
|
|
841
|
+
"note",
|
|
842
|
+
"option",
|
|
843
|
+
"paragraph",
|
|
844
|
+
"presentation",
|
|
845
|
+
"progressbar",
|
|
846
|
+
"radio",
|
|
847
|
+
"radiogroup",
|
|
848
|
+
"region",
|
|
849
|
+
"row",
|
|
850
|
+
"rowgroup",
|
|
851
|
+
"rowheader",
|
|
852
|
+
"scrollbar",
|
|
853
|
+
"search",
|
|
854
|
+
"searchbox",
|
|
855
|
+
"separator",
|
|
856
|
+
"slider",
|
|
857
|
+
"spinbutton",
|
|
858
|
+
"status",
|
|
859
|
+
"strong",
|
|
860
|
+
"subscript",
|
|
861
|
+
"superscript",
|
|
862
|
+
"switch",
|
|
863
|
+
"tab",
|
|
864
|
+
"table",
|
|
865
|
+
"tablist",
|
|
866
|
+
"tabpanel",
|
|
867
|
+
"term",
|
|
868
|
+
"textbox",
|
|
869
|
+
"time",
|
|
870
|
+
"timer",
|
|
871
|
+
"toolbar",
|
|
872
|
+
"tooltip",
|
|
873
|
+
"tree",
|
|
874
|
+
"treegrid",
|
|
875
|
+
"treeitem"
|
|
876
|
+
];
|
|
877
|
+
const DEFAULT_OPTIONS = { exact: true };
|
|
878
|
+
/**
|
|
879
|
+
* Creates a set of locator functions for every ARIA role, so tests don't
|
|
880
|
+
* have to repeat `getByRole` and its role string throughout a page object.
|
|
881
|
+
*
|
|
882
|
+
* Every generated locator applies `exact: true` by default, keeping name
|
|
883
|
+
* matches from accidentally matching unrelated elements with a similar
|
|
884
|
+
* accessible name. This can be overridden per call through `options`, or
|
|
885
|
+
* replaced entirely for every locator by passing a custom `defaultOptions`.
|
|
886
|
+
*
|
|
887
|
+
* The accessible name can be omitted when an element should be located by
|
|
888
|
+
* role alone, in which case `options` can be passed as the first argument.
|
|
889
|
+
*
|
|
890
|
+
* @param locator - The page or locator to scope the role locators to
|
|
891
|
+
* @param defaultOptions - Optional. Options applied to every generated locator, overridden by per-call options; defaults to `{ exact: true }`
|
|
892
|
+
* @returns A `RoleLocators` object with one locator function per ARIA role
|
|
893
|
+
*
|
|
894
|
+
* @example
|
|
895
|
+
* ```ts
|
|
896
|
+
* const { button, textbox } = roleLocators(page);
|
|
897
|
+
*
|
|
898
|
+
* await textbox("Email").fill("user@example.com");
|
|
899
|
+
* await button("Submit").click();
|
|
900
|
+
* ```
|
|
901
|
+
*/
|
|
902
|
+
function roleLocators(locator, defaultOptions = DEFAULT_OPTIONS) {
|
|
903
|
+
function getByRole(role) {
|
|
904
|
+
return (nameOrOptions, options) => {
|
|
905
|
+
if (typeof nameOrOptions === "string" || nameOrOptions instanceof RegExp) return locator.getByRole(role, {
|
|
906
|
+
name: nameOrOptions,
|
|
907
|
+
...defaultOptions,
|
|
908
|
+
...options
|
|
909
|
+
});
|
|
910
|
+
return locator.getByRole(role, {
|
|
911
|
+
...defaultOptions,
|
|
912
|
+
...nameOrOptions
|
|
913
|
+
});
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
return Object.fromEntries(ROLES.map((role) => [role, getByRole(role)]));
|
|
917
|
+
}
|
|
918
|
+
//#endregion
|
|
612
919
|
//#region src/normalizers/masked-value.ts
|
|
613
920
|
/**
|
|
614
921
|
* Formats a name as a masked placeholder, e.g. `maskedValue("base url")`
|
|
@@ -644,4 +951,4 @@ function identity(value) {
|
|
|
644
951
|
return value;
|
|
645
952
|
}
|
|
646
953
|
//#endregion
|
|
647
|
-
export { ConsoleCaptor, RouteInterceptor, RouteInterceptorFixture, captureConsole, createFetchAdapter, hasLogLevel, interceptRoute, isCI, maskBaseURL, maskedValue, maskedValueWithIndex, matchPath, modifyJsonBody, modifyTextBody, resolveFromPackageRoot };
|
|
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.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Utilities for writing tests with Playwright",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"playwright",
|
|
@@ -33,30 +33,29 @@
|
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@arethetypeswrong/core": "0.18.5",
|
|
36
|
-
"@changesets/cli": "3.0.
|
|
37
|
-
"@cronn/element-snapshot": "0.
|
|
38
|
-
"@cronn/playwright-file-snapshots": "2.2.
|
|
39
|
-
"@playwright/test": "1.
|
|
40
|
-
"@types/node": "24.13.
|
|
36
|
+
"@changesets/cli": "3.0.2",
|
|
37
|
+
"@cronn/element-snapshot": "0.30.0",
|
|
38
|
+
"@cronn/playwright-file-snapshots": "2.2.2",
|
|
39
|
+
"@playwright/test": "1.63.0",
|
|
40
|
+
"@types/node": "24.13.4",
|
|
41
41
|
"@typescript/native": "npm:typescript@^7.0.2",
|
|
42
|
-
"@vitest/coverage-v8": "
|
|
43
|
-
"eslint": "10.
|
|
42
|
+
"@vitest/coverage-v8": "5.0.0",
|
|
43
|
+
"eslint": "10.10.0",
|
|
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.
|
|
48
|
-
"oxc-minify": "0.
|
|
49
|
-
"oxfmt": "0.
|
|
47
|
+
"knip": "6.35.1",
|
|
48
|
+
"oxc-minify": "0.149.0",
|
|
49
|
+
"oxfmt": "0.67.0",
|
|
50
50
|
"publint": "0.3.24",
|
|
51
51
|
"rimraf": "6.1.3",
|
|
52
|
-
"tsdown": "0.
|
|
52
|
+
"tsdown": "0.23.0",
|
|
53
53
|
"turbo": "2.10.12",
|
|
54
54
|
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
55
|
-
"typescript-eslint": "8.
|
|
56
|
-
"vitepress": "2.0.0-alpha.
|
|
55
|
+
"typescript-eslint": "8.70.0",
|
|
56
|
+
"vitepress": "2.0.0-alpha.20",
|
|
57
57
|
"vitepress-plugin-group-icons": "1.7.6",
|
|
58
|
-
"vitest": "
|
|
59
|
-
"node": "runtime:24.20.0"
|
|
58
|
+
"vitest": "5.0.0"
|
|
60
59
|
},
|
|
61
60
|
"peerDependencies": {
|
|
62
61
|
"@cronn/playwright-file-snapshots": "^2",
|
|
@@ -65,12 +64,12 @@
|
|
|
65
64
|
"devEngines": {
|
|
66
65
|
"packageManager": {
|
|
67
66
|
"name": "pnpm",
|
|
68
|
-
"version": "
|
|
67
|
+
"version": "12.5.1",
|
|
69
68
|
"onFail": "download"
|
|
70
69
|
},
|
|
71
70
|
"runtime": {
|
|
72
71
|
"name": "node",
|
|
73
|
-
"version": "24.
|
|
72
|
+
"version": "24.21.0",
|
|
74
73
|
"onFail": "download"
|
|
75
74
|
}
|
|
76
75
|
},
|