@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
@@ -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 { restorePlayback, restoreVideoPlayback, settleVideos, waitForVideos } from './videos'
3
+ import {
4
+ restorePlayback,
5
+ restoreVideoPlayback,
6
+ settleVideos,
7
+ waitForVideos,
8
+ } from "./videos";
4
9
 
5
10
  /**
6
11
  * A stand-in for the parts of HTMLVideoElement settleVideos() touches.
@@ -10,45 +15,46 @@ import { restorePlayback, restoreVideoPlayback, settleVideos, waitForVideos } fr
10
15
  * frame, whether it is playing, and whether it can be seeked.
11
16
  */
12
17
  interface FakeVideoOptions {
13
- src?: string
18
+ src?: string;
14
19
  /** Sources of nested <source> elements, when there is no src attribute. */
15
- sourceSrc?: string
20
+ sourceSrc?: string;
16
21
  /** HTMLMediaElement.readyState. 2 (HAVE_CURRENT_DATA) means a frame exists. */
17
- readyState?: number
22
+ readyState?: number;
18
23
  /** readyState to switch to after `readyAfterMs`, simulating a slow load. */
19
- readyAfterMs?: number
24
+ readyAfterMs?: number;
20
25
  /** When true, readyState only starts climbing once load() is called. */
21
- readyAfterLoad?: boolean
26
+ readyAfterLoad?: boolean;
22
27
  /** The preload attribute. "none" means nothing loads until asked for. */
23
- preload?: string
24
- paused?: boolean
25
- autoplay?: boolean
26
- currentTime?: number
28
+ preload?: string;
29
+ paused?: boolean;
30
+ autoplay?: boolean;
31
+ currentTime?: number;
27
32
  /** When false, assigning currentTime throws, as on a live stream. */
28
- seekable?: boolean
33
+ seekable?: boolean;
29
34
  /** When true, a seek is accepted but the `seeked` event never arrives. */
30
- neverSeeked?: boolean
31
- rect?: { width: number, height: number }
32
- style?: { visibility?: string, display?: string }
35
+ neverSeeked?: boolean;
36
+ rect?: { width: number; height: number };
37
+ style?: { visibility?: string; display?: string };
33
38
  /** A scrollable ancestor that scrollIntoView() moves along with the window. */
34
- scrollParent?: any
39
+ scrollParent?: any;
35
40
  /** Where scrollIntoView() leaves the window, as the browser would. */
36
- scrollsWindowTo?: { x: number, y: number }
41
+ scrollsWindowTo?: { x: number; y: number };
37
42
  /** Error thrown after scrollIntoView has moved the page and its ancestors. */
38
- scrollError?: Error
43
+ scrollError?: Error;
39
44
  }
40
45
 
41
46
  function makeVideo(options: FakeVideoOptions = {}) {
42
- let currentTime = options.currentTime ?? 0
43
- let readyAt = options.readyAfterMs === undefined || options.readyAfterLoad
44
- ? undefined
45
- : Date.now() + options.readyAfterMs
46
- const initialReadyState = options.readyState ?? 4
47
- const listeners: Record<string, Array<() => void>> = {}
47
+ let currentTime = options.currentTime ?? 0;
48
+ let readyAt =
49
+ options.readyAfterMs === undefined || options.readyAfterLoad
50
+ ? undefined
51
+ : Date.now() + options.readyAfterMs;
52
+ const initialReadyState = options.readyState ?? 4;
53
+ const listeners: Record<string, Array<() => void>> = {};
48
54
 
49
55
  const video: any = {
50
- src: options.src ?? '',
51
- preload: options.preload ?? 'auto',
56
+ src: options.src ?? "",
57
+ preload: options.preload ?? "auto",
52
58
  autoplay: options.autoplay ?? false,
53
59
  paused: options.paused ?? true,
54
60
  parentElement: options.scrollParent ?? null,
@@ -59,78 +65,82 @@ function makeVideo(options: FakeVideoOptions = {}) {
59
65
  seeks: [] as number[],
60
66
  getBoundingClientRect: () => options.rect ?? { width: 640, height: 480 },
61
67
  querySelector: (selector: string) =>
62
- selector === 'source' && options.sourceSrc
68
+ selector === "source" && options.sourceSrc
63
69
  ? { getAttribute: () => options.sourceSrc }
64
70
  : null,
65
71
  events: [] as string[],
66
72
  addEventListener: (type: string, listener: () => void) => {
67
- listeners[type] = [...(listeners[type] ?? []), listener]
73
+ listeners[type] = [...(listeners[type] ?? []), listener];
68
74
  },
69
75
  removeEventListener: (type: string, listener: () => void) => {
70
- listeners[type] = (listeners[type] ?? []).filter(l => l !== listener)
76
+ listeners[type] = (listeners[type] ?? []).filter((l) => l !== listener);
71
77
  },
72
78
  scrollIntoView: () => {
73
- video.scrolledIntoView++
74
- video.events.push('scroll')
79
+ video.scrolledIntoView++;
80
+ video.events.push("scroll");
75
81
  // Bringing an element on screen scrolls every scrollable ancestor, not
76
82
  // just the window.
77
83
  if (options.scrollParent) {
78
- options.scrollParent.scrollTop = 900
79
- options.scrollParent.scrollLeft = 40
84
+ options.scrollParent.scrollTop = 900;
85
+ options.scrollParent.scrollLeft = 40;
80
86
  }
81
87
  if (options.scrollsWindowTo) {
82
- const win = (globalThis as any).window
83
- win.scrollX = options.scrollsWindowTo.x
84
- win.scrollY = options.scrollsWindowTo.y
88
+ const win = (globalThis as any).window;
89
+ win.scrollX = options.scrollsWindowTo.x;
90
+ win.scrollY = options.scrollsWindowTo.y;
85
91
  }
86
- if (options.scrollError) throw options.scrollError
92
+ if (options.scrollError) throw options.scrollError;
87
93
  // Chromium starts a muted autoplay video once it is on screen, which is
88
94
  // why the attribute has to be cleared before this happens and not after.
89
- if (video.autoplay) video.paused = false
95
+ if (video.autoplay) video.paused = false;
90
96
  },
91
97
  pause: () => {
92
- video.pauseCalls++
93
- video.events.push('pause')
94
- video.paused = true
98
+ video.pauseCalls++;
99
+ video.events.push("pause");
100
+ video.paused = true;
95
101
  },
96
102
  play: () => {
97
- video.playCalls++
98
- video.events.push('play')
99
- video.paused = false
100
- return Promise.resolve()
103
+ video.playCalls++;
104
+ video.events.push("play");
105
+ video.paused = false;
106
+ return Promise.resolve();
101
107
  },
102
108
  load: () => {
103
- video.loadCalls++
104
- video.events.push('load')
109
+ video.loadCalls++;
110
+ video.events.push("load");
105
111
  if (options.readyAfterLoad) {
106
- readyAt = Date.now() + (options.readyAfterMs ?? 0)
112
+ readyAt = Date.now() + (options.readyAfterMs ?? 0);
107
113
  }
108
114
  },
109
- }
115
+ };
110
116
 
111
- Object.defineProperty(video, 'readyState', {
112
- get: () => (readyAt !== undefined && Date.now() >= readyAt ? 4 : initialReadyState),
113
- })
117
+ Object.defineProperty(video, "readyState", {
118
+ get: () =>
119
+ readyAt !== undefined && Date.now() >= readyAt ? 4 : initialReadyState,
120
+ });
114
121
 
115
- Object.defineProperty(video, 'currentSrc', {
116
- get: () => (video.readyState >= 2 ? video.src : ''),
117
- })
122
+ Object.defineProperty(video, "currentSrc", {
123
+ get: () => (video.readyState >= 2 ? video.src : ""),
124
+ });
118
125
 
119
- Object.defineProperty(video, 'currentTime', {
126
+ Object.defineProperty(video, "currentTime", {
120
127
  get: () => currentTime,
121
128
  set: (value: number) => {
122
- if (options.seekable === false) throw new Error('not seekable')
123
- video.seeks.push(value)
124
- currentTime = value
129
+ if (options.seekable === false) throw new Error("not seekable");
130
+ video.seeks.push(value);
131
+ currentTime = value;
125
132
  // A seek is asynchronous: the browser decodes the frame at the new
126
133
  // position and only then fires `seeked`.
127
134
  if (!options.neverSeeked) {
128
- setTimeout(() => (listeners.seeked ?? []).forEach(listener => listener()), 0)
135
+ setTimeout(
136
+ () => (listeners.seeked ?? []).forEach((listener) => listener()),
137
+ 0,
138
+ );
129
139
  }
130
140
  },
131
- })
141
+ });
132
142
 
133
- return video
143
+ return video;
134
144
  }
