@cronn/playwright-utils 0.6.0 → 0.8.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 +207 -6
- package/dist/index.mjs +259 -5
- package/package.json +12 -14
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}
|
|
@@ -462,7 +606,64 @@ export declare class RouteInterceptorFixture {
|
|
|
462
606
|
*/
|
|
463
607
|
export declare function resolveFromPackageRoot(...paths: Array<string>): string;
|
|
464
608
|
//#endregion
|
|
465
|
-
//#region src/page-object-model/
|
|
609
|
+
//#region src/page-object-model/form-filler.d.ts
|
|
610
|
+
interface Fillable<TValue> {
|
|
611
|
+
fill: Filler<TValue>;
|
|
612
|
+
}
|
|
613
|
+
type Filler<TValue> = (value: TValue) => Promise<void>;
|
|
614
|
+
type FormFields<TData> = { [K in keyof TData]-?: Fillable<Exclude<TData[K], undefined>> | Filler<Exclude<TData[K], undefined>>; };
|
|
615
|
+
/**
|
|
616
|
+
* Creates a filler function that fills a form declaratively from a data
|
|
617
|
+
* object, e.g. as the `fill` method of a page object.
|
|
618
|
+
*
|
|
619
|
+
* Each key of `fields` maps a key of the data to a form input: either an
|
|
620
|
+
* object with a `fill` method (such as a `Locator` or a nested page object) or
|
|
621
|
+
* a filler function receiving the value. Fields are filled one after another
|
|
622
|
+
* in the order of the keys in `fields`, and fields whose value is `undefined`
|
|
623
|
+
* are skipped.
|
|
624
|
+
*
|
|
625
|
+
* The type of the data is either given explicitly as type argument or inferred
|
|
626
|
+
* from `fields`. When inferred, filler functions need an explicit parameter
|
|
627
|
+
* type, otherwise their value is `unknown`. In both cases, the returned
|
|
628
|
+
* function accepts partial data.
|
|
629
|
+
*
|
|
630
|
+
* As the returned function is a filler itself, it can be used as a field of
|
|
631
|
+
* another form filler to fill nested data.
|
|
632
|
+
*
|
|
633
|
+
* @param fields - Mapping from each key of the data to the input to fill
|
|
634
|
+
* @returns A function filling the form with the given data
|
|
635
|
+
* @example
|
|
636
|
+
* ```ts
|
|
637
|
+
* const { checkbox, textbox } = roleLocators(page);
|
|
638
|
+
*
|
|
639
|
+
* const registrationForm = {
|
|
640
|
+
* fill: formFiller<Registration>({
|
|
641
|
+
* name: textbox("Name"),
|
|
642
|
+
* acceptTerms: (accepted) => checkbox("Accept terms").setChecked(accepted),
|
|
643
|
+
* }),
|
|
644
|
+
* };
|
|
645
|
+
* ```
|
|
646
|
+
*/
|
|
647
|
+
export declare function formFiller<TData extends object>(fields: FormFields<TData>): Filler<Partial<TData>>;
|
|
648
|
+
//#endregion
|
|
649
|
+
//#region src/page-object-model/extend-locator.d.ts
|
|
650
|
+
type WithoutLocatorKeys = Partial<Record<Exclude<keyof Locator, "toString">, never>>;
|
|
651
|
+
type ExtendedLocator<TExtensions extends WithoutLocatorKeys> = Locator & TExtensions;
|
|
652
|
+
/**
|
|
653
|
+
* Extends a `Locator` with custom properties and functions while retaining the full `Locator` API.
|
|
654
|
+
*
|
|
655
|
+
* Extensions must not override members of `Locator`. Locators derived from the extended locator
|
|
656
|
+
* (e.g. via `first()`, `filter()` or `locator()`) are plain `Locator`s without the extensions.
|
|
657
|
+
*
|
|
658
|
+
* Methods of `Locator` are bound to the original locator on every access, so accessing the same method
|
|
659
|
+
* twice returns different function instances (e.g. `extended.click !== extended.click`).
|
|
660
|
+
*
|
|
661
|
+
* @param locator The locator to extend
|
|
662
|
+
* @param createExtensions Creates the extensions, receiving the original locator
|
|
663
|
+
*/
|
|
664
|
+
export declare function extendLocator<TExtensions extends object & WithoutLocatorKeys>(locator: Locator, createExtensions: (locator: Locator) => TExtensions): ExtendedLocator<TExtensions>;
|
|
665
|
+
//#endregion
|
|
666
|
+
//#region src/page-object-model/role-locators.d.ts
|
|
466
667
|
type RoleLocators = Record<Role, RoleLocator>;
|
|
467
668
|
interface RoleLocator {
|
|
468
669
|
(options?: RoleLocatorOptions): Locator;
|
|
@@ -529,4 +730,4 @@ export declare function maskedValueWithIndex(name: string): (index: number) => s
|
|
|
529
730
|
*/
|
|
530
731
|
type PlaywrightTarget = Page | Locator;
|
|
531
732
|
//#endregion
|
|
532
|
-
export type { ConsoleMessageFilter, ConsoleMessageLevel, HttpMethod, PlaywrightTarget, ResponseHandler, RoleLocators, RouteFilter };
|
|
733
|
+
export type { ConsoleMessageFilter, ConsoleMessageLevel, CspFixtures, CspViolation, ExtendedLocator, FormFields, 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,7 +789,81 @@ function resolveFromPackageRoot(...paths) {
|
|
|
609
789
|
return path.resolve(packageDir, ...paths);
|
|
610
790
|
}
|
|
611
791
|
//#endregion
|
|
612
|
-
//#region src/page-object-model/
|
|
792
|
+
//#region src/page-object-model/form-filler.ts
|
|
793
|
+
/**
|
|
794
|
+
* Creates a filler function that fills a form declaratively from a data
|
|
795
|
+
* object, e.g. as the `fill` method of a page object.
|
|
796
|
+
*
|
|
797
|
+
* Each key of `fields` maps a key of the data to a form input: either an
|
|
798
|
+
* object with a `fill` method (such as a `Locator` or a nested page object) or
|
|
799
|
+
* a filler function receiving the value. Fields are filled one after another
|
|
800
|
+
* in the order of the keys in `fields`, and fields whose value is `undefined`
|
|
801
|
+
* are skipped.
|
|
802
|
+
*
|
|
803
|
+
* The type of the data is either given explicitly as type argument or inferred
|
|
804
|
+
* from `fields`. When inferred, filler functions need an explicit parameter
|
|
805
|
+
* type, otherwise their value is `unknown`. In both cases, the returned
|
|
806
|
+
* function accepts partial data.
|
|
807
|
+
*
|
|
808
|
+
* As the returned function is a filler itself, it can be used as a field of
|
|
809
|
+
* another form filler to fill nested data.
|
|
810
|
+
*
|
|
811
|
+
* @param fields - Mapping from each key of the data to the input to fill
|
|
812
|
+
* @returns A function filling the form with the given data
|
|
813
|
+
* @example
|
|
814
|
+
* ```ts
|
|
815
|
+
* const { checkbox, textbox } = roleLocators(page);
|
|
816
|
+
*
|
|
817
|
+
* const registrationForm = {
|
|
818
|
+
* fill: formFiller<Registration>({
|
|
819
|
+
* name: textbox("Name"),
|
|
820
|
+
* acceptTerms: (accepted) => checkbox("Accept terms").setChecked(accepted),
|
|
821
|
+
* }),
|
|
822
|
+
* };
|
|
823
|
+
* ```
|
|
824
|
+
*/
|
|
825
|
+
function formFiller(fields) {
|
|
826
|
+
return async (data) => {
|
|
827
|
+
for (const [key, filler] of Object.entries(fields)) {
|
|
828
|
+
const value = data[key];
|
|
829
|
+
if (value === void 0) continue;
|
|
830
|
+
if (typeof filler === "function") await filler(value);
|
|
831
|
+
else await filler.fill(value);
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
//#endregion
|
|
836
|
+
//#region src/page-object-model/extend-locator.ts
|
|
837
|
+
/**
|
|
838
|
+
* Extends a `Locator` with custom properties and functions while retaining the full `Locator` API.
|
|
839
|
+
*
|
|
840
|
+
* Extensions must not override members of `Locator`. Locators derived from the extended locator
|
|
841
|
+
* (e.g. via `first()`, `filter()` or `locator()`) are plain `Locator`s without the extensions.
|
|
842
|
+
*
|
|
843
|
+
* Methods of `Locator` are bound to the original locator on every access, so accessing the same method
|
|
844
|
+
* twice returns different function instances (e.g. `extended.click !== extended.click`).
|
|
845
|
+
*
|
|
846
|
+
* @param locator The locator to extend
|
|
847
|
+
* @param createExtensions Creates the extensions, receiving the original locator
|
|
848
|
+
*/
|
|
849
|
+
function extendLocator(locator, createExtensions) {
|
|
850
|
+
const extensions = createExtensions(locator);
|
|
851
|
+
return new Proxy(locator, {
|
|
852
|
+
get(target, property) {
|
|
853
|
+
if (isOwnKeyOf(extensions, property)) return extensions[property];
|
|
854
|
+
const value = Reflect.get(target, property, target);
|
|
855
|
+
return typeof value === "function" && property !== "constructor" ? value.bind(target) : value;
|
|
856
|
+
},
|
|
857
|
+
has(target, property) {
|
|
858
|
+
return isOwnKeyOf(extensions, property) || Reflect.has(target, property);
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
function isOwnKeyOf(object, key) {
|
|
863
|
+
return Object.hasOwn(object, key);
|
|
864
|
+
}
|
|
865
|
+
//#endregion
|
|
866
|
+
//#region src/page-object-model/role-locators.ts
|
|
613
867
|
const ROLES = [
|
|
614
868
|
"alert",
|
|
615
869
|
"alertdialog",
|
|
@@ -771,4 +1025,4 @@ function identity(value) {
|
|
|
771
1025
|
return value;
|
|
772
1026
|
}
|
|
773
1027
|
//#endregion
|
|
774
|
-
export { ConsoleCaptor, RouteInterceptor, RouteInterceptorFixture, captureConsole, createFetchAdapter, hasLogLevel, interceptRoute, isCI, maskBaseURL, maskedValue, maskedValueWithIndex, matchPath, modifyJsonBody, modifyTextBody, resolveFromPackageRoot, roleLocators };
|
|
1028
|
+
export { ConsoleCaptor, CspCaptor, RouteInterceptor, RouteInterceptorFixture, captureConsole, captureCspViolations, createFetchAdapter, cspFixtures, extendLocator, formFiller, 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.8.0",
|
|
4
4
|
"description": "Utilities for writing tests with Playwright",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"playwright",
|
|
@@ -33,29 +33,29 @@
|
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@arethetypeswrong/core": "0.18.5",
|
|
36
|
-
"@changesets/cli": "3.0.
|
|
36
|
+
"@changesets/cli": "3.0.3",
|
|
37
37
|
"@cronn/element-snapshot": "0.30.0",
|
|
38
38
|
"@cronn/playwright-file-snapshots": "2.2.2",
|
|
39
39
|
"@playwright/test": "1.63.0",
|
|
40
|
-
"@types/node": "24.
|
|
40
|
+
"@types/node": "24.19.0",
|
|
41
41
|
"@typescript/native": "npm:typescript@^7.0.2",
|
|
42
|
-
"@vitest/coverage-v8": "5.0.
|
|
43
|
-
"eslint": "10.
|
|
42
|
+
"@vitest/coverage-v8": "5.0.2",
|
|
43
|
+
"eslint": "10.11.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.38.0",
|
|
48
|
+
"oxc-minify": "0.151.0",
|
|
49
|
+
"oxfmt": "0.70.0",
|
|
50
50
|
"publint": "0.3.24",
|
|
51
51
|
"rimraf": "6.1.3",
|
|
52
52
|
"tsdown": "0.23.0",
|
|
53
|
-
"turbo": "2.
|
|
53
|
+
"turbo": "2.11.5",
|
|
54
54
|
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
55
|
-
"typescript-eslint": "8.70.
|
|
55
|
+
"typescript-eslint": "8.70.1",
|
|
56
56
|
"vitepress": "2.0.0-alpha.20",
|
|
57
57
|
"vitepress-plugin-group-icons": "1.7.6",
|
|
58
|
-
"vitest": "5.0.
|
|
58
|
+
"vitest": "5.0.2"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"@cronn/playwright-file-snapshots": "^2",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"devEngines": {
|
|
65
65
|
"packageManager": {
|
|
66
66
|
"name": "pnpm",
|
|
67
|
-
"version": "12.
|
|
67
|
+
"version": "12.9.1",
|
|
68
68
|
"onFail": "download"
|
|
69
69
|
},
|
|
70
70
|
"runtime": {
|
|
@@ -90,9 +90,7 @@
|
|
|
90
90
|
"lint:check": "eslint . --max-warnings=0",
|
|
91
91
|
"lint:fix": "eslint . --max-warnings=0 --fix",
|
|
92
92
|
"playwright:install": "playwright install chromium",
|
|
93
|
-
"pnpm:check": "pnpm dedupe --check",
|
|
94
93
|
"pnpm:clean": "pnpm pm clean && pnpm store prune",
|
|
95
|
-
"pnpm:fix": "pnpm dedupe",
|
|
96
94
|
"release": "pnpm turbo run build && pnpm changeset publish",
|
|
97
95
|
"test:integration": "playwright test",
|
|
98
96
|
"test:unit": "vitest run",
|