testaro 14.10.0 → 14.12.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/package.json +1 -1
- package/testaro/focOp.js +53 -22
- package/testaro/lineHeight.js +113 -0
- package/testaro/linkExt.js +2 -1
- package/testaro/targetSize.js +103 -0
- package/tests/testaro.js +2 -0
package/package.json
CHANGED
package/testaro/focOp.js
CHANGED
|
@@ -10,9 +10,12 @@
|
|
|
10
10
|
operable, users are likely to be surprised that nothing happens when they try to operate such
|
|
11
11
|
elements. If operable elements are not focusable, users depending on keyboard navigation are
|
|
12
12
|
prevented from operating those elements. The test considers an element operable if it has a
|
|
13
|
-
non-inherited pointer cursor and is not a 'LABEL' element,
|
|
14
|
-
'BUTTON', 'IFRAME', 'INPUT', 'SELECT', 'TEXTAREA'),
|
|
15
|
-
|
|
13
|
+
non-inherited pointer cursor and is not a 'LABEL' element, has an operable tag name ('A',
|
|
14
|
+
'BUTTON', 'IFRAME', 'INPUT', 'SELECT', 'TEXTAREA'), has an interactive explicit role (button,
|
|
15
|
+
link, checkbox, switch, input, textbox, searchbox, combobox, option, treeitem, radio, slider,
|
|
16
|
+
spinbutton, menuitem, menuitemcheckbox, composite, grid, select, listbox, menu, menubar, tree,
|
|
17
|
+
tablist, tab, gridcell, radiogroup, treegrid, widget, or scrollbar), or has an 'onclick'
|
|
18
|
+
attribute. The test considers an element Tab-focusable if its tabIndex property has the value 0.
|
|
16
19
|
*/
|
|
17
20
|
exports.reporter = async (page, withItems) => {
|
|
18
21
|
// Get data on focusability-operability-discrepant elements.
|
|
@@ -29,10 +32,6 @@ exports.reporter = async (page, withItems) => {
|
|
|
29
32
|
onlyOperable: {
|
|
30
33
|
total: 0,
|
|
31
34
|
tagNames: {}
|
|
32
|
-
},
|
|
33
|
-
focusableAndOperable: {
|
|
34
|
-
total: 0,
|
|
35
|
-
tagNames: {}
|
|
36
35
|
}
|
|
37
36
|
}
|
|
38
37
|
}
|
|
@@ -40,22 +39,59 @@ exports.reporter = async (page, withItems) => {
|
|
|
40
39
|
if (withItems) {
|
|
41
40
|
data.items = {
|
|
42
41
|
onlyFocusable: [],
|
|
43
|
-
onlyOperable: []
|
|
44
|
-
focusableAndOperable: []
|
|
42
|
+
onlyOperable: []
|
|
45
43
|
};
|
|
46
44
|
}
|
|
47
45
|
// FUNCTION DEFINITIONS START
|
|
48
46
|
// Returns data on an element’s operability and prevents it from propagating a pointer.
|
|
49
47
|
const operabilityOf = element => {
|
|
48
|
+
// Identify the operable tag names.
|
|
50
49
|
const opTags = new Set(['A', 'BUTTON', 'IFRAME', 'INPUT', 'SELECT', 'TEXTAREA']);
|
|
50
|
+
// Identify the operable roles.
|
|
51
|
+
const opRoles = new Set([
|
|
52
|
+
'button',
|
|
53
|
+
'checkbox',
|
|
54
|
+
'combobox',
|
|
55
|
+
'composite',
|
|
56
|
+
'grid',
|
|
57
|
+
'gridcell',
|
|
58
|
+
'input',
|
|
59
|
+
'link',
|
|
60
|
+
'listbox',
|
|
61
|
+
'menu',
|
|
62
|
+
'menubar',
|
|
63
|
+
'menuitem',
|
|
64
|
+
'menuitemcheckbox',
|
|
65
|
+
'option',
|
|
66
|
+
'radio',
|
|
67
|
+
'radiogroup',
|
|
68
|
+
'scrollbar',
|
|
69
|
+
'searchbox',
|
|
70
|
+
'select',
|
|
71
|
+
'slider',
|
|
72
|
+
'spinbutton',
|
|
73
|
+
'switch',
|
|
74
|
+
'tab',
|
|
75
|
+
'tablist',
|
|
76
|
+
'textbox',
|
|
77
|
+
'tree',
|
|
78
|
+
'treegrid',
|
|
79
|
+
'treeitem',
|
|
80
|
+
'widget',
|
|
81
|
+
]);
|
|
82
|
+
// Identify whether the element has a pointer cursor.
|
|
51
83
|
const hasPointer = window.getComputedStyle(element).cursor === 'pointer';
|
|
84
|
+
// Identify the bases for considering an element operable.
|
|
52
85
|
const opBases = [
|
|
53
86
|
opTags.has(element.tagName),
|
|
54
87
|
element.hasAttribute('onclick'),
|
|
88
|
+
opRoles.has(element.getAttribute('role')),
|
|
55
89
|
hasPointer && element.tagName !== 'LABEL'
|
|
56
90
|
];
|
|
91
|
+
// If the element is operable:
|
|
57
92
|
const result = {operable: opBases.some(basis => basis)};
|
|
58
93
|
if (result.operable) {
|
|
94
|
+
// Add its data to its result.
|
|
59
95
|
result.byTag = opBases[0];
|
|
60
96
|
result.byOnClick = opBases[1];
|
|
61
97
|
result.byPointer = opBases[2];
|
|
@@ -65,14 +101,14 @@ exports.reporter = async (page, withItems) => {
|
|
|
65
101
|
// Change it to the browser default to prevent pointer propagation.
|
|
66
102
|
element.style.cursor = 'default';
|
|
67
103
|
}
|
|
104
|
+
// Return the result.
|
|
68
105
|
return result;
|
|
69
106
|
};
|
|
70
107
|
// Adds facts about an element to data.
|
|
71
108
|
const addFacts = (element, status, byTag, byOnClick, byPointer) => {
|
|
72
109
|
const statusNames = {
|
|
73
110
|
f: 'onlyFocusable',
|
|
74
|
-
o: 'onlyOperable'
|
|
75
|
-
b: 'focusableAndOperable'
|
|
111
|
+
o: 'onlyOperable'
|
|
76
112
|
};
|
|
77
113
|
const statusName = statusNames[status];
|
|
78
114
|
data.totals.types[statusName].total++;
|
|
@@ -100,17 +136,12 @@ exports.reporter = async (page, withItems) => {
|
|
|
100
136
|
elements.forEach(element => {
|
|
101
137
|
// If its tab index is 0, deem it focusable and:
|
|
102
138
|
if (element.tabIndex === 0) {
|
|
103
|
-
// Increment the grand total.
|
|
104
|
-
data.totals.total++;
|
|
105
139
|
// Determine whether and how it is operable.
|
|
106
|
-
const {operable
|
|
107
|
-
// If it is:
|
|
108
|
-
if (operable) {
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
// Otherwise, i.e. if it is not operable:
|
|
113
|
-
else {
|
|
140
|
+
const {operable} = operabilityOf(element);
|
|
141
|
+
// If it is not:
|
|
142
|
+
if (! operable) {
|
|
143
|
+
// Increment the total.
|
|
144
|
+
data.totals.total++;
|
|
114
145
|
// Add its data to the result.
|
|
115
146
|
addFacts(element, 'f');
|
|
116
147
|
}
|
|
@@ -121,7 +152,7 @@ exports.reporter = async (page, withItems) => {
|
|
|
121
152
|
const {operable, byTag, byOnClick, byPointer} = operabilityOf(element);
|
|
122
153
|
// If it is:
|
|
123
154
|
if (operable) {
|
|
124
|
-
// Increment the
|
|
155
|
+
// Increment the total.
|
|
125
156
|
data.totals.total++;
|
|
126
157
|
// Add its data to the result.
|
|
127
158
|
addFacts(element, 'o', byTag, byOnClick, byPointer);
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/*
|
|
2
|
+
targetSize
|
|
3
|
+
Related to Tenon rule 144.
|
|
4
|
+
This test reports text nodes whose line heights are less than 1.5 times their font sizes.
|
|
5
|
+
*/
|
|
6
|
+
exports.reporter = async (page, withItems) => {
|
|
7
|
+
// Identify the text nodes with substandard line heights.
|
|
8
|
+
const data = await page.evaluate(() => {
|
|
9
|
+
// Initialize the result.
|
|
10
|
+
const data = [];
|
|
11
|
+
// Collapse any adjacent text nodes.
|
|
12
|
+
document.body.normalize();
|
|
13
|
+
// Remove the irrelevant text content.
|
|
14
|
+
const extraElements = Array.from(document.body.querySelectorAll('style, script, svg'));
|
|
15
|
+
extraElements.forEach(element => {
|
|
16
|
+
element.textContent = '';
|
|
17
|
+
});
|
|
18
|
+
// FUNCTION DEFINITION START
|
|
19
|
+
// Returns a space-minimized copy of a string.
|
|
20
|
+
const compact = string => string.replace(/[\t\n]/g, ' ').replace(/\s{2,}/g, ' ').trim();
|
|
21
|
+
// FUNCTION DEFINITION END
|
|
22
|
+
// Create a collection of the text nodes.
|
|
23
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
|
24
|
+
const textNodes = [];
|
|
25
|
+
let more = true;
|
|
26
|
+
while(more) {
|
|
27
|
+
if (walker.nextNode()) {
|
|
28
|
+
const nodeText = walker.currentNode.nodeValue;
|
|
29
|
+
const compactNodeText = compact(nodeText);
|
|
30
|
+
if (compactNodeText) {
|
|
31
|
+
textNodes.push(walker.currentNode);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
more = false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// For each of them:
|
|
39
|
+
textNodes.forEach(textNode => {
|
|
40
|
+
// Get the font size and line height of its parent element.
|
|
41
|
+
const parentStyleDec = window.getComputedStyle(textNode.parentElement);
|
|
42
|
+
const parentFontSizeText = parentStyleDec.fontSize;
|
|
43
|
+
const parentLineHeightText = parentStyleDec.lineHeight;
|
|
44
|
+
const parentFontSizeNum = Number.parseFloat(parentFontSizeText);
|
|
45
|
+
const parentLineHeightNum = Number.parseFloat(parentLineHeightText);
|
|
46
|
+
// If the line height is substandard:
|
|
47
|
+
if (parentLineHeightNum < 1.5 * parentFontSizeNum) {
|
|
48
|
+
// Add data on the text node to the result.
|
|
49
|
+
const parentElement = textNode.parentElement;
|
|
50
|
+
let shortText = compact(textNode.nodeValue);
|
|
51
|
+
if (shortText.length > 400) {
|
|
52
|
+
shortText = `${shortText.slice(0, 200)} … ${shortText.slice(-200)}`;
|
|
53
|
+
}
|
|
54
|
+
data.push({
|
|
55
|
+
tagName: parentElement.tagName,
|
|
56
|
+
id: parentElement.id || '',
|
|
57
|
+
fontSize: parentFontSizeNum,
|
|
58
|
+
lineHeight: parentLineHeightNum,
|
|
59
|
+
text: shortText
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
return data;
|
|
64
|
+
});
|
|
65
|
+
// Initialize the result and standard result.
|
|
66
|
+
const totals = [0, data.length, 0, 0];
|
|
67
|
+
const standardInstances = [];
|
|
68
|
+
// If itemization is required:
|
|
69
|
+
if (withItems) {
|
|
70
|
+
// Add it to the standard result.
|
|
71
|
+
data.forEach(item => {
|
|
72
|
+
standardInstances.push({
|
|
73
|
+
ruleID: 'lineHeight',
|
|
74
|
+
what:
|
|
75
|
+
`Text line height ${item.lineHeight} px is less than 1.5 times its font size ${item.fontSize} px`,
|
|
76
|
+
ordinalSeverity: 1,
|
|
77
|
+
tagName: item.tagName,
|
|
78
|
+
id: item.id,
|
|
79
|
+
location: {
|
|
80
|
+
doc: '',
|
|
81
|
+
type: '',
|
|
82
|
+
spec: ''
|
|
83
|
+
},
|
|
84
|
+
excerpt: item.text
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
// Otherwise, i.e. if itemization is not required:
|
|
89
|
+
else {
|
|
90
|
+
// Add a summary instance to the standard result.
|
|
91
|
+
standardInstances.push({
|
|
92
|
+
ruleID: 'lineHeight',
|
|
93
|
+
what: 'Text line heights are less than 1.5 times their font sizes',
|
|
94
|
+
ordinalSeverity: 1,
|
|
95
|
+
count: data.length,
|
|
96
|
+
tagName: '',
|
|
97
|
+
id: '',
|
|
98
|
+
location: {
|
|
99
|
+
doc: '',
|
|
100
|
+
type: '',
|
|
101
|
+
spec: ''
|
|
102
|
+
},
|
|
103
|
+
excerpt: ''
|
|
104
|
+
});
|
|
105
|
+
// Delete the itemization from the result.
|
|
106
|
+
data.length = 0;
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
data,
|
|
110
|
+
totals,
|
|
111
|
+
standardInstances
|
|
112
|
+
};
|
|
113
|
+
};
|
package/testaro/linkExt.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/*
|
|
2
2
|
linkExt
|
|
3
3
|
Related to Tenon rule 218, but stricter.
|
|
4
|
-
This test reports links with target attributes with _blank values
|
|
4
|
+
This test reports links with target attributes with _blank values, because forcibly external links
|
|
5
|
+
risk miscommunication of the externality and remove control from the user.
|
|
5
6
|
*/
|
|
6
7
|
exports.reporter = async (page, withItems) => {
|
|
7
8
|
// Identify the links with target=_blank attributes.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/*
|
|
2
|
+
targetSize
|
|
3
|
+
Related to Tenon rule 152, but stricter.
|
|
4
|
+
This test reports buttons, inputs, and non-inline links with widths or heights smaller than 44 pixels.
|
|
5
|
+
*/
|
|
6
|
+
exports.reporter = async (page, withItems) => {
|
|
7
|
+
// Identify the buttons, inputs, and non-inline links smaller than 44 pixels.
|
|
8
|
+
const data = await page.$$eval(
|
|
9
|
+
// Identify all buttons, inputs, and links.
|
|
10
|
+
'button, input, a',
|
|
11
|
+
suspects => {
|
|
12
|
+
// Initialize the result.
|
|
13
|
+
const data = {
|
|
14
|
+
total: 0,
|
|
15
|
+
items: []
|
|
16
|
+
};
|
|
17
|
+
// FUNCTION DEFINITION START
|
|
18
|
+
// Returns a space-minimized copy of a string.
|
|
19
|
+
const compact = string => string.replace(/[\t\n]/g, '').replace(/\s{2,}/g, ' ').trim();
|
|
20
|
+
// FUNCTION DEFINITION END
|
|
21
|
+
// Identify the buttons, inputs, and non-inline links.
|
|
22
|
+
const eligibles = suspects.filter(suspect => {
|
|
23
|
+
if (['BUTTON', 'INPUT'].includes(suspect.tagName)) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
const parent = suspect.parentElement;
|
|
28
|
+
return (['DIV', 'P'].includes(parent.tagName) && parent.children.length === 1);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
// For each of them:
|
|
32
|
+
eligibles.forEach(eligible => {
|
|
33
|
+
// If its size is substandard:
|
|
34
|
+
const styleDec = window.getComputedStyle(eligible);
|
|
35
|
+
const widthString = styleDec.width;
|
|
36
|
+
const widthNum = Number.parseFloat(widthString);
|
|
37
|
+
const heightString = styleDec.height;
|
|
38
|
+
const heightNum = Number.parseFloat(heightString);
|
|
39
|
+
if (widthNum < 44 || heightNum < 44) {
|
|
40
|
+
// Add it to the result.
|
|
41
|
+
data.total++;
|
|
42
|
+
data.items.push({
|
|
43
|
+
width: widthNum,
|
|
44
|
+
height: heightNum,
|
|
45
|
+
tagName: eligible.tagName,
|
|
46
|
+
id: eligible.id || '',
|
|
47
|
+
text: compact(eligible.textContent)
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
return data;
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
// Initialize the standard result.
|
|
55
|
+
const totals = [data.total, 0, 0, 0];
|
|
56
|
+
const standardInstances = [];
|
|
57
|
+
// If itemization was requested:
|
|
58
|
+
if (withItems) {
|
|
59
|
+
// Add it to the standard result.
|
|
60
|
+
data.items.forEach(item => {
|
|
61
|
+
standardInstances.push({
|
|
62
|
+
ruleID: 'targetSize',
|
|
63
|
+
what:
|
|
64
|
+
`Interactive element has a substandard size (${item.width} px wide, ${item.height} px high)`,
|
|
65
|
+
ordinalSeverity: 0,
|
|
66
|
+
tagName: item.tagName,
|
|
67
|
+
id: item.id,
|
|
68
|
+
location: {
|
|
69
|
+
doc: '',
|
|
70
|
+
type: '',
|
|
71
|
+
spec: ''
|
|
72
|
+
},
|
|
73
|
+
excerpt: item.text
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
// Otherwise, i.e. if itemization was not requested:
|
|
78
|
+
else {
|
|
79
|
+
// Delete the items from the result.
|
|
80
|
+
data.items = [];
|
|
81
|
+
// Add a summary instance to the standard result.
|
|
82
|
+
standardInstances.push({
|
|
83
|
+
ruleID: 'targetSize',
|
|
84
|
+
what: 'Interactive elements are smaller than 44 px wide and high',
|
|
85
|
+
ordinalSeverity: 0,
|
|
86
|
+
count: data.total,
|
|
87
|
+
tagName: '',
|
|
88
|
+
id: '',
|
|
89
|
+
location: {
|
|
90
|
+
doc: '',
|
|
91
|
+
type: '',
|
|
92
|
+
spec: ''
|
|
93
|
+
},
|
|
94
|
+
excerpt: ''
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
// Return the result and standard result.
|
|
98
|
+
return {
|
|
99
|
+
data,
|
|
100
|
+
totals,
|
|
101
|
+
standardInstances
|
|
102
|
+
};
|
|
103
|
+
};
|
package/tests/testaro.js
CHANGED
|
@@ -19,6 +19,7 @@ const evalRules = {
|
|
|
19
19
|
focVis: 'links that are invisible when focused',
|
|
20
20
|
hover: 'hover-caused content changes',
|
|
21
21
|
labClash: 'labeling inconsistencies',
|
|
22
|
+
lineHeight: 'text with a line height less than 1.5 times its font size',
|
|
22
23
|
linkExt: 'links that automatically open new windows',
|
|
23
24
|
linkTo: 'links without destinations',
|
|
24
25
|
linkUl: 'missing underlines on inline links',
|
|
@@ -30,6 +31,7 @@ const evalRules = {
|
|
|
30
31
|
role: 'invalid and native-replacing explicit roles',
|
|
31
32
|
styleDiff: 'style inconsistencies',
|
|
32
33
|
tabNav: 'nonstandard keyboard navigation between elements with the tab role',
|
|
34
|
+
targetSize: 'buttons, inputs, and non-inline links smaller than 44 pixels wide and high',
|
|
33
35
|
titledEl: 'title attributes on inappropriate elements',
|
|
34
36
|
zIndex: 'non-default Z indexes'
|
|
35
37
|
};
|