@lullabot/playwright-testing 1.0.0 → 1.1.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.
Files changed (68) hide show
  1. package/bin/github-a11y-summary +2 -2
  2. package/bin/github-failure-summary +4 -4
  3. package/lib/accessibility-baseline-file.d.ts +6 -6
  4. package/lib/accessibility-baseline-file.js +29 -23
  5. package/lib/accessibility-baseline.js +5 -3
  6. package/lib/accessible-screenshot.d.ts +4 -4
  7. package/lib/accessible-screenshot.js +110 -88
  8. package/lib/focus.js +4 -1
  9. package/lib/frames.js +4 -4
  10. package/lib/github/a11y-summary.js +74 -79
  11. package/lib/github/attachments.d.ts +1 -1
  12. package/lib/github/attachments.js +36 -34
  13. package/lib/github/failure-summary.d.ts +4 -4
  14. package/lib/github/failure-summary.js +140 -108
  15. package/lib/github/index.d.ts +6 -6
  16. package/lib/github/report-paths.js +17 -9
  17. package/lib/hover.js +17 -17
  18. package/lib/images.js +15 -12
  19. package/lib/index.d.ts +13 -13
  20. package/lib/interaction-states.d.ts +2 -2
  21. package/lib/interaction-states.js +7 -7
  22. package/lib/mock/index.d.ts +1 -1
  23. package/lib/mock/youtube.d.ts +1 -1
  24. package/lib/mock/youtube.js +1 -1
  25. package/lib/pseudo-state.d.ts +2 -2
  26. package/lib/pseudo-state.js +15 -7
  27. package/lib/videos.js +21 -10
  28. package/lib/visualdiff.d.ts +4 -4
  29. package/lib/visualdiff.js +35 -27
  30. package/package.json +3 -3
  31. package/src/accessibility-baseline-file.test.ts +190 -154
  32. package/src/accessibility-baseline-file.ts +107 -83
  33. package/src/accessibility-baseline.test.ts +869 -433
  34. package/src/accessibility-baseline.ts +41 -21
  35. package/src/accessible-screenshot.test.ts +551 -423
  36. package/src/accessible-screenshot.ts +358 -251
  37. package/src/focus.test.ts +23 -23
  38. package/src/focus.ts +7 -2
  39. package/src/fonts.test.ts +13 -13
  40. package/src/fonts.ts +1 -1
  41. package/src/frames.test.ts +10 -8
  42. package/src/frames.ts +5 -5
  43. package/src/github/a11y-summary.test.ts +436 -332
  44. package/src/github/a11y-summary.ts +189 -179
  45. package/src/github/attachments.test.ts +228 -167
  46. package/src/github/attachments.ts +144 -121
  47. package/src/github/failure-summary.test.ts +660 -445
  48. package/src/github/failure-summary.ts +376 -274
  49. package/src/github/index.test.ts +14 -14
  50. package/src/github/index.ts +6 -6
  51. package/src/github/report-paths.test.ts +211 -156
  52. package/src/github/report-paths.ts +75 -61
  53. package/src/hover.test.ts +50 -46
  54. package/src/hover.ts +18 -18
  55. package/src/images.test.ts +251 -206
  56. package/src/images.ts +52 -43
  57. package/src/index.ts +13 -13
  58. package/src/interaction-states.test.ts +72 -42
  59. package/src/interaction-states.ts +43 -29
  60. package/src/mock/index.ts +1 -1
  61. package/src/mock/youtube.test.ts +26 -26
  62. package/src/mock/youtube.ts +4 -6
  63. package/src/pseudo-state.test.ts +58 -51
  64. package/src/pseudo-state.ts +32 -27
  65. package/src/videos.test.ts +559 -399
  66. package/src/videos.ts +68 -44
  67. package/src/visualdiff.test.ts +416 -315
  68. package/src/visualdiff.ts +140 -99
