@lullabot/playwright-testing 0.1.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
@@ -1,6 +1,11 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
2
 
3
- import { decodeVisibleImages, settleImage, waitForImages, waitForImagesToDecode } from './images'
3
+ import {
4
+ decodeVisibleImages,
5
+ settleImage,
6
+ waitForImages,
7
+ waitForImagesToDecode,
8
+ } from "./images";
4
9
 
5
10
  /**
6
11
  * A stand-in for the parts of HTMLImageElement decodeVisibleImages() touches.
@@ -11,195 +16,219 @@ import { decodeVisibleImages, settleImage, waitForImages, waitForImagesToDecode
11
16
  * Stage File Proxy URL does once its on-demand fetch finishes.
12
17
  */
13
18
  interface FakeImageOptions {
14
- src?: string
15
- currentSrc?: string
16
- width?: number
17
- height?: number
18
- rect?: { width: number, height: number }
19
- style?: { visibility?: string, display?: string }
19
+ src?: string;
20
+ currentSrc?: string;
21
+ width?: number;
22
+ height?: number;
23
+ rect?: { width: number; height: number };
24
+ style?: { visibility?: string; display?: string };
20
25
  /** 'loaded', 'loading' (never completes on its own), or 'broken'. */
21
- state?: 'loaded' | 'loading' | 'broken'
26
+ state?: "loaded" | "loading" | "broken";
22
27
  /** Whether a re-request makes a broken image decode. */
23
- recoversOnReload?: boolean
28
+ recoversOnReload?: boolean;
24
29
  /** <source> elements of an enclosing <picture>, if any. */
25
- sources?: string[]
30
+ sources?: string[];
26
31
  }
27
32
 
28
33
  function makeImage(options: FakeImageOptions = {}) {
29
- const state = options.state ?? 'loaded'
30
- const sources = options.sources
31
- let src = options.src ?? 'https://example.com/image.jpg'
32
- let broken = state === 'broken'
34
+ const state = options.state ?? "loaded";
35
+ const sources = options.sources;
36
+ let src = options.src ?? "https://example.com/image.jpg";
37
+ let broken = state === "broken";
33
38
 
34
39
  const img: any = {
35
40
  width: options.width ?? 100,
36
41
  height: options.height ?? 100,
37
- complete: state !== 'loading',
38
- naturalWidth: state === 'loaded' ? 100 : 0,
39
- currentSrc: options.currentSrc ?? '',
42
+ complete: state !== "loading",
43
+ naturalWidth: state === "loaded" ? 100 : 0,
44
+ currentSrc: options.currentSrc ?? "",
40
45
  removedAttributes: [] as string[],
41
46
  picture: sources
42
47
  ? {
43
- sources: sources.slice(),
44
- querySelectorAll: (_selector: string) =>
45
- img.picture.sources.map((name: string) => ({
46
- remove: () => {
47
- img.picture.sources = img.picture.sources.filter((s: string) => s !== name)
48
- },
49
- })),
50
- }
48
+ sources: sources.slice(),
49
+ querySelectorAll: () =>
50
+ img.picture.sources.map((name: string) => ({
51
+ remove: () => {
52
+ img.picture.sources = img.picture.sources.filter(
53
+ (s: string) => s !== name,
54
+ );
55
+ },
56
+ })),
57
+ }
51
58
  : null,
52
59
  getBoundingClientRect: () => options.rect ?? { width: 100, height: 100 },
53
60
  style: {
54
- visibility: options.style?.visibility ?? 'visible',
55
- display: options.style?.display ?? 'block',
61
+ visibility: options.style?.visibility ?? "visible",
62
+ display: options.style?.display ?? "block",
56
63
  },
57
- decode: () => (broken ? Promise.reject(new Error('decode failed')) : Promise.resolve()),
58
- closest: (selector: string) => (selector === 'picture' ? img.picture : null),
64
+ decode: () =>
65
+ broken ? Promise.reject(new Error("decode failed")) : Promise.resolve(),
66
+ closest: (selector: string) =>
67
+ selector === "picture" ? img.picture : null,
59
68
  removeAttribute: (name: string) => {
60
- img.removedAttributes.push(name)
69
+ img.removedAttributes.push(name);
61
70
  },
62
71
  /** Pretend the network finished, for images that start out loading. */
63
72
  finishLoading: () => {
64
- img.complete = true
65
- img.naturalWidth = 100
73
+ img.complete = true;
74
+ img.naturalWidth = 100;
66
75
  },
67
- }
76
+ };
68
77
 
