testaro 23.0.0 → 24.0.1
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/package.json +1 -1
- package/procs/operable.js +85 -0
- package/testaro/focOp.js +35 -162
- package/testaro/opFoc.js +58 -0
- package/tests/testaro.js +6 -5
package/package.json
CHANGED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/*
|
|
2
|
+
operable
|
|
3
|
+
|
|
4
|
+
Returns whether the element of a locator is operable., i.e. it has a non-inherited pointer cursor
|
|
5
|
+
and is not a 'LABEL' element, has an operable tag name, has an interactive explicit role, or has
|
|
6
|
+
an 'onclick' attribute.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ########## FUNCTIONS
|
|
10
|
+
|
|
11
|
+
// Gets whether an element is operable.
|
|
12
|
+
exports.isOperable = async loc => {
|
|
13
|
+
// Get whether and, if so, how the element is operable.
|
|
14
|
+
const operabilities = await loc.evaluate(el => {
|
|
15
|
+
// Operable tag names.
|
|
16
|
+
const opTags = new Set(['A', 'BUTTON', 'IFRAME', 'INPUT', 'SELECT', 'TEXTAREA']);
|
|
17
|
+
// Operable roles.
|
|
18
|
+
const opRoles = new Set([
|
|
19
|
+
'button',
|
|
20
|
+
'checkbox',
|
|
21
|
+
'combobox',
|
|
22
|
+
'composite',
|
|
23
|
+
'grid',
|
|
24
|
+
'gridcell',
|
|
25
|
+
'input',
|
|
26
|
+
'link',
|
|
27
|
+
'listbox',
|
|
28
|
+
'menu',
|
|
29
|
+
'menubar',
|
|
30
|
+
'menuitem',
|
|
31
|
+
'menuitemcheckbox',
|
|
32
|
+
'option',
|
|
33
|
+
'radio',
|
|
34
|
+
'radiogroup',
|
|
35
|
+
'scrollbar',
|
|
36
|
+
'searchbox',
|
|
37
|
+
'select',
|
|
38
|
+
'slider',
|
|
39
|
+
'spinbutton',
|
|
40
|
+
'switch',
|
|
41
|
+
'tab',
|
|
42
|
+
'tablist',
|
|
43
|
+
'textbox',
|
|
44
|
+
'tree',
|
|
45
|
+
'treegrid',
|
|
46
|
+
'treeitem',
|
|
47
|
+
'widget',
|
|
48
|
+
]);
|
|
49
|
+
// Initialize the operabilities.
|
|
50
|
+
const opHow = [];
|
|
51
|
+
// If the element is not a label and has a non-inherited pointer cursor:
|
|
52
|
+
let hasPointer = false;
|
|
53
|
+
if (el.tagName !== 'LABEL') {
|
|
54
|
+
const styleDec = window.getComputedStyle(el);
|
|
55
|
+
hasPointer = styleDec.cursor === 'pointer';
|
|
56
|
+
if (hasPointer) {
|
|
57
|
+
el.parentElement.style.cursor = 'default';
|
|
58
|
+
hasPointer = styleDec.cursor === 'pointer';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (hasPointer) {
|
|
62
|
+
// Add this to the operabilities.
|
|
63
|
+
opHow.push('pointer cursor');
|
|
64
|
+
}
|
|
65
|
+
// If the element is clickable:
|
|
66
|
+
if (el.onClick) {
|
|
67
|
+
// Add this to the operabilities.
|
|
68
|
+
opHow.push('click listener');
|
|
69
|
+
}
|
|
70
|
+
// If the element has an operable explicit role:
|
|
71
|
+
const role = el.getAttribute('role');
|
|
72
|
+
if (opRoles.has(role)) {
|
|
73
|
+
// Add this to the operabilities.
|
|
74
|
+
opHow.push(`role ${role}`);
|
|
75
|
+
}
|
|
76
|
+
// If the element has an operable type:
|
|
77
|
+
const tagName = el.tagName;
|
|
78
|
+
if (opTags.has(tagName)) {
|
|
79
|
+
// Add this to the operabilities.
|
|
80
|
+
opHow.push(`tag name ${tagName}`);
|
|
81
|
+
}
|
|
82
|
+
return opHow;
|
|
83
|
+
});
|
|
84
|
+
return operabilities;
|
|
85
|
+
};
|
package/testaro/focOp.js
CHANGED
|
@@ -2,183 +2,56 @@
|
|
|
2
2
|
focOp
|
|
3
3
|
Related to Tenon rule 190.
|
|
4
4
|
|
|
5
|
-
This test reports
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
'BUTTON', 'IFRAME', 'INPUT', 'SELECT', 'TEXTAREA'), has an interactive explicit role (button,
|
|
12
|
-
link, checkbox, switch, input, textbox, searchbox, combobox, option, treeitem, radio, slider,
|
|
13
|
-
spinbutton, menuitem, menuitemcheckbox, composite, grid, select, listbox, menu, menubar, tree,
|
|
14
|
-
tablist, tab, gridcell, radiogroup, treegrid, widget, or scrollbar), or has an 'onclick'
|
|
15
|
-
attribute. The test considers an element Tab-focusable if its tabIndex property has the value 0.
|
|
5
|
+
This test reports Tab-focusable elements that are not operable. The standard practice is to make
|
|
6
|
+
focusable elements operable. If focusable elements are not operable, users are likely to be
|
|
7
|
+
surprised that nothing happens when they try to operate such elements. The test considers an
|
|
8
|
+
element operable if it has a non-inherited pointer cursor and is not a 'LABEL' element, has an
|
|
9
|
+
operable tag name, has an interactive explicit role, or has an 'onclick' attribute. The test
|
|
10
|
+
considers an element Tab-focusable if its tabIndex property has the value 0.
|
|
16
11
|
*/
|
|
17
12
|
|
|
18
13
|
// ########## IMPORTS
|
|
19
14
|
|
|
20
|
-
// Module to
|
|
21
|
-
const {
|
|
15
|
+
// Module to perform common operations.
|
|
16
|
+
const {init, report} = require('../procs/testaro');
|
|
17
|
+
// Module to get operabilities.
|
|
18
|
+
const {isOperable} = require('../procs/operable');
|
|
22
19
|
|
|
23
20
|
// ########## FUNCTIONS
|
|
24
21
|
|
|
22
|
+
// Runs the test and returns the result.
|
|
25
23
|
exports.reporter = async (page, withItems) => {
|
|
26
|
-
// Initialize the
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
'menu',
|
|
44
|
-
'menubar',
|
|
45
|
-
'menuitem',
|
|
46
|
-
'menuitemcheckbox',
|
|
47
|
-
'option',
|
|
48
|
-
'radio',
|
|
49
|
-
'radiogroup',
|
|
50
|
-
'scrollbar',
|
|
51
|
-
'searchbox',
|
|
52
|
-
'select',
|
|
53
|
-
'slider',
|
|
54
|
-
'spinbutton',
|
|
55
|
-
'switch',
|
|
56
|
-
'tab',
|
|
57
|
-
'tablist',
|
|
58
|
-
'textbox',
|
|
59
|
-
'tree',
|
|
60
|
-
'treegrid',
|
|
61
|
-
'treeitem',
|
|
62
|
-
'widget',
|
|
63
|
-
]);
|
|
64
|
-
// Get a locator for all body elements.
|
|
65
|
-
const locAll = page.locator('body *');
|
|
66
|
-
const locsAll = await locAll.all();
|
|
67
|
-
// For each of them:
|
|
68
|
-
for (const loc of locsAll) {
|
|
69
|
-
// Get data on it.
|
|
70
|
-
const focOpData = await loc.evaluate(element => {
|
|
71
|
-
// Tab index.
|
|
72
|
-
const {tabIndex} = element;
|
|
73
|
-
// Cursor.
|
|
74
|
-
let hasPointer = false;
|
|
75
|
-
if (element.tagName !== 'LABEL') {
|
|
76
|
-
const styleDec = window.getComputedStyle(element);
|
|
77
|
-
hasPointer = styleDec.cursor === 'pointer';
|
|
78
|
-
// If the cursor is a pointer:
|
|
79
|
-
if (hasPointer) {
|
|
80
|
-
// Disregard this if the only reason is inheritance.
|
|
81
|
-
element.parentElement.style.cursor = 'default';
|
|
82
|
-
hasPointer = styleDec.cursor === 'pointer';
|
|
83
|
-
}
|
|
24
|
+
// Initialize the locators and result.
|
|
25
|
+
const all = await init(page, 'body *');
|
|
26
|
+
all.result.data.focusableCount = 0;
|
|
27
|
+
// For each locator:
|
|
28
|
+
for (const loc of all.allLocs) {
|
|
29
|
+
// Get whether its element is focusable.
|
|
30
|
+
const isFocusable = await loc.evaluate(el => el.tabIndex === 0);
|
|
31
|
+
// If it is:
|
|
32
|
+
if (isFocusable) {
|
|
33
|
+
// Add this to the report.
|
|
34
|
+
all.result.data.focusableCount++;
|
|
35
|
+
// Get whether it is operable.
|
|
36
|
+
const howOperable = await isOperable(loc);
|
|
37
|
+
// If it is not:
|
|
38
|
+
if (! howOperable.length) {
|
|
39
|
+
// Add the locator to the array of violators.
|
|
40
|
+
all.locs.push(loc);
|
|
84
41
|
}
|
|
85
|
-
const {tagName} = element;
|
|
86
|
-
return {
|
|
87
|
-
tabIndex,
|
|
88
|
-
hasPointer,
|
|
89
|
-
tagName
|
|
90
|
-
};
|
|
91
|
-
});
|
|
92
|
-
focOpData.onClick = await loc.getAttribute('onclick') !== null;
|
|
93
|
-
focOpData.role = await loc.getAttribute('role') || '';
|
|
94
|
-
focOpData.isFocusable = focOpData.tabIndex === 0;
|
|
95
|
-
focOpData.isOperable = focOpData.hasPointer
|
|
96
|
-
|| opTags.has(focOpData.tagName)
|
|
97
|
-
|| focOpData.onClick
|
|
98
|
-
|| opRoles.has(focOpData.role);
|
|
99
|
-
// If it is focusable or operable but not both:
|
|
100
|
-
if (focOpData.isFocusable !== focOpData.isOperable) {
|
|
101
|
-
// Get more data on it.
|
|
102
|
-
const elData = await getLocatorData(loc);
|
|
103
|
-
// Add to the standard result.
|
|
104
|
-
const howOperable = [];
|
|
105
|
-
if (opTags.has(focOpData.tagName)) {
|
|
106
|
-
howOperable.push(`tag name ${focOpData.tagName}`);
|
|
107
|
-
}
|
|
108
|
-
if (focOpData.hasPointer) {
|
|
109
|
-
howOperable.push('pointer cursor');
|
|
110
|
-
}
|
|
111
|
-
if (focOpData.onClick) {
|
|
112
|
-
howOperable.push('click listener');
|
|
113
|
-
}
|
|
114
|
-
if (opRoles.has(focOpData.role)) {
|
|
115
|
-
howOperable.push(`role ${focOpData.role}`);
|
|
116
|
-
}
|
|
117
|
-
const gripe = focOpData.isFocusable
|
|
118
|
-
? 'Tab-focusable but not operable'
|
|
119
|
-
: `operable (${howOperable.join(', ')}) but not Tab-focusable`;
|
|
120
|
-
const ordinalSeverity = focOpData.isFocusable ? 2 : 3;
|
|
121
|
-
totals[ordinalSeverity]++;
|
|
122
|
-
if (withItems) {
|
|
123
|
-
standardInstances.push({
|
|
124
|
-
ruleID: 'focOp',
|
|
125
|
-
what: `Element is ${gripe}`,
|
|
126
|
-
ordinalSeverity,
|
|
127
|
-
tagName: elData.tagName,
|
|
128
|
-
id: elData.id,
|
|
129
|
-
location: elData.location,
|
|
130
|
-
excerpt: elData.excerpt
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
// If itemization is not required:
|
|
136
|
-
if (! withItems) {
|
|
137
|
-
// Add summary instances to the standard result.
|
|
138
|
-
if (totals[2]) {
|
|
139
|
-
standardInstances.push({
|
|
140
|
-
ruleID: 'focOp',
|
|
141
|
-
what: 'Tab-focusable elements are inoperable',
|
|
142
|
-
count: totals[2],
|
|
143
|
-
ordinalSeverity: 2,
|
|
144
|
-
tagName: '',
|
|
145
|
-
id: '',
|
|
146
|
-
location: {
|
|
147
|
-
doc: '',
|
|
148
|
-
type: '',
|
|
149
|
-
spec: ''
|
|
150
|
-
},
|
|
151
|
-
excerpt: ''
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
if (totals[3]) {
|
|
155
|
-
standardInstances.push({
|
|
156
|
-
ruleID: 'focOp',
|
|
157
|
-
what: 'Operable elements are not Tab-focusable',
|
|
158
|
-
count: totals[3],
|
|
159
|
-
ordinalSeverity: 3,
|
|
160
|
-
tagName: '',
|
|
161
|
-
id: '',
|
|
162
|
-
location: {
|
|
163
|
-
doc: '',
|
|
164
|
-
type: '',
|
|
165
|
-
spec: ''
|
|
166
|
-
},
|
|
167
|
-
excerpt: ''
|
|
168
|
-
});
|
|
169
42
|
}
|
|
170
43
|
}
|
|
171
|
-
//
|
|
44
|
+
// Populate and return the result.
|
|
45
|
+
const whats = [
|
|
46
|
+
'Element is Tab-focusable but not operable', 'Elements are Tab-focusable but not operable'
|
|
47
|
+
];
|
|
48
|
+
const testReport = await report(withItems, all, 'focOp', whats, 2);
|
|
49
|
+
// Reload the page, because isOperable() modified it.
|
|
172
50
|
try {
|
|
173
51
|
await page.reload({timeout: 15000});
|
|
174
52
|
}
|
|
175
53
|
catch(error) {
|
|
176
54
|
console.log('ERROR: page reload timed out');
|
|
177
55
|
}
|
|
178
|
-
|
|
179
|
-
return {
|
|
180
|
-
data,
|
|
181
|
-
totals,
|
|
182
|
-
standardInstances
|
|
183
|
-
};
|
|
56
|
+
return testReport;
|
|
184
57
|
};
|
package/testaro/opFoc.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/*
|
|
2
|
+
opFoc
|
|
3
|
+
Related to Tenon rule 190.
|
|
4
|
+
|
|
5
|
+
This test reports operable elements that are not Tab-focusable. The standard practice is to make
|
|
6
|
+
operable elements focusable. If operable elements are not focusable, users who navigate with a
|
|
7
|
+
keyboard are prevented from operating those elements. The test considers an element operable if
|
|
8
|
+
it has a non-inherited pointer cursor and is not a 'LABEL' element, has an operable tag name, has
|
|
9
|
+
an interactive explicit role, or has an 'onclick' attribute. The test considers an element
|
|
10
|
+
Tab-focusable if its tabIndex property has the value 0.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// ########## IMPORTS
|
|
14
|
+
|
|
15
|
+
// Module to perform common operations.
|
|
16
|
+
const {init, report} = require('../procs/testaro');
|
|
17
|
+
// Module to get operabilities.
|
|
18
|
+
const {isOperable} = require('../procs/operable');
|
|
19
|
+
|
|
20
|
+
// ########## FUNCTIONS
|
|
21
|
+
|
|
22
|
+
// Runs the test and returns the result.
|
|
23
|
+
exports.reporter = async (page, withItems) => {
|
|
24
|
+
// Initialize the locators and result.
|
|
25
|
+
const all = await init(page, 'body *');
|
|
26
|
+
all.result.data.operableCount = 0;
|
|
27
|
+
// For each locator:
|
|
28
|
+
for (const loc of all.allLocs) {
|
|
29
|
+
// Get whether and, if so, how its element is operable.
|
|
30
|
+
const operabilities = await isOperable(loc);
|
|
31
|
+
// If it is:
|
|
32
|
+
if (operabilities.length) {
|
|
33
|
+
// Add this to the report.
|
|
34
|
+
all.result.data.operableCount++;
|
|
35
|
+
// Get whether it is focusable.
|
|
36
|
+
const isFocusable = await loc.evaluate(el => el.tabIndex === 0);
|
|
37
|
+
// If it is not:
|
|
38
|
+
if (! isFocusable) {
|
|
39
|
+
// Add the locator to the array of violators.
|
|
40
|
+
all.locs.push([loc, operabilities.join(', ')]);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Populate and return the result.
|
|
45
|
+
const whats = [
|
|
46
|
+
'Element is operable (__param__) but not Tab-focusable',
|
|
47
|
+
'Elements are operable but not Tab-focusable'
|
|
48
|
+
];
|
|
49
|
+
const testReport = await report(withItems, all, 'opFoc', whats, 3);
|
|
50
|
+
// Reload the page, because isOperable() modified it.
|
|
51
|
+
try {
|
|
52
|
+
await page.reload({timeout: 15000});
|
|
53
|
+
}
|
|
54
|
+
catch(error) {
|
|
55
|
+
console.log('ERROR: page reload timed out');
|
|
56
|
+
}
|
|
57
|
+
return testReport;
|
|
58
|
+
};
|
package/tests/testaro.js
CHANGED
|
@@ -19,7 +19,7 @@ const evalRules = {
|
|
|
19
19
|
filter: 'filter styles on elements',
|
|
20
20
|
focAll: 'discrepancies between focusable and Tab-focused elements',
|
|
21
21
|
focInd: 'missing and nonstandard focus indicators',
|
|
22
|
-
focOp: '
|
|
22
|
+
focOp: 'Tab-focusable elements that are not operable',
|
|
23
23
|
focVis: 'links that are invisible when focused',
|
|
24
24
|
headEl: 'invalid elements within the head',
|
|
25
25
|
headingAmb: 'same-level sibling headings with identical texts',
|
|
@@ -36,6 +36,7 @@ const evalRules = {
|
|
|
36
36
|
miniText: 'text smaller than 11 pixels',
|
|
37
37
|
motion: 'motion without user request',
|
|
38
38
|
nonTable: 'table elements used for layout',
|
|
39
|
+
opFoc: 'Operable elements that are not Tab-focusable',
|
|
39
40
|
pseudoP: 'adjacent br elements suspected of nonsemantically simulating p elements',
|
|
40
41
|
radioSet: 'radio buttons not grouped into standard field sets',
|
|
41
42
|
role: 'invalid and native-replacing explicit roles',
|
|
@@ -99,11 +100,11 @@ exports.reporter = async (page, options) => {
|
|
|
99
100
|
testTimes.push([rule, Math.round((endTime - startTime) / 1000)]);
|
|
100
101
|
Object.keys(ruleReport).forEach(key => {
|
|
101
102
|
data.rules[rule][key] = ruleReport[key];
|
|
102
|
-
data.rules[rule].totals = data.rules[rule].totals.map(total => Math.round(total));
|
|
103
|
-
if (ruleReport.prevented) {
|
|
104
|
-
data.preventions.push(rule);
|
|
105
|
-
}
|
|
106
103
|
});
|
|
104
|
+
data.rules[rule].totals = data.rules[rule].totals.map(total => Math.round(total));
|
|
105
|
+
if (ruleReport.prevented) {
|
|
106
|
+
data.preventions.push(rule);
|
|
107
|
+
}
|
|
107
108
|
// If testing is to stop after a failure and the page failed the test:
|
|
108
109
|
if (stopOnFail && ruleReport.totals.some(total => total)) {
|
|
109
110
|
// Stop testing.
|