@krizaka/page-audit 0.5.1 → 0.7.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.
package/src/self-test.mjs CHANGED
@@ -1,6 +1,29 @@
1
1
  // The audit proven on its own fixtures, in a real browser: each broken fixture must be reported, the fine ones must not.
2
2
  // A guard that stopped seeing is itself a red build.
3
- import { BOX_TOO_WIDE, BROKEN_PARTS, CARD_OVER_NEXT, CHROME_BAD, CHROME_FINE, COLUMN_CUT, EMPTY_MAIN, FINE, FOLDED_SECTION, FRAME_FITS, FRAME_FORCES_SCROLL, HIDDEN_CONTENT, MAP_COLLAPSED, MAP_FINE, MAP_HIDDEN, MAP_NO_CANVAS, SECTIONS_OVERLAP, SMALL_ACTION, TOKEN_IN_STRETCHED_ROW, TOKEN_WRAPPED } from "./fixtures.mjs";
3
+ import {
4
+ BOX_TOO_WIDE,
5
+ BROKEN_PARTS,
6
+ CARD_OVER_NEXT,
7
+ CHROME_BAD,
8
+ CHROME_FINE,
9
+ COLUMN_CUT,
10
+ CONTROLS_FINE,
11
+ CONTROLS_OVERLAP,
12
+ EMPTY_MAIN,
13
+ FINE,
14
+ FOLDED_SECTION,
15
+ FRAME_FITS,
16
+ FRAME_FORCES_SCROLL,
17
+ HIDDEN_CONTENT,
18
+ MAP_COLLAPSED,
19
+ MAP_FINE,
20
+ MAP_HIDDEN,
21
+ MAP_NO_CANVAS,
22
+ SECTIONS_OVERLAP,
23
+ SMALL_ACTION,
24
+ TOKEN_IN_STRETCHED_ROW,
25
+ TOKEN_WRAPPED,
26
+ } from "./fixtures.mjs";
4
27
  import { auditPage, problemsOf } from "./page-audit.mjs";
5
28
  import { auditContext } from "./settle.mjs";
6
29
 
@@ -21,6 +44,12 @@ const EXPECT = [
21
44
  ["a map with no canvas", MAP_NO_CANVAS, /has no canvas/, { maps: true }],
22
45
  ["no map where one is required", FINE, /no map on the page/, { maps: { required: true } }],
23
46
  ["two page footers and no nav", CHROME_BAD, /2 page footers/, { landmarks: "public" }],
47
+ [
48
+ "two controls on top of each other",
49
+ CONTROLS_OVERLAP,
50
+ /controls overlap, one cannot be clicked/,
51
+ { controlOverlaps: true },
52
+ ],
24
53
  ["no nav landmark", CHROME_BAD, /no <nav> landmark/, { landmarks: "app" }],
25
54
  ];