package/src/visualdiff.ts CHANGED
@@ -4,22 +4,22 @@ import {
4
4
  type Page,
5
5
  type TestInfo,
6
6
  type WebError,
7
- } from '@playwright/test';
7
+ } from "@playwright/test";
8
8
 
9
9
  import {
10
10
  takeAccessibleScreenshot,
11
11
  type ScreenshotOptions,
12
12
  } from "./accessible-screenshot.js";
13
- import type {AccessibilityBaseline} from './accessibility-baseline.js'
14
- import {forcePseudoState, type ForcedPseudoState} from './pseudo-state.js'
15
- import type {InteractionState} from './interaction-states.js'
13
+ import type { AccessibilityBaseline } from "./accessibility-baseline.js";
14
+ import { forcePseudoState, type ForcedPseudoState } from "./pseudo-state.js";
15
+ import type { InteractionState } from "./interaction-states.js";
16
16
 
17
17
  export interface VisualDiffExecutionContext {
18
- page: Page
19
- testInfo: TestInfo
20
- testCase: VisualDiff
21
- group: VisualDiffGroup
22
- config?: VisualDiffUrlConfig
18
+ page: Page;
19
+ testInfo: TestInfo;
20
+ testCase: VisualDiff;
21
+ group: VisualDiffGroup;
22
+ config?: VisualDiffUrlConfig;
23
23
  }
24
24
 
