@keboola/validate-ui 0.5.3 → 0.6.1
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/AGENTS.md +1 -0
- package/CHANGELOG.md +23 -0
- package/README.md +11 -2
- package/dist/cli.cjs +9 -4
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +9 -4
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +6 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +67 -53
- package/dist/index.d.ts +67 -53
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/{runner-CEdpbEON.cjs → runner-BwEjSueH.cjs} +153 -91
- package/dist/runner-BwEjSueH.cjs.map +1 -0
- package/dist/{runner-CLpaiCIG.js → runner-DH04MitR.js} +124 -92
- package/dist/runner-DH04MitR.js.map +1 -0
- package/package.json +9 -8
- package/dist/runner-CEdpbEON.cjs.map +0 -1
- package/dist/runner-CLpaiCIG.js.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"runner-CLpaiCIG.js","names":["DEFAULT_TIMEOUT_MS","toFinding","skip"],"sources":["../src/prepare-screenshot.ts","../src/capture.ts","../src/serve.ts","../src/axes/clipped-text.ts","../src/axes/accessibility.ts","../src/axes/vlm-provider.ts","../src/axes/brief-conformance.ts","../src/compare/diff.ts","../src/compare/regions.ts","../src/compare/types.ts","../src/compare/snapshot.ts","../src/axes/compare.ts","../src/axes/runtime-health.ts","../src/axes/visual-brand.ts","../src/axes/index.ts","../src/aggregate.ts","../src/compare/dedup.ts","../src/compare/runner.ts"],"sourcesContent":["import type { Page } from 'playwright';\n\n// Ported from @keboola/e2e-testing/src/visual-helpers.ts. That package is\n// workspace-internal (private), so validate-ui reimplements the small\n// deterministic-screenshot routine rather than depending on it.\n\nconst disableAnimations = async (page: Page): Promise<void> => {\n await page.addStyleTag({\n content: `*, *::before, *::after {\n animation-duration: 0s !important;\n animation-delay: 0s !important;\n transition-duration: 0s !important;\n transition-delay: 0s !important;\n caret-color: transparent !important;\n }`,\n });\n};\n\nconst parkMouseOffElements = async (page: Page): Promise<void> => {\n await page.mouse.move(0, 0);\n};\n\nconst waitForFonts = async (page: Page): Promise<void> => {\n await page.evaluate(() => document.fonts.ready.then(() => undefined));\n};\n\n/**\n * Settle the page before a screenshot: freeze animations first (so parking the\n * mouse can't trigger a transition), park the cursor off any element, and wait\n * for web fonts. Deterministic output makes visual diffs meaningful.\n */\nexport const prepareForScreenshot = async (page: Page): Promise<void> => {\n await disableAnimations(page);\n await parkMouseOffElements(page);\n await waitForFonts(page);\n};\n","import { type Browser, type BrowserContext, chromium, type Page } from 'playwright';\n\nimport { prepareForScreenshot } from './prepare-screenshot';\nimport type { CaptureArtifact, ConsoleEntry, NetworkEntry, Viewport } from './types';\n\nexport const DESKTOP_VIEWPORT: Viewport = { label: 'desktop', width: 1280, height: 800 };\nexport const MOBILE_VIEWPORT: Viewport = { label: 'mobile', width: 390, height: 844 };\n\nexport const DEFAULT_VIEWPORTS: Viewport[] = [DESKTOP_VIEWPORT, MOBILE_VIEWPORT];\n\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\nconst attachRecorders = (page: Page): { console: ConsoleEntry[]; network: NetworkEntry[] } => {\n const consoleEntries: ConsoleEntry[] = [];\n const networkEntries: NetworkEntry[] = [];\n\n page.on('console', (message) => {\n consoleEntries.push({\n type: message.type(),\n text: message.text(),\n location: message.location().url || undefined,\n });\n });\n page.on('pageerror', (error) => {\n consoleEntries.push({ type: 'pageerror', text: error.message });\n });\n page.on('requestfailed', (request) => {\n networkEntries.push({\n url: request.url(),\n method: request.method(),\n status: 0,\n failure: request.failure()?.errorText,\n });\n });\n page.on('response', (response) => {\n networkEntries.push({\n url: response.url(),\n method: response.request().method(),\n status: response.status(),\n });\n });\n\n return { console: consoleEntries, network: networkEntries };\n};\n\n/**\n * Render `url` in an already-open page at `viewport` and capture ground truth.\n * Recorders are attached before navigation so nothing is missed. The caller\n * owns the page lifecycle — this is the building block {@link capture} and the\n * orchestrator reuse.\n */\nexport const capturePage = async (\n page: Page,\n url: string,\n viewport: Viewport,\n timeoutMs: number = DEFAULT_TIMEOUT_MS,\n): Promise<CaptureArtifact> => {\n const recorders = attachRecorders(page);\n await page.setViewportSize({ width: viewport.width, height: viewport.height });\n await page.goto(url, { waitUntil: 'networkidle', timeout: timeoutMs });\n await prepareForScreenshot(page);\n const screenshot = await page.screenshot({ fullPage: true });\n const dom = await page.content();\n const renderedText = (await page.evaluate(() => document.body.innerText)).trim();\n return {\n url,\n viewport,\n screenshot,\n dom,\n renderedText,\n console: recorders.console,\n network: recorders.network,\n };\n};\n\nexport type CaptureOptions = {\n viewports?: Viewport[];\n /** Reuse an existing browser instead of launching (and closing) one. */\n browser?: Browser;\n timeoutMs?: number;\n};\n\n/**\n * Launch a headless browser (unless one is supplied), render `url` at each\n * viewport in a fresh context, and return one artifact per viewport.\n */\nexport const capture = async (\n url: string,\n options: CaptureOptions = {},\n): Promise<CaptureArtifact[]> => {\n const viewports = options.viewports ?? DEFAULT_VIEWPORTS;\n const browser = options.browser ?? (await chromium.launch());\n const ownsBrowser = !options.browser;\n try {\n const artifacts: CaptureArtifact[] = [];\n for (const viewport of viewports) {\n const context: BrowserContext = await browser.newContext({\n viewport: { width: viewport.width, height: viewport.height },\n });\n const page = await context.newPage();\n try {\n artifacts.push(await capturePage(page, url, viewport, options.timeoutMs));\n } finally {\n await context.close();\n }\n }\n return artifacts;\n } finally {\n if (ownsBrowser) await browser.close();\n }\n};\n","import { createReadStream } from 'node:fs';\nimport { stat } from 'node:fs/promises';\nimport { createServer, type Server } from 'node:http';\nimport { extname, join, normalize, resolve, sep } from 'node:path';\n\nconst CONTENT_TYPES: Record<string, string> = {\n '.html': 'text/html; charset=utf-8',\n '.js': 'text/javascript; charset=utf-8',\n '.css': 'text/css; charset=utf-8',\n '.json': 'application/json; charset=utf-8',\n '.svg': 'image/svg+xml',\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.woff2': 'font/woff2',\n '.woff': 'font/woff',\n};\n\nexport type StaticServer = {\n url: string;\n close: () => Promise<void>;\n};\n\n/**\n * Serve a built SPA directory (e.g. `apps/boilerplate/dist`) on an ephemeral\n * port with client-side-routing fallback to `index.html`, so a validator can\n * render any client route without a dev server. Node stdlib only — no runtime\n * dependency added.\n */\nexport const serveStatic = async (rootDir: string): Promise<StaticServer> => {\n const indexPath = join(rootDir, 'index.html');\n\n const rootResolved = resolve(rootDir);\n\n const server: Server = createServer((request, response) => {\n const send = (filePath: string, status = 200): void => {\n response.writeHead(status, {\n 'content-type': CONTENT_TYPES[extname(filePath)] ?? 'application/octet-stream',\n });\n const stream = createReadStream(filePath);\n stream.on('error', (error) => {\n console.error(`serveStatic: failed to read ${filePath}:`, error);\n if (!response.headersSent) response.writeHead(500);\n response.end();\n });\n stream.pipe(response);\n };\n const notFound = (): void => {\n response.writeHead(404);\n response.end();\n };\n\n let rawPath: string;\n try {\n rawPath = decodeURIComponent((request.url ?? '/').split('?')[0] ?? '/');\n } catch {\n // Malformed percent-encoding — reject rather than crash the request handler.\n response.writeHead(400);\n response.end();\n return;\n }\n\n const relative = normalize(rawPath).replace(/^(\\.\\.[/\\\\])+/, '');\n const candidate = join(rootDir, relative);\n const candidateResolved = resolve(candidate);\n // Enforce (not just argue) that the resolved path stays inside the root.\n if (candidateResolved !== rootResolved && !candidateResolved.startsWith(rootResolved + sep)) {\n notFound();\n return;\n }\n\n stat(candidate)\n .then((info) => {\n send(info.isDirectory() ? indexPath : candidate);\n })\n .catch(() => {\n // Missing path: a client route (no extension) gets the SPA shell; a missing\n // asset (has an extension) is a real 404 — so the network recorder sees it\n // instead of a 200 index.html masking a broken build.\n if (extname(candidate) === '') {\n send(indexPath);\n return;\n }\n notFound();\n });\n });\n\n await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));\n const address = server.address();\n if (address === null || typeof address === 'string') {\n throw new Error('serveStatic: failed to bind an ephemeral port');\n }\n\n return {\n url: `http://127.0.0.1:${address.port}`,\n close: () =>\n new Promise<void>((resolve, reject) => {\n server.close((error) => (error ? reject(error) : resolve()));\n }),\n };\n};\n","import type { Page } from 'playwright';\n\nimport type { Finding } from '../types';\n\n/**\n * Elements whose content is cut off with no way for the user to reveal it.\n *\n * This exists because a page can be free of console errors, axe violations and layout\n * overflow while still showing the user the wrong thing. The case that prompted it: an\n * editable financial grid whose cells were narrower than their own padding, so a value of\n * `800` rendered as `8`. Every other axis scored it 100.\n *\n * Deliberate truncation is not a finding. Text clipped with `text-overflow: ellipsis`\n * announces itself, and a scrollable box (`overflow: auto | scroll`) can be scrolled to\n * the rest. Only silent loss is reported — `overflow: hidden | clip` with no ellipsis, and\n * `<input>`s whose value is wider than the box that holds it (a single-line input is never\n * scrollable by eye; the user has to select into it to discover the rest). A `<textarea>`\n * is judged like any other box: it renders a real scrollbar when its content overflows\n * horizontally, so it is only a finding once something hides the overflow.\n */\n\n/** Ignore sub-pixel and rounding noise — real clipping is wider than a stray pixel. */\nconst CLIP_TOLERANCE_PX = 2;\n\n/** Cap the reported list so one systematically broken component can't flood the report. */\nconst MAX_REPORTED = 10;\n\nexport type ClippedElement = {\n selector: string;\n text: string;\n visibleWidth: number;\n contentWidth: number;\n isInput: boolean;\n};\n\n/**\n * Runs in the page. Kept self-contained (no imports, no outer-scope references) because it\n * is serialised into the browser context.\n */\n/* c8 ignore start -- executes in the browser, not under node coverage */\nconst findClippedElements = ({\n tolerance,\n max,\n}: {\n tolerance: number;\n max: number;\n}): ClippedElement[] => {\n // Prefer whatever a human can act on. A utility-class chain identifies every cell in a\n // grid identically, which makes a report of ten of them useless; an accessible name or\n // id points at the one that is actually wrong.\n const describe = (el: Element): string => {\n const tag = el.tagName.toLowerCase();\n const label = el.getAttribute('aria-label');\n if (label) return `${tag}[aria-label=\"${label}\"]`;\n if (el.id) return `${tag}#${el.id}`;\n const name = el.getAttribute('name');\n if (name) return `${tag}[name=\"${name}\"]`;\n const cls =\n typeof el.className === 'string' && el.className.trim()\n ? `.${el.className.trim().split(/\\s+/).slice(0, 2).join('.')}`\n : '';\n return `${tag}${cls}`;\n };\n\n const results: ClippedElement[] = [];\n // Elements already reported. `querySelectorAll` yields document order, so an ancestor is\n // always seen before its descendants — a descendant of a reported element is the same\n // problem seen twice, and the ancestor is the one whose CSS actually does the clipping.\n const reported = new Set<Element>();\n\n const hasReportedAncestor = (el: Element): boolean => {\n let parent = el.parentElement;\n while (parent) {\n if (reported.has(parent)) return true;\n parent = parent.parentElement;\n }\n return false;\n };\n\n for (const el of document.querySelectorAll<HTMLElement>('*')) {\n if (results.length >= max) break;\n if (hasReportedAncestor(el)) continue;\n\n const style = window.getComputedStyle(el);\n if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {\n continue;\n }\n\n // Visually-hidden text (`sr-only` and friends) is a 1px box with `overflow: hidden`\n // holding a full sentence — clipped by construction, and deliberately so: it exists\n // for screen readers, which are unaffected by the visual box. Flagging it would punish\n // the accessible thing to do, so anything with a collapsed box is skipped.\n if (el.clientWidth <= 1 || el.clientHeight <= 1) continue;\n\n const overflowX = style.overflowX;\n // A `<textarea>` scrolls horizontally like any other box, so only `<input>` skips the\n // scrollability and ellipsis tests below. Both still read `.value` rather than\n // `textContent`, which on a textarea is the *default* value, not what the user sees.\n const isFormValue = el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement;\n const isSingleLineInput = el instanceof HTMLInputElement;\n\n // A scrollable box is not clipped — the content is reachable.\n if (!isSingleLineInput && overflowX !== 'hidden' && overflowX !== 'clip') continue;\n // Ellipsis is an explicit, visible \"there is more\" affordance.\n if (!isSingleLineInput && style.textOverflow === 'ellipsis') continue;\n\n const overflowBy = el.scrollWidth - el.clientWidth;\n if (overflowBy <= tolerance) continue;\n\n // Only report elements that actually carry text the user is meant to read.\n const text = isFormValue\n ? (el as HTMLInputElement | HTMLTextAreaElement).value\n : (el.textContent ?? '').trim();\n if (!text) continue;\n\n reported.add(el);\n results.push({\n selector: describe(el),\n text: text.length > 40 ? `${text.slice(0, 40)}…` : text,\n visibleWidth: Math.round(el.clientWidth),\n contentWidth: Math.round(el.scrollWidth),\n isInput: isFormValue,\n });\n }\n\n return results;\n};\n/* c8 ignore stop */\n\n/** Format one clipped element as a finding. */\nconst toFinding = (clipped: ClippedElement): Finding => ({\n message: `Content is cut off with no way to reveal it (${clipped.selector})`,\n severity: 'serious',\n detail:\n `${clipped.isInput ? 'Input value' : 'Text'} \"${clipped.text}\" needs ` +\n `${clipped.contentWidth}px but has ${clipped.visibleWidth}px. ` +\n 'Widen the element, or make the truncation explicit with `text-overflow: ellipsis`.',\n});\n\n/**\n * Report every element whose content is silently cut off. Returns no findings when the\n * page cannot be inspected, so a caller can always spread the result.\n */\nexport const clippedTextFindings = async (page: Page): Promise<Finding[]> => {\n try {\n // `findClippedElements` is serialised into the page, which is why it takes a single\n // plain-object argument and closes over nothing.\n const clipped = await page.evaluate(findClippedElements, {\n tolerance: CLIP_TOLERANCE_PX,\n max: MAX_REPORTED,\n });\n\n return clipped.map(toFinding);\n } catch (error) {\n // A closed page or a lost execution context must not take the whole a11y axis down\n // with it — this check is additive to axe, so losing it degrades the report rather\n // than invalidating it.\n console.error('clipped-text: could not inspect the page', error);\n return [];\n }\n};\n","import axe from 'axe-core';\n\nimport type { Axis, Finding, Severity, Verdict } from '../types';\n\nimport { clippedTextFindings } from './clipped-text';\n\n/** Minimal shape of an axe result node — only the fields we read. */\ntype AxeNode = { target: string[] };\n\n/** Minimal shape of an axe violation — only the fields we read. */\ntype AxeViolation = {\n id: string;\n impact: Severity | null;\n help: string;\n description: string;\n tags: string[];\n nodes: AxeNode[];\n};\n\n/** Minimal shape of the axe.run result at the page-evaluate boundary. */\ntype AxeResults = { violations: AxeViolation[] };\n\n/** axe.run injected onto the page global by evaluating `axe.source`. */\ntype AxeGlobal = { run: (context: Document) => Promise<AxeResults> };\n\nconst IMPACTS: readonly Severity[] = ['critical', 'serious', 'moderate', 'minor'];\n\nconst toSeverity = (impact: Severity | null): Severity =>\n impact && IMPACTS.includes(impact) ? impact : 'minor';\n\n/** First `wcag*` tag if present, else the first tag, else undefined. */\nconst wcagTag = (tags: string[]): string | undefined =>\n tags.find((tag) => tag.startsWith('wcag')) ?? tags[0];\n\nconst toFinding = (violation: AxeViolation): Finding => {\n const targets = violation.nodes\n .map((node) => node.target.join(' '))\n .filter((target) => target.length > 0)\n .join(', ');\n const wcag = wcagTag(violation.tags);\n return {\n message: `${violation.help || violation.description}${targets ? ` (${targets})` : ''}`,\n severity: toSeverity(violation.impact),\n ref: [wcag, violation.id].filter(Boolean).join(' · '),\n };\n};\n\n/**\n * A3 · Accessibility verdict — UT-4494.\n *\n * Injects axe-core into `context.page` and maps each violation to a Finding\n * (WCAG tag + rule id as `ref`, axe impact as severity). Pass rule: a page\n * passes when no violation has impact `critical` or `serious` — moderate/minor\n * violations are reported but don't fail the axis. Falls back to a clear\n * finding rather than throwing when no live page is provided.\n */\n/** Not-assessed outcome — a11y can't fail if it never ran. */\nconst notAssessed = (message: string): Verdict => ({\n axis: 'a11y',\n pass: true,\n findings: [{ message, severity: 'minor' }],\n});\n\nexport const accessibilityAxis: Axis = {\n name: 'a11y',\n run: async (context) => {\n const { page } = context;\n // Degrade to a non-fatal skip (like the other axes) rather than failing the run.\n if (!page) return notAssessed('a11y skipped: no live page provided');\n\n // `page.evaluate`, not `addScriptTag`. The latter appends an inline `<script>`, which a target\n // page's `script-src` can block — and Playwright rejects on that, so the guard below never sees\n // it and the axis crashes. Evaluation goes through CDP, which a page's CSP does not govern.\n await page.evaluate(axe.source);\n // Injection can still leave window.axe undefined — detect that in-page and surface a clear\n // finding instead of a \"Cannot read properties of undefined\" crash.\n const results = await page.evaluate<AxeResults | null>(() => {\n const injected = (window as unknown as { axe?: AxeGlobal }).axe;\n return injected ? injected.run(document) : null;\n });\n if (results === null) {\n return notAssessed('a11y not assessed: axe-core failed to inject (page CSP?)');\n }\n\n // axe has no rule for content that is silently cut off, but unreadable content is an\n // a11y problem by any reasonable reading — and it is invisible to every other axis.\n const findings = [...results.violations.map(toFinding), ...(await clippedTextFindings(page))];\n const pass = !findings.some(\n (finding) => finding.severity === 'critical' || finding.severity === 'serious',\n );\n\n return { axis: 'a11y', pass, findings };\n },\n};\n","import Anthropic from '@anthropic-ai/sdk';\n\n/**\n * The one call brief-conformance needs from a vision model: send a screenshot +\n * prompt, get the reply's text back. Backends differ only in how the underlying\n * Anthropic client is authenticated and routed.\n */\nexport type VlmProvider = {\n /** Human-readable backend id, surfaced in skip/diagnostic findings. */\n readonly backend: 'raw-anthropic' | 'keboola-llm';\n /** Send a base64 PNG + text prompt, resolve to the model reply's text. */\n judge: (input: { screenshotBase64: string; prompt: string; model: string }) => Promise<string>;\n};\n\n/** Concatenate a message's text blocks. */\nconst textFromMessage = (message: Anthropic.Message): string =>\n message.content\n .filter((block): block is Anthropic.TextBlock => block.type === 'text')\n .map((block) => block.text)\n .join('');\n\nconst makeProvider = (\n backend: VlmProvider['backend'],\n clientOptions: { apiKey: string; baseURL?: string },\n): VlmProvider => {\n const client = new Anthropic(clientOptions);\n return {\n backend,\n judge: async ({ screenshotBase64, prompt, model }) => {\n const message = await client.messages.create({\n model,\n max_tokens: 1024,\n messages: [\n {\n role: 'user',\n content: [\n {\n type: 'image',\n source: { type: 'base64', media_type: 'image/png', data: screenshotBase64 },\n },\n { type: 'text', text: prompt },\n ],\n },\n ],\n });\n return textFromMessage(message);\n },\n };\n};\n\nconst nonEmpty = (value: string | undefined): value is string =>\n value !== undefined && value !== '';\n\n/**\n * Pick a VLM backend from the environment, or `undefined` when none is\n * configured (the axis then skips gracefully).\n *\n * Preference order mirrors how kai-agent routes Claude for internal/team use\n * (`apps/kai-agent/src/services/entrypoint-builder.ts`): the Keboola LLM path\n * points the Anthropic SDK at a Keboola-hosted proxy via `baseURL` and\n * authenticates with a Keboola-minted token sent as `x-api-key` (the SDK's\n * `apiKey`). kai-agent injects that pair through the SDK-native\n * `ANTHROPIC_BASE_URL` + `ANTHROPIC_API_KEY` env vars; we accept those, and\n * prefer the explicit `VALIDATE_UI_LLM_BASE_URL` / `VALIDATE_UI_LLM_TOKEN`\n * (with `KBC_TOKEN` as a Keboola-convention alias) when set. The proxy path is\n * preferred because the team plan has no dedicated raw key.\n *\n * When no proxy `baseURL` is configured, a bare `ANTHROPIC_API_KEY` selects the\n * raw Anthropic backend — the original behavior and the fallback.\n *\n * Both backends pass the resolved key (and, for the proxy, `baseURL`) into the\n * `Anthropic` constructor explicitly, so resolution never reads ambient\n * `process.env` behind the injected `env` arg.\n */\nexport const resolveVlmProvider = (\n env: NodeJS.ProcessEnv = process.env,\n): VlmProvider | undefined => {\n const baseUrl = env.VALIDATE_UI_LLM_BASE_URL ?? env.ANTHROPIC_BASE_URL;\n const proxyToken = env.VALIDATE_UI_LLM_TOKEN ?? env.KBC_TOKEN ?? env.ANTHROPIC_API_KEY;\n\n if (nonEmpty(baseUrl) && nonEmpty(proxyToken)) {\n return makeProvider('keboola-llm', { baseURL: baseUrl, apiKey: proxyToken });\n }\n\n if (nonEmpty(env.ANTHROPIC_API_KEY)) {\n return makeProvider('raw-anthropic', { apiKey: env.ANTHROPIC_API_KEY });\n }\n\n return undefined;\n};\n","import { z } from 'zod';\n\nimport type { Axis, AxisContext, Finding, Verdict } from '../types';\n\nimport { resolveVlmProvider } from './vlm-provider';\n\n/** Structured judgement the VLM must return, validated before interpretation. */\nexport const JudgeResult = z.object({\n matchesBrief: z.boolean(),\n missingStates: z.array(z.string()),\n reasons: z.array(z.string()),\n});\nexport type JudgeResult = z.infer<typeof JudgeResult>;\n\nconst MODEL = process.env.VALIDATE_UI_MODEL ?? 'claude-sonnet-4-6';\n// DOM excerpt cap — enough structure for the judge, small enough to keep tokens sane.\nconst DOM_EXCERPT_LIMIT = 12_000;\n\n/**\n * Pure interpreter: turn a validated {@link JudgeResult} into a {@link Verdict}.\n * A brief mismatch is serious; each missing state is a moderate gap.\n */\nexport const interpretJudgeResult = (result: JudgeResult): Verdict => {\n const findings: Finding[] = [];\n\n if (!result.matchesBrief) {\n findings.push({\n message: `UI does not match the brief: ${result.reasons.join('; ')}`,\n severity: 'serious',\n });\n }\n\n for (const state of result.missingStates) {\n findings.push({ message: `missing state: ${state}`, severity: 'moderate' });\n }\n\n const pass = result.matchesBrief && result.missingStates.length === 0;\n return { axis: 'brief-conformance', pass, findings };\n};\n\nconst skip = (message: string): Verdict => ({\n axis: 'brief-conformance',\n pass: true,\n findings: [{ message, severity: 'minor' }],\n});\n\nconst RUBRIC = [\n 'You are judging whether a rendered UI conforms to the brief it was generated from.',\n 'Assess two things:',\n '1. matchesBrief — does the screenshot + DOM implement what the brief describes?',\n '2. missingStates — which UI states the brief implies are NOT represented.',\n ' Use short lowercase labels like \"empty\", \"loading\", \"error\".',\n 'Reply with ONLY a JSON object, no prose, matching exactly:',\n '{ \"matchesBrief\": boolean, \"missingStates\": string[], \"reasons\": string[] }',\n 'reasons: brief explanations for any mismatch (empty array if it matches).',\n].join('\\n');\n\nexport const briefConformanceAxis: Axis = {\n name: 'brief-conformance',\n run: async (context: AxisContext): Promise<Verdict> => {\n const brief = context.brief?.trim();\n if (brief === undefined || brief === '') {\n return skip('no brief provided; conformance not assessed');\n }\n\n const provider = resolveVlmProvider();\n if (provider === undefined) {\n return skip(\n 'brief-conformance skipped: no VLM backend configured ' +\n '(set VALIDATE_UI_LLM_BASE_URL + VALIDATE_UI_LLM_TOKEN for the Keboola LLM path, ' +\n 'or ANTHROPIC_API_KEY for a raw key)',\n );\n }\n\n const { artifact } = context;\n const screenshotBase64 = Buffer.from(artifact.screenshot).toString('base64');\n const domExcerpt = artifact.dom.slice(0, DOM_EXCERPT_LIMIT);\n\n const reply = await provider.judge({\n model: MODEL,\n screenshotBase64,\n prompt: `${RUBRIC}\\n\\nBrief:\\n${brief}\\n\\nDOM excerpt:\\n${domExcerpt}`,\n });\n\n try {\n const parsed = JudgeResult.parse(JSON.parse(reply));\n return interpretJudgeResult(parsed);\n } catch (error) {\n console.error('brief-conformance: failed to parse judge response', error);\n return {\n axis: 'brief-conformance',\n pass: false,\n findings: [{ message: 'could not parse judge response', severity: 'moderate' }],\n };\n }\n },\n};\n","import type {\n CompareConfig,\n CompareFinding,\n CompareSnapshot,\n ExpectedAbsentRule,\n Region,\n} from './types';\n\n/** Match a `*`-glob (or exact string) against a region key. */\nconst matchesGlob = (pattern: string, value: string): boolean => {\n if (!pattern.includes('*')) return pattern === value;\n const escaped = pattern.replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&').replace(/\\*/g, '.*');\n return new RegExp(`^${escaped}$`).test(value);\n};\n\n/** Multiset difference: tokens present more often in `from` than in `to`. */\nconst multisetMissing = (from: string[], to: string[]): string[] => {\n const remaining = new Map<string, number>();\n for (const value of to) remaining.set(value, (remaining.get(value) ?? 0) + 1);\n const missing: string[] = [];\n for (const value of from) {\n const count = remaining.get(value) ?? 0;\n if (count > 0) remaining.set(value, count - 1);\n else missing.push(value);\n }\n return missing;\n};\n\n/** Stable signature of a delta — identical across routes for the same shared element. */\nconst signature = (regionKey: string, tokens: string[]): string =>\n `${regionKey}::${[...tokens].sort().join('|')}`;\n\n/** The rule that suppresses removing a whole region (region match, no value scope). */\nconst regionRemovalRule = (\n rules: ExpectedAbsentRule[],\n key: string,\n): ExpectedAbsentRule | undefined =>\n rules.find(\n (rule) =>\n rule.value === undefined && rule.region !== undefined && matchesGlob(rule.region, key),\n );\n\n/** Does a value rule apply to this (region, value) pair? */\nconst valueRuleMatches = (rule: ExpectedAbsentRule, key: string, value: string): boolean =>\n rule.value !== undefined &&\n matchesGlob(rule.value, value) &&\n (rule.region === undefined || matchesGlob(rule.region, key));\n\nconst removedFinding = (region: Region, rule: ExpectedAbsentRule | undefined): CompareFinding => ({\n regionKey: region.key,\n kind: 'region-removed',\n valueSignature: signature(region.key, region.values),\n message:\n rule === undefined\n ? `region \"${region.label}\" was removed`\n : `region \"${region.label}\" removed as expected`,\n severity: rule === undefined ? 'serious' : 'minor',\n ...(rule !== undefined && { suppressed: { reason: rule.reason } }),\n});\n\nconst degradedFinding = (region: Region): CompareFinding => ({\n regionKey: region.key,\n kind: 'region-degraded',\n valueSignature: signature(region.key, [region.state]),\n message: `region \"${region.label}\" degraded to ${region.state} state — not compared as value loss`,\n severity: 'minor',\n});\n\nconst addedFinding = (region: Region): CompareFinding => ({\n regionKey: region.key,\n kind: 'region-added',\n valueSignature: signature(region.key, []),\n message: `region \"${region.label}\" is new`,\n severity: 'minor',\n});\n\n/**\n * Diff one region present on both sides. Returns the value-missing findings\n * (active and/or suppressed), or none when values are unchanged.\n */\nconst diffRegionValues = (\n oldRegion: Region,\n newRegion: Region,\n rules: ExpectedAbsentRule[],\n): CompareFinding[] => {\n const missing = multisetMissing(oldRegion.values, newRegion.values);\n if (missing.length === 0) return [];\n\n const active: string[] = [];\n const suppressed: string[] = [];\n const reasons = new Set<string>();\n for (const value of missing) {\n const rule = rules.find((r) => valueRuleMatches(r, newRegion.key, value));\n if (rule === undefined) active.push(value);\n else {\n suppressed.push(value);\n reasons.add(rule.reason);\n }\n }\n\n const findings: CompareFinding[] = [];\n if (active.length > 0) {\n findings.push({\n regionKey: newRegion.key,\n kind: 'value-missing',\n valueSignature: signature(newRegion.key, active),\n message: `region \"${newRegion.label}\" is missing ${active.length} value(s)`,\n severity: 'moderate',\n detail: active.join(', '),\n });\n }\n if (suppressed.length > 0) {\n findings.push({\n regionKey: newRegion.key,\n kind: 'value-missing',\n valueSignature: signature(newRegion.key, suppressed),\n message: `region \"${newRegion.label}\" is missing ${suppressed.length} expected value(s)`,\n severity: 'minor',\n detail: suppressed.join(', '),\n suppressed: { reason: [...reasons].join('; ') },\n });\n }\n return findings;\n};\n\n/**\n * Semantic old-vs-new diff of two snapshots. Walks regions by key and localizes\n * every delta to a region: a removed region is `serious` unless allowlisted; a\n * region that degraded to an empty/error state is reported informationally and\n * NOT diffed as value loss; surviving regions get a per-region value-multiset\n * diff with allowlist suppression; new regions are non-regression notes. Pure —\n * no browser, no I/O.\n */\nexport const diffSnapshots = (\n oldSnap: CompareSnapshot,\n newSnap: CompareSnapshot,\n config: CompareConfig = {},\n): CompareFinding[] => {\n const rules = config.expectedAbsent ?? [];\n const newByKey = new Map(newSnap.regions.map((region) => [region.key, region]));\n const oldKeys = new Set(oldSnap.regions.map((region) => region.key));\n const findings: CompareFinding[] = [];\n\n for (const oldRegion of oldSnap.regions) {\n const newRegion = newByKey.get(oldRegion.key);\n if (newRegion === undefined) {\n findings.push(removedFinding(oldRegion, regionRemovalRule(rules, oldRegion.key)));\n continue;\n }\n if (oldRegion.state === 'ok' && newRegion.state !== 'ok') {\n findings.push(degradedFinding(newRegion));\n continue;\n }\n findings.push(...diffRegionValues(oldRegion, newRegion, rules));\n }\n\n for (const newRegion of newSnap.regions) {\n if (!oldKeys.has(newRegion.key)) findings.push(addedFinding(newRegion));\n }\n\n return findings;\n};\n\n/** A compare run passes when no active (non-suppressed) delta is serious/critical. */\nexport const comparePass = (findings: CompareFinding[]): boolean =>\n !findings.some(\n (f) => f.suppressed === undefined && (f.severity === 'serious' || f.severity === 'critical'),\n );\n","import type { Page } from 'playwright';\n\nimport type { CompareConfig, Region } from './types';\n\n// Region boundaries: explicit opt-in (`data-region`), then ARIA landmarks and\n// labelled/role'd sections. `data-testid` is deliberately NOT a boundary (it is\n// on every button/input and would fragment the page) — it is only a key source.\nconst CANDIDATE_SELECTOR = [\n '[data-region]',\n 'main',\n 'nav',\n 'header',\n 'aside',\n 'footer',\n 'section[aria-label]',\n '[role=main]',\n '[role=navigation]',\n '[role=banner]',\n '[role=complementary]',\n '[role=contentinfo]',\n '[role=region]',\n].join(',');\n\nconst DEFAULT_EMPTY_MARKERS = [\n 'no data',\n 'no results',\n 'nothing to show',\n 'nothing here',\n 'get started',\n];\nconst DEFAULT_ERROR_MARKERS = [\n 'something went wrong',\n 'failed to load',\n 'could not load',\n 'unavailable',\n 'try again',\n];\n\n/**\n * Reduce the live page to its diffable regions, in the browser. An explicit\n * `[data-region]` is an authoritative boundary (its subtree is one region);\n * elsewhere boundaries are the outermost-empty (\"leaf\") landmarks/sections, so\n * regions never overlap or double-count values. Each region's key is resolved by\n * precedence\n * (`data-region` → `data-testid` → role → `aria-label` → nearest heading), its\n * state from `data-state`/text markers, and its values as normalized numeric\n * tokens. Symmetric across old and new so the two sides diff cleanly.\n */\nexport const extractRegions = async (page: Page, config: CompareConfig = {}): Promise<Region[]> => {\n const emptyMarkers = [...DEFAULT_EMPTY_MARKERS, ...(config.emptyStateMarkers ?? [])];\n const errorMarkers = [...DEFAULT_ERROR_MARKERS, ...(config.errorStateMarkers ?? [])];\n\n return page.evaluate(\n ({ selector, emptyMarkers, errorMarkers }): Region[] => {\n const STATES: Region['state'][] = ['ok', 'empty', 'error', 'loading'];\n const isState = (value: string): value is Region['state'] =>\n (STATES as string[]).includes(value);\n const slugify = (raw: string): string =>\n raw\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 60) || 'region';\n\n const roleOf = (el: Element): string | undefined => {\n const explicit = el.getAttribute('role');\n if (explicit !== null && explicit !== '') return explicit;\n const tag = el.tagName.toLowerCase();\n if (tag === 'main') return 'main';\n if (tag === 'nav') return 'navigation';\n if (tag === 'aside') return 'complementary';\n // Per ARIA-in-HTML, header→banner and footer→contentinfo only when NOT\n // scoped inside sectioning content; a nested header/footer has no\n // landmark role, so its key falls through to aria-label/heading.\n if (tag === 'header' || tag === 'footer') {\n if (el.closest('main,nav,section,article,aside') !== null) return undefined;\n return tag === 'header' ? 'banner' : 'contentinfo';\n }\n return undefined;\n };\n\n const headingOf = (el: Element): string | undefined => {\n const heading = el.querySelector('h1,h2,h3,h4,h5,h6');\n const text = heading?.textContent?.trim();\n return text !== undefined && text !== '' ? text : undefined;\n };\n\n const keyOf = (el: Element): string => {\n const region = el.getAttribute('data-region');\n if (region !== null && region !== '') return slugify(region);\n const testid = el.getAttribute('data-testid');\n if (testid !== null && testid !== '') return slugify(testid);\n const role = roleOf(el);\n if (role !== undefined) return role;\n const label = el.getAttribute('aria-label');\n if (label !== null && label !== '') return slugify(label);\n const heading = headingOf(el);\n if (heading !== undefined) return slugify(heading);\n return slugify(el.tagName);\n };\n\n const labelOf = (el: Element, key: string): string => {\n const label = el.getAttribute('aria-label');\n if (label !== null && label.trim() !== '') return label.trim();\n const heading = headingOf(el);\n if (heading !== undefined) return heading;\n const testid = el.getAttribute('data-testid');\n if (testid !== null && testid !== '') return testid;\n return key;\n };\n\n const textOf = (el: HTMLElement): string => (el.innerText || el.textContent || '').trim();\n\n const stateOf = (el: Element, text: string): Region['state'] => {\n const explicit = (el.getAttribute('data-state') ?? '').toLowerCase();\n if (isState(explicit)) return explicit;\n const lower = text.toLowerCase();\n if (errorMarkers.some((marker) => lower.includes(marker))) return 'error';\n if (lower.includes('loading')) return 'loading';\n if (emptyMarkers.some((marker) => lower.includes(marker))) return 'empty';\n return 'ok';\n };\n\n const valuesOf = (text: string): string[] =>\n (text.match(/-?\\$?\\s?\\d[\\d,]*(?:\\.\\d+)?%?/g) ?? [])\n .map((token) => token.replace(/[$\\s,]/g, ''))\n .filter((token) => token !== '' && token !== '-');\n\n // A `[data-region]` is an authoritative boundary: the outermost one is a\n // region and its whole subtree is absorbed into it (nested candidates do\n // not split it). Elsewhere, a region is a leaf landmark/section — one with\n // no candidate descendant — that is not itself inside a `[data-region]`.\n const qualifies = (el: Element): boolean => {\n const dataRegion = el.getAttribute('data-region');\n if (dataRegion !== null && dataRegion !== '') {\n const parent = el.parentElement;\n return parent === null || parent.closest('[data-region]') === null;\n }\n return el.querySelector(selector) === null && el.closest('[data-region]') === null;\n };\n const candidates = Array.from(document.querySelectorAll(selector));\n const regions = candidates.filter(qualifies);\n const elements: Element[] = regions.length > 0 ? regions : [document.body];\n\n const used: Record<string, number> = {};\n const uniqueKey = (key: string): string => {\n const count = (used[key] ?? 0) + 1;\n used[key] = count;\n return count === 1 ? key : `${key}-${count}`;\n };\n\n return elements.map((el) => {\n const text = textOf(el as HTMLElement);\n const key = uniqueKey(keyOf(el));\n return {\n key,\n label: labelOf(el, key),\n state: stateOf(el, text),\n values: valuesOf(text),\n };\n });\n },\n { selector: CANDIDATE_SELECTOR, emptyMarkers, errorMarkers },\n );\n};\n","import { z } from 'zod';\n\nimport type { Finding } from '../types';\n\n/**\n * Backend/render state of a region. A region that rendered its data is `ok`;\n * `empty`/`error`/`loading` are non-data states — an old→new transition into one\n * of them is a degrade, not a value regression (a backend was unavailable, or an\n * intentional empty state is showing).\n */\nexport const REGION_STATES = ['ok', 'empty', 'error', 'loading'] as const;\nexport type RegionState = (typeof REGION_STATES)[number];\n\n/** One diffable region of a page — a landmark, labelled section, or component. */\nexport const Region = z.object({\n /** Stable id: data-region → data-testid → role → aria-label → heading slug. */\n key: z.string(),\n /** Human label for the report. */\n label: z.string(),\n state: z.enum(REGION_STATES),\n /** Normalized numeric / currency / percent tokens found in the region. */\n values: z.array(z.string()),\n});\nexport type Region = z.infer<typeof Region>;\n\n/**\n * A page reduced to its diffable regions — the currency both input paths (a live\n * boot or a recorded JSON file) produce and the differ consumes. Pure JSON, so\n * it is safe to serialize to disk and re-load; {@link loadSnapshot} validates it.\n */\nexport const CompareSnapshot = z\n .object({\n /** Route the snapshot was captured at, e.g. `/financials`. */\n route: z.string(),\n regions: z.array(Region),\n })\n // The differ indexes regions by key, so duplicates would silently overwrite.\n // Live capture guarantees uniqueness; this rejects a malformed recorded snapshot.\n .refine(\n (snap) => new Set(snap.regions.map((region) => region.key)).size === snap.regions.length,\n {\n message: 'region keys must be unique',\n },\n );\nexport type CompareSnapshot = z.infer<typeof CompareSnapshot>;\n\n/** One old-vs-new delta, always attributed to a region. */\nexport type CompareFinding = Finding & {\n regionKey: string;\n kind: 'region-removed' | 'region-degraded' | 'value-missing' | 'region-added';\n /** Stable signature of the delta — identical across routes for shared chrome. */\n valueSignature: string;\n /** Present when a config rule suppressed this delta; it never fails the verdict. */\n suppressed?: { reason: string };\n};\n\n/**\n * One expected-absent rule. `region` (exact or `*`-glob against {@link Region.key})\n * alone suppresses a whole `region-removed`; add `value` to suppress a specific\n * missing value token (optionally scoped to a region). `reason` is required and\n * surfaced in the report so suppression is auditable, never silent.\n */\nexport const ExpectedAbsentRule = z\n .object({\n region: z.string().optional(),\n value: z.string().optional(),\n reason: z.string().min(1),\n })\n .refine((rule) => rule.region !== undefined || rule.value !== undefined, {\n message: 'expectedAbsent rule needs at least one of `region` or `value`',\n });\nexport type ExpectedAbsentRule = z.infer<typeof ExpectedAbsentRule>;\n\n/** Semantic-compare configuration — the allowlist plus extra state markers. */\nexport const CompareConfig = z.object({\n expectedAbsent: z.array(ExpectedAbsentRule).optional(),\n emptyStateMarkers: z.array(z.string()).optional(),\n errorStateMarkers: z.array(z.string()).optional(),\n});\nexport type CompareConfig = z.infer<typeof CompareConfig>;\n","import { readFile, writeFile } from 'node:fs/promises';\nimport type { Page } from 'playwright';\n\nimport { extractRegions } from './regions';\nimport { type CompareConfig, CompareSnapshot } from './types';\n\n/** Reduce a loaded live page to a {@link CompareSnapshot} for the given route. */\nexport const captureSnapshot = async (\n page: Page,\n route: string,\n config?: CompareConfig,\n): Promise<CompareSnapshot> => ({\n route,\n regions: await extractRegions(page, config),\n});\n\n/** Persist a snapshot as pretty JSON so an \"old\" side can be recorded once and re-diffed. */\nexport const saveSnapshot = async (snapshot: CompareSnapshot, path: string): Promise<void> => {\n await writeFile(path, JSON.stringify(snapshot, null, 2));\n};\n\n/** Read and validate a recorded snapshot from disk. */\nexport const loadSnapshot = async (path: string): Promise<CompareSnapshot> => {\n const raw = await readFile(path, 'utf8');\n return CompareSnapshot.parse(JSON.parse(raw));\n};\n","import { comparePass, diffSnapshots } from '../compare/diff';\nimport { captureSnapshot } from '../compare/snapshot';\nimport type { Axis, Verdict } from '../types';\n\nconst skip = (message: string): Verdict => ({\n axis: 'compare',\n pass: true,\n findings: [{ message, severity: 'minor' }],\n});\n\n/**\n * A5 · Semantic old-vs-new compare verdict — UT-4572.\n *\n * Activates only when `context.comparison` (an \"old\" {@link CompareSnapshot}) is\n * supplied; otherwise it degrades to a passing skip, so ordinary single-page runs\n * are unaffected. Extracts the \"new\" snapshot from the live `context.page`, then\n * runs the pure {@link diffSnapshots}: deltas are localized per region, declared\n * expected-absent deltas are suppressed (not dropped), and a region that degraded\n * to an empty/error state is reported informationally rather than as value loss.\n */\nexport const compareAxis: Axis = {\n name: 'compare',\n run: async (context): Promise<Verdict> => {\n const { comparison, compareConfig, page } = context;\n if (comparison === undefined) {\n return skip('no comparison snapshot provided; old-vs-new not assessed');\n }\n if (page === undefined) {\n return skip('compare needs a live page to extract the new snapshot');\n }\n\n const newSnapshot = await captureSnapshot(page, comparison.route, compareConfig);\n const findings = diffSnapshots(comparison, newSnapshot, compareConfig);\n return { axis: 'compare', pass: comparePass(findings), findings };\n },\n};\n","import type { Axis, ConsoleEntry, Finding, NetworkEntry, Severity } from '../types';\n\n// Overlay/error-boundary markers that signal a crashed render rather than real content.\nconst ERROR_MARKERS = [\n 'something went wrong',\n 'vite-error-overlay',\n 'vite-plugin-checker-error-overlay',\n 'react-error-overlay',\n] as const;\n\nconst pageErrorFindings = (console: ConsoleEntry[]): Finding[] =>\n console\n .filter((entry) => entry.type === 'pageerror')\n .map((entry) => ({\n message: `Uncaught page error: ${entry.text}`,\n severity: 'critical',\n detail: entry.location,\n }));\n\n// The browser echoes every failed resource load to the console as a generic\n// \"Failed to load resource\" error — noise that duplicates the network finding.\nconst RESOURCE_LOAD_ECHO = 'failed to load resource';\n\nconst isResourceLoadEcho = (entry: ConsoleEntry): boolean =>\n entry.text.toLowerCase().includes(RESOURCE_LOAD_ECHO);\n\n/**\n * Real app console errors only — the generic \"Failed to load resource\" echoes\n * are dropped (the network finding for the same URL is more useful). Identical\n * texts are deduped since one noisy error often logs on every render.\n */\nconst consoleErrorFindings = (console: ConsoleEntry[]): Finding[] => {\n const seen = new Set<string>();\n const findings: Finding[] = [];\n for (const entry of console) {\n if (entry.type !== 'error' || isResourceLoadEcho(entry) || seen.has(entry.text)) continue;\n seen.add(entry.text);\n findings.push({\n message: `Console error: ${entry.text}`,\n severity: 'serious',\n detail: entry.location,\n });\n }\n return findings;\n};\n\n/** Turn one network entry into a finding, or null for a healthy (<400) response. */\nconst toNetworkFinding = (entry: NetworkEntry): Finding | null => {\n if (entry.status === 0) {\n return {\n message: `Request failed: ${entry.method} ${entry.url}`,\n severity: 'serious',\n detail: entry.failure,\n ref: entry.url,\n };\n }\n if (entry.status >= 500) {\n return {\n message: `Server error ${entry.status}: ${entry.method} ${entry.url}`,\n severity: 'serious',\n ref: entry.url,\n };\n }\n if (entry.status >= 400) {\n return {\n message: `Client error ${entry.status}: ${entry.method} ${entry.url}`,\n severity: 'moderate',\n ref: entry.url,\n };\n }\n return null;\n};\n\nconst SEVERITY_RANK: Record<Severity, number> = { critical: 3, serious: 2, moderate: 1, minor: 0 };\n\n/**\n * One finding per URL. A single failing resource surfaces as a `requestfailed`,\n * a 4xx/5xx response, and a console echo — all the same root cause. Collapse\n * them by URL, keeping the most severe (a request-failure or explicit HTTP\n * status over the generic browser echo).\n */\nconst networkFindings = (network: NetworkEntry[]): Finding[] => {\n const byUrl = new Map<string, Finding>();\n for (const entry of network) {\n const finding = toNetworkFinding(entry);\n if (finding === null) continue;\n const existing = byUrl.get(entry.url);\n if (\n existing === undefined ||\n SEVERITY_RANK[finding.severity] > SEVERITY_RANK[existing.severity]\n ) {\n byUrl.set(entry.url, finding);\n }\n }\n return [...byUrl.values()];\n};\n\n/** An error overlay / boundary in the DOM means a crashed render, not real content. */\nconst errorMarkerFinding = (dom: string): Finding | null => {\n const lower = dom.toLowerCase();\n const marker = ERROR_MARKERS.find((m) => lower.includes(m));\n return marker === undefined\n ? null\n : {\n message: 'Error boundary / error overlay rendered instead of the app',\n severity: 'critical',\n detail: marker,\n };\n};\n\n/**\n * White-screen check on the reliable live-DOM signal (`renderedText`). Skipped\n * when absent — parsing the HTML string for \"emptiness\" gives false positives.\n */\nconst whiteScreenFinding = (renderedText: string | undefined): Finding | null =>\n renderedText !== undefined && renderedText.length === 0\n ? { message: 'White screen: page rendered no visible text', severity: 'critical' }\n : null;\n\n/**\n * A2 · Runtime-health verdict — UT-4493.\n *\n * Pure over `context.artifact` (no live page). Collects uncaught page errors,\n * console errors, failed/4xx/5xx requests, and white-screen / error-overlay\n * signals into `findings`.\n */\nexport const runtimeHealthAxis: Axis = {\n name: 'runtime-health',\n run: (context) => {\n const { artifact } = context;\n const findings: Finding[] = [\n ...pageErrorFindings(artifact.console),\n ...consoleErrorFindings(artifact.console),\n ...networkFindings(artifact.network),\n ];\n const errorMarker = errorMarkerFinding(artifact.dom);\n if (errorMarker !== null) findings.push(errorMarker);\n const whiteScreen = whiteScreenFinding(artifact.renderedText);\n if (whiteScreen !== null) findings.push(whiteScreen);\n\n // Fail only on blocking severities; moderate/minor are reported but non-fatal.\n const fatal: Severity[] = ['critical', 'serious'];\n const pass = !findings.some((f) => fatal.includes(f.severity));\n\n return Promise.resolve({ axis: 'runtime-health', pass, findings });\n },\n};\n","import pixelmatch from 'pixelmatch';\nimport type { Page } from 'playwright';\nimport { PNG } from 'pngjs';\n\nimport type { Brand } from '@keboola/brand-registry';\nimport { brands, checkInteractiveContrast } from '@keboola/brand-registry';\n\nimport type { Axis, Finding } from '../types';\n\n// Ratio above which two screenshots count as meaningfully drifted from a baseline.\nconst DRIFT_THRESHOLD = 0.01;\n// Ratio below which an alt-brand capture is treated as \"chrome did not re-skin\".\nconst RESKIN_THRESHOLD = 0.005;\n// pixelmatch per-pixel colour-distance threshold for baseline drift. Deliberately coarse:\n// drift detection wants to ignore antialiasing and sub-perceptual noise between runs.\nconst DRIFT_PIXEL_THRESHOLD = 0.1;\n// …but a brand re-skin is mostly broad, low-amplitude hue and tone shifts (background,\n// muted surfaces, borders), which the coarse threshold discards almost entirely. Measured\n// on the reference app, a correct and obvious re-skin — new logo, purple accents, different\n// typeface — scored 0.0008–0.0038 at 0.1, i.e. UNDER the reskin floor, so a correctly\n// branded app was told its chrome did not re-skin. At 0.02 the same pages score\n// 0.008–0.012. Sensitivity here costs nothing: this comparison is deliberately looking for\n// difference, so over-detecting only makes a genuine re-skin easier to see.\nconst RESKIN_PIXEL_THRESHOLD = 0.02;\n// Default alternate brand the fitness function re-renders under.\nconst DEFAULT_ALT_BRAND = 'example-customer';\n\n/** Comparison of two PNG buffers: mismatched-pixel ratio, and whether they differ in size. */\nexport type PngMismatch = { ratio: number; sizeMismatch: boolean };\n\n/** Decode two PNGs and return their pixel mismatch ratio (0 = identical, 1 = every pixel differs). */\nexport const pngMismatchRatio = (\n a: Uint8Array,\n b: Uint8Array,\n pixelThreshold: number = DRIFT_PIXEL_THRESHOLD,\n): PngMismatch => {\n const imgA = PNG.sync.read(Buffer.from(a));\n const imgB = PNG.sync.read(Buffer.from(b));\n\n if (imgA.width !== imgB.width || imgA.height !== imgB.height) {\n return { ratio: 1, sizeMismatch: true };\n }\n\n const total = imgA.width * imgA.height;\n if (total === 0) return { ratio: 0, sizeMismatch: false };\n\n const mismatched = pixelmatch(imgA.data, imgB.data, undefined, imgA.width, imgA.height, {\n threshold: pixelThreshold,\n });\n return { ratio: mismatched / total, sizeMismatch: false };\n};\n\nconst baselineFinding = (screenshot: Uint8Array, baseline: Uint8Array): Finding | null => {\n const { ratio, sizeMismatch } = pngMismatchRatio(screenshot, baseline);\n if (sizeMismatch) {\n return {\n message: 'screenshot dimensions differ from baseline',\n severity: 'moderate',\n };\n }\n if (ratio > DRIFT_THRESHOLD) {\n return {\n message: `visual drift ${(ratio * 100).toFixed(1)}% vs baseline`,\n severity: 'moderate',\n detail: `${(ratio * 100).toFixed(2)}% of pixels changed`,\n };\n }\n return null;\n};\n\n/**\n * Whether the alt-brand capture actually activated that brand.\n *\n * `captureUnderBrand` requests a brand with `?brand=<id>`, which only works if the app\n * reads that param and re-resolves its `ThemeProvider`. Most apps deliberately do not:\n * the brand is build configuration, not a runtime switch, and shipping a query-param\n * brand switcher into every generated app is not something a scaffold should impose.\n *\n * Without this check, such an app captures the same brand twice, and the identical\n * screenshots are reported as \"chrome did not re-skin\" — a false failure for code that is\n * entirely correct. The brand is nominated via `data-brand` on the root element (written\n * by `@keboola/design`'s ThemeProvider), so absence of the requested id means the request\n * was ignored, not that the chrome failed to re-skin.\n */\nconst altBrandWasApplied = (altDom: string, brand: string): boolean =>\n altDom.includes(`data-brand=\"${brand}\"`);\n\nconst altBrandFinding = (\n screenshot: Uint8Array,\n altScreenshot: Uint8Array,\n brand: string,\n altDom: string,\n): Finding | null => {\n if (!altBrandWasApplied(altDom, brand)) {\n return {\n message: `alt-brand re-skin not assessed: the app did not apply '${brand}' from ?brand=`,\n severity: 'minor',\n detail:\n 'Brand is usually build configuration rather than a runtime switch. To have this ' +\n 'axis assess re-skinning, register the alternate brand and resolve the active id ' +\n 'from the `brand` query parameter.',\n };\n }\n\n const { ratio } = pngMismatchRatio(screenshot, altScreenshot, RESKIN_PIXEL_THRESHOLD);\n // A future refinement would assert categorical colors (Badge/Alert/ModalIcon) stay\n // fixed pixel-wise; that is out of scope here — we only require the chrome to re-skin.\n if (ratio < RESKIN_THRESHOLD) {\n return {\n message: `chrome did not re-skin under alternate brand '${brand}'`,\n severity: 'serious',\n detail: `${(ratio * 100).toFixed(2)}% of pixels changed`,\n };\n }\n return null;\n};\n\n/**\n * Non-fatal `moderate` findings for a brand's interactive foreground↔background\n * pairings that fall below WCAG-AA (button/link labels on branded fills). Surfaces\n * the guarantee `@keboola/brand-registry` computes so an AA-failing token is a\n * build-time warning here, not a runtime axe surprise. Empty for an AA-safe brand.\n */\nexport const interactiveContrastFindings = (brand: Brand): Finding[] =>\n checkInteractiveContrast(brand).map((warning) => ({\n message: `brand '${brand.id}' ${warning.message}`,\n severity: 'moderate',\n detail: `${warning.foreground} on ${warning.background} (${warning.palette})`,\n }));\n\nconst overflowFinding = async (page: Page): Promise<Finding | null> => {\n const overflows = await page.evaluate(\n () => document.documentElement.scrollWidth > document.documentElement.clientWidth,\n );\n if (overflows) {\n return { message: 'horizontal layout overflow', severity: 'moderate' };\n }\n return null;\n};\n\n/**\n * A4 · Visual & brand-correctness verdict — UT-4495.\n *\n * Baseline diff of `context.artifact.screenshot` vs `context.baseline`\n * (pixelmatch + pngjs), plus the alt-brand fitness function via\n * `context.captureUnderBrand`: the chrome must visibly re-skin under an\n * alternate brand. Flags layout overflow. Each check degrades to no finding when\n * its input is absent. (Asserting categorical colors — Badge/Alert/ModalIcon —\n * stay fixed pixel-wise is future work; see `altBrandFinding`.)\n */\nexport const visualBrandAxis: Axis = {\n name: 'visual-brand',\n run: async (context) => {\n const { artifact, baseline, captureUnderBrand, page } = context;\n const findings: Finding[] = [];\n\n // 1. Baseline diff — first run has no baseline, so nothing to compare.\n if (baseline !== undefined) {\n const finding = baselineFinding(artifact.screenshot, baseline);\n if (finding !== null) findings.push(finding);\n }\n\n // 2. Alt-brand fitness function — chrome must visibly re-skin, and the alt\n // brand's interactive tokens must themselves be AA-safe.\n if (captureUnderBrand !== undefined) {\n const altArtifact = await captureUnderBrand(DEFAULT_ALT_BRAND);\n const finding = altBrandFinding(\n artifact.screenshot,\n altArtifact.screenshot,\n DEFAULT_ALT_BRAND,\n altArtifact.dom,\n );\n if (finding !== null) findings.push(finding);\n\n const altBrand = brands.find((brand) => brand.id === DEFAULT_ALT_BRAND);\n if (altBrand !== undefined) findings.push(...interactiveContrastFindings(altBrand));\n }\n\n // 3. Layout overflow — needs a live page.\n if (page !== undefined) {\n const finding = await overflowFinding(page);\n if (finding !== null) findings.push(finding);\n }\n\n // Fail only on blocking severities; moderate/minor are reported but non-fatal.\n const pass = !findings.some((f) => f.severity === 'critical' || f.severity === 'serious');\n\n return { axis: 'visual-brand', pass, findings };\n },\n};\n","import type { Axis } from '../types';\n\nimport { accessibilityAxis } from './accessibility';\nimport { briefConformanceAxis } from './brief-conformance';\nimport { compareAxis } from './compare';\nimport { runtimeHealthAxis } from './runtime-health';\nimport { visualBrandAxis } from './visual-brand';\n\nexport { accessibilityAxis } from './accessibility';\nexport { briefConformanceAxis } from './brief-conformance';\nexport { compareAxis } from './compare';\nexport { runtimeHealthAxis } from './runtime-health';\nexport { visualBrandAxis } from './visual-brand';\n\n/**\n * The registered axes, in report order. `compare` runs last and only activates\n * when a comparison snapshot is supplied, so ordinary single-page runs are\n * unaffected.\n */\nexport const ALL_AXES: Axis[] = [\n runtimeHealthAxis,\n accessibilityAxis,\n visualBrandAxis,\n briefConformanceAxis,\n compareAxis,\n];\n","import { type Browser, chromium } from 'playwright';\n\nimport type { CompareConfig, CompareSnapshot } from './compare/types';\nimport { ALL_AXES } from './axes';\nimport { capturePage, DESKTOP_VIEWPORT } from './capture';\nimport type { AggregateVerdict, Axis, AxisContext, Verdict, Viewport } from './types';\n\n/**\n * Run each axis against the shared context, isolating failures: an axis that\n * throws becomes a failing verdict instead of aborting the whole run. The error\n * is logged (never swallowed) and surfaced as a critical finding.\n */\nexport const runAxes = async (axes: Axis[], context: AxisContext): Promise<Verdict[]> => {\n const verdicts: Verdict[] = [];\n for (const axis of axes) {\n try {\n verdicts.push(await axis.run(context));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n console.error(`validate-ui: axis \"${axis.name}\" threw:`, error);\n verdicts.push({\n axis: axis.name,\n pass: false,\n findings: [{ message: `Axis crashed: ${message}`, severity: 'critical' }],\n });\n }\n }\n return verdicts;\n};\n\n/** A run passes only when every axis passed. */\nexport const aggregatePass = (verdicts: Verdict[]): boolean => verdicts.every((v) => v.pass);\n\nconst withBrandParam = (url: string, brand: string): string => {\n const parsed = new URL(url);\n parsed.searchParams.set('brand', brand);\n return parsed.toString();\n};\n\nexport type ValidateOptions = {\n url: string;\n brief?: string;\n baseline?: Uint8Array;\n /** Viewport axes run against (a11y/visual use one). Defaults to desktop. */\n viewport?: Viewport;\n /** Override the axis set — defaults to all four registered axes. */\n axes?: Axis[];\n /** Reuse a browser instead of launching one. */\n browser?: Browser;\n timeoutMs?: number;\n /** The \"old\" UI reduced to regions — enables the compare axis when present. */\n comparison?: CompareSnapshot;\n /** Allowlist + state markers for the compare axis. */\n compareConfig?: CompareConfig;\n};\n\n/**\n * The closed loop: render `url`, capture ground truth, and run every axis over\n * it, returning one machine-readable {@link AggregateVerdict}. Keeps the page\n * open for axes that need a live DOM (a11y, visual) and provides\n * `captureUnderBrand` for the alt-brand fitness function.\n */\nexport const validate = async (options: ValidateOptions): Promise<AggregateVerdict> => {\n const viewport = options.viewport ?? DESKTOP_VIEWPORT;\n const axes = options.axes ?? ALL_AXES;\n const browser = options.browser ?? (await chromium.launch());\n const ownsBrowser = !options.browser;\n const context = await browser.newContext({\n viewport: { width: viewport.width, height: viewport.height },\n });\n const page = await context.newPage();\n try {\n const artifact = await capturePage(page, options.url, viewport, options.timeoutMs);\n const axisContext: AxisContext = {\n artifact,\n page,\n browser,\n brief: options.brief,\n baseline: options.baseline,\n comparison: options.comparison,\n compareConfig: options.compareConfig,\n captureUnderBrand: async (brand) => {\n const brandContext = await browser.newContext({\n viewport: { width: viewport.width, height: viewport.height },\n });\n const brandPage = await brandContext.newPage();\n try {\n return await capturePage(\n brandPage,\n withBrandParam(options.url, brand),\n viewport,\n options.timeoutMs,\n );\n } finally {\n await brandContext.close();\n }\n },\n };\n const verdicts = await runAxes(axes, axisContext);\n return { url: options.url, pass: aggregatePass(verdicts), verdicts };\n } finally {\n await context.close();\n if (ownsBrowser) await browser.close();\n }\n};\n","import type { CompareFinding } from './types';\n\n/** One route's compare findings. */\nexport type RouteFindings = {\n route: string;\n findings: CompareFinding[];\n};\n\nexport type DedupResult = {\n /** Findings seen on ≥2 routes, collapsed to one entry tagged with the route count. */\n sharedChrome: CompareFinding[];\n /** Per-route findings with the shared-chrome copies removed. */\n perRoute: RouteFindings[];\n};\n\n// A delta is \"the same delta\" when its signature and kind match; the signature\n// already encodes region key + tokens, so identical shared chrome collapses.\nconst groupKey = (finding: CompareFinding): string => `${finding.kind}::${finding.valueSignature}`;\n\n/**\n * Collapse deltas that recur across routes into a single `shared-chrome` finding.\n * A value flagged on a shared header/daily-brief element would otherwise be\n * multiplied across every route (the prototype's `8.4/7.7/-8%/67%` on all 10\n * pages); here it is reported once, tagged with the routes it spans, and dropped\n * from each route's own list. Pure — no browser, no I/O.\n */\nexport const dedupeSharedChrome = (perRoute: RouteFindings[]): DedupResult => {\n const routesByGroup = new Map<string, Set<string>>();\n for (const { route, findings } of perRoute) {\n for (const finding of findings) {\n const key = groupKey(finding);\n const routes = routesByGroup.get(key) ?? new Set<string>();\n routes.add(route);\n routesByGroup.set(key, routes);\n }\n }\n\n const sharedGroups = new Set(\n [...routesByGroup].filter(([, routes]) => routes.size >= 2).map(([key]) => key),\n );\n\n const sharedChrome: CompareFinding[] = [];\n const seenShared = new Set<string>();\n const trimmedPerRoute: RouteFindings[] = perRoute.map(({ route, findings }) => ({\n route,\n findings: findings.filter((finding) => !sharedGroups.has(groupKey(finding))),\n }));\n\n for (const { findings } of perRoute) {\n for (const finding of findings) {\n const key = groupKey(finding);\n if (!sharedGroups.has(key) || seenShared.has(key)) continue;\n seenShared.add(key);\n const routes = [...(routesByGroup.get(key) ?? [])].sort();\n sharedChrome.push({\n ...finding,\n message: `${finding.message} (shared across ${routes.length} routes: ${routes.join(', ')})`,\n });\n }\n }\n\n return { sharedChrome, perRoute: trimmedPerRoute };\n};\n","import { join } from 'node:path';\nimport { type Browser, chromium } from 'playwright';\n\nimport { DESKTOP_VIEWPORT } from '../capture';\nimport { prepareForScreenshot } from '../prepare-screenshot';\nimport type { Viewport } from '../types';\n\nimport { dedupeSharedChrome, type RouteFindings } from './dedup';\nimport { comparePass, diffSnapshots } from './diff';\nimport { captureSnapshot, loadSnapshot } from './snapshot';\nimport type { CompareConfig, CompareFinding, CompareSnapshot } from './types';\n\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\n/** How the \"old\" side is sourced — booted live or loaded from recorded snapshots, never both. */\ntype OldSource =\n | { oldBaseUrl: string; oldSnapshotDir?: never }\n | { oldSnapshotDir: string; oldBaseUrl?: never };\n\nexport type CompareRoutesOptions = OldSource & {\n /** Base URL of the new build, joined with each route. */\n newBaseUrl: string;\n routes: string[];\n config?: CompareConfig;\n browser?: Browser;\n viewport?: Viewport;\n timeoutMs?: number;\n};\n\nexport type RouteCompareResult = { route: string; findings: CompareFinding[]; pass: boolean };\n\nexport type CompareRoutesResult = {\n routes: RouteCompareResult[];\n /** Deltas seen on ≥2 routes, reported once (e.g. shared chrome / daily-brief). */\n sharedChrome: CompareFinding[];\n pass: boolean;\n};\n\nconst joinUrl = (base: string, route: string): string =>\n `${base.replace(/\\/$/, '')}/${route.replace(/^\\//, '')}`;\n\n/** Recorded old snapshot filename for a route: `/a/b` → `a_b.json`, `/` → `index.json`. */\nexport const routeSnapshotFile = (route: string): string =>\n `${route.replace(/^\\//, '').replace(/\\/+$/, '').replace(/\\//g, '_') || 'index'}.json`;\n\n/**\n * Multi-route semantic compare. Boots each route on the new side (and the old\n * side too, unless recorded snapshots are supplied), diffs per route, then\n * collapses deltas that recur across routes into a single shared-chrome entry so\n * one shared element is not reported as N regressions. Exactly one of\n * `oldBaseUrl` / `oldSnapshotDir` must be given.\n */\nexport const compareRoutes = async (\n options: CompareRoutesOptions,\n): Promise<CompareRoutesResult> => {\n if ((options.oldBaseUrl === undefined) === (options.oldSnapshotDir === undefined)) {\n throw new Error('compareRoutes: provide exactly one of oldBaseUrl or oldSnapshotDir');\n }\n const viewport = options.viewport ?? DESKTOP_VIEWPORT;\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const browser = options.browser ?? (await chromium.launch());\n const ownsBrowser = options.browser === undefined;\n\n const { oldBaseUrl, oldSnapshotDir } = options;\n\n const snapshotUrl = async (url: string, route: string): Promise<CompareSnapshot> => {\n const context = await browser.newContext({\n viewport: { width: viewport.width, height: viewport.height },\n });\n const page = await context.newPage();\n try {\n await page.goto(url, { waitUntil: 'networkidle', timeout: timeoutMs });\n await prepareForScreenshot(page);\n return await captureSnapshot(page, route, options.config);\n } finally {\n await context.close();\n }\n };\n\n // The XOR guard above proves one branch holds; the trailing throw both narrows\n // the type (no `as`) and defends the invariant.\n const oldSnapshotFor = (route: string): Promise<CompareSnapshot> => {\n if (oldBaseUrl !== undefined) return snapshotUrl(joinUrl(oldBaseUrl, route), route);\n if (oldSnapshotDir !== undefined) {\n return loadSnapshot(join(oldSnapshotDir, routeSnapshotFile(route)));\n }\n throw new Error('compareRoutes: provide exactly one of oldBaseUrl or oldSnapshotDir');\n };\n\n try {\n const perRoute: RouteFindings[] = [];\n for (const route of options.routes) {\n const oldSnapshot = await oldSnapshotFor(route);\n const newSnapshot = await snapshotUrl(joinUrl(options.newBaseUrl, route), route);\n perRoute.push({ route, findings: diffSnapshots(oldSnapshot, newSnapshot, options.config) });\n }\n\n const { sharedChrome, perRoute: trimmed } = dedupeSharedChrome(perRoute);\n const routes: RouteCompareResult[] = trimmed.map(({ route, findings }) => ({\n route,\n findings,\n pass: comparePass(findings),\n }));\n const pass = comparePass(sharedChrome) && routes.every((route) => route.pass);\n return { routes, sharedChrome, pass };\n } finally {\n if (ownsBrowser) await browser.close();\n }\n};\n"],"mappings":"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"}
|