@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/README.md +12 -1
- package/package.json +6 -2
- package/src/cli.mjs +18 -2
- package/src/fixtures.mjs +38 -10
- package/src/index.d.ts +50 -9
- package/src/index.mjs +1 -1
- package/src/journey.d.ts +54 -0
- package/src/journey.mjs +162 -0
- package/src/page-audit.mjs +264 -46
- package/src/run.mjs +66 -11
- package/src/self-test.mjs +45 -3
- package/src/settle.mjs +5 -1
- package/src/visual.d.ts +35 -8
- package/src/visual.mjs +32 -7
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 {
|
|
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 = {
|
|
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)))
|
|
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(
|
|
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 {
|
|
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;
|
|
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 {
|
|
16
|
-
|
|
17
|
-
|
|
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;
|
|
24
|
-
|
|
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({
|
|
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 =
|
|
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({
|
|
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(
|
|
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 ({
|
|
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(
|
|
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(), {
|
|
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 });
|