135
145
 
136
146
  /**
@@ -145,7 +155,7 @@ function makeScrollBox() {
145
155
  scrollWidth: 800,
146
156
  clientWidth: 400,
147
157
  parentElement: null,
148
- }
158
+ };
149
159
  }
150
160
 
151
161
  /**
@@ -154,484 +164,634 @@ function makeScrollBox() {
154
164
  * @param options.lockScroll Accept window.scroll() calls but ignore them, as
155
165
  * happens when application UI has locked the body.
156
166
  */
157
- function installDom(videos: any[], scroll = { x: 0, y: 0 }, options: { lockScroll?: boolean } = {}) {
158
- const scrolls: Array<{ top: number, left: number }> = []
167
+ function installDom(
168
+ videos: any[],
169
+ scroll = { x: 0, y: 0 },
170
+ options: { lockScroll?: boolean } = {},
171
+ ) {
172
+ const scrolls: Array<{ top: number; left: number }> = [];
159
173
  const win: any = {
160
174
  scrollX: scroll.x,
161
175
  scrollY: scroll.y,
162
- scroll: (opts: { top: number, left: number }) => {
163
- scrolls.push(opts)
164
- if (options.lockScroll) return
165
- win.scrollX = opts.left
166
- win.scrollY = opts.top
176
+ scroll: (opts: { top: number; left: number }) => {
177
+ scrolls.push(opts);
178
+ if (options.lockScroll) return;
179
+ win.scrollX = opts.left;
180
+ win.scrollY = opts.top;
167
181
  },
168
- }
169
- vi.stubGlobal('document', {
170
- querySelectorAll: (selector: string) => (selector === 'video' ? videos : []),
171
- })
172
- vi.stubGlobal('getComputedStyle', (el: any) => ({
173
- visibility: el.styleVisibility ?? 'visible',
174
- display: el.styleDisplay ?? 'block',
175
- }))
176
- vi.stubGlobal('requestAnimationFrame', (cb: () => void) => {
177
- setTimeout(cb, 0)
178
- return 0
179
- })
180
- vi.stubGlobal('location', { href: 'https://example.com/page' })
181
- vi.stubGlobal('window', win)
182
- return scrolls
182
+ };
183
+ vi.stubGlobal("document", {
184
+ querySelectorAll: (selector: string) =>
185
+ selector === "video" ? videos : [],
186
+ });
187
+ vi.stubGlobal("getComputedStyle", (el: any) => ({
188
+ visibility: el.styleVisibility ?? "visible",
189
+ display: el.styleDisplay ?? "block",
190
+ }));
191
+ vi.stubGlobal("requestAnimationFrame", (cb: () => void) => {
192
+ setTimeout(cb, 0);
193
+ return 0;
194
+ });
195
+ vi.stubGlobal("location", { href: "https://example.com/page" });
196
+ vi.stubGlobal("window", win);
197
+ return scrolls;
183
198
  }
184
199
 
185
- describe('settleVideos', () => {
200
+ describe("settleVideos", () => {
186
201
  afterEach(() => {
187
- vi.unstubAllGlobals()
188
- })
189
-
190
- it('reports nothing when there are no videos', async () => {
191
- installDom([])
192
- expect(await settleVideos({ timeoutMs: 100 })).toEqual({ notReady: [], scrolled: false })
193
- })
194
-
195
- it('composites, pauses and rewinds a playing autoplay video', async () => {
196
- const video = makeVideo({ src: 'https://example.com/v.mp4', autoplay: true, paused: false, currentTime: 3.5 })
197
- installDom([video])
202
+ vi.unstubAllGlobals();
203
+ });
204
+
205
+ it("reports nothing when there are no videos", async () => {
206
+ installDom([]);
207
+ expect(await settleVideos({ timeoutMs: 100 })).toEqual({
208
+ notReady: [],
209
+ scrolled: false,
210
+ });
211
+ });
212
+
213
+ it("composites, pauses and rewinds a playing autoplay video", async () => {
214
+ const video = makeVideo({
215
+ src: "https://example.com/v.mp4",
216
+ autoplay: true,
217
+ paused: false,
218
+ currentTime: 3.5,
219
+ });
220
+ installDom([video]);
198
221
 
199
- expect(await settleVideos({ timeoutMs: 100 })).toEqual({ notReady: [], scrolled: true })
222
+ expect(await settleVideos({ timeoutMs: 100 })).toEqual({
223
+ notReady: [],
224
+ scrolled: true,
225
+ });
200
226
 
201
227
  // Scrolled into view once, so Chromium has a reason to composite the frame.
202
- expect(video.scrolledIntoView).toBe(1)
228
+ expect(video.scrolledIntoView).toBe(1);
203
229
  // Left paused at the first frame, so the capture is the same every run.
204
- expect(video.paused).toBe(true)
205
- expect(video.autoplay).toBe(false)
206
- expect(video.currentTime).toBe(0)
207
- expect(video.seeks).toEqual([0])
208
- })
230
+ expect(video.paused).toBe(true);
231
+ expect(video.autoplay).toBe(false);
232
+ expect(video.currentTime).toBe(0);
233
+ expect(video.seeks).toEqual([0]);
234
+ });
209
235
 
210
- it('pins playback before scrolling, so the scroll cannot restart it', async () => {
211
- const video = makeVideo({ src: 'https://example.com/v.mp4', autoplay: true, paused: false })
212
- installDom([video])
236
+ it("pins playback before scrolling, so the scroll cannot restart it", async () => {
237
+ const video = makeVideo({
238
+ src: "https://example.com/v.mp4",
239
+ autoplay: true,
240
+ paused: false,
241
+ });
242
+ installDom([video]);
213
243
 
214
- await settleVideos({ timeoutMs: 100 })
244
+ await settleVideos({ timeoutMs: 100 });
215
245
 
216
246
  // If the scroll came first, Chromium would restart the muted autoplay video
217
247
  // and the frame captured would depend on how long the rest of the waits took.
218
- expect(video.events[0]).toBe('pause')
219
- expect(video.events.indexOf('scroll')).toBeGreaterThan(0)
220
- expect(video.paused).toBe(true)
221
- })
248
+ expect(video.events[0]).toBe("pause");
249
+ expect(video.events.indexOf("scroll")).toBeGreaterThan(0);
250
+ expect(video.paused).toBe(true);
251
+ });
222
252
 
223
- it('does not seek a video already at its first frame', async () => {
224
- const video = makeVideo({ src: 'https://example.com/v.mp4', currentTime: 0 })
225
- installDom([video])
226
-
227
- await settleVideos({ timeoutMs: 100 })
253
+ it("does not seek a video already at its first frame", async () => {
254
+ const video = makeVideo({
255
+ src: "https://example.com/v.mp4",
256
+ currentTime: 0,
257
+ });
258
+ installDom([video]);
228
259
 
229
- expect(video.seeks).toEqual([])
230
- expect(video.paused).toBe(true)
231
- })
260
+ await settleVideos({ timeoutMs: 100 });
232
261
 
233
- it('still settles a video whose source cannot be seeked', async () => {
234
- const video = makeVideo({ src: 'https://example.com/live.m3u8', paused: false, currentTime: 12, seekable: false })
235
- installDom([video])
262
+ expect(video.seeks).toEqual([]);
263
+ expect(video.paused).toBe(true);
264
+ });
236
265
 
237
- expect(await settleVideos({ timeoutMs: 100 })).toEqual({ notReady: [], scrolled: true })
238
- expect(video.paused).toBe(true)
239
- })
266
+ it("still settles a video whose source cannot be seeked", async () => {
267
+ const video = makeVideo({
268
+ src: "https://example.com/live.m3u8",
269
+ paused: false,
270
+ currentTime: 12,
271
+ seekable: false,
272
+ });
273
+ installDom([video]);
240
274
 
241
- it('does not hang when a seek never completes', async () => {
242
- const video = makeVideo({ src: 'https://example.com/v.mp4', currentTime: 7, neverSeeked: true })
243
- installDom([video])
275
+ expect(await settleVideos({ timeoutMs: 100 })).toEqual({
276
+ notReady: [],
277
+ scrolled: true,
278
+ });
279
+ expect(video.paused).toBe(true);
280
+ });
244
281
 
245
- expect(await settleVideos({ timeoutMs: 100, paintTimeoutMs: 20 })).toEqual({ notReady: [], scrolled: true })
246
- expect(video.seeks).toEqual([0])
247
- })
282
+ it("does not hang when a seek never completes", async () => {
283
+ const video = makeVideo({
284
+ src: "https://example.com/v.mp4",
285
+ currentTime: 7,
286
+ neverSeeked: true,
287
+ });
288
+ installDom([video]);
289
+
290
+ expect(await settleVideos({ timeoutMs: 100, paintTimeoutMs: 20 })).toEqual({
291
+ notReady: [],
292
+ scrolled: true,
293
+ });
294
+ expect(video.seeks).toEqual([0]);
295
+ });
248
296
 
249
- it('does not hang when the page is never painted', async () => {
250
- const video = makeVideo({ src: 'https://example.com/v.mp4' })
251
- installDom([video])
297
+ it("does not hang when the page is never painted", async () => {
298
+ const video = makeVideo({ src: "https://example.com/v.mp4" });
299
+ installDom([video]);
252
300
  // A page Chromium is not rendering never runs an animation frame callback.
253
- vi.stubGlobal('requestAnimationFrame', () => 0)
301
+ vi.stubGlobal("requestAnimationFrame", () => 0);
254
302
 
255
- expect(await settleVideos({ timeoutMs: 100, paintTimeoutMs: 20 })).toEqual({ notReady: [], scrolled: true })
256
- })
303
+ expect(await settleVideos({ timeoutMs: 100, paintTimeoutMs: 20 })).toEqual({
304
+ notReady: [],
305
+ scrolled: true,
306
+ });
307
+ });
257
308
 
258
- it('waits for a slow video to produce a frame', async () => {
259
- const video = makeVideo({ src: 'https://example.com/slow.mp4', readyState: 0, readyAfterMs: 120 })
260
- installDom([video])
309
+ it("waits for a slow video to produce a frame", async () => {
310
+ const video = makeVideo({
311
+ src: "https://example.com/slow.mp4",
312
+ readyState: 0,
313
+ readyAfterMs: 120,
314
+ });
315
+ installDom([video]);
261
316
 
262
- expect(await settleVideos({ timeoutMs: 2000, pollMs: 10 })).toEqual({ notReady: [], scrolled: true })
263
- expect(video.readyState).toBe(4)
264
- })
317
+ expect(await settleVideos({ timeoutMs: 2000, pollMs: 10 })).toEqual({
318
+ notReady: [],
319
+ scrolled: true,
320
+ });
321
+ expect(video.readyState).toBe(4);
322
+ });
265
323
 
266
- it('gives every video its own timeout instead of a shared budget', async () => {
267
- const slow = makeVideo({ src: 'https://example.com/never.mp4', readyState: 0 })
268
- const ready = makeVideo({ src: 'https://example.com/ready.mp4', readyState: 0, readyAfterMs: 30 })
269
- installDom([slow, ready])
324
+ it("gives every video its own timeout instead of a shared budget", async () => {
325
+ const slow = makeVideo({
326
+ src: "https://example.com/never.mp4",
327
+ readyState: 0,
328
+ });
329
+ const ready = makeVideo({
330
+ src: "https://example.com/ready.mp4",
331
+ readyState: 0,
332
+ readyAfterMs: 30,
333
+ });
334
+ installDom([slow, ready]);
270
335
 
271
336
  // The first video spends its whole timeout without ever becoming ready. The
272
337
  // second must still be waited for rather than reported on a budget the
273
338
  // first one already emptied.
274
- const result = await settleVideos({ timeoutMs: 80, pollMs: 10 })
339
+ const result = await settleVideos({ timeoutMs: 80, pollMs: 10 });
275
340
 
276
- expect(result.notReady).toEqual(['https://example.com/never.mp4'])
277
- expect(ready.readyState).toBe(4)
278
- })
341
+ expect(result.notReady).toEqual(["https://example.com/never.mp4"]);
342
+ expect(ready.readyState).toBe(4);
343
+ });
279
344
 
280
- it('loads a lazy video instead of waiting for one that was never asked for', async () => {
345
+ it("loads a lazy video instead of waiting for one that was never asked for", async () => {
281
346
  const video = makeVideo({
282
- src: 'https://example.com/lazy.mp4',
283
- preload: 'none',
347
+ src: "https://example.com/lazy.mp4",
348
+ preload: "none",
284
349
  readyState: 0,
285
350
  readyAfterLoad: true,
286
351
  readyAfterMs: 30,
287
- })
288
- installDom([video])
352
+ });
353
+ installDom([video]);
289
354
 
290
- expect(await settleVideos({ timeoutMs: 2000, pollMs: 10 })).toEqual({ notReady: [], scrolled: true })
355
+ expect(await settleVideos({ timeoutMs: 2000, pollMs: 10 })).toEqual({
356
+ notReady: [],
357
+ scrolled: true,
358
+ });
291
359
  // Asked for on screen first, then told to load: polling a preload="none"
292
360
  // video without either would just burn the whole timeout.
293
- expect(video.events.slice(0, 2)).toEqual(['scroll', 'load'])
294
- expect(video.preload).toBe('auto')
295
- expect(video.loadCalls).toBe(1)
296
- })
361
+ expect(video.events.slice(0, 2)).toEqual(["scroll", "load"]);
362
+ expect(video.preload).toBe("auto");
363
+ expect(video.loadCalls).toBe(1);
364
+ });
297
365
 
298
- it('reports a video that never produces a frame instead of hanging', async () => {
299
- const video = makeVideo({ src: 'https://example.com/missing.mp4', readyState: 0 })
300
- installDom([video])
366
+ it("reports a video that never produces a frame instead of hanging", async () => {
367
+ const video = makeVideo({
368
+ src: "https://example.com/missing.mp4",
369
+ readyState: 0,
370
+ });
371
+ installDom([video]);
301
372
 
302
373
  expect(await settleVideos({ timeoutMs: 60, pollMs: 10 })).toEqual({
303
- notReady: ['https://example.com/missing.mp4'],
374
+ notReady: ["https://example.com/missing.mp4"],
304
375
  scrolled: true,
305
- })
376
+ });
306
377
  // Reported, but still pinned down: it must not start playing if it becomes
307
378
  // ready between here and the capture.
308
- expect(video.autoplay).toBe(false)
309
- })
310
-
311
- it('falls back to a nested <source> when reporting an unready video', async () => {
312
- const video = makeVideo({ readyState: 0, sourceSrc: '/files/missing.mp4' })
313
- installDom([video])
314
-
315
- expect((await settleVideos({ timeoutMs: 60, pollMs: 10 })).notReady)
316
- .toEqual(['https://example.com/files/missing.mp4'])
317
- })
318
-
319
- it('skips videos that are not visible', async () => {
320
- const zeroSized = makeVideo({ src: 'https://example.com/a.mp4', rect: { width: 0, height: 0 } })
321
- const hidden = makeVideo({ src: 'https://example.com/b.mp4' })
322
- hidden.styleVisibility = 'hidden'
323
- const notDisplayed = makeVideo({ src: 'https://example.com/c.mp4' })
324
- notDisplayed.styleDisplay = 'none'
325
- installDom([zeroSized, hidden, notDisplayed])
326
-
327
- expect(await settleVideos({ timeoutMs: 100 })).toEqual({ notReady: [], scrolled: false })
328
- expect(zeroSized.scrolledIntoView).toBe(0)
329
- expect(hidden.scrolledIntoView).toBe(0)
330
- expect(notDisplayed.scrolledIntoView).toBe(0)
331
- })
332
-
333
- it('restores the scroll position it found', async () => {
334
- const video = makeVideo({ src: 'https://example.com/v.mp4' })
335
- const scrolls = installDom([video], { x: 12, y: 3400 })
336
-
337
- await settleVideos({ timeoutMs: 100 })
338
-
339
- expect(scrolls).toEqual([{ top: 3400, left: 12, behavior: 'instant' }])
340
- })
341
-
342
- it('restores the scroll of every container the video was scrolled inside', async () => {
343
- const box = makeScrollBox()
344
- const video = makeVideo({ src: 'https://example.com/v.mp4', scrollParent: box })
345
- installDom([video], { x: 0, y: 200 })
379
+ expect(video.autoplay).toBe(false);
380
+ });
381
+
382
+ it("falls back to a nested <source> when reporting an unready video", async () => {
383
+ const video = makeVideo({ readyState: 0, sourceSrc: "/files/missing.mp4" });
384
+ installDom([video]);
385
+
386
+ expect(
387
+ (await settleVideos({ timeoutMs: 60, pollMs: 10 })).notReady,
388
+ ).toEqual(["https://example.com/files/missing.mp4"]);
389
+ });
390
+
391
+ it("skips videos that are not visible", async () => {
392
+ const zeroSized = makeVideo({
393
+ src: "https://example.com/a.mp4",
394
+ rect: { width: 0, height: 0 },
395
+ });
396
+ const hidden = makeVideo({ src: "https://example.com/b.mp4" });
397
+ hidden.styleVisibility = "hidden";
398
+ const notDisplayed = makeVideo({ src: "https://example.com/c.mp4" });
399
+ notDisplayed.styleDisplay = "none";
400
+ installDom([zeroSized, hidden, notDisplayed]);
401
+
402
+ expect(await settleVideos({ timeoutMs: 100 })).toEqual({
403
+ notReady: [],
404
+ scrolled: false,
405
+ });
406
+ expect(zeroSized.scrolledIntoView).toBe(0);
407
+ expect(hidden.scrolledIntoView).toBe(0);
408
+ expect(notDisplayed.scrolledIntoView).toBe(0);
409
+ });
410
+
411
+ it("restores the scroll position it found", async () => {
412
+ const video = makeVideo({ src: "https://example.com/v.mp4" });
413
+ const scrolls = installDom([video], { x: 12, y: 3400 });
414
+
415
+ await settleVideos({ timeoutMs: 100 });
416
+
417
+ expect(scrolls).toEqual([{ top: 3400, left: 12, behavior: "instant" }]);
418
+ });
419
+
420
+ it("restores the scroll of every container the video was scrolled inside", async () => {
421
+ const box = makeScrollBox();
422
+ const video = makeVideo({
423
+ src: "https://example.com/v.mp4",
424
+ scrollParent: box,
425
+ });
426
+ installDom([video], { x: 0, y: 200 });
346
427
 
347
- await settleVideos({ timeoutMs: 100 })
428
+ await settleVideos({ timeoutMs: 100 });
348
429
 
349
430
  // A video in a modal body or a carousel track must not leave that region
350
431
  // showing something the baseline never had.
351
- expect(box.scrollTop).toBe(0)
352
- expect(box.scrollLeft).toBe(0)
353
- })
432
+ expect(box.scrollTop).toBe(0);
433
+ expect(box.scrollLeft).toBe(0);
434
+ });
354
435
 