26
55
  const FINES = [
@@ -30,11 +59,23 @@ const FINES = [
30
59
  ["a short token in a row a taller neighbour stretches", TOKEN_IN_STRETCHED_ROW],
31
60
  ];
32
61
  /** Fixtures that must stay clean even with every opt-in check on. */
33
- const ALL = { emptyMain: true, frame: true, minAction: 36, boxes: true, pictures: true, emptyControls: true, hardColors: true, landmarks: "public", maps: true };
62
+ const ALL = {
63
+ emptyMain: true,
64
+ frame: true,
65
+ minAction: 36,
66
+ boxes: true,
67
+ pictures: true,
68
+ emptyControls: true,
69
+ hardColors: true,
70
+ landmarks: "public",
71
+ maps: true,
72
+ controlOverlaps: true,
73
+ };
34
74
  const FINES_STRICT = [
35
75
  ["a frame that fits its screen", FRAME_FITS, { frame: true, emptyMain: false }],
36
76
  ["the family chrome", CHROME_FINE, ALL],
37
77
  ["maps with a size and something drawn", MAP_FINE, { maps: true }],
78
+ ["controls side by side, in a label, in a scroller", CONTROLS_FINE, { controlOverlaps: true }],
38
79
  ["a map that is not drawn (display none)", MAP_HIDDEN, { maps: true }],
39
80
  ];
40
81
 
@@ -57,7 +98,8 @@ export async function selfTest({ launch } = {}) {
57
98
  const page = await (await auditContext(browser, { viewport: { width: 1280, height: 800 } })).newPage();
58
99
  for (const [name, html, pattern, checks] of EXPECT) {
59
100
  const problems = await problemsOn(page, html, checks);
60
- if (!problems.some((p) => pattern.test(p))) failures.push(`the audit does not see ${String(name)}: ${problems.join(" | ") || "no problem reported"}`);
101
+ if (!problems.some((p) => pattern.test(p)))
102
+ failures.push(`the audit does not see ${String(name)}: ${problems.join(" | ") || "no problem reported"}`);
61
103
  }
62
104
  for (const [name, html] of FINES) failures.push(...(await reported(page, name, html)));
63
105
  await page.setViewportSize({ width: 1280, height: 700 });
package/src/settle.mjs CHANGED
@@ -28,7 +28,11 @@ export async function auditContext(browser, options) {
28
28
 
29
29
  /** Resolves once no view transition is running (none starts under reduced motion; this makes the wait explicit). */
30
30
  export const settled = (page) =>
31
- page.waitForFunction(() => !document.activeViewTransition && !document.documentElement.matches(":active-view-transition"), null, { timeout: 10_000 });
31
+ page.waitForFunction(
32
+ () => !document.activeViewTransition && !document.documentElement.matches(":active-view-transition"),
33
+ null,
34
+ { timeout: 10_000 },
35
+ );
32
36
 
33
37
  /**
34
38
  * Loads a page, then waits for the faces and for the transitions: what is read afterwards is final. Returns the response.
package/src/visual.d.ts CHANGED
@@ -1,6 +1,9 @@
1
- import type { Browser,Page } from "playwright-core";
1
+ import type { Browser, Page } from "playwright-core";
2
2
 
3
- export interface Size { width: number; height: number }
3
+ export interface Size {
4
+ width: number;
5
+ height: number;
6
+ }
4
7
  export const VIEWPORTS: Readonly<{ desktop: Size; phone: Size }>;
5
8
  export const VISUAL_THEMES: readonly ["light", "dark"];
6
9
  export const FROZEN_TIME: string;
@@ -8,19 +11,43 @@ export const VISUAL_LOCALE: string;
8
11
  export const VISUAL_TIME_ZONE: string;
9
12
  export const FREEZE_CSS: string;
10
13
  export interface Journey {
11
- name: string; path: string | ((page: Page, o: { origin: string }) => Promise<string>); session?: boolean; mask?: string[]; viewports?: string[];
14
+ name: string;
15
+ path: string | ((page: Page, o: { origin: string }) => Promise<string>);
16
+ session?: boolean;
17
+ mask?: string[];
18
+ viewports?: string[];
12
19
  ready?: string | ((page: Page) => Promise<unknown>);
13
20
  firstPaint?: true | { hold: string[] };
14
21
  }
15
- export interface Cell { viewport: string; size: Size; theme: string; snapshot: string }
16
- export function visualMatrix(journey: Journey, options?: { viewports?: Record<string, Size>; themes?: readonly string[] }): Cell[];
17
- export function visualContextOptions(o: { size: Size; theme: string; javaScript?: boolean; storageState?: unknown }): Record<string, unknown>;
22
+ export interface Cell {
23
+ viewport: string;
24
+ size: Size;
25
+ theme: string;
26
+ snapshot: string;
27
+ }
28
+ export function visualMatrix(
29
+ journey: Journey,
30
+ options?: { viewports?: Record<string, Size>; themes?: readonly string[] },
31
+ ): Cell[];
32
+ export function visualContextOptions(o: {
33
+ size: Size;
34
+ theme: string;
35
+ javaScript?: boolean;
36
+ storageState?: unknown;
37
+ }): Record<string, unknown>;
18
38
  export function initVisualPage(o: { theme: string; themeKey: string }): void;
19
39
  export function prepareVisualPage(page: Page, o?: { theme?: string; themeKey?: string; now?: string }): Promise<void>;
20
40
  export function settleForShot(page: Page, o?: { javaScript?: boolean }): Promise<void>;
21
41
  export function shotOptions(o?: { mask?: string[]; page?: Page }): Record<string, unknown>;
22
42
  export function defineJourneyTests(o: {
23
- test: unknown; expect: unknown; journeys: Journey[]; signIn: (page: Page, o: { base?: string }) => Promise<void>;
24
- base?: string; viewports?: Record<string, Size>; themes?: readonly string[]; themeKey?: string; now?: string;
43
+ test: unknown;
44
+ expect: unknown;
45
+ journeys: Journey[];
46
+ signIn: (page: Page, o: { base?: string }) => Promise<void>;
47
+ base?: string;
48
+ viewports?: Record<string, Size>;
49
+ themes?: readonly string[];
50
+ themeKey?: string;
51
+ now?: string;
25
52
  }): void;
26
53
  export type { Browser };
package/src/visual.mjs CHANGED
@@ -31,7 +31,12 @@ export function visualMatrix(journey, { viewports = VIEWPORTS, themes = VISUAL_T
31
31
  if (!viewports[viewport]) throw new Error(`unknown viewport « ${viewport} » in journey « ${journey.name} »`);
32
32
  for (const theme of themes) {
33
33
  const suffix = journey.firstPaint ? "-first-paint" : "";
34
- cells.push({ viewport, size: viewports[viewport], theme, snapshot: `${journey.name}${suffix}-${viewport}-${theme}.png` });
34
+ cells.push({
35
+ viewport,
36
+ size: viewports[viewport],
37
+ theme,
38
+ snapshot: `${journey.name}${suffix}-${viewport}-${theme}.png`,
39
+ });
35
40
  }
36
41
  }
37
42
  return cells;
@@ -53,7 +58,8 @@ export function visualContextOptions({ size, theme, javaScript = true, storageSt
53
58
  }
54
59
 
55
60
  /** The style every capture carries: nothing moves, nothing blinks. */
56
- export const FREEZE_CSS = "*,*::before,*::after{animation:none!important;transition:none!important;caret-color:transparent!important;scroll-behavior:auto!important}";
61
+ export const FREEZE_CSS =
62
+ "*,*::before,*::after{animation:none!important;transition:none!important;caret-color:transparent!important;scroll-behavior:auto!important}";
57
63
 
58
64
  /** Runs before the page's own scripts: the theme the app reads, no View Transition. */
59
65
  export const initVisualPage = ({ theme, themeKey }) => {
@@ -106,11 +112,23 @@ export function shotOptions({ mask = [], page } = {}) {
106
112
  * now: the instant of the browser clock (default FROZEN_TIME); an app whose seed stamps rows with the day of the run passes that moment.
107
113
  * signIn(page, { base }): signs the persona in; its cookies are kept and reused by every signed-in capture of the worker.
108
114
  */
109
- export function defineJourneyTests({ test, expect, journeys, signIn, base, viewports = VIEWPORTS, themes = VISUAL_THEMES, themeKey = "kz-theme", now = FROZEN_TIME }) {
115
+ export function defineJourneyTests({
116
+ test,
117
+ expect,
118
+ journeys,
119
+ signIn,
120
+ base,
121
+ viewports = VIEWPORTS,
122
+ themes = VISUAL_THEMES,
123
+ themeKey = "kz-theme",
124
+ now = FROZEN_TIME,
125
+ }) {
110
126
  let session;
111
127
  const sessionState = (browser) => {
112
128
  session ??= (async () => {
113
- const context = await browser.newContext(visualContextOptions({ size: viewports.desktop ?? Object.values(viewports)[0], theme: "light" }));
129
+ const context = await browser.newContext(
130
+ visualContextOptions({ size: viewports.desktop ?? Object.values(viewports)[0], theme: "light" }),
131
+ );
114
132
  const page = await context.newPage();
115
133
  await signIn(page, { base });
116
134
  const state = await context.storageState();
@@ -121,17 +139,24 @@ export function defineJourneyTests({ test, expect, journeys, signIn, base, viewp
121
139
  };
122
140
  for (const journey of journeys) {
123
141
  for (const cell of visualMatrix(journey, { viewports, themes })) {
124
- test(`${journey.name} · ${cell.viewport} · ${cell.theme}${journey.firstPaint ? " · first paint" : ""}`, async ({ browser, baseURL }) => {
142
+ test(`${journey.name} · ${cell.viewport} · ${cell.theme}${journey.firstPaint ? " · first paint" : ""}`, async ({
143
+ browser,
144
+ baseURL,
145
+ }) => {
125
146
  const origin = base ?? baseURL;
126
147
  const javaScript = !(journey.firstPaint === true);
127
148
  const storageState = journey.session ? await sessionState(browser) : undefined;
128
- const context = await browser.newContext(visualContextOptions({ size: cell.size, theme: cell.theme, javaScript, storageState }));
149
+ const context = await browser.newContext(
150
+ visualContextOptions({ size: cell.size, theme: cell.theme, javaScript, storageState }),
151
+ );
129
152
  try {
130
153
  const page = await context.newPage();
131
154
  await prepareVisualPage(page, { theme: cell.theme, themeKey, now });
132
155
  for (const pattern of journey.firstPaint?.hold ?? []) await page.route(pattern, () => undefined);
133
156
  const path = typeof journey.path === "function" ? await journey.path(page, { origin }) : journey.path;
134
- await page.goto(new URL(path, origin).toString(), { waitUntil: journey.firstPaint ? "domcontentloaded" : "load" });
157
+ await page.goto(new URL(path, origin).toString(), {
158
+ waitUntil: journey.firstPaint ? "domcontentloaded" : "load",
159
+ });
135
160
  if (typeof journey.ready === "string") await page.locator(journey.ready).first().waitFor();
136
161
  else if (journey.ready) await journey.ready(page);
137
162
  await settleForShot(page, { javaScript });