@krizaka/page-audit 0.3.0 → 0.4.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 +2 -1
- package/package.json +1 -1
- package/src/fixtures.mjs +14 -0
- package/src/index.d.ts +2 -0
- package/src/page-audit.mjs +41 -5
- package/src/self-test.mjs +9 -2
package/README.md
CHANGED
|
@@ -24,7 +24,8 @@ before anything is measured (yomizaka#354). `prepare(page, route)` signs in or s
|
|
|
24
24
|
Opt-in through `checks` (nothing changes for a repo that does not ask): `status` (HTTP 4xx/5xx or no response), `emptyMain`, `frame`
|
|
25
25
|
(the signed-in frame, `[data-app-frame-slot]`, forces a scroll), `minAction` (a button-styled action under N px), `boxes` (a landmark
|
|
26
26
|
wider than its box), `landmarks` (`"public"` one footer naming Krizaka and a nav, `"app"`, `"auth"`), `pictures`, `emptyControls`,
|
|
27
|
-
`hardColors
|
|
27
|
+
`hardColors`, `maps` (an empty map, akinaza#488: a GL / Leaflet / `[data-kz-map]` box under 120 px on a side, a GL map with no canvas,
|
|
28
|
+
a Leaflet map with no loaded tile; `maps: { required: true }` also fails a page that should draw one but has none). Console noise the app tolerates: `ignoreConsole: [/…/]` (`[]` counts every error); `attempts` retries a route.
|
|
28
29
|
|
|
29
30
|
```js
|
|
30
31
|
const routes = await routesFromSitemap(base, { first: ["/", "/docs"], limit: 14 }); // a crawl as the route source (null: nothing answers)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krizaka/page-audit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "The page audit of every Krizaka app, in one place: HTTP status, empty main, horizontal overflow, a card out of its panel, sections covering each other, a column cut off, top chrome overlaps, unnamed buttons, empty headings. Runs in a real browser (Playwright) over a route list, viewports and themes, with View Transitions neutralised and a self-test on its own fixtures.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"krizaka",
|
package/src/fixtures.mjs
CHANGED
|
@@ -43,3 +43,17 @@ export const BROKEN_PARTS = page(`<img src="data:image/png;base64,AAAA" width="4
|
|
|
43
43
|
/** Chrome that fits the family: one footer naming Krizaka, a nav. */
|
|
44
44
|
export const CHROME_FINE = `<!doctype html><html><body><nav>n</nav><main><p>Hello</p></main><footer>A Krizaka product</footer></body></html>`;
|
|
45
45
|
export const CHROME_BAD = `<!doctype html><html><body><main><p>Hello</p></main><footer>one</footer><footer>two</footer></body></html>`;
|
|
46
|
+
|
|
47
|
+
/** A short token in a table row stretched by a tall neighbour: the cell is tall, its text is one line. */
|
|
48
|
+
export const TOKEN_IN_STRETCHED_ROW = page(`<table style="width:600px"><tbody><tr><td>orazaka-media-pack</td><td>${"A long description. ".repeat(30)}</td></tr></tbody></table>`);
|
|
49
|
+
/** An identifier in a narrow cell that breaks every few characters. */
|
|
50
|
+
export const TOKEN_WRAPPED = page(`<table style="width:100px;table-layout:fixed"><tbody><tr><td style="word-break:break-all;font-size:20px">abcdefghijklmnopqrstuvwxyz0123456789</td></tr></tbody></table>`);
|
|
51
|
+
|
|
52
|
+
/** A map that laid out at 0 px: `absolute inset-0` in a frame with no height of its own (akinaza#488). */
|
|
53
|
+
export const MAP_COLLAPSED = page(`<section><div style="position:relative"><div class="maplibregl-map" style="position:absolute;inset:0"><canvas width="0" height="0"></canvas></div></div></section>`);
|
|
54
|
+
/** A map box with a size and a canvas, and a Leaflet one with a loaded tile. */
|
|
55
|
+
export const MAP_FINE = page(`<section><div class="maplibregl-map" style="height:300px;width:600px"><canvas style="width:600px;height:300px" width="600" height="300"></canvas></div><div class="leaflet-container" style="height:300px;width:600px"><img class="leaflet-tile-loaded" alt="" width="256" height="256"></div></section>`);
|
|
56
|
+
/** A GL map box of a good size where nothing was drawn (no canvas). */
|
|
57
|
+
export const MAP_NO_CANVAS = page(`<section><div class="maplibregl-map" style="height:300px;width:600px"></div></section>`);
|
|
58
|
+
/** A map that is not drawn (display none: a closed tab, a small screen): nothing to measure. */
|
|
59
|
+
export const MAP_HIDDEN = page(`<section><div class="maplibregl-map" style="display:none"></div></section>`);
|
package/src/index.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface ProblemChecks {
|
|
|
9
9
|
/** HTTP status of the load (`null`: no response); a 4xx/5xx is a problem. */
|
|
10
10
|
status?: number | null;
|
|
11
11
|
emptyMain?: boolean; frame?: boolean; minAction?: number; boxes?: boolean; landmarks?: Chrome; pictures?: boolean; emptyControls?: boolean; hardColors?: boolean;
|
|
12
|
+
/** Empty maps (a GL / Leaflet / `[data-kz-map]` box under 120 px, no canvas, no loaded tile); `{ min, required }` to tune, `required` fails a page with no map. */
|
|
13
|
+
maps?: boolean | { min?: number; required?: boolean };
|
|
12
14
|
}
|
|
13
15
|
export function problemsOf(where: string, result: Record<string, unknown>, checks?: ProblemChecks): string[];
|
|
14
16
|
export function auditOptions<T extends object>(options?: T): T & { reducedMotion: "reduce" };
|
package/src/page-audit.mjs
CHANGED
|
@@ -164,14 +164,15 @@ function clipsOf() {
|
|
|
164
164
|
.map(nameOf);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
/** A long token (an id, an address) that wraps character by character: a cell taller
|
|
167
|
+
/** A long token (an id, an address) that wraps character by character: text without a space whose own lines (not the cell's, which a taller neighbour in the row stretches) are more than two and a half. */
|
|
168
168
|
function tooTall(el) {
|
|
169
169
|
const text = (el.textContent || "").trim();
|
|
170
170
|
if (text.length < 12 || /\s/.test(text) || el.children.length > 1) return false;
|
|
171
171
|
const cs = getComputedStyle(el);
|
|
172
172
|
const lh = Number.parseFloat(cs.lineHeight) || Number.parseFloat(cs.fontSize) * 1.4;
|
|
173
|
-
const
|
|
174
|
-
|
|
173
|
+
const range = document.createRange();
|
|
174
|
+
range.selectNodeContents(el);
|
|
175
|
+
return range.getBoundingClientRect().height > lh * 2.5;
|
|
175
176
|
}
|
|
176
177
|
|
|
177
178
|
function wrappedOf() {
|
|
@@ -260,6 +261,21 @@ function hardColorsOf() {
|
|
|
260
261
|
return [...document.querySelectorAll("[style]")].filter((e) => /#[0-9a-f]{3,8}\b|rgba?\(/i.test(e.getAttribute("style") || "") && !/var\(/.test(e.getAttribute("style") || "")).length;
|
|
261
262
|
}
|
|
262
263
|
|
|
264
|
+
/**
|
|
265
|
+
* The live maps of the page (maplibre / mapbox GL, Leaflet, or anything marked `[data-kz-map]`): a map is a box with a size and something drawn in it.
|
|
266
|
+
* One that lays out at 0 px (an `absolute inset-0` map in a frame with no height of its own) overflows nothing, so no other probe sees it.
|
|
267
|
+
* GL maps are told by their canvas; Leaflet by its loaded tiles. A map with no client rect (`display: none`, a closed tab) is not drawn, so not measured.
|
|
268
|
+
*/
|
|
269
|
+
function mapsOf() {
|
|
270
|
+
return [...document.querySelectorAll(".maplibregl-map, .mapboxgl-map, .leaflet-container, [data-kz-map]")]
|
|
271
|
+
.filter((el) => el.getClientRects().length > 0 && !el.closest(ARIA_HIDDEN) && !folded(el) && !el.parentElement?.closest(".maplibregl-map, .mapboxgl-map, .leaflet-container, [data-kz-map]"))
|
|
272
|
+
.map((el) => {
|
|
273
|
+
const box = el.getBoundingClientRect();
|
|
274
|
+
const canvas = el.querySelector("canvas")?.getBoundingClientRect();
|
|
275
|
+
return { name: nameOf(el), width: Math.round(box.width), height: Math.round(box.height), canvas: !!canvas && canvas.width > 0 && canvas.height > 0, leaflet: el.classList.contains("leaflet-container"), tiles: el.querySelectorAll(".leaflet-tile-loaded").length };
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
263
279
|
function measurePage() {
|
|
264
280
|
const doc = document.documentElement;
|
|
265
281
|
const panels = [...document.querySelectorAll("[data-panel], main section, main [role=region]")].filter((el) => !el.closest(ARIA_HIDDEN) && !folded(el));
|
|
@@ -289,11 +305,12 @@ function measurePage() {
|
|
|
289
305
|
brokenPictures: brokenPicturesOf(),
|
|
290
306
|
emptyControls: emptyControlsOf(),
|
|
291
307
|
hardColors: hardColorsOf(),
|
|
308
|
+
maps: mapsOf(),
|
|
292
309
|
segmentError: !!document.querySelector('[data-testid="segment-error"]'),
|
|
293
310
|
};
|
|
294
311
|
}
|
|
295
312
|
|
|
296
|
-
const PROBES = [tooTall, wrappedOf, className, nameOf, scrolls, meets, apart, clipped, wideOf, named, unnamedOf, headerActionOf, folded, keptBetween, notDrawn, runsOut, spillOf, overlapsOf, cutBy, cutColumnsOf, topBoxOf, isTopBar, chromeOverlapsOf, clipsOf, emptyHeadingsOf, unlabelledFilesOf, emptyMainOf, actionsOf, frameOf, boxesOf, landmarksOf, brokenPicturesOf, emptyControlsOf, hardColorsOf, measurePage];
|
|
313
|
+
const PROBES = [tooTall, wrappedOf, className, nameOf, scrolls, meets, apart, clipped, wideOf, named, unnamedOf, headerActionOf, folded, keptBetween, notDrawn, runsOut, spillOf, overlapsOf, cutBy, cutColumnsOf, topBoxOf, isTopBar, chromeOverlapsOf, clipsOf, emptyHeadingsOf, unlabelledFilesOf, emptyMainOf, actionsOf, frameOf, boxesOf, landmarksOf, brokenPicturesOf, emptyControlsOf, hardColorsOf, mapsOf, measurePage];
|
|
297
314
|
|
|
298
315
|
/** The expression Playwright runs in the page (`page.evaluate(auditPage)`): the probes, serialised together, then the measure. */
|
|
299
316
|
export const auditPage = `(() => {\nconst ARIA_HIDDEN = ${JSON.stringify(ARIA_HIDDEN)};\n${PROBES.map((f) => `const ${f.name} = ${f.toString()};`).join("\n")}\nreturn measurePage();\n})()`;
|
|
@@ -310,7 +327,7 @@ function accessibilityProblems(where, result, fileLabels) {
|
|
|
310
327
|
return problems;
|
|
311
328
|
}
|
|
312
329
|
|
|
313
|
-
export function problemsOf(where, result, { errors = [], h1 = false, headerAction = false, fileLabels = false, status, emptyMain = false, frame = false, minAction, boxes = false, landmarks, pictures = false, emptyControls = false, hardColors = false } = {}) {
|
|
330
|
+
export function problemsOf(where, result, { errors = [], h1 = false, headerAction = false, fileLabels = false, status, emptyMain = false, frame = false, minAction, boxes = false, landmarks, pictures = false, emptyControls = false, hardColors = false, maps = false } = {}) {
|
|
314
331
|
return [
|
|
315
332
|
...statusProblems(where, status),
|
|
316
333
|
...(emptyMain && result.emptyMain ? [`${where}: empty <main>`] : []),
|
|
@@ -321,6 +338,7 @@ export function problemsOf(where, result, { errors = [], h1 = false, headerActio
|
|
|
321
338
|
...(pictures && result.brokenPictures?.length ? [`${where}: broken picture ${result.brokenPictures.join(", ")}`] : []),
|
|
322
339
|
...(emptyControls ? (result.emptyControls ?? []).map((c) => `${where}: ${c}`) : []),
|
|
323
340
|
...(hardColors && result.hardColors ? [`${where}: ${result.hardColors} element(s) with a hard-coded colour in an inline style`] : []),
|
|
341
|
+
...(maps ? mapProblems(where, result.maps ?? [], maps) : []),
|
|
324
342
|
...errorsOf(where, errors),
|
|
325
343
|
...layoutProblems(where, result),
|
|
326
344
|
...(h1 ? namingProblems(where, result) : []),
|
|
@@ -330,6 +348,24 @@ export function problemsOf(where, result, { errors = [], h1 = false, headerActio
|
|
|
330
348
|
];
|
|
331
349
|
}
|
|
332
350
|
|
|
351
|
+
/** The smallest side (px) of a map worth the name. */
|
|
352
|
+
export const MIN_MAP_SIDE = 120;
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* An empty map (akinaza#488): a box under `min` px on a side, a GL map with no canvas, a Leaflet map with no loaded tile.
|
|
356
|
+
* `maps` is `true` (the default minimum) or `{ min, required }`; with `required`, a page that was expected to draw a map and has none is a problem too.
|
|
357
|
+
*/
|
|
358
|
+
function mapProblems(where, found, option) {
|
|
359
|
+
const { min = MIN_MAP_SIDE, required = false } = option === true ? {} : option;
|
|
360
|
+
if (!found.length) return required ? [`${where}: no map on the page`] : [];
|
|
361
|
+
const out = [];
|
|
362
|
+
for (const m of found) {
|
|
363
|
+
if (m.width < min || m.height < min) out.push(`${where}: the map ${m.name} is ${m.width}x${m.height} px (empty)`);
|
|
364
|
+
else if (m.leaflet ? m.tiles === 0 : !m.canvas) out.push(`${where}: the map ${m.name} ${m.leaflet ? "has no loaded tile" : "has no canvas"}`);
|
|
365
|
+
}
|
|
366
|
+
return out;
|
|
367
|
+
}
|
|
368
|
+
|
|
333
369
|
function errorsOf(where, errors) {
|
|
334
370
|
return errors.length ? [`${where}: console error — ${errors[0]}`] : [];
|
|
335
371
|
}
|
package/src/self-test.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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, SECTIONS_OVERLAP, SMALL_ACTION } from "./fixtures.mjs";
|
|
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";
|
|
4
4
|
import { auditPage, problemsOf } from "./page-audit.mjs";
|
|
5
5
|
import { auditContext } from "./settle.mjs";
|
|
6
6
|
|
|
@@ -9,6 +9,7 @@ const EXPECT = [
|
|
|
9
9
|
["a card over the next section", CARD_OVER_NEXT, /covers a neighbouring section/],
|
|
10
10
|
["two sections on top of each other", SECTIONS_OVERLAP, /covers a neighbouring section/],
|
|
11
11
|
["a column cut by a clipping wrapper", COLUMN_CUT, /is cut off/],
|
|
12
|
+
["an identifier that breaks every few characters", TOKEN_WRAPPED, /wraps one character per line/],
|
|
12
13
|
["an empty <main>", EMPTY_MAIN, /empty <main>/, { emptyMain: true }],
|
|
13
14
|
["a frame that forces a scroll", FRAME_FORCES_SCROLL, /the frame forces a scroll/, { frame: true }],
|
|
14
15
|
["an action under the tap target", SMALL_ACTION, /an action is 24 px tall \(< 36\)/, { minAction: 36 }],
|
|
@@ -16,6 +17,9 @@ const EXPECT = [
|
|
|
16
17
|
["a broken picture", BROKEN_PARTS, /broken picture/, { pictures: true }],
|
|
17
18
|
["a tile that draws nothing", BROKEN_PARTS, /empty tile/, { emptyControls: true }],
|
|
18
19
|
["a hard-coded inline colour", BROKEN_PARTS, /hard-coded colour/, { hardColors: true }],
|
|
20
|
+
["a map that laid out at 0 px", MAP_COLLAPSED, /the map .* is \d+x0 px \(empty\)/, { maps: true }],
|
|
21
|
+
["a map with no canvas", MAP_NO_CANVAS, /has no canvas/, { maps: true }],
|
|
22
|
+
["no map where one is required", FINE, /no map on the page/, { maps: { required: true } }],
|
|
19
23
|
["two page footers and no nav", CHROME_BAD, /2 page footers/, { landmarks: "public" }],
|
|
20
24
|
["no nav landmark", CHROME_BAD, /no <nav> landmark/, { landmarks: "app" }],
|
|
21
25
|
];
|
|
@@ -23,12 +27,15 @@ const FINES = [
|
|
|
23
27
|
["a fine layout", FINE],
|
|
24
28
|
["content that is not drawn (a visually hidden table, a folded details)", HIDDEN_CONTENT],
|
|
25
29
|
["a section folded in a details", FOLDED_SECTION],
|
|
30
|
+
["a short token in a row a taller neighbour stretches", TOKEN_IN_STRETCHED_ROW],
|
|
26
31
|
];
|
|
27
32
|
/** Fixtures that must stay clean even with every opt-in check on. */
|
|
28
|
-
const ALL = { emptyMain: true, frame: true, minAction: 36, boxes: true, pictures: true, emptyControls: true, hardColors: true, landmarks: "public" };
|
|
33
|
+
const ALL = { emptyMain: true, frame: true, minAction: 36, boxes: true, pictures: true, emptyControls: true, hardColors: true, landmarks: "public", maps: true };
|
|
29
34
|
const FINES_STRICT = [
|
|
30
35
|
["a frame that fits its screen", FRAME_FITS, { frame: true, emptyMain: false }],
|
|
31
36
|
["the family chrome", CHROME_FINE, ALL],
|
|
37
|
+
["maps with a size and something drawn", MAP_FINE, { maps: true }],
|
|
38
|
+
["a map that is not drawn (display none)", MAP_HIDDEN, { maps: true }],
|
|
32
39
|
];
|
|
33
40
|
|
|
34
41
|
async function problemsOn(page, html, checks) {
|