355
- it('restores window and container scroll when settling throws', async () => {
356
- const box = makeScrollBox()
357
- const failure = new Error('scroll failed')
436
+ it("restores window and container scroll when settling throws", async () => {
437
+ const box = makeScrollBox();
438
+ const failure = new Error("scroll failed");
358
439
  const video = makeVideo({
359
- src: 'https://example.com/v.mp4',
440
+ src: "https://example.com/v.mp4",
360
441
  scrollParent: box,
361
- scrollsWindowTo: {x: 0, y: 800},
442
+ scrollsWindowTo: { x: 0, y: 800 },
362
443
  scrollError: failure,
363
- })
364
- const scrolls = installDom([video], {x: 15, y: 200})
444
+ });
445
+ const scrolls = installDom([video], { x: 15, y: 200 });
365
446
 
366
- await expect(settleVideos({timeoutMs: 100})).rejects.toBe(failure)
447
+ await expect(settleVideos({ timeoutMs: 100 })).rejects.toBe(failure);
367
448
 
368
- expect(box.scrollTop).toBe(0)
369
- expect(box.scrollLeft).toBe(0)
370
- expect(scrolls).toEqual([{top: 200, left: 15, behavior: 'instant'}])
371
- })
449
+ expect(box.scrollTop).toBe(0);
450
+ expect(box.scrollLeft).toBe(0);
451
+ expect(scrolls).toEqual([{ top: 200, left: 15, behavior: "instant" }]);
452
+ });
372
453
 