69
- Object.defineProperty(img, 'src', {
78
+ Object.defineProperty(img, "src", {
70
79
  get: () => src,
71
80
  set: (value: string) => {
72
- src = value
81
+ src = value;
73
82
  // A re-request clears whatever the browser had resolved from srcset.
74
- img.currentSrc = ''
83
+ img.currentSrc = "";
75
84
  if (options.recoversOnReload) {
76
- broken = false
77
- img.naturalWidth = 100
85
+ broken = false;
86
+ img.naturalWidth = 100;
78
87
  }
79
88
  },
80
- })
89
+ });
81
90
 
82
- return img
91
+ return img;
83
92
  }
84
93
 
85
94
  function stubDom(images: any[]) {
86
- vi.stubGlobal('document', { images })
87
- vi.stubGlobal('location', { href: 'https://example.com/page' })
88
- vi.stubGlobal('getComputedStyle', (element: any) => element.style)
95
+ vi.stubGlobal("document", { images });
96
+ vi.stubGlobal("location", { href: "https://example.com/page" });
97
+ vi.stubGlobal("getComputedStyle", (element: any) => element.style);
89
98
  }
90
99
 
91
100
  // Real timers, so the polling is real: keep the intervals tiny.
92
- const fast = { pollMs: 1, reloadIntervalMs: 0 }
101
+ const fast = { pollMs: 1, reloadIntervalMs: 0 };
93
102
 
94
- describe('decodeVisibleImages', () => {
103
+ describe("decodeVisibleImages", () => {
95
104
  afterEach(() => {
96
- vi.unstubAllGlobals()
97
- })
105
+ vi.unstubAllGlobals();
106
+ });
98
107
 
99
- it('returns no failures when every visible image has decoded', async () => {
100
- stubDom([makeImage(), makeImage()])
108
+ it("returns no failures when every visible image has decoded", async () => {
109
+ stubDom([makeImage(), makeImage()]);
101
110
 
102
- expect(await decodeVisibleImages({ timeoutMs: 1000, ...fast })).toEqual([])
103
- })
111
+ expect(await decodeVisibleImages({ timeoutMs: 1000, ...fast })).toEqual([]);
112
+ });
104
113
 
105
- it('treats a dimensionless but decodable image as loaded', async () => {
114
+ it("treats a dimensionless but decodable image as loaded", async () => {
106
115
  // An SVG with no intrinsic size is complete with a naturalWidth of 0, but
107
116
  // decode() resolves for it. It must not be re-requested or reported.
108
- const svg = makeImage({ src: 'https://example.com/logo.svg', state: 'broken' })
109
- svg.decode = () => Promise.resolve()
110
- stubDom([svg])
111
-
112
- expect(await decodeVisibleImages({ timeoutMs: 1000, ...fast })).toEqual([])
113
- expect(svg.src).toBe('https://example.com/logo.svg')
114
- })
115
-
116
- it('ignores 1x1, zero-size, and hidden images even when they are broken', async () => {
117
+ const svg = makeImage({
118
+ src: "https://example.com/logo.svg",
119
+ state: "broken",
120
+ });
121
+ svg.decode = () => Promise.resolve();
122
+ stubDom([svg]);
123
+
124
+ expect(await decodeVisibleImages({ timeoutMs: 1000, ...fast })).toEqual([]);
125
+ expect(svg.src).toBe("https://example.com/logo.svg");
126
+ });
127
+
128
+ it("ignores 1x1, zero-size, and hidden images even when they are broken", async () => {
117
129
  stubDom([
118
- makeImage({ state: 'broken', width: 1, height: 1 }),
119
- makeImage({ state: 'broken', rect: { width: 0, height: 0 } }),
120
- makeImage({ state: 'broken', style: { visibility: 'hidden' } }),
121
- makeImage({ state: 'broken', style: { display: 'none' } }),
122
- ])
130
+ makeImage({ state: "broken", width: 1, height: 1 }),
131
+ makeImage({ state: "broken", rect: { width: 0, height: 0 } }),
132
+ makeImage({ state: "broken", style: { visibility: "hidden" } }),
133
+ makeImage({ state: "broken", style: { display: "none" } }),
134
+ ]);
123
135
 
124
- expect(await decodeVisibleImages({ timeoutMs: 0, ...fast })).toEqual([])
125
- })
136
+ expect(await decodeVisibleImages({ timeoutMs: 0, ...fast })).toEqual([]);
137
+ });
126
138
 
