tewip-ai 0.58.0 → 0.59.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.
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
// div that only answers to a mouse. The ones that need a rendered page (contrast, focus order,
|
|
7
7
|
// whether alt text is any good) are not attempted here and are listed in NOT_CHECKED instead.
|
|
8
8
|
import { inside, labelledBy } from './markup.js';
|
|
9
|
-
import { ARIA, INTERACTIVE, UNLABELLED_TYPES, at, clickHandler, decorative, hiddenFromEveryone, keyHandler, notRendered, show, tabIndexOf } from './element.js';
|
|
9
|
+
import { ARIA, INTERACTIVE, UNLABELLED_TYPES, at, clickHandler, customElement, decorative, hiddenFromEveryone, keyHandler, notRendered, show, tabIndexOf } from './element.js';
|
|
10
10
|
/** Checks that look at one element at a time. */
|
|
11
|
-
function perElement(t, file, labelFor, openLabels, ancestorKey, namedByComponent = new Set(), boundLabels = new Set()) {
|
|
11
|
+
function perElement(t, file, labelFor, openLabels, ancestorKey, namedByComponent = new Set(), boundLabels = new Set(), labelledByAComponent = false) {
|
|
12
12
|
const out = [];
|
|
13
13
|
const place = at(t, file);
|
|
14
14
|
const name = labelledBy(t);
|
|
@@ -47,13 +47,15 @@ function perElement(t, file, labelFor, openLabels, ancestorKey, namedByComponent
|
|
|
47
47
|
const bound = ['asp-for', 'formControlName', 'v-model', 'data-valmsg-for'].some((key) => typeof t.attrs[key] === 'string' && boundLabels.has(`${key}=${t.attrs[key]}`));
|
|
48
48
|
const linked = bound || (typeof t.attrs.id === 'string' && labelFor.has(t.attrs.id));
|
|
49
49
|
if (!name.named && !linked && openLabels === 0) {
|
|
50
|
-
if (name.unknown || t.dynamic.has('id') || t.dynamic.has('style') || (typeof t.attrs.id === 'string' && namedByComponent.has(t.attrs.id))) {
|
|
50
|
+
if (labelledByAComponent || name.unknown || t.dynamic.has('id') || t.dynamic.has('style') || (typeof t.attrs.id === 'string' && namedByComponent.has(t.attrs.id))) {
|
|
51
51
|
out.push({ rule: nameRule, confidence: 'ask-a-person', ...place,
|
|
52
|
-
why:
|
|
53
|
-
?
|
|
54
|
-
: t.
|
|
55
|
-
?
|
|
56
|
-
: '
|
|
52
|
+
why: labelledByAComponent
|
|
53
|
+
? 'no label is written on this control, but it sits inside a custom element that contains a label - a component that binds the two when it renders, which this file cannot show'
|
|
54
|
+
: typeof t.attrs.id === 'string' && namedByComponent.has(t.attrs.id)
|
|
55
|
+
? `no <label for> is written here, but a component in this file names id="${t.attrs.id}" in one of its props, so the label may be rendered there`
|
|
56
|
+
: t.dynamic.has('style')
|
|
57
|
+
? 'no label can be read here, and its style is an expression, so whether this is displayed at all cannot be read from the markup'
|
|
58
|
+
: 'no label, aria-label or wrapping <label> can be read here, and part of the element is an expression',
|
|
57
59
|
next: 'confirm the field is labelled at runtime; if not, add aria-label or a <label for>' });
|
|
58
60
|
}
|
|
59
61
|
else {
|
|
@@ -87,7 +89,11 @@ function perElement(t, file, labelFor, openLabels, ancestorKey, namedByComponent
|
|
|
87
89
|
// world is built - a backdrop div closes on click, and Escape closes it from the dialog itself.
|
|
88
90
|
// WCAG 2.1.1 asks that the *function* be reachable from the keyboard, not that this particular
|
|
89
91
|
// div be focusable. Excalidraw's Modal.tsx is exactly this shape, and calling it a blocker is wrong.
|
|
90
|
-
|
|
92
|
+
// `!customElement(t)` as well as `!t.component`: `<mat-checkbox (click)="toggle()">` is a checkbox
|
|
93
|
+
// that Angular renders with full keyboard handling, and `<mat-list-item>` likewise. Reading the
|
|
94
|
+
// markup alone they look like bare tags carrying a click. A `<tr (click)>` beside them in the same
|
|
95
|
+
// file is a genuine barrier, and still reported as one - table rows really are not focusable.
|
|
96
|
+
if (clickHandler(t) && !INTERACTIVE.has(t.name) && !t.component && !customElement(t) && !t.attrs.role && tabindex === null && !keyHandler(t) && !ancestorKey) {
|
|
91
97
|
out.push({ rule: 'click-without-keyboard', confidence: 'barrier', ...place,
|
|
92
98
|
why: 'it responds to a click, but it has no role, no tabindex and no key handler, so the keyboard cannot reach or fire it',
|
|
93
99
|
next: 'use a <button>, or add role="button", tabindex="0" and a key handler' });
|
|
@@ -160,7 +166,28 @@ export function checkFile(source, all, file, isDocument = false) {
|
|
|
160
166
|
let openLabels = 0;
|
|
161
167
|
let interactiveDepth = 0;
|
|
162
168
|
let lastHeading = 0;
|
|
163
|
-
/**
|
|
169
|
+
/**
|
|
170
|
+
* The elements currently open around this one: whether each handles keys, whether it is a custom
|
|
171
|
+
* element, and whether a label has appeared inside it.
|
|
172
|
+
*
|
|
173
|
+
* A custom element does its own labelling and does it somewhere else. Angular Material writes
|
|
174
|
+
*
|
|
175
|
+
* <mat-form-field>
|
|
176
|
+
* <mat-label>Title</mat-label>
|
|
177
|
+
* <input matInput type="text">
|
|
178
|
+
* </mat-form-field>
|
|
179
|
+
*
|
|
180
|
+
* and `MatFormField` generates the id on the input and the `for` on the label when it renders.
|
|
181
|
+
* The label is real; it is simply not expressed here. Reading the markup alone, Tewip called 426
|
|
182
|
+
* inputs in **the Angular team's own accessibility-focused component library** unlabelled.
|
|
183
|
+
*
|
|
184
|
+
* The same family as PatternFly's `<FormGroup fieldId>` in Phase 43 and ASP.NET's `asp-for` in
|
|
185
|
+
* Phase 45. This is the third spelling: the component wraps the label and the control together.
|
|
186
|
+
*
|
|
187
|
+
* A dash in the name is what marks a custom element - no standard HTML element has one - and the
|
|
188
|
+
* label must actually appear inside it, so an ordinary `<my-card>` around a bare input is still a
|
|
189
|
+
* barrier rather than a shrug.
|
|
190
|
+
*/
|
|
164
191
|
const open = [];
|
|
165
192
|
for (const [i, t] of all.entries()) {
|
|
166
193
|
if (t.name === 'label')
|
|
@@ -174,9 +201,15 @@ export function checkFile(source, all, file, isDocument = false) {
|
|
|
174
201
|
open.length = from;
|
|
175
202
|
continue;
|
|
176
203
|
}
|
|
177
|
-
|
|
204
|
+
// A label, or a component that is one by name - `<mat-label>`, `<ion-label>`, `<sl-label>`.
|
|
205
|
+
if (t.name === 'label' || /(^|-)labels?$/.test(t.name))
|
|
206
|
+
for (const o of open)
|
|
207
|
+
if (o.custom)
|
|
208
|
+
o.sawLabel = true;
|
|
209
|
+
const labelledByAComponent = open.some((o) => o.custom && o.sawLabel);
|
|
210
|
+
out.push(...perElement(t, file, labelFor, Math.max(openLabels - (t.name === 'label' ? 1 : 0), 0), open.some((o) => o.keys), namedByComponent, boundLabels, labelledByAComponent));
|
|
178
211
|
if (!t.selfClosing)
|
|
179
|
-
open.push({ name: t.name, keys: keyHandler(t) });
|
|
212
|
+
open.push({ name: t.name, keys: keyHandler(t), custom: t.name.includes('-'), sawLabel: false });
|
|
180
213
|
const id = t.attrs.id;
|
|
181
214
|
if (isDocument && id && !t.dynamic.has('id')) {
|
|
182
215
|
if (seenIds.has(id)) {
|
|
@@ -29,8 +29,44 @@ export const ARIA = new Set([
|
|
|
29
29
|
'aria-sort', 'aria-valuemax', 'aria-valuemin', 'aria-valuenow', 'aria-valuetext',
|
|
30
30
|
]);
|
|
31
31
|
/** A click handler, in any of the four dialects this scanner reads. */
|
|
32
|
+
/**
|
|
33
|
+
* A click handler, in any of the spellings the frameworks use.
|
|
34
|
+
*
|
|
35
|
+
* This was a list of four exact names, and two whole frameworks wrote theirs differently:
|
|
36
|
+
*
|
|
37
|
+
* - **Angular** binds events as `(click)="..."`. angular/components has **414** of them and Tewip
|
|
38
|
+
* saw none, so `click-without-keyboard` could never fire in any Angular project at all. The
|
|
39
|
+
* library reported zero, which looked like a clean result and was blindness.
|
|
40
|
+
* - **Vue** allows modifiers, so the attribute is `@click.stop` or `@click.prevent` rather than
|
|
41
|
+
* `@click`. nocodb has **455** of those.
|
|
42
|
+
*
|
|
43
|
+
* Svelte was already covered both ways - `on:click` and Svelte 5's `onclick` - which is why this
|
|
44
|
+
* only showed up when a third framework was finally pointed at.
|
|
45
|
+
*
|
|
46
|
+
* `keyHandler` below had no such problem because it matches on a substring; the trade is that this
|
|
47
|
+
* one has to name the shapes, so that `data-click-target` is not read as a handler.
|
|
48
|
+
*/
|
|
49
|
+
// Angular's `(click)="go()"` reaches here as an attribute plainly named `click`, because the
|
|
50
|
+
// attribute reader strips the parentheses - which is also why `keyHandler` below already worked for
|
|
51
|
+
// Angular and this did not: it matches on a substring and this matched on an exact name. No standard
|
|
52
|
+
// HTML attribute is called `click`, so taking the bare form costs nothing.
|
|
53
|
+
const CLICK = /^(?:on)?(?:click|dblclick)$|^(?:@|v-on:|on:)(?:click|dblclick)(?:[.|][\w.|$-]*)?$/i;
|
|
54
|
+
/**
|
|
55
|
+
* A custom element: a component in markup that does not spell its name with a capital.
|
|
56
|
+
*
|
|
57
|
+
* `t.component` catches React's `<Select>` and `<Foo.Bar>`, and misses every web component and every
|
|
58
|
+
* Angular or Ionic element - `<mat-checkbox>`, `<mat-list-item>`, `<ion-button>`. A dash in the name
|
|
59
|
+
* is what the HTML spec reserves for them, and no standard element has one.
|
|
60
|
+
*
|
|
61
|
+
* It matters wherever Tewip is about to say something certain about an element whose behaviour is
|
|
62
|
+
* written somewhere else. `<mat-checkbox (click)="x()">` is a real checkbox with real keyboard
|
|
63
|
+
* handling once Angular renders it; reading the markup alone it looks like a bare tag with a click.
|
|
64
|
+
*/
|
|
65
|
+
export function customElement(t) {
|
|
66
|
+
return t.name.includes('-');
|
|
67
|
+
}
|
|
32
68
|
export function clickHandler(t) {
|
|
33
|
-
return [
|
|
69
|
+
return [...Object.keys(t.attrs), ...t.dynamic].some((a) => CLICK.test(a));
|
|
34
70
|
}
|
|
35
71
|
export function keyHandler(t) {
|
|
36
72
|
return Object.keys(t.attrs).concat([...t.dynamic]).some((a) => /key(down|up|press)/i.test(a));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tewip-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.59.0",
|
|
4
4
|
"mcpName": "io.github.hmamut39/tewip",
|
|
5
5
|
"description": "Tewip finds why each failing test failed - real bug, flaky test, environment, test data or changed selector - for any stack, from the command line, CI, or your editor.",
|
|
6
6
|
"keywords": [
|