373
- it('asks again when the scroll back is dropped', async () => {
374
- const video = makeVideo({ src: 'https://example.com/v.mp4', scrollsWindowTo: { x: 0, y: 4200 } })
454
+ it("asks again when the scroll back is dropped", async () => {
455
+ const video = makeVideo({
456
+ src: "https://example.com/v.mp4",
457
+ scrollsWindowTo: { x: 0, y: 4200 },
458
+ });
375
459
  // window.scroll is async and does nothing at all when application UI has
376
460
  // locked the body.
377
- const scrolls = installDom([video], { x: 0, y: 900 }, { lockScroll: true })
461
+ const scrolls = installDom([video], { x: 0, y: 900 }, { lockScroll: true });
378
462
 
379
- await settleVideos({ timeoutMs: 100 })
463
+ await settleVideos({ timeoutMs: 100 });
380
464
 
381
465
  expect(scrolls).toEqual([
382
- { top: 900, left: 0, behavior: 'instant' },
383
- { top: 900, left: 0, behavior: 'instant' },
384
- ])
385
- })
386
-
387
- it('does not scroll at all when the page has no visible videos', async () => {
388
- const hidden = makeVideo({ src: 'https://example.com/b.mp4', rect: { width: 0, height: 0 } })
389
- const scrolls = installDom([hidden], { x: 0, y: 900 })
390
-
391
- await settleVideos({ timeoutMs: 100 })
392
-
393
- expect(scrolls).toEqual([])
394
- })
395
-
396
- it('settles every video on the page', async () => {
397
- const first = makeVideo({ src: 'https://example.com/1.mp4', autoplay: true, paused: false, currentTime: 2 })
398
- const second = makeVideo({ src: 'https://example.com/2.mp4', autoplay: true, paused: false, currentTime: 5 })
399
- installDom([first, second])
466
+ { top: 900, left: 0, behavior: "instant" },
467
+ { top: 900, left: 0, behavior: "instant" },
468
+ ]);
469
+ });
470
+
471
+ it("does not scroll at all when the page has no visible videos", async () => {
472
+ const hidden = makeVideo({
473
+ src: "https://example.com/b.mp4",
474
+ rect: { width: 0, height: 0 },
475
+ });
476
+ const scrolls = installDom([hidden], { x: 0, y: 900 });
477
+
478
+ await settleVideos({ timeoutMs: 100 });
479
+
480
+ expect(scrolls).toEqual([]);
481
+ });
482
+
483
+ it("settles every video on the page", async () => {
484
+ const first = makeVideo({
485
+ src: "https://example.com/1.mp4",
486
+ autoplay: true,
487
+ paused: false,
488
+ currentTime: 2,
489
+ });
490
+ const second = makeVideo({
491
+ src: "https://example.com/2.mp4",
492
+ autoplay: true,
493
+ paused: false,
494
+ currentTime: 5,
495
+ });
496
+ installDom([first, second]);
400
497
 
401
- expect(await settleVideos({ timeoutMs: 100 })).toEqual({ notReady: [], scrolled: true })
498
+ expect(await settleVideos({ timeoutMs: 100 })).toEqual({
499
+ notReady: [],
500
+ scrolled: true,
501
+ });
402
502
  for (const video of [first, second]) {
403
- expect(video.scrolledIntoView).toBe(1)
404
- expect(video.paused).toBe(true)
405
- expect(video.currentTime).toBe(0)
503
+ expect(video.scrolledIntoView).toBe(1);
504
+ expect(video.paused).toBe(true);
505
+ expect(video.currentTime).toBe(0);
406
506
  }
407
- })
507
+ });
408
508
 
