@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,5 +1,5 @@
1
- import * as fs from 'fs'
2
- import * as path from 'path'
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
3
 
4
4
  /**
5
5
  * Translate the file paths a Playwright JSON report records into paths the
@@ -40,31 +40,31 @@ import * as path from 'path'
40
40
  * context makes a coincidence unlikely, and longer tails are tried first
41
41
  * regardless, so the most specific match always wins.
42
42
  */
43
- const MIN_TAIL_COMPONENTS = 2
43
+ const MIN_TAIL_COMPONENTS = 2;
44
44
 
45
45
  export interface PathPrefix {
46
46
  /** Absolute path as recorded in the report — typically a container path. */
47
- from: string
47
+ from: string;
48
48
  /** The same directory as seen from here. */
49
- to: string
49
+ to: string;
50
50
  }
51
51
 
52
52
  export interface PathResolution {
53
53
  /** The path to read. Unchanged when the recorded one was already fine. */
54
- path: string
54
+ path: string;
55
55
  /** Whether a readable file was found. */
56
- found: boolean
56
+ found: boolean;
57
57
  /** The mapping that made it readable, when one was needed. */
58
- prefix?: PathPrefix
58
+ prefix?: PathPrefix;
59
59
  }
60
60
 
61
61
  export interface PathResolverOptions {
62
62
  /** Where the JSON report was read from, on this side of the boundary. */
63
- reportPath: string
63
+ reportPath: string;
64
64
  /** Explicit mappings. Tried before anything is worked out. */
65
- prefixes?: PathPrefix[]
65
+ prefixes?: PathPrefix[];
66
66
  /** Existence check, injectable for tests. */
67
- exists?: (filePath: string) => boolean
67
+ exists?: (filePath: string) => boolean;
68
68
  }
69
69
 
70
70
  /**
@@ -74,14 +74,14 @@ export interface PathResolverOptions {
74
74
  * contain one on Windows, whereas `FROM` comes from a container and will not.
75
75
  */
76
76
  export function parsePathPrefix(value: string): PathPrefix | null {
77
- const separator = value.indexOf(':')
78
- if (separator <= 0) return null
77
+ const separator = value.indexOf(":");
78
+ if (separator <= 0) return null;
79
79
 
80
- const from = value.slice(0, separator).trim()
81
- const to = value.slice(separator + 1).trim()
82
- if (!from || !to) return null
80
+ const from = value.slice(0, separator).trim();
81
+ const to = value.slice(separator + 1).trim();
82
+ if (!from || !to) return null;
83
83
 
84
- return { from, to }
84
+ return { from, to };
85
85
  }
86
86
 
87
87
  /**
@@ -95,64 +95,75 @@ export function parsePathPrefix(value: string): PathPrefix | null {
95
95
  export function createPathResolver(
96
96
  options: PathResolverOptions,
97
97
  ): (filePath: string) => PathResolution {
98
- const exists = options.exists ?? ((filePath: string) => fs.existsSync(filePath))
99
- const explicit = options.prefixes ?? []
100
- const searchRoots = ancestors(path.dirname(path.resolve(options.reportPath)))
101
- const learned: PathPrefix[] = []
98
+ const exists =
99
+ options.exists ?? ((filePath: string) => fs.existsSync(filePath));
100
+ const explicit = options.prefixes ?? [];
101
+ const searchRoots = ancestors(path.dirname(path.resolve(options.reportPath)));
102
+ const learned: PathPrefix[] = [];
102
103
 
103
104
  /** Hang tails of the recorded path off each directory around the report. */
104
105
  function search(filePath: string): PathResolution | null {
105
- const target = splitPath(filePath)
106
+ const target = splitPath(filePath);
106
107
 
107
108
  // Longest tail first, so the most specific match wins. Stop one short of
108
109
  // the whole path: something has to be left to rewrite.
109
- for (let length = target.components.length - 1; length >= MIN_TAIL_COMPONENTS; length--) {
110
- const tail = target.components.slice(target.components.length - length).join('/')
110
+ for (
111
+ let length = target.components.length - 1;
112
+ length >= MIN_TAIL_COMPONENTS;
113
+ length--
114
+ ) {
115
+ const tail = target.components
116
+ .slice(target.components.length - length)
117
+ .join("/");
111
118
 
112
119
  for (const root of searchRoots) {
113
- const candidate = `${trimTrailingSeparators(root)}/${tail}`
114
- if (!exists(candidate)) continue
120
+ const candidate = `${trimTrailingSeparators(root)}/${tail}`;
121
+ if (!exists(candidate)) continue;
115
122
 
116
123
  return {
117
124
  path: candidate,
118
125
  found: true,
119
- prefix: { from: joinPath(target, target.components.length - length), to: root },
120
- }
126
+ prefix: {
127
+ from: joinPath(target, target.components.length - length),
128
+ to: root,
129
+ },
130
+ };
121
131
  }
122
132
  }
123
133
 
124
- return null
134
+ return null;
125
135
  }
