automatica11y 0.4.1 → 0.7.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.
Files changed (47) hide show
  1. package/README.md +37 -130
  2. package/package.json +30 -2
  3. package/skills/automatica11y-runner/SKILL.md +16 -3
  4. package/skills/automatica11y-runner/references/fixtures.md +24 -0
  5. package/src/cli.js +1 -0
  6. package/src/commands/common.js +14 -11
  7. package/src/frameworks/angular-errors.js +27 -0
  8. package/src/frameworks/angular-selectors.js +36 -0
  9. package/src/frameworks/angular.js +162 -0
  10. package/src/frameworks/errors.js +12 -0
  11. package/src/frameworks/html.js +163 -0
  12. package/src/frameworks/index.js +13 -7
  13. package/src/frameworks/react.js +8 -4
  14. package/src/frameworks/svelte-errors.js +30 -0
  15. package/src/frameworks/svelte.js +162 -0
  16. package/src/frameworks/vue.js +2 -1
  17. package/src/globals.d.ts +123 -6
  18. package/src/harness/bundle.js +2 -1
  19. package/src/harness/generate/angular-recipes.js +360 -0
  20. package/src/harness/generate/dialects.js +35 -1
  21. package/src/harness/generate/jsx-recipes.js +4 -6
  22. package/src/harness/generate/probe.js +16 -5
  23. package/src/harness/npm-install.js +47 -11
  24. package/src/harness/shadow.js +1 -1
  25. package/src/harness/storybook.js +18 -5
  26. package/src/plan/build-plan.js +1 -0
  27. package/src/plan/classify.js +39 -11
  28. package/src/plan/mapping.js +8 -6
  29. package/src/plan/resolve-npm.js +49 -18
  30. package/src/plan/subpath.js +17 -0
  31. package/src/report/comparison.js +4 -1
  32. package/src/report/index.js +1 -1
  33. package/src/report/parts.js +13 -1
  34. package/src/report/single.js +1 -1
  35. package/src/run/audit-npm.js +95 -27
  36. package/src/run/fail-check.js +10 -2
  37. package/src/run/generate-fixture.js +7 -5
  38. package/src/run/run-plan.js +10 -8
  39. package/src/run/summary.js +1 -1
  40. package/src/schema.js +10 -2
  41. package/src/tiers/computed/checks.js +3 -1
  42. package/src/tiers/conditions/kit.js +3 -3
  43. package/src/tiers/interactions/archetypes.js +6 -2
  44. package/src/tiers/interactions/focus-indicator.js +4 -2
  45. package/src/tiers/interactions/helpers.js +2 -1
  46. package/src/tiers/rules/ibm.js +2 -2
  47. package/src/tiers/rules/index.js +1 -1
