@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.
- package/bin/github-a11y-summary +2 -2
- package/bin/github-failure-summary +4 -4
- package/lib/accessibility-baseline-file.d.ts +6 -6
- package/lib/accessibility-baseline-file.js +29 -23
- package/lib/accessibility-baseline.js +5 -3
- package/lib/accessible-screenshot.d.ts +4 -4
- package/lib/accessible-screenshot.js +88 -76
- package/lib/focus.js +4 -1
- package/lib/frames.js +4 -4
- package/lib/github/a11y-summary.js +74 -79
- package/lib/github/attachments.d.ts +1 -1
- package/lib/github/attachments.js +36 -34
- package/lib/github/failure-summary.d.ts +4 -4
- package/lib/github/failure-summary.js +139 -107
- package/lib/github/index.d.ts +6 -6
- package/lib/github/report-paths.js +17 -9
- package/lib/hover.js +17 -17
- package/lib/images.js +15 -12
- package/lib/index.d.ts +13 -13
- package/lib/interaction-states.d.ts +2 -2
- package/lib/interaction-states.js +7 -7
- package/lib/mock/index.d.ts +1 -1
- package/lib/mock/youtube.d.ts +1 -1
- package/lib/mock/youtube.js +1 -1
- package/lib/pseudo-state.d.ts +2 -2
- package/lib/pseudo-state.js +15 -7
- package/lib/videos.js +21 -10
- package/lib/visualdiff.d.ts +4 -4
- package/lib/visualdiff.js +35 -27
- package/package.json +3 -3
- package/src/accessibility-baseline-file.test.ts +190 -154
- package/src/accessibility-baseline-file.ts +107 -83
- package/src/accessibility-baseline.test.ts +633 -433
- package/src/accessibility-baseline.ts +41 -21
- package/src/accessible-screenshot.test.ts +551 -423
- package/src/accessible-screenshot.ts +334 -245
- package/src/focus.test.ts +23 -23
- package/src/focus.ts +7 -2
- package/src/fonts.test.ts +13 -13
- package/src/fonts.ts +1 -1
- package/src/frames.test.ts +10 -8
- package/src/frames.ts +5 -5
- package/src/github/a11y-summary.test.ts +436 -332
- package/src/github/a11y-summary.ts +189 -179
- package/src/github/attachments.test.ts +228 -167
- package/src/github/attachments.ts +144 -121
- package/src/github/failure-summary.test.ts +659 -445
- package/src/github/failure-summary.ts +376 -274
- package/src/github/index.test.ts +14 -14
- package/src/github/index.ts +6 -6
- package/src/github/report-paths.test.ts +211 -156
- package/src/github/report-paths.ts +75 -61
- package/src/hover.test.ts +50 -46
- package/src/hover.ts +18 -18
- package/src/images.test.ts +251 -206
- package/src/images.ts +52 -43
- package/src/index.ts +13 -13
- package/src/interaction-states.test.ts +72 -42
- package/src/interaction-states.ts +43 -29
- package/src/mock/index.ts +1 -1
- package/src/mock/youtube.test.ts +26 -26
- package/src/mock/youtube.ts +4 -6
- package/src/pseudo-state.test.ts +58 -51
- package/src/pseudo-state.ts +32 -27
- package/src/videos.test.ts +559 -399
- package/src/videos.ts +68 -44
- package/src/visualdiff.test.ts +416 -315
- package/src/visualdiff.ts +140 -99
package/src/images.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {Page} from "@playwright/test";
|
|
1
|
+
import { Page } from "@playwright/test";
|
|
2
2
|
|
|
3
3
|
export interface WaitForImagesOptions {
|
|
4
4
|
/** How long to wait for visible images to decode. */
|
|
@@ -36,13 +36,15 @@ export async function waitForImages(
|
|
|
36
36
|
for (const l of await locators.all()) {
|
|
37
37
|
// Ensure images are connected to the DOM before trying to scroll to them.
|
|
38
38
|
// https://github.com/microsoft/playwright/issues/23758
|
|
39
|
-
if (await l.evaluate(image => image.isConnected)) {
|
|
39
|
+
if (await l.evaluate((image) => image.isConnected)) {
|
|
40
40
|
await l.scrollIntoViewIfNeeded();
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// Make sure all images have loaded.
|
|
45
|
-
const promises = (await locators.all()).map(locator =>
|
|
45
|
+
const promises = (await locators.all()).map((locator) =>
|
|
46
|
+
locator.evaluate(settleImage),
|
|
47
|
+
);
|
|
46
48
|
await Promise.all(promises);
|
|
47
49
|
|
|
48
50
|
// The wait above treats an errored image as "loaded". Decode each visible
|
|
@@ -59,16 +61,16 @@ export async function waitForImages(
|
|
|
59
61
|
window.scroll({
|
|
60
62
|
top: 0,
|
|
61
63
|
left: 0,
|
|
62
|
-
behavior:
|
|
63
|
-
})
|
|
64
|
+
behavior: "instant",
|
|
65
|
+
}),
|
|
64
66
|
);
|
|
65
67
|
|
|
66
68
|
// window.scroll is async and doesn't return a promise, so wait until the
|
|
67
69
|
// browser confirms we are at the top again.
|
|
68
|
-
const scrollState = {forced: false};
|
|
69
|
-
await page.waitForFunction(state => {
|
|
70
|
+
const scrollState = { forced: false };
|
|
71
|
+
await page.waitForFunction((state) => {
|
|
70
72
|
if (window.scrollY !== 0 && !state.forced) {
|
|
71
|
-
window.scroll({top: 0, left: 0, behavior:
|
|
73
|
+
window.scroll({ top: 0, left: 0, behavior: "instant" });
|
|
72
74
|
state.forced = true;
|
|
73
75
|
}
|
|
74
76
|
return window.scrollY === 0;
|
|
@@ -111,7 +113,7 @@ export function settleImage(image: HTMLImageElement): void | Promise<void> {
|
|
|
111
113
|
if ((image.width <= 1 && image.height <= 1) || image.complete) {
|
|
112
114
|
return;
|
|
113
115
|
}
|
|
114
|
-
return new Promise<void>(resolve => {
|
|
116
|
+
return new Promise<void>((resolve) => {
|
|
115
117
|
const settled = () => {
|
|
116
118
|
image.removeEventListener("load", settled);
|
|
117
119
|
image.removeEventListener("error", settled);
|
|
@@ -131,7 +133,7 @@ export async function waitForAllImages(
|
|
|
131
133
|
page: Page,
|
|
132
134
|
options: WaitForImagesOptions = {},
|
|
133
135
|
): Promise<void> {
|
|
134
|
-
await waitForImages(page,
|
|
136
|
+
await waitForImages(page, "img:visible", options);
|
|
135
137
|
}
|
|
136
138
|
|
|
137
139
|
/**
|
|
@@ -167,14 +169,12 @@ export async function waitForAllImages(
|
|
|
167
169
|
* rewritten and re-requested. Defaults to false.
|
|
168
170
|
* @returns The URLs of the images that never decoded. Empty when they all did.
|
|
169
171
|
*/
|
|
170
|
-
export async function decodeVisibleImages(
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
): Promise<string[]> {
|
|
172
|
+
export async function decodeVisibleImages(options: {
|
|
173
|
+
timeoutMs: number;
|
|
174
|
+
pollMs?: number;
|
|
175
|
+
reloadIntervalMs?: number;
|
|
176
|
+
recoverErroredImages?: boolean;
|
|
177
|
+
}): Promise<string[]> {
|
|
178
178
|
const timeoutMs = options.timeoutMs;
|
|
179
179
|
const pollMs = options.pollMs ?? 250;
|
|
180
180
|
const reloadIntervalMs = options.reloadIntervalMs ?? 2000;
|
|
@@ -204,7 +204,7 @@ export async function decodeVisibleImages(
|
|
|
204
204
|
// one fetched, instead of re-running srcset selection.
|
|
205
205
|
const picture = img.closest("picture");
|
|
206
206
|
if (picture) {
|
|
207
|
-
picture.querySelectorAll("source").forEach(source => source.remove());
|
|
207
|
+
picture.querySelectorAll("source").forEach((source) => source.remove());
|
|
208
208
|
}
|
|
209
209
|
img.removeAttribute("srcset");
|
|
210
210
|
img.src = url.href;
|
|
@@ -220,31 +220,37 @@ export async function decodeVisibleImages(
|
|
|
220
220
|
const candidates = Array.from(document.images).filter(isCandidate);
|
|
221
221
|
// Collect the verdicts positionally rather than pushing as each check
|
|
222
222
|
// settles, so anything reported below stays in document order.
|
|
223
|
-
const verdicts = await Promise.all(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
223
|
+
const verdicts = await Promise.all(
|
|
224
|
+
candidates.map(async (img) => {
|
|
225
|
+
if (img.complete && img.naturalWidth > 0) {
|
|
226
|
+
return "loaded"; // Loaded with intrinsic dimensions.
|
|
227
|
+
}
|
|
228
|
+
if (!img.complete) {
|
|
229
|
+
return "loading";
|
|
230
|
+
}
|
|
231
|
+
// Complete with a zero naturalWidth is ambiguous: a genuine load error
|
|
232
|
+
// (404/503) rejects decode(), while a valid but dimensionless image
|
|
233
|
+
// (such as an SVG with no intrinsic size) resolves it. Only the former
|
|
234
|
+
// should be re-requested; the latter is already settled.
|
|
235
|
+
try {
|
|
236
|
+
await img.decode();
|
|
237
|
+
return "loaded";
|
|
238
|
+
} catch {
|
|
239
|
+
return "errored";
|
|
240
|
+
}
|
|
241
|
+
}),
|
|
242
|
+
);
|
|
243
|
+
const pending = candidates.filter(
|
|
244
|
+
(img, index) => verdicts[index] !== "loaded",
|
|
245
|
+
);
|
|
246
|
+
const errored = candidates.filter(
|
|
247
|
+
(img, index) => verdicts[index] === "errored",
|
|
248
|
+
);
|
|
243
249
|
if (pending.length === 0) {
|
|
244
250
|
return [];
|
|
245
251
|
}
|
|
246
252
|
if (Date.now() >= deadline) {
|
|
247
|
-
return pending.map(img => {
|
|
253
|
+
return pending.map((img) => {
|
|
248
254
|
const src = img.currentSrc || img.src;
|
|
249
255
|
try {
|
|
250
256
|
// Report the URL as the page authored it, without the cache-busting
|
|
@@ -262,7 +268,7 @@ export async function decodeVisibleImages(
|
|
|
262
268
|
lastReload = Date.now();
|
|
263
269
|
errored.forEach(reload);
|
|
264
270
|
}
|
|
265
|
-
await new Promise(resolve => setTimeout(resolve, pollMs));
|
|
271
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
266
272
|
}
|
|
267
273
|
}
|
|
268
274
|
|
|
@@ -289,10 +295,13 @@ export async function waitForImagesToDecode(
|
|
|
289
295
|
timeoutMs = 15000,
|
|
290
296
|
recoverErroredImages = false,
|
|
291
297
|
): Promise<string[]> {
|
|
292
|
-
const undecoded = await page.evaluate(decodeVisibleImages, {
|
|
298
|
+
const undecoded = await page.evaluate(decodeVisibleImages, {
|
|
299
|
+
timeoutMs,
|
|
300
|
+
recoverErroredImages,
|
|
301
|
+
});
|
|
293
302
|
if (undecoded.length > 0) {
|
|
294
303
|
console.warn(
|
|
295
|
-
`waitForImagesToDecode: ${undecoded.length} image(s) did not finish loading within ${timeoutMs}ms and may be captured as broken: ${undecoded.join(
|
|
304
|
+
`waitForImagesToDecode: ${undecoded.length} image(s) did not finish loading within ${timeoutMs}ms and may be captured as broken: ${undecoded.join(", ")}`,
|
|
296
305
|
);
|
|
297
306
|
}
|
|
298
307
|
return undecoded;
|
package/src/index.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
export * from
|
|
2
|
-
export * from
|
|
3
|
-
export * from
|
|
4
|
-
export * from
|
|
5
|
-
export * from
|
|
6
|
-
export * from
|
|
7
|
-
export * from
|
|
8
|
-
export * from
|
|
9
|
-
export * from
|
|
10
|
-
export * from
|
|
11
|
-
export * from
|
|
12
|
-
export * from
|
|
13
|
-
export * from
|
|
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,48 +1,78 @@
|
|
|
1
|
-
import {describe, expect, it, vi} from
|
|
2
|
-
import {
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
applyInteractionStates,
|
|
4
|
+
validateInteractionStates,
|
|
5
|
+
} from "./interaction-states";
|
|
3
6
|
|
|
4
|
-
describe(
|
|
5
|
-
it(
|
|
6
|
-
const locator = {} as never
|
|
7
|
-
expect(() =>
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
describe("validateInteractionStates", () => {
|
|
8
|
+
it("rejects states that real input cannot hold simultaneously", () => {
|
|
9
|
+
const locator = {} as never;
|
|
10
|
+
expect(() =>
|
|
11
|
+
validateInteractionStates([
|
|
12
|
+
{ locator, states: ["hover"] },
|
|
13
|
+
{ locator, states: ["hover"] },
|
|
14
|
+
]),
|
|
15
|
+
).toThrow("at most one");
|
|
16
|
+
expect(() =>
|
|
17
|
+
validateInteractionStates([{ locator, states: ["focus", "focus"] }]),
|
|
18
|
+
).toThrow("cannot repeat");
|
|
19
|
+
});
|
|
20
|
+
});
|
|
16
21
|
|
|
17
|
-
describe(
|
|
18
|
-
it(
|
|
19
|
-
const hover = vi.fn().mockResolvedValue(undefined)
|
|
20
|
-
const focus = vi.fn().mockResolvedValue(undefined)
|
|
21
|
-
const blur = vi.fn().mockResolvedValue(undefined)
|
|
22
|
-
const clearHover = vi.fn().mockResolvedValue(undefined)
|
|
22
|
+
describe("applyInteractionStates", () => {
|
|
23
|
+
it("applies hover before focus and cleans both states up", async () => {
|
|
24
|
+
const hover = vi.fn().mockResolvedValue(undefined);
|
|
25
|
+
const focus = vi.fn().mockResolvedValue(undefined);
|
|
26
|
+
const blur = vi.fn().mockResolvedValue(undefined);
|
|
27
|
+
const clearHover = vi.fn().mockResolvedValue(undefined);
|
|
23
28
|
|
|
24
|
-
const cleanup = await applyInteractionStates(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
const cleanup = await applyInteractionStates(
|
|
30
|
+
[
|
|
31
|
+
{
|
|
32
|
+
locator: { hover, focus: vi.fn(), blur: vi.fn() } as never,
|
|
33
|
+
states: ["hover"],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
locator: { hover: vi.fn(), focus, blur } as never,
|
|
37
|
+
states: ["focus"],
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
{ clearHover },
|
|
41
|
+
);
|
|
28
42
|
|
|
29
|
-
expect(hover.mock.invocationCallOrder[0]).toBeLessThan(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
43
|
+
expect(hover.mock.invocationCallOrder[0]).toBeLessThan(
|
|
44
|
+
focus.mock.invocationCallOrder[0],
|
|
45
|
+
);
|
|
46
|
+
await cleanup();
|
|
47
|
+
await cleanup();
|
|
48
|
+
expect(blur).toHaveBeenCalledOnce();
|
|
49
|
+
expect(clearHover).toHaveBeenCalledOnce();
|
|
50
|
+
});
|
|
35
51
|
|
|
36
|
-
it(
|
|
37
|
-
const clearHover = vi.fn().mockResolvedValue(undefined)
|
|
38
|
-
const blur = vi.fn().mockResolvedValue(undefined)
|
|
39
|
-
const failure = new Error(
|
|
52
|
+
it("cleans already-applied state when a later interaction fails", async () => {
|
|
53
|
+
const clearHover = vi.fn().mockResolvedValue(undefined);
|
|
54
|
+
const blur = vi.fn().mockResolvedValue(undefined);
|
|
55
|
+
const failure = new Error("focus failed");
|
|
40
56
|
|
|
41
|
-
await expect(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
57
|
+
await expect(
|
|
58
|
+
applyInteractionStates(
|
|
59
|
+
[
|
|
60
|
+
{
|
|
61
|
+
locator: { hover: vi.fn().mockResolvedValue(undefined) } as never,
|
|
62
|
+
states: ["hover"],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
locator: {
|
|
66
|
+
focus: vi.fn().mockRejectedValue(failure),
|
|
67
|
+
blur,
|
|
68
|
+
} as never,
|
|
69
|
+
states: ["focus"],
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
{ clearHover },
|
|
73
|
+
),
|
|
74
|
+
).rejects.toBe(failure);
|
|
75
|
+
expect(blur).toHaveBeenCalledOnce();
|
|
76
|
+
expect(clearHover).toHaveBeenCalledOnce();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -1,35 +1,45 @@
|
|
|
1
|
-
import type {Locator} from
|
|
1
|
+
import type { Locator } from "@playwright/test";
|
|
2
2
|
|
|
3
3
|
/** Real browser interaction states supported consistently by Playwright. */
|
|
4
|
-
export type InteractionState =
|
|
4
|
+
export type InteractionState = "hover" | "focus";
|
|
5
5
|
|
|
6
6
|
/** A locator and the real browser interaction states to apply to it. */
|
|
7
7
|
export interface ScreenshotInteractionState {
|
|
8
|
-
locator: Locator
|
|
9
|
-
states: InteractionState[]
|
|
8
|
+
locator: Locator;
|
|
9
|
+
states: InteractionState[];
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export interface InteractionStateCleanupOptions {
|
|
13
13
|
/** Move the pointer away from the hovered locator during cleanup. */
|
|
14
|
-
clearHover?: () => Promise<void
|
|
14
|
+
clearHover?: () => Promise<void>;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/** Validate that the requested states are possible with real browser input. */
|
|
18
18
|
export function validateInteractionStates(
|
|
19
19
|
interactionStates: ScreenshotInteractionState[],
|
|
20
20
|
): void {
|
|
21
|
-
const hoverTargets = interactionStates.filter(({states}) =>
|
|
22
|
-
|
|
21
|
+
const hoverTargets = interactionStates.filter(({ states }) =>
|
|
22
|
+
states.includes("hover"),
|
|
23
|
+
);
|
|
24
|
+
const focusTargets = interactionStates.filter(({ states }) =>
|
|
25
|
+
states.includes("focus"),
|
|
26
|
+
);
|
|
23
27
|
|
|
24
28
|
if (hoverTargets.length > 1) {
|
|
25
|
-
throw new Error(
|
|
29
|
+
throw new Error(
|
|
30
|
+
"interactionStates can hover at most one locator at a time.",
|
|
31
|
+
);
|
|
26
32
|
}
|
|
27
33
|
if (focusTargets.length > 1) {
|
|
28
|
-
throw new Error(
|
|
34
|
+
throw new Error(
|
|
35
|
+
"interactionStates can focus at most one locator at a time.",
|
|
36
|
+
);
|
|
29
37
|
}
|
|
30
|
-
for (const {states} of interactionStates) {
|
|
38
|
+
for (const { states } of interactionStates) {
|
|
31
39
|
if (new Set(states).size !== states.length) {
|
|
32
|
-
throw new Error(
|
|
40
|
+
throw new Error(
|
|
41
|
+
"interactionStates cannot repeat a state for the same locator.",
|
|
42
|
+
);
|
|
33
43
|
}
|
|
34
44
|
}
|
|
35
45
|
}
|
|
@@ -45,50 +55,54 @@ export async function applyInteractionStates(
|
|
|
45
55
|
interactionStates: ScreenshotInteractionState[],
|
|
46
56
|
cleanupOptions: InteractionStateCleanupOptions = {},
|
|
47
57
|
): Promise<() => Promise<void>> {
|
|
48
|
-
validateInteractionStates(interactionStates)
|
|
58
|
+
validateInteractionStates(interactionStates);
|
|
49
59
|
|
|
50
|
-
const hoverTarget = interactionStates.find(({states}) =>
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
const hoverTarget = interactionStates.find(({ states }) =>
|
|
61
|
+
states.includes("hover"),
|
|
62
|
+
)?.locator;
|
|
63
|
+
const focusTarget = interactionStates.find(({ states }) =>
|
|
64
|
+
states.includes("focus"),
|
|
65
|
+
)?.locator;
|
|
66
|
+
let hoverApplied = false;
|
|
67
|
+
let focusApplied = false;
|
|
68
|
+
let cleaned = false;
|
|
55
69
|
|
|
56
70
|
const cleanup = async () => {
|
|
57
71
|
if (cleaned) {
|
|
58
|
-
return
|
|
72
|
+
return;
|
|
59
73
|
}
|
|
60
|
-
cleaned = true
|
|
74
|
+
cleaned = true;
|
|
61
75
|
try {
|
|
62
76
|
if (focusApplied) {
|
|
63
|
-
await focusTarget?.blur()
|
|
77
|
+
await focusTarget?.blur();
|
|
64
78
|
}
|
|
65
79
|
} finally {
|
|
66
80
|
if (hoverApplied) {
|
|
67
|
-
await cleanupOptions.clearHover?.()
|
|
81
|
+
await cleanupOptions.clearHover?.();
|
|
68
82
|
}
|
|
69
83
|
}
|
|
70
|
-
}
|
|
84
|
+
};
|
|
71
85
|
|
|
72
86
|
try {
|
|
73
87
|
if (hoverTarget) {
|
|
74
88
|
// Mark the state first: Playwright can move the pointer and then fail
|
|
75
89
|
// while waiting for actionability, and that partial effect still needs
|
|
76
90
|
// cleanup.
|
|
77
|
-
hoverApplied = true
|
|
78
|
-
await hoverTarget.hover()
|
|
91
|
+
hoverApplied = true;
|
|
92
|
+
await hoverTarget.hover();
|
|
79
93
|
}
|
|
80
94
|
if (focusTarget) {
|
|
81
|
-
focusApplied = true
|
|
82
|
-
await focusTarget.focus()
|
|
95
|
+
focusApplied = true;
|
|
96
|
+
await focusTarget.focus();
|
|
83
97
|
}
|
|
84
|
-
return cleanup
|
|
98
|
+
return cleanup;
|
|
85
99
|
} catch (error) {
|
|
86
100
|
try {
|
|
87
|
-
await cleanup()
|
|
101
|
+
await cleanup();
|
|
88
102
|
} catch {
|
|
89
103
|
// Preserve the interaction failure. Cleanup is best effort on this path;
|
|
90
104
|
// callers cannot act on a cleanup error if applying the state failed.
|
|
91
105
|
}
|
|
92
|
-
throw error
|
|
106
|
+
throw error;
|
|
93
107
|
}
|
|
94
108
|
}
|
package/src/mock/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { YoutubeMock } from
|
|
1
|
+
export { YoutubeMock } from "./youtube.js";
|
package/src/mock/youtube.test.ts
CHANGED
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from
|
|
2
|
-
import { YoutubeMock } from
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { YoutubeMock } from "./youtube";
|
|
3
3
|
|
|
4
|
-
describe(
|
|
5
|
-
it(
|
|
4
|
+
describe("YoutubeMock", () => {
|
|
5
|
+
it("registers a route handler for youtube.com", async () => {
|
|
6
6
|
const mockPage = {
|
|
7
7
|
route: vi.fn(),
|
|
8
|
-
}
|
|
8
|
+
};
|
|
9
9
|
|
|
10
|
-
const mock = new YoutubeMock()
|
|
11
|
-
await mock.mock(mockPage as any)
|
|
10
|
+
const mock = new YoutubeMock();
|
|
11
|
+
await mock.mock(mockPage as any);
|
|
12
12
|
|
|
13
|
-
expect(mockPage.route).toHaveBeenCalledOnce()
|
|
14
|
-
const [pattern] = mockPage.route.mock.calls[0]
|
|
15
|
-
expect(pattern).toBeInstanceOf(RegExp)
|
|
16
|
-
expect(pattern.test(
|
|
17
|
-
expect(pattern.test(
|
|
18
|
-
})
|
|
13
|
+
expect(mockPage.route).toHaveBeenCalledOnce();
|
|
14
|
+
const [pattern] = mockPage.route.mock.calls[0];
|
|
15
|
+
expect(pattern).toBeInstanceOf(RegExp);
|
|
16
|
+
expect(pattern.test("https://www.youtube.com/embed/abc123")).toBe(true);
|
|
17
|
+
expect(pattern.test("https://www.notyoutube.com")).toBe(false);
|
|
18
|
+
});
|
|
19
19
|
|
|
20
|
-
it(
|
|
21
|
-
const mockFulfill = vi.fn()
|
|
20
|
+
it("fulfills with HTML mock content", async () => {
|
|
21
|
+
const mockFulfill = vi.fn();
|
|
22
22
|
const mockPage = {
|
|
23
23
|
route: vi.fn(),
|
|
24
|
-
}
|
|
24
|
+
};
|
|
25
25
|
|
|
26
|
-
const mock = new YoutubeMock()
|
|
27
|
-
await mock.mock(mockPage as any)
|
|
26
|
+
const mock = new YoutubeMock();
|
|
27
|
+
await mock.mock(mockPage as any);
|
|
28
28
|
|
|
29
29
|
// Call the route handler
|
|
30
|
-
const handler = mockPage.route.mock.calls[0][1]
|
|
31
|
-
await handler({ fulfill: mockFulfill })
|
|
30
|
+
const handler = mockPage.route.mock.calls[0][1];
|
|
31
|
+
await handler({ fulfill: mockFulfill });
|
|
32
32
|
|
|
33
|
-
expect(mockFulfill).toHaveBeenCalledOnce()
|
|
34
|
-
const fulfillArg = mockFulfill.mock.calls[0][0]
|
|
35
|
-
expect(fulfillArg.contentType).toBe(
|
|
36
|
-
expect(fulfillArg.body).toContain(
|
|
37
|
-
})
|
|
38
|
-
})
|
|
33
|
+
expect(mockFulfill).toHaveBeenCalledOnce();
|
|
34
|
+
const fulfillArg = mockFulfill.mock.calls[0][0];
|
|
35
|
+
expect(fulfillArg.contentType).toBe("text/html");
|
|
36
|
+
expect(fulfillArg.body).toContain("Youtube Video Mock");
|
|
37
|
+
});
|
|
38
|
+
});
|
package/src/mock/youtube.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import type {Page} from
|
|
2
|
-
import type {Mockable} from "../visualdiff.js";
|
|
1
|
+
import type { Page } from "@playwright/test";
|
|
2
|
+
import type { Mockable } from "../visualdiff.js";
|
|
3
3
|
|
|
4
4
|
export class YoutubeMock implements Mockable {
|
|
5
|
-
|
|
6
5
|
public async mock(page: Page): Promise<void> {
|
|
7
|
-
await page.route(/www\.youtube\.com/i, async route => {
|
|
6
|
+
await page.route(/www\.youtube\.com/i, async (route) => {
|
|
8
7
|
await route.fulfill({
|
|
9
|
-
contentType:
|
|
8
|
+
contentType: "text/html",
|
|
10
9
|
body: `
|
|
11
10
|
<html>
|
|
12
11
|
<head>
|
|
@@ -35,5 +34,4 @@ export class YoutubeMock implements Mockable {
|
|
|
35
34
|
});
|
|
36
35
|
});
|
|
37
36
|
}
|
|
38
|
-
|
|
39
37
|
}
|