126
136
 
127
137
  return (filePath: string): PathResolution => {
128
- if (exists(filePath)) return { path: filePath, found: true }
138
+ if (exists(filePath)) return { path: filePath, found: true };
129
139
 
130
140
  for (const prefix of [...explicit, ...learned]) {
131
- const rewritten = applyPrefix(filePath, prefix)
132
- if (rewritten && exists(rewritten)) return { path: rewritten, found: true, prefix }
141
+ const rewritten = applyPrefix(filePath, prefix);
142
+ if (rewritten && exists(rewritten))
143
+ return { path: rewritten, found: true, prefix };
133
144
  }
134
145
 
135
- const discovered = search(filePath)
136
- if (!discovered) return { path: filePath, found: false }
146
+ const discovered = search(filePath);
147
+ if (!discovered) return { path: filePath, found: false };
137
148
 
138
- if (discovered.prefix) learned.push(discovered.prefix)
139
- return discovered
140
- }
149
+ if (discovered.prefix) learned.push(discovered.prefix);
150
+ return discovered;
151
+ };
141
152
  }
142
153
 
143
154
  /** Swap one prefix for another, or null when the path is not under it. */
144
155
  function applyPrefix(filePath: string, prefix: PathPrefix): string | null {
145
- const from = splitPath(prefix.from)
146
- const target = splitPath(filePath)
156
+ const from = splitPath(prefix.from);
157
+ const target = splitPath(filePath);
147
158
 
148
- if (target.components.length < from.components.length) return null
159
+ if (target.components.length < from.components.length) return null;
149
160
  for (let index = 0; index < from.components.length; index++) {
150
- if (target.components[index] !== from.components[index]) return null
161
+ if (target.components[index] !== from.components[index]) return null;
151
162
  }
152
163
 
153
- const rest = target.components.slice(from.components.length)
154
- const to = trimTrailingSeparators(prefix.to)
155
- return rest.length === 0 ? to : `${to}/${rest.join('/')}`
164
+ const rest = target.components.slice(from.components.length);
165
+ const to = trimTrailingSeparators(prefix.to);
166
+ return rest.length === 0 ? to : `${to}/${rest.join("/")}`;
156
167
  }
157
168
 
158
169
  /**
@@ -163,15 +174,15 @@ function applyPrefix(filePath: string, prefix: PathPrefix): string | null {
163
174
  * out of a report this package did not write.
164
175
  */
165
176
  function trimTrailingSeparators(value: string): string {
166
- let end = value.length
167
- while (end > 0 && (value[end - 1] === '/' || value[end - 1] === '\\')) end--
168
- return value.slice(0, end)
177
+ let end = value.length;
178
+ while (end > 0 && (value[end - 1] === "/" || value[end - 1] === "\\")) end--;
179
+ return value.slice(0, end);
169
180
  }
170
181
 
171
182
  interface SplitPath {
172
183
  /** `/`, `C:/`, or empty for a relative path. */
173
- root: string
174
- components: string[]
184
+ root: string;
185
+ components: string[];
175
186
  }
176
187
 
177
188
  /**
@@ -181,28 +192,31 @@ interface SplitPath {
181
192
  * recorded paths come from a report rather than from this platform.
182
193
  */
183
194
  function splitPath(value: string): SplitPath {
184
- const match = /^([A-Za-z]:[\\/]|[\\/])?/.exec(value)
185
- const prefix = match?.[1] ?? ''
195
+ const match = /^([A-Za-z]:[\\/]|[\\/])?/.exec(value);
196
+ const prefix = match?.[1] ?? "";
186
197
  return {
187
- root: prefix.replace(/\\/g, '/'),
188
- components: value.slice(prefix.length).split(/[\\/]+/).filter(Boolean),
189
- }
198
+ root: prefix.replace(/\\/g, "/"),
199
+ components: value
200
+ .slice(prefix.length)
201
+ .split(/[\\/]+/)
202
+ .filter(Boolean),
203
+ };
190
204
  }
