@setsquare/review-sdk 0.0.1 → 0.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/NOTICE +3 -2
- package/README.md +220 -0
- package/dist/autoscan.d.ts +15 -1
- package/dist/autoscan.d.ts.map +1 -1
- package/dist/autoscan.js +168 -13
- package/dist/autoscan.js.map +1 -1
- package/dist/brand.d.ts +51 -2
- package/dist/brand.d.ts.map +1 -1
- package/dist/brand.js +560 -5
- package/dist/brand.js.map +1 -1
- package/dist/checks/context-change.d.ts +96 -0
- package/dist/checks/context-change.d.ts.map +1 -0
- package/dist/checks/context-change.js +655 -0
- package/dist/checks/context-change.js.map +1 -0
- package/dist/checks/focus.d.ts +154 -0
- package/dist/checks/focus.d.ts.map +1 -0
- package/dist/checks/focus.js +1123 -0
- package/dist/checks/focus.js.map +1 -0
- package/dist/checks/heuristics.d.ts +116 -0
- package/dist/checks/heuristics.d.ts.map +1 -0
- package/dist/checks/heuristics.js +931 -0
- package/dist/checks/heuristics.js.map +1 -0
- package/dist/checks/keyboard.d.ts +106 -0
- package/dist/checks/keyboard.d.ts.map +1 -0
- package/dist/checks/keyboard.js +452 -0
- package/dist/checks/keyboard.js.map +1 -0
- package/dist/checks/reflow.d.ts +75 -0
- package/dist/checks/reflow.d.ts.map +1 -0
- package/dist/checks/reflow.js +285 -0
- package/dist/checks/reflow.js.map +1 -0
- package/dist/checks/rule-pack.d.ts +1 -1
- package/dist/checks/rule-pack.d.ts.map +1 -1
- package/dist/checks/rule-pack.js +350 -45
- package/dist/checks/rule-pack.js.map +1 -1
- package/dist/checks/shortcuts.d.ts +71 -0
- package/dist/checks/shortcuts.d.ts.map +1 -0
- package/dist/checks/shortcuts.js +196 -0
- package/dist/checks/shortcuts.js.map +1 -0
- package/dist/checks/versions.d.ts.map +1 -1
- package/dist/checks/versions.js +33 -1
- package/dist/checks/versions.js.map +1 -1
- package/dist/contract.d.ts +18 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/covers.d.ts +56 -0
- package/dist/covers.d.ts.map +1 -0
- package/dist/covers.js +135 -0
- package/dist/covers.js.map +1 -0
- package/dist/dedupe.d.ts +23 -0
- package/dist/dedupe.d.ts.map +1 -1
- package/dist/dedupe.js +96 -2
- package/dist/dedupe.js.map +1 -1
- package/dist/document.d.ts.map +1 -1
- package/dist/document.js +144 -0
- package/dist/document.js.map +1 -1
- package/dist/envelope.d.ts +8 -0
- package/dist/envelope.d.ts.map +1 -1
- package/dist/envelope.js +28 -0
- package/dist/envelope.js.map +1 -1
- package/dist/fixture.d.ts.map +1 -1
- package/dist/fixture.js +12 -0
- package/dist/fixture.js.map +1 -1
- package/dist/html-report.d.ts +24 -7
- package/dist/html-report.d.ts.map +1 -1
- package/dist/html-report.js +608 -136
- package/dist/html-report.js.map +1 -1
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -1
- package/dist/links.d.ts +43 -0
- package/dist/links.d.ts.map +1 -1
- package/dist/links.js +43 -0
- package/dist/links.js.map +1 -1
- package/dist/observe.d.ts +8 -0
- package/dist/observe.d.ts.map +1 -1
- package/dist/observe.js +45 -1
- package/dist/observe.js.map +1 -1
- package/dist/options.d.ts +53 -0
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +3 -0
- package/dist/options.js.map +1 -1
- package/dist/reading-level.d.ts +22 -1
- package/dist/reading-level.d.ts.map +1 -1
- package/dist/reading-level.js +122 -19
- package/dist/reading-level.js.map +1 -1
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +31 -15
- package/dist/reporter.js.map +1 -1
- package/dist/scan.d.ts +36 -1
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +16 -2
- package/dist/scan.js.map +1 -1
- package/dist/screens.d.ts +55 -4
- package/dist/screens.d.ts.map +1 -1
- package/dist/screens.js +243 -43
- package/dist/screens.js.map +1 -1
- package/dist/summary.d.ts +22 -0
- package/dist/summary.d.ts.map +1 -1
- package/dist/summary.js +197 -22
- package/dist/summary.js.map +1 -1
- package/dist/viewports.d.ts +105 -0
- package/dist/viewports.d.ts.map +1 -0
- package/dist/viewports.js +134 -0
- package/dist/viewports.js.map +1 -0
- package/package.json +5 -4
package/dist/checks/rule-pack.js
CHANGED
|
@@ -25,7 +25,13 @@ import { LINK_PURPOSE_LINK_ONLY_HELP_URL, TARGET_SIZE_ENHANCED_HELP_URL, VISUAL_
|
|
|
25
25
|
* boundary. Version every behavioural change via SETSQUARE_RULE_PACK_VERSION — it
|
|
26
26
|
* feeds the ruleset digest.
|
|
27
27
|
*/
|
|
28
|
-
|
|
28
|
+
// @2 measures the character count with the real `ch` metric rather than a
|
|
29
|
+
// half-em guess, scopes line spacing to paragraphs that have lines, and puts
|
|
30
|
+
// the measurements in the message. @3 scopes the rule to blocks of text —
|
|
31
|
+
// WCAG's own "more than one sentence" — and reports a VIOLATION rather than a
|
|
32
|
+
// judgement, for consistency with how 2.1.2 already handles the same
|
|
33
|
+
// mechanism clause.
|
|
34
|
+
export const SETSQUARE_RULE_PACK_VERSION = 'setsquare-rules@6';
|
|
29
35
|
export const SETSQUARE_RULE_IDS = [
|
|
30
36
|
'setsquare-target-size-aaa',
|
|
31
37
|
'setsquare-visual-presentation',
|
|
@@ -65,36 +71,131 @@ export function rulePackSource() {
|
|
|
65
71
|
// because a rule author's care is not a boundary (D13).
|
|
66
72
|
var href = (node.getAttribute('href') || '').split('#')[0].split('?')[0];
|
|
67
73
|
|
|
74
|
+
/*
|
|
75
|
+
* A DIGEST of the accessible name, never the name itself.
|
|
76
|
+
*
|
|
77
|
+
* The after() phase has to know whether two controls share a name,
|
|
78
|
+
* and for a while it read a data.name this check does not set — so
|
|
79
|
+
* the equivalent-by-name branch below could not fire at all, and
|
|
80
|
+
* every same-named small control was reported as a flat failure with
|
|
81
|
+
* the exception never considered. The fixture lane found it the day
|
|
82
|
+
* it first ran in a browser.
|
|
83
|
+
*
|
|
84
|
+
* The fix is not to start carrying the name: the rule above still
|
|
85
|
+
* holds, and this finding reads the same without the page's text. A
|
|
86
|
+
* 32-bit FNV-1a of the normalised name answers "same or not" without
|
|
87
|
+
* the value being recoverable from it, which is the only question
|
|
88
|
+
* after() asks.
|
|
89
|
+
*/
|
|
90
|
+
var label = (node.getAttribute('aria-label') || node.textContent || '')
|
|
91
|
+
.replace(/\s+/g, ' ')
|
|
92
|
+
.trim()
|
|
93
|
+
.toLowerCase();
|
|
94
|
+
var nameKey = '';
|
|
95
|
+
if (label) {
|
|
96
|
+
var hash = 2166136261;
|
|
97
|
+
for (var c = 0; c < label.length; c += 1) {
|
|
98
|
+
hash ^= label.charCodeAt(c);
|
|
99
|
+
hash = (hash * 16777619) >>> 0;
|
|
100
|
+
}
|
|
101
|
+
nameKey = hash.toString(16);
|
|
102
|
+
}
|
|
103
|
+
|
|
68
104
|
this.data({
|
|
69
105
|
width: width,
|
|
70
106
|
height: height,
|
|
71
107
|
href: href,
|
|
108
|
+
nameKey: nameKey,
|
|
72
109
|
});
|
|
73
110
|
|
|
74
111
|
if (width >= 44 && height >= 44) return true;
|
|
75
112
|
|
|
76
|
-
|
|
113
|
+
/*
|
|
114
|
+
* INLINE EXCEPTION: the target is in a sentence or block of text.
|
|
115
|
+
*
|
|
116
|
+
* This PASSES rather than asking a person, and the difference is the
|
|
117
|
+
* whole point of the exception. An inline element with a text
|
|
118
|
+
* sibling in its parent IS in a sentence — that is what the phrase
|
|
119
|
+
* describes, and the DOM says so plainly. The check used to detect
|
|
120
|
+
* exactly this condition and then return needs-review with the
|
|
121
|
+
* comment "a person decides", which sent every reference link in
|
|
122
|
+
* every paragraph of this repository's own site to a human with
|
|
123
|
+
* nothing to decide: the exception it had just matched exempts them.
|
|
124
|
+
*/
|
|
77
125
|
var display = window.getComputedStyle(node).display;
|
|
78
126
|
if (display === 'inline' || display === 'inline-block') {
|
|
79
127
|
var parent = node.parentElement;
|
|
80
128
|
if (parent) {
|
|
81
129
|
for (var i = 0; i < parent.childNodes.length; i += 1) {
|
|
82
130
|
var sibling = parent.childNodes[i];
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
|
|
131
|
+
if (
|
|
132
|
+
sibling !== node &&
|
|
133
|
+
sibling.nodeType === 3 &&
|
|
134
|
+
sibling.textContent &&
|
|
135
|
+
sibling.textContent.trim()
|
|
136
|
+
) {
|
|
137
|
+
this.data({
|
|
138
|
+
width: width,
|
|
139
|
+
height: height,
|
|
140
|
+
href: href,
|
|
141
|
+
nameKey: nameKey,
|
|
142
|
+
exception: 'inline',
|
|
143
|
+
});
|
|
144
|
+
return true;
|
|
86
145
|
}
|
|
87
146
|
}
|
|
88
147
|
}
|
|
89
148
|
}
|
|
90
149
|
|
|
91
|
-
|
|
92
|
-
|
|
150
|
+
/*
|
|
151
|
+
* USER-AGENT EXCEPTION: the size is the browser's and the author has
|
|
152
|
+
* not modified it.
|
|
153
|
+
*
|
|
154
|
+
* Also decidable, by asking the browser. An unstyled control of the
|
|
155
|
+
* same kind is rendered once, measured, and removed; a control
|
|
156
|
+
* matching that size is the user agent's own and is exempt, and one
|
|
157
|
+
* that does not match has been resized by somebody and is not. The
|
|
158
|
+
* check used to return needs-review for every native checkbox and
|
|
159
|
+
* radio without looking, which exempts the author's 20px checkbox
|
|
160
|
+
* along with the browser's.
|
|
161
|
+
*/
|
|
93
162
|
if (
|
|
94
163
|
node.tagName === 'INPUT' &&
|
|
95
164
|
(node.type === 'checkbox' || node.type === 'radio')
|
|
96
165
|
) {
|
|
97
|
-
|
|
166
|
+
var cache = (window.__setsquareUaSize = window.__setsquareUaSize || {});
|
|
167
|
+
var reference = cache[node.type];
|
|
168
|
+
if (reference === undefined) {
|
|
169
|
+
reference = null;
|
|
170
|
+
try {
|
|
171
|
+
var probe = document.createElement('input');
|
|
172
|
+
probe.type = node.type;
|
|
173
|
+
probe.setAttribute('aria-hidden', 'true');
|
|
174
|
+
probe.style.position = 'absolute';
|
|
175
|
+
probe.style.left = '-9999px';
|
|
176
|
+
document.body.appendChild(probe);
|
|
177
|
+
var probeRect = probe.getBoundingClientRect();
|
|
178
|
+
reference = { w: Math.round(probeRect.width), h: Math.round(probeRect.height) };
|
|
179
|
+
probe.remove();
|
|
180
|
+
} catch (e) {
|
|
181
|
+
reference = null;
|
|
182
|
+
}
|
|
183
|
+
cache[node.type] = reference;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (reference && width === reference.w && height === reference.h) {
|
|
187
|
+
this.data({
|
|
188
|
+
width: width,
|
|
189
|
+
height: height,
|
|
190
|
+
href: href,
|
|
191
|
+
nameKey: nameKey,
|
|
192
|
+
exception: 'user-agent',
|
|
193
|
+
});
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
// Resized by the author, so the exception does not apply and the
|
|
197
|
+
// measurement stands.
|
|
198
|
+
return false;
|
|
98
199
|
}
|
|
99
200
|
|
|
100
201
|
// Deterministically small; the Equivalent exception is ruled out (or
|
|
@@ -102,22 +203,52 @@ export function rulePackSource() {
|
|
|
102
203
|
return false;
|
|
103
204
|
},
|
|
104
205
|
after: function (results) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
206
|
+
/*
|
|
207
|
+
* EQUIVALENT EXCEPTION, split by how certain it is.
|
|
208
|
+
*
|
|
209
|
+
* The SC exempts a target reachable through an equivalent control on
|
|
210
|
+
* the same page that IS big enough. Two ways to match one, and they
|
|
211
|
+
* do not deserve the same answer:
|
|
212
|
+
*
|
|
213
|
+
* same destination — the same normalised href, big enough
|
|
214
|
+
* somewhere else on the page. Same href is the same link; that
|
|
215
|
+
* is the exception, and it PASSES.
|
|
216
|
+
*
|
|
217
|
+
* same name only — a control with the same accessible name and a
|
|
218
|
+
* different destination. That may be a genuine equivalent and
|
|
219
|
+
* may be a coincidence of wording, and nothing in the DOM says
|
|
220
|
+
* which. Needs-review, which is what this exception is actually
|
|
221
|
+
* uncertain about.
|
|
222
|
+
*
|
|
223
|
+
* Both used to collapse into needs-review. Only the second one is a
|
|
224
|
+
* judgement.
|
|
225
|
+
*/
|
|
226
|
+
var bigByHref = {};
|
|
227
|
+
var bigByName = {};
|
|
111
228
|
results.forEach(function (result) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
229
|
+
/*
|
|
230
|
+
* Passed ON SIZE, not passed for any reason. A row carrying an
|
|
231
|
+
* exception passed BECAUSE it is small and exempt — an inline link
|
|
232
|
+
* in a sentence, a 13px browser checkbox — and letting one of those
|
|
233
|
+
* stand as the "equivalent control that is big enough" would exempt
|
|
234
|
+
* every other small control sharing its name or destination on the
|
|
235
|
+
* strength of something smaller still.
|
|
236
|
+
*/
|
|
237
|
+
if (result.result === true && result.data && !result.data.exception) {
|
|
238
|
+
if (result.data.href) bigByHref[result.data.href] = true;
|
|
239
|
+
if (result.data.nameKey) bigByName[result.data.nameKey] = true;
|
|
115
240
|
}
|
|
116
241
|
});
|
|
117
242
|
results.forEach(function (result) {
|
|
118
|
-
if (result.result
|
|
119
|
-
|
|
120
|
-
|
|
243
|
+
if (result.result !== false || !result.data) return;
|
|
244
|
+
if (result.data.href && bigByHref[result.data.href]) {
|
|
245
|
+
result.data.exception = 'equivalent-href';
|
|
246
|
+
result.result = true;
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (result.data.nameKey && bigByName[result.data.nameKey]) {
|
|
250
|
+
result.data.exception = 'equivalent-name';
|
|
251
|
+
result.result = undefined;
|
|
121
252
|
}
|
|
122
253
|
});
|
|
123
254
|
return results;
|
|
@@ -125,10 +256,17 @@ export function rulePackSource() {
|
|
|
125
256
|
metadata: {
|
|
126
257
|
impact: 'moderate',
|
|
127
258
|
messages: {
|
|
128
|
-
pass: 'Target is at least 44 by 44 CSS pixels',
|
|
129
|
-
fail:
|
|
259
|
+
pass: 'Target is at least 44 by 44 CSS pixels, or an exception applies',
|
|
260
|
+
fail:
|
|
261
|
+
'Target measures \${data.width} by \${data.height} CSS pixels against the 44 by 44 ' +
|
|
262
|
+
'WCAG 2.5.5 asks for, and none of the SC exceptions applies: it is not in a ' +
|
|
263
|
+
'sentence, it is not a control the browser sized, and nothing else on the page ' +
|
|
264
|
+
'goes to the same place at a larger size.',
|
|
130
265
|
incomplete:
|
|
131
|
-
'Target
|
|
266
|
+
'Target measures \${data.width} by \${data.height} CSS pixels. Another control on ' +
|
|
267
|
+
'the page shares its name at a larger size, which MAY be the equivalent-control ' +
|
|
268
|
+
'exception — same wording is not the same function, and nothing in the markup ' +
|
|
269
|
+
'says which. A person decides that one.',
|
|
132
270
|
},
|
|
133
271
|
},
|
|
134
272
|
},
|
|
@@ -136,51 +274,149 @@ export function rulePackSource() {
|
|
|
136
274
|
// WCAG 1.4.8 Visual Presentation, AAA. Implemented from the W3C
|
|
137
275
|
// Understanding document for 1.4.8 (linked as this rule's helpUrl).
|
|
138
276
|
// Scoped to the prongs an author actively defeats: justified text,
|
|
139
|
-
// fixed line
|
|
140
|
-
// (40 for CJK)
|
|
141
|
-
//
|
|
277
|
+
// fixed line spacing below 1.5, and measures past 80 characters
|
|
278
|
+
// (40 for CJK) — and only on a BLOCK of text, which WCAG defines as
|
|
279
|
+
// more than one sentence.
|
|
142
280
|
id: 'setsquare-visual-presentation-defaults',
|
|
143
281
|
evaluate: function (node) {
|
|
144
282
|
var style = window.getComputedStyle(node);
|
|
145
283
|
var problems = [];
|
|
146
284
|
|
|
147
|
-
|
|
285
|
+
/*
|
|
286
|
+
* A BLOCK OF TEXT IS MORE THAN ONE SENTENCE. That is WCAG's own
|
|
287
|
+
* definition, and applying the SC without it is why this rule used
|
|
288
|
+
* to fire on card titles, standfirsts and one-line labels — a
|
|
289
|
+
* two-line heading styled as a paragraph has no leading to be
|
|
290
|
+
* insufficient and no measure to be too wide.
|
|
291
|
+
*
|
|
292
|
+
* Counted as sentence-ending punctuation followed by a space or the
|
|
293
|
+
* end of the text, which over-counts an abbreviation and under-counts
|
|
294
|
+
* a sentence ending in a bracket. Both errors are small and both
|
|
295
|
+
* point the same way — towards saying nothing.
|
|
296
|
+
*
|
|
297
|
+
* Note the DOUBLE backslash. This file is one long template literal,
|
|
298
|
+
* so a single one is eaten on the way out: the first version emitted
|
|
299
|
+
* a class followed by (s|$), which matches a full stop followed by a
|
|
300
|
+
* literal letter s, so nothing counted as a sentence and the whole
|
|
301
|
+
* rule silently passed everything on the site it was written for.
|
|
302
|
+
* Check the emitted source, not this one.
|
|
303
|
+
*
|
|
304
|
+
* And no backticks in this comment, for the reason the file header
|
|
305
|
+
* gives — one ends the string, and the parse error lands three
|
|
306
|
+
* checks away from the character that caused it. Which is how this
|
|
307
|
+
* note came to be rewritten.
|
|
308
|
+
*/
|
|
309
|
+
var text = (node.textContent || '').trim();
|
|
310
|
+
var sentences = (text.match(/[.!?…](\\s|$)/g) || []).length;
|
|
311
|
+
if (sentences < 2) return true;
|
|
312
|
+
|
|
313
|
+
if (style.textAlign === 'justify') problems.push('justified text');
|
|
148
314
|
|
|
149
315
|
var fontSize = parseFloat(style.fontSize);
|
|
150
316
|
var lineHeight = parseFloat(style.lineHeight);
|
|
317
|
+
var rect = node.getBoundingClientRect();
|
|
318
|
+
|
|
319
|
+
// Line spacing is a property of a paragraph that HAS lines. A
|
|
320
|
+
// one-line paragraph — a standfirst, a label, a caption — has no
|
|
321
|
+
// leading to be insufficient, and flagging it reported a design
|
|
322
|
+
// decision as an accessibility signal.
|
|
323
|
+
var wraps =
|
|
324
|
+
isFinite(lineHeight) && lineHeight > 0 ? rect.height > lineHeight * 1.5 : false;
|
|
325
|
+
|
|
151
326
|
if (
|
|
327
|
+
wraps &&
|
|
152
328
|
style.lineHeight !== 'normal' &&
|
|
153
329
|
isFinite(lineHeight) &&
|
|
154
330
|
isFinite(fontSize) &&
|
|
155
331
|
fontSize > 0 &&
|
|
156
332
|
lineHeight / fontSize < 1.5
|
|
157
333
|
) {
|
|
158
|
-
problems.push(
|
|
334
|
+
problems.push(
|
|
335
|
+
'line spacing ' +
|
|
336
|
+
(Math.round((lineHeight / fontSize) * 100) / 100) +
|
|
337
|
+
' (needs 1.5)'
|
|
338
|
+
);
|
|
159
339
|
}
|
|
160
340
|
|
|
161
341
|
var lang = (node.closest('[lang]') || document.documentElement)
|
|
162
342
|
.getAttribute('lang');
|
|
163
343
|
var cjk = /^(ja|zh|ko)/i.test(lang || '');
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
344
|
+
|
|
345
|
+
/*
|
|
346
|
+
* ONE CHARACTER IS THE WIDTH OF "0" IN THIS ELEMENT'S OWN FONT.
|
|
347
|
+
*
|
|
348
|
+
* That is what the CSS \`ch\` unit means and it is the conventional
|
|
349
|
+
* proxy for a character. The first version assumed half an em, which
|
|
350
|
+
* is wrong for every real typeface and wrong in the direction that
|
|
351
|
+
* manufactures findings: a measure set to 68ch by hand reported as
|
|
352
|
+
* ~90 characters and was flagged on every prose block of this
|
|
353
|
+
* repository's own site. The probe is memoised on the font string,
|
|
354
|
+
* because a page asks about the same three fonts a hundred times.
|
|
355
|
+
*/
|
|
356
|
+
var probes = (window.__setsquareChProbe = window.__setsquareChProbe || {});
|
|
357
|
+
var font =
|
|
358
|
+
style.fontStyle + ' ' + style.fontWeight + ' ' + style.fontSize + ' ' +
|
|
359
|
+
style.fontFamily;
|
|
360
|
+
var chWidth = probes[font];
|
|
361
|
+
if (chWidth === undefined) {
|
|
362
|
+
chWidth = 0;
|
|
363
|
+
try {
|
|
364
|
+
var probeCtx = (probes.__ctx =
|
|
365
|
+
probes.__ctx || document.createElement('canvas').getContext('2d'));
|
|
366
|
+
if (probeCtx) {
|
|
367
|
+
probeCtx.font = font;
|
|
368
|
+
chWidth = probeCtx.measureText('0').width;
|
|
369
|
+
}
|
|
370
|
+
} catch (e) {
|
|
371
|
+
chWidth = 0;
|
|
372
|
+
}
|
|
373
|
+
probes[font] = chWidth;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// CJK glyphs are square — one em each — so they need no probe.
|
|
377
|
+
var perChar = cjk ? fontSize : chWidth;
|
|
378
|
+
var chars = perChar > 0 ? rect.width / perChar : 0;
|
|
168
379
|
var limit = cjk ? 40 : 80;
|
|
169
|
-
if (isFinite(chars) && chars > limit) {
|
|
170
|
-
problems.push('measure
|
|
380
|
+
if (isFinite(chars) && chars > 0 && chars > limit) {
|
|
381
|
+
problems.push('measure ' + Math.round(chars) + ' characters (limit ' + limit + ')');
|
|
171
382
|
}
|
|
172
383
|
|
|
173
384
|
if (problems.length === 0) return true;
|
|
174
385
|
this.data({ problems: problems.join('; ') });
|
|
175
|
-
|
|
386
|
+
|
|
387
|
+
/*
|
|
388
|
+
* A VIOLATION, not needs-review, and the change is about being
|
|
389
|
+
* consistent with our own precedent rather than about the SC moving.
|
|
390
|
+
*
|
|
391
|
+
* 1.4.8 asks that a MECHANISM be available to achieve these, and no
|
|
392
|
+
* check can see a mechanism — which is exactly the shape of 2.1.2,
|
|
393
|
+
* where the SC also accepts a mechanism the page documents in text
|
|
394
|
+
* and we report the trap as a violation while saying so. Reporting
|
|
395
|
+
* one as a failure and the other as a judgement was incoherent.
|
|
396
|
+
*
|
|
397
|
+
* The stronger reason: a needs-review row on every block of prose is
|
|
398
|
+
* a number nobody acts on, and the measurement here is not in doubt.
|
|
399
|
+
* The page's default presentation either meets the SC's numbers or
|
|
400
|
+
* it does not; whether something elsewhere lets a user override it
|
|
401
|
+
* is a question the finding raises rather than one it pretends to
|
|
402
|
+
* have answered.
|
|
403
|
+
*/
|
|
404
|
+
return false;
|
|
176
405
|
},
|
|
177
406
|
metadata: {
|
|
178
407
|
impact: 'minor',
|
|
179
408
|
messages: {
|
|
180
|
-
pass: 'Text block
|
|
181
|
-
fail:
|
|
182
|
-
|
|
183
|
-
'
|
|
409
|
+
pass: 'Text block presentation meets the 1.4.8 numbers',
|
|
410
|
+
fail:
|
|
411
|
+
'Measured \${data.problems}. WCAG 1.4.8 asks for text that is not justified, ' +
|
|
412
|
+
'line spacing of at least 1.5 within paragraphs, and a measure of no more than ' +
|
|
413
|
+
'80 characters. Line spacing INHERITS, so a run of these usually has one cause ' +
|
|
414
|
+
'in a type scale or a container rather than one per block — worth checking ' +
|
|
415
|
+
'before editing anything. The SC is also met by a mechanism the page offers to ' +
|
|
416
|
+
'change the presentation, which this check cannot see, so a reading-preferences ' +
|
|
417
|
+
'control would change the answer.',
|
|
418
|
+
// Kept for completeness; the check no longer returns undefined.
|
|
419
|
+
incomplete: 'Text block presentation against 1.4.8 could not be measured',
|
|
184
420
|
},
|
|
185
421
|
},
|
|
186
422
|
},
|
|
@@ -207,17 +443,86 @@ export function rulePackSource() {
|
|
|
207
443
|
];
|
|
208
444
|
var isVague = vague.indexOf(name) !== -1 || /^https?:\\/\\//.test(name);
|
|
209
445
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
446
|
+
/*
|
|
447
|
+
* A DIGEST of the destination, for the after() phase below. Never
|
|
448
|
+
* the href itself: this rule already carries the page's own link
|
|
449
|
+
* text, which D13 allowed because for 2.4.9 the name IS the finding,
|
|
450
|
+
* and that permission does not extend to a second page-derived
|
|
451
|
+
* value. The only question after() asks is "same destination or
|
|
452
|
+
* not", and a hash answers it.
|
|
453
|
+
*/
|
|
454
|
+
var href = (node.getAttribute('href') || '').split('#')[0].split('?')[0];
|
|
455
|
+
var hash = 2166136261;
|
|
456
|
+
for (var c = 0; c < href.length; c += 1) {
|
|
457
|
+
hash ^= href.charCodeAt(c);
|
|
458
|
+
hash = (hash * 16777619) >>> 0;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
this.data({ name: name.slice(0, 80), hrefKey: hash.toString(16) });
|
|
462
|
+
|
|
463
|
+
/*
|
|
464
|
+
* A FAILURE, not a question.
|
|
465
|
+
*
|
|
466
|
+
* 2.4.9 asks that the purpose be identifiable from the link text
|
|
467
|
+
* ALONE — that is the whole difference between it and 2.4.4, which
|
|
468
|
+
* is the AA criterion and does allow context. "Click here" does not
|
|
469
|
+
* identify a purpose in any context, and the SC's exception is for
|
|
470
|
+
* links whose purpose is ambiguous to users in general, which is
|
|
471
|
+
* about the destination being genuinely unknowable rather than
|
|
472
|
+
* permission for lazy text.
|
|
473
|
+
*
|
|
474
|
+
* Reported at minor impact because the vague list is ours: the SC
|
|
475
|
+
* names no phrases,
|
|
476
|
+
* so this is a closed set of English wordings standing in for "does
|
|
477
|
+
* not describe its purpose", and the name is in the message so an
|
|
478
|
+
* author can see what we matched.
|
|
479
|
+
*/
|
|
480
|
+
return !isVague;
|
|
481
|
+
},
|
|
482
|
+
after: function (results) {
|
|
483
|
+
/*
|
|
484
|
+
* The other half of 2.4.9, which nothing here was doing: TWO links
|
|
485
|
+
* with the same text going to different places.
|
|
486
|
+
*
|
|
487
|
+
* The text cannot identify the purpose of either, because it is the
|
|
488
|
+
* same text for two purposes. axe's own identical-links-same-purpose
|
|
489
|
+
* finds this and reports incomplete — reasonably, since it is a
|
|
490
|
+
* generic rule — but the SC's requirement is readable: same name,
|
|
491
|
+
* different destination, so the name alone does not distinguish
|
|
492
|
+
* them.
|
|
493
|
+
*
|
|
494
|
+
* Same name AND same destination is not a finding at all. A
|
|
495
|
+
* "Documentation" link in a header and another in a footer are the
|
|
496
|
+
* same link twice, which is how sites are built.
|
|
497
|
+
*/
|
|
498
|
+
var destinations = {};
|
|
499
|
+
results.forEach(function (result) {
|
|
500
|
+
if (!result.data || !result.data.name) return;
|
|
501
|
+
var seen = destinations[result.data.name];
|
|
502
|
+
if (!seen) destinations[result.data.name] = { key: result.data.hrefKey, many: false };
|
|
503
|
+
else if (seen.key !== result.data.hrefKey) seen.many = true;
|
|
504
|
+
});
|
|
505
|
+
results.forEach(function (result) {
|
|
506
|
+
if (result.result === false || !result.data || !result.data.name) return;
|
|
507
|
+
var seen = destinations[result.data.name];
|
|
508
|
+
if (seen && seen.many) {
|
|
509
|
+
result.data.ambiguous = true;
|
|
510
|
+
result.result = false;
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
return results;
|
|
213
514
|
},
|
|
214
515
|
metadata: {
|
|
215
516
|
impact: 'minor',
|
|
216
517
|
messages: {
|
|
217
518
|
pass: 'Link text describes its purpose',
|
|
218
|
-
fail:
|
|
219
|
-
|
|
220
|
-
'
|
|
519
|
+
fail:
|
|
520
|
+
'Link text does not identify the purpose of the link on its own. 2.4.9 asks for ' +
|
|
521
|
+
'the purpose to be identifiable from the text ALONE — that is what separates it ' +
|
|
522
|
+
'from 2.4.4, which is the AA criterion and does allow surrounding context. ' +
|
|
523
|
+
'Resolve it by saying where the link goes: "Read the 2.4.9 criterion" rather ' +
|
|
524
|
+
'than "read more".',
|
|
525
|
+
incomplete: 'Link purpose could not be assessed',
|
|
221
526
|
},
|
|
222
527
|
},
|
|
223
528
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rule-pack.js","sourceRoot":"","sources":["../../src/checks/rule-pack.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,+BAA+B,EAC/B,6BAA6B,EAC7B,4BAA4B,GAC7B,MAAM,aAAa,CAAC;AAErB;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,MAAM,CAAC,MAAM,2BAA2B,GAAG,mBAAmB,CAAC;AAE/D,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,2BAA2B;IAC3B,+BAA+B;IAC/B,wBAAwB;CAChB,CAAC;AAEX;;;GAGG;AACH,MAAM,UAAU,cAAc;IAC5B,OAAO
|
|
1
|
+
{"version":3,"file":"rule-pack.js","sourceRoot":"","sources":["../../src/checks/rule-pack.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,+BAA+B,EAC/B,6BAA6B,EAC7B,4BAA4B,GAC7B,MAAM,aAAa,CAAC;AAErB;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,0EAA0E;AAC1E,6EAA6E;AAC7E,0EAA0E;AAC1E,8EAA8E;AAC9E,qEAAqE;AACrE,oBAAoB;AACpB,MAAM,CAAC,MAAM,2BAA2B,GAAG,mBAAmB,CAAC;AAE/D,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,2BAA2B;IAC3B,+BAA+B;IAC/B,wBAAwB;CAChB,CAAC;AAEX;;;GAGG;AACH,MAAM,UAAU,cAAc;IAC5B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;sBAkfa,6BAA6B;;;;;;;;;;;;;sBAa7B,4BAA4B;;;;;;;;;;;;sBAY5B,+BAA+B;;;;;GAKlD,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Page } from '@playwright/test';
|
|
2
|
+
import type { DynamicCheckEvidence } from './dynamic.js';
|
|
3
|
+
/**
|
|
4
|
+
* WCAG 2.1.4 Character Key Shortcuts (A): a single-character shortcut with no
|
|
5
|
+
* way to turn it off or remap it makes a page unusable for anyone using speech
|
|
6
|
+
* input, because every dictated word is a volley of shortcuts.
|
|
7
|
+
*
|
|
8
|
+
* A small rule and a real failure — and the only one in this package that
|
|
9
|
+
* needs to run BEFORE the customer's own code. There is no way to enumerate a
|
|
10
|
+
* page's event listeners after the fact: `getEventListeners` is a DevTools
|
|
11
|
+
* API and does not exist in page script. So the only route is to wrap
|
|
12
|
+
* `addEventListener` at init time and watch what gets registered.
|
|
13
|
+
*
|
|
14
|
+
* ---------------------------------------------------------------------------
|
|
15
|
+
* **Opt-in, and off by default, because of the footprint.**
|
|
16
|
+
*
|
|
17
|
+
* Everything else this SDK does happens after a page has loaded and can be
|
|
18
|
+
* undone. This patches a DOM method before any of the customer's scripts run,
|
|
19
|
+
* for the whole life of the page. Even proven inert, that is a larger presence
|
|
20
|
+
* in someone's application than a scanner has any business having by default —
|
|
21
|
+
* and "it is only a pass-through" is exactly the reasoning that precedes a
|
|
22
|
+
* subtle break in someone else's product.
|
|
23
|
+
*
|
|
24
|
+
* So it is a choice, `keyShortcuts: true`, and while it is off the criterion
|
|
25
|
+
* reports as a check that exists and did not run here, which is what it is.
|
|
26
|
+
*
|
|
27
|
+
* **Inertness, concretely.** The wrapper records `(type, target)` and the key
|
|
28
|
+
* literals it can read out of the handler's own source, then delegates with
|
|
29
|
+
* the ORIGINAL arguments — never a wrapped listener. That last part is what
|
|
30
|
+
* keeps `removeEventListener` working: a wrapped listener is a different
|
|
31
|
+
* function object and would never match, so a page that adds and removes a
|
|
32
|
+
* handler would silently keep it forever. The fixture asserts exactly that.
|
|
33
|
+
* ---------------------------------------------------------------------------
|
|
34
|
+
*/
|
|
35
|
+
export declare const SETSQUARE_SHORTCUTS_CHECK_VERSION = "setsquare-shortcuts@1";
|
|
36
|
+
export declare const KEY_SHORTCUTS_RULE_ID = "setsquare-key-shortcuts";
|
|
37
|
+
/**
|
|
38
|
+
* Installed before any page script. See the class comment for why this is
|
|
39
|
+
* opt-in; see `fixture.ts` for where it is installed.
|
|
40
|
+
*
|
|
41
|
+
* A source string rather than a function, because `addInitScript` serialises
|
|
42
|
+
* it and a closure over anything in this module would not survive.
|
|
43
|
+
*/
|
|
44
|
+
export declare const KEY_LISTENER_SCRIPT = "\n(() => {\n const found = [];\n Object.defineProperty(window, '__setsquareKeyListeners', {\n value: found,\n // Non-enumerable so it does not appear in a customer's own iteration over\n // window, and non-writable so page code cannot clobber it by accident.\n enumerable: false,\n writable: false,\n configurable: true,\n });\n\n const KEY_EVENTS = new Set(['keydown', 'keypress', 'keyup']);\n const MODIFIER = /\\b(ctrlKey|metaKey|altKey|shiftKey|getModifierState)\\b/;\n\n /*\n * Which keys a handler reacts to, read out of its own source.\n *\n * The instrumentation can see THAT a key listener exists and never which key\n * it cares about \u2014 so without this the finding would be \"this page has a\n * keydown handler\", which is true of most of the web and useful to nobody.\n * Reading the source is a heuristic and is treated as one: everything this\n * rule reports is needs-review.\n *\n * A handler that consults a modifier is not what 2.1.4 is about, so it is\n * left alone. That is most of the false positives.\n */\n const keysIn = (source) => {\n if (typeof source !== 'string' || source.length === 0) return [];\n if (source.indexOf('[native code]') !== -1) return [];\n if (MODIFIER.test(source)) return [];\n\n const keys = new Set();\n const literal = /(?:\\.key\\s*={2,3}\\s*|case\\s+)['\"]([^'\"\\\\]{1})['\"]/g;\n let match;\n while ((match = literal.exec(source)) !== null) {\n const key = match[1];\n // Printable, single character, not whitespace. Enter, Escape and Tab are\n // multi-character names and fall out here, which is right: 2.1.4 is\n // about character keys.\n if (key.trim().length === 1) keys.add(key);\n }\n\n const code = /(?:keyCode|which)\\s*={2,3}\\s*(\\d{2,3})/g;\n while ((match = code.exec(source)) !== null) {\n const value = Number(match[1]);\n // Letters and digits only. The named keys have codes too, and reporting\n // Escape as a character shortcut would be wrong.\n if (value >= 48 && value <= 57) keys.add(String(value - 48));\n else if (value >= 65 && value <= 90) keys.add(String.fromCharCode(value).toLowerCase());\n }\n\n return [...keys];\n };\n\n const original = EventTarget.prototype.addEventListener;\n EventTarget.prototype.addEventListener = function (type, listener, options) {\n try {\n if (KEY_EVENTS.has(type) && (this === document || this === window) && found.length < 40) {\n let source = '';\n try {\n source = typeof listener === 'function' ? Function.prototype.toString.call(listener) : '';\n } catch {}\n const keys = keysIn(source);\n if (keys.length > 0) {\n found.push({ type, target: this === window ? 'window' : 'document', keys });\n }\n }\n } catch {}\n\n /*\n * The original arguments, never a wrapper. A wrapped listener is a\n * different function object, so removeEventListener would never match it\n * and a page that adds and removes a handler would silently keep it for\n * ever. That is the failure this line exists to prevent.\n */\n return original.call(this, type, listener, options);\n };\n})();\n";
|
|
45
|
+
export interface KeyShortcutRow {
|
|
46
|
+
/** `keydown`, `keypress` or `keyup`. */
|
|
47
|
+
type: string;
|
|
48
|
+
/** `document` or `window`. */
|
|
49
|
+
target: string;
|
|
50
|
+
/** The single characters the handler's own source compares against. */
|
|
51
|
+
keys: string[];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Read what the instrumentation saw. Returns nothing when it was never
|
|
55
|
+
* installed, which is the honest answer for an opt-in check that is off.
|
|
56
|
+
*/
|
|
57
|
+
export declare function runKeyShortcuts(page: Page): Promise<DynamicCheckEvidence>;
|
|
58
|
+
/**
|
|
59
|
+
* The verdict, and it is always needs-review or inapplicable — never a
|
|
60
|
+
* violation, and the reason is worth stating rather than assuming.
|
|
61
|
+
*
|
|
62
|
+
* 2.1.4 is satisfied if the shortcut can be turned off, remapped, or is only
|
|
63
|
+
* active when a specific component has focus. All three are page-level
|
|
64
|
+
* affordances: the first two live in a settings screen we are not looking at,
|
|
65
|
+
* and the third is a property of the handler's own logic. We can see that a
|
|
66
|
+
* handler compares against `j`; we cannot see whether the user was ever
|
|
67
|
+
* offered a way to stop it. Reporting a violation from what we CAN see would
|
|
68
|
+
* be asserting the part we cannot.
|
|
69
|
+
*/
|
|
70
|
+
export declare function keyShortcutEvidence(rows: readonly KeyShortcutRow[]): DynamicCheckEvidence;
|
|
71
|
+
//# sourceMappingURL=shortcuts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shortcuts.d.ts","sourceRoot":"","sources":["../../src/checks/shortcuts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAI7C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEzD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,eAAO,MAAM,iCAAiC,0BAA0B,CAAC;AAEzE,eAAO,MAAM,qBAAqB,4BAA4B,CAAC;AAK/D;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,2qGA+E/B,CAAC;AAEF,MAAM,WAAW,cAAc;IAC7B,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,8BAA8B;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAW/E;AAID;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,SAAS,cAAc,EAAE,GAAG,oBAAoB,CA0CzF"}
|