409
- it('records the playback state it found so it can be restored', async () => {
410
- const video = makeVideo({ src: 'https://example.com/v.mp4', autoplay: true, paused: false, currentTime: 3.5 })
411
- installDom([video])
509
+ it("records the playback state it found so it can be restored", async () => {
510
+ const video = makeVideo({
511
+ src: "https://example.com/v.mp4",
512
+ autoplay: true,
513
+ paused: false,
514
+ currentTime: 3.5,
515
+ });
516
+ installDom([video]);
412
517
 
413
- await settleVideos({ timeoutMs: 100 })
518
+ await settleVideos({ timeoutMs: 100 });
414
519
 
415
520
  expect(video.__playwrightTestingVideoState).toEqual({
416
521
  autoplay: true,
417
522
  paused: false,
418
523
  currentTime: 3.5,
419
- })
420
- })
524
+ });
525
+ });
421
526
 
422
- it('keeps the first recorded state when it settles the same video twice', async () => {
423
- const video = makeVideo({ src: 'https://example.com/v.mp4', autoplay: true, paused: false, currentTime: 3.5 })
424
- installDom([video])
527
+ it("keeps the first recorded state when it settles the same video twice", async () => {
528
+ const video = makeVideo({
529
+ src: "https://example.com/v.mp4",
530
+ autoplay: true,
531
+ paused: false,
532
+ currentTime: 3.5,
533
+ });
534
+ installDom([video]);
425
535
 
426
- await settleVideos({ timeoutMs: 100 })
427
- await settleVideos({ timeoutMs: 100 })
536
+ await settleVideos({ timeoutMs: 100 });
537
+ await settleVideos({ timeoutMs: 100 });
428
538
 
429
539
  // Otherwise a second capture would record the pinned state as the original
430
540
  // one and there would be nothing left to restore.
431
- expect(video.__playwrightTestingVideoState.paused).toBe(false)
432
- expect(video.__playwrightTestingVideoState.currentTime).toBe(3.5)
433
- })
434
- })
541
+ expect(video.__playwrightTestingVideoState.paused).toBe(false);
542
+ expect(video.__playwrightTestingVideoState.currentTime).toBe(3.5);
543
+ });
544
+ });
435
545
 
