@lullabot/playwright-testing 1.0.0 → 1.0.1

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 +88 -76
  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 +139 -107
  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 +633 -433
  34. package/src/accessibility-baseline.ts +41 -21
  35. package/src/accessible-screenshot.test.ts +551 -423
  36. package/src/accessible-screenshot.ts +334 -245
  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 +659 -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/lib/hover.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.clearHover = clearHover;
4
- const HOVER_SHIELD_ATTRIBUTE = 'data-playwright-testing-hover-shield';
4
+ const HOVER_SHIELD_ATTRIBUTE = "data-playwright-testing-hover-shield";
5
5
  /**
6
6
  * Move the pointer away from page content for a deterministic screenshot.
7
7
  *
@@ -16,23 +16,23 @@ const HOVER_SHIELD_ATTRIBUTE = 'data-playwright-testing-hover-shield';
16
16
  async function clearHover(page) {
17
17
  await page.evaluate((attribute) => {
18
18
  document.querySelector(`[${attribute}]`)?.remove();
19
- const shield = document.createElement('playwright-testing-hover-shield');
20
- shield.setAttribute(attribute, '');
21
- shield.setAttribute('aria-hidden', 'true');
19
+ const shield = document.createElement("playwright-testing-hover-shield");
20
+ shield.setAttribute(attribute, "");
21
+ shield.setAttribute("aria-hidden", "true");
22
22
  shield.style.cssText = [
23
- 'all: initial !important',
24
- 'position: fixed !important',
25
- 'inset: 0 !important',
26
- 'display: block !important',
27
- 'width: 100vw !important',
28
- 'height: 100vh !important',
29
- 'margin: 0 !important',
30
- 'padding: 0 !important',
31
- 'border: 0 !important',
32
- 'opacity: 0 !important',
33
- 'pointer-events: auto !important',
34
- 'z-index: 2147483647 !important',
35
- ].join(';');
23
+ "all: initial !important",
24
+ "position: fixed !important",
25
+ "inset: 0 !important",
26
+ "display: block !important",
27
+ "width: 100vw !important",
28
+ "height: 100vh !important",
29
+ "margin: 0 !important",
30
+ "padding: 0 !important",
31
+ "border: 0 !important",
32
+ "opacity: 0 !important",
33
+ "pointer-events: auto !important",
34
+ "z-index: 2147483647 !important",
35
+ ].join(";");
36
36
  document.documentElement.appendChild(shield);
37
37
  }, HOVER_SHIELD_ATTRIBUTE);
38
38
  try {
package/lib/images.js CHANGED
@@ -24,12 +24,12 @@ async function waitForImages(page, selector, options = {}) {
24
24
  for (const l of await locators.all()) {
25
25
  // Ensure images are connected to the DOM before trying to scroll to them.
26
26
  // https://github.com/microsoft/playwright/issues/23758
27
- if (await l.evaluate(image => image.isConnected)) {
27
+ if (await l.evaluate((image) => image.isConnected)) {
28
28
  await l.scrollIntoViewIfNeeded();
29
29
  }
30
30
  }
31
31
  // Make sure all images have loaded.
32
- const promises = (await locators.all()).map(locator => locator.evaluate(settleImage));
32
+ const promises = (await locators.all()).map((locator) => locator.evaluate(settleImage));
33
33
  await Promise.all(promises);
34
34
  // The wait above treats an errored image as "loaded". Decode each visible
35
35
  // image as well; source rewriting is available only when explicitly enabled.
@@ -41,14 +41,14 @@ async function waitForImages(page, selector, options = {}) {
41
41
  await page.evaluate(() => window.scroll({
42
42
  top: 0,
43
43
  left: 0,
44
- behavior: 'instant',
44
+ behavior: "instant",
45
45
  }));
46
46
  // window.scroll is async and doesn't return a promise, so wait until the
47
47
  // browser confirms we are at the top again.
48
48
  const scrollState = { forced: false };
49
- await page.waitForFunction(state => {
49
+ await page.waitForFunction((state) => {
50
50
  if (window.scrollY !== 0 && !state.forced) {
51
- window.scroll({ top: 0, left: 0, behavior: 'instant' });
51
+ window.scroll({ top: 0, left: 0, behavior: "instant" });
52
52
  state.forced = true;
53
53
  }
54
54
  return window.scrollY === 0;
@@ -89,7 +89,7 @@ function settleImage(image) {
89
89
  if ((image.width <= 1 && image.height <= 1) || image.complete) {
90
90
  return;
91
91
  }
92
- return new Promise(resolve => {
92
+ return new Promise((resolve) => {
93
93
  const settled = () => {
94
94
  image.removeEventListener("load", settled);
95
95
  image.removeEventListener("error", settled);
@@ -105,7 +105,7 @@ function settleImage(image) {
105
105
  * @param page
106
106
  */