127
- it('waits for an image that is still loading', async () => {
128
- const loading = makeImage({ state: 'loading' })
129
- stubDom([loading])
130
- setTimeout(() => loading.finishLoading(), 5)
139
+ it("waits for an image that is still loading", async () => {
140
+ const loading = makeImage({ state: "loading" });
141
+ stubDom([loading]);
142
+ setTimeout(() => loading.finishLoading(), 5);
131
143
 
132
- expect(await decodeVisibleImages({ timeoutMs: 2000, ...fast })).toEqual([])
133
- })
144
+ expect(await decodeVisibleImages({ timeoutMs: 2000, ...fast })).toEqual([]);
145
+ });
134
146
 
135
- it('re-requests a broken image and reports success once it decodes', async () => {
147
+ it("re-requests a broken image and reports success once it decodes", async () => {
136
148
  const broken = makeImage({
137
- src: 'https://example.com/broken.jpg',
138
- currentSrc: 'https://example.com/broken-800.jpg',
139
- state: 'broken',
149
+ src: "https://example.com/broken.jpg",
150
+ currentSrc: "https://example.com/broken-800.jpg",
151
+ state: "broken",
140
152
  recoversOnReload: true,
141
- sources: ['source-1', 'source-2'],
142
- })
143
- stubDom([broken])
144
-
145
- expect(await decodeVisibleImages({
146
- timeoutMs: 2000,
147
- recoverErroredImages: true,
148
- ...fast,
149
- })).toEqual([])
153
+ sources: ["source-1", "source-2"],
154
+ });
155
+ stubDom([broken]);
156
+
157
+ expect(
158
+ await decodeVisibleImages({
159
+ timeoutMs: 2000,
160
+ recoverErroredImages: true,
161
+ ...fast,
162
+ }),
163
+ ).toEqual([]);
150
164
  // The retry reuses the URL already resolved from srcset, cache-busted...
151
- expect(broken.src).toMatch(/^https:\/\/example\.com\/broken-800\.jpg\?playwrightReload=\d+$/)
165
+ expect(broken.src).toMatch(
166
+ /^https:\/\/example\.com\/broken-800\.jpg\?playwrightReload=\d+$/,
167
+ );
152
168
  // ...with the responsive sources dropped so that exact URL is fetched.
153
- expect(broken.removedAttributes).toContain('srcset')
154
- expect(broken.picture.sources).toEqual([])
155
- })
169
+ expect(broken.removedAttributes).toContain("srcset");
170
+ expect(broken.picture.sources).toEqual([]);
171
+ });
156
172
 
157
- it('does not rewrite a broken image source unless recovery is opted into', async () => {
173
+ it("does not rewrite a broken image source unless recovery is opted into", async () => {
158
174
  const broken = makeImage({
159
- src: 'https://example.com/broken.jpg',
160
- state: 'broken',
175
+ src: "https://example.com/broken.jpg",
176
+ state: "broken",
161
177
  recoversOnReload: true,
162
- })
163
- stubDom([broken])
164
-
165
- expect(await decodeVisibleImages({timeoutMs: 0, ...fast})).toEqual([
166
- 'https://example.com/broken.jpg',
167
- ])
168
- expect(broken.src).toBe('https://example.com/broken.jpg')
169
- expect(broken.removedAttributes).toEqual([])
170
- })
178
+ });
179
+ stubDom([broken]);
171
180
 
172
- it('leaves data: URLs alone', async () => {
173
- const data = makeImage({ src: 'data:image/png;base64,AAAA', state: 'broken' })
174
- stubDom([data])
181
+ expect(await decodeVisibleImages({ timeoutMs: 0, ...fast })).toEqual([
182
+ "https://example.com/broken.jpg",
183
+ ]);
184
+ expect(broken.src).toBe("https://example.com/broken.jpg");
185
+ expect(broken.removedAttributes).toEqual([]);
186
+ });
187
+
188
+ it("leaves data: URLs alone", async () => {
189
+ const data = makeImage({
190
+ src: "data:image/png;base64,AAAA",
191
+ state: "broken",
192
+ });
193
+ stubDom([data]);
175
194
 
176
- expect(await decodeVisibleImages({ timeoutMs: 0, ...fast })).toEqual(['data:image/png;base64,AAAA'])
177
- expect(data.src).toBe('data:image/png;base64,AAAA')
178
- })
195
+ expect(await decodeVisibleImages({ timeoutMs: 0, ...fast })).toEqual([
196
+ "data:image/png;base64,AAAA",
197
+ ]);
198
+ expect(data.src).toBe("data:image/png;base64,AAAA");
199
+ });
179
200
 
