@godxjp/ui 16.10.0 → 17.0.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/README.md +30 -5
- package/dist/components/charts/chart-cartesian.js +1 -6
- package/dist/components/charts/pie-chart.js +1 -8
- package/dist/components/data-display/empty-state.d.ts +1 -1
- package/dist/components/data-display/empty-state.js +25 -7
- package/dist/components/data-entry/form-field.js +13 -4
- package/dist/components/data-entry/search-select.d.ts +1 -1
- package/dist/components/data-entry/search-select.js +97 -19
- package/dist/components/data-entry/select.js +15 -4
- package/dist/components/feedback/alert.d.ts +11 -2
- package/dist/components/feedback/alert.js +51 -9
- package/dist/components/general/button.js +2 -0
- package/dist/components/general/index.d.ts +2 -0
- package/dist/components/general/index.js +2 -0
- package/dist/components/general/reveal.d.ts +14 -0
- package/dist/components/general/reveal.js +21 -0
- package/dist/components/layout/auth-shell.d.ts +11 -0
- package/dist/components/layout/auth-shell.js +15 -0
- package/dist/components/layout/flex.js +1 -1
- package/dist/components/layout/index.d.ts +2 -0
- package/dist/components/layout/index.js +2 -0
- package/dist/components/layout/page-container.js +4 -4
- package/dist/components/navigation/app-setting-picker.js +19 -4
- package/dist/components/navigation/pagination.d.ts +1 -1
- package/dist/components/navigation/pagination.js +2 -1
- package/dist/components/query/data-state.d.ts +1 -1
- package/dist/components/query/data-state.js +27 -4
- package/dist/components/query/index.d.ts +2 -0
- package/dist/components/query/index.js +4 -1
- package/dist/i18n/messages/en.json +30 -2
- package/dist/i18n/messages/ja.json +30 -2
- package/dist/i18n/messages/vi.json +30 -2
- package/dist/lib/query-error.d.ts +17 -0
- package/dist/lib/query-error.js +58 -0
- package/dist/props/components/app.prop.d.ts +9 -1
- package/dist/props/components/data-display.prop.d.ts +15 -0
- package/dist/props/components/data-entry.prop.d.ts +8 -0
- package/dist/props/components/feedback.prop.d.ts +6 -0
- package/dist/props/components/general.prop.d.ts +23 -1
- package/dist/props/components/layout.prop.d.ts +17 -0
- package/dist/props/components/query.prop.d.ts +7 -1
- package/dist/props/registry.d.ts +31 -2
- package/dist/props/registry.js +44 -2
- package/dist/props/vocabulary/index.d.ts +1 -1
- package/dist/props/vocabulary/interaction.prop.d.ts +17 -1
- package/dist/styles/alert-layout.css +1 -2
- package/dist/styles/base.css +1 -1
- package/dist/styles/card-layout.css +6 -6
- package/dist/styles/index.css +1 -0
- package/dist/styles/layout.css +28 -0
- package/dist/styles/motion.css +52 -0
- package/dist/styles/shell-layout.css +42 -0
- package/dist/styles/text-layout.css +1 -1
- package/dist/theme/famgia.service.css +15 -14
- package/dist/tokens/components/card.css +1 -1
- package/dist/tokens/components/feedback.css +4 -0
- package/dist/tokens/components/shell.css +10 -0
- package/dist/tokens/foundation.css +11 -5
- package/package.json +7 -3
- package/scripts/ui-audit.mjs +2 -2
- package/scripts/visual-audit-rules.mjs +2 -2
- package/scripts/visual-audit.mjs +293 -126
|
@@ -29,7 +29,7 @@ export const VISUAL_RULES = [
|
|
|
29
29
|
id: "oversaturated-accent",
|
|
30
30
|
severity: "warn",
|
|
31
31
|
category: "color",
|
|
32
|
-
standard: "@godxjp/ui
|
|
32
|
+
standard: "@godxjp/ui reference-design 渋み (OKLCH chroma ≤ 0.18)",
|
|
33
33
|
fix: "Desaturate brand/primary surfaces — keep OKLCH chroma ≤ 0.18. Read --primary tokens; never paint a full-width bar in raw vivid blue.",
|
|
34
34
|
},
|
|
35
35
|
{
|
|
@@ -75,7 +75,7 @@ export function oklchChroma({ r, g, b }) {
|
|
|
75
75
|
return Math.hypot(oa, ob);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
/**
|
|
78
|
+
/** reference-design restraint bound. A signal/brand surface above this chroma "screams". */
|
|
79
79
|
export const CHROMA_LIMIT = 0.18;
|
|
80
80
|
|
|
81
81
|
/** @returns {boolean} true when an accent surface exceeds the 渋み chroma limit. */
|
package/scripts/visual-audit.mjs
CHANGED
|
@@ -12,12 +12,27 @@
|
|
|
12
12
|
* Playwright + @axe-core/playwright are OPTIONAL peer deps — installed only by apps that
|
|
13
13
|
* run this audit, so the static audit and the library stay browser-free.
|
|
14
14
|
*
|
|
15
|
+
* TESTED peer range (see TESTED_VERSIONS + README "Runtime visual audit"):
|
|
16
|
+
* playwright >=1.55 <2 (tested 1.61.1)
|
|
17
|
+
* @axe-core/playwright >=4.10 <5 (tested 4.12.1)
|
|
18
|
+
* axe-core >=4.10 <5 (tested 4.12.1)
|
|
19
|
+
*
|
|
15
20
|
* Usage (from the consuming app, against its running dev/preview server):
|
|
16
21
|
* node node_modules/@godxjp/ui/scripts/visual-audit.mjs http://localhost:5173 /invoices /settings
|
|
17
|
-
* node node_modules/@godxjp/ui/scripts/visual-audit.mjs
|
|
22
|
+
* node node_modules/@godxjp/ui/scripts/visual-audit.mjs http://localhost:5173 --format json
|
|
18
23
|
* node node_modules/@godxjp/ui/scripts/visual-audit.mjs --strict http://localhost:5173 # CI gate
|
|
19
24
|
* PLAYWRIGHT_CHROMIUM_EXECUTABLE=/usr/bin/chromium node …/visual-audit.mjs http://… # system browser
|
|
25
|
+
*
|
|
26
|
+
* JSON contract (--format json ALWAYS emits valid JSON, even on bootstrap failure):
|
|
27
|
+
* { status: "ok" | "partial" | "error",
|
|
28
|
+
* tool: <TESTED_VERSIONS>,
|
|
29
|
+
* summary: { errors, warnings, pages, infrastructureErrors } | null,
|
|
30
|
+
* findings: [ … product findings … ],
|
|
31
|
+
* errors: [ … infrastructure errors (bootstrap / navigate / analyze) … ] }
|
|
32
|
+
* `status: "ok"` ONLY when every page was audited with zero infrastructure errors —
|
|
33
|
+
* so a tool/bootstrap failure can NEVER be misread as "zero violations".
|
|
20
34
|
*/
|
|
35
|
+
import { pathToFileURL } from "node:url";
|
|
21
36
|
import {
|
|
22
37
|
VISUAL_RULES,
|
|
23
38
|
isOversaturated,
|
|
@@ -27,55 +42,79 @@ import {
|
|
|
27
42
|
alertControlIssues,
|
|
28
43
|
} from "./visual-audit-rules.mjs";
|
|
29
44
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Peer versions this audit is tested against. Surfaced in the JSON `tool` field, the
|
|
47
|
+
* README, and the MCP `list_visual_checks` command so agents pin a compatible range.
|
|
48
|
+
* Playwright 1.55+ is required for the `browser.newContext()` / `context.newPage()`
|
|
49
|
+
* flow that @axe-core/playwright 4.10+ expects (older `browser.newPage()` throws
|
|
50
|
+
* "Please use browser.newContext()").
|
|
51
|
+
*/
|
|
52
|
+
export const TESTED_VERSIONS = {
|
|
53
|
+
playwright: { range: ">=1.55 <2", tested: "1.61.1" },
|
|
54
|
+
"@axe-core/playwright": { range: ">=4.10 <5", tested: "4.12.1" },
|
|
55
|
+
"axe-core": { range: ">=4.10 <5", tested: "4.12.1" },
|
|
56
|
+
};
|
|
34
57
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
58
|
+
/** Shape one product finding from a rule id (pure — unit-tested without a browser). */
|
|
59
|
+
export function buildFinding(ruleId, url, message) {
|
|
60
|
+
const rule = VISUAL_RULES.find((r) => r.id === ruleId);
|
|
61
|
+
return {
|
|
62
|
+
url,
|
|
63
|
+
rule: ruleId,
|
|
64
|
+
severity: rule?.severity ?? "warn",
|
|
65
|
+
standard: rule?.standard,
|
|
66
|
+
message,
|
|
67
|
+
};
|
|
38
68
|
}
|
|
39
69
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
70
|
+
/**
|
|
71
|
+
* Assemble the machine-readable result (pure). `findings` are PRODUCT results; `errors`
|
|
72
|
+
* are INFRASTRUCTURE failures (a page that would not load, axe that would not inject).
|
|
73
|
+
* `status` is "ok" only when no infra error occurred, "error" when nothing could be
|
|
74
|
+
* audited, "partial" when some pages ran and some failed.
|
|
75
|
+
*/
|
|
76
|
+
export function buildResult({ findings = [], errors = [], pages = 0 } = {}) {
|
|
77
|
+
const productErrors = findings.filter((f) => f.severity === "error");
|
|
78
|
+
const warnings = findings.filter((f) => f.severity === "warn");
|
|
79
|
+
const failedToLoad = errors.filter((e) => e.stage === "navigate").length;
|
|
80
|
+
const audited = pages - failedToLoad;
|
|
81
|
+
const status = errors.length === 0 ? "ok" : audited > 0 ? "partial" : "error";
|
|
82
|
+
return {
|
|
83
|
+
status,
|
|
84
|
+
tool: TESTED_VERSIONS,
|
|
85
|
+
summary: {
|
|
86
|
+
errors: productErrors.length,
|
|
87
|
+
warnings: warnings.length,
|
|
88
|
+
pages,
|
|
89
|
+
infrastructureErrors: errors.length,
|
|
90
|
+
},
|
|
91
|
+
findings,
|
|
92
|
+
errors,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
47
95
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Assemble a BOOTSTRAP failure result (pure) — missing peers, no browser, bad args.
|
|
98
|
+
* `summary` is null (never `{ warnings: 0 }`) so a consumer cannot read a bootstrap
|
|
99
|
+
* failure as a clean pass.
|
|
100
|
+
*/
|
|
101
|
+
export function buildBootstrapError(message, hint) {
|
|
102
|
+
return {
|
|
103
|
+
status: "error",
|
|
104
|
+
tool: TESTED_VERSIONS,
|
|
105
|
+
summary: null,
|
|
106
|
+
findings: [],
|
|
107
|
+
errors: [{ stage: "bootstrap", message, ...(hint ? { hint } : {}) }],
|
|
108
|
+
};
|
|
55
109
|
}
|
|
56
|
-
const base = urls[0].replace(/\/$/, "");
|
|
57
|
-
const routes = urls.length > 1 ? urls.slice(1) : ["/"];
|
|
58
|
-
const targets = routes.map((r) =>
|
|
59
|
-
r.startsWith("http") ? r : `${base}${r.startsWith("/") ? r : `/${r}`}`,
|
|
60
|
-
);
|
|
61
110
|
|
|
62
|
-
/**
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return { chromium, AxeBuilder: axe.default ?? axe.AxeBuilder };
|
|
70
|
-
} catch {
|
|
71
|
-
console.error(
|
|
72
|
-
"visual-audit needs the optional peers `playwright` and `@axe-core/playwright`:\n" +
|
|
73
|
-
" pnpm add -D playwright @axe-core/playwright\n" +
|
|
74
|
-
" pnpm exec playwright install chromium\n" +
|
|
75
|
-
"(or set PLAYWRIGHT_CHROMIUM_EXECUTABLE to a system Chromium).",
|
|
76
|
-
);
|
|
77
|
-
process.exit(2);
|
|
78
|
-
}
|
|
111
|
+
/** Exit code for a result: 2 = could not audit, 1 = findings/partial gate, 0 = clean. */
|
|
112
|
+
export function exitCodeFor(result, { strict = false } = {}) {
|
|
113
|
+
if (result.status === "error") return 2;
|
|
114
|
+
if (result.status === "partial") return 1;
|
|
115
|
+
if (strict && result.findings.length > 0) return 1;
|
|
116
|
+
if (result.summary && result.summary.errors > 0) return 1;
|
|
117
|
+
return 0;
|
|
79
118
|
}
|
|
80
119
|
|
|
81
120
|
/** Collect raw measurements from the rendered page; the PURE rules run back in node. */
|
|
@@ -160,102 +199,230 @@ function collectInPage() {
|
|
|
160
199
|
}
|
|
161
200
|
/* c8 ignore stop */
|
|
162
201
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
202
|
+
/** Optional-peer loader — throws a tagged bootstrap error instead of a raw stack. */
|
|
203
|
+
async function loadDeps() {
|
|
204
|
+
try {
|
|
205
|
+
const [{ chromium }, axe] = await Promise.all([
|
|
206
|
+
import("playwright"),
|
|
207
|
+
import("@axe-core/playwright"),
|
|
208
|
+
]);
|
|
209
|
+
return { chromium, AxeBuilder: axe.default ?? axe.AxeBuilder };
|
|
210
|
+
} catch (cause) {
|
|
211
|
+
const err = new Error(
|
|
212
|
+
"visual-audit needs the optional peers `playwright` and `@axe-core/playwright`",
|
|
213
|
+
);
|
|
214
|
+
err.bootstrap = true;
|
|
215
|
+
err.hint =
|
|
216
|
+
"pnpm add -D playwright @axe-core/playwright && pnpm exec playwright install chromium " +
|
|
217
|
+
"(or set PLAYWRIGHT_CHROMIUM_EXECUTABLE to a system Chromium).";
|
|
218
|
+
err.cause = cause;
|
|
219
|
+
throw err;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
180
222
|
|
|
181
|
-
|
|
223
|
+
/**
|
|
224
|
+
* Drive the browser over every target and gather findings + infra errors. Cleanup
|
|
225
|
+
* (page/context/browser) is guaranteed via finally on BOTH success and failure.
|
|
226
|
+
* A launch failure is re-tagged as a bootstrap error so it surfaces as `status:error`.
|
|
227
|
+
*/
|
|
228
|
+
/* c8 ignore start — the browser-driving glue; exercised by scripts/visual-audit-smoke.mjs in CI. */
|
|
229
|
+
async function audit(targets, { chromium, AxeBuilder }) {
|
|
230
|
+
const findings = [];
|
|
231
|
+
const errors = [];
|
|
232
|
+
let browser;
|
|
233
|
+
let context;
|
|
234
|
+
try {
|
|
182
235
|
try {
|
|
183
|
-
await
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
236
|
+
browser = await chromium.launch({
|
|
237
|
+
executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE || undefined,
|
|
238
|
+
});
|
|
239
|
+
} catch (cause) {
|
|
240
|
+
const err = new Error(`could not launch Chromium: ${cause.message}`);
|
|
241
|
+
err.bootstrap = true;
|
|
242
|
+
err.hint =
|
|
243
|
+
"install a browser: pnpm exec playwright install chromium " +
|
|
244
|
+
"(or set PLAYWRIGHT_CHROMIUM_EXECUTABLE to a system Chromium).";
|
|
245
|
+
throw err;
|
|
187
246
|
}
|
|
247
|
+
context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
|
248
|
+
const page = await context.newPage();
|
|
188
249
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
"
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
);
|
|
197
|
-
}
|
|
250
|
+
for (const url of targets) {
|
|
251
|
+
try {
|
|
252
|
+
await page.goto(url, { waitUntil: "networkidle", timeout: 30_000 });
|
|
253
|
+
} catch (e) {
|
|
254
|
+
errors.push({ url, stage: "navigate", message: `could not load page: ${e.message}` });
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
198
257
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
258
|
+
try {
|
|
259
|
+
// 1) axe-core — real WCAG/ARIA engine.
|
|
260
|
+
const { violations } = await new AxeBuilder({ page }).analyze();
|
|
261
|
+
for (const v of violations) {
|
|
262
|
+
findings.push(
|
|
263
|
+
buildFinding(
|
|
264
|
+
"axe-violations",
|
|
265
|
+
url,
|
|
266
|
+
`${v.id} (${v.impact}) — ${v.help} [${v.nodes.length} node(s)] ${v.helpUrl}`,
|
|
267
|
+
),
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// 2) computed-style / layout heuristics via the PURE rules.
|
|
272
|
+
const m = await page.evaluate(collectInPage);
|
|
273
|
+
for (const a of m.accents) {
|
|
274
|
+
if (isOversaturated(a.rgb))
|
|
275
|
+
findings.push(
|
|
276
|
+
buildFinding(
|
|
277
|
+
"oversaturated-accent",
|
|
278
|
+
url,
|
|
279
|
+
`<${a.tag}> background OKLCH chroma ${oklchChroma(a.rgb).toFixed(3)} > 0.18 (rgb ${a.rgb.r},${a.rgb.g},${a.rgb.b})`,
|
|
280
|
+
),
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
for (const t of m.targets) {
|
|
284
|
+
if (isUndersizedTarget(t))
|
|
285
|
+
findings.push(
|
|
286
|
+
buildFinding(
|
|
287
|
+
"target-size-min",
|
|
288
|
+
url,
|
|
289
|
+
`target "${t.name || "(unnamed)"}" is ${t.width}×${t.height}px (< 24×24)`,
|
|
290
|
+
),
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
if (hasEmoji(m.text)) {
|
|
294
|
+
const found = (m.text.match(/\p{Extended_Pictographic}/gu) || []).slice(0, 8).join(" ");
|
|
295
|
+
findings.push(
|
|
296
|
+
buildFinding("emoji-rendered", url, `emoji rendered in product text: ${found}`),
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
m.alerts.forEach((al, i) => {
|
|
300
|
+
for (const issue of alertControlIssues(al))
|
|
301
|
+
findings.push(
|
|
302
|
+
buildFinding("alert-controls-misplaced", url, `banner #${i + 1}: ${issue}`),
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
} catch (e) {
|
|
306
|
+
errors.push({ url, stage: "analyze", message: `audit engine failed: ${e.message}` });
|
|
307
|
+
}
|
|
220
308
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
});
|
|
309
|
+
} finally {
|
|
310
|
+
await context?.close().catch(() => {});
|
|
311
|
+
await browser?.close().catch(() => {});
|
|
225
312
|
}
|
|
226
313
|
|
|
227
|
-
|
|
228
|
-
report(findings);
|
|
314
|
+
return buildResult({ findings, errors, pages: targets.length });
|
|
229
315
|
}
|
|
316
|
+
/* c8 ignore stop */
|
|
230
317
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
318
|
+
const C = {
|
|
319
|
+
red: "\x1b[31m",
|
|
320
|
+
yellow: "\x1b[33m",
|
|
321
|
+
dim: "\x1b[2m",
|
|
322
|
+
bold: "\x1b[1m",
|
|
323
|
+
reset: "\x1b[0m",
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
/** Render a result to stdout in the requested format, then exit with the right code. */
|
|
327
|
+
/* c8 ignore start — thin process-exit glue, driven by the smoke/CLI paths. */
|
|
328
|
+
function finish(result, { asJson, strict, pages }) {
|
|
329
|
+
const code = exitCodeFor(result, { strict });
|
|
234
330
|
if (asJson) {
|
|
235
|
-
process.stdout.write(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
)
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const tag = f.severity === "error" ? `${C.red}error${C.reset}` : `${C.yellow}warn ${C.reset}`;
|
|
245
|
-
console.log(`${tag} ${C.bold}${f.url}${C.reset} ${C.dim}[${f.rule}]${C.reset}`);
|
|
246
|
-
console.log(` ${f.message}`);
|
|
247
|
-
if (f.standard) console.log(` ${C.dim}standard: ${f.standard}${C.reset}`);
|
|
331
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
332
|
+
process.exit(code);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Bootstrap failure — no page was audited.
|
|
336
|
+
if (result.summary === null) {
|
|
337
|
+
for (const e of result.errors) {
|
|
338
|
+
process.stderr.write(`visual-audit failed: ${e.message}\n`);
|
|
339
|
+
if (e.hint) process.stderr.write(` ${e.hint}\n`);
|
|
248
340
|
}
|
|
341
|
+
process.exit(code);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
for (const f of result.findings) {
|
|
345
|
+
const tag = f.severity === "error" ? `${C.red}error${C.reset}` : `${C.yellow}warn ${C.reset}`;
|
|
346
|
+
console.log(`${tag} ${C.bold}${f.url}${C.reset} ${C.dim}[${f.rule}]${C.reset}`);
|
|
347
|
+
console.log(` ${f.message}`);
|
|
348
|
+
if (f.standard) console.log(` ${C.dim}standard: ${f.standard}${C.reset}`);
|
|
349
|
+
}
|
|
350
|
+
for (const e of result.errors) {
|
|
249
351
|
console.log(
|
|
250
|
-
|
|
352
|
+
`${C.red}infra${C.reset} ${C.bold}${e.url ?? "(bootstrap)"}${C.reset} ${C.dim}[${e.stage}]${C.reset}`,
|
|
251
353
|
);
|
|
252
|
-
|
|
354
|
+
console.log(` ${e.message}`);
|
|
253
355
|
}
|
|
254
|
-
|
|
255
|
-
|
|
356
|
+
const { errors, warnings } = result.summary;
|
|
357
|
+
console.log(
|
|
358
|
+
`\ngodxjp-ui visual-audit [${result.status}]: ${C.red}${errors} error(s)${C.reset}, ` +
|
|
359
|
+
`${C.yellow}${warnings} warning(s)${C.reset}` +
|
|
360
|
+
(result.errors.length ? `, ${C.red}${result.errors.length} infra error(s)${C.reset}` : "") +
|
|
361
|
+
` across ${pages} page(s).`,
|
|
362
|
+
);
|
|
363
|
+
if (result.status === "ok" && result.findings.length === 0)
|
|
364
|
+
console.log("✓ No visual/runtime a11y violations found.");
|
|
365
|
+
process.exit(code);
|
|
256
366
|
}
|
|
367
|
+
/* c8 ignore stop */
|
|
368
|
+
|
|
369
|
+
/** CLI entry — parse args, run the audit, print, exit. */
|
|
370
|
+
/* c8 ignore start — CLI driver, exercised via subprocess by the smoke test. */
|
|
371
|
+
async function main() {
|
|
372
|
+
const args = process.argv.slice(2);
|
|
373
|
+
const fmtIdx = args.indexOf("--format");
|
|
374
|
+
const asJson = fmtIdx !== -1 && args[fmtIdx + 1] === "json";
|
|
375
|
+
const strict = args.includes("--strict");
|
|
376
|
+
|
|
377
|
+
if (args.includes("--rules")) {
|
|
378
|
+
process.stdout.write(JSON.stringify(VISUAL_RULES, null, 2) + "\n");
|
|
379
|
+
process.exit(0);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const urls = args.filter((a, i) => !a.startsWith("--") && args[i - 1] !== "--format");
|
|
383
|
+
if (urls.length === 0) {
|
|
384
|
+
const msg =
|
|
385
|
+
"no baseUrl given — usage: visual-audit.mjs [--format json] [--strict] <baseUrl> [route …] " +
|
|
386
|
+
"(baseUrl is the running app; extra args are routes appended to it)";
|
|
387
|
+
finish(buildBootstrapError(msg), { asJson, strict, pages: 0 });
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
257
390
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
})
|
|
391
|
+
const base = urls[0].replace(/\/$/, "");
|
|
392
|
+
const routes = urls.length > 1 ? urls.slice(1) : ["/"];
|
|
393
|
+
const targets = routes.map((r) =>
|
|
394
|
+
r.startsWith("http") ? r : `${base}${r.startsWith("/") ? r : `/${r}`}`,
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
let deps;
|
|
398
|
+
try {
|
|
399
|
+
deps = await loadDeps();
|
|
400
|
+
} catch (e) {
|
|
401
|
+
finish(buildBootstrapError(e.message, e.hint), { asJson, strict, pages: targets.length });
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
const result = await audit(targets, deps);
|
|
407
|
+
finish(result, { asJson, strict, pages: targets.length });
|
|
408
|
+
} catch (e) {
|
|
409
|
+
// A launch/setup failure = infrastructure error (never "zero violations").
|
|
410
|
+
finish(buildBootstrapError(e.message, e.hint), { asJson, strict, pages: targets.length });
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
/* c8 ignore stop */
|
|
414
|
+
|
|
415
|
+
// Run only as a CLI — importing this module (for unit tests) must NOT execute.
|
|
416
|
+
/* c8 ignore start */
|
|
417
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
418
|
+
main().catch((e) => {
|
|
419
|
+
const asJson =
|
|
420
|
+
process.argv.indexOf("--format") !== -1 &&
|
|
421
|
+
process.argv[process.argv.indexOf("--format") + 1] === "json";
|
|
422
|
+
if (asJson) {
|
|
423
|
+
process.stdout.write(JSON.stringify(buildBootstrapError(e.message, e.hint)) + "\n");
|
|
424
|
+
} else console.error(`visual-audit failed: ${e.message}`);
|
|
425
|
+
process.exit(2);
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
/* c8 ignore stop */
|