107
107
  async function waitForAllImages(page, options = {}) {
108
- await waitForImages(page, 'img:visible', options);
108
+ await waitForImages(page, "img:visible", options);
109
109
  }
110
110
  /**
111
111
  * Poll every visible image until it decodes, optionally re-requesting failures.
@@ -170,7 +170,7 @@ async function decodeVisibleImages(options) {
170
170
  // one fetched, instead of re-running srcset selection.
171
171
  const picture = img.closest("picture");
172
172
  if (picture) {
173
- picture.querySelectorAll("source").forEach(source => source.remove());
173
+ picture.querySelectorAll("source").forEach((source) => source.remove());
174
174
  }
175
175
  img.removeAttribute("srcset");
176
176
  img.src = url.href;
@@ -211,7 +211,7 @@ async function decodeVisibleImages(options) {
211
211
  return [];
212
212
  }
213
213
  if (Date.now() >= deadline) {
214
- return pending.map(img => {
214
+ return pending.map((img) => {
215
215
  const src = img.currentSrc || img.src;
216
216
  try {
217
217
  // Report the URL as the page authored it, without the cache-busting
@@ -230,7 +230,7 @@ async function decodeVisibleImages(options) {
230
230
  lastReload = Date.now();
231
231
  errored.forEach(reload);
232
232
  }
233
- await new Promise(resolve => setTimeout(resolve, pollMs));
233
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
234
234
  }
235
235
  }
236
236
  /**
@@ -252,9 +252,12 @@ async function decodeVisibleImages(options) {
252
252
  * @returns The URLs of the images that never decoded. Empty when they all did.
253
253
  */
254
254
  async function waitForImagesToDecode(page, timeoutMs = 15000, recoverErroredImages = false) {
255
- const undecoded = await page.evaluate(decodeVisibleImages, { timeoutMs, recoverErroredImages });
255
+ const undecoded = await page.evaluate(decodeVisibleImages, {
256
+ timeoutMs,
257
+ recoverErroredImages,
258
+ });
256
259
  if (undecoded.length > 0) {
257
- console.warn(`waitForImagesToDecode: ${undecoded.length} image(s) did not finish loading within ${timeoutMs}ms and may be captured as broken: ${undecoded.join(', ')}`);
260
+ console.warn(`waitForImagesToDecode: ${undecoded.length} image(s) did not finish loading within ${timeoutMs}ms and may be captured as broken: ${undecoded.join(", ")}`);
258
261
  }
259
262
  return undecoded;
260
263
  }