180
- it('reports the images that never decode instead of returning silently', async () => {
181
- const broken = makeImage({ src: 'https://example.com/missing.jpg', state: 'broken' })
182
- const loading = makeImage({ src: 'https://example.com/slow.jpg', state: 'loading' })
183
- stubDom([makeImage(), broken, loading])
201
+ it("reports the images that never decode instead of returning silently", async () => {
202
+ const broken = makeImage({
203
+ src: "https://example.com/missing.jpg",
204
+ state: "broken",
205
+ });
206
+ const loading = makeImage({
207
+ src: "https://example.com/slow.jpg",
208
+ state: "loading",
209
+ });
210
+ stubDom([makeImage(), broken, loading]);
184
211
 
185
- const undecoded = await decodeVisibleImages({ timeoutMs: 20, ...fast })
212
+ const undecoded = await decodeVisibleImages({ timeoutMs: 20, ...fast });
186
213
 
187
214
  // The cache-busting parameter the retries added is stripped back off, so
188
215
  // the reported URL is the one the page asked for.
189
216
  expect(undecoded).toEqual([
190
- 'https://example.com/missing.jpg',
191
- 'https://example.com/slow.jpg',
192
- ])
193
- })
217
+ "https://example.com/missing.jpg",
218
+ "https://example.com/slow.jpg",
219
+ ]);
220
+ });
194
221
 
195
- it('checks the images at least once even with an elapsed timeout', async () => {
196
- stubDom([makeImage({ src: 'https://example.com/missing.jpg', state: 'broken' })])
222
+ it("checks the images at least once even with an elapsed timeout", async () => {
223
+ stubDom([
224
+ makeImage({ src: "https://example.com/missing.jpg", state: "broken" }),
225
+ ]);
197
226
 
198
227
  expect(await decodeVisibleImages({ timeoutMs: 0, ...fast })).toEqual([
199
- 'https://example.com/missing.jpg',
200
- ])
201
- })
202
- })
228
+ "https://example.com/missing.jpg",
229
+ ]);
230
+ });
231
+ });
203
232
 
204
233
  /**
205
234
  * A stand-in for the parts of HTMLImageElement settleImage() touches.
@@ -207,8 +236,10 @@ describe('decodeVisibleImages', () => {
207
236
  * settleImage() also runs in the browser, so it is exercised against a fake
208
237
  * element whose load and error events can be fired on demand.
209
238
  */
