@krizaka/page-audit
The page audit of every app: overflow, a card out of its panel, overlaps and cut columns, over routes, widths and themes, with a self-test.
Install
npm install @krizaka/page-auditAlso published as subpath exports: @krizaka/page-audit/settle, @krizaka/page-audit/fixtures, @krizaka/page-audit/journey, @krizaka/page-audit/visual.
The page audit of every Krizaka app, in one place (ADR-013: one source per rule set). It measures a rendered page in a real browser:
horizontal overflow, a card out of its panel, sections covering each other, a column cut by a clipping ancestor, top chrome that
overlaps, content cut inside its box, a token wrapped per character, unnamed buttons, empty headings. Closed <details>, aria-hidden
layers, scrollers and fixed elements are not counted. Library code, not UI: it lives in krizaka-ts.
npm i -D @krizaka/page-audit playwright-core
npx krizaka-page-audit --self-test # proves it on its own fixtures
npx krizaka-page-audit --base http://localhost:3000 --routes /en,/fr --widths 360,390,1440 --themes dark,lightimport { auditRoutes, auditContext, loadSettled, resizeSettled, auditPage, problemsOf } from "@krizaka/page-audit";
const problems = await auditRoutes({ base, routes: ["/en", "/fr"], checks: { h1: true } }); // [] when fineauditContext / loadSettled / resizeSettled open a context with reduced motion and no View Transition, and wait for none to run
before anything is measured (yomizaka#354). prepare(page, route) signs in or seeds; launch gives your own browser.
The dev-preview crawls, in one audit (0.3)
Opt-in through checks (nothing changes for a repo that does not ask): status (HTTP 4xx/5xx or no response), emptyMain, frame
(the signed-in frame, [data-app-frame-slot], forces a scroll), minAction (a button-styled action under N px), boxes (a landmark
wider than its box), landmarks ("public" one footer naming Krizaka and a nav, "app", "auth"), pictures, emptyControls,
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,
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.
const routes = await routesFromSitemap(base, { first: ["/", "/docs"], limit: 14 }); // a crawl as the route source (null: nothing answers)
await auditRoutes({
base, routes: [...routes, { url: "/login", chrome: "auth" }, { url: "/chat", session: true, chrome: "app" }],
widths: [{ width: 1440, height: 900 }, { width: 390, height: 844 }],
session: { storageState }, // or { signIn(context, { base, theme }) {…}, routes: async (page) => […] } for pages found once signed in
a11y: { app: "my-app", baseline: "a11y-baseline.json", widths: [390, 1440], session: { app: "my-app-app", baseline: "a11y-baseline.app.json" } },
checks: { status: true, emptyMain: true, minAction: 36, landmarks: "public", frame: true },
});a11y runs axe through @krizaka/a11y-audit (an optional peer) and ratchets with the repo's own baseline file; --a11y-update and
--a11y-init work as there. The CLI takes --sitemap, --status, --empty-main, --min-action, --a11y app:baseline.json.
Visual regression of the journeys (0.4)
@krizaka/page-audit/visual (krizaka/.github#375) compares screenshots of an app's main journeys against Linux baselines, with @playwright/test
as the runner. It freezes what moves (clock, motion, caret, View Transition, locale, time zone, fonts) and leaves the app its routes, its persona and its masks.
import { test, expect } from "@playwright/test";
import { defineJourneyTests } from "@krizaka/page-audit/visual";
defineJourneyTests({
test, expect,
signIn: async (page) => { /* the seeded persona signs in through the form */ },
journeys: [
{ name: "landing", path: "/fr" },
{ name: "home", path: "/fr/app", session: true, mask: ["[data-live]"] },
{ name: "home", path: "/fr/app", session: true, firstPaint: true, viewports: ["phone"] }, // the HTML the server sent
{ name: "home", path: "/fr/app", session: true, firstPaint: { hold: ["**/api/me"] } }, // hydrated, account query pending
],
});Baselines are taken in the official Playwright container by the shared workflows of krizaka/.github (visual-regression.yml to compare,
update-visual-baselines.yml to commit them on a branch); never on a Mac.
The journey kit (0.6)
@krizaka/page-audit/journey is the kit of the customer journeys (ADR-017, krizaka/.github#423). A product writes no copy of it.
- Catalogue:
validateJourneychecks a row ofjourneys/journeys.json(kebab-case id, a promise, a persona of the seeded cast, platforms, steps, a budget of 90 s by default);checkCataloguefails a file without a row, a row without a file, an id twice;journeysByTag(index, "smoke")picks what runs on every pull request;budgetProblemnames a journey over its budget. - Persona sign-in once per worker:
personaSessions({ dir, signIn }).stateFor("eric")signs the persona in on the first call and reuses the browser state after (parallel calls share one sign-in; one file per worker; a failed sign-in is not remembered). - Seeded cast:
seededPersona(cast, "eric")reads a member or names who exists. - Masks:
journeyMaskSelectors({ extra })=time,[data-mask],[data-date],[data-testid$="-id"],[data-run-id]and the journey's own. - Video hooks:
videoContextOptions(journey, { dir })records only the journeys withvideo: true;videoFile(journey, "web")names the file. - Flake policy:
assertNoRetries(playwrightConfig)refuses retries above 0 (also per project);quarantine(entries)holds a named test with its reason and its issue, capped at one.
Exports
The 14 names the package exports, read from its types.
| Name | Kind | What it is |
|---|---|---|
auditContext | function | A browser context for an audit (reduced motion emulated, no View Transition). |
auditOptions | function | The options of an audit's browser context: the caller's, plus reduced motion. |
auditPage | constant | The expression Playwright runs in the page (page.evaluate(auditPage)): the probes, serialised together, then the measure. |
auditRoutes | function | |
isPageError | function | A blocked third-party resource is not a page error; a script error and a hydration mismatch are. |
loadSettled | function | Loads a page, then waits for the faces and for the transitions: what is read afterwards is final. |
problemsOf | function | |
resizeSettled | function | Resizes the viewport, then waits for the transitions a resize can start. |
routesFromSitemap | function | A route source that is a crawl: the paths of the site's own sitemap (a page added there is audited), the first ones always first, at most limit. |
selfTest | function | |
settled | function | Resolves once no view transition is running (none starts under reduced motion; this makes the wait explicit). |
THEMES | constant | |
WIDTHS | constant | |
withoutViewTransitions | function | Runs before any page script: the browser offers no View Transition, so PageTransition (@krizaka/ui) changes the page at once. |
@krizaka/page-audit/settle
| Name | Kind | What it is |
|---|---|---|
auditContext | function | A browser context for an audit (reduced motion emulated, no View Transition). |
auditOptions | function | The options of an audit's browser context: the caller's, plus reduced motion. |
loadSettled | function | Loads a page, then waits for the faces and for the transitions: what is read afterwards is final. |
resizeSettled | function | Resizes the viewport, then waits for the transitions a resize can start. |
settled | function | Resolves once no view transition is running (none starts under reduced motion; this makes the wait explicit). |
withoutViewTransitions | function | Runs before any page script: the browser offers no View Transition, so PageTransition (@krizaka/ui) changes the page at once. |
@krizaka/page-audit/fixtures
| Name | Kind | What it is |
|---|---|---|
BOX_TOO_WIDE | constant | A landmark whose content is wider than its box. |
BROKEN_PARTS | constant | Broken picture, a tile that draws nothing, an inline hard-coded colour. |
CARD_OVER_NEXT | constant | The binder as it was: the open folder's card is as tall as its grid row (a fixed height) and runs over the next section. |
CHROME_BAD | constant | |
CHROME_FINE | constant | Chrome that fits the family: one footer naming Krizaka, a nav. |
COLUMN_CUT | constant | A two-column layout whose wrapper clips the right column. |
EMPTY_MAIN | constant | A page whose <main> draws nothing. |
FINE | constant | What it should be: the card follows its content, the columns fit, a wide table scrolls inside its own scroller. |
FOLDED_SECTION | constant | The report tab: a section inside a folded details (the closing note) beside sections it must not be taken to cover. |
FRAME_FITS | constant | The same frame, sized to what is left of the screen. |
FRAME_FORCES_SCROLL | constant | The signed-in frame asks 1000 px of a 700 px screen under a 60 px notice (akinaza#444). |
HIDDEN_CONTENT | constant | Not drawn, so not a problem: a chart's text version in a visually hidden wrapper, and the content of a folded <details> (the browser still gives both a box). |
MAP_COLLAPSED | constant | A map that laid out at 0 px: absolute inset-0 in a frame with no height of its own (akinaza#488). |
MAP_FINE | constant | A map box with a size and a canvas, and a Leaflet one with a loaded tile. |
MAP_HIDDEN | constant | A map that is not drawn (display none: a closed tab, a small screen): nothing to measure. |
MAP_NO_CANVAS | constant | A GL map box of a good size where nothing was drawn (no canvas). |
SECTIONS_OVERLAP | constant | Two sections laid out on top of each other. |
SMALL_ACTION | constant | A button-styled action 24 px tall, under the 36 px tap target. |
TOKEN_IN_STRETCHED_ROW | constant | A short token in a table row stretched by a tall neighbour: the cell is tall, its text is one line. |
TOKEN_WRAPPED | constant | An identifier in a narrow cell that breaks every few characters. |
@krizaka/page-audit/journey
| Name | Kind | What it is |
|---|---|---|
assertNoRetries | function | A flake is a bug, not a retry: retries are 0 (top level and every project). |
budgetProblem | function | The message when a run took longer than its journey's budget, undefined within it. |
checkCatalogue | function | Files and rows must match: a file with no row, a row with no file, an id twice. |
DEFAULT_BUDGET_SECONDS | constant | |
JOURNEY_MASKS | constant | What changes between two runs of the same build: dates, ids, anything marked data-mask. |
journeyMaskSelectors | function | The selectors to mask in a capture of a journey screen. |
journeysByTag | function | The rows that carry a tag (smoke runs on every pull request, the rest in the nightly pass). |
personaSessions | function | Persona sign-in once per worker. |
quarantine | function | The quarantine: a named test, why, and the issue that fixes it. |
QUARANTINE_CAP | constant | |
seededPersona | function | A member of the seeded cast (the same people locally and deployed), or an error naming who exists. |
validateJourney | function | The problems of one catalogue row (journeys/journeys.json), empty when it is a good journey. |
VIDEO_SIZE | constant | The size of a recorded video: the laptop of the visual matrix. |
videoContextOptions | function | Context options that record the run of a journey whose video is true (the source of the product video), none otherwise. |
videoFile | function | The name the recorded video is published under. |
@krizaka/page-audit/visual
| Name | Kind | What it is |
|---|---|---|
defineJourneyTests | function | Registers one test per journey × viewport × theme. |
FREEZE_CSS | constant | The style every capture carries: nothing moves, nothing blinks. |
FROZEN_TIME | constant | The clock of every capture (a Thursday, in winter: no daylight-saving edge). |
initVisualPage | function | Runs before the page's own scripts: the theme the app reads, no View Transition. |
prepareVisualPage | function | Prepares a page: fixed time, the theme key, no View Transition. |
settleForShot | function | Waits until what is on screen is final: the faces, the images, the transitions. |
shotOptions | function | The options of toHaveScreenshot: a small tolerance for anti-aliasing, never for a missing element. |
VIEWPORTS | constant | The two sizes of every journey: a laptop and a phone. |
VISUAL_LOCALE | constant | |
VISUAL_THEMES | constant | |
VISUAL_TIME_ZONE | constant | |
visualContextOptions | function | The options of a browser context for a capture. |
visualMatrix | function | The viewport × theme cells of a journey, with the snapshot name of each (<journey>-<viewport>-<theme>.png). |
Package
| Version on npm | 0.6.0 (npm) |
| Licence | Apache-2.0 |
| Node.js | >=22 |
| Dependencies | none |
| Peer dependencies | @krizaka/a11y-audit >=0.2.0, playwright-core >=1.40 |
| Source | page-audit |
Changelog
0.6.0
Minor changes
@krizaka/page-audit/journey, the shared journey kit of ADR-017: the catalogue row and its check (validateJourney,checkCatalogue,journeysByTag,budgetProblem), a persona signed in once per worker (personaSessions), the seeded cast (seededPersona), the masks of dates and ids (journeyMaskSelectors), the video hooks of journeys that feed a product video (videoContextOptions,videoFile) and the flake policy (assertNoRetries, a cappedquarantine).
0.5.1
Patch changes
@krizaka/page-audit/visual: a first-paint capture (JavaScript off) no longer hangs for the test timeout:settleForShotdoes not inject the freeze style (Playwright cannot run a script in such a page);animations: "disabled"still freezes CSS animations.
0.5.0
Minor changes
@krizaka/page-audit/visual: visual regression of the main journeys, one source for every app (krizaka/.github#375).defineJourneyTestsregisters one PlaywrighttoHaveScreenshottest per journey × viewport (desktop, phone) × theme (light, dark) with a frozen clock, reduced motion, no View Transition, a fixed locale and time zone, masks for live values, a signed-in persona reused across captures, and a first-paint capture (JavaScript off, or a query held pending) for flashes like krizaka-ui#827.
0.4.0
Minor changes
mapscheck: an empty map fails the audit (a GL / Leaflet /[data-kz-map]box under 120 px on a side, a GL map with no canvas, a Leaflet map with no loaded tile;maps: { required: true }also fails a page that should draw one). Proven on its own fixtures in the self-test (krizaka-ts#199, akinaza#488).
0.3.1
Patch changes
- A token in a table cell is not « wrapped one character per line » because a taller neighbour stretches its row: the check measures the text's own lines, not the cell's box.
0.3.0
Minor changes
- The union of the dev-preview crawls (krizaka-ui#747): opt-in HTTP status, empty
<main>, signed-in frame forced scroll, minimum action height, landmark wider than its box, page chrome (footer, nav), broken pictures, empty tiles and hard-coded inline colours; an optional axe step through@krizaka/a11y-auditwith the repo's own baseline (the ratchet stays per repo); a crawl route source (routesFromSitemap), per-routechromeandsession(stored state or sign-in), per-viewport heights,ignoreConsoleandattempts.
0.2.0
Minor changes
- New package: the page audit of every Krizaka app in one place (overflow, containment, overlap, cut columns, top chrome overlaps, closed
<details>skipped, View Transitions neutralised,auditContext/settle), anauditRoutesAPI over routes x widths x themes, akrizaka-page-auditbin with a--self-teston fixtures. Ported from yomizaka with its tests (krizaka-ui#747).
@krizaka/architecture
The C4 view of a repository generated from its code: containers, components, the imports between them and the external services, as JSON and text, on a local checkout.
@krizaka/config
The quality kit of every TypeScript repository: ESLint configs, the dependency-cruiser preset, tsconfig bases, Prettier and the debt ratchet.