tosijs-ui 1.17.0-beta.2 → 1.17.0-beta.3
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/CHANGELOG.md +15 -1
- package/dist/doc-system/site/a11y-audit-cli.js +65 -21
- package/dist/doc-system/site/a11y-audit.d.ts +8 -0
- package/dist/doc-system/site/a11y-audit.js +5 -2
- package/dist/doc-system/site/orchestrator.js +18 -5
- package/dist/iife.js +2 -2
- package/dist/iife.js.map +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 1.17.0-beta.
|
|
3
|
+
## 1.17.0-beta.3
|
|
4
|
+
|
|
5
|
+
> Prerelease, published under the `beta` dist-tag: install with `tosijs-ui@beta`. `latest` stays
|
|
6
|
+
> on 1.16.41. beta.2 was staged and never published; its notes follow.
|
|
7
|
+
|
|
8
|
+
- **The axe audit waits for a page marked `aria-busy`, and one slow page no longer ends it.** A
|
|
9
|
+
page that is still loading (a 3D scene fetching large models, say) marks an element
|
|
10
|
+
`aria-busy="true"`, the standard ARIA signal, and is waited for up to 30 seconds; other pages
|
|
11
|
+
are audited once they have stopped adding elements, or after five seconds. A page that never
|
|
12
|
+
loads, or is still busy at 30 seconds, is listed as not fully checked, and the build makes no
|
|
13
|
+
claim about it. Before, each page load waited for the network to go idle, so a page that kept
|
|
14
|
+
downloading for 30 seconds, or that polled, threw and ended the audit of every page. The doc
|
|
15
|
+
site reference shows a component setting and clearing `aria-busy`.
|
|
16
|
+
|
|
17
|
+
### 1.17.0-beta.2 (prerelease detail)
|
|
4
18
|
|
|
5
19
|
> Prerelease, published under the `beta` dist-tag: install with `tosijs-ui@beta`. `latest` stays
|
|
6
20
|
> on 1.16.41. Fixes from Learn 3D's first run of beta.1.
|
|
@@ -10,7 +10,7 @@ tosijs-ui. When either is missing, or Chromium is not installed, it says which a
|
|
|
10
10
|
NOT_RUN_EXIT, so the build can say the audit did not run instead of reporting a pass.
|
|
11
11
|
|
|
12
12
|
bun a11y-audit-cli.ts <payload.json> # payload = { outputDir, basePath, pages }
|
|
13
|
-
stdout: one JSON line, { violations: AxeFinding[] }
|
|
13
|
+
stdout: one JSON line, { violations: AxeFinding[], unsettled: { page, reason }[] }
|
|
14
14
|
|
|
15
15
|
Build-time only. Never import this from browser code.
|
|
16
16
|
*/
|
|
@@ -19,7 +19,7 @@ import { createRequire } from 'module';
|
|
|
19
19
|
import { existsSync } from 'fs';
|
|
20
20
|
import { NOT_RUN_EXIT, AXE_TAGS } from './a11y-audit.js';
|
|
21
21
|
const payloadPath = process.argv[2];
|
|
22
|
-
const { outputDir, basePath, pages } = JSON.parse(await Bun.file(payloadPath).text());
|
|
22
|
+
const { outputDir, basePath, pages, slowPageMs } = JSON.parse(await Bun.file(payloadPath).text());
|
|
23
23
|
const notRun = (why) => {
|
|
24
24
|
console.error(why);
|
|
25
25
|
process.exit(NOT_RUN_EXIT);
|
|
@@ -69,7 +69,12 @@ Pages are audited a few at a time, each in its own tab: one at a time took Learn
|
|
|
69
69
|
to ~73s (#3585). Results are kept in page order, so the report reads the same either way.
|
|
70
70
|
*/
|
|
71
71
|
const CONCURRENCY = 4;
|
|
72
|
+
/** How long a page may take to fire `load`, and to finish being `aria-busy`. */
|
|
73
|
+
const SLOW_PAGE_MS = slowPageMs ?? 30_000;
|
|
74
|
+
/** How long an un-busy page may keep adding elements before it is audited as it stands. */
|
|
75
|
+
const QUIET_CAP_MS = 5_000;
|
|
72
76
|
const perPage = [];
|
|
77
|
+
const unsettled = [];
|
|
73
78
|
try {
|
|
74
79
|
const context = await browser.newContext();
|
|
75
80
|
/*
|
|
@@ -83,9 +88,32 @@ try {
|
|
|
83
88
|
while (next < pages.length) {
|
|
84
89
|
const index = next++;
|
|
85
90
|
const pagePath = pages[index];
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
/*
|
|
92
|
+
One slow or broken page must not end the audit of every other page, and must not pass
|
|
93
|
+
unremarked either: it is listed as not fully checked.
|
|
94
|
+
*/
|
|
95
|
+
try {
|
|
96
|
+
await page.goto(`http://localhost:${server.port}${prefix}${pagePath}`, {
|
|
97
|
+
waitUntil: 'load',
|
|
98
|
+
timeout: SLOW_PAGE_MS,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
const message = error.message.split('\n')[0];
|
|
103
|
+
if (!/timeout/i.test(message)) {
|
|
104
|
+
unsettled.push({ page: pagePath, reason: `not audited: ${message}` });
|
|
105
|
+
perPage[index] = [];
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
unsettled.push({
|
|
109
|
+
page: pagePath,
|
|
110
|
+
reason: `did not finish loading in ${SLOW_PAGE_MS / 1000}s; audited as it stood`,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
// Best effort: a page that polls or streams never goes idle, and that is not a defect.
|
|
114
|
+
await page
|
|
115
|
+
.waitForLoadState('networkidle', { timeout: 10_000 })
|
|
116
|
+
.catch(() => undefined);
|
|
89
117
|
// Audit the page a reader uses: hydrated when the bundle loads, static if it does not.
|
|
90
118
|
if (await page.$('tosi-doc-system'))
|
|
91
119
|
await page
|
|
@@ -94,26 +122,42 @@ try {
|
|
|
94
122
|
})
|
|
95
123
|
.catch(() => undefined);
|
|
96
124
|
/*
|
|
97
|
-
Then until
|
|
98
|
-
|
|
99
|
-
|
|
125
|
+
Then until the page has settled: no element added or removed for half a second, and
|
|
126
|
+
nothing marked `aria-busy="true"`. A fixed pause audited a busy machine's pages
|
|
127
|
+
half-built. A page that is still loading (a 3D scene fetching its models, say) says so with
|
|
128
|
+
aria-busy, which screen readers also announce, and is waited for up to SLOW_PAGE_MS; one
|
|
129
|
+
that merely keeps changing is audited after QUIET_CAP_MS.
|
|
100
130
|
*/
|
|
101
|
-
await page.evaluate(() => new Promise((resolve) => {
|
|
102
|
-
|
|
103
|
-
|
|
131
|
+
const settled = await page.evaluate(({ quietCap, busyCap }) => new Promise((resolve) => {
|
|
132
|
+
const start = performance.now();
|
|
133
|
+
let last = start;
|
|
104
134
|
const observer = new MutationObserver(() => {
|
|
105
|
-
|
|
106
|
-
timer = setTimeout(done, 500);
|
|
135
|
+
last = performance.now();
|
|
107
136
|
});
|
|
108
|
-
function done() {
|
|
109
|
-
observer.disconnect();
|
|
110
|
-
clearTimeout(timer);
|
|
111
|
-
clearTimeout(deadline);
|
|
112
|
-
resolve();
|
|
113
|
-
}
|
|
114
137
|
// Elements arriving, not attributes: an animation changes a style forever.
|
|
115
138
|
observer.observe(document, { subtree: true, childList: true });
|
|
116
|
-
|
|
139
|
+
const tick = () => {
|
|
140
|
+
const now = performance.now();
|
|
141
|
+
const busy = !!document.querySelector('[aria-busy="true"]');
|
|
142
|
+
const outcome = now - last >= 500 && !busy
|
|
143
|
+
? 'settled'
|
|
144
|
+
: !busy && now - start >= quietCap
|
|
145
|
+
? 'changing'
|
|
146
|
+
: now - start >= busyCap
|
|
147
|
+
? 'busy'
|
|
148
|
+
: '';
|
|
149
|
+
if (!outcome)
|
|
150
|
+
return void setTimeout(tick, 100);
|
|
151
|
+
observer.disconnect();
|
|
152
|
+
resolve(outcome);
|
|
153
|
+
};
|
|
154
|
+
tick();
|
|
155
|
+
}), { quietCap: QUIET_CAP_MS, busyCap: SLOW_PAGE_MS });
|
|
156
|
+
if (settled === 'busy')
|
|
157
|
+
unsettled.push({
|
|
158
|
+
page: pagePath,
|
|
159
|
+
reason: `still aria-busy after ${SLOW_PAGE_MS / 1000}s; audited as it stood`,
|
|
160
|
+
});
|
|
117
161
|
await page.addScriptTag({ path: axePath });
|
|
118
162
|
const found = await page.evaluate(async (tags) => {
|
|
119
163
|
const result = await window.axe.run(document, {
|
|
@@ -136,4 +180,4 @@ finally {
|
|
|
136
180
|
server.stop(true);
|
|
137
181
|
}
|
|
138
182
|
const violations = perPage.flat();
|
|
139
|
-
console.log(JSON.stringify({ violations }));
|
|
183
|
+
console.log(JSON.stringify({ violations, unsettled }));
|
|
@@ -11,9 +11,15 @@ export interface AxeFinding {
|
|
|
11
11
|
/** a CSS selector for each element that fails the rule */
|
|
12
12
|
targets: string[];
|
|
13
13
|
}
|
|
14
|
+
/** A page the audit could not check fully: it never loaded, or never stopped being busy. */
|
|
15
|
+
export interface UnsettledPage {
|
|
16
|
+
page: string;
|
|
17
|
+
reason: string;
|
|
18
|
+
}
|
|
14
19
|
export type AxeAuditResult = {
|
|
15
20
|
ran: true;
|
|
16
21
|
violations: AxeFinding[];
|
|
22
|
+
unsettled: UnsettledPage[];
|
|
17
23
|
} | {
|
|
18
24
|
ran: false;
|
|
19
25
|
reason: string;
|
|
@@ -25,6 +31,8 @@ export declare function auditBuiltSite(opts: {
|
|
|
25
31
|
pages: string[];
|
|
26
32
|
/** the project Playwright and axe-core are resolved from (default: the working directory) */
|
|
27
33
|
projectDir?: string;
|
|
34
|
+
/** how long a page may take to load, or stay `aria-busy="true"` (default 30s) */
|
|
35
|
+
slowPageMs?: number;
|
|
28
36
|
}): Promise<AxeAuditResult>;
|
|
29
37
|
/** The report: one line per rule per page, worst impact first. */
|
|
30
38
|
export declare function formatAxeFindings(violations: AxeFinding[]): string;
|
|
@@ -25,6 +25,7 @@ export async function auditBuiltSite(opts) {
|
|
|
25
25
|
outputDir: path.resolve(opts.outputDir),
|
|
26
26
|
basePath: opts.basePath ?? '',
|
|
27
27
|
pages: opts.pages,
|
|
28
|
+
slowPageMs: opts.slowPageMs,
|
|
28
29
|
}));
|
|
29
30
|
// `--no-install`: without it Bun resolves a package the project does not have from its global
|
|
30
31
|
// cache, so "not installed" would never be said and the audit would use a Playwright nobody chose.
|
|
@@ -42,8 +43,10 @@ export async function auditBuiltSite(opts) {
|
|
|
42
43
|
return { ran: false, reason: stderr.trim() };
|
|
43
44
|
const line = stdout.trim().split('\n').pop() ?? '';
|
|
44
45
|
try {
|
|
45
|
-
if (code === 0)
|
|
46
|
-
|
|
46
|
+
if (code === 0) {
|
|
47
|
+
const { violations, unsettled = [] } = JSON.parse(line);
|
|
48
|
+
return { ran: true, violations, unsettled };
|
|
49
|
+
}
|
|
47
50
|
}
|
|
48
51
|
catch {
|
|
49
52
|
// fall through: a run that printed no result did not run
|
|
@@ -1706,12 +1706,25 @@ export async function buildSite(config, opts = {}) {
|
|
|
1706
1706
|
console.warn(`\n⚠️ Accessibility: the axe audit did not run, so this build makes no claim\n` +
|
|
1707
1707
|
` about what it would find. ${audit.reason}\n`);
|
|
1708
1708
|
}
|
|
1709
|
-
else if (audit.violations.length) {
|
|
1710
|
-
misconfigured(config.strict, `\n⚠️ Accessibility: axe found ${audit.violations.length} WCAG 2.2 AA failure(s) in the built pages:\n` +
|
|
1711
|
-
`${formatAxeFindings(audit.violations)}\n`);
|
|
1712
|
-
}
|
|
1713
1709
|
else {
|
|
1714
|
-
|
|
1710
|
+
// A page the audit could not check fully is said, never counted as clean.
|
|
1711
|
+
if (audit.unsettled.length)
|
|
1712
|
+
console.warn(`\n⚠️ Accessibility: ${audit.unsettled.length} page(s) were not fully checked, so ` +
|
|
1713
|
+
`this build makes no claim about them. A page that is still loading should mark ` +
|
|
1714
|
+
`itself aria-busy="true"; the audit waits up to 30s for it:\n` +
|
|
1715
|
+
audit.unsettled
|
|
1716
|
+
.map((u) => ` ${u.page} ${u.reason}`)
|
|
1717
|
+
.join('\n') +
|
|
1718
|
+
'\n');
|
|
1719
|
+
const checked = pages.length - audit.unsettled.length;
|
|
1720
|
+
if (audit.violations.length)
|
|
1721
|
+
misconfigured(config.strict, `\n⚠️ Accessibility: axe found ${audit.violations.length} WCAG 2.2 AA failure(s) in the built pages:\n` +
|
|
1722
|
+
`${formatAxeFindings(audit.violations)}\n`);
|
|
1723
|
+
else
|
|
1724
|
+
console.log(`♿ axe: no WCAG 2.2 AA failures in ${checked} fully checked page(s)` +
|
|
1725
|
+
(audit.unsettled.length
|
|
1726
|
+
? ` (${audit.unsettled.length} not fully checked, above)`
|
|
1727
|
+
: ''));
|
|
1715
1728
|
}
|
|
1716
1729
|
}
|
|
1717
1730
|
console.timeEnd('build');
|