436
- describe('restorePlayback', () => {
546
+ describe("restorePlayback", () => {
437
547
  afterEach(() => {
438
- vi.unstubAllGlobals()
439
- })
548
+ vi.unstubAllGlobals();
549
+ });
440
550
 
441
- it('puts back the playback state settleVideos() found', async () => {
442
- const video = makeVideo({ src: 'https://example.com/v.mp4', autoplay: true, paused: false, currentTime: 3.5 })
443
- installDom([video])
551
+ it("puts back the playback state settleVideos() found", async () => {
552
+ const video = makeVideo({
553
+ src: "https://example.com/v.mp4",
554
+ autoplay: true,
555
+ paused: false,
556
+ currentTime: 3.5,
557
+ });
558
+ installDom([video]);
444
559
 
445
- await settleVideos({ timeoutMs: 100 })
446
- restorePlayback()
560
+ await settleVideos({ timeoutMs: 100 });
561
+ restorePlayback();
447
562
 
448
- expect(video.autoplay).toBe(true)
449
- expect(video.paused).toBe(false)
450
- expect(video.currentTime).toBe(3.5)
451
- expect(video.playCalls).toBe(1)
452
- })
563
+ expect(video.autoplay).toBe(true);
564
+ expect(video.paused).toBe(false);
565
+ expect(video.currentTime).toBe(3.5);
566
+ expect(video.playCalls).toBe(1);
567
+ });
453
568
 
454
- it('leaves a video that was already paused where it was', async () => {
455
- const video = makeVideo({ src: 'https://example.com/v.mp4', currentTime: 0 })
456
- installDom([video])
569
+ it("leaves a video that was already paused where it was", async () => {
570
+ const video = makeVideo({
571
+ src: "https://example.com/v.mp4",
572
+ currentTime: 0,
573
+ });
574
+ installDom([video]);
457
575
 
458
- await settleVideos({ timeoutMs: 100 })
459
- restorePlayback()
576
+ await settleVideos({ timeoutMs: 100 });
577
+ restorePlayback();
460
578
 
461
- expect(video.paused).toBe(true)
462
- expect(video.playCalls).toBe(0)
463
- })
579
+ expect(video.paused).toBe(true);
580
+ expect(video.playCalls).toBe(0);
581
+ });
464
582
 
465
- it('ignores videos it never settled', () => {
466
- const video = makeVideo({ src: 'https://example.com/v.mp4', autoplay: true, paused: false })
467
- installDom([video])
583
+ it("ignores videos it never settled", () => {
584
+ const video = makeVideo({
585
+ src: "https://example.com/v.mp4",
586
+ autoplay: true,
587
+ paused: false,
588
+ });
589
+ installDom([video]);
468
590
 
469
- restorePlayback()
591
+ restorePlayback();
470
592
 
471
- expect(video.playCalls).toBe(0)
472
- expect(video.pauseCalls).toBe(0)
473
- })
593
+ expect(video.playCalls).toBe(0);
594
+ expect(video.pauseCalls).toBe(0);
595
+ });
474
596
 
475
- it('does not undo what the test did after the first restore', async () => {
476
- const video = makeVideo({ src: 'https://example.com/v.mp4', autoplay: true, paused: false, currentTime: 3.5 })
477
- installDom([video])
597
+ it("does not undo what the test did after the first restore", async () => {
598
+ const video = makeVideo({
599
+ src: "https://example.com/v.mp4",
600
+ autoplay: true,
601
+ paused: false,
602
+ currentTime: 3.5,
603
+ });
604
+ installDom([video]);
478
605
 
479
- await settleVideos({ timeoutMs: 100 })
480
- restorePlayback()
481
- video.pause()
482
- restorePlayback()
606
+ await settleVideos({ timeoutMs: 100 });
607
+ restorePlayback();
608
+ video.pause();
609
+ restorePlayback();
483
610
 
484
- expect(video.paused).toBe(true)
485
- expect(video.playCalls).toBe(1)
486
- })
611
+ expect(video.paused).toBe(true);
612
+ expect(video.playCalls).toBe(1);
613
+ });
487
614
 
488
- it('survives a video whose source cannot be seeked', async () => {
489
- const video = makeVideo({ src: 'https://example.com/live.m3u8', paused: false, currentTime: 12, seekable: false })
490
- installDom([video])
615
+ it("survives a video whose source cannot be seeked", async () => {
616
+ const video = makeVideo({
617
+ src: "https://example.com/live.m3u8",
618
+ paused: false,
619
+ currentTime: 12,
620
+ seekable: false,
621
+ });
622
+ installDom([video]);
491
623
 
492
- await settleVideos({ timeoutMs: 100 })
493
- expect(() => restorePlayback()).not.toThrow()
494
- expect(video.paused).toBe(false)
495
- })
496
- })
624
+ await settleVideos({ timeoutMs: 100 });
625
+ expect(() => restorePlayback()).not.toThrow();
626
+ expect(video.paused).toBe(false);
627
+ });
628
+ });
497
629
 