package/lib/index.d.ts CHANGED
@@ -1,13 +1,13 @@
1
- export * from './focus.js';
2
- export * from './accessibility-baseline.js';
3
- export * from './accessibility-baseline-file.js';
4
- export * from './accessible-screenshot.js';
5
- export * from './fonts.js';
6
- export * from './frames.js';
7
- export * from './hover.js';
8
- export * from './images.js';
9
- export * from './interaction-states.js';
10
- export * from './pseudo-state.js';
11
- export * from './videos.js';
12
- export * from './visualdiff.js';
13
- export * from './mock/index.js';
1
+ export * from "./focus.js";
2
+ export * from "./accessibility-baseline.js";
3
+ export * from "./accessibility-baseline-file.js";
4
+ export * from "./accessible-screenshot.js";
5
+ export * from "./fonts.js";
6
+ export * from "./frames.js";
7
+ export * from "./hover.js";
8
+ export * from "./images.js";
9
+ export * from "./interaction-states.js";
10
+ export * from "./pseudo-state.js";
11
+ export * from "./videos.js";
12
+ export * from "./visualdiff.js";
13
+ export * from "./mock/index.js";
@@ -1,6 +1,6 @@
1
- import type { Locator } from '@playwright/test';
1
+ import type { Locator } from "@playwright/test";
2
2
  /** Real browser interaction states supported consistently by Playwright. */
3
- export type InteractionState = 'hover' | 'focus';
3
+ export type InteractionState = "hover" | "focus";
4
4
  /** A locator and the real browser interaction states to apply to it. */