191
205
 
192
206
  /** Rebuild a path from its first `count` components. */
193
207
  function joinPath(split: SplitPath, count: number): string {
194
- return split.root + split.components.slice(0, count).join('/')
208
+ return split.root + split.components.slice(0, count).join("/");
195
209
  }
196
210
 
197
211
  /** A directory and every directory above it. */
198
212
  function ancestors(directory: string): string[] {
199
- const out: string[] = []
200
- let current = directory
213
+ const out: string[] = [];
214
+ let current = directory;
201
215
 
202
216
  for (;;) {
203
- out.push(current)
204
- const parent = path.dirname(current)
205
- if (parent === current) return out
206
- current = parent
217
+ out.push(current);
218
+ const parent = path.dirname(current);
219
+ if (parent === current) return out;
220
+ current = parent;
207
221
  }
208
222
  }
package/src/hover.test.ts CHANGED
@@ -1,76 +1,80 @@
1
- import {afterEach, describe, expect, it, vi} from 'vitest'
2
- import {clearHover} from './hover'
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { clearHover } from "./hover";
3
3
 
4
4
  function fakePage() {
5
- let shield: any
5
+ let shield: any;
6
6
  const document = {
7
7
  querySelector: vi.fn(() => shield),
8
8
  createElement: vi.fn(() => {
9
9
  shield = {
10
10
  setAttribute: vi.fn(),
11
- style: {cssText: ''},
11
+ style: { cssText: "" },
12
12
  remove: vi.fn(() => {
13
- shield = undefined
13
+ shield = undefined;
14
14
  }),
15
- }
16
- return shield
15
+ };
16
+ return shield;
17
17
  }),
18
18
  documentElement: {
19
19
  appendChild: vi.fn(),
20
20
  },
21
- }
21
+ };
22
22
  const page = {
23
- evaluate: vi.fn(async (callback: (attribute: string) => void, attribute: string) => {
24
- vi.stubGlobal('document', document)
25
- callback(attribute)
26
- }),
23
+ evaluate: vi.fn(
24
+ async (callback: (attribute: string) => void, attribute: string) => {
25
+ vi.stubGlobal("document", document);
26
+ callback(attribute);
27
+ },
28
+ ),
27
29
  mouse: {
28
30
  move: vi.fn().mockResolvedValue(undefined),
29
31
  },
30
- }
32
+ };
31
33
 
32
- return {document, page, getShield: () => shield}
34
+ return { document, page, getShield: () => shield };
33
35
  }
34
36
 