210
- function makeSettleImage(options: { width?: number, height?: number, complete?: boolean } = {}) {
211
- const listeners: Record<string, Array<() => void>> = { load: [], error: [] }
239
+ function makeSettleImage(
240
+ options: { width?: number; height?: number; complete?: boolean } = {},
241
+ ) {
242
+ const listeners: Record<string, Array<() => void>> = { load: [], error: [] };
212
243
 
213
244
  return {
214
245
  width: options.width ?? 100,
@@ -218,138 +249,152 @@ function makeSettleImage(options: { width?: number, height?: number, complete?:
218
249
  onload: null,
219
250
  onerror: null,
220
251
  addEventListener: (type: string, handler: () => void) => {
221
- listeners[type].push(handler)
252
+ listeners[type].push(handler);
222
253
  },
223
254
  removeEventListener: (type: string, handler: () => void) => {
224
- listeners[type] = listeners[type].filter(h => h !== handler)
255
+ listeners[type] = listeners[type].filter((h) => h !== handler);
225
256
  },
226
- fire: (type: 'load' | 'error') => {
227
- listeners[type].slice().forEach(handler => handler())
257
+ fire: (type: "load" | "error") => {
258
+ listeners[type].slice().forEach((handler) => handler());
228
259
  },
229
260
  listenerCount: () => listeners.load.length + listeners.error.length,
230
- }
261
+ };
231
262
  }
232
263
 
233
- describe('settleImage', () => {
234
- it('does not wait for an image that has already settled', () => {
235
- const settled = makeSettleImage({ complete: true })
264
+ describe("settleImage", () => {
265
+ it("does not wait for an image that has already settled", () => {
266
+ const settled = makeSettleImage({ complete: true });
236
267
 
237
268
  // No promise back means nothing to await.
238
- expect(settleImage(settled as any)).toBeUndefined()
239
- expect(settled.listenerCount()).toBe(0)
240
- })
269
+ expect(settleImage(settled as any)).toBeUndefined();
270
+ expect(settled.listenerCount()).toBe(0);
271
+ });
241
272
 
242
- it('does not wait for a 1x1 visually-hidden image', () => {
273
+ it("does not wait for a 1x1 visually-hidden image", () => {
243
274
  // Chrome never loads these at desktop widths, so waiting would hang.
244
- const hidden = makeSettleImage({ width: 1, height: 1, complete: false })
275
+ const hidden = makeSettleImage({ width: 1, height: 1, complete: false });
245
276
 
246
- expect(settleImage(hidden as any)).toBeUndefined()
247
- expect(hidden.listenerCount()).toBe(0)
248
- })
277
+ expect(settleImage(hidden as any)).toBeUndefined();
278
+ expect(hidden.listenerCount()).toBe(0);
279
+ });
249
280
 
250
- it('resolves once a loading image loads', async () => {
251
- const loading = makeSettleImage()
252
- const settled = settleImage(loading as any)
253
- loading.fire('load')
281
+ it("resolves once a loading image loads", async () => {
282
+ const loading = makeSettleImage();
283
+ const settled = settleImage(loading as any);
284
+ loading.fire("load");
254
285
 
255
- await expect(settled).resolves.toBeUndefined()
256
- })
286
+ await expect(settled).resolves.toBeUndefined();
287
+ });
257
288
 
258
- it('resolves once a loading image errors', async () => {
289
+ it("resolves once a loading image errors", async () => {
259
290
  // The regression: an image that 404s or 503s after the wait begins only
260
291
  // ever fires `error`. Waiting on `load` alone hung until the test timed
261
292
  // out, and never reached the decode retry that could have recovered it.
262
- const loading = makeSettleImage()
263
- const settled = settleImage(loading as any)
264
- loading.fire('error')
293
+ const loading = makeSettleImage();
294
+ const settled = settleImage(loading as any);
295
+ loading.fire("error");
265
296
 
266
- await expect(settled).resolves.toBeUndefined()
267
- })
297
+ await expect(settled).resolves.toBeUndefined();
298
+ });
268
299
 
269
- it('cleans up both listeners once settled, and leaves the page handlers alone', async () => {
270
- const loading = makeSettleImage()
271
- const settled = settleImage(loading as any)
272
- expect(loading.listenerCount()).toBe(2)
300
+ it("cleans up both listeners once settled, and leaves the page handlers alone", async () => {
301
+ const loading = makeSettleImage();
302
+ const settled = settleImage(loading as any);
303
+ expect(loading.listenerCount()).toBe(2);
273
304
 
274
- loading.fire('error')
275
- await settled
305
+ loading.fire("error");
306
+ await settled;
276
307
 
277
- expect(loading.listenerCount()).toBe(0)
308
+ expect(loading.listenerCount()).toBe(0);
278
309
  // Assigning onload/onerror would have replaced whatever the page installed.
279
- expect(loading.onload).toBeNull()
280
- expect(loading.onerror).toBeNull()
281
- })
282
- })
310
+ expect(loading.onload).toBeNull();
311
+ expect(loading.onerror).toBeNull();
312
+ });
313
+ });
283
314
 
284
- describe('waitForImagesToDecode', () => {
285
- let warn: any
315
+ describe("waitForImagesToDecode", () => {
316
+ let warn: any;
286
317
 
287
318
  beforeEach(() => {
288
- warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
289
- })
319
+ warn = vi.spyOn(console, "warn").mockImplementation(() => {});
320
+ });
290
321
 
291
322
  afterEach(() => {
292
- warn.mockRestore()
293
- })
323
+ warn.mockRestore();
324
+ });
294
325
 
295
326
  function makePage(undecoded: string[]) {
296
327
  return {
297
328
  evaluate: vi.fn().mockResolvedValue(undecoded),
298
- } as any
329
+ } as any;
299
330
  }
300
331
 
301
- it('runs the decode poll in the page with the requested timeout', async () => {
302
- const page = makePage([])
332
+ it("runs the decode poll in the page with the requested timeout", async () => {
333
+ const page = makePage([]);
303
334
 
304
- expect(await waitForImagesToDecode(page, 5000)).toEqual([])
335
+ expect(await waitForImagesToDecode(page, 5000)).toEqual([]);
305
336
  // The browser-side function is handed to evaluate() by reference so
306
337
  // Playwright serializes it: it must not be wrapped in a closure over
307
338
  // anything in this module.
308
339
  expect(page.evaluate).toHaveBeenCalledWith(decodeVisibleImages, {
309
340
  timeoutMs: 5000,
310
341
  recoverErroredImages: false,
311
- })
312
- expect(warn).not.toHaveBeenCalled()
313
- })
342
+ });
343
+ expect(warn).not.toHaveBeenCalled();
344
+ });
314
345
 
