@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
package/AGENTS.md
CHANGED
|
@@ -8,6 +8,7 @@ rendered result. Reach for it after generating/scaffolding a module, before a PR
|
|
|
8
8
|
- **Five axes** live one-per-file in `src/axes/` (`runtime-health`, `a11y`, `visual-brand`, `brief-conformance`, `compare`), each implementing the `Axis` contract in `src/types.ts`. Add an axis: implement `{ name, run }`, register it in `src/axes/index.ts`.
|
|
9
9
|
- **Contract boundary:** everything an axis needs is on `AxisContext` (`artifact`, live `page`, `brief`, `baseline`, `captureUnderBrand`, `comparison`, `compareConfig`). Don't reach outside it.
|
|
10
10
|
- **Compare (old-vs-new)** is semantic, not a value multiset — `src/compare/`. Both sides reduce to a serializable `CompareSnapshot` (regions with key/state/values) via `extractRegions`; the pure `diffSnapshots` localizes every delta to a region, suppresses declared `expectedAbsent` deltas (recorded, not dropped), and reports an empty/error `region-degraded` instead of value loss. Multi-route: `validate-ui compare --new <url> (--old <url> | --old-snapshots <dir>) --routes a,b,c [--config f.json]` — `dedupeSharedChrome` collapses a delta seen on ≥2 routes into one finding so shared chrome isn't multiplied. The `compare` axis only activates when `context.comparison` is set, so single-page runs are unaffected.
|
|
11
|
+
- **Viewport is a judgement, not just a size.** `validate({ viewport })` / `--viewport <WIDTHxHEIGHT | desktop | mobile | phone>` renders at that size, and `isPhoneViewport` (< 768px) is what makes horizontal overflow `critical` instead of `moderate`. An axis that cares how narrow the page is reads `context.artifact.viewport` and asks `isPhoneViewport`, never a literal width.
|
|
11
12
|
- **Do not** depend on `@keboola/e2e-testing` (private) — the screenshot-prep helper is reimplemented in `src/prepare-screenshot.ts`.
|
|
12
13
|
- **Want one pass/fail number + a gate exit code**, not raw findings? Use `@keboola/ui-gen-bench` — `ui-gen-bench eval --serve <dir> --route <path> --brief "…"` boots via this validator, scores the verdict, and exits 0/1. That's the per-generation gate; this package is the validator underneath it.
|
|
13
14
|
- **VLM backend** for `brief-conformance` is chosen in `src/axes/vlm-provider.ts`: Keboola LLM proxy (base URL `VALIDATE_UI_LLM_BASE_URL` → `ANTHROPIC_BASE_URL`; token `VALIDATE_UI_LLM_TOKEN` → `KBC_TOKEN` → `ANTHROPIC_API_KEY` — so kai-agent's SDK-native `ANTHROPIC_BASE_URL` + `ANTHROPIC_API_KEY` pair works, preferred) → raw `ANTHROPIC_API_KEY` (no proxy base URL) → skip. Route new VLM calls through `resolveVlmProvider()`, never `new Anthropic()` directly.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# @keboola/validate-ui
|
|
2
2
|
|
|
3
|
+
## 0.6.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Runtime health no longer fails a page because an error string such as "something went wrong" appears only inside an inline `<script>`, `<style>`, `<template>`, `<noscript>` or comment. Error-boundary copy is matched against the rendered text, and dev-server overlay names are matched against the DOM with those bodies removed.
|
|
8
|
+
|
|
9
|
+
- Updated dependencies:
|
|
10
|
+
- @keboola/brand-registry@1.10.0
|
|
11
|
+
|
|
12
|
+
## 0.6.0
|
|
13
|
+
|
|
14
|
+
### Minor Changes
|
|
15
|
+
|
|
16
|
+
- Horizontal overflow is blocking below 768px wide
|
|
17
|
+
|
|
18
|
+
`visual-brand` weighs the finding by the width it was measured at: `critical` on a phone-class
|
|
19
|
+
viewport, where the reader has to scroll sideways to finish a sentence, and `moderate` as before at
|
|
20
|
+
desktop widths. Nothing changes for an existing caller — `validate()` still renders at 1280x800
|
|
21
|
+
unless given a `viewport`.
|
|
22
|
+
|
|
23
|
+
`--viewport <WIDTHxHEIGHT | desktop | mobile | phone>` is now on the CLI, and `parseViewport`,
|
|
24
|
+
`PHONE_VIEWPORT`, `PHONE_MAX_WIDTH`, `VIEWPORT_PRESETS` and `isPhoneViewport` are exported.
|
|
25
|
+
|
|
3
26
|
## 0.5.3
|
|
4
27
|
|
|
5
28
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -8,8 +8,8 @@ human looks:
|
|
|
8
8
|
- **runtime-health** — white-screens, console/page errors, failed requests
|
|
9
9
|
- **a11y** — axe-core violations mapped to WCAG
|
|
10
10
|
- **visual-brand** — baseline diff + the alt-brand fitness function (chrome must
|
|
11
|
-
re-skin under an alternate brand) + layout overflow
|
|
12
|
-
colors stay fixed is future work)
|
|
11
|
+
re-skin under an alternate brand) + layout overflow, blocking below 768px wide
|
|
12
|
+
(asserting categorical colors stay fixed is future work)
|
|
13
13
|
- **brief-conformance** — a VLM judge on "did it build what the brief asked,
|
|
14
14
|
and are the empty/loading/error states present?"
|
|
15
15
|
|
|
@@ -33,10 +33,19 @@ validate-ui --url http://localhost:5173 --brief "Users list with empty state"
|
|
|
33
33
|
|
|
34
34
|
# Serve a built SPA dir and validate a route
|
|
35
35
|
validate-ui --serve apps/boilerplate/dist --route /tokens --json
|
|
36
|
+
|
|
37
|
+
# Judge the page at a phone size instead of the 1280x800 default
|
|
38
|
+
validate-ui --url http://localhost:5173 --viewport 375x812
|
|
36
39
|
```
|
|
37
40
|
|
|
38
41
|
Exit code is `0` when every axis passes, `1` otherwise.
|
|
39
42
|
|
|
43
|
+
`--viewport` takes `WIDTHxHEIGHT` or one of `desktop` | `mobile` | `phone`.
|
|
44
|
+
Below 768px wide a horizontal layout overflow is a **critical** finding rather
|
|
45
|
+
than a moderate one: the reader has to scroll sideways to finish a sentence, so
|
|
46
|
+
the screen is broken and the verdict fails. Omitting the flag renders at
|
|
47
|
+
1280x800 and is unchanged.
|
|
48
|
+
|
|
40
49
|
## Programmatic
|
|
41
50
|
|
|
42
51
|
```ts
|
package/dist/cli.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const require_runner = require("./runner-
|
|
2
|
+
const require_runner = require("./runner-BwEjSueH.cjs");
|
|
3
3
|
let node_fs_promises = require("node:fs/promises");
|
|
4
4
|
let node_path = require("node:path");
|
|
5
5
|
let node_process = require("node:process");
|
|
@@ -8,8 +8,8 @@ node_process = require_runner.__toESM(node_process, 1);
|
|
|
8
8
|
const HELP = `@keboola/validate-ui — closed-loop validation for generated Keboola UI
|
|
9
9
|
|
|
10
10
|
Usage:
|
|
11
|
-
validate-ui --url <url> [--brief <text>] [--json]
|
|
12
|
-
validate-ui --serve <dir> [--route <path>] [--brief <text>] [--json]
|
|
11
|
+
validate-ui --url <url> [--brief <text>] [--viewport <size>] [--json]
|
|
12
|
+
validate-ui --serve <dir> [--route <path>] [--brief <text>] [--viewport <size>] [--json]
|
|
13
13
|
validate-ui compare --new <baseUrl> (--old <baseUrl> | --old-snapshots <dir>) --routes <a,b,c> [--config <file>] [--json]
|
|
14
14
|
|
|
15
15
|
Options:
|
|
@@ -17,6 +17,8 @@ Options:
|
|
|
17
17
|
--serve <dir> Serve a built SPA dir (e.g. apps/boilerplate/dist) and validate it
|
|
18
18
|
--route <path> Route to open when using --serve (default: /)
|
|
19
19
|
--brief <text> The brief the UI was generated from (feeds brief-conformance)
|
|
20
|
+
--viewport <size> WIDTHxHEIGHT, or desktop | mobile | phone (default: desktop).
|
|
21
|
+
Horizontal overflow is blocking below 768px wide.
|
|
20
22
|
--json Emit the verdict as JSON
|
|
21
23
|
--help, -h Show this help
|
|
22
24
|
|
|
@@ -114,6 +116,8 @@ const main = async () => {
|
|
|
114
116
|
}
|
|
115
117
|
const brief = flagValue(argv, "--brief");
|
|
116
118
|
const serveDir = flagValue(argv, "--serve");
|
|
119
|
+
const viewportSpec = flagValue(argv, "--viewport");
|
|
120
|
+
const viewport = viewportSpec === void 0 ? void 0 : require_runner.parseViewport(viewportSpec);
|
|
117
121
|
let url = flagValue(argv, "--url");
|
|
118
122
|
let server;
|
|
119
123
|
if (serveDir !== void 0) {
|
|
@@ -129,7 +133,8 @@ const main = async () => {
|
|
|
129
133
|
try {
|
|
130
134
|
const verdict = await require_runner.validate({
|
|
131
135
|
url,
|
|
132
|
-
brief
|
|
136
|
+
brief,
|
|
137
|
+
viewport
|
|
133
138
|
});
|
|
134
139
|
node_process.default.stdout.write((asJson ? JSON.stringify(verdict, null, 2) : formatText(verdict)) + "\n");
|
|
135
140
|
node_process.default.exitCode = verdict.pass ? 0 : 1;
|
package/dist/cli.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.cjs","names":["CompareConfig","readFile","resolve","compareRoutes","process","serveStatic","validate"],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { readFile } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport process from 'node:process';\n\nimport { compareRoutes, type CompareRoutesResult } from './compare/runner';\nimport { CompareConfig } from './compare/types';\nimport { validate } from './aggregate';\nimport { serveStatic } from './serve';\nimport type { AggregateVerdict } from './types';\n\nconst HELP = `@keboola/validate-ui — closed-loop validation for generated Keboola UI\n\nUsage:\n validate-ui --url <url> [--brief <text>] [--json]\n validate-ui --serve <dir> [--route <path>] [--brief <text>] [--json]\n validate-ui compare --new <baseUrl> (--old <baseUrl> | --old-snapshots <dir>) --routes <a,b,c> [--config <file>] [--json]\n\nOptions:\n --url <url> Render and validate a running URL\n --serve <dir> Serve a built SPA dir (e.g. apps/boilerplate/dist) and validate it\n --route <path> Route to open when using --serve (default: /)\n --brief <text> The brief the UI was generated from (feeds brief-conformance)\n --json Emit the verdict as JSON\n --help, -h Show this help\n\ncompare options:\n --new <baseUrl> Base URL of the new build\n --old <baseUrl> Base URL of the old build (booted live)\n --old-snapshots <dir> Directory of recorded old snapshots (alternative to --old)\n --routes <a,b,c> Comma-separated routes to compare\n --config <file> JSON compare config (expectedAbsent allowlist + state markers)\n\nExit code is 0 when every axis / route passes, 1 otherwise.\n`;\n\nconst flagValue = (argv: string[], name: string): string | undefined => {\n const index = argv.indexOf(name);\n if (index < 0) return undefined;\n const value = argv[index + 1];\n // A missing value, or the next token being another flag, means no value was\n // given (e.g. `--url --json`) — reject it so callers fail fast.\n return value === undefined || value.startsWith('-') ? undefined : value;\n};\n\nconst formatText = (verdict: AggregateVerdict): string => {\n const lines = [`validate-ui ${verdict.pass ? 'PASS' : 'FAIL'} — ${verdict.url}`];\n for (const axis of verdict.verdicts) {\n lines.push(` ${axis.pass ? '✓' : '✗'} ${axis.axis}`);\n for (const finding of axis.findings) {\n lines.push(` [${finding.severity}] ${finding.message}`);\n }\n }\n return lines.join('\\n');\n};\n\nconst formatCompareFinding = (finding: CompareRoutesResult['sharedChrome'][number]): string => {\n const tag = finding.suppressed !== undefined ? ' (suppressed)' : '';\n const detail = finding.detail !== undefined ? ` — ${finding.detail}` : '';\n return ` [${finding.severity}] ${finding.message}${detail}${tag}`;\n};\n\nconst formatCompare = (result: CompareRoutesResult): string => {\n const lines = [`validate-ui compare ${result.pass ? 'PASS' : 'FAIL'}`];\n if (result.sharedChrome.length > 0) {\n lines.push(' shared across routes:');\n for (const finding of result.sharedChrome) lines.push(formatCompareFinding(finding));\n }\n for (const route of result.routes) {\n lines.push(` ${route.pass ? '✓' : '✗'} ${route.route}`);\n for (const finding of route.findings) lines.push(formatCompareFinding(finding));\n }\n return lines.join('\\n');\n};\n\nconst loadCompareConfig = async (path: string | undefined): Promise<CompareConfig | undefined> => {\n if (path === undefined) return undefined;\n return CompareConfig.parse(JSON.parse(await readFile(resolve(path), 'utf8')));\n};\n\nconst runCompare = async (argv: string[], asJson: boolean): Promise<void> => {\n const newBaseUrl = flagValue(argv, '--new');\n const oldBaseUrl = flagValue(argv, '--old');\n const oldSnapshotDir = flagValue(argv, '--old-snapshots');\n const routes = flagValue(argv, '--routes')\n ?.split(',')\n .map((route) => route.trim())\n .filter((route) => route !== '');\n\n if (newBaseUrl === undefined || routes === undefined || routes.length === 0) {\n process.stderr.write('validate-ui compare: --new and --routes are required\\n\\n' + HELP);\n process.exitCode = 1;\n return;\n }\n if (oldBaseUrl !== undefined && oldSnapshotDir !== undefined) {\n process.stderr.write(\n 'validate-ui compare: pass only one of --old or --old-snapshots\\n\\n' + HELP,\n );\n process.exitCode = 1;\n return;\n }\n\n const config = await loadCompareConfig(flagValue(argv, '--config'));\n const base = { newBaseUrl, routes, config };\n // Branch so the XOR-typed options narrow without an assertion.\n let result: CompareRoutesResult;\n if (oldBaseUrl !== undefined) {\n result = await compareRoutes({ ...base, oldBaseUrl });\n } else if (oldSnapshotDir !== undefined) {\n result = await compareRoutes({ ...base, oldSnapshotDir });\n } else {\n process.stderr.write('validate-ui compare: provide --old or --old-snapshots\\n\\n' + HELP);\n process.exitCode = 1;\n return;\n }\n process.stdout.write((asJson ? JSON.stringify(result, null, 2) : formatCompare(result)) + '\\n');\n process.exitCode = result.pass ? 0 : 1;\n};\n\nconst main = async (): Promise<void> => {\n const argv = process.argv.slice(2);\n if (argv.includes('--help') || argv.includes('-h') || argv.length === 0) {\n process.stdout.write(HELP);\n return;\n }\n\n const asJson = argv.includes('--json');\n if (argv[0] === 'compare') {\n await runCompare(argv.slice(1), asJson);\n return;\n }\n\n const brief = flagValue(argv, '--brief');\n const serveDir = flagValue(argv, '--serve');\n\n let url = flagValue(argv, '--url');\n let server: Awaited<ReturnType<typeof serveStatic>> | undefined;\n if (serveDir !== undefined) {\n server = await serveStatic(resolve(serveDir));\n const route = flagValue(argv, '--route') ?? '/';\n url = `${server.url}${route.startsWith('/') ? route : `/${route}`}`;\n }\n\n if (url === undefined) {\n process.stderr.write('validate-ui: one of --url or --serve is required\\n\\n' + HELP);\n process.exitCode = 1;\n return;\n }\n\n try {\n const verdict = await validate({ url, brief });\n process.stdout.write((asJson ? JSON.stringify(verdict, null, 2) : formatText(verdict)) + '\\n');\n process.exitCode = verdict.pass ? 0 : 1;\n } finally {\n await server?.close();\n }\n};\n\nmain().catch((error: unknown) => {\n process.stderr.write(`validate-ui: ${error instanceof Error ? error.message : String(error)}\\n`);\n process.exitCode = 1;\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cli.cjs","names":["CompareConfig","readFile","resolve","compareRoutes","process","parseViewport","serveStatic","validate"],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { readFile } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport process from 'node:process';\n\nimport { compareRoutes, type CompareRoutesResult } from './compare/runner';\nimport { CompareConfig } from './compare/types';\nimport { validate } from './aggregate';\nimport { serveStatic } from './serve';\nimport type { AggregateVerdict } from './types';\nimport { parseViewport } from './viewport';\n\nconst HELP = `@keboola/validate-ui — closed-loop validation for generated Keboola UI\n\nUsage:\n validate-ui --url <url> [--brief <text>] [--viewport <size>] [--json]\n validate-ui --serve <dir> [--route <path>] [--brief <text>] [--viewport <size>] [--json]\n validate-ui compare --new <baseUrl> (--old <baseUrl> | --old-snapshots <dir>) --routes <a,b,c> [--config <file>] [--json]\n\nOptions:\n --url <url> Render and validate a running URL\n --serve <dir> Serve a built SPA dir (e.g. apps/boilerplate/dist) and validate it\n --route <path> Route to open when using --serve (default: /)\n --brief <text> The brief the UI was generated from (feeds brief-conformance)\n --viewport <size> WIDTHxHEIGHT, or desktop | mobile | phone (default: desktop).\n Horizontal overflow is blocking below 768px wide.\n --json Emit the verdict as JSON\n --help, -h Show this help\n\ncompare options:\n --new <baseUrl> Base URL of the new build\n --old <baseUrl> Base URL of the old build (booted live)\n --old-snapshots <dir> Directory of recorded old snapshots (alternative to --old)\n --routes <a,b,c> Comma-separated routes to compare\n --config <file> JSON compare config (expectedAbsent allowlist + state markers)\n\nExit code is 0 when every axis / route passes, 1 otherwise.\n`;\n\nconst flagValue = (argv: string[], name: string): string | undefined => {\n const index = argv.indexOf(name);\n if (index < 0) return undefined;\n const value = argv[index + 1];\n // A missing value, or the next token being another flag, means no value was\n // given (e.g. `--url --json`) — reject it so callers fail fast.\n return value === undefined || value.startsWith('-') ? undefined : value;\n};\n\nconst formatText = (verdict: AggregateVerdict): string => {\n const lines = [`validate-ui ${verdict.pass ? 'PASS' : 'FAIL'} — ${verdict.url}`];\n for (const axis of verdict.verdicts) {\n lines.push(` ${axis.pass ? '✓' : '✗'} ${axis.axis}`);\n for (const finding of axis.findings) {\n lines.push(` [${finding.severity}] ${finding.message}`);\n }\n }\n return lines.join('\\n');\n};\n\nconst formatCompareFinding = (finding: CompareRoutesResult['sharedChrome'][number]): string => {\n const tag = finding.suppressed !== undefined ? ' (suppressed)' : '';\n const detail = finding.detail !== undefined ? ` — ${finding.detail}` : '';\n return ` [${finding.severity}] ${finding.message}${detail}${tag}`;\n};\n\nconst formatCompare = (result: CompareRoutesResult): string => {\n const lines = [`validate-ui compare ${result.pass ? 'PASS' : 'FAIL'}`];\n if (result.sharedChrome.length > 0) {\n lines.push(' shared across routes:');\n for (const finding of result.sharedChrome) lines.push(formatCompareFinding(finding));\n }\n for (const route of result.routes) {\n lines.push(` ${route.pass ? '✓' : '✗'} ${route.route}`);\n for (const finding of route.findings) lines.push(formatCompareFinding(finding));\n }\n return lines.join('\\n');\n};\n\nconst loadCompareConfig = async (path: string | undefined): Promise<CompareConfig | undefined> => {\n if (path === undefined) return undefined;\n return CompareConfig.parse(JSON.parse(await readFile(resolve(path), 'utf8')));\n};\n\nconst runCompare = async (argv: string[], asJson: boolean): Promise<void> => {\n const newBaseUrl = flagValue(argv, '--new');\n const oldBaseUrl = flagValue(argv, '--old');\n const oldSnapshotDir = flagValue(argv, '--old-snapshots');\n const routes = flagValue(argv, '--routes')\n ?.split(',')\n .map((route) => route.trim())\n .filter((route) => route !== '');\n\n if (newBaseUrl === undefined || routes === undefined || routes.length === 0) {\n process.stderr.write('validate-ui compare: --new and --routes are required\\n\\n' + HELP);\n process.exitCode = 1;\n return;\n }\n if (oldBaseUrl !== undefined && oldSnapshotDir !== undefined) {\n process.stderr.write(\n 'validate-ui compare: pass only one of --old or --old-snapshots\\n\\n' + HELP,\n );\n process.exitCode = 1;\n return;\n }\n\n const config = await loadCompareConfig(flagValue(argv, '--config'));\n const base = { newBaseUrl, routes, config };\n // Branch so the XOR-typed options narrow without an assertion.\n let result: CompareRoutesResult;\n if (oldBaseUrl !== undefined) {\n result = await compareRoutes({ ...base, oldBaseUrl });\n } else if (oldSnapshotDir !== undefined) {\n result = await compareRoutes({ ...base, oldSnapshotDir });\n } else {\n process.stderr.write('validate-ui compare: provide --old or --old-snapshots\\n\\n' + HELP);\n process.exitCode = 1;\n return;\n }\n process.stdout.write((asJson ? JSON.stringify(result, null, 2) : formatCompare(result)) + '\\n');\n process.exitCode = result.pass ? 0 : 1;\n};\n\nconst main = async (): Promise<void> => {\n const argv = process.argv.slice(2);\n if (argv.includes('--help') || argv.includes('-h') || argv.length === 0) {\n process.stdout.write(HELP);\n return;\n }\n\n const asJson = argv.includes('--json');\n if (argv[0] === 'compare') {\n await runCompare(argv.slice(1), asJson);\n return;\n }\n\n const brief = flagValue(argv, '--brief');\n const serveDir = flagValue(argv, '--serve');\n const viewportSpec = flagValue(argv, '--viewport');\n const viewport = viewportSpec === undefined ? undefined : parseViewport(viewportSpec);\n\n let url = flagValue(argv, '--url');\n let server: Awaited<ReturnType<typeof serveStatic>> | undefined;\n if (serveDir !== undefined) {\n server = await serveStatic(resolve(serveDir));\n const route = flagValue(argv, '--route') ?? '/';\n url = `${server.url}${route.startsWith('/') ? route : `/${route}`}`;\n }\n\n if (url === undefined) {\n process.stderr.write('validate-ui: one of --url or --serve is required\\n\\n' + HELP);\n process.exitCode = 1;\n return;\n }\n\n try {\n const verdict = await validate({ url, brief, viewport });\n process.stdout.write((asJson ? JSON.stringify(verdict, null, 2) : formatText(verdict)) + '\\n');\n process.exitCode = verdict.pass ? 0 : 1;\n } finally {\n await server?.close();\n }\n};\n\nmain().catch((error: unknown) => {\n process.stderr.write(`validate-ui: ${error instanceof Error ? error.message : String(error)}\\n`);\n process.exitCode = 1;\n});\n"],"mappings":"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"}
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { m as CompareConfig, o as validate, t as compareRoutes, w as serveStatic } from "./runner-
|
|
2
|
+
import { k as parseViewport, m as CompareConfig, o as validate, t as compareRoutes, w as serveStatic } from "./runner-DH04MitR.js";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
5
|
import process from "node:process";
|
|
@@ -7,8 +7,8 @@ import process from "node:process";
|
|
|
7
7
|
const HELP = `@keboola/validate-ui — closed-loop validation for generated Keboola UI
|
|
8
8
|
|
|
9
9
|
Usage:
|
|
10
|
-
validate-ui --url <url> [--brief <text>] [--json]
|
|
11
|
-
validate-ui --serve <dir> [--route <path>] [--brief <text>] [--json]
|
|
10
|
+
validate-ui --url <url> [--brief <text>] [--viewport <size>] [--json]
|
|
11
|
+
validate-ui --serve <dir> [--route <path>] [--brief <text>] [--viewport <size>] [--json]
|
|
12
12
|
validate-ui compare --new <baseUrl> (--old <baseUrl> | --old-snapshots <dir>) --routes <a,b,c> [--config <file>] [--json]
|
|
13
13
|
|
|
14
14
|
Options:
|
|
@@ -16,6 +16,8 @@ Options:
|
|
|
16
16
|
--serve <dir> Serve a built SPA dir (e.g. apps/boilerplate/dist) and validate it
|
|
17
17
|
--route <path> Route to open when using --serve (default: /)
|
|
18
18
|
--brief <text> The brief the UI was generated from (feeds brief-conformance)
|
|
19
|
+
--viewport <size> WIDTHxHEIGHT, or desktop | mobile | phone (default: desktop).
|
|
20
|
+
Horizontal overflow is blocking below 768px wide.
|
|
19
21
|
--json Emit the verdict as JSON
|
|
20
22
|
--help, -h Show this help
|
|
21
23
|
|
|
@@ -113,6 +115,8 @@ const main = async () => {
|
|
|
113
115
|
}
|
|
114
116
|
const brief = flagValue(argv, "--brief");
|
|
115
117
|
const serveDir = flagValue(argv, "--serve");
|
|
118
|
+
const viewportSpec = flagValue(argv, "--viewport");
|
|
119
|
+
const viewport = viewportSpec === void 0 ? void 0 : parseViewport(viewportSpec);
|
|
116
120
|
let url = flagValue(argv, "--url");
|
|
117
121
|
let server;
|
|
118
122
|
if (serveDir !== void 0) {
|
|
@@ -128,7 +132,8 @@ const main = async () => {
|
|
|
128
132
|
try {
|
|
129
133
|
const verdict = await validate({
|
|
130
134
|
url,
|
|
131
|
-
brief
|
|
135
|
+
brief,
|
|
136
|
+
viewport
|
|
132
137
|
});
|
|
133
138
|
process.stdout.write((asJson ? JSON.stringify(verdict, null, 2) : formatText(verdict)) + "\n");
|
|
134
139
|
process.exitCode = verdict.pass ? 0 : 1;
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { readFile } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport process from 'node:process';\n\nimport { compareRoutes, type CompareRoutesResult } from './compare/runner';\nimport { CompareConfig } from './compare/types';\nimport { validate } from './aggregate';\nimport { serveStatic } from './serve';\nimport type { AggregateVerdict } from './types';\n\nconst HELP = `@keboola/validate-ui — closed-loop validation for generated Keboola UI\n\nUsage:\n validate-ui --url <url> [--brief <text>] [--json]\n validate-ui --serve <dir> [--route <path>] [--brief <text>] [--json]\n validate-ui compare --new <baseUrl> (--old <baseUrl> | --old-snapshots <dir>) --routes <a,b,c> [--config <file>] [--json]\n\nOptions:\n --url <url> Render and validate a running URL\n --serve <dir> Serve a built SPA dir (e.g. apps/boilerplate/dist) and validate it\n --route <path> Route to open when using --serve (default: /)\n --brief <text> The brief the UI was generated from (feeds brief-conformance)\n --json Emit the verdict as JSON\n --help, -h Show this help\n\ncompare options:\n --new <baseUrl> Base URL of the new build\n --old <baseUrl> Base URL of the old build (booted live)\n --old-snapshots <dir> Directory of recorded old snapshots (alternative to --old)\n --routes <a,b,c> Comma-separated routes to compare\n --config <file> JSON compare config (expectedAbsent allowlist + state markers)\n\nExit code is 0 when every axis / route passes, 1 otherwise.\n`;\n\nconst flagValue = (argv: string[], name: string): string | undefined => {\n const index = argv.indexOf(name);\n if (index < 0) return undefined;\n const value = argv[index + 1];\n // A missing value, or the next token being another flag, means no value was\n // given (e.g. `--url --json`) — reject it so callers fail fast.\n return value === undefined || value.startsWith('-') ? undefined : value;\n};\n\nconst formatText = (verdict: AggregateVerdict): string => {\n const lines = [`validate-ui ${verdict.pass ? 'PASS' : 'FAIL'} — ${verdict.url}`];\n for (const axis of verdict.verdicts) {\n lines.push(` ${axis.pass ? '✓' : '✗'} ${axis.axis}`);\n for (const finding of axis.findings) {\n lines.push(` [${finding.severity}] ${finding.message}`);\n }\n }\n return lines.join('\\n');\n};\n\nconst formatCompareFinding = (finding: CompareRoutesResult['sharedChrome'][number]): string => {\n const tag = finding.suppressed !== undefined ? ' (suppressed)' : '';\n const detail = finding.detail !== undefined ? ` — ${finding.detail}` : '';\n return ` [${finding.severity}] ${finding.message}${detail}${tag}`;\n};\n\nconst formatCompare = (result: CompareRoutesResult): string => {\n const lines = [`validate-ui compare ${result.pass ? 'PASS' : 'FAIL'}`];\n if (result.sharedChrome.length > 0) {\n lines.push(' shared across routes:');\n for (const finding of result.sharedChrome) lines.push(formatCompareFinding(finding));\n }\n for (const route of result.routes) {\n lines.push(` ${route.pass ? '✓' : '✗'} ${route.route}`);\n for (const finding of route.findings) lines.push(formatCompareFinding(finding));\n }\n return lines.join('\\n');\n};\n\nconst loadCompareConfig = async (path: string | undefined): Promise<CompareConfig | undefined> => {\n if (path === undefined) return undefined;\n return CompareConfig.parse(JSON.parse(await readFile(resolve(path), 'utf8')));\n};\n\nconst runCompare = async (argv: string[], asJson: boolean): Promise<void> => {\n const newBaseUrl = flagValue(argv, '--new');\n const oldBaseUrl = flagValue(argv, '--old');\n const oldSnapshotDir = flagValue(argv, '--old-snapshots');\n const routes = flagValue(argv, '--routes')\n ?.split(',')\n .map((route) => route.trim())\n .filter((route) => route !== '');\n\n if (newBaseUrl === undefined || routes === undefined || routes.length === 0) {\n process.stderr.write('validate-ui compare: --new and --routes are required\\n\\n' + HELP);\n process.exitCode = 1;\n return;\n }\n if (oldBaseUrl !== undefined && oldSnapshotDir !== undefined) {\n process.stderr.write(\n 'validate-ui compare: pass only one of --old or --old-snapshots\\n\\n' + HELP,\n );\n process.exitCode = 1;\n return;\n }\n\n const config = await loadCompareConfig(flagValue(argv, '--config'));\n const base = { newBaseUrl, routes, config };\n // Branch so the XOR-typed options narrow without an assertion.\n let result: CompareRoutesResult;\n if (oldBaseUrl !== undefined) {\n result = await compareRoutes({ ...base, oldBaseUrl });\n } else if (oldSnapshotDir !== undefined) {\n result = await compareRoutes({ ...base, oldSnapshotDir });\n } else {\n process.stderr.write('validate-ui compare: provide --old or --old-snapshots\\n\\n' + HELP);\n process.exitCode = 1;\n return;\n }\n process.stdout.write((asJson ? JSON.stringify(result, null, 2) : formatCompare(result)) + '\\n');\n process.exitCode = result.pass ? 0 : 1;\n};\n\nconst main = async (): Promise<void> => {\n const argv = process.argv.slice(2);\n if (argv.includes('--help') || argv.includes('-h') || argv.length === 0) {\n process.stdout.write(HELP);\n return;\n }\n\n const asJson = argv.includes('--json');\n if (argv[0] === 'compare') {\n await runCompare(argv.slice(1), asJson);\n return;\n }\n\n const brief = flagValue(argv, '--brief');\n const serveDir = flagValue(argv, '--serve');\n\n let url = flagValue(argv, '--url');\n let server: Awaited<ReturnType<typeof serveStatic>> | undefined;\n if (serveDir !== undefined) {\n server = await serveStatic(resolve(serveDir));\n const route = flagValue(argv, '--route') ?? '/';\n url = `${server.url}${route.startsWith('/') ? route : `/${route}`}`;\n }\n\n if (url === undefined) {\n process.stderr.write('validate-ui: one of --url or --serve is required\\n\\n' + HELP);\n process.exitCode = 1;\n return;\n }\n\n try {\n const verdict = await validate({ url, brief });\n process.stdout.write((asJson ? JSON.stringify(verdict, null, 2) : formatText(verdict)) + '\\n');\n process.exitCode = verdict.pass ? 0 : 1;\n } finally {\n await server?.close();\n }\n};\n\nmain().catch((error: unknown) => {\n process.stderr.write(`validate-ui: ${error instanceof Error ? error.message : String(error)}\\n`);\n process.exitCode = 1;\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { readFile } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport process from 'node:process';\n\nimport { compareRoutes, type CompareRoutesResult } from './compare/runner';\nimport { CompareConfig } from './compare/types';\nimport { validate } from './aggregate';\nimport { serveStatic } from './serve';\nimport type { AggregateVerdict } from './types';\nimport { parseViewport } from './viewport';\n\nconst HELP = `@keboola/validate-ui — closed-loop validation for generated Keboola UI\n\nUsage:\n validate-ui --url <url> [--brief <text>] [--viewport <size>] [--json]\n validate-ui --serve <dir> [--route <path>] [--brief <text>] [--viewport <size>] [--json]\n validate-ui compare --new <baseUrl> (--old <baseUrl> | --old-snapshots <dir>) --routes <a,b,c> [--config <file>] [--json]\n\nOptions:\n --url <url> Render and validate a running URL\n --serve <dir> Serve a built SPA dir (e.g. apps/boilerplate/dist) and validate it\n --route <path> Route to open when using --serve (default: /)\n --brief <text> The brief the UI was generated from (feeds brief-conformance)\n --viewport <size> WIDTHxHEIGHT, or desktop | mobile | phone (default: desktop).\n Horizontal overflow is blocking below 768px wide.\n --json Emit the verdict as JSON\n --help, -h Show this help\n\ncompare options:\n --new <baseUrl> Base URL of the new build\n --old <baseUrl> Base URL of the old build (booted live)\n --old-snapshots <dir> Directory of recorded old snapshots (alternative to --old)\n --routes <a,b,c> Comma-separated routes to compare\n --config <file> JSON compare config (expectedAbsent allowlist + state markers)\n\nExit code is 0 when every axis / route passes, 1 otherwise.\n`;\n\nconst flagValue = (argv: string[], name: string): string | undefined => {\n const index = argv.indexOf(name);\n if (index < 0) return undefined;\n const value = argv[index + 1];\n // A missing value, or the next token being another flag, means no value was\n // given (e.g. `--url --json`) — reject it so callers fail fast.\n return value === undefined || value.startsWith('-') ? undefined : value;\n};\n\nconst formatText = (verdict: AggregateVerdict): string => {\n const lines = [`validate-ui ${verdict.pass ? 'PASS' : 'FAIL'} — ${verdict.url}`];\n for (const axis of verdict.verdicts) {\n lines.push(` ${axis.pass ? '✓' : '✗'} ${axis.axis}`);\n for (const finding of axis.findings) {\n lines.push(` [${finding.severity}] ${finding.message}`);\n }\n }\n return lines.join('\\n');\n};\n\nconst formatCompareFinding = (finding: CompareRoutesResult['sharedChrome'][number]): string => {\n const tag = finding.suppressed !== undefined ? ' (suppressed)' : '';\n const detail = finding.detail !== undefined ? ` — ${finding.detail}` : '';\n return ` [${finding.severity}] ${finding.message}${detail}${tag}`;\n};\n\nconst formatCompare = (result: CompareRoutesResult): string => {\n const lines = [`validate-ui compare ${result.pass ? 'PASS' : 'FAIL'}`];\n if (result.sharedChrome.length > 0) {\n lines.push(' shared across routes:');\n for (const finding of result.sharedChrome) lines.push(formatCompareFinding(finding));\n }\n for (const route of result.routes) {\n lines.push(` ${route.pass ? '✓' : '✗'} ${route.route}`);\n for (const finding of route.findings) lines.push(formatCompareFinding(finding));\n }\n return lines.join('\\n');\n};\n\nconst loadCompareConfig = async (path: string | undefined): Promise<CompareConfig | undefined> => {\n if (path === undefined) return undefined;\n return CompareConfig.parse(JSON.parse(await readFile(resolve(path), 'utf8')));\n};\n\nconst runCompare = async (argv: string[], asJson: boolean): Promise<void> => {\n const newBaseUrl = flagValue(argv, '--new');\n const oldBaseUrl = flagValue(argv, '--old');\n const oldSnapshotDir = flagValue(argv, '--old-snapshots');\n const routes = flagValue(argv, '--routes')\n ?.split(',')\n .map((route) => route.trim())\n .filter((route) => route !== '');\n\n if (newBaseUrl === undefined || routes === undefined || routes.length === 0) {\n process.stderr.write('validate-ui compare: --new and --routes are required\\n\\n' + HELP);\n process.exitCode = 1;\n return;\n }\n if (oldBaseUrl !== undefined && oldSnapshotDir !== undefined) {\n process.stderr.write(\n 'validate-ui compare: pass only one of --old or --old-snapshots\\n\\n' + HELP,\n );\n process.exitCode = 1;\n return;\n }\n\n const config = await loadCompareConfig(flagValue(argv, '--config'));\n const base = { newBaseUrl, routes, config };\n // Branch so the XOR-typed options narrow without an assertion.\n let result: CompareRoutesResult;\n if (oldBaseUrl !== undefined) {\n result = await compareRoutes({ ...base, oldBaseUrl });\n } else if (oldSnapshotDir !== undefined) {\n result = await compareRoutes({ ...base, oldSnapshotDir });\n } else {\n process.stderr.write('validate-ui compare: provide --old or --old-snapshots\\n\\n' + HELP);\n process.exitCode = 1;\n return;\n }\n process.stdout.write((asJson ? JSON.stringify(result, null, 2) : formatCompare(result)) + '\\n');\n process.exitCode = result.pass ? 0 : 1;\n};\n\nconst main = async (): Promise<void> => {\n const argv = process.argv.slice(2);\n if (argv.includes('--help') || argv.includes('-h') || argv.length === 0) {\n process.stdout.write(HELP);\n return;\n }\n\n const asJson = argv.includes('--json');\n if (argv[0] === 'compare') {\n await runCompare(argv.slice(1), asJson);\n return;\n }\n\n const brief = flagValue(argv, '--brief');\n const serveDir = flagValue(argv, '--serve');\n const viewportSpec = flagValue(argv, '--viewport');\n const viewport = viewportSpec === undefined ? undefined : parseViewport(viewportSpec);\n\n let url = flagValue(argv, '--url');\n let server: Awaited<ReturnType<typeof serveStatic>> | undefined;\n if (serveDir !== undefined) {\n server = await serveStatic(resolve(serveDir));\n const route = flagValue(argv, '--route') ?? '/';\n url = `${server.url}${route.startsWith('/') ? route : `/${route}`}`;\n }\n\n if (url === undefined) {\n process.stderr.write('validate-ui: one of --url or --serve is required\\n\\n' + HELP);\n process.exitCode = 1;\n return;\n }\n\n try {\n const verdict = await validate({ url, brief, viewport });\n process.stdout.write((asJson ? JSON.stringify(verdict, null, 2) : formatText(verdict)) + '\\n');\n process.exitCode = verdict.pass ? 0 : 1;\n } finally {\n await server?.close();\n }\n};\n\nmain().catch((error: unknown) => {\n process.stderr.write(`validate-ui: ${error instanceof Error ? error.message : String(error)}\\n`);\n process.exitCode = 1;\n});\n"],"mappings":"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"}
|
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_runner = require("./runner-
|
|
2
|
+
const require_runner = require("./runner-BwEjSueH.cjs");
|
|
3
3
|
//#region src/types.ts
|
|
4
4
|
const SEVERITIES = [
|
|
5
5
|
"critical",
|
|
@@ -15,9 +15,12 @@ exports.DEFAULT_VIEWPORTS = require_runner.DEFAULT_VIEWPORTS;
|
|
|
15
15
|
exports.DESKTOP_VIEWPORT = require_runner.DESKTOP_VIEWPORT;
|
|
16
16
|
exports.ExpectedAbsentRule = require_runner.ExpectedAbsentRule;
|
|
17
17
|
exports.MOBILE_VIEWPORT = require_runner.MOBILE_VIEWPORT;
|
|
18
|
+
exports.PHONE_MAX_WIDTH = require_runner.PHONE_MAX_WIDTH;
|
|
19
|
+
exports.PHONE_VIEWPORT = require_runner.PHONE_VIEWPORT;
|
|
18
20
|
exports.REGION_STATES = require_runner.REGION_STATES;
|
|
19
21
|
exports.Region = require_runner.Region;
|
|
20
22
|
exports.SEVERITIES = SEVERITIES;
|
|
23
|
+
exports.VIEWPORT_PRESETS = require_runner.VIEWPORT_PRESETS;
|
|
21
24
|
exports.accessibilityAxis = require_runner.accessibilityAxis;
|
|
22
25
|
exports.aggregatePass = require_runner.aggregatePass;
|
|
23
26
|
exports.briefConformanceAxis = require_runner.briefConformanceAxis;
|
|
@@ -30,7 +33,9 @@ exports.compareRoutes = require_runner.compareRoutes;
|
|
|
30
33
|
exports.dedupeSharedChrome = require_runner.dedupeSharedChrome;
|
|
31
34
|
exports.diffSnapshots = require_runner.diffSnapshots;
|
|
32
35
|
exports.extractRegions = require_runner.extractRegions;
|
|
36
|
+
exports.isPhoneViewport = require_runner.isPhoneViewport;
|
|
33
37
|
exports.loadSnapshot = require_runner.loadSnapshot;
|
|
38
|
+
exports.parseViewport = require_runner.parseViewport;
|
|
34
39
|
exports.prepareForScreenshot = require_runner.prepareForScreenshot;
|
|
35
40
|
exports.routeSnapshotFile = require_runner.routeSnapshotFile;
|
|
36
41
|
exports.runAxes = require_runner.runAxes;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../src/types.ts"],"sourcesContent":["import type { Browser, Page } from 'playwright';\n\nimport type { CompareConfig, CompareSnapshot } from './compare/types';\n\
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../src/types.ts"],"sourcesContent":["import type { Browser, Page } from 'playwright';\n\nimport type { CompareConfig, CompareSnapshot } from './compare/types';\n\nexport type Viewport = {\n label: string;\n width: number;\n height: number;\n};\n\n/** A console or 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 every verdict axis reasons over, produced by {@link capture}.\n *\n * - Holds no live browser handles, 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. */\n screenshot: Uint8Array;\n /** Outer HTML after load. */\n dom: string;\n /** Trimmed `body.innerText`; empty is a reliable white-screen signal. Absent without a live page. */\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 /** Extra context — a stack, a WCAG id, a selector, a diff ratio. */\n detail?: string;\n /** Pointer — WCAG ref, DOM selector, request URL. */\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.\n *\n * - Any optional field may be absent; an axis then reports a clear finding instead of throwing.\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 rendering additional pages. */\n browser?: Browser;\n /** Natural-language brief the UI was generated from. */\n brief?: string;\n /** Prior screenshot to diff against. */\n baseline?: Uint8Array;\n /** Re-render and capture the same route under another brand id. */\n captureUnderBrand?: (brand: string) => Promise<CaptureArtifact>;\n /** The \"old\" UI reduced to regions — enables the compare axis when present. */\n comparison?: CompareSnapshot;\n compareConfig?: CompareConfig;\n};\n\n/**\n * A verdict axis.\n *\n * - `run` should not throw on missing input; the aggregator turns a throw into a failing verdict.\n */\nexport type Axis = {\n name: string;\n run: (context: AxisContext) => Promise<Verdict>;\n};\n"],"mappings":";;;AA4CA,MAAa,aAAa;CAAC;CAAY;CAAW;CAAY;AAAO"}
|
package/dist/index.d.cts
CHANGED
|
@@ -79,13 +79,12 @@ declare const CompareConfig: z.ZodObject<{
|
|
|
79
79
|
type CompareConfig = z.infer<typeof CompareConfig>;
|
|
80
80
|
//#endregion
|
|
81
81
|
//#region src/types.d.ts
|
|
82
|
-
/** A named viewport the harness renders and captures at. */
|
|
83
82
|
type Viewport = {
|
|
84
83
|
label: string;
|
|
85
84
|
width: number;
|
|
86
85
|
height: number;
|
|
87
86
|
};
|
|
88
|
-
/** A
|
|
87
|
+
/** A console or page-error message emitted while the page loaded. */
|
|
89
88
|
type ConsoleEntry = {
|
|
90
89
|
type: string;
|
|
91
90
|
text: string;
|
|
@@ -100,22 +99,18 @@ type NetworkEntry = {
|
|
|
100
99
|
failure?: string;
|
|
101
100
|
};
|
|
102
101
|
/**
|
|
103
|
-
* Ground truth
|
|
104
|
-
*
|
|
105
|
-
* so it is safe to serialize and pass to a VLM.
|
|
102
|
+
* Ground truth every verdict axis reasons over, produced by {@link capture}.
|
|
103
|
+
*
|
|
104
|
+
* - Holds no live browser handles, so it is safe to serialize and pass to a VLM.
|
|
106
105
|
*/
|
|
107
106
|
type CaptureArtifact = {
|
|
108
107
|
url: string;
|
|
109
108
|
viewport: Viewport;
|
|
110
|
-
/** Full-page PNG
|
|
109
|
+
/** Full-page PNG. */
|
|
111
110
|
screenshot: Uint8Array;
|
|
112
|
-
/**
|
|
111
|
+
/** Outer HTML after load. */
|
|
113
112
|
dom: string;
|
|
114
|
-
/**
|
|
115
|
-
* Trimmed `body.innerText` read from the live DOM when available. An empty
|
|
116
|
-
* string is a reliable white-screen signal — more robust than parsing
|
|
117
|
-
* {@link dom}. Absent only when an artifact is built without a live page.
|
|
118
|
-
*/
|
|
113
|
+
/** Trimmed `body.innerText`; empty is a reliable white-screen signal. Absent without a live page. */
|
|
119
114
|
renderedText?: string;
|
|
120
115
|
console: ConsoleEntry[];
|
|
121
116
|
network: NetworkEntry[];
|
|
@@ -126,9 +121,9 @@ type Severity = (typeof SEVERITIES)[number];
|
|
|
126
121
|
type Finding = {
|
|
127
122
|
message: string;
|
|
128
123
|
severity: Severity;
|
|
129
|
-
/**
|
|
124
|
+
/** Extra context — a stack, a WCAG id, a selector, a diff ratio. */
|
|
130
125
|
detail?: string;
|
|
131
|
-
/**
|
|
126
|
+
/** Pointer — WCAG ref, DOM selector, request URL. */
|
|
132
127
|
ref?: string;
|
|
133
128
|
};
|
|
134
129
|
/** The result of running one axis against one page. */
|
|
@@ -144,36 +139,30 @@ type AggregateVerdict = {
|
|
|
144
139
|
verdicts: Verdict[];
|
|
145
140
|
};
|
|
146
141
|
/**
|
|
147
|
-
* Everything an axis may need.
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
* {@link AxisContext.captureUnderBrand}. All optional fields may be absent, and
|
|
151
|
-
* an axis must degrade to a clear finding rather than throw when its input is
|
|
152
|
-
* missing.
|
|
142
|
+
* Everything an axis may need.
|
|
143
|
+
*
|
|
144
|
+
* - Any optional field may be absent; an axis then reports a clear finding instead of throwing.
|
|
153
145
|
*/
|
|
154
146
|
type AxisContext = {
|
|
155
147
|
artifact: CaptureArtifact;
|
|
156
148
|
/** Live page for the primary capture, when the run keeps a browser open. */
|
|
157
149
|
page?: Page;
|
|
158
|
-
/** Shared browser
|
|
150
|
+
/** Shared browser for rendering additional pages. */
|
|
159
151
|
browser?: Browser;
|
|
160
|
-
/**
|
|
152
|
+
/** Natural-language brief the UI was generated from. */
|
|
161
153
|
brief?: string;
|
|
162
|
-
/**
|
|
154
|
+
/** Prior screenshot to diff against. */
|
|
163
155
|
baseline?: Uint8Array;
|
|
164
|
-
/** Re-render the same route under
|
|
156
|
+
/** Re-render and capture the same route under another brand id. */
|
|
165
157
|
captureUnderBrand?: (brand: string) => Promise<CaptureArtifact>;
|
|
166
158
|
/** The "old" UI reduced to regions — enables the compare axis when present. */
|
|
167
159
|
comparison?: CompareSnapshot;
|
|
168
|
-
/** Allowlist + state markers for the compare axis. */
|
|
169
160
|
compareConfig?: CompareConfig;
|
|
170
161
|
};
|
|
171
162
|
/**
|
|
172
|
-
* A verdict axis.
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
* should not throw for expected-missing input — the aggregator converts a throw
|
|
176
|
-
* into a failing verdict, but a clear finding is better.
|
|
163
|
+
* A verdict axis.
|
|
164
|
+
*
|
|
165
|
+
* - `run` should not throw on missing input; the aggregator turns a throw into a failing verdict.
|
|
177
166
|
*/
|
|
178
167
|
type Axis = {
|
|
179
168
|
name: string;
|
|
@@ -203,6 +192,43 @@ type CaptureOptions = {
|
|
|
203
192
|
*/
|
|
204
193
|
declare const capture: (url: string, options?: CaptureOptions) => Promise<CaptureArtifact[]>;
|
|
205
194
|
//#endregion
|
|
195
|
+
//#region src/viewport.d.ts
|
|
196
|
+
/**
|
|
197
|
+
* The phone-first baseline every Keboola screen is designed against.
|
|
198
|
+
*
|
|
199
|
+
* - 375×812 is the narrowest width still in meaningful use; a layout that survives it
|
|
200
|
+
* survives every wider phone.
|
|
201
|
+
* - Distinct from {@link MOBILE_VIEWPORT} (390×844), which is one of `capture`'s two
|
|
202
|
+
* default capture sizes rather than a bar anything is held to.
|
|
203
|
+
*/
|
|
204
|
+
declare const PHONE_VIEWPORT: Viewport;
|
|
205
|
+
/** Names `--viewport` accepts in place of a `WIDTHxHEIGHT` pair. */
|
|
206
|
+
declare const VIEWPORT_PRESETS: Readonly<Record<string, Viewport>>;
|
|
207
|
+
/**
|
|
208
|
+
* Widths below this count as a phone.
|
|
209
|
+
*
|
|
210
|
+
* - 768 is the tablet breakpoint: at or above it a page has room for a desktop-shaped
|
|
211
|
+
* layout, below it the content has one column to live in.
|
|
212
|
+
* - Drives severity, not layout — see {@link isPhoneViewport}.
|
|
213
|
+
*/
|
|
214
|
+
declare const PHONE_MAX_WIDTH = 768;
|
|
215
|
+
/**
|
|
216
|
+
* Whether a viewport is phone-class.
|
|
217
|
+
*
|
|
218
|
+
* - Horizontal overflow here is a broken page, not a cosmetic slip: the reader has to
|
|
219
|
+
* scroll sideways to read a sentence. Axes weigh findings accordingly.
|
|
220
|
+
*/
|
|
221
|
+
declare const isPhoneViewport: (viewport: Viewport) => boolean;
|
|
222
|
+
/**
|
|
223
|
+
* Read a `--viewport` value: a preset name, or `WIDTHxHEIGHT` in CSS pixels.
|
|
224
|
+
*
|
|
225
|
+
* - Throws on anything else rather than falling back to a default — a typo'd size would
|
|
226
|
+
* otherwise silently evaluate the viewport the caller was trying to move off.
|
|
227
|
+
* - Zero in either dimension is rejected for the same reason; playwright accepts it and
|
|
228
|
+
* renders nothing.
|
|
229
|
+
*/
|
|
230
|
+
declare const parseViewport: (spec: string) => Viewport;
|
|
231
|
+
//#endregion
|
|
206
232
|
//#region src/prepare-screenshot.d.ts
|
|
207
233
|
/**
|
|
208
234
|
* Settle the page before a screenshot: freeze animations first (so parking the
|
|
@@ -265,14 +291,10 @@ declare const briefConformanceAxis: Axis;
|
|
|
265
291
|
//#endregion
|
|
266
292
|
//#region src/axes/compare.d.ts
|
|
267
293
|
/**
|
|
268
|
-
* A5 · Semantic old-vs-new compare verdict
|
|
294
|
+
* A5 · Semantic old-vs-new compare verdict.
|
|
269
295
|
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
* are unaffected. Extracts the "new" snapshot from the live `context.page`, then
|
|
273
|
-
* runs the pure {@link diffSnapshots}: deltas are localized per region, declared
|
|
274
|
-
* expected-absent deltas are suppressed (not dropped), and a region that degraded
|
|
275
|
-
* to an empty/error state is reported informationally rather than as value loss.
|
|
296
|
+
* - Passing skip unless `context.comparison` (an "old" {@link CompareSnapshot}) is supplied.
|
|
297
|
+
* - Diffs it against a snapshot of the live `context.page` via {@link diffSnapshots}.
|
|
276
298
|
*/
|
|
277
299
|
declare const compareAxis: Axis;
|
|
278
300
|
//#endregion
|
|
@@ -288,14 +310,10 @@ declare const runtimeHealthAxis: Axis;
|
|
|
288
310
|
//#endregion
|
|
289
311
|
//#region src/axes/visual-brand.d.ts
|
|
290
312
|
/**
|
|
291
|
-
* A4 · Visual & brand-correctness verdict
|
|
313
|
+
* A4 · Visual & brand-correctness verdict.
|
|
292
314
|
*
|
|
293
|
-
* Baseline diff
|
|
294
|
-
*
|
|
295
|
-
* `context.captureUnderBrand`: the chrome must visibly re-skin under an
|
|
296
|
-
* alternate brand. Flags layout overflow. Each check degrades to no finding when
|
|
297
|
-
* its input is absent. (Asserting categorical colors — Badge/Alert/ModalIcon —
|
|
298
|
-
* stay fixed pixel-wise is future work; see `altBrandFinding`.)
|
|
315
|
+
* - Baseline pixel diff; alt brand must visibly re-skin the chrome; layout overflow.
|
|
316
|
+
* - Each check yields no finding when its input is absent.
|
|
299
317
|
*/
|
|
300
318
|
declare const visualBrandAxis: Axis;
|
|
301
319
|
//#endregion
|
|
@@ -343,14 +361,10 @@ declare const dedupeSharedChrome: (perRoute: RouteFindings[]) => DedupResult;
|
|
|
343
361
|
//#endregion
|
|
344
362
|
//#region src/compare/regions.d.ts
|
|
345
363
|
/**
|
|
346
|
-
* Reduce the live page to its diffable regions, in the browser.
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
* precedence
|
|
351
|
-
* (`data-region` → `data-testid` → role → `aria-label` → nearest heading), its
|
|
352
|
-
* state from `data-state`/text markers, and its values as normalized numeric
|
|
353
|
-
* tokens. Symmetric across old and new so the two sides diff cleanly.
|
|
364
|
+
* Reduce the live page to its diffable regions, in the browser.
|
|
365
|
+
*
|
|
366
|
+
* - Regions never overlap, so no value is counted twice.
|
|
367
|
+
* - Must stay symmetric across old and new so the two sides diff cleanly.
|
|
354
368
|
*/
|
|
355
369
|
declare const extractRegions: (page: Page, config?: CompareConfig) => Promise<Region[]>;
|
|
356
370
|
//#endregion
|
|
@@ -402,5 +416,5 @@ declare const saveSnapshot: (snapshot: CompareSnapshot, path: string) => Promise
|
|
|
402
416
|
/** Read and validate a recorded snapshot from disk. */
|
|
403
417
|
declare const loadSnapshot: (path: string) => Promise<CompareSnapshot>;
|
|
404
418
|
//#endregion
|
|
405
|
-
export { ALL_AXES, type AggregateVerdict, type Axis, type AxisContext, type CaptureArtifact, type CaptureOptions, CompareConfig, type CompareFinding, type CompareRoutesOptions, type CompareRoutesResult, CompareSnapshot, type ConsoleEntry, DEFAULT_VIEWPORTS, DESKTOP_VIEWPORT, type DedupResult, ExpectedAbsentRule, type Finding, MOBILE_VIEWPORT, type NetworkEntry, REGION_STATES, Region, type RegionState, type RouteCompareResult, type RouteFindings, SEVERITIES, type Severity, type StaticServer, type ValidateOptions, type Verdict, type Viewport, accessibilityAxis, aggregatePass, briefConformanceAxis, capture, capturePage, captureSnapshot, compareAxis, comparePass, compareRoutes, dedupeSharedChrome, diffSnapshots, extractRegions, loadSnapshot, prepareForScreenshot, routeSnapshotFile, runAxes, runtimeHealthAxis, saveSnapshot, serveStatic, validate, visualBrandAxis };
|
|
419
|
+
export { ALL_AXES, type AggregateVerdict, type Axis, type AxisContext, type CaptureArtifact, type CaptureOptions, CompareConfig, type CompareFinding, type CompareRoutesOptions, type CompareRoutesResult, CompareSnapshot, type ConsoleEntry, DEFAULT_VIEWPORTS, DESKTOP_VIEWPORT, type DedupResult, ExpectedAbsentRule, type Finding, MOBILE_VIEWPORT, type NetworkEntry, PHONE_MAX_WIDTH, PHONE_VIEWPORT, REGION_STATES, Region, type RegionState, type RouteCompareResult, type RouteFindings, SEVERITIES, type Severity, type StaticServer, VIEWPORT_PRESETS, type ValidateOptions, type Verdict, type Viewport, accessibilityAxis, aggregatePass, briefConformanceAxis, capture, capturePage, captureSnapshot, compareAxis, comparePass, compareRoutes, dedupeSharedChrome, diffSnapshots, extractRegions, isPhoneViewport, loadSnapshot, parseViewport, prepareForScreenshot, routeSnapshotFile, runAxes, runtimeHealthAxis, saveSnapshot, serveStatic, validate, visualBrandAxis };
|
|
406
420
|
//# sourceMappingURL=index.d.cts.map
|