testaro 71.0.3 → 72.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +11 -0
- package/CLAUDE.md +116 -0
- package/README.md +3 -1
- package/UPGRADES.md +1 -1
- package/memory/MEMORY.md +3 -0
- package/memory/project_validation_pause.md +10 -0
- package/package.json +1 -1
- package/procs/launch.js +3 -3
- package/procs/testaro.js +1 -1
- package/testaro/adbID.js +1 -1
- package/testaro/allCapStyle.js +51 -0
- package/testaro/allCaps.js +166 -33
- package/testaro/allSlanted.js +14 -2
- package/testaro/altScheme.js +1 -1
- package/testaro/attVal.js +1 -1
- package/testaro/autocomplete.js +1 -1
- package/testaro/captionLoc.js +1 -1
- package/testaro/datalistRef.js +1 -1
- package/testaro/distortion.js +1 -1
- package/testaro/focAndOp.js +1 -1
- package/testaro/focInd.js +1 -1
- package/testaro/focVis.js +1 -1
- package/testaro/hovInd.js +1 -1
- package/testaro/hr.js +1 -1
- package/testaro/imageLink.js +1 -1
- package/testaro/labClash.js +1 -1
- package/testaro/legendLoc.js +1 -1
- package/testaro/lineHeight.js +15 -1
- package/testaro/linkExt.js +1 -1
- package/testaro/linkOldAtt.js +1 -1
- package/testaro/linkTo.js +1 -1
- package/testaro/linkUl.js +1 -1
- package/testaro/miniText.js +12 -1
- package/testaro/nonTable.js +1 -1
- package/testaro/optRoleSel.js +1 -1
- package/testaro/phOnly.js +1 -1
- package/testaro/radioSet.js +1 -1
- package/testaro/secHeading.js +1 -1
- package/testaro/textSem.js +1 -1
- package/testaro/titledEl.js +1 -1
- package/testaro/zIndex.js +1 -1
- package/tests/testaro.js +2 -2
- package/validation/tests/targets/allSlanted/index.html +4 -1
- package/pw-aslint-claude.js +0 -88
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Install / update all dependencies and Playwright browsers
|
|
9
|
+
npm run deps
|
|
10
|
+
|
|
11
|
+
# Build the bundled accessible-name computation script
|
|
12
|
+
npm run build
|
|
13
|
+
|
|
14
|
+
# Lint (ESLint)
|
|
15
|
+
npx eslint .
|
|
16
|
+
|
|
17
|
+
# Validate a single Testaro rule (replace <ruleID> with the rule name, e.g. allSlanted)
|
|
18
|
+
npm test <ruleID>
|
|
19
|
+
|
|
20
|
+
# Validate all Testaro rules
|
|
21
|
+
npm run tests
|
|
22
|
+
|
|
23
|
+
# Run a job from JOBDIR/todo (optionally prefix-match by timestamp)
|
|
24
|
+
node call run [jobIDStart]
|
|
25
|
+
|
|
26
|
+
# Watch a directory for jobs
|
|
27
|
+
node call dirWatch <true|false> <intervalSeconds>
|
|
28
|
+
|
|
29
|
+
# Poll a network server for jobs
|
|
30
|
+
node call netWatch <true|false> <intervalSeconds> [<true|false>]
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Architecture
|
|
34
|
+
|
|
35
|
+
Testaro is an ensemble accessibility test runner. It integrates 10 external tools plus ~50 of its own rules (the "testaro" tool) and produces standardized reports from a job document.
|
|
36
|
+
|
|
37
|
+
### Job → Report lifecycle
|
|
38
|
+
|
|
39
|
+
1. A caller provides a **job** — a plain JS/JSON object with a `target` URL, `browserID`, optional `device`, and an `acts` array.
|
|
40
|
+
2. `run.js` exports `doJob(job)`, which validates the job (`procs/job.js`), builds a DOM **catalog** (`procs/catalog.js`), then delegates to `procs/doActs.js`.
|
|
41
|
+
3. `doActs.js` iterates the acts array. For `test` acts it forks a child process running `procs/doTestAct.js` (one per tool invocation) and enforces per-tool time limits. Non-test acts (browser interactions) execute in the parent.
|
|
42
|
+
4. Each tool's test module lives in `tests/<toolID>.js`. It calls the tool's library and converts the native result to the **standard result** shape (`prevented`, `totals[4]`, `instances[]`).
|
|
43
|
+
5. `doJob` returns the job object with `jobData` and `catalog` added at the top level and `result` added inside each `test` act. The job's `standard` property controls what `result` contains: `'no'` → `nativeResult` only; `'only'` → `standardResult` only; `'also'` → both.
|
|
44
|
+
|
|
45
|
+
The 18 act types are defined and documented in `actSpecs.js` (`etc` property). The main interactive types are `button`, `checkbox`, `focus`, `link`, `press`, `presses`, `radio`, `reveal`, `search`, `select`, `text`, `url`, `wait`. The `test` type runs a tool. `launch`, `next`, `page`, `state` control flow.
|
|
46
|
+
|
|
47
|
+
### Testaro rules (`testaro/` directory)
|
|
48
|
+
|
|
49
|
+
Each file exports a `reporter(page, catalog, withItems)` async function. Rules are registered in `tests/testaro.js` in the `allRules` array, which controls default execution order, contamination flags, time limits, and `defaultOn`.
|
|
50
|
+
|
|
51
|
+
The default order in `allRules` reflects a two-phase execution strategy: non-contaminating rules (`contaminates: false`) come first and all share a single page load; contaminating rules (`contaminates: true`) follow, each tested on a freshly loaded copy of the page.
|
|
52
|
+
|
|
53
|
+
Two rules, `shoot0` and `shoot1`, are atypical: they report no violations. Instead they take screenshots at different points during a job so that the `motion` rule can compare them to detect and measure visible page change, avoiding the latency that would be required if `motion` took its own screenshots and waited between them.
|
|
54
|
+
|
|
55
|
+
Two implementation patterns exist:
|
|
56
|
+
|
|
57
|
+
- **`doTest`** (`procs/testaro.js`): for rules whose verdict on each element can be computed in-browser via `page.evaluate`. The caller passes a CSS selector and a serialized `getBadWhat(element)` function string. This is the preferred pattern (~48 of ~50 rules).
|
|
58
|
+
- **`getBasicResult`** (`procs/testaro.js`): for rules that need Playwright APIs or cross-element state (currently only `hover` and `role`).
|
|
59
|
+
|
|
60
|
+
### Validation
|
|
61
|
+
|
|
62
|
+
Validation is currently broken. A reconsideration of its architecture is anticipated, but in the meantime the next paragraph describes its current inoperative design.
|
|
63
|
+
|
|
64
|
+
Validation tests live in `validation/tests/jobs/` (jobs with `expect` arrays) and `validation/tests/targets/` (static HTML pages served locally as test targets). Running `npm test <ruleID>` executes `validation/executors/test.js` → `validation/validateTest.js`, which runs the job and compares `result` fields against `expect` clauses.
|
|
65
|
+
|
|
66
|
+
### Tool XPath strategy
|
|
67
|
+
|
|
68
|
+
The catalog maps XPaths to element metadata. Each tool uses a different approach to report the elements it flags:
|
|
69
|
+
|
|
70
|
+
- `alfa`, `aslint`: report their own XPaths; Testaro normalizes them.
|
|
71
|
+
- `ed11y`, `wave`: Testaro injects `window.getXPath` and calls it per element.
|
|
72
|
+
- `axe`, `htmlcs`, `ibm`, `nuVal`, `nuVnu`, `qualWeb`: Testaro stamps `data-xpath` attributes on all elements before the tool runs.
|
|
73
|
+
- `testaro`: each rule reports XPaths directly.
|
|
74
|
+
|
|
75
|
+
### Environment (`.env`)
|
|
76
|
+
|
|
77
|
+
Key variables:
|
|
78
|
+
|
|
79
|
+
- `HEADED_BROWSER` — show browser windows during tests
|
|
80
|
+
- `DEBUG` — mirror browser console to terminal
|
|
81
|
+
- `WAITS` — ms delay inserted between Playwright operations (useful for debugging)
|
|
82
|
+
- `WAVE_KEY` — API key for the WAVE subscription API
|
|
83
|
+
- `JOBDIR` / `REPORTDIR` — root directories for job files and report output
|
|
84
|
+
- `AGENT` — instance name used in network-watch mode
|
|
85
|
+
- `NETWATCH_URL_<N>_JOB/OBSERVE/REPORT/AUTH`, `NETWATCH_URLS` — server polling configuration
|
|
86
|
+
- `TIMEOUT_MULTIPLIER` — scales all per-tool time limits (default 1)
|
|
87
|
+
|
|
88
|
+
## Code style
|
|
89
|
+
|
|
90
|
+
ESLint (`eslintrc.json`): 2-space indent, single quotes, semicolons, Stroustrup brace style (`else`/`catch` on a new line after `}`), `no-use-before-define`. The `htmlcs/HTMLCS.js` file uses a separate, looser ESLint config and must not be reformatted.
|
|
91
|
+
|
|
92
|
+
Long comments are not broken into multiple lines per paragraph.
|
|
93
|
+
|
|
94
|
+
## Adding a new Testaro rule
|
|
95
|
+
|
|
96
|
+
1. Add an entry to `allRules` in `tests/testaro.js`.
|
|
97
|
+
2. Create `testaro/<ruleID>.js` exporting a `reporter` function using `doTest` (preferred) or `getBasicResult`.
|
|
98
|
+
3. Write a validation job in `validation/tests/jobs/<ruleID>.json` with an `expect` array and corresponding HTML target(s) in `validation/tests/targets/<ruleID>/`.
|
|
99
|
+
4. Run `npm test <ruleID>` until validation passes.
|
|
100
|
+
|
|
101
|
+
## Key files
|
|
102
|
+
|
|
103
|
+
| File | Purpose |
|
|
104
|
+
| ---- | ------- |
|
|
105
|
+
| `run.js` | `doJob()` — the main entry point |
|
|
106
|
+
| `call.js` | CLI wrapper for `run`, `dirWatch`, `netWatch` |
|
|
107
|
+
| `procs/doActs.js` | Iterates acts; forks child for each tool |
|
|
108
|
+
| `procs/doTestAct.js` | Child-process entry point for tool invocations |
|
|
109
|
+
| `procs/testaro.js` | `doTest` / `getBasicResult` helpers for Testaro rules |
|
|
110
|
+
| `procs/launch.js` | Browser lifecycle (launch, goTo, nonce, wait) |
|
|
111
|
+
| `procs/catalog.js` | Builds the element catalog from the DOM |
|
|
112
|
+
| `procs/job.js` | Job validation; `tools` constant |
|
|
113
|
+
| `actSpecs.js` | Canonical specs for all 18 act types |
|
|
114
|
+
| `tests/testaro.js` | `allRules` registry for the testaro tool |
|
|
115
|
+
| `testaro/<ruleID>.js` | One file per Testaro rule |
|
|
116
|
+
| `validation/validateTest.js` | Core validation harness |
|
package/README.md
CHANGED
|
@@ -502,7 +502,9 @@ The `testaro` tool (like the `ibm` tool) has a `withItems` property. If you set
|
|
|
502
502
|
|
|
503
503
|
Unlike any other tool, the `testaro` tool requires a `stopOnFail` property, which specifies whether a failure to conform to any rule (i.e. any value of `totals` other than `[0, 0, 0, 0]`) should terminate the execution of tests for the remaining rules.
|
|
504
504
|
|
|
505
|
-
Tests of the `testaro` tests (i.e. _validation_)
|
|
505
|
+
Tests of the `testaro` tests (i.e. _validation_) could previously be performed as documented in the `VALIDATION.md` file. This functionality has broken and its redesign is planned.
|
|
506
|
+
|
|
507
|
+
One Testaro rule, `allCaps`, is currently being tested for in part with the assistance of the Claude Haiku artificial intelligence (AI) model. To obtain that assistance, you need an Anthropic API key, and its value must be assigned to the `ANTHROPIC_API_KEY` environment variable in the `.env` file. If no valid API key is set there, the rule will be tested for, but without AI assistance.
|
|
506
508
|
|
|
507
509
|
### WAVE
|
|
508
510
|
|
package/UPGRADES.md
CHANGED
|
@@ -809,7 +809,7 @@ You're absolutely right about `nuVal`'s limitations. Since it requires publicly
|
|
|
809
809
|
|
|
810
810
|
1. **`htmlcs`** - Pure JavaScript, no external dependencies, works on any HTML
|
|
811
811
|
2. **`wave`** - Also API-based, so same limitations as `nuVal`
|
|
812
|
-
|
|
812
|
+
3. **`axe`** - Self-contained, works on any page, excellent for establishing patterns
|
|
813
813
|
|
|
814
814
|
**I recommend starting with `axe`** because:
|
|
815
815
|
|
package/memory/MEMORY.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Validation system paused
|
|
3
|
+
description: All new validation test work is on hold pending a redesign of the currently broken validation architecture
|
|
4
|
+
type: project
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
New validation tests and updates to existing validation tests are paused. The validation system is currently broken and a redesign is anticipated. Do not create or modify files in validation/tests/ until the user indicates the redesign is underway.
|
|
8
|
+
|
|
9
|
+
**Why:** The validation architecture is broken and being reconsidered.
|
|
10
|
+
**How to apply:** Skip validation file creation/updates when adding or modifying Testaro rules until further notice.
|
package/package.json
CHANGED
package/procs/launch.js
CHANGED
|
@@ -82,10 +82,10 @@ const browserClose = exports.browserClose = async page => {
|
|
|
82
82
|
};
|
|
83
83
|
// Visits a URL and returns the response of the server.
|
|
84
84
|
const goTo = exports.goTo = async (report, page, url, timeout, waitUntil) => {
|
|
85
|
-
// If the URL is a file path:
|
|
85
|
+
// If the URL is a file path relative to the project root:
|
|
86
86
|
if (url.startsWith('file://')) {
|
|
87
|
-
// Make it absolute.
|
|
88
|
-
url = url.replace('file://', `file://${__dirname}
|
|
87
|
+
// Make it the absolute path to the specified file.
|
|
88
|
+
url = url.replace('file://', `file://${__dirname}/../`);
|
|
89
89
|
}
|
|
90
90
|
// Visit the URL.
|
|
91
91
|
const startTime = Date.now();
|
package/procs/testaro.js
CHANGED
|
@@ -39,7 +39,7 @@ exports.doTest = async (
|
|
|
39
39
|
getBadWhatString
|
|
40
40
|
] = args;
|
|
41
41
|
// Get all violator candidates.
|
|
42
|
-
const candidates = document.
|
|
42
|
+
const candidates = document.querySelectorAll(candidateSelector);
|
|
43
43
|
let violationCount = 0;
|
|
44
44
|
// Initialize proto-instances.
|
|
45
45
|
const protoInstances = [];
|
package/testaro/adbID.js
CHANGED
|
@@ -57,6 +57,6 @@ exports.reporter = async (page, catalog, withItems) => {
|
|
|
57
57
|
};
|
|
58
58
|
const whats = 'Elements have aria-describedby attributes with missing or invalid id values';
|
|
59
59
|
return await doTest(
|
|
60
|
-
page, catalog, withItems, 'adbID', '[aria-describedby]', whats, 3, getBadWhat.toString()
|
|
60
|
+
page, catalog, withItems, 'adbID', 'body [aria-describedby]', whats, 3, getBadWhat.toString()
|
|
61
61
|
);
|
|
62
62
|
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/*
|
|
2
|
+
© 2023 CVS Health and/or one of its affiliates. All rights reserved.
|
|
3
|
+
© 2025–2026 Jonathan Robert Pool.
|
|
4
|
+
|
|
5
|
+
Licensed under the MIT License. See LICENSE file at the project root or https://opensource.org/license/mit/ for details.
|
|
6
|
+
|
|
7
|
+
SPDX-License-Identifier: MIT
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
allCapStyle
|
|
12
|
+
Related to Tenon rule 153.
|
|
13
|
+
This test reports elements with transformed upper-case text. Blocks of upper-case text are difficult to read.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// IMPORTS
|
|
17
|
+
|
|
18
|
+
const {doTest} = require('../procs/testaro');
|
|
19
|
+
|
|
20
|
+
// FUNCTIONS
|
|
21
|
+
|
|
22
|
+
// Runs the test and returns the result.
|
|
23
|
+
exports.reporter = async (page, catalog, withItems) => {
|
|
24
|
+
const getBadWhat = element => {
|
|
25
|
+
// Get the style declaration of the element.
|
|
26
|
+
const styleDec = window.getComputedStyle(element);
|
|
27
|
+
const {textTransform} = styleDec;
|
|
28
|
+
// If the style declaration transforms the element text to upper case:
|
|
29
|
+
if (textTransform === 'uppercase') {
|
|
30
|
+
const parent = element.parentElement;
|
|
31
|
+
// If the element has a parent:
|
|
32
|
+
if (parent) {
|
|
33
|
+
// Get the style declaration of the parent.
|
|
34
|
+
const parentStyleDec = window.getComputedStyle(parent);
|
|
35
|
+
const {textTransform: parentTextTransform} = parentStyleDec;
|
|
36
|
+
// If the style declaration transforms the parent text to upper case:
|
|
37
|
+
if (parentTextTransform === 'uppercase') {
|
|
38
|
+
// Do not report a violation, because the transformation may be inherited.
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// If it has no parent or its transformation is autonomous, return a violation description.
|
|
43
|
+
return 'Element text is transformed into all-capitals';
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const selector = 'body, body *:not(style, script, svg)';
|
|
47
|
+
const whats = 'Elements have an all-capital text transformation style';
|
|
48
|
+
return await doTest(
|
|
49
|
+
page, catalog, withItems, 'allCapStyle', selector, whats, 0, getBadWhat.toString()
|
|
50
|
+
);
|
|
51
|
+
};
|
package/testaro/allCaps.js
CHANGED
|
@@ -10,47 +10,180 @@
|
|
|
10
10
|
/*
|
|
11
11
|
allCaps
|
|
12
12
|
Related to Tenon rule 153.
|
|
13
|
-
This test reports elements
|
|
13
|
+
This test reports elements whose text contains upper-case strings that are not intrinsically upper-case (i.e., not acronyms, abbreviations, or terms whose standard form is all-capitals). Claude Haiku classifies qualifying catalog entries and estimates the probability of a rule violation. If the AI call fails, the test falls back to a rule-based check for 8+ consecutive upper-case letters.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
// IMPORTS
|
|
17
17
|
|
|
18
|
-
const
|
|
18
|
+
const https = require('https');
|
|
19
|
+
|
|
20
|
+
// PARAMETERS
|
|
21
|
+
|
|
22
|
+
const MIN_CONFIDENCE = 0.5;
|
|
23
|
+
const MAX_MARGIN = 100;
|
|
24
|
+
const MAX_TOTAL = 2000;
|
|
25
|
+
const MAX_QUALIFYING = 100;
|
|
26
|
+
|
|
27
|
+
// CONSTANTS
|
|
28
|
+
|
|
29
|
+
const ruleID = 'allCaps';
|
|
30
|
+
const whats = 'Elements have all-capital text';
|
|
19
31
|
|
|
20
32
|
// FUNCTIONS
|
|
21
33
|
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
34
|
+
// Returns text limited to MAX_MARGIN characters before the first and after the last uppercase match, capped at MAX_TOTAL characters.
|
|
35
|
+
const getContext = text => {
|
|
36
|
+
const matches = [...text.matchAll(/\p{Lu}{2,}/gu)];
|
|
37
|
+
const first = matches[0];
|
|
38
|
+
const last = matches[matches.length - 1];
|
|
39
|
+
const start = Math.max(0, first.index - MAX_MARGIN);
|
|
40
|
+
const end = Math.min(text.length, last.index + last[0].length + MAX_MARGIN);
|
|
41
|
+
return text.slice(start, end).slice(0, MAX_TOTAL);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Returns violations using the rule-based fallback (8+ consecutive uppercase letters).
|
|
45
|
+
const getRuleBasedViolations = catalog =>
|
|
46
|
+
Object.entries(catalog)
|
|
47
|
+
.filter(([, entry]) => entry.text && /\p{Lu}{8,}/u.test(entry.text))
|
|
48
|
+
.map(([index]) => ({
|
|
49
|
+
catalogIndex: index,
|
|
50
|
+
what: '[No AI available] Element contains all-capital text'
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
// Sends qualifying entries to Claude Haiku and returns confidence scores.
|
|
54
|
+
const classifyWithAI = entries => new Promise((resolve, reject) => {
|
|
55
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
56
|
+
if (!apiKey) {
|
|
57
|
+
reject(new Error('ANTHROPIC_API_KEY not set'));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const prompt =
|
|
61
|
+
'Classify HTML elements for an accessibility rule. All-capital text violates the rule UNLESS it is an acronym, abbreviation, or term whose standard form is all-capitals (NASA, WHO, etc.).\n\n'
|
|
62
|
+
+ 'For each element, give a confidence score (0.0–1.0, rounded to one decimal place) for the probability that the element VIOLATES the rule (its all-caps text is NOT intrinsically all-caps).\n\n'
|
|
63
|
+
+ 'Respond with ONLY a JSON array. Each element: {"index": <number>, "confidence": <number>}\n\n'
|
|
64
|
+
+ 'Elements:\n'
|
|
65
|
+
+ JSON.stringify(entries);
|
|
66
|
+
const payload = JSON.stringify({
|
|
67
|
+
model: 'claude-haiku-4-5-20251001',
|
|
68
|
+
max_tokens: 2048,
|
|
69
|
+
messages: [{role: 'user', content: prompt}]
|
|
70
|
+
});
|
|
71
|
+
const options = {
|
|
72
|
+
hostname: 'api.anthropic.com',
|
|
73
|
+
path: '/v1/messages',
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: {
|
|
76
|
+
'x-api-key': apiKey,
|
|
77
|
+
'anthropic-version': '2023-06-01',
|
|
78
|
+
'content-type': 'application/json',
|
|
79
|
+
'content-length': Buffer.byteLength(payload)
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const req = https.request(options, res => {
|
|
83
|
+
let body = '';
|
|
84
|
+
res.on('data', chunk => { body += chunk; });
|
|
85
|
+
res.on('end', () => {
|
|
86
|
+
try {
|
|
87
|
+
const parsed = JSON.parse(body);
|
|
88
|
+
if (parsed.error) {
|
|
89
|
+
reject(new Error(parsed.error.message));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const text = parsed.content[0].text;
|
|
93
|
+
resolve(
|
|
94
|
+
[...text.matchAll(/\{"index":\s*\d+,\s*"confidence":\s*[01]\.\d{1,2}\}/g)]
|
|
95
|
+
.map(m => {
|
|
96
|
+
const {index, confidence} = JSON.parse(m[0]);
|
|
97
|
+
return {index, confidence: Math.round(confidence * 10) / 10};
|
|
98
|
+
})
|
|
99
|
+
);
|
|
43
100
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// Return a violation description.
|
|
47
|
-
return 'Element contains all-capital text';
|
|
101
|
+
catch(error) {
|
|
102
|
+
reject(new Error(`Haiku response error: ${error.message}`));
|
|
48
103
|
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
req.on('error', reject);
|
|
107
|
+
req.setTimeout(20000, () => req.destroy(new Error('Haiku API timeout')));
|
|
108
|
+
req.write(payload);
|
|
109
|
+
req.end();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Runs the test and returns the result.
|
|
113
|
+
exports.reporter = async (_, catalog, withItems) => {
|
|
114
|
+
const data = {};
|
|
115
|
+
const totals = [0, 0, 0, 0];
|
|
116
|
+
const standardInstances = [];
|
|
117
|
+
// Get data on the catalog entries whose text values contain 2+ consecutive capital letters.
|
|
118
|
+
const qualifying = Object.entries(catalog)
|
|
119
|
+
.filter(([, entry]) => entry.text && /\p{Lu}{2,}/u.test(entry.text))
|
|
120
|
+
.map(([index, entry]) => ({
|
|
121
|
+
index: Number(index),
|
|
122
|
+
tagName: entry.tagName,
|
|
123
|
+
text: getContext(entry.text)
|
|
124
|
+
}));
|
|
125
|
+
// If there are none:
|
|
126
|
+
if (!qualifying.length) {
|
|
127
|
+
// Report this.
|
|
128
|
+
return {data, totals, standardInstances};
|
|
129
|
+
}
|
|
130
|
+
// Sort them, with unbiased (Fisher–Yates) randomization.
|
|
131
|
+
const sample = qualifying.slice();
|
|
132
|
+
for (let i = sample.length - 1; i > 0; i--) {
|
|
133
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
134
|
+
[sample[i], sample[j]] = [sample[j], sample[i]];
|
|
135
|
+
}
|
|
136
|
+
// If their count exceeds the limit on AI assistance:
|
|
137
|
+
if (sample.length > MAX_QUALIFYING) {
|
|
138
|
+
// Truncate them.
|
|
139
|
+
sample.length = MAX_QUALIFYING;
|
|
140
|
+
}
|
|
141
|
+
let violations;
|
|
142
|
+
try {
|
|
143
|
+
// Get AI estimates of the probabilities of their violating the rule.
|
|
144
|
+
const classifications = await classifyWithAI(sample);
|
|
145
|
+
// Treat the entries with above-minimum violation confidence levels as violations.
|
|
146
|
+
violations = classifications
|
|
147
|
+
.filter(({confidence}) => confidence >= MIN_CONFIDENCE)
|
|
148
|
+
.map(({index, confidence}) => ({
|
|
149
|
+
catalogIndex: String(index),
|
|
150
|
+
what: `Element contains unnecessarily (with confidence ${Math.round(confidence * 100)}%) all-capital text`
|
|
151
|
+
}));
|
|
152
|
+
const evaluated = classifications.length;
|
|
153
|
+
const leftOut = qualifying.length - evaluated;
|
|
154
|
+
// If any entries were truncated out and any AI confidence levels were above-minimum:
|
|
155
|
+
if (leftOut > 0 && evaluated > 0) {
|
|
156
|
+
// Add data about the estimated violation rate among those entries.
|
|
157
|
+
data.leftOut = {
|
|
158
|
+
count: leftOut,
|
|
159
|
+
estimatedViolations: Math.round((violations.length / evaluated) * leftOut)
|
|
160
|
+
};
|
|
49
161
|
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
162
|
+
}
|
|
163
|
+
catch(error) {
|
|
164
|
+
data.aiError = error.message;
|
|
165
|
+
violations = getRuleBasedViolations(catalog);
|
|
166
|
+
}
|
|
167
|
+
const estimatedLeftOut = data.leftOut?.estimatedViolations ?? 0;
|
|
168
|
+
// Add the estimated violation count to the totals.
|
|
169
|
+
totals[0] = violations.length + estimatedLeftOut;
|
|
170
|
+
// If itemization is required:
|
|
171
|
+
if (withItems) {
|
|
172
|
+
// For each entry deemed a violation:
|
|
173
|
+
for (const {catalogIndex, what} of violations) {
|
|
174
|
+
// Add an instance to the standard instances.
|
|
175
|
+
standardInstances.push({ruleID, what, ordinalSeverity: 0, count: 1, catalogIndex});
|
|
176
|
+
}
|
|
177
|
+
// If any entries were truncated:
|
|
178
|
+
if (estimatedLeftOut) {
|
|
179
|
+
// Add a summary instance for them.
|
|
180
|
+
standardInstances.push({ruleID, what: whats, ordinalSeverity: 0, count: estimatedLeftOut});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Otherwise, i.e. if itemization is not required, and if any violations exist:
|
|
184
|
+
else if (totals[0]) {
|
|
185
|
+
// Add a summary instance for them.
|
|
186
|
+
standardInstances.push({ruleID, what: whats, ordinalSeverity: 0, count: totals[0]});
|
|
187
|
+
}
|
|
188
|
+
return {data, totals, standardInstances};
|
|
56
189
|
};
|
package/testaro/allSlanted.js
CHANGED
|
@@ -26,11 +26,23 @@ exports.reporter = async (page, catalog, withItems) => {
|
|
|
26
26
|
const {textContent} = element;
|
|
27
27
|
// If the element contains 40 or more characters of slanted text:
|
|
28
28
|
if (['italic', 'oblique'].includes(styleDec.fontStyle) && textContent.length > 39) {
|
|
29
|
-
|
|
29
|
+
const parent = element.parentElement;
|
|
30
|
+
// If the element has a parent:
|
|
31
|
+
if (parent) {
|
|
32
|
+
// Get the style declaration of the parent.
|
|
33
|
+
const parentStyleDec = window.getComputedStyle(parent);
|
|
34
|
+
const {fontStyle: parentFontStyle} = parentStyleDec;
|
|
35
|
+
// If the parent also has slanted text:
|
|
36
|
+
if (['italic', 'oblique'].includes(parentFontStyle)) {
|
|
37
|
+
// Do not report a violation, because the slant may be inherited.
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// If it has no parent or its slant is autonomous, return a violation description.
|
|
30
42
|
return 'Element contains all-slanted text';
|
|
31
43
|
}
|
|
32
44
|
};
|
|
33
|
-
const selector = 'body *:not(style, script, svg)';
|
|
45
|
+
const selector = 'body, body *:not(style, script, svg)';
|
|
34
46
|
const whats = 'Elements contain all-slanted text';
|
|
35
47
|
return await doTest(
|
|
36
48
|
page, catalog, withItems, 'allSlanted', selector, whats, 0, getBadWhat.toString()
|
package/testaro/altScheme.js
CHANGED
|
@@ -41,6 +41,6 @@ exports.reporter = async (page, catalog, withItems) => {
|
|
|
41
41
|
};
|
|
42
42
|
const whats = 'img elements have alt attributes with URL or filename values';
|
|
43
43
|
return await doTest(
|
|
44
|
-
page, catalog, withItems, 'altScheme', 'img[alt]', whats, 1, getBadWhat.toString()
|
|
44
|
+
page, catalog, withItems, 'altScheme', 'body img[alt]', whats, 1, getBadWhat.toString()
|
|
45
45
|
);
|
|
46
46
|
};
|
package/testaro/attVal.js
CHANGED
|
@@ -31,6 +31,6 @@ exports.reporter = async (page, catalog, withItems, attributeName, areLicit, val
|
|
|
31
31
|
};
|
|
32
32
|
const whats = `Elements have attribute ${attributeName} with illicit values`;
|
|
33
33
|
return await doTest(
|
|
34
|
-
page, catalog, withItems, 'attVal', `[${attributeName}]`, whats, 2, getBadWhat.toString()
|
|
34
|
+
page, catalog, withItems, 'attVal', `body [${attributeName}]`, whats, 2, getBadWhat.toString()
|
|
35
35
|
);
|
|
36
36
|
};
|
package/testaro/autocomplete.js
CHANGED
|
@@ -76,7 +76,7 @@ exports.reporter = async (
|
|
|
76
76
|
return `input has no autocomplete="${requiredAuto}" attribute`;
|
|
77
77
|
}
|
|
78
78
|
};
|
|
79
|
-
const selector = 'input[type=text], input[type=email], input:not([type])';
|
|
79
|
+
const selector = 'body input[type=text], body input[type=email], body input:not([type])';
|
|
80
80
|
const whats = 'Inputs are missing required autocomplete attributes';
|
|
81
81
|
const placeHolders = Object.keys(labels).map(key => `__${key}Labels__`);
|
|
82
82
|
const replacers = Object.values(labels).map(value => JSON.stringify(value));
|
package/testaro/captionLoc.js
CHANGED
|
@@ -31,6 +31,6 @@ exports.reporter = async (page, catalog, withItems) => {
|
|
|
31
31
|
};
|
|
32
32
|
const whats = 'caption elements are not the first children of table elements';
|
|
33
33
|
return await doTest(
|
|
34
|
-
page, catalog, withItems, 'captionLoc', 'caption', whats, 3, getBadWhat.toString()
|
|
34
|
+
page, catalog, withItems, 'captionLoc', 'body caption', whats, 3, getBadWhat.toString()
|
|
35
35
|
);
|
|
36
36
|
};
|
package/testaro/datalistRef.js
CHANGED
|
@@ -46,6 +46,6 @@ exports.reporter = async (page, catalog, withItems) => {
|
|
|
46
46
|
};
|
|
47
47
|
const whats = 'list attributes of input elements are empty or IDs of no or non-datalist elements';
|
|
48
48
|
return await doTest(
|
|
49
|
-
page, catalog, withItems, 'datalistRef', 'input[list]', whats, 3, getBadWhat.toString()
|
|
49
|
+
page, catalog, withItems, 'datalistRef', 'body input[list]', whats, 3, getBadWhat.toString()
|
|
50
50
|
);
|
|
51
51
|
};
|
package/testaro/distortion.js
CHANGED
|
@@ -38,6 +38,6 @@ exports.reporter = async (page, catalog, withItems) => {
|
|
|
38
38
|
};
|
|
39
39
|
const whats = 'Elements distort their texts';
|
|
40
40
|
return await doTest(
|
|
41
|
-
page, catalog, withItems, 'distortion', 'body *', whats, 0, getBadWhat.toString()
|
|
41
|
+
page, catalog, withItems, 'distortion', 'body, body *', whats, 0, getBadWhat.toString()
|
|
42
42
|
);
|
|
43
43
|
};
|
package/testaro/focAndOp.js
CHANGED
|
@@ -116,6 +116,6 @@ exports.reporter = async (page, catalog, withItems) => {
|
|
|
116
116
|
};
|
|
117
117
|
const whats = 'Elements are Tab-focusable but not operable or vice versa';
|
|
118
118
|
return await doTest(
|
|
119
|
-
page, catalog, withItems, 'focAndOp', 'body *', whats, 2, getBadWhat.toString()
|
|
119
|
+
page, catalog, withItems, 'focAndOp', 'body, body *', whats, 2, getBadWhat.toString()
|
|
120
120
|
);
|
|
121
121
|
};
|
package/testaro/focInd.js
CHANGED
|
@@ -86,6 +86,6 @@ exports.reporter = async (page, catalog, withItems) => {
|
|
|
86
86
|
};
|
|
87
87
|
const whats = 'Elements fail to have standard focus indicators';
|
|
88
88
|
return await doTest(
|
|
89
|
-
page, catalog, withItems, 'focInd', 'body *', whats, 1, getBadWhat.toString()
|
|
89
|
+
page, catalog, withItems, 'focInd', 'body, body *', whats, 1, getBadWhat.toString()
|
|
90
90
|
);
|
|
91
91
|
};
|
package/testaro/focVis.js
CHANGED
|
@@ -42,6 +42,6 @@ exports.reporter = async (page, catalog, withItems) => {
|
|
|
42
42
|
};
|
|
43
43
|
const whats = 'Visible links are above or to the left of the display';
|
|
44
44
|
return await doTest(
|
|
45
|
-
page, catalog, withItems, 'focVis', 'a', whats, 2, getBadWhat.toString()
|
|
45
|
+
page, catalog, withItems, 'focVis', 'body a', whats, 2, getBadWhat.toString()
|
|
46
46
|
);
|
|
47
47
|
};
|
package/testaro/hovInd.js
CHANGED
|
@@ -138,7 +138,7 @@ exports.reporter = async (page, catalog, withItems) => {
|
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
};
|
|
141
|
-
const selector = 'a, button, input, [onmouseenter], [onmouseover]';
|
|
141
|
+
const selector = 'body a, body button, body input, body [onmouseenter], body [onmouseover]';
|
|
142
142
|
const whats = 'elements have confusing hover indicators';
|
|
143
143
|
return await doTest(page, catalog, withItems, 'hovInd', selector, whats, 1, getBadWhat.toString());
|
|
144
144
|
};
|
package/testaro/hr.js
CHANGED
|
@@ -27,6 +27,6 @@ exports.reporter = async (page, catalog, withItems) => {
|
|
|
27
27
|
}
|
|
28
28
|
const whats = 'HR elements are used for vertical segmentation';
|
|
29
29
|
return await doTest(
|
|
30
|
-
page, catalog, withItems, 'hr', 'hr', whats, 0, getBadWhat.toString()
|
|
30
|
+
page, catalog, withItems, 'hr', 'body hr', whats, 0, getBadWhat.toString()
|
|
31
31
|
);
|
|
32
32
|
};
|
package/testaro/imageLink.js
CHANGED
|
@@ -33,6 +33,6 @@ exports.reporter = async (page, catalog, withItems) => {
|
|
|
33
33
|
};
|
|
34
34
|
const whats = 'Links have image files as their destinations';
|
|
35
35
|
return await doTest(
|
|
36
|
-
page, catalog, withItems, 'imageLink', 'a[href]', whats, 0, getBadWhat.toString()
|
|
36
|
+
page, catalog, withItems, 'imageLink', 'body a[href]', whats, 0, getBadWhat.toString()
|
|
37
37
|
);
|
|
38
38
|
};
|
package/testaro/labClash.js
CHANGED
|
@@ -41,7 +41,7 @@ exports.reporter = async (page, catalog, withItems) => {
|
|
|
41
41
|
return `Element has inconsistent label types (${labelTypes.join(', ')})`;
|
|
42
42
|
}
|
|
43
43
|
};
|
|
44
|
-
const selector = 'button, input:not([type=hidden]), select, textarea';
|
|
44
|
+
const selector = 'body button, body input:not([type=hidden]), body select, body textarea';
|
|
45
45
|
const whats = 'Elements have inconsistent label types';
|
|
46
46
|
return await doTest(
|
|
47
47
|
page, catalog, withItems, 'labClash', selector, whats, 2, getBadWhat.toString()
|
package/testaro/legendLoc.js
CHANGED
|
@@ -33,6 +33,6 @@ exports.reporter = async (page, catalog, withItems) => {
|
|
|
33
33
|
};
|
|
34
34
|
const whats = 'Legend elements are not the first children of fieldset elements';
|
|
35
35
|
return await doTest(
|
|
36
|
-
page, catalog, withItems, 'legendLoc', 'legend', whats, 3, getBadWhat.toString()
|
|
36
|
+
page, catalog, withItems, 'legendLoc', 'body legend', whats, 3, getBadWhat.toString()
|
|
37
37
|
);
|
|
38
38
|
};
|
package/testaro/lineHeight.js
CHANGED
|
@@ -38,6 +38,20 @@ exports.reporter = async (page, catalog, withItems) => {
|
|
|
38
38
|
const isBad = lineHeightNum < 1.495 * fontSizeNum;
|
|
39
39
|
// If it does:
|
|
40
40
|
if (isBad) {
|
|
41
|
+
const parent = element.parentElement;
|
|
42
|
+
// If the element has a parent:
|
|
43
|
+
if (parent) {
|
|
44
|
+
// Get the style properties of the parent.
|
|
45
|
+
const parentStyleDec = window.getComputedStyle(parent);
|
|
46
|
+
const {fontSize: parentFontSize, lineHeight: parentLineHeight} = parentStyleDec;
|
|
47
|
+
const parentFontSizeNum = Number.parseFloat(parentFontSize);
|
|
48
|
+
const parentLineHeightNum = Number.parseFloat(parentLineHeight);
|
|
49
|
+
// If the parent also violates the rule:
|
|
50
|
+
if (parentLineHeightNum < 1.495 * parentFontSizeNum) {
|
|
51
|
+
// Do not report a violation, because the line height may be inherited.
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
41
55
|
const whatFontSize = `font size (${fontSizeNum.toFixed(1)}px)`;
|
|
42
56
|
const whatLineHeight = `line height (${lineHeightNum.toFixed(1)}px)`;
|
|
43
57
|
// Return a violation description.
|
|
@@ -47,6 +61,6 @@ exports.reporter = async (page, catalog, withItems) => {
|
|
|
47
61
|
};
|
|
48
62
|
const whats = 'Element line heights are less than 1.5 times their font sizes';
|
|
49
63
|
return await doTest(
|
|
50
|
-
page, catalog, withItems, 'lineHeight', '*', whats, 1, getBadWhat.toString()
|
|
64
|
+
page, catalog, withItems, 'lineHeight', 'body, body *', whats, 1, getBadWhat.toString()
|
|
51
65
|
);
|
|
52
66
|
};
|
package/testaro/linkExt.js
CHANGED
|
@@ -26,6 +26,6 @@ exports.reporter = async (page, catalog, withItems) => {
|
|
|
26
26
|
};
|
|
27
27
|
const whats = 'Links have target=_blank attributes';
|
|
28
28
|
return await doTest(
|
|
29
|
-
page, catalog, withItems, 'linkExt', 'a[target=_blank]', whats, 0, getBadWhat.toString()
|
|
29
|
+
page, catalog, withItems, 'linkExt', 'body a[target=_blank]', whats, 0, getBadWhat.toString()
|
|
30
30
|
);
|
|
31
31
|
};
|
package/testaro/linkOldAtt.js
CHANGED
|
@@ -36,7 +36,7 @@ exports.reporter = async (page, catalog, withItems) => {
|
|
|
36
36
|
return `Element has deprecated attributes: ${elementBadAttNames.join(', ')}`;
|
|
37
37
|
}
|
|
38
38
|
};
|
|
39
|
-
const selector = 'a[charset], a[coords], a[name], a[rev], a[shape]';
|
|
39
|
+
const selector = 'body a[charset], body a[coords], body a[name], body a[rev], body a[shape]';
|
|
40
40
|
const whats = 'Links have deprecated attributes';
|
|
41
41
|
return await doTest(
|
|
42
42
|
page, catalog, withItems, 'linkOldAtt', selector, whats, 1, getBadWhat.toString()
|
package/testaro/linkTo.js
CHANGED
|
@@ -33,6 +33,6 @@ exports.reporter = async (page, catalog, withItems) => {
|
|
|
33
33
|
};
|
|
34
34
|
const whats = 'Links are missing href attributes';
|
|
35
35
|
return await doTest(
|
|
36
|
-
page, catalog, withItems, 'linkTo', 'a:not([href]', whats, 2, getBadWhat.toString()
|
|
36
|
+
page, catalog, withItems, 'linkTo', 'body a:not([href]', whats, 2, getBadWhat.toString()
|
|
37
37
|
);
|
|
38
38
|
};
|
package/testaro/linkUl.js
CHANGED
|
@@ -40,6 +40,6 @@ exports.reporter = async (page, catalog, withItems) => {
|
|
|
40
40
|
};
|
|
41
41
|
const whats = 'Links that are not list items are not underlined';
|
|
42
42
|
return await doTest(
|
|
43
|
-
page, catalog, withItems, 'linkUl', 'a', whats, 1, getBadWhat.toString()
|
|
43
|
+
page, catalog, withItems, 'linkUl', 'body a', whats, 1, getBadWhat.toString()
|
|
44
44
|
);
|
|
45
45
|
};
|
package/testaro/miniText.js
CHANGED
|
@@ -40,6 +40,17 @@ exports.reporter = async (page, catalog, withItems) => {
|
|
|
40
40
|
const fontSize = Number.parseFloat(fontSizeString);
|
|
41
41
|
// If its font size is smaller than 11 pixels:
|
|
42
42
|
if (fontSize < 11) {
|
|
43
|
+
const parent = element.parentElement;
|
|
44
|
+
// If the element has a parent:
|
|
45
|
+
if (parent) {
|
|
46
|
+
const parentStyleDec = window.getComputedStyle(parent);
|
|
47
|
+
const parentFontSize = Number.parseFloat(parentStyleDec.fontSize);
|
|
48
|
+
// If the parent also has a font size smaller than 11 pixels:
|
|
49
|
+
if (parentFontSize < 11) {
|
|
50
|
+
// Do not report a violation, because the font size may be inherited.
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
43
54
|
// Return a violation description.
|
|
44
55
|
return `Element is visible but its font size is ${fontSize}px, smaller than 11px`;
|
|
45
56
|
}
|
|
@@ -48,6 +59,6 @@ exports.reporter = async (page, catalog, withItems) => {
|
|
|
48
59
|
};
|
|
49
60
|
const whats = 'Visible elements have font sizes smaller than 11 pixels';
|
|
50
61
|
return await doTest(
|
|
51
|
-
page, catalog, withItems, 'miniText', 'body *:not(script, style)', whats, 2, getBadWhat.toString()
|
|
62
|
+
page, catalog, withItems, 'miniText', 'body, body *:not(script, style)', whats, 2, getBadWhat.toString()
|
|
52
63
|
);
|
|
53
64
|
};
|
package/testaro/nonTable.js
CHANGED
|
@@ -56,6 +56,6 @@ exports.reporter = async (page, catalog, withItems) => {
|
|
|
56
56
|
};
|
|
57
57
|
const whats = 'table elements are misused for non-table content';
|
|
58
58
|
return await doTest(
|
|
59
|
-
page, catalog, withItems, 'nonTable', 'table', whats, 2, getBadWhat.toString()
|
|
59
|
+
page, catalog, withItems, 'nonTable', 'body table', whats, 2, getBadWhat.toString()
|
|
60
60
|
);
|
|
61
61
|
};
|
package/testaro/optRoleSel.js
CHANGED
|
@@ -32,6 +32,6 @@ exports.reporter = async (page, catalog, withItems) => {
|
|
|
32
32
|
};
|
|
33
33
|
const whats = 'Elements with role=option have no aria-selected attributes';
|
|
34
34
|
return await doTest(
|
|
35
|
-
page, catalog, withItems, 'optRoleSel', '[role="option"]', whats, 1, getBadWhat.toString()
|
|
35
|
+
page, catalog, withItems, 'optRoleSel', 'body [role="option"]', whats, 1, getBadWhat.toString()
|
|
36
36
|
);
|
|
37
37
|
};
|
package/testaro/phOnly.js
CHANGED
|
@@ -34,6 +34,6 @@ exports.reporter = async (page, catalog, withItems) => {
|
|
|
34
34
|
};
|
|
35
35
|
const whats = 'input elements have placeholders but no accessible names';
|
|
36
36
|
return await doTest(
|
|
37
|
-
page, catalog, withItems, 'phOnly', 'input[placeholder]', whats, 2, getBadWhat.toString()
|
|
37
|
+
page, catalog, withItems, 'phOnly', 'body input[placeholder]', whats, 2, getBadWhat.toString()
|
|
38
38
|
);
|
|
39
39
|
};
|
package/testaro/radioSet.js
CHANGED
|
@@ -69,6 +69,6 @@ exports.reporter = async (page, catalog, withItems) => {
|
|
|
69
69
|
};
|
|
70
70
|
const whats = 'Radio buttons are not validly grouped in fieldsets with legends';
|
|
71
71
|
return await doTest(
|
|
72
|
-
page, catalog, withItems, 'radioSet', 'input[type=radio]', whats, 2, getBadWhat.toString()
|
|
72
|
+
page, catalog, withItems, 'radioSet', 'body input[type=radio]', whats, 2, getBadWhat.toString()
|
|
73
73
|
);
|
|
74
74
|
};
|
package/testaro/secHeading.js
CHANGED
|
@@ -41,7 +41,7 @@ exports.reporter = async (page, catalog, withItems) => {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
};
|
|
44
|
-
const selector = 'section, article, nav, aside, main';
|
|
44
|
+
const selector = 'body section, body article, body nav, body aside, body main';
|
|
45
45
|
const whats = 'First child headings of sectioning containers are deeper than others';
|
|
46
46
|
return await doTest(
|
|
47
47
|
page, catalog, withItems, 'secHeading', selector, whats, 0, getBadWhat.toString()
|
package/testaro/textSem.js
CHANGED
|
@@ -35,7 +35,7 @@ exports.reporter = async (page, catalog, withItems) => {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
};
|
|
38
|
-
const selector = 'i, b, small';
|
|
38
|
+
const selector = 'body i, body b, body small';
|
|
39
39
|
const whats = 'Semantically vague elements i, b, and/or small are used';
|
|
40
40
|
return await doTest(
|
|
41
41
|
page, catalog, withItems, 'textSem', selector, whats, 0, getBadWhat.toString()
|
package/testaro/titledEl.js
CHANGED
|
@@ -26,7 +26,7 @@ exports.reporter = async (page, catalog, withItems) => {
|
|
|
26
26
|
// Return a violation description.
|
|
27
27
|
return `Likely ineffective title attribute is used on the ${elementType} element`;
|
|
28
28
|
}
|
|
29
|
-
const selector = '[title]:not(iframe, link, style)';
|
|
29
|
+
const selector = 'body [title]:not(iframe, link, style)';
|
|
30
30
|
const whats = 'title attributes are used on elements they are likely ineffective on';
|
|
31
31
|
return await doTest(
|
|
32
32
|
page, catalog, withItems, 'titledEl', selector, whats, 0, getBadWhat.toString()
|
package/testaro/zIndex.js
CHANGED
|
@@ -33,6 +33,6 @@ exports.reporter = async (page, catalog, withItems) => {
|
|
|
33
33
|
};
|
|
34
34
|
const whats = 'Elements have non-default Z indexes';
|
|
35
35
|
return await doTest(
|
|
36
|
-
page, catalog, withItems, 'zIndex', 'body *', whats, 0, getBadWhat.toString()
|
|
36
|
+
page, catalog, withItems, 'zIndex', 'body, body *', whats, 0, getBadWhat.toString()
|
|
37
37
|
);
|
|
38
38
|
};
|
package/tests/testaro.js
CHANGED
|
@@ -41,10 +41,10 @@ const allRules = [
|
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
id: 'allCaps',
|
|
44
|
-
what: '
|
|
44
|
+
what: 'elements with unnecessarily all-capital text substrings',
|
|
45
45
|
contaminates: false,
|
|
46
46
|
needsAccessibleName: false,
|
|
47
|
-
timeOut:
|
|
47
|
+
timeOut: 30,
|
|
48
48
|
defaultOn: true
|
|
49
49
|
},
|
|
50
50
|
{
|
|
@@ -30,7 +30,10 @@
|
|
|
30
30
|
<p class="oblique">This text has been transformed by its style to oblique.</p>
|
|
31
31
|
<p><em id="indirect">This text has no explicit style, but is in an em element, so is made italic by the user agent</em> except at the end</p>
|
|
32
32
|
<p>This text is transformed to italic <span class="italic">only in small part</span>.</p>
|
|
33
|
-
<
|
|
33
|
+
<div id="slantedParent" class="oblique">
|
|
34
|
+
<p>This text is oblique by inheritance from its parent.</p>
|
|
35
|
+
</div>
|
|
36
|
+
<p>Qualifying text nodes: 4</p>
|
|
34
37
|
</main>
|
|
35
38
|
</body>
|
|
36
39
|
</html>
|
package/pw-aslint-claude.js
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
© 2025 Jonathan Robert Pool.
|
|
3
|
-
Licensed under the MIT License. See LICENSE file for details.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// aslint.config.js
|
|
7
|
-
const { defineConfig } = require('aslint');
|
|
8
|
-
|
|
9
|
-
module.exports = defineConfig({
|
|
10
|
-
// Configure ASLint rules
|
|
11
|
-
rules: {
|
|
12
|
-
'image-alt': 'error',
|
|
13
|
-
'button-name': 'error',
|
|
14
|
-
'html-has-lang': 'error',
|
|
15
|
-
'link-name': 'error',
|
|
16
|
-
'page-title': 'error'
|
|
17
|
-
}
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
// test.spec.js
|
|
21
|
-
const { test, expect } = require('@playwright/test');
|
|
22
|
-
const { createASLinter } = require('aslint');
|
|
23
|
-
|
|
24
|
-
test.describe('Accessibility Tests', () => {
|
|
25
|
-
let linter;
|
|
26
|
-
|
|
27
|
-
test.beforeAll(async () => {
|
|
28
|
-
linter = await createASLinter();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test('should pass accessibility checks', async ({ page }) => {
|
|
32
|
-
// Navigate to your page
|
|
33
|
-
await page.goto('https://your-website.com');
|
|
34
|
-
|
|
35
|
-
// Get the page content
|
|
36
|
-
const content = await page.content();
|
|
37
|
-
|
|
38
|
-
// Run ASLint
|
|
39
|
-
const results = await linter.lint(content);
|
|
40
|
-
|
|
41
|
-
// Assert no accessibility violations
|
|
42
|
-
expect(results.violations).toHaveLength(0);
|
|
43
|
-
|
|
44
|
-
// Optional: Log detailed results
|
|
45
|
-
if (results.violations.length > 0) {
|
|
46
|
-
console.log('Accessibility violations found:',
|
|
47
|
-
results.violations.map(v => ({
|
|
48
|
-
rule: v.rule,
|
|
49
|
-
message: v.message,
|
|
50
|
-
element: v.element
|
|
51
|
-
}))
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
// Test specific components
|
|
57
|
-
test('check specific component accessibility', async ({ page }) => {
|
|
58
|
-
await page.goto('https://your-website.com/component');
|
|
59
|
-
|
|
60
|
-
// Wait for specific component to be visible
|
|
61
|
-
await page.waitForSelector('.your-component');
|
|
62
|
-
|
|
63
|
-
// Get component HTML
|
|
64
|
-
const componentHTML = await page.$eval(
|
|
65
|
-
'.your-component',
|
|
66
|
-
el => el.outerHTML
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
const results = await linter.lint(componentHTML);
|
|
70
|
-
expect(results.violations).toHaveLength(0);
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
// playwright.config.js
|
|
75
|
-
const { defineConfig } = require('@playwright/test');
|
|
76
|
-
|
|
77
|
-
module.exports = defineConfig({
|
|
78
|
-
testDir: './tests',
|
|
79
|
-
/* Other Playwright configs */
|
|
80
|
-
use: {
|
|
81
|
-
// Enable automatic accessibility violations logging
|
|
82
|
-
trace: 'retain-on-failure'
|
|
83
|
-
},
|
|
84
|
-
reporter: [
|
|
85
|
-
['html'],
|
|
86
|
-
['list']
|
|
87
|
-
]
|
|
88
|
-
});
|