@keboola/validate-ui 0.1.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 +80 -0
- package/dist/cli.cjs +637 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +628 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +609 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +197 -0
- package/dist/index.d.ts +197 -0
- package/dist/index.js +586 -0
- package/dist/index.js.map +1 -0
- package/package.json +80 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/prepare-screenshot.ts","../src/capture.ts","../src/serve.ts","../src/axes/accessibility.ts","../src/axes/vlm-provider.ts","../src/axes/brief-conformance.ts","../src/axes/runtime-health.ts","../src/axes/visual-brand.ts","../src/axes/index.ts","../src/aggregate.ts"],"names":["resolve","console","chromium"],"mappings":"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","file":"index.js","sourcesContent":["import type { Browser, Page } from 'playwright';\n\n/** A named viewport the harness renders and captures at. */\nexport type Viewport = {\n label: string;\n width: number;\n height: number;\n};\n\n/** A single console / page-error message emitted while the page loaded. */\nexport type ConsoleEntry = {\n type: string;\n text: string;\n location?: string;\n};\n\n/** A network response or failed request observed during load. */\nexport type NetworkEntry = {\n url: string;\n method: string;\n /** HTTP status, or 0 for a request that failed before a response. */\n status: number;\n failure?: string;\n};\n\n/**\n * Ground truth captured from a rendered page — the input every verdict axis\n * reasons over. Produced by {@link capture}; never contains live browser handles\n * so it is safe to serialize and pass to a VLM.\n */\nexport type CaptureArtifact = {\n url: string;\n viewport: Viewport;\n /** Full-page PNG bytes. */\n screenshot: Uint8Array;\n /** Serialized outer HTML of the document after load. */\n dom: string;\n /**\n * Trimmed `body.innerText` read from the live DOM when available. An empty\n * string is a reliable white-screen signal — more robust than parsing\n * {@link dom}. Absent only when an artifact is built without a live page.\n */\n renderedText?: string;\n console: ConsoleEntry[];\n network: NetworkEntry[];\n};\n\nexport const SEVERITIES = ['critical', 'serious', 'moderate', 'minor'] as const;\nexport type Severity = (typeof SEVERITIES)[number];\n\n/** One problem an axis found. */\nexport type Finding = {\n message: string;\n severity: Severity;\n /** Optional extra context — a stack, a WCAG id, a selector, a diff ratio. */\n detail?: string;\n /** Optional pointer — WCAG ref, DOM selector, request URL, etc. */\n ref?: string;\n};\n\n/** The result of running one axis against one page. */\nexport type Verdict = {\n axis: string;\n pass: boolean;\n findings: Finding[];\n};\n\n/** The combined result of running every axis against one page. */\nexport type AggregateVerdict = {\n url: string;\n pass: boolean;\n verdicts: Verdict[];\n};\n\n/**\n * Everything an axis may need. Different axes use different fields — a11y and\n * visual axes need the live {@link Page}; brief-conformance needs the brief;\n * the visual axis re-renders under an alternate brand via\n * {@link AxisContext.captureUnderBrand}. All optional fields may be absent, and\n * an axis must degrade to a clear finding rather than throw when its input is\n * missing.\n */\nexport type AxisContext = {\n artifact: CaptureArtifact;\n /** Live page for the primary capture, when the run keeps a browser open. */\n page?: Page;\n /** Shared browser, for axes that need to render additional pages. */\n browser?: Browser;\n /** The natural-language brief the UI was generated from. */\n brief?: string;\n /** A prior screenshot to diff the current one against. */\n baseline?: Uint8Array;\n /** Re-render the same route under a different brand id and capture it. */\n captureUnderBrand?: (brand: string) => Promise<CaptureArtifact>;\n};\n\n/**\n * A verdict axis. Each of the four axes (runtime-health, accessibility,\n * visual-brand, brief-conformance) implements this contract in its own file so\n * they can be built independently. `run` must resolve to a {@link Verdict} and\n * should not throw for expected-missing input — the aggregator converts a throw\n * into a failing verdict, but a clear finding is better.\n */\nexport type Axis = {\n name: string;\n run: (context: AxisContext) => Promise<Verdict>;\n};\n","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 axe from 'axe-core';\n\nimport type { Axis, Finding, Severity, Verdict } from '../types';\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 addScriptTag. */\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 await page.addScriptTag({ content: axe.source });\n // axe.source may fail to define window.axe (e.g. a target-page CSP blocks the\n // injected script) — detect that in-page and surface a clear finding instead\n // 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 const findings = results.violations.map(toFinding);\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 { 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 { 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// 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 = (a: Uint8Array, b: Uint8Array): 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: 0.1,\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\nconst altBrandFinding = (\n screenshot: Uint8Array,\n altScreenshot: Uint8Array,\n brand: string,\n): Finding | null => {\n const { ratio } = pngMismatchRatio(screenshot, altScreenshot);\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\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.\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 );\n if (finding !== null) findings.push(finding);\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 { runtimeHealthAxis } from './runtime-health';\nimport { visualBrandAxis } from './visual-brand';\n\nexport { accessibilityAxis } from './accessibility';\nexport { briefConformanceAxis } from './brief-conformance';\nexport { runtimeHealthAxis } from './runtime-health';\nexport { visualBrandAxis } from './visual-brand';\n\n/** The four axes, in report order. */\nexport const ALL_AXES: Axis[] = [\n runtimeHealthAxis,\n accessibilityAxis,\n visualBrandAxis,\n briefConformanceAxis,\n];\n","import { type Browser, chromium } from 'playwright';\n\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};\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 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"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@keboola/validate-ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Closed-loop validation for agent-generated Keboola UI — boots a module headless, drives it, and returns a machine-readable verdict (runtime health, accessibility, visual/brand, brief-conformance)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "./dist/index.cjs",
|
|
11
|
+
"module": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"bin": "./dist/cli.js",
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"sideEffects": false,
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"import": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"default": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"require": {
|
|
26
|
+
"types": "./dist/index.d.cts",
|
|
27
|
+
"default": "./dist/index.cjs"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"type-check": "tsc",
|
|
33
|
+
"lint": "pnpm lint:format && pnpm lint:oxlint",
|
|
34
|
+
"lint:fix": "oxlint --fix . && oxfmt",
|
|
35
|
+
"lint:oxlint": "oxlint . --quiet",
|
|
36
|
+
"lint:format": "oxfmt --check",
|
|
37
|
+
"knip:check": "pnpm --dir ../.. exec knip --workspace packages/validate-ui",
|
|
38
|
+
"knip:fix": "pnpm --dir ../.. exec knip --workspace packages/validate-ui --fix-type types,exports,files",
|
|
39
|
+
"test": "vitest run --passWithNoTests --reporter=dot",
|
|
40
|
+
"transit": "pnpm build",
|
|
41
|
+
"build": "tsup",
|
|
42
|
+
"build:check": "attw --pack ."
|
|
43
|
+
},
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "https://github.com/keboola/ui.git",
|
|
47
|
+
"directory": "packages/validate-ui"
|
|
48
|
+
},
|
|
49
|
+
"keywords": [
|
|
50
|
+
"keboola",
|
|
51
|
+
"validate",
|
|
52
|
+
"playwright",
|
|
53
|
+
"accessibility",
|
|
54
|
+
"visual-regression",
|
|
55
|
+
"agent",
|
|
56
|
+
"cli"
|
|
57
|
+
],
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@anthropic-ai/sdk": "^0.100.1",
|
|
60
|
+
"axe-core": "^4.10.2",
|
|
61
|
+
"pixelmatch": "^7.1.0",
|
|
62
|
+
"playwright": "1.60.0",
|
|
63
|
+
"pngjs": "^7.0.0",
|
|
64
|
+
"zod": "^4.3.6"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@arethetypeswrong/cli": "^0.18.2",
|
|
68
|
+
"@keboola/oxlint-config": "workspace:^",
|
|
69
|
+
"@keboola/tsconfig": "workspace:^",
|
|
70
|
+
"@types/node": "24.12.0",
|
|
71
|
+
"@types/pngjs": "^6.0.5",
|
|
72
|
+
"esbuild": "^0.28.1",
|
|
73
|
+
"oxfmt": "^0.57.0",
|
|
74
|
+
"oxlint": "^1.72.0",
|
|
75
|
+
"tsup": "^8.5.1",
|
|
76
|
+
"typescript": "^6.0.3",
|
|
77
|
+
"vite": "^8.0.16",
|
|
78
|
+
"vitest": "^4.1.8"
|
|
79
|
+
}
|
|
80
|
+
}
|