25
25
  export interface VisualDiffPreset {
@@ -27,12 +27,17 @@ export interface VisualDiffPreset {
27
27
  * Neutral adapter seam for screenshot defaults such as exclusions, browser
28
28
  * thresholds, or post-scroll settling hooks.
29
29
  */
30
- screenshotOptions?: ScreenshotOptions | (
31
- (context: VisualDiffExecutionContext) => ScreenshotOptions | Promise<ScreenshotOptions>
32
- )
30
+ screenshotOptions?:
31
+ | ScreenshotOptions
32
+ | ((
33
+ context: VisualDiffExecutionContext,
34
+ ) => ScreenshotOptions | Promise<ScreenshotOptions>);
33
35
  }
34
36
 
35
- export function defineVisualDiffConfig(cases: VisualDiffUrlConfig, preset?: VisualDiffPreset) {
37
+ export function defineVisualDiffConfig(
38
+ cases: VisualDiffUrlConfig,
39
+ preset?: VisualDiffPreset,
40
+ ) {
36
41
  return new VisualDiffTestCases(cases, preset);
37
42
  }
38
43
 
@@ -43,40 +48,51 @@ export function defaultTestFunction(
43
48
  preset?: VisualDiffPreset,
44
49
  ) {
45
50
  return async (
46
- {page, context}: {page: Page, context: BrowserContext},
51
+ { page, context }: { page: Page; context: BrowserContext },
47
52
  testInfo: TestInfo,
48
53
  ) => {
49
- const MockClass = testCase.mockClass ?? group.mockClass ?? config?.mockClass
54
+ const MockClass =
55
+ testCase.mockClass ?? group.mockClass ?? config?.mockClass;
50
56
  if (MockClass) {
51
57
  const mock = new MockClass();
52
58
  await mock.mock(page);
53
59
  }
54
60
  // Log any errors to the Playwright console too.
55
61
 
56
- context.on('weberror', (webError: WebError) => console.log(webError.error()));
62
+ context.on("weberror", (webError: WebError) =>
63
+ console.log(webError.error()),
64
+ );
57
65
  testInfo.annotations.push({
58
- type: 'Description',
66
+ type: "Description",
59
67
  description: testCase.description,
60
- })
68
+ });
61
69
 
62
- const representativeUrl = testCase.representativeUrl
63
- ?? group.representativeUrl
64
- ?? config?.representativeUrl;
70
+ const representativeUrl =
71
+ testCase.representativeUrl ??
72
+ group.representativeUrl ??
73
+ config?.representativeUrl;
65
74
 
66
75
  if (representativeUrl) {
67
76
  testInfo.annotations.push({
68
- type: 'Representative URL',
77
+ type: "Representative URL",
69
78
  description: representativeUrl,
70
79
  });
71
80
  }
72
81
 
73
- const path = `${config?.pathPrefix ?? ''}${group.pathPrefix ?? ''}${testCase.path}`;
82
+ const path = `${config?.pathPrefix ?? ""}${group.pathPrefix ?? ""}${testCase.path}`;
74
83
 
75
84
  await page.goto(path);
76
85
 
77
- const configuredScreenshotOptions = typeof preset?.screenshotOptions === 'function'
78
- ? await preset.screenshotOptions({page, testInfo, testCase, group, config})
79
- : preset?.screenshotOptions ?? {}
86
+ const configuredScreenshotOptions =
87
+ typeof preset?.screenshotOptions === "function"
88
+ ? await preset.screenshotOptions({
89
+ page,
90
+ testInfo,
91
+ testCase,
92
+ group,
93
+ config,
94
+ })
95
+ : (preset?.screenshotOptions ?? {});
80
96
 
81
97
  // Merge masks from all three levels: config, group, and testCase.
82
98
  const maskSelectors: string[] = [
@@ -86,14 +102,15 @@ export function defaultTestFunction(
86
102
  ];
87
103
  const maskLocators = [
88
104
  ...(configuredScreenshotOptions.mask ?? []),
89
- ...maskSelectors.map(selector => page.locator(selector)),
105
+ ...maskSelectors.map((selector) => page.locator(selector)),
90
106
  ];
91
107
 
92
108
  // Most-specific-wins for maskColor: testCase > group > config.
93
- const maskColor = testCase.maskColor
94
- ?? group.maskColor
95
- ?? config?.maskColor
96
- ?? configuredScreenshotOptions.maskColor;
109
+ const maskColor =
110
+ testCase.maskColor ??
111
+ group.maskColor ??
112
+ config?.maskColor ??
113
+ configuredScreenshotOptions.maskColor;
97
114
 
98
115
  const screenshotOptions: ScreenshotOptions = {
99
116
  ...configuredScreenshotOptions,
@@ -105,15 +122,16 @@ export function defaultTestFunction(
105
122
  if (maskColor) {
106
123
  screenshotOptions.maskColor = maskColor;
107
124
  }
108
- const a11yBaseline = testCase.a11yBaseline
109
- ?? group.a11yBaseline
110
- ?? config?.a11yBaseline
111
- ?? configuredScreenshotOptions.accessibility?.baseline
125
+ const a11yBaseline =
126
+ testCase.a11yBaseline ??
127
+ group.a11yBaseline ??
128
+ config?.a11yBaseline ??
129
+ configuredScreenshotOptions.accessibility?.baseline;
112
130
  if (a11yBaseline || configuredScreenshotOptions.accessibility) {
113
131
  screenshotOptions.accessibility = {
114
132
  ...configuredScreenshotOptions.accessibility,
115
- ...(a11yBaseline ? {baseline: a11yBaseline} : {}),
116
- }
133
+ ...(a11yBaseline ? { baseline: a11yBaseline } : {}),
134
+ };
117
135
  }
118
136
 
119
137
  const interactionStates: VisualDiffInteractionState[] = [
@@ -121,10 +139,13 @@ export function defaultTestFunction(
121
139
  ...(group.interactionStates ?? []),
122
140
  ...(testCase.interactionStates ?? []),
123
141
  ];
124
- if (interactionStates.length > 0 || configuredScreenshotOptions.interactionStates) {
142
+ if (
143
+ interactionStates.length > 0 ||
144
+ configuredScreenshotOptions.interactionStates
145
+ ) {
125
146
  screenshotOptions.interactionStates = [
126
147
  ...(configuredScreenshotOptions.interactionStates ?? []),
127
- ...interactionStates.map(({selector, states}) => ({
148
+ ...interactionStates.map(({ selector, states }) => ({
128
149
  locator: page.locator(selector),
129
150
  states,
130
151
  })),
@@ -141,42 +162,50 @@ export function defaultTestFunction(
141
162
  ...(testCase.pseudoStates ?? []),
142
163
  ];
143
164
  const clearPseudoStates: Array<() => Promise<void>> = [];
144
- let operationFailed = false
145
- let operationError: unknown
165
+ let operationFailed = false;
166
+ let operationError: unknown;
146
167
  try {
147
168
  for (const pseudoState of pseudoStates) {
148
- clearPseudoStates.push(await forcePseudoState(
149
- page,
150
- pseudoState.selector,
151
- pseudoState.pseudoClasses,
152
- ));
169
+ clearPseudoStates.push(
170
+ await forcePseudoState(
171
+ page,
172
+ pseudoState.selector,
173
+ pseudoState.pseudoClasses,
174
+ ),
175
+ );
153
176
  }
154
177
  await takeAccessibleScreenshot(page, testInfo, screenshotOptions);
155
178
  } catch (error) {
156
- operationFailed = true
157
- operationError = error
179
+ operationFailed = true;
180
+ operationError = error;
158
181
  }
159
182
 
160
- const cleanupErrors: unknown[] = []
183
+ const cleanupErrors: unknown[] = [];
161
184
  for (const clearPseudoState of clearPseudoStates.reverse()) {
162
185
  try {
163
186
  await clearPseudoState();
164
187
  } catch (error) {
165
- cleanupErrors.push(error)
188
+ cleanupErrors.push(error);
166
189
  }
167
190
  }
168
191
 
169
192
  if (operationFailed) {
170
193
  if (cleanupErrors.length > 0) {
171
- throw new AggregateError([operationError, ...cleanupErrors], 'Visual diff capture and pseudo-state cleanup failed.')
194
+ throw new AggregateError(
195
+ [operationError, ...cleanupErrors],
196
+ "Visual diff capture and pseudo-state cleanup failed.",
197
+ );
172
198
  }
173
- throw operationError
199
+ throw operationError;
174
200
  }
175
201
  if (cleanupErrors.length === 1) {
176
- throw cleanupErrors[0]
202
+ throw cleanupErrors[0];
177
203
  }
178
204
  if (cleanupErrors.length > 1) {
179
- throw new AggregateError(cleanupErrors, 'Multiple pseudo-state cleanup operations failed.')
205
+ throw new AggregateError(
206
+ cleanupErrors,
207
+ "Multiple pseudo-state cleanup operations failed.",
208
+ );
180
209
  }
181
210
  };
182
211
  }
@@ -209,15 +238,24 @@ export class VisualDiffTestCases {
209
238
  * using a custom test function, automatic mask, interaction-state, and
210
239
  * pseudo-state handling is bypassed. You must apply them yourself.
211
240
  */
212
- public describe(overriddenTestFunction?: (testCase: VisualDiff, group: VisualDiffGroup) => Function | void) {
241
+ public describe(
242
+ overriddenTestFunction?: (
243
+ testCase: VisualDiff,
244
+ group: VisualDiffGroup,
245
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -- Preserve the existing public callback signature.
246
+ ) => Function | void,
247
+ ) {
213
248
  // Handle skipping of test cases, either based on a simple boolean or a callback.
214
249
  function shouldSkip(testCase: BaseVisualDiff): boolean {
215
- return testCase.skip !== undefined
216
- && (testCase.skip.callback === undefined || testCase.skip.callback(testCase))
250
+ return (
251
+ testCase.skip !== undefined &&
252
+ (testCase.skip.callback === undefined ||
253
+ testCase.skip.callback(testCase))
254
+ );
217
255
  }
218
256
 
219
257
  function registerSkip(testCase: BaseVisualDiff): void {
220
- const skip = testCase.skip!
258
+ const skip = testCase.skip!;
221
259
  test.skip(`${testCase.name}: ${skip.reason} <${skip.willBeFixedIn}>`, async () => {});
222
260
  }
223
261
 
@@ -230,7 +268,7 @@ export class VisualDiffTestCases {
230
268
 
231
269
  // Actually describe the group.
232
270
  test.describe(group.name, () => {
233
- group.testCases.forEach((testCase => {
271
+ group.testCases.forEach((testCase) => {
234
272
  // Allow skipping of individual test cases.
235
273
  if (shouldSkip(testCase)) {
236
274
  registerSkip(testCase);
@@ -239,87 +277,90 @@ export class VisualDiffTestCases {
239
277
 
240
278
  // Define a default function for test cases.
241
279
  let testFunction: any;
242
- if (typeof overriddenTestFunction != 'function') {
243
- testFunction = defaultTestFunction(testCase, group, this.config, this.preset)
280
+ if (typeof overriddenTestFunction != "function") {
281
+ testFunction = defaultTestFunction(
282
+ testCase,
283
+ group,
284
+ this.config,
285
+ this.preset,
286
+ );
244
287
  } else {
245
288
  testFunction = overriddenTestFunction(testCase, group);
246
289
  }
247
290
 
248
291
  test(`${testCase.name}: ${testCase.path}`, testFunction);
249
- }));
292
+ });
250
293
  });
251
294
  });
252
-
253
295
  }
254
296
  }
255
297
 
256
-
257
298
  /**
258
299
  * The top level configuration object.
259
300
  */
260
301
  export type VisualDiffUrlConfig = {
261
302
  // The name of the visual diff configuration, such as "Example Site Visual Diffs".
262
- name: string,
303
+ name: string;
263
304
  // A further description of the configuration.
264
- description?: string,
305
+ description?: string;
265
306
  // An array of groups of visual diffs. Good groups include by content type, site
266
307
  // section, or common feature.
267
- groups: VisualDiffGroup[],
308
+ groups: VisualDiffGroup[];
268
309
  /** Prefix prepended before group and case paths. */
269
- pathPrefix?: string,
310
+ pathPrefix?: string;
270
311
  /** Fallback representative URL for every case. */
271
- representativeUrl?: string,
312
+ representativeUrl?: string;
272
313
  /** Fallback network/page mock for every case. */
273
- mockClass?: MockableConstructor,
314
+ mockClass?: MockableConstructor;
274
315
  /**
275
316
  * CSS selectors for elements to mask globally across all test cases.
276
317
  * Useful for dynamic content like copyright years that change over time.
277
318
  * These are merged with any group-level and test-case-level masks.
278
319
  */
279
- mask?: string[],
320
+ mask?: string[];
280
321
  /**
281
322
  * The color of the overlay box for masked elements, in CSS color format.
282
323
  * Can be overridden at the group or test-case level.
283
324
  */
284
- maskColor?: string,
325
+ maskColor?: string;
285
326
  /**
286
327
  * Accessibility baseline for managing known violations.
287
328
  * When provided, violations matching the baseline are suppressed and
288
329
  * toMatchSnapshot() is skipped in favour of baseline-driven assertions.
289
330
  */
290
- a11yBaseline?: AccessibilityBaseline,
331
+ a11yBaseline?: AccessibilityBaseline;
291
332
  /**
292
333
  * Cross-browser hover and focus states to apply for every test case. These
293
334
  * are merged with group-level and test-case-level interaction states.
294
335
  */
295
- interactionStates?: VisualDiffInteractionState[],
336
+ interactionStates?: VisualDiffInteractionState[];
296
337
  /**
297
338
  * Chromium-only pseudo-states to force for every test case. These are merged
298
339
  * with group-level and test-case-level pseudo-states.
299
340
  */
300
- pseudoStates?: ForcedPseudoState[],
301
- }
341
+ pseudoStates?: ForcedPseudoState[];
342
+ };
302
343
 
303
344
  /**
304
345
  * A group of Visual Diff test cases.
305
346
  */
306
347
  export type VisualDiffGroup = BaseVisualDiff & {
307
- pathPrefix?: string,
348
+ pathPrefix?: string;
308
349
  // An array of test cases.
309
- testCases: VisualDiff[],
310
- }
350
+ testCases: VisualDiff[];
351
+ };
311
352
 
312
353
  export interface MockableConstructor {
313
354
  new (): Mockable;
314
355
  }
315
356
  export interface Mockable {
316
- mock(page: Page): Promise<void>
357
+ mock(page: Page): Promise<void>;
317
358
  }
318
359
 
319
360
  /** A selector and real cross-browser interaction states to apply to it. */
320
361
  export interface VisualDiffInteractionState {
321
- selector: string
322
- states: InteractionState[]
362
+ selector: string;
363
+ states: InteractionState[];
323
364
  }
324
365
 
325
366
  /**
@@ -327,42 +368,42 @@ export interface VisualDiffInteractionState {
327
368
  */
328
369
  export type VisualDiff = BaseVisualDiff & {
329
370
  // A relative path for the test case.
330
- path: string,
331
- }
371
+ path: string;
372
+ };
332
373
 
333
374
  export type BaseVisualDiff = {
334
375
  // The name of the test case, such as "Alert (White Background)".
335
- name: string,
376
+ name: string;
336
377
  // An optional description of the test case.
337
- description?: string,
378
+ description?: string;
338
379
  // An optional representative URL for this test.
339
- representativeUrl?: string,
380
+ representativeUrl?: string;
340
381
  // Allow skipping of this test.
341
- skip?: SkipTest,
342
- mockClass?: MockableConstructor,
382
+ skip?: SkipTest;
383
+ mockClass?: MockableConstructor;
343
384
  /** Accessibility baseline overriding less-specific configuration. */
344
- a11yBaseline?: AccessibilityBaseline,
385
+ a11yBaseline?: AccessibilityBaseline;
345
386
  /**
346
387
  * CSS selectors for elements to mask when taking screenshots.
347
388
  * These are merged with any config-level and (for test cases) group-level masks.
348
389
  */
349
- mask?: string[],
390
+ mask?: string[];
350
391
  /**
351
392
  * The color of the overlay box for masked elements, in CSS color format.
352
393
  * Overrides the mask color set at less-specific levels (config or group).
353
394
  */
354
- maskColor?: string,
395
+ maskColor?: string;
355
396
  /**
356
397
  * Cross-browser hover and focus states to apply while capturing the
357
398
  * screenshot and running its accessibility scan.
358
399
  */
359
- interactionStates?: VisualDiffInteractionState[],
400
+ interactionStates?: VisualDiffInteractionState[];
360
401
  /**
361
402
  * Chromium-only pseudo-states to force while capturing the screenshot and
362
403
  * running its accessibility scan.
363
404
  */
364
- pseudoStates?: ForcedPseudoState[],
365
- }
405
+ pseudoStates?: ForcedPseudoState[];
406
+ };
366
407
 
367
408
  /**
368
409
  * A declaration that a test should be skipped.
@@ -372,10 +413,10 @@ export type BaseVisualDiff = {
372
413
  */
373
414
  export type SkipTest = {
374
415
  // The reason why this test should be skipped, such as "The News listing has undefined ordering".
375
- reason: string,
416
+ reason: string;
376
417
  // A link to the ticket or issue that will allow this test to be re-enabled.
377
- willBeFixedIn: string,
418
+ willBeFixedIn: string;
378
419
  // An optional callback to control if this test is skipped. For example, a skip
379
420
  // callback could check the VisualDiff.path property.
380
- callback?: (testCase: BaseVisualDiff) => boolean
381
- }
421
+ callback?: (testCase: BaseVisualDiff) => boolean;
422
+ };