testaro 4.10.4 → 4.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/commands.js CHANGED
@@ -153,7 +153,7 @@ exports.commands = {
153
153
  axe: [
154
154
  'Perform an Axe test',
155
155
  {
156
- withItems: [true, 'boolean'],
156
+ detailLevel: [true, 'number', '', '0 = least, 4 = most'],
157
157
  rules: [true, 'array', 'areStrings', 'rule names, or empty if all']
158
158
  }
159
159
  ],
@@ -206,7 +206,7 @@ exports.commands = {
206
206
  }
207
207
  ],
208
208
  menuNav: [
209
- 'Perform a tabNav test',
209
+ 'Perform a menuNav test',
210
210
  {
211
211
  withItems: [true, 'boolean']
212
212
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "4.10.4",
3
+ "version": "4.12.0",
4
4
  "description": "Automation of accessibility testing",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/tests/aatt.js CHANGED
@@ -65,9 +65,12 @@ exports.reporter = async (page, waitLong, tryLimit = 4) => {
65
65
  const issueArray = JSON.parse(reportJSON);
66
66
  // Remove the notices from the array.
67
67
  const nonNotices = issueArray.filter(issue => issue.type !== 'notice');
68
- // Convert the technique property from a string to an array of strings.
68
+ // For each non-notice issue:
69
+ let errors = 0;
70
+ let warnings = 0;
69
71
  nonNotices.forEach(issue => {
70
72
  if (issue.type) {
73
+ // Convert the technique property from a string to an array of strings.
71
74
  const longTech = issue.techniques;
72
75
  issue.techniques = longTech.replace(/a><a/g, 'a>%<a').split('%');
73
76
  issue.id = issue
@@ -75,12 +78,24 @@ exports.reporter = async (page, waitLong, tryLimit = 4) => {
75
78
  .map(technique => technique.replace(/^.+?>|<\/a>$/g, ''))
76
79
  .sort()
77
80
  .join('+');
81
+ // Add the issue to the totals.
82
+ if (issue.type === 'error') {
83
+ errors++;
84
+ }
85
+ else if (issue.type === 'warning') {
86
+ warnings++;
87
+ }
78
88
  }
79
89
  });
90
+ // Return the result.
80
91
  return {
81
92
  result: {
93
+ totals: {
94
+ errors,
95
+ warnings
96
+ },
82
97
  report: nonNotices,
83
- triesLeft
98
+ preventionCount: tryLimit - triesLeft - 1
84
99
  }
85
100
  };
86
101
  }
package/tests/alfa.js CHANGED
@@ -44,7 +44,13 @@ exports.reporter = async page => {
44
44
  });
45
45
  await rulePage.close();
46
46
  }
47
- const data = [];
47
+ const data = {
48
+ totals: {
49
+ failures: 0,
50
+ warnings: 0
51
+ },
52
+ items: []
53
+ };
48
54
  await Scraper.with(async scraper => {
49
55
  for (const input of await scraper.scrape(page.url())) {
50
56
  const audit = Audit.of(input, alfaRules.default);
@@ -96,7 +102,13 @@ exports.reporter = async page => {
96
102
  if (etcTags.length) {
97
103
  outcomeData.etcTags = etcTags;
98
104
  }
99
- data.push(outcomeData);
105
+ if (outcomeData.verdict === 'failed') {
106
+ data.totals.failures++;
107
+ }
108
+ else if (outcomeData.verdict === 'cantTell') {
109
+ data.totals.warnings++;
110
+ }
111
+ data.items.push(outcomeData);
100
112
  }
101
113
  }
102
114
  });
package/tests/axe.js CHANGED
@@ -3,13 +3,26 @@
3
3
  This test implements the axe-core ruleset for accessibility.
4
4
 
5
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.
6
+
7
+ The detailLevel argument specifies how many result categories are to be included in the
8
+ details. 0 = none; 1 = violations; 2 = violations and incomplete; 3 = violations, incomplete,
9
+ and passes; 4 = violations, incomplete, passes, and inapplicable. Regardless of the value of this
10
+ argument, Axe-core is instructed to report all nodes with violation or incomplete results, but only
11
+ 1 node per rule found to be passed or inapplicable. Therefore, from the results of this test it
12
+ is possible to count the rules passed and the inapplicable rules, but not the nodes for which each
13
+ rule is passed or inapplicable. To count those nodes, one would need to revise the 'resultTypes'
14
+ property of the 'axeOptions' object.
15
+
16
+ The report of this test shows rule totals by result category and, within the violation and
17
+ incomplete categories, node totals by severity. It does not show rule or node totals by test
18
+ category (“tag”), such as 'wcag21aaa'. Scoring can consider test categories by getting the value
19
+ of the 'tags' property of each rule.
7
20
  */
8
21
  // IMPORTS
9
- const {injectAxe, getViolations} = require('axe-playwright');
22
+ const {injectAxe, getAxeResults} = require('axe-playwright');
10
23
  // FUNCTIONS
11
24
  // Conducts and reports an Axe test.
12
- exports.reporter = async (page, withItems, rules = []) => {
25
+ exports.reporter = async (page, detailLevel, rules = []) => {
13
26
  // Initialize the report.
14
27
  const data = {};
15
28
  // Inject axe-core into the page.
@@ -22,81 +35,65 @@ exports.reporter = async (page, withItems, rules = []) => {
22
35
  // If the injection succeeded:
23
36
  if (! data.prevented) {
24
37
  // Get the data on the elements violating the specified axe-core rules.
25
- const axeOptions = {};
38
+ const axeOptions = {
39
+ resultTypes: ['violations', 'incomplete']
40
+ };
26
41
  if (rules.length) {
27
42
  axeOptions.runOnly = rules;
28
43
  }
29
- const axeReport = await getViolations(page, null, axeOptions)
44
+ else {
45
+ axeOptions.runOnly = ['experimental', 'best-practice', 'wcag2a', 'wcag2aa', 'wcag2aaa', 'wcag21a', 'wcag21aa', 'wcag21aaa'];
46
+ }
47
+ const axeReport = await getAxeResults(page, null, axeOptions)
30
48
  .catch(error => {
31
49
  console.log(`ERROR: Axe failed (${error.message}'`);
32
50
  return '';
33
51
  });
34
52
  // If the test succeeded:
35
- if (Array.isArray(axeReport)) {
36
- // Initialize a report.
37
- data.warnings = 0;
38
- data.violations = {
39
- minor: 0,
40
- moderate: 0,
41
- serious: 0,
42
- critical: 0
53
+ const {inapplicable, passes, incomplete, violations} = axeReport;
54
+ if (violations) {
55
+ // Initialize the result.
56
+ data.totals = {
57
+ rulesNA: 0,
58
+ rulesPassed: 0,
59
+ rulesWarned: 0,
60
+ rulesViolated: 0,
61
+ warnings: {
62
+ minor: 0,
63
+ moderate: 0,
64
+ serious: 0,
65
+ critical: 0
66
+ },
67
+ violations: {
68
+ minor: 0,
69
+ moderate: 0,
70
+ serious: 0,
71
+ critical: 0
72
+ }
43
73
  };
44
- if (withItems) {
45
- data.items = [];
46
- }
47
- // If there were any violations:
48
- if (axeReport.length) {
49
- // FUNCTION DEFINITIONS START
50
- // Compacts a check violation.
51
- const compactCheck = checkObj => {
52
- return {
53
- check: checkObj.id,
54
- description: checkObj.message,
55
- impact: checkObj.impact
56
- };
57
- };
58
- // Compacts a violating element.
59
- const compactViolator = elObj => {
60
- const out = {
61
- selector: elObj.target[0],
62
- impact: elObj.impact
63
- };
64
- if (elObj.any && elObj.any.length) {
65
- out['must pass any of'] = elObj.any.map(checkObj => compactCheck(checkObj));
66
- }
67
- if (elObj.none && elObj.none.length) {
68
- out['must pass all of'] = elObj.none.map(checkObj => compactCheck(checkObj));
69
- }
70
- return out;
71
- };
72
- // Compacts a violated rule.
73
- const compactRule = rule => {
74
- const out = {
75
- rule: rule.id,
76
- description: rule.description,
77
- impact: rule.impact,
78
- elements: {}
79
- };
80
- if (rule.nodes && rule.nodes.length) {
81
- out.elements = rule.nodes.map(el => compactViolator(el));
82
- }
83
- return out;
84
- };
85
- // FUNCTION DEFINITIONS END
86
- // For each rule violated:
87
- axeReport.forEach(rule => {
88
- // For each element violating the rule:
89
- rule.nodes.forEach(element => {
90
- // Increment the element count of the impact of its violation.
91
- data.violations[element.impact]++;
92
- });
93
- // If details are required:
94
- if (withItems) {
95
- // Add it to the report.
96
- data.items.push(compactRule(rule));
97
- }
74
+ data.details = axeReport;
75
+ // Populate the totals.
76
+ const {totals} = data;
77
+ totals.rulesNA = inapplicable.length;
78
+ totals.rulesPassed = passes.length;
79
+ incomplete.forEach(rule => {
80
+ totals.rulesWarned++;
81
+ rule.nodes.forEach(node => {
82
+ totals.warnings[node.impact]++;
83
+ });
84
+ });
85
+ violations.forEach(rule => {
86
+ totals.rulesViolated++;
87
+ rule.nodes.forEach(node => {
88
+ totals.violations[node.impact]++;
98
89
  });
99
- }
90
+ });
91
+ // Delete irrelevant properties from the report details.
92
+ const irrelevants = ['inapplicable', 'passes', 'incomplete', 'violations']
93
+ .slice(0, 4 - detailLevel);
94
+ irrelevants.forEach(irrelevant => {
95
+ delete axeReport[irrelevant];
96
+ });
100
97
  }
101
98
  // Otherwise, i.e. if the test failed:
102
99
  else {
package/tests/focAll.js CHANGED
@@ -1,17 +1,12 @@
1
1
  /*
2
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.
3
+ This test reports discrepancies between focusable and Tab-focused element counts. The test first
4
+ counts all the visible focusable (i.e. with tabIndex 0) elements (except counting each group of
5
+ radio buttons as only one focusable element). Then it repeatedly presses the Tab (or Option-Tab
6
+ in webkit) key until it has reached all the elements it can and counts those elements. If the
7
+ two counts differ, navigation can be made more difficult. The cause may be surprising changes in
8
+ content during navigation with the Tab key, or inability to reach every focusable element (or
9
+ widget, such as one radio button or tab in each group) merely by pressing the Tab key.
15
10
  */
16
11
  exports.reporter = async page => {
17
12
  // Identify the count of visible focusable elements.
package/tests/focInd.js CHANGED
@@ -5,10 +5,13 @@
5
5
  line thickness and non-transparent color. The test is based on the assumption that outlines are
6
6
  the standard and thus most familiar focus indicator. Other focus indicators are assumed better
7
7
  than none, but more likely to be misunderstood. For example, underlines may be mistaken for
8
- selection indicators. Some pages delay the appearance of focus indicators. This test waits for
9
- focus indicators to appear if specified and, if there is a delay, reports on its magnitude.
8
+ selection indicators. Some pages delay the appearance of focus indicators. If a wait is
9
+ specified, the test checks every 100 ms for an outline until the allow wait time expires,
10
+ and once more after it expires. If no outline appears by then, the test checks for other focus
11
+ indicators. If an outline does not appear immediately but appears on a subsequent check, the test
12
+ reports the amount of the delay.
10
13
 
11
- Bug: This test fails to recognize outlines when run with firefox.
14
+ WARNING: This test fails to recognize outlines when run with firefox.
12
15
  */
13
16
  exports.reporter = async (page, revealAll, allowedDelay, withItems) => {
14
17
  // If required, make all elements visible.
package/tests/motion.js CHANGED
@@ -9,7 +9,8 @@
9
9
  between screen shots (interval), and how many screen shots to make (count). The test compares the
10
10
  screen shots and reports 9 statistics:
11
11
  0. bytes: an array of the sizes of the screen shots, in bytes
12
- 1. localRatios: an array of the ratios of bytes of the larger to the smaller of adjacent pairs of screen shots
12
+ 1. localRatios: an array of the ratios of bytes of the larger to the smaller of adjacent pairs
13
+ of screen shots
13
14
  2. meanLocalRatio: the mean of the ratios in the localRatios array
14
15
  3. maxLocalRatio: the greatest of the ratios in the localRatios array
15
16
  4. globalRatio: the ratio of bytes of the largest to the smallest screen shot
@@ -17,6 +18,10 @@
17
18
  6. meanPixelChange: the mean of the counts in the pixelChanges array
18
19
  7. maxPixelChange: the greatest of the counts in the pixelChanges array
19
20
  8. changeFrequency: what fraction of the adjacent pairs of screen shots has pixel differences
21
+
22
+ WARNING: This test uses the Playwright page.screenshot method, which produces incorrect results
23
+ when the browser type is chromium and is not implemented for the firefox browser type. The only
24
+ browser type usable with this test is webkit.
20
25
  */
21
26
  const pixelmatch = require('pixelmatch');
22
27
  const {PNG} = require('pngjs');
package/tests/tabNav.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  tabNav
3
- This test reports nonstandard keyboard navigation among tab elements in tab lists.
3
+ This test reports nonstandard keyboard navigation among tab elements in visible tab lists.
4
4
  Standards are based on https://www.w3.org/TR/wai-aria-practices-1.1/#tabpanel.
5
5
  */
6
6
 
@@ -73,8 +73,8 @@ exports.reporter = async (page, withItems) => {
73
73
  correct: []
74
74
  };
75
75
  }
76
- // Identify an array of the tablists.
77
- const tabLists = await page.$$('[role=tablist]');
76
+ // Identify an array of the visible tablists.
77
+ const tabLists = await page.$$('[role=tablist]:visible');
78
78
  if (tabLists.length) {
79
79
  // FUNCTION DEFINITIONS START
80
80
  // Returns text associated with an element.
@@ -94,22 +94,34 @@ exports.reporter = async (page, withItems) => {
94
94
  ) => {
95
95
  let pressed = true;
96
96
  // Click the tab element, to make the focus on it effective.
97
- await tabElement.click({timeout: 1500})
97
+ await tabElement.click({
98
+ timeout: 500
99
+ })
100
+ .catch(async error => {
101
+ await tabElement.click({
102
+ force: true
103
+ });
104
+ })
98
105
  .catch(error => {
99
106
  console.log(`ERROR: could not click tab element ${itemData.text} (${error.message})`);
100
- pressed = false;
107
+ pressed = false;
101
108
  });
109
+ // Increment the counts of navigations and key navigations.
110
+ data.totals.navigations.all.total++;
111
+ data.totals.navigations.specific[keyProp].total++;
112
+ const {navigationErrors} = itemData;
113
+ // If the click succeeded:
102
114
  if (pressed) {
103
115
  // Refocus the tab element and press the specified key (page.keyboard.press may fail).
104
- await tabElement.press(keyName)
116
+ await tabElement.press(keyName, {
117
+ timeout: 1000
118
+ })
105
119
  .catch(error => {
106
120
  console.log(`ERROR: could not press ${keyName} (${error.message})`);
107
121
  pressed = false;
108
122
  });
123
+ // If the refocus and keypress succeeded:
109
124
  if (pressed) {
110
- // Increment the counts of navigations and key navigations.
111
- data.totals.navigations.all.total++;
112
- data.totals.navigations.specific[keyProp].total++;
113
125
  // Identify which tab element is now focused, if any.
114
126
  const focusIndex = await focusedTab(tabs);
115
127
  // If the focus is correct:
@@ -128,15 +140,36 @@ exports.reporter = async (page, withItems) => {
128
140
  // If itemization is required:
129
141
  if (withItems) {
130
142
  // Update the element report.
131
- itemData.navigationErrors.push(keyName);
143
+ navigationErrors.push(keyName);
132
144
  }
133
145
  }
134
146
  return elementIsCorrect;
135
147
  }
148
+ // Otherwise, i.e. if the refocus or keypress failed:
136
149
  else {
150
+ // Increment the counts of incorrect navigations and incorrect key navigations.
151
+ data.totals.navigations.all.incorrect++;
152
+ data.totals.navigations.specific[keyProp].incorrect++;
153
+ // If itemization is required and a focus failure has not yet been reported:
154
+ if (withItems && ! navigationErrors.includes('focus')) {
155
+ // Update the element report.
156
+ navigationErrors.push('focus');
157
+ }
137
158
  return false;
138
159
  }
139
160
  }
161
+ // Otherwise, i.e. if the click failed:
162
+ else {
163
+ // Increment the counts of incorrect navigations and incorrect key navigations.
164
+ data.totals.navigations.all.incorrect++;
165
+ data.totals.navigations.specific[keyProp].incorrect++;
166
+ // If itemization is required and a click failure has not yet been reported:
167
+ if (withItems && ! navigationErrors.includes('click')) {
168
+ // Update the element report.
169
+ navigationErrors.push('click');
170
+ }
171
+ return false;
172
+ }
140
173
  };
141
174
  // Returns the index to which an arrow key should move the focus.
142
175
  const arrowTarget = (startIndex, tabCount, orientation, direction) => {