package/README.md CHANGED
@@ -1,171 +1,78 @@
1
- # automatica11y.
1
+ [![automatica11y Open Graph preview](https://automatica11y.dev/og.png)](https://automatica11y.dev/)
2
+
3
+ # automatica11y
2
4
 
3
5
  Test and compare the accessibility of web pages, Storybook builds, and npm component libraries.
4
6
 
5
- automatica11y answers two questions:
7
+ automatica11y helps answer two questions:
6
8
 
7
9
  - How accessible is this? (`audit`)
8
- - How do these compare? (`compare`, two or more targets)
9
-
10
- It's **not** an attestation or certification tool. Automated checks cover only part of WCAG, so a report says "no automated violations found" only where an engine found none, and never says "accessible." See [Limits](#limits).
10
+ - How do these compare? (`compare`)
11
11
 
12
- ## Quick start.
12
+ ## Quick start
13
13
 
14
- You need Node 20 or newer and Chrome or Chromium.
15
-
16
- Install it once, and `automatica11y` is on your `PATH`, with the short name `a11y` too:
14
+ Run it from your terminal:
17
15
 
18
16
  ```bash
19
- npm i -g automatica11y
17
+ npx -y automatica11y audit npm:@instructure/ui-buttons
20
18
  ```
21
19
 
22
- Then run it without `npx`:
20
+ Install the [skill](skills/automatica11y/SKILL.md) and use it with an agent:
23
21
 
24
- ```bash
25
- automatica11y doctor
26
- automatica11y audit https://example.com
27
- automatica11y compare radix=npm:@radix-ui/react-dialog aria=npm:react-aria-components
22
+ ```text
23
+ /automatica11y compare "example.com/a.html" "example.com/b.html"
28
24
  ```
29
-
30
- You can skip the install and put `npx` in front instead, for example `npx automatica11y doctor`. `npx` fetches the tool the first time and checks the registry after that. A global install doesn't update itself, so run `npm i -g automatica11y@latest` to upgrade, and `automatica11y --version` to see what you have. The rest of this page writes the short form. Add `npx` if you didn't install it.
31
-
32
- `doctor` checks your setup. If it can't find a browser, it prints the command that installs one:
25
+ Add it to your CD/CI pipeline:
33
26
 
34
27
  ```bash
35
- npx playwright-core install --only-shell chromium
28
+ automatica11y audit ./dist --fail-on-axe serious
36
29
  ```
37
30
 
38
- Each run writes a folder (`./a11y-report` by default) with `report.md`, `results.json`, and `plan.json`.
39
-
40
- ## Targets.
41
-
42
- A target is `[label=]<spec>`. The label is optional, and names the target in the report.
43
-
44
- | Target | Spec |
45
- |---|---|
46
- | A live page | `https://example.com/page` |
47
- | A local page or site | `./page.html` or `./dist`. A path with no prefix is relative to the working folder, so `dist` means `./dist`. Prefixes `../`, `/`, `~`, and `file:` work too. |
48
- | A Storybook | Its URL, or a local folder with `index.json` or `stories.json`. |
49
- | An npm package | `npm:name`, `npm:@scope/name`, or `npm:name@version`. Add a sub-path to test one entry of a package: `npm:@scope/pkg/button` or `npm:@scope/pkg@1.2.3/button/v2`. React, Vue 3, and web component libraries work. |
50
-
51
- A bare word such as `button` is a path: the folder or file `./button`. Write `npm:button` to pick the package. The prefix is what chooses a package, so a folder with the same name never gets in the way. A bare word that isn't a path fails with a hint, such as "If you meant the npm package, write npm:react."
52
-
53
- A local `.html` file is served over `http://localhost`, never `file://`. A static site audits its `index.html` only.
54
-
55
- ## What it checks.
56
-
57
- Five tiers run by default. Use `--tiers` to pick fewer.
58
-
59
- - **Rules.** axe-core and IBM Equal Access run side by side. They overlap, and each catches things the other misses. Their findings are reported separately and never added together. axe-core reports an impact (`minor` to `critical`). IBM reports its Toolkit level, a staged adoption scale where level 1 is essential, high-impact requirements. The two scales aren't comparable.
60
- - **Interactions.** Keyboard and focus checks for ten archetypes: button, link, dialog, menu, tabs, combobox, form-field, accordion, tooltip, and live-region (a message that appears or changes without moving focus, such as an alert or status message). Each check runs on a fresh page.
61
- - **Computed checks.** automatica11y's own measurements from resolved styles in the browser, for the trigger of each archetype fixture: text contrast in rest, hover, keyboard focus, and pressed states (1.4.3), the contrast of the control's edge, fill, or icon (1.4.11), and the contrast and thickness of the focus indicator (1.4.11, and 2.4.13 at level AAA). A page that uses a gradient, an image, or transparency behind the control can't be reduced to one color, so that check reports `undetermined`, which is a gap and never a pass. These results are reported on their own and never added to the rule engines' counts.
62
- - **Conditions.** How the page holds up under a user's settings and environment. Each check opens fresh copies of the page and says what happened:
63
- - `prefers-reduced-motion: reduce` (2.3.3, 2.2.2): do animations that move or repeat stop or go away? Motion driven by JavaScript timers isn't visible to this check.
64
- - `prefers-color-scheme: dark` (1.4.3): if the page changes in dark mode, does every piece of text keep its contrast? A page that doesn't adapt isn't failed.
65
- - `prefers-contrast: more` (1.4.3, 1.4.6): if the page responds, does the lowest text contrast stay above the minimum and not drop? The check says whether enhanced contrast (7:1) is reached. `prefers-contrast: less` (1.4.3): if the page softens, does text stay above the minimum? A page that doesn't respond isn't failed.
66
- - `prefers-reduced-transparency: reduce` (1.4.3, 1.4.11): do surfaces that hold text stop being see-through (translucent backgrounds, backdrop blur)? This preference isn't a WCAG requirement. It matters because see-through backgrounds make text contrast unpredictable.
67
- - Forced colors (1.4.11, 2.4.7): is the focus indicator still visible? A ring drawn with `box-shadow` disappears in forced colors, so use an outline.
68
- - Reflow at 320 CSS pixels (1.4.10): does the page scroll sideways, or does anything reach past the right edge? Two-dimensional content such as data tables and maps is exempt, so a person judges those.
69
- - Text spacing (1.4.12): with the spacing the criterion names, does any element cut off its text? Overlapping text isn't checked.
70
-
71
- Conditions run on whole pages and on component fixtures. Storybook stories are reported as not applicable.
72
- - **Virtual screen reader.** The announcements a simulated screen reader makes, recorded as data. The output is simulated. It isn't a real screen reader, and real ones announce things differently.
31
+ **[Read the documentation](https://automatica11y.dev/)**
73
32
 
74
- Every result says what it ran, or why it didn't. A gap, a failure, or a result that can't be tested is a finding. It never counts as a pass.
33
+ ## Why use it
75
34
 
76
- ## Options.
35
+ Most accessibility tools scan one page and hand back one score. automatica11y is built for the questions that come before and after that scan.
77
36
 
78
- | Option | Default | What it does |
79
- |---|---|---|
80
- | `--wcag 2.0\|2.1\|2.2` | `2.2` | The WCAG version. |
81
- | `--level A\|AA\|AAA` | `AA` | The conformance level. IBM Equal Access has no AAA rules, so it runs its AA rules and says so. |
82
- | `--engine axe,ibm` | both | Which rule engines run. |
83
- | `--tiers rules,interactions,computed,conditions,vsr` | all | Which tiers run. |
84
- | `--archetypes a,b` | all | Limit npm and Storybook targets to these archetypes. |
85
- | `--lib-a11y on,off` | both | For libraries with opt-in accessibility features. See [the fixture guide](skills/automatica11y-runner/references/fixtures.md). |
86
- | `--mapping <file>` | none | A mapping file for npm targets. |
87
- | `--no-generate` | off | Don't build fixtures for npm packages. Only authored fixtures and the `button` and `link` templates run. |
88
- | `--max-stories <n>` | `200` | The Storybook story cap. The cap spreads over components. |
89
- | `--out <dir>` | `./a11y-report` | Where results go. |
90
- | `--plan` | off | Classify the targets and write `plan.json`, then stop. Nothing installs and no browser launches. |
91
- | `--fail-on-axe <impact>` | off | Exit 1 if axe-core reports a violation at or above `minor`, `moderate`, `serious`, or `critical`. |
92
- | `--fail-on-ibm <1\|2\|3>` | off | Exit 1 if IBM reports a violation at or below that Toolkit level. |
93
- | `--fail-mode any\|all` | `any` | `any` trips when either engine's check trips. `all` trips only when both do, on the same target. |
94
-
95
- Run `automatica11y run --plan <plan.json>` to repeat a saved plan. It warns if tool versions have changed.
96
-
97
- ## Output.
98
-
99
- - `report.md` is a complete report, written without any model. It opens with a coverage matrix, then lists findings, then ends with a method note.
100
- - `results.json` holds everything, including element selectors and announcement logs.
101
- - `plan.json` records what ran and the resolved tool and package versions.
102
- - `mapping.json` appears for npm targets. See below.
37
+ ### It compares
103
38
 
104
- ## Exit codes.
39
+ Pick two component libraries, two versions of one component, or two sites, and `compare` runs them under the same settings, side by side. "Is this library's dialog better than that one?" gets an answer you can check.
105
40
 
106
- | Code | Meaning |
107
- |---|---|
108
- | 0 | The run completed. Findings don't change this unless a fail flag is set. |
109
- | 1 | The run completed and a fail check tripped. |
110
- | 2 | The command line was wrong. |
111
- | 3 | An environment problem, such as a missing browser or an old Node. The message says how to fix it. |
112
- | 4 | No target produced results. |
41
+ ### It tests components, not just pages
113
42
 
114
- One failing target doesn't stop a comparison. It's recorded with its reason, and the others run.
43
+ Point it at an npm package. It installs the package on its own, finds the components, builds the test fixtures it needs (React, Vue 3, Angular 22 and newer, Svelte 5 and newer, plain HTML, and web components), then opens the dialogs and menus and presses the keys.
115
44
 
116
- ## npm packages and fixtures.
45
+ ### It's' composable
117
46
 
118
- The tool installs each package into its own temporary folder (with install scripts turned off), loads it in the browser, and finds its exports or custom elements. It writes its guesses to `mapping.json`.
47
+ Out of the box it looks at standard accessibility interactions and archetypes (buttons, links, dialogs, contrast, etc.) and you can bring your own simple fixtures for more complex components.
119
48
 
120
- Each archetype's fixture comes from the first of these that applies:
49
+ ### An AI agent can run it
121
50
 
122
- 1. **Authored.** A file you or your agent write at `fixtures/<target id>/<archetype>.jsx` (`.js` for web components). The same `.jsx` works for React and for Vue 3, following [the fixture guide](skills/automatica11y-runner/references/fixtures.md). It always wins.
123
- 2. **Template.** The tool builds `button` and `link` tests from the export name alone.
124
- 3. **Generated.** For dialog, menu, tooltip, tabs, accordion, combobox, form-field, and live-region, the tool builds candidate fixtures from what the package exports. It looks for compound parts by common names (a root, a trigger, a content part, a title, a close part, and so on), either as `Dialog.Root` or as `DialogRoot`, and it tries the usual ways of switching a component on (an `open` prop and a close handler). For web components it reads what each element says about itself: its observed attributes, its members, and its slots. It then bundles each candidate, loads it, and checks that exactly one element is the trigger, that nothing logged an error, that activating the trigger shows a root, and that the root carries a role that fits. The first candidate that passes is used. If none does, the archetype is a gap, and the report lists what was tried and why each attempt failed.
125
- 4. **Gap.** Anything else is a gap in the report.
51
+ The tool prints its own instructions, so you can ask an agent "how accessible is Radix Dialog?" and it knows what to do.
126
52
 
127
- 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. Reports mark generated results and treat them as lower evidence than an authored fixture. The source of each generated fixture is written to `generated/<target id>/<archetype>.jsx` beside the report. Copy one to `fixtures/<target id>/` and edit it to make it an authored fixture. Use `--no-generate` to turn generation off, for example when you want a strict comparison of authored fixtures only.
53
+ ## What a result looks like
128
54
 
129
- Fixtures are code that the tool bundles and runs in a browser on your machine. Write them from the library's documentation, and read ones you didn't write.
55
+ automatica11y generates a machine-readable json findings report, and a human-readable markdown version. Here's an excerpt from a real run on the carousel in shadcn/ui:
130
56
 
131
- ## Use it with an AI agent.
132
-
133
- The tool prints its own guidance, so any agent that can run `npx` can learn to use it. If the tool is installed globally, the agent can run `automatica11y guide skill` instead of `npx automatica11y@latest guide skill`:
134
-
135
- ```bash
136
- npx automatica11y@latest guide # where to start (the AGENTS.md file)
137
- npx automatica11y@latest guide skill # the full steps: build the command, run it, write the report
138
- npx automatica11y@latest guide fixtures # how to write the fixtures an npm package needs
139
- ```
140
-
141
- The guidance ships with the tool, so it always matches the version you run. Tell your agent to run `npx automatica11y@latest guide` and follow it, then ask for things like "How accessible is Radix Dialog?" or "Compare the accessibility of React Aria and Headless UI." The agent needs to run shell commands and read and write files. Nothing here is tied to one agent.
142
-
143
- **Skills.** If your agent loads skills from a folder, copy [`skills/automatica11y`](skills/automatica11y) into it. That's one small file, `SKILL.md`. It advertises the tool to the agent, and sends it to `guide skill`. It names no version, so it doesn't go stale. The full steps are the [`automatica11y-runner`](skills/automatica11y-runner) skill, which ships in the package and is what `guide skill` prints. Copy it too if you want the steps available without the network.
144
-
145
- **AGENTS.md.** [`AGENTS.md`](AGENTS.md) is for agents that read it but don't load skills. It points to the same steps, and tells contributors how to run and change the code.
146
-
147
- ## Limits.
57
+ | Check | Result | Detail |
58
+ |---|---|---|
59
+ | `focus-indicator-contrast` | fail | The border has 2.58:1 against what's next to it (needs 3:1). |
60
+ | `forced-colors-focus-visible` | fail | With forced colors on, only four of 156 changed pixels reach 3:1 against their unfocused color. |
61
+ | `boundary-contrast` | pass | The strongest mark is its icon stroke, at 19.79:1 (needs 3:1). |
148
62
 
149
- - Automated rules find only part of what WCAG covers. They can't judge whether alt text is meaningful, whether link and heading text make sense in context, cognitive load, real focus and reading order in use, or how real screen readers behave. A person has to check those.
150
- - Components are tested in the states a fixture shows. Dialogs, menus, tooltips, and comboboxes run closed and open, and live regions run before and after the message. Other states aren't visited.
151
- - Content on a canvas with no alternative, or inside a closed shadow root, can't be tested, and the report says so. The virtual screen reader can't read inside shadow roots at all.
152
- - Svelte, Angular, Vue 2, and other frameworks report "unsupported framework." Their Storybooks work, because a Storybook renders stories in a page whatever the framework.
153
- - Native screen readers aren't part of this version.
154
- - Results are a snapshot. The tools run at their latest versions, and the report records them.
63
+ > axe-core found no automated violations on that component. IBM Equal Access found one: the carousel's region has no label.
155
64
 
156
- ## Using it in CI.
65
+ ## Limits
157
66
 
158
- ```bash
159
- npx automatica11y audit ./dist --fail-on-axe serious --fail-on-ibm 1 --fail-mode any
160
- ```
67
+ automatica11y is not an attestation or certification tool. Automated checks cover only part of WCAG. They can't judge whether alt text is meaningful, whether the reading order makes sense, or how real screen readers behave, and the screen reader it runs is simulated. A person has to check those.
161
68
 
162
- The run exits 1 when the check trips. Needs-review items never trip it.
69
+ A report says "no automated violations found" where an engine found none, and never says "accessible." A gap, an error, or a result that isn't testable is a finding, not a pass.
163
70
 
164
- ## License.
71
+ ## License
165
72
 
166
73
  MIT. See [LICENSE](LICENSE). The WCAG data below has its own terms.
167
74
 
168
- ## Attribution.
75
+ ## Attribution
169
76
 
170
77
  automatica11y reads WCAG criterion numbers, names, levels, and versions from the W3C's published JSON, [wcag.json](https://www.w3.org/WAI/WCAG22/wcag.json). The package ships that file in `src/data/` without changes.
171
78
 
package/package.json CHANGED
@@ -1,7 +1,15 @@
1
1
  {
2
2
  "name": "automatica11y",
3
- "version": "0.4.1",
3
+ "version": "0.7.0",
4
4
  "description": "Test and compare the accessibility of web pages, Storybook builds, and npm component libraries.",
5
+ "homepage": "https://automatica11y.dev",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/thedannywahl/automatica11y.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/thedannywahl/automatica11y/issues"
12
+ },
5
13
  "license": "MIT",
6
14
  "type": "module",
7
15
  "bin": {
@@ -21,6 +29,8 @@
21
29
  "test": "node --test test/*.test.js",
22
30
  "lint": "tsc -p jsconfig.json",
23
31
  "update-wcag": "node scripts/update-wcag.js",
32
+ "docs:build": "node scripts/build-docs.js",
33
+ "docs:preview": "node scripts/build-docs.js && npx --yes serve site",
24
34
  "prepublishOnly": "npm run lint"
25
35
  },
26
36
  "dependencies": {
@@ -33,11 +43,29 @@
33
43
  "valibot": "^1.5.0"
34
44
  },
35
45
  "devDependencies": {
46
+ "@angular/cdk": "^22.2.2",
47
+ "@angular/common": "^22.2.2",
48
+ "@angular/compiler": "^22.2.2",
49
+ "@angular/core": "^22.2.2",
50
+ "@angular/forms": "^22.2.2",
51
+ "@angular/material": "^22.2.2",
52
+ "@angular/platform-browser": "^22.2.2",
53
+ "@pantoken/components": "^2.0.2",
54
+ "@pantoken/css": "^0.4.3",
55
+ "@pantoken/plugin-custom-theme-colors": "^0.3.3",
56
+ "@shikijs/langs": "^4.5.0",
57
+ "@shikijs/themes": "^4.5.0",
36
58
  "@types/node": "^26.6.4",
59
+ "bits-ui": "^2.19.5",
60
+ "marked": "^18.1.0",
37
61
  "react": "^19.3.0",
38
62
  "react-dom": "^19.3.0",
63
+ "rxjs": "^7.8.2",
64
+ "shiki": "^4.5.0",
65
+ "svelte": "^5.57.2",
39
66
  "typescript": "^7.0.2",
40
- "vue": "^3.5.43"
67
+ "vue": "^3.5.43",
68
+ "zone.js": "^0.16.3"
41
69
  },
42
70
  "publishConfig": {
43
71
  "access": "public"
@@ -16,13 +16,13 @@ The tool is **not** an attestation or certification tool. Automated checks cover
16
16
 
17
17
  Do sections 1 and 2 before you run an audit. A question to the user about a missing target or an unsupported setting (section 3) can come before or after them.
18
18
 
19
- This skill works with automatica11y **0.4.x**. Run:
19
+ This skill works with automatica11y **0.7.x**. Run:
20
20
 
21
21
  ```bash
22
22
  npx --yes automatica11y@latest --version
23
23
  ```
24
24
 
25
- If the command exits with an error or prints nothing, tell the user it failed, include the error text, and stop. If the output doesn't start with `0.4.`, stop. Tell the user the version you got and the series this copy expects (`0.4.x`). If this copy came from a file, offer to read the matching steps with `npx --yes automatica11y@latest guide skill`. Don't run an audit until the user confirms how to proceed.
25
+ If the command exits with an error or prints nothing, tell the user it failed, include the error text, and stop. If the output doesn't start with `0.7.`, stop. Tell the user the version you got and the series this copy expects (`0.7.x`). If this copy came from a file, offer to read the matching steps with `npx --yes automatica11y@latest guide skill`. Don't run an audit until the user confirms how to proceed.
26
26
 
27
27
  If the user installed the tool globally (`npm i -g automatica11y`), `automatica11y` is on the `PATH` (so is the short name `a11y`), and it runs the same commands as `npx --yes automatica11y@latest`. Every command in this skill is written with `npx`, so replace that prefix with `automatica11y` only when `automatica11y --version` passes the check above. If the installed version is the wrong series, or nothing is installed, use `npx`.
28
28
 
@@ -54,6 +54,19 @@ Use `audit` for one target and `compare` for two or more, even when they're diff
54
54
 
55
55
  A bare word such as `button` is a path: the folder or file `./button`. Always write an npm package with the `npm:` prefix. If a word could mean a package or a folder, ask the user which one before you run anything.
56
56
 
57
+ **Plain HTML libraries.** A library with no framework, such as a stylesheet package and a script package, is tested by naming the files the page should load as sub-paths: `npm:pkg/base.css,pkg/components.css,other/behavior.iife.js`. A sub-path ending in `.css` is a stylesheet, and one ending in `.js` or `.mjs` is a script. Read the package's documentation or its `exports` to find the right files, and ask the user which stylesheets and scripts they use when the documentation doesn't say. Nothing is generated for plain HTML. Button, link, form-field, dialog, accordion, live-region, menu, and tooltip have templates of bare native markup, so they run with no fixture, and the report says what that does and doesn't show. Say so plainly: a pass on a native template never means the package's own components pass. Tabs, combobox, and chart need a fixture (`fixtures/<id>/<archetype>.html`) or they're gaps. To test the package's own components, write a fixture from its documentation.
58
+
59
+ **One target can be several packages.** Some libraries ship in pieces, such as a stylesheet package and a script package. Join them with commas and no spaces: `npm:@scope/components,@scope/interactions`. They install into one folder and are tested as one target. The first is the primary: it names the target and decides the framework. Every other package is a companion. Each entry can have its own version and sub-path (`npm:a@1.2,b/button`). A list with a space, an empty entry, or the same package twice is rejected. Targets themselves are separated by spaces, as before.
60
+
61
+ **Reading a comparison in plain language.** People write "compare A, B and C and D." Work out the targets before you build the command:
62
+
63
+ - A comma joins packages into one target. The words "and", "vs", "versus", "against", and "compared with" separate targets. So "compare A, B and C and D" is three targets: `A,B`, `C`, and `D`.
64
+ - Give each target a short label that says what it is, such as `pantoken-html=npm:A,B instui=npm:C pantoken-wc=npm:D`. Use the framework or the library's own name, so the report reads clearly.
65
+ - Packages from the same organization are separate targets when they're alternatives for the same job (a React build and a web component build of one design system). They belong in one target only when the user says they work together, or when one package is the stylesheet or script half of the other.
66
+ - Don't guess when the grouping is unclear. "A, B and C" could be one target of three packages, a target of two beside a third, or three targets. A comma list with no "and", a package that might be a companion or an alternative, and "with" or "plus" between packages are all unclear.
67
+ - Always show the grouping before you run, as a short table of label, packages, and role. If the grouping came from anything but an obvious reading, ask the user to confirm it, with your best reading offered as the default, in the same message as your other questions.
68
+ - Run the same archetypes on every target in a comparison. Say in the report which targets are made of several packages.
69
+
57
70
  Options you can set, and nothing else:
58
71
 
59
72
  | Option | Default | Use it when |
@@ -206,7 +219,7 @@ Use the structure of `report.md`. Quote selectors and rule IDs exactly as `resul
206
219
 
207
220
  Say these things plainly. Don't soften them, and don't fill in a result.
208
221
 
209
- - **Unsupported framework.** The package needs a framework other than React, Vue 3, or web components. Name it, and say which version is unsupported (Vue 2, for example). This version covers React, Vue 3, and web components. A Storybook for the library still works, whatever the framework.
222
+ - **Unsupported framework.** The package needs a framework other than React, Vue 3, Angular 22 and newer, Svelte 5 and newer, plain HTML, or web components. Name it, and say which version is unsupported (Vue 2, Svelte 4, or Angular 21, for example). This version covers React, Vue 3, Angular 22 and newer, Svelte 5 and newer, plain HTML, and web components. A Storybook for the library still works, whatever the framework.
210
223
  - **Not applicable.** The package has no rendering surface, such as a utility library. There's nothing to test.
211
224
  - **Not testable.** The content is a canvas with no alternative, or sits in a closed shadow root. The rule engines can't see it, so the result is untested, not clean. The virtual screen reader also can't read open shadow roots.
212
225
  - **Gap.** The archetype has no usable fixture or no matching export. Say what the archetype needs.
@@ -46,6 +46,30 @@ For React, JSX works without importing React. Import the library from its packag
46
46
 
47
47
  For Vue 3, a fixture is also JSX, and the default export is a component: a function that returns JSX is a functional component, or you can export a component object. The tool turns JSX into `h()` calls and supplies `h` and `Fragment` itself, so don't import them. Importing `h` fails the bundle with "symbol already declared". JSX children become the default slot. Vue's reactivity works, so `import { ref } from "vue"` for state, and wrap the JSX in `defineComponent({ setup() { ... return () => (<jsx/>) } })`. A library that needs a plugin, a theme, or global components can export a function named `setup(app)`, which runs before the app mounts, for example `export function setup(app) { app.use(plugin) }`. Named slots and `v-model` aren't JSX syntax. Pass a slots object as the children, or set `modelValue` and `onUpdate:modelValue` with a spread, for example `{...{ "onUpdate:open": (next) => (open.value = next) }}`.
48
48
 
49
+ For Angular 22 and newer, a fixture is a plain `.js` file with no decorator syntax. Define each component by calling the decorator as a function, and make the component class the default export. The tool loads Angular's runtime compiler, so libraries that ship partly compiled work as they are. Import what you use in the component's `imports` list, and export `providers` (an array) when the library needs application providers. State lives in signals. For example:
50
+
51
+ ```js
52
+ import { Component, signal } from "@angular/core";
53
+ import { MatMenu, MatMenuItem, MatMenuTrigger } from "@angular/material/menu";
54
+
55
+ class Fixture { shown = signal(false); }
56
+ Component({
57
+ selector: "app-fixture",
58
+ imports: [MatMenu, MatMenuItem, MatMenuTrigger],
59
+ template: `<button type="button" data-a11y-trigger [matMenuTriggerFor]="m">Actions</button>
60
+ <mat-menu #m="matMenu"><button mat-menu-item>Copy</button></mat-menu>`,
61
+ })(Fixture);
62
+ export default Fixture;
63
+ ```
64
+
65
+ An Angular fixture can also be TypeScript (`fixtures/<target id>/<archetype>.ts`) with `@Component({ ... })` decorators. The tool strips the types, but it can't emit constructor parameter metadata, so get services with `inject(Service)` and not through constructor parameters. A missing provider or an unknown element is reported in a plain sentence with Angular's error code.
66
+
67
+ Angular libraries split their parts across sub-paths, so audit them with a sub-path target, for example `npm:@angular/material/menu`.
68
+
69
+ For Svelte 5 and newer, a fixture is a `.svelte` file (`fixtures/<target id>/<archetype>.svelte`). Import the library from its package name and write the markup with the library's components, for example `<Dialog.Root>` with `<Dialog.Trigger data-a11y-trigger>`. State uses runes (`let open = $state(false)`), and `lang="ts"` works. A component that takes content takes it as children between its tags. Some Svelte libraries give you builders and not components: you create an object in the script (`const dialog = new Dialog()`) and spread its attributes onto your own elements (`<button {...dialog.trigger} data-a11y-trigger>`). Nothing is generated for those, so write the fixture from the documentation. A fixture can also declare `let { libA11y } = $props()` to receive the library accessibility option. Svelte libraries ship `.svelte` source, and the tool compiles it, so a library that imports SvelteKit-only modules (`$app/...`) can't run.
70
+
71
+ For plain HTML, a fixture is a `.html` file of markup (`fixtures/<target id>/<archetype>.html`). It's a snippet, not a whole page: no `<html>` or `<body>`. Put `data-a11y-trigger` and `data-a11y-root` on the right elements yourself. The target says what loads, as sub-paths in a list: `npm:pkg/base.css,pkg/components.css,other/behavior.iife.js`. Stylesheets load with the page, the markup goes on next, and the scripts load last, so a script that wires up its elements when it starts finds them. A `<script>` inside the snippet runs. Use the classes and attributes the library's documentation names, and don't invent them.
72
+
49
73
  For web components, make `mount(container)` the file's default export. It adds the archetype to the container. The tool imports the package first, so its elements are defined before `mount` runs. The `data-a11y-trigger` and `data-a11y-root` attributes can sit on a host element, a slotted child, or an element inside an **open** shadow root. A **closed** shadow root hides its content from every tool, so the report lists it as not testable.
50
74
 
51
75
  ## Library accessibility options
package/src/cli.js CHANGED
@@ -7,6 +7,7 @@ import { ownVersion } from "./env/versions.js";
7
7
 
8
8
  const COMMANDS = {
9
9
  audit: auditCommand,
10
+ check: auditCommand,
10
11
  compare: compareCommand,
11
12
  run: runSavedPlan,
12
13
  doctor: doctorCommand,
@@ -14,7 +14,7 @@ export const EXIT = { OK: 0, FAIL_THRESHOLD: 1, USAGE: 2, ENVIRONMENT: 3, ALL_TA
14
14
  export class UsageError extends Error {}
15
15
 
16
16
  /**
17
- * @typedef {{ stdout: { write(text: string): unknown }, stderr: { write(text: string): unknown }, cwd: string, env: NodeJS.ProcessEnv, fetch?: import("../plan/classify.js").FetchLike, npmView?: (spec: string) => Promise<any>, installPackage?: Function, platform?: NodeJS.Platform }} Io
17
+ * @typedef {{ stdout: { write(text: string): unknown }, stderr: { write(text: string): unknown }, cwd: string, env: NodeJS.ProcessEnv, fetch?: import("../plan/classify.js").FetchLike, npmView?: (spec: string) => Promise<unknown>, installPackage?: Function, platform?: NodeJS.Platform }} Io
18
18
  */
19
19
 
20
20
  const OPTION_DEFS = /** @type {const} */ ({
@@ -62,7 +62,8 @@ export function parseRunArgs(command, argv) {
62
62
  } catch (error) {
63
63
  throw new UsageError(error instanceof Error ? error.message.split(". ")[0] : String(error));
64
64
  }
65
- const { values, positionals } = parsed;
65
+ const values = /** @type {{ [K in keyof typeof OPTION_DEFS]?: typeof OPTION_DEFS[K]["type"] extends "boolean" ? boolean : string }} */ (parsed.values);
66
+ const { positionals } = parsed;
66
67
  if (values.help) return { help: true, targets: [], planOnly: false, options: null };
67
68
 
68
69
  if (command === "audit" && positionals.length !== 1) {
@@ -99,16 +100,16 @@ export function parseRunArgs(command, argv) {
99
100
  const ibm = ibmFlag === undefined ? null : Number(parseChoice("fail-on-ibm", ibmFlag, TOOLKIT_LEVELS.map(String)));
100
101
  if (axe && !engines.includes("axe")) throw new UsageError("--fail-on-axe needs the axe engine. Add axe to --engine or drop the flag.");
101
102
  if (ibm && !engines.includes("ibm")) throw new UsageError("--fail-on-ibm needs the ibm engine. Add ibm to --engine or drop the flag.");
102
- fail = { mode: /** @type {"any" | "all"} */ (modeFlag === undefined ? "any" : parseChoice("fail-mode", modeFlag, FAIL_MODES)), axe: /** @type {any} */ (axe), ibm: /** @type {any} */ (ibm) };
103
+ fail = { mode: /** @type {"any" | "all"} */ (modeFlag === undefined ? "any" : parseChoice("fail-mode", modeFlag, FAIL_MODES)), axe, ibm };
103
104
  }
104
105
 
105
106
  const options = {
106
- wcag: /** @type {any} */ (values.wcag === undefined ? "2.2" : parseChoice("wcag", values.wcag, WCAG_VERSIONS)),
107
- level: /** @type {any} */ (values.level === undefined ? "AA" : parseChoice("level", values.level, LEVELS)),
108
- tiers: /** @type {any} */ (tiers),
109
- engines: /** @type {any} */ (engines),
110
- libA11y: /** @type {any} */ (libA11y),
111
- archetypes: /** @type {any} */ (archetypes),
107
+ wcag: values.wcag === undefined ? "2.2" : parseChoice("wcag", values.wcag, WCAG_VERSIONS),
108
+ level: values.level === undefined ? "AA" : parseChoice("level", values.level, LEVELS),
109
+ tiers,
110
+ engines,
111
+ libA11y,
112
+ archetypes,
112
113
  mapping: values.mapping ?? null,
113
114
  maxStories,
114
115
  generate: !values["no-generate"],
@@ -122,7 +123,7 @@ export function parseRunArgs(command, argv) {
122
123
  export function describeTarget(target) {
123
124
  if (target.status === "failed") return `failed: ${target.reason}`;
124
125
  const r = target.resolved ?? {};
125
- if (target.kind?.startsWith("npm")) return `${r.name}@${r.version ?? r.requested ?? "latest"}${r.subpath ? `/${r.subpath}` : ""}${r.framework ? ` (${r.framework})` : ""}`;
126
+ if (target.kind?.startsWith("npm")) return `${r.name}@${r.version ?? r.requested ?? "latest"}${r.subpath ? `/${r.subpath}` : ""}${target.companions?.length ? ` with ${target.companions.map((c) => `${c.name}@${c.version ?? c.requested ?? "latest"}${c.subpath ? `/${c.subpath}` : ""}`).join(", ")}` : ""}${r.framework ? ` (${r.framework})` : ""}`;
126
127
  return r.url ?? r.path ?? "";
127
128
  }
128
129
 
@@ -256,7 +257,8 @@ const FLAGS = `Options:
256
257
  /** @param {string} [command] */
257
258
  export function usage(command) {
258
259
  const header = {
259
- audit: "Usage: automatica11y audit <target> [options]\n\nCheck how accessible one target is.\n\n",
260
+ audit: "Usage: automatica11y audit <target> [options]\n automatica11y check <target> [options]\n\nCheck how accessible one target is.\n\n",
261
+ check: "Usage: automatica11y audit <target> [options]\n automatica11y check <target> [options]\n\nCheck how accessible one target is.\n\n",
260
262
  compare: "Usage: automatica11y compare <target> <target> [<target>...] [options]\n\nCompare two or more targets.\n\n",
261
263
  run: "Usage: automatica11y run --plan <plan.json>\n\nRe-run a saved plan.\n",
262
264
  }[command ?? ""];
@@ -266,6 +268,7 @@ export function usage(command) {
266
268
  }
267
269
  return `Usage:
268
270
  automatica11y audit <target> [options]
271
+ automatica11y check <target> [options]
269
272
  automatica11y compare <target> <target> [<target>...] [options]
270
273
  automatica11y run --plan <plan.json>
271
274
  automatica11y doctor
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Angular's own errors are written for someone with a stack trace and the source open. A report is read without either, so the
3
+ * ones a fixture is likely to hit get a plain sentence that keeps Angular's code and the name it reports. Anything else passes through.
4
+ * @param {string} text The first line of an error.
5
+ * @returns {string}
6
+ */
7
+ export function explainAngularError(text) {
8
+ const code = /\bNG0*(\d+)\b/.exec(text)?.[1];
9
+ if (!code) return text;
10
+ const quoted = /`([^`]+)`|'([^']+)'|"([^"]+)"/.exec(text);
11
+ const name = quoted?.[1] ?? quoted?.[2] ?? quoted?.[3];
12
+ switch (code) {
13
+ case "201":
14
+ case "200":
15
+ return `the fixture is missing a provider that the library needs${name ? ` (${name})` : ""}. Add it to the fixture's \`providers\` export, or put the part that provides it around this one (NG0${code})`;
16
+ case "303":
17
+ case "304":
18
+ case "8002":
19
+ return `the template uses an element or input that the library doesn't define${name ? ` (${name})` : ""}, or the component isn't in the template's imports (NG0${code})`;
20
+ case "5105":
21
+ return "the library animates with a legacy animations package that isn't installed in the fixture (NG05105)";
22
+ case "908":
23
+ return "Angular couldn't find the root element to start from (NG0908)";
24
+ default:
25
+ return text;
26
+ }
27
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Angular describes what turns a component or directive on with selectors. At runtime a class carries them in Ivy's own form:
3
+ * each alternative is an array that starts with the element name (empty for an attribute-only selector), followed by
4
+ * attribute names and values: `button[matButton]` is `["button", "matButton", ""]`. Numbers in the array mark class names,
5
+ * `:not()` parts, and other forms that a template can't write from a name alone, so those alternatives are skipped.
6
+ */
7
+
8
+ /** Is this alternative plain: an element name and attributes, nothing else? */
9
+ const isPlain = (alternative) => Array.isArray(alternative) && alternative.length % 2 === 1 && alternative.every((part) => typeof part === "string");
10
+
11
+ /**
12
+ * The simplest element and attributes that match one of the selectors, preferring a given element.
13
+ * An attribute-only selector (`[uiTooltip]`) gets the preferred element, or `fallback` when none is preferred.
14
+ * @param {unknown[][]} selectors
15
+ * @param {{ prefer?: string, fallback?: string }} [options]
16
+ * @returns {{ tag: string, attrs: Array<[string, string]> } | null}
17
+ */
18
+ export function markupFor(selectors, { prefer, fallback } = {}) {
19
+ const plain = (selectors ?? []).filter(isPlain).map((alt) => ({ tag: alt[0], attrs: pairs(alt) }));
20
+ const choose = (list) => list.sort((a, b) => a.attrs.length - b.attrs.length)[0] ?? null;
21
+ const match = prefer ? choose(plain.filter((alt) => alt.tag === prefer)) ?? choose(plain.filter((alt) => alt.tag === "")) : choose(plain.filter((alt) => alt.tag !== "")) ?? choose(plain);
22
+ if (!match) return null;
23
+ const tag = match.tag || prefer || fallback;
24
+ return tag ? { tag, attrs: match.attrs } : null;
25
+ }
26
+
27
+ function pairs(alternative) {
28
+ const out = [];
29
+ for (let i = 1; i < alternative.length; i += 2) out.push([alternative[i], alternative[i + 1]]);
30
+ return out;
31
+ }
32
+
33
+ /** The attributes as they're written in a template: `matButton`, or `kind="primary"`. */
34
+ export function attributeText(attrs) {
35
+ return attrs.map(([name, value]) => (value ? `${name}="${String(value).replace(/"/g, "&quot;")}"` : name)).join(" ");
36
+ }