498
- describe('waitForVideos', () => {
499
- let warn: any
630
+ describe("waitForVideos", () => {
631
+ let warn: any;
500
632
 
501
633
  beforeEach(() => {
502
- warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
503
- })
634
+ warn = vi.spyOn(console, "warn").mockImplementation(() => {});
635
+ });
504
636
 
505
637
  afterEach(() => {
506
- warn.mockRestore()
507
- })
638
+ warn.mockRestore();
639
+ });
508
640
 
509
641
  /**
510
642
  * A stand-in for the parts of Page waitForVideos() uses, with one result per
511
643
  * frame.
512
644
  */
513
- function makePage(results: Array<{notReady: string[], scrolled: boolean} | Error>) {
514
- const frames = results.map(result => ({
515
- evaluate: vi.fn().mockImplementation(() =>
516
- result instanceof Error ? Promise.reject(result) : Promise.resolve(result)
517
- ),
518
- }))
645
+ function makePage(
646
+ results: Array<{ notReady: string[]; scrolled: boolean } | Error>,
647
+ ) {
648
+ const frames = results.map((result) => ({
649
+ evaluate: vi
650
+ .fn()
651
+ .mockImplementation(() =>
652
+ result instanceof Error
653
+ ? Promise.reject(result)
654
+ : Promise.resolve(result),
655
+ ),
656
+ }));
519
657
  const page = {
520
658
  frames: () => frames,
521
- } as any
522
- return { page, frames }
659
+ } as any;
660
+ return { page, frames };
523
661
  }
524
662
 
525
- it('runs the settle in the page with the requested timeout', async () => {
526
- const { page, frames } = makePage([{ notReady: [], scrolled: true }])
663
+ it("runs the settle in the page with the requested timeout", async () => {
664
+ const { page, frames } = makePage([{ notReady: [], scrolled: true }]);
527
665
 
528
- expect(await waitForVideos(page, 5000)).toEqual([])
666
+ expect(await waitForVideos(page, 5000)).toEqual([]);
529
667
  // The browser-side function is handed to evaluate() by reference so
530
668
  // Playwright serializes it: it must not be wrapped in a closure over
531
669
  // anything in this module.
532
- expect(frames[0].evaluate).toHaveBeenCalledWith(settleVideos, { timeoutMs: 5000 })
533
- expect(warn).not.toHaveBeenCalled()
534
- })
670
+ expect(frames[0].evaluate).toHaveBeenCalledWith(settleVideos, {
671
+ timeoutMs: 5000,
672
+ });
673
+ expect(warn).not.toHaveBeenCalled();
674
+ });
535
675
 
536
- it('defaults to a 5 second timeout', async () => {
537
- const { page, frames } = makePage([{ notReady: [], scrolled: false }])
676
+ it("defaults to a 5 second timeout", async () => {
677
+ const { page, frames } = makePage([{ notReady: [], scrolled: false }]);
538
678
 
539
- await waitForVideos(page)
679
+ await waitForVideos(page);
540
680
 
541
- expect(frames[0].evaluate).toHaveBeenCalledWith(settleVideos, { timeoutMs: 5000 })
542
- })
681
+ expect(frames[0].evaluate).toHaveBeenCalledWith(settleVideos, {
682
+ timeoutMs: 5000,
683
+ });
684
+ });
543
685
 
544
- it('settles videos in every frame, not just the main one', async () => {
686
+ it("settles videos in every frame, not just the main one", async () => {
545
687
  // Media embeds commonly render inside iframes; a video in one of those is no less able to
546
688
  // break a comparison.
547
689
  const { page, frames } = makePage([
548
690
  { notReady: [], scrolled: true },
549
- { notReady: ['https://example.com/embed.mp4'], scrolled: true },
550
- ])
551
-
552
- expect(await waitForVideos(page)).toEqual(['https://example.com/embed.mp4'])
553
- expect(frames[1].evaluate).toHaveBeenCalledWith(settleVideos, { timeoutMs: 5000 })
554
- })
555
-
556
- it('keeps going when a frame has gone away', async () => {
691
+ { notReady: ["https://example.com/embed.mp4"], scrolled: true },
692
+ ]);
693
+
694
+ expect(await waitForVideos(page)).toEqual([
695
+ "https://example.com/embed.mp4",
696
+ ]);
697
+ expect(frames[1].evaluate).toHaveBeenCalledWith(settleVideos, {
698
+ timeoutMs: 5000,
699
+ });
700
+ });
701
+
702
+ it("keeps going when a frame has gone away", async () => {
557
703
  const { page } = makePage([
558
- new Error('Execution context was destroyed'),
704
+ new Error("Execution context was destroyed"),
559
705
  { notReady: [], scrolled: true },
560
- ])
706
+ ]);
561
707
 
