testaro 1.0.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.
Files changed (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +502 -0
  3. package/aceconfig.js +7 -0
  4. package/commands.js +249 -0
  5. package/index.js +1248 -0
  6. package/package.json +39 -0
  7. package/procs/score/asp09.js +555 -0
  8. package/procs/test/allText.js +76 -0
  9. package/procs/test/allVis.js +17 -0
  10. package/procs/test/linksByType.js +90 -0
  11. package/procs/test/textOf.txt +73 -0
  12. package/scoring/correlation.js +74 -0
  13. package/scoring/correlations.json +327 -0
  14. package/scoring/data.json +26021 -0
  15. package/scoring/dupCounts.js +39 -0
  16. package/scoring/dupCounts.json +112 -0
  17. package/scoring/duplications.json +253 -0
  18. package/scoring/issues.json +304 -0
  19. package/scoring/packageData.js +171 -0
  20. package/scoring/packageIssues.js +34 -0
  21. package/scoring/rulesetData.json +15 -0
  22. package/tests/aatt.js +64 -0
  23. package/tests/alfa.js +107 -0
  24. package/tests/axe.js +109 -0
  25. package/tests/bulk.js +21 -0
  26. package/tests/embAc.js +36 -0
  27. package/tests/focAll.js +62 -0
  28. package/tests/focInd.js +99 -0
  29. package/tests/focOp.js +132 -0
  30. package/tests/hover.js +195 -0
  31. package/tests/ibm.js +89 -0
  32. package/tests/labClash.js +157 -0
  33. package/tests/linkUl.js +65 -0
  34. package/tests/menuNav.js +254 -0
  35. package/tests/motion.js +115 -0
  36. package/tests/radioSet.js +87 -0
  37. package/tests/role.js +164 -0
  38. package/tests/styleDiff.js +146 -0
  39. package/tests/tabNav.js +282 -0
  40. package/tests/wave.js +44 -0
  41. package/tests/zIndex.js +49 -0
  42. package/validation/batches/sample.json +13 -0
  43. package/validation/executors/sample.js +11 -0
  44. package/validation/scripts/app/sample.json +21 -0
  45. package/validation/scripts/test/bulk.json +39 -0
  46. package/validation/scripts/test/embAc.json +45 -0
  47. package/validation/scripts/test/focAll.json +59 -0
  48. package/validation/scripts/test/focInd.json +55 -0
  49. package/validation/scripts/test/focOp.json +53 -0
  50. package/validation/scripts/test/hover.json +47 -0
  51. package/validation/scripts/test/labClash.json +43 -0
  52. package/validation/scripts/test/linkUl.json +62 -0
  53. package/validation/scripts/test/menuNav.json +97 -0
  54. package/validation/scripts/test/motion.json +53 -0
  55. package/validation/scripts/test/radioSet.json +43 -0
  56. package/validation/scripts/test/role.json +42 -0
  57. package/validation/scripts/test/styleDiff.json +61 -0
  58. package/validation/scripts/test/tabNav.json +97 -0
  59. package/validation/scripts/test/zIndex.json +40 -0
  60. package/validation/targets/bulk/bad.html +48 -0
  61. package/validation/targets/bulk/good.html +15 -0
  62. package/validation/targets/embAc/bad.html +21 -0
  63. package/validation/targets/embAc/good.html +15 -0
  64. package/validation/targets/focAll/good.html +15 -0
  65. package/validation/targets/focAll/less.html +15 -0
  66. package/validation/targets/focAll/more.html +16 -0
  67. package/validation/targets/focInd/bad.html +31 -0
  68. package/validation/targets/focInd/good.html +22 -0
  69. package/validation/targets/focOp/bad.html +18 -0
  70. package/validation/targets/focOp/good.html +15 -0
  71. package/validation/targets/hover/bad.html +19 -0
  72. package/validation/targets/hover/good.html +15 -0
  73. package/validation/targets/labClash/bad.html +20 -0
  74. package/validation/targets/labClash/good.html +18 -0
  75. package/validation/targets/linkUl/bad.html +16 -0
  76. package/validation/targets/linkUl/good.html +30 -0
  77. package/validation/targets/linkUl/na.html +20 -0
  78. package/validation/targets/menuNav/bad.html +106 -0
  79. package/validation/targets/menuNav/bad.js +348 -0
  80. package/validation/targets/menuNav/good.html +106 -0
  81. package/validation/targets/menuNav/good.js +365 -0
  82. package/validation/targets/menuNav/style.css +22 -0
  83. package/validation/targets/motion/bad.css +15 -0
  84. package/validation/targets/motion/bad.html +16 -0
  85. package/validation/targets/motion/good.html +15 -0
  86. package/validation/targets/radioSet/bad.html +34 -0
  87. package/validation/targets/radioSet/good.html +27 -0
  88. package/validation/targets/role/bad.html +26 -0
  89. package/validation/targets/role/good.html +22 -0
  90. package/validation/targets/styleDiff/bad.html +35 -0
  91. package/validation/targets/styleDiff/good.html +36 -0
  92. package/validation/targets/tabNav/bad.html +51 -0
  93. package/validation/targets/tabNav/bad.js +35 -0
  94. package/validation/targets/tabNav/good.html +53 -0
  95. package/validation/targets/tabNav/good.js +83 -0
  96. package/validation/targets/tabNav/goodMoz.js +206 -0
  97. package/validation/targets/tabNav/style.css +34 -0
  98. package/validation/targets/zIndex/bad.html +17 -0
  99. package/validation/targets/zIndex/good.html +15 -0
@@ -0,0 +1,171 @@
1
+ /*
2
+ packageData
3
+ Compiles data on the per-URL distributions of issues in package tests.
4
+ Arguments:
5
+ 0. Name of sibling repo containing reports/script directory.
6
+ */
7
+ const fs = require('fs');
8
+ const compilers = {
9
+ aatt: act => {
10
+ const {result} = act;
11
+ let data = null;
12
+ if (Array.isArray(result)) {
13
+ data = {};
14
+ result.forEach(issue => {
15
+ const {type, id} = issue;
16
+ if (type && id) {
17
+ const typeID = `${type[0]}:${id}`;
18
+ if (data[typeID]) {
19
+ data[typeID]++;
20
+ }
21
+ else {
22
+ data[typeID] = 1;
23
+ }
24
+ }
25
+ });
26
+ }
27
+ return data;
28
+ },
29
+ alfa: act => {
30
+ const {result} = act;
31
+ let data = null;
32
+ if (Array.isArray(result)) {
33
+ data = {};
34
+ result.forEach(issue => {
35
+ const {rule} = issue;
36
+ if (rule) {
37
+ const {ruleID} = rule;
38
+ if (data[ruleID]) {
39
+ data[ruleID]++;
40
+ }
41
+ else {
42
+ data[ruleID] = 1;
43
+ }
44
+ }
45
+ });
46
+ }
47
+ return data;
48
+ },
49
+ axe: act => {
50
+ const {result} = act;
51
+ const {items} = result;
52
+ let data = null;
53
+ if (items) {
54
+ data = {};
55
+ items.forEach(item => {
56
+ const {rule, elements} = item;
57
+ if (data[rule]) {
58
+ data[rule] += elements.length;
59
+ }
60
+ else {
61
+ data[rule] = elements.length;
62
+ }
63
+ });
64
+ }
65
+ return data;
66
+ },
67
+ ibm: act => {
68
+ const {result} = act;
69
+ const {content, url} = result;
70
+ const contentViolations = content && content.totals && content.totals.violation;
71
+ const urlViolations = url && url.totals && url.totals.violation;
72
+ let data = null;
73
+ if (contentViolations || urlViolations) {
74
+ let items;
75
+ if (contentViolations && urlViolations) {
76
+ items = contentViolations > urlViolations ? content.items : url.items;
77
+ }
78
+ else {
79
+ items = content.items || url.items;
80
+ }
81
+ if (items) {
82
+ data = {};
83
+ items.forEach(item => {
84
+ const {ruleId, level} = item;
85
+ const issueID = `${level[0]}:${ruleId}`;
86
+ if (data[issueID]) {
87
+ data[issueID]++;
88
+ }
89
+ else {
90
+ data[issueID] = 1;
91
+ }
92
+ });
93
+ }
94
+ }
95
+ return data;
96
+ },
97
+ wave: act => {
98
+ const {result} = act;
99
+ const {categories} = result;
100
+ let data = null;
101
+ if (categories) {
102
+ data = {};
103
+ const {error, contrast, alert} = categories;
104
+ [error, contrast, alert].forEach((category, index) => {
105
+ const {items} = category;
106
+ Object.keys(items).forEach(ruleName => {
107
+ const ruleID = `${'eca'[index]}:${ruleName}`;
108
+ const {count} = items[ruleName];
109
+ if (data[ruleID]) {
110
+ data[ruleID] += count;
111
+ }
112
+ else {
113
+ data[ruleID] = count;
114
+ }
115
+ });
116
+ });
117
+ }
118
+ return data;
119
+ }
120
+ };
121
+ const repo = process.argv[2];
122
+ const compile = repo => {
123
+ const dirPath = `../${repo}/reports/script`;
124
+ const batchDirNames = fs
125
+ .readdirSync(dirPath, {withFileTypes: true})
126
+ .filter(dirEnt => dirEnt.isDirectory())
127
+ .map(dirEnt => dirEnt.name)
128
+ .filter(dirName => fs.readdirSync(`${dirPath}/${dirName}`).includes('jsonReports'));
129
+ const data = {};
130
+ batchDirNames.forEach(batchDirName => {
131
+ const reportNames = fs.readdirSync(`${dirPath}/${batchDirName}/jsonReports`);
132
+ reportNames.forEach(reportName => {
133
+ const reportJSON = fs.readFileSync(`${dirPath}/${batchDirName}/jsonReports/${reportName}`);
134
+ const report = JSON.parse(reportJSON);
135
+ const {acts} = report;
136
+ const urlAct = acts.find(act => act.type === 'url');
137
+ if (urlAct) {
138
+ const url = urlAct.result;
139
+ if (! data[url]) {
140
+ data[url] = {
141
+ aatt: {},
142
+ alfa: {},
143
+ axe: {},
144
+ ibm: {},
145
+ wave: {}
146
+ };
147
+ }
148
+ const testActs = acts.filter(act => act.type === 'test');
149
+ testActs.forEach(testAct => {
150
+ if (testAct.which === 'aatt') {
151
+ data[url].aatt = compilers.aatt(testAct);
152
+ }
153
+ else if (testAct.which === 'alfa') {
154
+ data[url].alfa = compilers.alfa(testAct);
155
+ }
156
+ else if (testAct.which === 'axe') {
157
+ data[url].axe = compilers.axe(testAct);
158
+ }
159
+ else if (testAct.which === 'ibm') {
160
+ data[url].ibm = compilers.ibm(testAct);
161
+ }
162
+ else if (testAct.which === 'wave') {
163
+ data[url].wave = compilers.wave(testAct);
164
+ }
165
+ });
166
+ }
167
+ });
168
+ });
169
+ return data;
170
+ };
171
+ fs.writeFileSync('scoring/package/data.json', JSON.stringify(compile(repo), null, 2));
@@ -0,0 +1,34 @@
1
+ /*
2
+ packageIssues
3
+ Compiles a list of all issue types appearing in packageData.json.
4
+ */
5
+ const fs = require('fs');
6
+ const compile = () => {
7
+ const dataJSON = fs.readFileSync('scoring/package/data.json', 'utf8');
8
+ const reportData = JSON.parse(dataJSON);
9
+ const reports = Object.values(reportData);
10
+ const data = {
11
+ aatt: new Set(),
12
+ alfa: new Set(),
13
+ axe: new Set(),
14
+ ibm: new Set(),
15
+ wave: new Set()
16
+ };
17
+ reports.forEach(entry => {
18
+ Object.keys(entry).forEach(key => {
19
+ if (entry[key] !== null) {
20
+ Object.keys(entry[key]).forEach(issueName => {
21
+ data[key].add(issueName);
22
+ });
23
+ }
24
+ });
25
+ });
26
+ return {
27
+ aatt: Array.from(data.aatt).sort(),
28
+ alfa: Array.from(data.alfa).sort(),
29
+ axe: Array.from(data.axe).sort(),
30
+ ibm: Array.from(data.ibm).sort(),
31
+ wave: Array.from(data.wave).sort()
32
+ };
33
+ };
34
+ fs.writeFileSync('scoring/package/issues.json', JSON.stringify(compile(), null, 2));
@@ -0,0 +1,15 @@
1
+ {
2
+ "aatt": {
3
+ "public": true,
4
+ "url": ""
5
+ },
6
+ "axe": {
7
+ "public": true
8
+ },
9
+ "ibm": {
10
+ "public": true
11
+ },
12
+ "wave": {
13
+ "public": false
14
+ }
15
+ }
package/tests/aatt.js ADDED
@@ -0,0 +1,64 @@
1
+ /*
2
+ aatt
3
+ This test implements the HTML CodeSniffer ruleset for accessibility via AATT.
4
+ */
5
+
6
+ // IMPORTS
7
+ const {evaluate} = require('aatt');
8
+
9
+ // FUNCTIONS
10
+ exports.reporter = async page => {
11
+ const timeLimit = 30;
12
+ // Get the HTML of the document body.
13
+ const source = await page.content();
14
+ // Return the result of a test with the HTML CodeSniffer WCAG 2.1 AA ruleset as a string.
15
+ const report = evaluate({
16
+ source,
17
+ output: 'json',
18
+ engine: 'htmlcs',
19
+ level: 'WCAG2AA'
20
+ });
21
+ // Wait for it until the time limit expires.
22
+ let timeoutID;
23
+ const wait = new Promise(resolve => {
24
+ timeoutID = setTimeout(() => {
25
+ resolve('');
26
+ }, 1000 * timeLimit);
27
+ });
28
+ const resultIfFast = await Promise.race([report, wait]);
29
+ // If it arrived within the time limit:
30
+ if (resultIfFast) {
31
+ clearTimeout(timeoutID);
32
+ // Remove the non-JSON prefix and (if any) suffix from the string.
33
+ const reportJSON = resultIfFast.replace(/^.+?Object]\s+|\s+done\s*$/sg, '');
34
+ try {
35
+ // Convert the JSON string to an array.
36
+ const issueArray = JSON.parse(reportJSON);
37
+ // Remove the notices from the array.
38
+ const nonNotices = issueArray.filter(issue => issue.type !== 'notice');
39
+ // Convert the technique property from a string to an array of strings.
40
+ nonNotices.forEach(issue => {
41
+ if (issue.type) {
42
+ const longTech = issue.techniques;
43
+ issue.techniques = longTech.replace(/a><a/g, 'a>%<a').split('%');
44
+ issue.id = issue
45
+ .techniques
46
+ .map(technique => technique.replace(/^.+?>|<\/a>$/g, ''))
47
+ .sort()
48
+ .join('+');
49
+ }
50
+ });
51
+ return {result: nonNotices};
52
+ }
53
+ catch (error) {
54
+ console.log(`ERROR processing AATT report (${error.message})`);
55
+ return {result: 'ERROR processing AATT report'};
56
+ }
57
+ }
58
+ // Otherwise, i.e. if the result did not arrive within the time limit:
59
+ else {
60
+ // Report the failure.
61
+ console.log('ERROR: getting report took too long');
62
+ return {result: 'ERROR: getting AATT report took too long'};
63
+ }
64
+ };
package/tests/alfa.js ADDED
@@ -0,0 +1,107 @@
1
+ /*
2
+ alfa
3
+ This test implements the alfa ruleset for accessibility.
4
+ */
5
+
6
+ // IMPORTS
7
+ const {Audit} = require('@siteimprove/alfa-act');
8
+ const {Scraper} = require('@siteimprove/alfa-scraper');
9
+ const alfaRules = require('@siteimprove/alfa-rules');
10
+
11
+ // FUNCTIONS
12
+ // Conducts and reports an alfa test.
13
+ exports.reporter = async page => {
14
+ // Get the document containing the summaries of the alfa rules.
15
+ const context = page.context();
16
+ const rulePage = await context.newPage();
17
+ rulePage.on('console', msg => {
18
+ const msgText = msg.text();
19
+ console.log(msgText);
20
+ });
21
+ const response = await rulePage.goto('https://alfa.siteimprove.com/rules', {timeout: 10000})
22
+ .catch(error => {
23
+ console.log(`ERROR: navigation to URL timed out (${error})`);
24
+ return {result: {error: 'ERROR: navigation to URL timed out'}};
25
+ });
26
+ let ruleData = {};
27
+ if (response.status() === 200) {
28
+ // Compile data on the rule IDs and summaries.
29
+ ruleData = await rulePage.evaluate(() => {
30
+ const rulePs = Array.from(document.querySelectorAll('p.h5'));
31
+ const ruleData = {};
32
+ rulePs.forEach(ruleP => {
33
+ const childNodes = Array.from(ruleP.childNodes);
34
+ const ruleID = childNodes[0].textContent.slice(4).toLowerCase();
35
+ const ruleText = childNodes
36
+ .slice(1)
37
+ .map(node => node.textContent)
38
+ .join(' ')
39
+ .trim()
40
+ .replace(/"/g, '\'')
41
+ .replace(/\s+/g, ' ');
42
+ ruleData[ruleID] = ruleText;
43
+ });
44
+ return ruleData;
45
+ });
46
+ await rulePage.close();
47
+ }
48
+ const data = [];
49
+ await Scraper.with(async scraper => {
50
+ for (const input of await scraper.scrape(page.url())) {
51
+ const audit = Audit.of(input, alfaRules.default);
52
+ const outcomes = Array.from(await audit.evaluate());
53
+ outcomes.forEach((outcome, index) => {
54
+ const {target} = outcome;
55
+ if (target && ! target._members) {
56
+ const outcomeJ = outcome.toJSON();
57
+ const verdict = outcomeJ.outcome;
58
+ if (verdict !== 'passed') {
59
+ const {rule} = outcomeJ;
60
+ const {tags, uri, requirements} = rule;
61
+ const ruleID = uri.replace(/^.+-/, '');
62
+ const ruleSummary = ruleData[ruleID] || '';
63
+ const targetJ = outcomeJ.target;
64
+ const codeLines = target.toString().split('\n');
65
+ if (codeLines[0] === '#document') {
66
+ codeLines.splice(2, codeLines.length - 3, '...');
67
+ }
68
+ else if (codeLines[0].startsWith('<html')) {
69
+ codeLines.splice(1, codeLines.length - 2, '...');
70
+ }
71
+ const outcomeData = {
72
+ index,
73
+ verdict,
74
+ rule: {
75
+ ruleID,
76
+ ruleSummary,
77
+ scope: '',
78
+ uri,
79
+ requirements
80
+ },
81
+ target: {
82
+ type: targetJ.type,
83
+ tagName: targetJ.name || '',
84
+ path: target.path(),
85
+ codeLines: codeLines.map(line => line.length > 99 ? `${line.slice(0, 99)}...` : line)
86
+ }
87
+ };
88
+ const etcTags = [];
89
+ tags.forEach(tag => {
90
+ if (tag.type === 'scope') {
91
+ outcomeData.rule.scope = tag.scope;
92
+ }
93
+ else {
94
+ etcTags.push(tag);
95
+ }
96
+ });
97
+ if (etcTags.length) {
98
+ outcomeData.etcTags = etcTags;
99
+ }
100
+ data.push(outcomeData);
101
+ }
102
+ }
103
+ });
104
+ }
105
+ });
106
+ return {result: data};
107
+ };
package/tests/axe.js ADDED
@@ -0,0 +1,109 @@
1
+ /*
2
+ axe
3
+ This test implements the axe-core ruleset for accessibility.
4
+
5
+ The rules argument defaults to all rules; otherwise, specify an array of rule names.
6
+ Experimental, needs-review, and best-practice rules are ignored.
7
+ */
8
+ // IMPORTS
9
+ const {injectAxe, getViolations} = require('axe-playwright');
10
+ // FUNCTIONS
11
+ // Conducts and reports an Axe test.
12
+ exports.reporter = async (page, withItems, rules = []) => {
13
+ // Initialize the report.
14
+ const data = {};
15
+ // Inject axe-core into the page.
16
+ await injectAxe(page)
17
+ .catch(error => {
18
+ console.log(`ERROR: Axe injection failed (${error.message})`);
19
+ data.result = 'ERROR: axe injection failed';
20
+ });
21
+ // If the injection succeeded:
22
+ if (! data.result) {
23
+ // Get the data on the elements violating the specified axe-core rules.
24
+ const axeOptions = {};
25
+ if (rules.length) {
26
+ axeOptions.runOnly = rules;
27
+ }
28
+ const axeReport = await getViolations(page, null, axeOptions)
29
+ .catch(error => {
30
+ console.log(`ERROR: Axe failed (${error.message}'`);
31
+ return '';
32
+ });
33
+ // If the test succeeded:
34
+ if (Array.isArray(axeReport)) {
35
+ // Initialize a report.
36
+ data.warnings = 0;
37
+ data.violations = {
38
+ minor: 0,
39
+ moderate: 0,
40
+ serious: 0,
41
+ critical: 0
42
+ };
43
+ if (withItems) {
44
+ data.items = [];
45
+ }
46
+ // If there were any violations:
47
+ if (axeReport.length) {
48
+ // FUNCTION DEFINITIONS START
49
+ // Compacts a check violation.
50
+ const compactCheck = checkObj => {
51
+ return {
52
+ check: checkObj.id,
53
+ description: checkObj.message,
54
+ impact: checkObj.impact
55
+ };
56
+ };
57
+ // Compacts a violating element.
58
+ const compactViolator = elObj => {
59
+ const out = {
60
+ selector: elObj.target[0],
61
+ impact: elObj.impact
62
+ };
63
+ if (elObj.any && elObj.any.length) {
64
+ out['must pass any of'] = elObj.any.map(checkObj => compactCheck(checkObj));
65
+ }
66
+ if (elObj.none && elObj.none.length) {
67
+ out['must pass all of'] = elObj.none.map(checkObj => compactCheck(checkObj));
68
+ }
69
+ return out;
70
+ };
71
+ // Compacts a violated rule.
72
+ const compactRule = rule => {
73
+ const out = {
74
+ rule: rule.id,
75
+ description: rule.description,
76
+ impact: rule.impact,
77
+ elements: {}
78
+ };
79
+ if (rule.nodes && rule.nodes.length) {
80
+ out.elements = rule.nodes.map(el => compactViolator(el));
81
+ }
82
+ return out;
83
+ };
84
+ // FUNCTION DEFINITIONS END
85
+ // For each rule violated:
86
+ axeReport.forEach(rule => {
87
+ // For each element violating the rule:
88
+ rule.nodes.forEach(element => {
89
+ // Increment the element count of the impact of its violation.
90
+ data.violations[element.impact]++;
91
+ });
92
+ // If details are required:
93
+ if (withItems) {
94
+ // Add it to the report.
95
+ data.items.push(compactRule(rule));
96
+ }
97
+ });
98
+ }
99
+ }
100
+ // Otherwise, i.e. if the test failed:
101
+ else {
102
+ // Report this.
103
+ data.error = 'ERROR: axe failed';
104
+ console.log('ERROR: axe failed');
105
+ }
106
+ }
107
+ // Return the result.
108
+ return {result: data};
109
+ };
package/tests/bulk.js ADDED
@@ -0,0 +1,21 @@
1
+ /*
2
+ bulk
3
+ This test reports the count of visible elements.
4
+
5
+ The test assumes that simplicity and compactness, with one page having one purpose,
6
+ is an accessibility virtue. Users with visual, motor, and cognitive disabilities
7
+ often have trouble finding what they want or understanding the purpose of a page
8
+ if the page is cluttered with content.
9
+ */
10
+ exports.reporter = async page => {
11
+ const data = {};
12
+ await page.waitForSelector('body', {timeout: 10000})
13
+ .catch(error => {
14
+ console.log(`ERROR (${error.message})`);
15
+ data.error = 'ERROR: bulk timed out';
16
+ return {result: data};
17
+ });
18
+ const visibleElements = await page.$$('body :visible');
19
+ data.visibleElements = visibleElements.length;
20
+ return {result: data};
21
+ };
package/tests/embAc.js ADDED
@@ -0,0 +1,36 @@
1
+ /*
2
+ embAc
3
+ This test reports interactive elements (links, buttons, inputs, and select lists)
4
+ contained by links or buttons. Such embedding not only violates the HTML standard,
5
+ but also complicates user interaction and creates risks of error. It becomes
6
+ non-obvious what a user will activate with a click.
7
+ */
8
+ exports.reporter = async (page, withItems) => await page.$$eval(
9
+ 'a a, a button, a input, a select, button a, button button, button input, button select',
10
+ (bads, withItems) => {
11
+ // FUNCTION DEFINITION START
12
+ // Returns a space-minimized copy of a string.
13
+ const compact = string => string.replace(/[\t\n]/g, '').replace(/\s{2,}/g, ' ').trim();
14
+ // FUNCTION DEFINITION END
15
+ const totals = {
16
+ links: 0,
17
+ buttons: 0,
18
+ inputs: 0,
19
+ selects: 0
20
+ };
21
+ const items = [];
22
+ // Total and, if requested, itemize the faulty elements.
23
+ bads.forEach(bad => {
24
+ totals[Object.keys(totals)[['A', 'BUTTON', 'INPUT', 'SELECT'].indexOf(bad.tagName)]]++;
25
+ if (withItems) {
26
+ items.push(compact(bad.outerHTML));
27
+ }
28
+ });
29
+ // Return the result.
30
+ const data = {totals};
31
+ if (withItems) {
32
+ data.items = items;
33
+ }
34
+ return {result: data};
35
+ }, withItems
36
+ );
@@ -0,0 +1,62 @@
1
+ /*
2
+ focAll
3
+ This test reports discrepancies between focusable and Tab-focused element counts.
4
+ The test first counts all the visible focusable (i.e. with tabIndex 0) elements
5
+ (except counting each group of radio buttons as only one focusable element). Then
6
+ it repeatedly presses the Tab (or Option-Tab in webkit) key until it has reached
7
+ all the elements it can and counts those elements. If the latter are more than the
8
+ former, Tab-key navigation made more focusable elements visible. If the latter are
9
+ fewer than the former, the page manages focus so as to prevent Tab-key navigation
10
+ from reaching all focusable elements. Either of these can complicate navigation for
11
+ users. It may disappoint the expectation that the content will remain stable as they
12
+ move with the Tab key, or the expectation that they can reach every focusable element
13
+ (or widget, such as one radio button or tab in each group) merely by pressing the Tab
14
+ key.
15
+ */
16
+ exports.reporter = async page => {
17
+ // Identify the count of visible focusable elements.
18
+ const tabFocusables = await page.$$eval(
19
+ 'body *:visible',
20
+ visibles => {
21
+ const focusables = visibles.filter(visible => visible.tabIndex === 0);
22
+ // Count as focusable only 1 radio button per group.
23
+ const radios = focusables.filter(el => el.tagName === 'INPUT' && el.type === 'radio');
24
+ const radioNames = new Set(radios.map(radio => radio.name));
25
+ return focusables.length - radios.length + radioNames.size;
26
+ }
27
+ );
28
+ /*
29
+ Repeatedly perform a Tab or (in webkit) Opt-Tab keypress and count the focused elements.
30
+ Asumptions:
31
+ 0. No page has more than 2000 focusable elements.
32
+ 1. No shadow root that reports itself as the active element has more than 100 focusable
33
+ descendants.
34
+ */
35
+ let tabFocused = 0;
36
+ let refocused = 0;
37
+ while (refocused < 100 && tabFocused < 2000) {
38
+ await page.keyboard.press(page.browserTypeName === 'webkit' ? 'Alt+Tab' : 'Tab');
39
+ const isNewFocus = await page.evaluate(() => {
40
+ const focus = document.activeElement;
41
+ if (focus === null || focus.tagName === 'BODY' || focus.dataset.autotestfocused) {
42
+ return false;
43
+ }
44
+ else {
45
+ focus.dataset.autotestfocused = 'true';
46
+ return true;
47
+ }
48
+ });
49
+ if (isNewFocus) {
50
+ tabFocused++;
51
+ }
52
+ else {
53
+ refocused++;
54
+ }
55
+ }
56
+ // Return the result.
57
+ return {result: {
58
+ tabFocusables,
59
+ tabFocused,
60
+ discrepancy: tabFocused - tabFocusables
61
+ }};
62
+ };