315
- it('defaults to a 15 second timeout', async () => {
316
- const page = makePage([])
346
+ it("defaults to a 15 second timeout", async () => {
347
+ const page = makePage([]);
317
348
 
318
- await waitForImagesToDecode(page)
349
+ await waitForImagesToDecode(page);
319
350
 
320
351
  expect(page.evaluate).toHaveBeenCalledWith(decodeVisibleImages, {
321
352
  timeoutMs: 15000,
322
353
  recoverErroredImages: false,
323
- })
324
- })
325
-
326
- it('warns about, and returns, the images that never decoded', async () => {
327
- const page = makePage(['https://example.com/a.jpg', 'https://example.com/b.jpg'])
354
+ });
355
+ });
328
356
 
329
- const undecoded = await waitForImagesToDecode(page, 1000)
357
+ it("warns about, and returns, the images that never decoded", async () => {
358
+ const page = makePage([
359
+ "https://example.com/a.jpg",
360
+ "https://example.com/b.jpg",
361
+ ]);
330
362
 
331
- expect(undecoded).toEqual(['https://example.com/a.jpg', 'https://example.com/b.jpg'])
332
- expect(warn).toHaveBeenCalledTimes(1)
333
- expect(warn.mock.calls[0][0]).toContain('2 image(s) did not finish loading within 1000ms')
334
- expect(warn.mock.calls[0][0]).toContain('https://example.com/a.jpg, https://example.com/b.jpg')
335
- })
336
- })
363
+ const undecoded = await waitForImagesToDecode(page, 1000);
337
364
 
338
- describe('waitForImages', () => {
339
- it('restores the viewport and runs the adapter hook when decoding fails', async () => {
340
- const failure = new Error('decode failed')
341
- const afterScroll = vi.fn().mockResolvedValue(undefined)
365
+ expect(undecoded).toEqual([
366
+ "https://example.com/a.jpg",
367
+ "https://example.com/b.jpg",
368
+ ]);
369
+ expect(warn).toHaveBeenCalledTimes(1);
370
+ expect(warn.mock.calls[0][0]).toContain(
371
+ "2 image(s) did not finish loading within 1000ms",
372
+ );
373
+ expect(warn.mock.calls[0][0]).toContain(
374
+ "https://example.com/a.jpg, https://example.com/b.jpg",
375
+ );
376
+ });
377
+ });
378
+
379
+ describe("waitForImages", () => {
380
+ it("restores the viewport and runs the adapter hook when decoding fails", async () => {
381
+ const failure = new Error("decode failed");
382
+ const afterScroll = vi.fn().mockResolvedValue(undefined);
342
383
  const page = {
343
- locator: vi.fn().mockReturnValue({all: vi.fn().mockResolvedValue([])}),
384
+ locator: vi.fn().mockReturnValue({ all: vi.fn().mockResolvedValue([]) }),
344
385
  evaluate: vi.fn((callback: unknown) =>
345
- callback === decodeVisibleImages ? Promise.reject(failure) : Promise.resolve(undefined)
386
+ callback === decodeVisibleImages
387
+ ? Promise.reject(failure)
388
+ : Promise.resolve(undefined),
346
389
  ),
347
390
  waitForFunction: vi.fn().mockResolvedValue(undefined),
348
- } as any
391
+ } as any;
349
392
 
350
- await expect(waitForImages(page, 'img', {afterScroll})).rejects.toBe(failure)
393
+ await expect(waitForImages(page, "img", { afterScroll })).rejects.toBe(
394
+ failure,
395
+ );
351
396
 
352
- expect(page.waitForFunction).toHaveBeenCalledOnce()
353
- expect(afterScroll).toHaveBeenCalledWith(page)
354
- })
355
- })
397
+ expect(page.waitForFunction).toHaveBeenCalledOnce();
398
+ expect(afterScroll).toHaveBeenCalledWith(page);
399
+ });
400
+ });