automatica11y 0.4.1 → 0.6.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 +37 -130
- package/package.json +28 -2
- package/skills/automatica11y-runner/SKILL.md +16 -3
- package/skills/automatica11y-runner/references/fixtures.md +22 -0
- package/src/cli.js +1 -0
- package/src/commands/common.js +14 -11
- package/src/frameworks/angular-errors.js +27 -0
- package/src/frameworks/angular-selectors.js +36 -0
- package/src/frameworks/angular.js +156 -0
- package/src/frameworks/html.js +163 -0
- package/src/frameworks/index.js +12 -7
- package/src/frameworks/react.js +2 -1
- package/src/frameworks/vue.js +2 -1
- package/src/globals.d.ts +123 -6
- package/src/harness/bundle.js +2 -1
- package/src/harness/generate/angular-recipes.js +360 -0
- package/src/harness/generate/probe.js +16 -5
- package/src/harness/npm-install.js +43 -11
- package/src/harness/shadow.js +1 -1
- package/src/harness/storybook.js +18 -5
- package/src/plan/build-plan.js +1 -0
- package/src/plan/classify.js +39 -11
- package/src/plan/mapping.js +5 -5
- package/src/plan/resolve-npm.js +47 -17
- package/src/plan/subpath.js +17 -0
- package/src/report/comparison.js +4 -1
- package/src/report/index.js +1 -1
- package/src/report/parts.js +13 -1
- package/src/report/single.js +1 -1
- package/src/run/audit-npm.js +93 -26
- package/src/run/fail-check.js +10 -2
- package/src/run/generate-fixture.js +5 -4
- package/src/run/run-plan.js +8 -7
- package/src/run/summary.js +1 -1
- package/src/schema.js +9 -2
- package/src/tiers/computed/checks.js +3 -1
- package/src/tiers/conditions/kit.js +3 -3
- package/src/tiers/interactions/archetypes.js +6 -2
- package/src/tiers/interactions/focus-indicator.js +4 -2
- package/src/tiers/interactions/helpers.js +2 -1
- package/src/tiers/rules/ibm.js +2 -2
- package/src/tiers/rules/index.js +1 -1
package/src/plan/classify.js
CHANGED
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import { cleanSubpath } from "./subpath.js";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* @typedef {"npm" | "storybook" | "url" | "html-file" | "static-dir"} TargetKind
|
|
8
|
+
* @typedef {"npm" | "npm-react" | "npm-vue" | "npm-angular" | "npm-html" | "npm-wc" | "npm-unsupported" | "storybook" | "url" | "html-file" | "static-dir"} TargetKind
|
|
9
9
|
* @typedef {{
|
|
10
10
|
* input: string,
|
|
11
11
|
* label: string | null,
|
|
@@ -15,9 +15,10 @@ import { cleanSubpath } from "./subpath.js";
|
|
|
15
15
|
* kind: TargetKind | null,
|
|
16
16
|
* evidenceLevel: "component" | "page" | null,
|
|
17
17
|
* resolved: Record<string, string | null> | null,
|
|
18
|
+
* companions?: Array<{ name: string, requested: string | null, version: string | null, subpath: string | null }>,
|
|
18
19
|
* }} ClassifiedTarget
|
|
19
20
|
* @typedef {(url: string | URL, init?: { signal?: AbortSignal, redirect?: string }) => Promise<{ ok: boolean, status: number, text(): Promise<string> }>} FetchLike
|
|
20
|
-
* @typedef {{ cwd?: string, home?: string, fetch?: FetchLike, timeoutMs?: number, npmView?: (spec: string) => Promise<
|
|
21
|
+
* @typedef {{ cwd?: string, home?: string, fetch?: FetchLike, timeoutMs?: number, npmView?: (spec: string) => Promise<unknown> }} ClassifyContext
|
|
21
22
|
*/
|
|
22
23
|
|
|
23
24
|
const LOCAL_PATH = /^(\.{1,2}(\/|$)|\/|~(\/|$)|file:)/;
|
|
@@ -123,30 +124,57 @@ async function classifyUrl(spec, base, ctx) {
|
|
|
123
124
|
}
|
|
124
125
|
}
|
|
125
126
|
|
|
126
|
-
/**
|
|
127
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Read one npm spec: `name`, `name@version`, or either with a sub-path.
|
|
129
|
+
* @param {string} spec
|
|
130
|
+
* @returns {{ name: string, requested: string | null, subpath: string | null } | { error: string }}
|
|
131
|
+
*/
|
|
132
|
+
function parseNpmSpec(spec) {
|
|
128
133
|
const match = NPM_NAME.exec(spec);
|
|
129
134
|
if (!match) {
|
|
130
|
-
if (/[A-Z]/.test(spec) && NPM_NAME.test(spec.toLowerCase())) return
|
|
131
|
-
return
|
|
135
|
+
if (/[A-Z]/.test(spec) && NPM_NAME.test(spec.toLowerCase())) return { error: `"${spec}" isn't a valid package name. npm package names are lowercase.` };
|
|
136
|
+
return { error: `"${spec}" isn't a valid npm package name. Write npm:name, npm:@scope/name, or npm:name@version.` };
|
|
132
137
|
}
|
|
133
138
|
const [, scope, name, version, rawSubpath] = match;
|
|
134
|
-
if (version === "") return
|
|
139
|
+
if (version === "") return { error: `"${spec}" ends with @ but has no version.` };
|
|
135
140
|
let subpath = null;
|
|
136
141
|
if (rawSubpath !== undefined) {
|
|
137
142
|
const cleaned = cleanSubpath(rawSubpath);
|
|
138
|
-
if ("reason" in cleaned) return
|
|
143
|
+
if ("reason" in cleaned) return { error: `"${spec}" isn't a valid sub-path: ${cleaned.reason}. Write npm:name/sub/path or npm:name@version/sub/path.` };
|
|
139
144
|
subpath = /** @type {{ subpath: string }} */ (cleaned).subpath;
|
|
140
145
|
}
|
|
141
|
-
|
|
146
|
+
return { name: scope ? `@${scope}/${name}` : name, requested: version ?? null, subpath };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* An npm target. A comma-separated list (`npm:a,b@1.2`) is one target made of several packages that install into one folder.
|
|
151
|
+
* The first is the primary: it names the target and decides the framework. The rest are companions, such as a script package
|
|
152
|
+
* that goes with a stylesheet package.
|
|
153
|
+
* @param {string} spec @param {{ input: string, label: string | null, name: string }} base @returns {ClassifiedTarget}
|
|
154
|
+
*/
|
|
155
|
+
function classifyNpm(spec, base) {
|
|
156
|
+
const entries = spec.split(",").map((entry) => entry.trim());
|
|
157
|
+
if (entries.length > 1 && entries.some((entry) => entry === "")) {
|
|
158
|
+
return failed(`"${spec}" has an empty entry in its list. Write the packages with commas and no spaces, for example npm:a,b.`, base);
|
|
159
|
+
}
|
|
160
|
+
const parsed = entries.map(parseNpmSpec);
|
|
161
|
+
for (const entry of parsed) if ("error" in entry) return failed(entry.error, base);
|
|
162
|
+
const [primary, ...companions] = /** @type {Array<{ name: string, requested: string | null, subpath: string | null }>} */ (parsed);
|
|
163
|
+
const seen = new Set();
|
|
164
|
+
for (const entry of [primary, ...companions]) {
|
|
165
|
+
const key = `${entry.name}/${entry.subpath ?? ""}`;
|
|
166
|
+
if (seen.has(key)) return failed(`"${entry.name}${entry.subpath ? `/${entry.subpath}` : ""}" is in the list more than once.`, base);
|
|
167
|
+
seen.add(key);
|
|
168
|
+
}
|
|
142
169
|
return {
|
|
143
170
|
...base,
|
|
144
|
-
name: base.name || (subpath ? `${
|
|
171
|
+
name: base.name || (primary.subpath ? `${primary.name}/${primary.subpath}` : primary.name),
|
|
145
172
|
status: "ok",
|
|
146
173
|
reason: null,
|
|
147
174
|
kind: "npm",
|
|
148
175
|
evidenceLevel: "component",
|
|
149
|
-
resolved: { name:
|
|
176
|
+
resolved: { name: primary.name, requested: primary.requested, version: null, subpath: primary.subpath },
|
|
177
|
+
...(companions.length ? { companions: companions.map((c) => ({ name: c.name, requested: c.requested, version: null, subpath: c.subpath })) } : {}),
|
|
150
178
|
};
|
|
151
179
|
}
|
|
152
180
|
|
package/src/plan/mapping.js
CHANGED
|
@@ -39,14 +39,14 @@ export function score(archetype, name) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* Guess which exports (React or
|
|
42
|
+
* Guess which exports (React, Vue, or Angular) or tags (web components) stand for each archetype.
|
|
43
43
|
* The result is a starting point. A person or the skill checks it before trusting it.
|
|
44
|
-
* @param {{ flavor: "react" | "vue" | "wc", exports?: Array<{ name: string, type: string, parts: string[] }>, tags?: string[] }} input
|
|
45
|
-
* @returns {
|
|
44
|
+
* @param {{ flavor: "react" | "vue" | "angular" | "html" | "wc", exports?: Array<{ name: string, type: string, parts: string[] }>, tags?: string[] }} input
|
|
45
|
+
* @returns {ReturnType<typeof parseMappingFile>[string]}
|
|
46
46
|
*/
|
|
47
47
|
export function candidateMapping({ flavor, exports = [], tags = [] }) {
|
|
48
48
|
const names = flavor === "wc" ? tags : exports.filter((e) => /^[A-Z]/.test(e.name)).map((e) => e.name);
|
|
49
|
-
/** @type {
|
|
49
|
+
/** @type {ReturnType<typeof parseMappingFile>[string]} */
|
|
50
50
|
const mapping = {};
|
|
51
51
|
for (const archetype of ARCHETYPES) {
|
|
52
52
|
const ranked = names
|
|
@@ -97,7 +97,7 @@ export function findAuthoredFixture({ cwd, targetId, archetype, mapped }) {
|
|
|
97
97
|
const file = resolve(cwd, mapped.fixture);
|
|
98
98
|
return existsSync(file) ? file : null;
|
|
99
99
|
}
|
|
100
|
-
for (const ext of ["jsx", "js"]) {
|
|
100
|
+
for (const ext of ["jsx", "js", "ts", "html"]) {
|
|
101
101
|
const file = resolve(cwd, "fixtures", targetId, `${archetype}.${ext}`);
|
|
102
102
|
if (existsSync(file)) return file;
|
|
103
103
|
}
|
package/src/plan/resolve-npm.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
+
import { isAsset } from "../frameworks/html.js";
|
|
2
3
|
import { ADAPTERS } from "../frameworks/index.js";
|
|
3
4
|
import { checkExports, notExportedMessage } from "./subpath.js";
|
|
4
5
|
|
|
5
|
-
const FIELDS = ["name", "version", "peerDependencies", "dependencies", "keywords", "customElements", "deprecated", "exports"];
|
|
6
|
+
const FIELDS = ["name", "version", "peerDependencies", "dependencies", "keywords", "customElements", "deprecated", "exports", "style", "unpkg", "jsdelivr"];
|
|
6
7
|
const OTHER_FRAMEWORKS = {
|
|
7
|
-
"@angular/core": "Angular",
|
|
8
8
|
svelte: "Svelte",
|
|
9
9
|
"solid-js": "Solid",
|
|
10
10
|
preact: "Preact",
|
|
@@ -13,11 +13,16 @@ const OTHER_FRAMEWORKS = {
|
|
|
13
13
|
};
|
|
14
14
|
const WEB_COMPONENT_BASES = ["lit", "lit-element", "@lit/reactive-element", "@stencil/core", "@microsoft/fast-element", "@polymer/polymer"];
|
|
15
15
|
|
|
16
|
+
/** @param {unknown} value @returns {value is Record<string, unknown>} */
|
|
17
|
+
function isRecord(value) {
|
|
18
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
16
21
|
/**
|
|
17
22
|
* Read a package's registry metadata without installing it.
|
|
18
23
|
* @param {string} spec `name`, `name@version`, or `name@range`
|
|
19
24
|
* @param {{ timeoutMs?: number }} [options]
|
|
20
|
-
* @returns {Promise<
|
|
25
|
+
* @returns {Promise<unknown>} The metadata. Throws an Error with a plain reason when the package can't be read.
|
|
21
26
|
*/
|
|
22
27
|
export function npmView(spec, { timeoutMs = 60_000 } = {}) {
|
|
23
28
|
return new Promise((resolve, reject) => {
|
|
@@ -45,48 +50,73 @@ export function npmView(spec, { timeoutMs = 60_000 } = {}) {
|
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
/**
|
|
48
|
-
* Guess how a package renders from its metadata alone. React wins when both signals appear, then a custom elements manifest, then Vue.
|
|
53
|
+
* Guess how a package renders from its metadata alone. React wins when both signals appear, then a custom elements manifest, then Vue, then Angular.
|
|
49
54
|
* `npm` means the metadata can't say, so the run decides after it installs and loads the package.
|
|
50
|
-
* @param {
|
|
51
|
-
* @returns {{ kind: "npm-react" | "npm-vue" | "npm-wc" | "npm-unsupported" | "npm", framework: string | null, reason: string }}
|
|
55
|
+
* @param {unknown} meta
|
|
56
|
+
* @returns {{ kind: "npm-react" | "npm-vue" | "npm-angular" | "npm-html" | "npm-wc" | "npm-unsupported" | "npm", framework: string | null, reason: string }}
|
|
52
57
|
*/
|
|
53
58
|
export function detectFlavor(meta) {
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
const
|
|
59
|
+
const data = isRecord(meta) ? meta : {};
|
|
60
|
+
const peers = isRecord(data.peerDependencies) ? data.peerDependencies : {};
|
|
61
|
+
const deps = isRecord(data.dependencies) ? data.dependencies : {};
|
|
62
|
+
const react = ADAPTERS.react.detect(data);
|
|
57
63
|
if (react) return react;
|
|
58
|
-
if (
|
|
59
|
-
const vue =
|
|
64
|
+
if (data.customElements) return { kind: "npm-wc", framework: "Web components", reason: "The package has a customElements manifest." };
|
|
65
|
+
const vue = ADAPTERS.vue.detect(data);
|
|
60
66
|
if (vue) return vue;
|
|
67
|
+
const angular = ADAPTERS.angular.detect(data);
|
|
68
|
+
if (angular) return angular;
|
|
61
69
|
for (const [name, label] of Object.entries(OTHER_FRAMEWORKS)) {
|
|
62
70
|
if (name in peers) return { kind: "npm-unsupported", framework: label, reason: `The package needs ${label}.` };
|
|
63
71
|
}
|
|
64
72
|
const base = WEB_COMPONENT_BASES.find((name) => name in deps || name in peers);
|
|
65
73
|
if (base) return { kind: "npm-wc", framework: "Web components", reason: `The package builds on ${base}.` };
|
|
74
|
+
const html = ADAPTERS.html.detect(data);
|
|
75
|
+
if (html) return html;
|
|
66
76
|
return { kind: "npm", framework: null, reason: "The metadata doesn't say. The run decides after it loads the package." };
|
|
67
77
|
}
|
|
68
78
|
|
|
69
79
|
/**
|
|
70
80
|
* Fill in a classified npm target: the concrete version and the framework guess.
|
|
71
81
|
* @param {import("./classify.js").ClassifiedTarget} target
|
|
72
|
-
* @param {(spec: string) => Promise<
|
|
82
|
+
* @param {(spec: string) => Promise<unknown>} view
|
|
73
83
|
* @returns {Promise<import("./classify.js").ClassifiedTarget>}
|
|
74
84
|
*/
|
|
75
85
|
export async function resolveNpmTarget(target, view) {
|
|
76
86
|
if (target.status !== "ok" || target.kind !== "npm" || !target.resolved) return target;
|
|
77
87
|
const { name, requested, subpath } = target.resolved;
|
|
78
88
|
try {
|
|
79
|
-
const
|
|
89
|
+
const metadata = await view(`${name}@${requested ?? "latest"}`);
|
|
90
|
+
if (!isRecord(metadata)) throw new Error("npm returned package metadata in an unreadable shape.");
|
|
91
|
+
const meta = metadata;
|
|
80
92
|
// The registry lists a package's exports, so a wrong sub-path is caught here, before anything is installed.
|
|
81
93
|
if (subpath) {
|
|
82
94
|
const result = checkExports(meta.exports, subpath);
|
|
83
|
-
if (result.checked && !result.ok) throw new Error(notExportedMessage({ name, version: meta.version
|
|
95
|
+
if (result.checked && !result.ok) throw new Error(notExportedMessage({ name, version: typeof meta.version === "string" ? meta.version : null, subpath, exact: result.exact, patterns: result.patterns }));
|
|
96
|
+
}
|
|
97
|
+
let flavor = detectFlavor(meta);
|
|
98
|
+
// A list that names a stylesheet or a script (`npm:a/components.css,b/interactions.iife.js`) is plain HTML when the metadata names no framework.
|
|
99
|
+
if (flavor.kind === "npm" && [target.resolved.subpath, ...(target.companions ?? []).map((c) => c.subpath)].some(isAsset)) {
|
|
100
|
+
flavor = { kind: "npm-html", framework: "HTML", reason: "The target names a stylesheet or a script to load." };
|
|
101
|
+
}
|
|
102
|
+
// Each companion is looked up the same way, so a wrong name, version, or sub-path fails here, before anything is installed.
|
|
103
|
+
/** @type {Array<{ name: string, requested: string | null, version: string | null, subpath: string | null }>} */
|
|
104
|
+
const companions = [];
|
|
105
|
+
for (const companion of target.companions ?? []) {
|
|
106
|
+
const found = await view(`${companion.name}@${companion.requested ?? "latest"}`);
|
|
107
|
+
if (!isRecord(found)) throw new Error(`npm returned metadata for ${companion.name} in an unreadable shape.`);
|
|
108
|
+
const version = typeof found.version === "string" ? found.version : null;
|
|
109
|
+
if (companion.subpath) {
|
|
110
|
+
const result = checkExports(found.exports, companion.subpath);
|
|
111
|
+
if (result.checked && !result.ok) throw new Error(notExportedMessage({ name: companion.name, version, subpath: companion.subpath, exact: result.exact, patterns: result.patterns }));
|
|
112
|
+
}
|
|
113
|
+
companions.push({ ...companion, version });
|
|
84
114
|
}
|
|
85
|
-
const flavor = detectFlavor(meta);
|
|
86
115
|
return {
|
|
87
116
|
...target,
|
|
88
|
-
kind:
|
|
89
|
-
resolved: { ...target.resolved, version: meta.version
|
|
117
|
+
kind: flavor.kind,
|
|
118
|
+
resolved: { ...target.resolved, version: typeof meta.version === "string" ? meta.version : null, framework: flavor.framework, detectedBy: flavor.reason },
|
|
119
|
+
...(companions.length ? { companions } : {}),
|
|
90
120
|
};
|
|
91
121
|
} catch (error) {
|
|
92
122
|
return { ...target, status: "failed", reason: error instanceof Error ? error.message : String(error), kind: null, evidenceLevel: null, resolved: null };
|
package/src/plan/subpath.js
CHANGED
|
@@ -77,6 +77,23 @@ export function subpathProblem(workDir, name, subpath, version) {
|
|
|
77
77
|
return fileExists(packageDir, subpath) ? null : `"${name}/${subpath}" isn't a file in ${name}${version ? `@${version}` : ""}, and the package has no exports map that lists what it offers.`;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Sub-paths an installed package offers, as specs a person could pass (`@scope/pkg/button`), for a message that says where
|
|
82
|
+
* to look when a package's main entry has no components. Patterns and `package.json` are left out.
|
|
83
|
+
* @param {string} workDir
|
|
84
|
+
* @param {string} name
|
|
85
|
+
* @param {number} [limit]
|
|
86
|
+
* @returns {string[]}
|
|
87
|
+
*/
|
|
88
|
+
export function offeredSubpaths(workDir, name, limit = 4) {
|
|
89
|
+
try {
|
|
90
|
+
const manifest = JSON.parse(readFileSync(join(workDir, "node_modules", name, "package.json"), "utf8"));
|
|
91
|
+
return exportKeys(manifest.exports).exact.slice(0, limit).map((key) => `${name}${key.slice(1)}`);
|
|
92
|
+
} catch {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
80
97
|
/** The extensions a file can be imported without writing, for a package with no exports map. */
|
|
81
98
|
const EXTENSIONS = ["", ".js", ".mjs", ".cjs", ".json", "/index.js", "/index.mjs", "/index.cjs"];
|
|
82
99
|
|
package/src/report/comparison.js
CHANGED
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
configLabel,
|
|
11
11
|
finish,
|
|
12
12
|
hasGenerated,
|
|
13
|
+
hasNativeTemplates,
|
|
14
|
+
NATIVE_NOTE,
|
|
13
15
|
notTestableLines,
|
|
14
16
|
reportFooter,
|
|
15
17
|
reportHeader,
|
|
@@ -168,7 +170,7 @@ function impactTables(results, engines) {
|
|
|
168
170
|
/**
|
|
169
171
|
* Render report.md for a comparison of two or more targets.
|
|
170
172
|
* Every target ran with the same settings, so the columns line up. A gap or a failure shows in its cell, and never reads as a pass.
|
|
171
|
-
* @param {{ plan:
|
|
173
|
+
* @param {{ plan: ReturnType<typeof import("../schema.js").parsePlan>, results: ReturnType<typeof import("../schema.js").parseResults> }} input
|
|
172
174
|
*/
|
|
173
175
|
export function renderComparison({ plan, results }) {
|
|
174
176
|
const o = plan.options;
|
|
@@ -185,6 +187,7 @@ export function renderComparison({ plan, results }) {
|
|
|
185
187
|
}
|
|
186
188
|
lines.push(...notTestableLines(results));
|
|
187
189
|
if (hasGenerated(results)) lines.push(GENERATED_NOTE, "");
|
|
190
|
+
if (hasNativeTemplates(results)) lines.push(NATIVE_NOTE, "");
|
|
188
191
|
|
|
189
192
|
lines.push("## Findings.", "", FINDINGS_NOTE, "", ...(o.tiers.includes("rules") ? impactTables(results, o.engines) : []));
|
|
190
193
|
for (const key of keys) {
|
package/src/report/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { renderSingleReport } from "./single.js";
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Render report.md. A comparison of two or more targets gets the side-by-side report. Anything else gets the single report.
|
|
6
|
-
* @param {{ plan:
|
|
6
|
+
* @param {{ plan: ReturnType<typeof import("../schema.js").parsePlan>, results: ReturnType<typeof import("../schema.js").parseResults> }} input
|
|
7
7
|
*/
|
|
8
8
|
export function renderReport(input) {
|
|
9
9
|
return input.plan.command === "compare" && input.results.targets.length > 1 ? renderComparison(input) : renderSingleReport(input);
|
package/src/report/parts.js
CHANGED
|
@@ -2,6 +2,8 @@ import { adapterFor } from "../frameworks/index.js";
|
|
|
2
2
|
import { criterion, criterionName, wcagAttribution } from "../wcag/index.js";
|
|
3
3
|
import { cap, num, plural } from "../text.js";
|
|
4
4
|
|
|
5
|
+
/** @typedef {NonNullable<ReturnType<typeof import("../schema.js").parseResults>["targets"][number]["archetypes"][string]["configs"][number]["tiers"][string]["flags"]>[number]} VsrFlag */
|
|
6
|
+
|
|
5
7
|
/** Markdown helpers. */
|
|
6
8
|
export const cell = (text) => String(text ?? "").replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
7
9
|
export const code = (text) => `\`${String(text).replace(/`/g, "'")}\``;
|
|
@@ -162,7 +164,7 @@ export function storybookSection(planTarget, target) {
|
|
|
162
164
|
}
|
|
163
165
|
const walks = Object.entries(target.archetypes).map(([key, a]) => ({ id: key.replace(/^story:/, ""), vsr: a.configs[0]?.tiers.vsr })).filter((w) => w.vsr?.status === "ran");
|
|
164
166
|
if (walks.length) {
|
|
165
|
-
/** @type {Map<string, { flag:
|
|
167
|
+
/** @type {Map<string, { flag: VsrFlag, stories: string[] }>} */
|
|
166
168
|
const byFlag = new Map();
|
|
167
169
|
for (const { id, vsr } of walks) for (const flag of vsr.flags) {
|
|
168
170
|
const key = `${flag.type}|${flag.phrase}`;
|
|
@@ -265,6 +267,14 @@ export function hasGenerated(results) {
|
|
|
265
267
|
return results.targets.some((t) => Object.values(t.archetypes ?? {}).some((a) => a.fixture?.source === "generated"));
|
|
266
268
|
}
|
|
267
269
|
|
|
270
|
+
/** True when a plain HTML target ran from a native-markup template. */
|
|
271
|
+
export function hasNativeTemplates(results) {
|
|
272
|
+
return results.targets.some((t) => t.npm?.flavor === "html" && Object.values(t.archetypes ?? {}).some((a) => a.fixture?.source === "template"));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** What a plain HTML template is, and what it can't say about a package. */
|
|
276
|
+
export const NATIVE_NOTE = "**Native markup templates.** For a plain HTML target the tool used bare native markup (a button, a link, a fieldset, a dialog, details, a popover) with none of the package's classes, so these results show what the package's styles and scripts do to ordinary elements. They don't test a component the package built, and a pass here never means the package's own components pass. A check that needs script behavior the markup doesn't have, such as arrow keys in a menu, measures the bare markup and not the package. Bring a fixture of your own to test the package's components.";
|
|
277
|
+
|
|
268
278
|
/** What "generated" means, for any report that has one. */
|
|
269
279
|
export const GENERATED_NOTE = "**Generated fixtures.** Where no fixture was written, the tool built one from the parts the package exports (or from what a custom element says about itself) and ran it only after it checked that the trigger and root behaved. A generated fixture is a guess about how the library is meant to be assembled, so a failure may come from how it was wired and not from the library. Treat generated results as lower evidence than an authored fixture. The source of each is in the `generated` folder beside this report. Copy one to `fixtures/<target id>/<archetype>.jsx` (`.js` for web components) and edit it to make it an authored fixture.";
|
|
270
280
|
|
|
@@ -279,6 +289,7 @@ export function targetSection(planTarget, target, { generatedNote = true } = {})
|
|
|
279
289
|
if (target.npm) {
|
|
280
290
|
const n = target.npm;
|
|
281
291
|
lines.push(`Installed ${code(`${n.name}@${n.version}`)} on its own${n.subpath ? ` and tested its ${code(`${n.name}/${n.subpath}`)} entry` : ""}, as ${adapterFor(n.flavor).describe(n)}.`, "");
|
|
292
|
+
if (n.companions?.length) lines.push(`Installed beside it, in the same folder: ${n.companions.map((c) => code(`${c.name}@${c.version}${c.subpath ? `/${c.subpath}` : ""}`)).join(", ")}.`, "");
|
|
282
293
|
}
|
|
283
294
|
for (const warning of target.warnings) lines.push(`Warning: ${warning}`, "");
|
|
284
295
|
if (target.status === "failed") {
|
|
@@ -286,6 +297,7 @@ export function targetSection(planTarget, target, { generatedNote = true } = {})
|
|
|
286
297
|
}
|
|
287
298
|
if (target.npm) lines.push("**Archetypes.** A gap means the archetype wasn't tested, so it counts against coverage and never as a pass.", "", ...archetypeTable(target), "");
|
|
288
299
|
if (generatedNote && target.npm && hasGenerated({ targets: [target] })) lines.push(GENERATED_NOTE, "");
|
|
300
|
+
if (generatedNote && target.npm && hasNativeTemplates({ targets: [target] })) lines.push(NATIVE_NOTE, "");
|
|
289
301
|
/** @type {Set<string>} */
|
|
290
302
|
const skipped = new Set();
|
|
291
303
|
for (const [name, archetype] of Object.entries(target.archetypes)) {
|
package/src/report/single.js
CHANGED
|
@@ -2,7 +2,7 @@ import { FINDINGS_NOTE, coverageMatrix, finish, failCheckSection, notTestableLin
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Render report.md for an audit. The text comes from results.json and nothing else.
|
|
5
|
-
* @param {{ plan:
|
|
5
|
+
* @param {{ plan: ReturnType<typeof import("../schema.js").parsePlan>, results: ReturnType<typeof import("../schema.js").parseResults> }} input
|
|
6
6
|
*/
|
|
7
7
|
export function renderSingleReport({ plan, results }) {
|
|
8
8
|
const lines = reportHeader(plan, results, "Accessibility report.");
|