5
5
  export interface ScreenshotInteractionState {
6
6
  locator: Locator;
@@ -4,17 +4,17 @@ exports.validateInteractionStates = validateInteractionStates;
4
4
  exports.applyInteractionStates = applyInteractionStates;
5
5
  /** Validate that the requested states are possible with real browser input. */
6
6
  function validateInteractionStates(interactionStates) {
7
- const hoverTargets = interactionStates.filter(({ states }) => states.includes('hover'));
8
- const focusTargets = interactionStates.filter(({ states }) => states.includes('focus'));
7
+ const hoverTargets = interactionStates.filter(({ states }) => states.includes("hover"));
8
+ const focusTargets = interactionStates.filter(({ states }) => states.includes("focus"));
9
9
  if (hoverTargets.length > 1) {
10
- throw new Error('interactionStates can hover at most one locator at a time.');
10
+ throw new Error("interactionStates can hover at most one locator at a time.");
11
11
  }
12
12
  if (focusTargets.length > 1) {
13
- throw new Error('interactionStates can focus at most one locator at a time.');
13
+ throw new Error("interactionStates can focus at most one locator at a time.");
14
14
  }
15
15
  for (const { states } of interactionStates) {
16
16
  if (new Set(states).size !== states.length) {
17
- throw new Error('interactionStates cannot repeat a state for the same locator.');
17
+ throw new Error("interactionStates cannot repeat a state for the same locator.");
18
18
  }
19
19
  }
20
20
  }
@@ -27,8 +27,8 @@ function validateInteractionStates(interactionStates) {
27
27
  */
28
28
  async function applyInteractionStates(interactionStates, cleanupOptions = {}) {
29
29
  validateInteractionStates(interactionStates);
30
- const hoverTarget = interactionStates.find(({ states }) => states.includes('hover'))?.locator;
31
- const focusTarget = interactionStates.find(({ states }) => states.includes('focus'))?.locator;
30
+ const hoverTarget = interactionStates.find(({ states }) => states.includes("hover"))?.locator;
31
+ const focusTarget = interactionStates.find(({ states }) => states.includes("focus"))?.locator;
32
32
  let hoverApplied = false;
33
33
  let focusApplied = false;
34
34
  let cleaned = false;
@@ -1 +1 @@
1
- export { YoutubeMock } from './youtube.js';
1
+ export { YoutubeMock } from "./youtube.js";
@@ -1,4 +1,4 @@
1
- import type { Page } from '@playwright/test';
1
+ import type { Page } from "@playwright/test";
2
2
  import type { Mockable } from "../visualdiff.js";
3
3
  export declare class YoutubeMock implements Mockable {
4
4
  mock(page: Page): Promise<void>;
@@ -5,7 +5,7 @@ class YoutubeMock {
5
5
  async mock(page) {
6
6
  await page.route(/www\.youtube\.com/i, async (route) => {
7
7
  await route.fulfill({
8
- contentType: 'text/html',
8
+ contentType: "text/html",
9
9
  body: `
10
10
  <html>
11
11
  <head>
@@ -1,6 +1,6 @@
1
- import { Page } from '@playwright/test';
1
+ import { Page } from "@playwright/test";
2
2
  /** Pseudo-classes supported by Chromium's CSS.forcePseudoState command. */
3
- export type ForcedPseudoClass = 'active' | 'focus' | 'focus-visible' | 'focus-within' | 'hover' | 'target';
3
+ export type ForcedPseudoClass = "active" | "focus" | "focus-visible" | "focus-within" | "hover" | "target";
4
4
  /** A CSS selector and the pseudo-classes to force on its first match. */
5
5
  export interface ForcedPseudoState {
6
6
  selector: string;
@@ -15,20 +15,25 @@ async function forcePseudoState(page, selector, pseudoClasses) {
15
15
  session = await page.context().newCDPSession(page);
16
16
  }
17
17
  catch (error) {
18
- throw new Error('forcePseudoState() requires a Chromium browser project.', { cause: error });
18
+ throw new Error("forcePseudoState() requires a Chromium browser project.", {
19
+ cause: error,
20
+ });
19
21
  }
20
22
  try {
21
- await session.send('DOM.enable');
22
- await session.send('CSS.enable');
23
- const { root } = await session.send('DOM.getDocument');
24
- const { nodeId } = await session.send('DOM.querySelector', {
23
+ await session.send("DOM.enable");
24
+ await session.send("CSS.enable");
25
+ const { root } = await session.send("DOM.getDocument");
26
+ const { nodeId } = await session.send("DOM.querySelector", {
25
27
  nodeId: root.nodeId,
26
28
  selector,
27
29
  });
28
30
  if (!nodeId) {
29
31
  throw new Error(`forcePseudoState() could not find an element matching "${selector}".`);
30
32
  }
31
- await session.send('CSS.forcePseudoState', { nodeId, forcedPseudoClasses: pseudoClasses });
33
+ await session.send("CSS.forcePseudoState", {
34
+ nodeId,
35
+ forcedPseudoClasses: pseudoClasses,
36
+ });
32
37
  let cleared = false;
33
38
  return async () => {
34
39
  if (cleared) {
@@ -36,7 +41,10 @@ async function forcePseudoState(page, selector, pseudoClasses) {
36
41
  }
37
42
  cleared = true;
38
43
  try {
39
- await session.send('CSS.forcePseudoState', { nodeId, forcedPseudoClasses: [] });
44
+ await session.send("CSS.forcePseudoState", {
45
+ nodeId,
46
+ forcedPseudoClasses: [],
47
+ });
40
48
  }
41
49
  finally {
42
50
  await session.detach();
package/lib/videos.js CHANGED
@@ -128,7 +128,7 @@ async function settleVideos(options) {
128
128
  // running animation frames for a page it is not rendering -- one behind
129
129
  // another tab of the same context, say -- and page.evaluate() has no timeout
130
130
  // of its own, so an unguarded wait here hangs the whole capture.
131
- const nextPaint = () => new Promise(resolve => {
131
+ const nextPaint = () => new Promise((resolve) => {
132
132
  const timer = setTimeout(resolve, paintTimeoutMs);
133
133
  requestAnimationFrame(() => requestAnimationFrame(() => {
134
134
  clearTimeout(timer);
@@ -139,7 +139,7 @@ async function settleVideos(options) {
139
139
  // decode the frame at the new position and present it, which lands several
140
140
  // frames later when the position is not a keyframe. Waiting for `seeked` is
141
141
  // what makes the rewind visible in the capture rather than a race.
142
- const seeked = (video) => new Promise(resolve => {
142
+ const seeked = (video) => new Promise((resolve) => {
143
143
  const done = () => {
144
144
  clearTimeout(timer);
145
145
  video.removeEventListener("seeked", done);
@@ -155,15 +155,22 @@ async function settleVideos(options) {
155
155
  const ancestorScrolls = (video) => {
156
156
  const saved = [];
157
157
  for (let node = video.parentElement; node; node = node.parentElement) {
158
- if (node.scrollHeight > node.clientHeight || node.scrollWidth > node.clientWidth) {
159
- saved.push({ element: node, top: node.scrollTop, left: node.scrollLeft });
158
+ if (node.scrollHeight > node.clientHeight ||
159
+ node.scrollWidth > node.clientWidth) {
160
+ saved.push({
161
+ element: node,
162
+ top: node.scrollTop,
163
+ left: node.scrollLeft,
164
+ });
160
165
  }
161
166
  }
162
167
  return saved;
163
168
  };
164
169
  const sourceOf = (video) => {
165
- const src = video.currentSrc || video.src
166
- || video.querySelector("source")?.getAttribute("src") || "";
170
+ const src = video.currentSrc ||
171
+ video.src ||
172
+ video.querySelector("source")?.getAttribute("src") ||
173
+ "";
167
174
  try {
168
175
  return new URL(src, location.href).href;
169
176
  }
@@ -198,7 +205,11 @@ async function settleVideos(options) {
198
205
  // paint.
199
206
  const ancestors = ancestorScrolls(video);
200
207
  try {
201
- video.scrollIntoView({ block: "center", inline: "nearest", behavior: "instant" });
208
+ video.scrollIntoView({
209
+ block: "center",
210
+ inline: "nearest",
211
+ behavior: "instant",
212
+ });
202
213
  await nextPaint();
203
214
  // `preload="none"` means the browser fetches nothing until playback is
204
215
  // asked for, and nothing here ever asks. Scrolling alone does not override
@@ -218,7 +229,7 @@ async function settleVideos(options) {
218
229
  const deadline = Date.now() + timeoutMs;
219
230
  const readyBeforeWait = video.readyState >= HAVE_CURRENT_DATA;
220
231
  while (video.readyState < HAVE_CURRENT_DATA && Date.now() < deadline) {
221
- await new Promise(resolve => setTimeout(resolve, pollMs));
232
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
222
233
  }
223
234
  if (video.readyState < HAVE_CURRENT_DATA) {
224
235
  // Reported below, but still settled: a video that becomes ready between
@@ -298,7 +309,7 @@ function restorePlayback() {
298
309
  }
299
310
  }
300
311
  async function waitForVideos(page, options = {}) {
301
- const timeoutMs = typeof options === "number" ? options : options.timeoutMs ?? 5000;
312
+ const timeoutMs = typeof options === "number" ? options : (options.timeoutMs ?? 5000);
302
313
  const afterScroll = typeof options === "number" ? undefined : options.afterScroll;
303
314
  const notReady = [];
304
315
  let scrolled = false;
@@ -323,7 +334,7 @@ async function waitForVideos(page, options = {}) {
323
334
  }
324
335
  }
325
336
  if (notReady.length > 0) {
326
- console.warn(`waitForVideos: ${notReady.length} video(s) had no frame available within ${timeoutMs}ms and may be captured as an empty box: ${notReady.join(', ')}`);
337
+ console.warn(`waitForVideos: ${notReady.length} video(s) had no frame available within ${timeoutMs}ms and may be captured as an empty box: ${notReady.join(", ")}`);
327
338
  }
328
339
  return notReady;
329
340
  }
@@ -1,8 +1,8 @@
1
- import { type BrowserContext, type Page, type TestInfo } from '@playwright/test';
1
+ import { type BrowserContext, type Page, type TestInfo } from "@playwright/test";
2
2
  import { type ScreenshotOptions } from "./accessible-screenshot.js";
3
- import type { AccessibilityBaseline } from './accessibility-baseline.js';
4
- import { type ForcedPseudoState } from './pseudo-state.js';
5
- import type { InteractionState } from './interaction-states.js';
3
+ import type { AccessibilityBaseline } from "./accessibility-baseline.js";
4
+ import { type ForcedPseudoState } from "./pseudo-state.js";
5
+ import type { InteractionState } from "./interaction-states.js";
6
6
  export interface VisualDiffExecutionContext {
7
7
  page: Page;
8
8
  testInfo: TestInfo;
package/lib/visualdiff.js CHANGED
@@ -17,25 +17,31 @@ function defaultTestFunction(testCase, group, config, preset) {
17
17
  await mock.mock(page);
18
18
  }
19
19
  // Log any errors to the Playwright console too.
20
- context.on('weberror', (webError) => console.log(webError.error()));
20
+ context.on("weberror", (webError) => console.log(webError.error()));
21
21
  testInfo.annotations.push({
22
- type: 'Description',
22
+ type: "Description",
23
23
  description: testCase.description,
24
24
  });
25
- const representativeUrl = testCase.representativeUrl
26
- ?? group.representativeUrl
27
- ?? config?.representativeUrl;
25
+ const representativeUrl = testCase.representativeUrl ??
26
+ group.representativeUrl ??
27
+ config?.representativeUrl;
28
28
  if (representativeUrl) {
29
29
  testInfo.annotations.push({
30
- type: 'Representative URL',
30
+ type: "Representative URL",
31
31
  description: representativeUrl,
32
32
  });
33
33
  }
34
- const path = `${config?.pathPrefix ?? ''}${group.pathPrefix ?? ''}${testCase.path}`;
34
+ const path = `${config?.pathPrefix ?? ""}${group.pathPrefix ?? ""}${testCase.path}`;
35
35
  await page.goto(path);
36
- const configuredScreenshotOptions = typeof preset?.screenshotOptions === 'function'
37
- ? await preset.screenshotOptions({ page, testInfo, testCase, group, config })
38
- : preset?.screenshotOptions ?? {};
36
+ const configuredScreenshotOptions = typeof preset?.screenshotOptions === "function"
37
+ ? await preset.screenshotOptions({
38
+ page,
39
+ testInfo,
40
+ testCase,
41
+ group,
42
+ config,
43
+ })
44
+ : (preset?.screenshotOptions ?? {});
39
45
  // Merge masks from all three levels: config, group, and testCase.
40
46
  const maskSelectors = [
41
47
  ...(config?.mask ?? []),
@@ -44,13 +50,13 @@ function defaultTestFunction(testCase, group, config, preset) {
44
50
  ];
45
51
  const maskLocators = [
46
52
  ...(configuredScreenshotOptions.mask ?? []),
47
- ...maskSelectors.map(selector => page.locator(selector)),
53
+ ...maskSelectors.map((selector) => page.locator(selector)),
48
54
  ];
49
55
  // Most-specific-wins for maskColor: testCase > group > config.
50
- const maskColor = testCase.maskColor
51
- ?? group.maskColor
52
- ?? config?.maskColor
53
- ?? configuredScreenshotOptions.maskColor;
56
+ const maskColor = testCase.maskColor ??
57
+ group.maskColor ??
58
+ config?.maskColor ??
59
+ configuredScreenshotOptions.maskColor;
54
60
  const screenshotOptions = {
55
61
  ...configuredScreenshotOptions,
56
62
  fullPage: true,
@@ -61,10 +67,10 @@ function defaultTestFunction(testCase, group, config, preset) {
61
67
  if (maskColor) {
62
68
  screenshotOptions.maskColor = maskColor;
63
69
  }
64
- const a11yBaseline = testCase.a11yBaseline
65
- ?? group.a11yBaseline
66
- ?? config?.a11yBaseline
67
- ?? configuredScreenshotOptions.accessibility?.baseline;
70
+ const a11yBaseline = testCase.a11yBaseline ??
71
+ group.a11yBaseline ??
72
+ config?.a11yBaseline ??
73
+ configuredScreenshotOptions.accessibility?.baseline;
68
74
  if (a11yBaseline || configuredScreenshotOptions.accessibility) {
69
75
  screenshotOptions.accessibility = {
70
76
  ...configuredScreenshotOptions.accessibility,
@@ -76,7 +82,8 @@ function defaultTestFunction(testCase, group, config, preset) {
76
82
  ...(group.interactionStates ?? []),
77
83
  ...(testCase.interactionStates ?? []),
78
84
  ];
79
- if (interactionStates.length > 0 || configuredScreenshotOptions.interactionStates) {
85
+ if (interactionStates.length > 0 ||
86
+ configuredScreenshotOptions.interactionStates) {
80
87
  screenshotOptions.interactionStates = [
81
88
  ...(configuredScreenshotOptions.interactionStates ?? []),
82
89
  ...interactionStates.map(({ selector, states }) => ({
@@ -118,7 +125,7 @@ function defaultTestFunction(testCase, group, config, preset) {
118
125
  }
119
126
  if (operationFailed) {
120
127
  if (cleanupErrors.length > 0) {
121
- throw new AggregateError([operationError, ...cleanupErrors], 'Visual diff capture and pseudo-state cleanup failed.');
128
+ throw new AggregateError([operationError, ...cleanupErrors], "Visual diff capture and pseudo-state cleanup failed.");
122
129
  }
123
130
  throw operationError;
124
131
  }
@@ -126,7 +133,7 @@ function defaultTestFunction(testCase, group, config, preset) {
126
133
  throw cleanupErrors[0];
127
134
  }
128
135
  if (cleanupErrors.length > 1) {
129
- throw new AggregateError(cleanupErrors, 'Multiple pseudo-state cleanup operations failed.');
136
+ throw new AggregateError(cleanupErrors, "Multiple pseudo-state cleanup operations failed.");
130
137
  }
131
138
  };
132
139
  }
@@ -159,8 +166,9 @@ class VisualDiffTestCases {
159
166
  describe(overriddenTestFunction) {
160
167
  // Handle skipping of test cases, either based on a simple boolean or a callback.
161
168
  function shouldSkip(testCase) {
162
- return testCase.skip !== undefined
163
- && (testCase.skip.callback === undefined || testCase.skip.callback(testCase));
169
+ return (testCase.skip !== undefined &&
170
+ (testCase.skip.callback === undefined ||
171
+ testCase.skip.callback(testCase)));
164
172
  }
165
173
  function registerSkip(testCase) {
166
174
  const skip = testCase.skip;
@@ -174,7 +182,7 @@ class VisualDiffTestCases {
174
182
  }
175
183
  // Actually describe the group.
176
184
  test_1.test.describe(group.name, () => {
177
- group.testCases.forEach((testCase => {
185
+ group.testCases.forEach((testCase) => {
178
186
  // Allow skipping of individual test cases.
179
187
  if (shouldSkip(testCase)) {
180
188
  registerSkip(testCase);
@@ -182,14 +190,14 @@ class VisualDiffTestCases {
182
190
  }
183
191
  // Define a default function for test cases.
184
192
  let testFunction;
185
- if (typeof overriddenTestFunction != 'function') {
193
+ if (typeof overriddenTestFunction != "function") {
186
194
  testFunction = defaultTestFunction(testCase, group, this.config, this.preset);
187
195
  }
188
196
  else {
189
197
  testFunction = overriddenTestFunction(testCase, group);
190
198
  }
191
199
  (0, test_1.test)(`${testCase.name}: ${testCase.path}`, testFunction);
192
- }));
200
+ });
193
201
  });
194
202
  });
195
203
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lullabot/playwright-testing",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Framework-neutral Playwright testing utilities",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -17,8 +17,8 @@
17
17
  "README.md"
18
18
  ],
19
19
  "bin": {
20
- "playwright-testing-a11y-summary": "./bin/github-a11y-summary",
21
- "playwright-testing-failure-summary": "./bin/github-failure-summary"
20
+ "playwright-testing-a11y-summary": "bin/github-a11y-summary",
21
+ "playwright-testing-failure-summary": "bin/github-failure-summary"
22
22
  },
23
23
  "exports": {
24
24
  ".": {