562
- expect(await waitForVideos(page)).toEqual([])
563
- })
708
+ expect(await waitForVideos(page)).toEqual([]);
709
+ });
564
710
 
565
- it('runs the adapter hook after a video moved the page', async () => {
566
- const { page } = makePage([{ notReady: [], scrolled: true }])
567
- const afterScroll = vi.fn().mockResolvedValue(undefined)
711
+ it("runs the adapter hook after a video moved the page", async () => {
712
+ const { page } = makePage([{ notReady: [], scrolled: true }]);
713
+ const afterScroll = vi.fn().mockResolvedValue(undefined);
568
714
 
569
- await waitForVideos(page, {afterScroll})
715
+ await waitForVideos(page, { afterScroll });
570
716
 
571
- expect(afterScroll).toHaveBeenCalledWith(page)
572
- })
717
+ expect(afterScroll).toHaveBeenCalledWith(page);
718
+ });
573
719
 
574
- it('does not run the adapter hook when nothing scrolled', async () => {
575
- const { page } = makePage([{ notReady: [], scrolled: false }])
576
- const afterScroll = vi.fn().mockResolvedValue(undefined)
720
+ it("does not run the adapter hook when nothing scrolled", async () => {
721
+ const { page } = makePage([{ notReady: [], scrolled: false }]);
722
+ const afterScroll = vi.fn().mockResolvedValue(undefined);
577
723
 
578
- await waitForVideos(page, {afterScroll})
724
+ await waitForVideos(page, { afterScroll });
579
725
 
580
- expect(afterScroll).not.toHaveBeenCalled()
581
- })
726
+ expect(afterScroll).not.toHaveBeenCalled();
727
+ });
582
728
 
583
- it('restores playback when the adapter hook fails', async () => {
584
- const {page, frames} = makePage([{notReady: [], scrolled: true}])
585
- const failure = new Error('adapter failed')
729
+ it("restores playback when the adapter hook fails", async () => {
730
+ const { page, frames } = makePage([{ notReady: [], scrolled: true }]);
731
+ const failure = new Error("adapter failed");
586
732
 
587
- await expect(waitForVideos(page, {
588
- afterScroll: vi.fn().mockRejectedValue(failure),
589
- })).rejects.toBe(failure)
733
+ await expect(
734
+ waitForVideos(page, {
735
+ afterScroll: vi.fn().mockRejectedValue(failure),
736
+ }),
737
+ ).rejects.toBe(failure);
590
738
 
591
- expect(frames[0].evaluate).toHaveBeenLastCalledWith(restorePlayback)
592
- })
739
+ expect(frames[0].evaluate).toHaveBeenLastCalledWith(restorePlayback);
740
+ });
593
741
 
594
- it('warns about, and returns, the videos that never became ready', async () => {
742
+ it("warns about, and returns, the videos that never became ready", async () => {
595
743
  const { page } = makePage([
596
- { notReady: ['https://example.com/a.mp4', 'https://example.com/b.mp4'], scrolled: true },
597
- ])
598
-
599
- const notReady = await waitForVideos(page, 1000)
600
-
601
- expect(notReady).toEqual(['https://example.com/a.mp4', 'https://example.com/b.mp4'])
602
- expect(warn).toHaveBeenCalledTimes(1)
603
- expect(warn.mock.calls[0][0]).toContain('2 video(s) had no frame available within 1000ms')
604
- expect(warn.mock.calls[0][0]).toContain('https://example.com/a.mp4, https://example.com/b.mp4')
605
- })
606
- })
607
-
608
- describe('restoreVideoPlayback', () => {
744
+ {
745
+ notReady: ["https://example.com/a.mp4", "https://example.com/b.mp4"],
746
+ scrolled: true,
747
+ },
748
+ ]);
749
+
750
+ const notReady = await waitForVideos(page, 1000);
751
+
752
+ expect(notReady).toEqual([
753
+ "https://example.com/a.mp4",
754
+ "https://example.com/b.mp4",
755
+ ]);
756
+ expect(warn).toHaveBeenCalledTimes(1);
757
+ expect(warn.mock.calls[0][0]).toContain(
758
+ "2 video(s) had no frame available within 1000ms",
759
+ );
760
+ expect(warn.mock.calls[0][0]).toContain(
761
+ "https://example.com/a.mp4, https://example.com/b.mp4",
762
+ );
763
+ });
764
+ });
765
+
766
+ describe("restoreVideoPlayback", () => {
609
767
  function makePage(frameCount: number, failing: number[] = []) {
610
768
  const frames = Array.from({ length: frameCount }, (_unused, index) => ({
611
- evaluate: vi.fn().mockImplementation(() =>
612
- failing.includes(index)
613
- ? Promise.reject(new Error('Execution context was destroyed'))
614
- : Promise.resolve(undefined)
615
- ),
616
- }))
617
- return { page: { frames: () => frames } as any, frames }
769
+ evaluate: vi
770
+ .fn()
771
+ .mockImplementation(() =>
772
+ failing.includes(index)
773
+ ? Promise.reject(new Error("Execution context was destroyed"))
774
+ : Promise.resolve(undefined),
775
+ ),
776
+ }));
777
+ return { page: { frames: () => frames } as any, frames };
618
778
  }
619
779
 
620
- it('restores playback in every frame', async () => {
621
- const { page, frames } = makePage(2)
780
+ it("restores playback in every frame", async () => {
781
+ const { page, frames } = makePage(2);
622
782
 
623
- await restoreVideoPlayback(page)
783
+ await restoreVideoPlayback(page);
624
784
 
625
785
  for (const frame of frames) {
626
786
  // By reference, for the same serialization reason as settleVideos().
627
- expect(frame.evaluate).toHaveBeenCalledWith(restorePlayback)
787
+ expect(frame.evaluate).toHaveBeenCalledWith(restorePlayback);
628
788
  }
629
- })
789
+ });
630
790
 
631
- it('keeps going when a frame has gone away', async () => {
632
- const { page, frames } = makePage(2, [0])
791
+ it("keeps going when a frame has gone away", async () => {
792
+ const { page, frames } = makePage(2, [0]);
633
793
 
634
- await expect(restoreVideoPlayback(page)).resolves.toBeUndefined()
635
- expect(frames[1].evaluate).toHaveBeenCalledWith(restorePlayback)
636
- })
637
- })
794
+ await expect(restoreVideoPlayback(page)).resolves.toBeUndefined();
795
+ expect(frames[1].evaluate).toHaveBeenCalledWith(restorePlayback);
796
+ });
797
+ });