35
- describe('clearHover', () => {
37
+ describe("clearHover", () => {
36
38
  afterEach(() => {
37
- vi.unstubAllGlobals()
38
- })
39
+ vi.unstubAllGlobals();
40
+ });
39
41
 
40
- it('covers page content and moves the pointer onto the shield', async () => {
41
- const {document, page, getShield} = fakePage()
42
+ it("covers page content and moves the pointer onto the shield", async () => {
43
+ const { document, page, getShield } = fakePage();
42
44
 
43
- const cleanup = await clearHover(page as any)
44
- const shield = getShield()
45
+ const cleanup = await clearHover(page as any);
46
+ const shield = getShield();
45
47
 
46
- expect(document.createElement).toHaveBeenCalledWith('playwright-testing-hover-shield')
47
- expect(shield.setAttribute).toHaveBeenCalledWith('aria-hidden', 'true')
48
- expect(shield.style.cssText).toContain('position: fixed !important')
49
- expect(shield.style.cssText).toContain('pointer-events: auto !important')
50
- expect(shield.style.cssText).toContain('opacity: 0 !important')
51
- expect(document.documentElement.appendChild).toHaveBeenCalledWith(shield)
52
- expect(page.mouse.move).toHaveBeenCalledWith(0, 0)
48
+ expect(document.createElement).toHaveBeenCalledWith(
49
+ "playwright-testing-hover-shield",
50
+ );
51
+ expect(shield.setAttribute).toHaveBeenCalledWith("aria-hidden", "true");
52
+ expect(shield.style.cssText).toContain("position: fixed !important");
53
+ expect(shield.style.cssText).toContain("pointer-events: auto !important");
54
+ expect(shield.style.cssText).toContain("opacity: 0 !important");
55
+ expect(document.documentElement.appendChild).toHaveBeenCalledWith(shield);
56
+ expect(page.mouse.move).toHaveBeenCalledWith(0, 0);
53
57
 
54
- await cleanup()
55
- expect(shield.remove).toHaveBeenCalledOnce()
56
- })
58
+ await cleanup();
59
+ expect(shield.remove).toHaveBeenCalledOnce();
60
+ });
57
61
 
58
- it('makes cleanup idempotent', async () => {
59
- const {page, getShield} = fakePage()
60
- const cleanup = await clearHover(page as any)
61
- const shield = getShield()
62
+ it("makes cleanup idempotent", async () => {
63
+ const { page, getShield } = fakePage();
64
+ const cleanup = await clearHover(page as any);
65
+ const shield = getShield();
62
66
 
63
- await cleanup()
64
- await cleanup()
67
+ await cleanup();
68
+ await cleanup();
65
69
 
66
- expect(shield.remove).toHaveBeenCalledOnce()
67
- })
70
+ expect(shield.remove).toHaveBeenCalledOnce();
71
+ });
68
72
 
69
- it('removes the shield when moving the pointer fails', async () => {
70
- const {page, getShield} = fakePage()
71
- page.mouse.move.mockRejectedValueOnce(new Error('mouse failed'))
73
+ it("removes the shield when moving the pointer fails", async () => {
74
+ const { page, getShield } = fakePage();
75
+ page.mouse.move.mockRejectedValueOnce(new Error("mouse failed"));
72
76
 
73
- await expect(clearHover(page as any)).rejects.toThrow('mouse failed')
74
- expect(getShield()).toBeUndefined()
75
- })
76
- })
77
+ await expect(clearHover(page as any)).rejects.toThrow("mouse failed");
78
+ expect(getShield()).toBeUndefined();
79
+ });
80
+ });
package/src/hover.ts CHANGED
@@ -1,6 +1,6 @@
1
- import {Page} from "@playwright/test";
1
+ import { Page } from "@playwright/test";
2
2
 
3
- const HOVER_SHIELD_ATTRIBUTE = 'data-playwright-testing-hover-shield';
3
+ const HOVER_SHIELD_ATTRIBUTE = "data-playwright-testing-hover-shield";
4
4
 
5
5
  /**
6
6
  * Move the pointer away from page content for a deterministic screenshot.
@@ -17,23 +17,23 @@ export async function clearHover(page: Page): Promise<() => Promise<void>> {
17
17
  await page.evaluate((attribute) => {
18
18
  document.querySelector(`[${attribute}]`)?.remove();
19
19
 
20
- const shield = document.createElement('playwright-testing-hover-shield');
21
- shield.setAttribute(attribute, '');
22
- shield.setAttribute('aria-hidden', 'true');
20
+ const shield = document.createElement("playwright-testing-hover-shield");
21
+ shield.setAttribute(attribute, "");
22
+ shield.setAttribute("aria-hidden", "true");
23
23
  shield.style.cssText = [
24
- 'all: initial !important',
25
- 'position: fixed !important',
26
- 'inset: 0 !important',
27
- 'display: block !important',
28
- 'width: 100vw !important',
29
- 'height: 100vh !important',
30
- 'margin: 0 !important',
31
- 'padding: 0 !important',
32
- 'border: 0 !important',
33
- 'opacity: 0 !important',
34
- 'pointer-events: auto !important',
35
- 'z-index: 2147483647 !important',
36
- ].join(';');
24
+ "all: initial !important",
25
+ "position: fixed !important",
26
+ "inset: 0 !important",
27
+ "display: block !important",
28
+ "width: 100vw !important",
29
+ "height: 100vh !important",
30
+ "margin: 0 !important",
31
+ "padding: 0 !important",
32
+ "border: 0 !important",
33
+ "opacity: 0 !important",
34
+ "pointer-events: auto !important",
35
+ "z-index: 2147483647 !important",
36
+ ].join(";");
37
37
  document.documentElement.appendChild(shield);
38
38
  }, HOVER_SHIELD_ATTRIBUTE);
39
39