testaro 2.2.0 → 2.2.3

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/README.md CHANGED
@@ -377,6 +377,8 @@ The second item in each array, if there are 3 items in the array, is an operator
377
377
  - `>`: greater than
378
378
  - `!`: unequal to
379
379
 
380
+ A typical use for an `expect` property is checking the correctness of a Testaro test. Thus, the validation scripts in the `validation/tests/scripts` directory all contain `test` commands with `expect` properties. See the “Validation” section below.
381
+
380
382
  ## Batches
381
383
 
382
384
  There are two ways to use a script to give instructions to Testaro:
@@ -447,6 +449,8 @@ The executors are:
447
449
 
448
450
  To execute any executor `xyz.js`, call it with the statement `node validation/executors/xyz`. The results will appear in the standard output.
449
451
 
452
+ The `tests.js` executor makes use of the scripts in the `validation/tests/scripts` directory, and they, in turn, run tests on HTML files in the `validation/tests/targets` directory.
453
+
450
454
  ## Contribution
451
455
 
452
456
  You can define additional Testaro commands and functionality. Contributions are welcome.
package/index.js CHANGED
@@ -1121,7 +1121,7 @@ const doBatch = async (options, reportTemplate, hostIndex = 0) => {
1121
1121
  const host = hosts[hostIndex];
1122
1122
  // If the specified host exists:
1123
1123
  if (host) {
1124
- // Copy the report for it.
1124
+ // Create a report for it.
1125
1125
  const hostReport = JSON.parse(JSON.stringify(reportTemplate));
1126
1126
  // Copy the properties of the specified host to all url acts.
1127
1127
  hostReport.acts.forEach(act => {
@@ -1132,6 +1132,18 @@ const doBatch = async (options, reportTemplate, hostIndex = 0) => {
1132
1132
  });
1133
1133
  // Perform the commands on the host.
1134
1134
  await doScript(options, hostReport);
1135
+ // Add the host’s ID to the host report.
1136
+ hostReport.hostName = host.id;
1137
+ // Add data from the template to the host report.
1138
+ hostReport.orderName = reportTemplate.id;
1139
+ hostReport.id = `${hostReport.orderName}-${host.id}`;
1140
+ hostReport.orderUserName = reportTemplate.userName;
1141
+ hostReport.orderTime = reportTemplate.orderTime;
1142
+ hostReport.scriptName = reportTemplate.scriptName;
1143
+ hostReport.batchName = reportTemplate.batchName;
1144
+ hostReport.scriptIsValid = reportTemplate.scriptIsValid;
1145
+ hostReport.batchIsValid = reportTemplate.batchIsValid;
1146
+ hostReport.host = host;
1135
1147
  // Process the remaining hosts.
1136
1148
  await doBatch(options, reportTemplate, hostIndex + 1);
1137
1149
  }
@@ -1210,6 +1222,7 @@ exports.handleRequest = async options => {
1210
1222
  }
1211
1223
  // Create a report template, containing a copy of the commands as its acts.
1212
1224
  const reportTemplate = {
1225
+ host: '',
1213
1226
  acts: JSON.parse(JSON.stringify(options.script.commands))
1214
1227
  };
1215
1228
  // Inject url acts where necessary to undo DOM changes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "2.2.0",
3
+ "version": "2.2.3",
4
4
  "description": "Automation of accessibility testing",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/tests/hover.js CHANGED
@@ -8,188 +8,226 @@
8
8
  examined are the descendants of the grandparent of the element hovered over if that element
9
9
  has the tag name 'A' or 'BUTTON' or otherwise the descendants of the element. The only
10
10
  elements counted as being made visible by hovering are those with tag names 'A', 'BUTTON',
11
- 'INPUT', and 'SPAN', and those with 'role="menuitem"' attributes.
11
+ 'INPUT', and 'SPAN', and those with 'role="menuitem"' attributes. The test waits 700 ms after
12
+ each hover in case of delayed effects. Despite this delay, the test makes the execution time
13
+ practical by randomly sampling targets instead of hovering over all of them. Therefore, the
14
+ results may vary from one execution to another.
12
15
  */
13
- exports.reporter = async (page, withItems) => {
14
- // Initialize a counter.
15
- let elementsChecked = 0;
16
- // Identify the elements that are likely to trigger disclosures on hover.
17
- const triggers = await page.$$(
18
- 'body a:visible, body button:visible, body li:visible, body [onmouseenter]:visible, body [onmouseover]:visible'
19
- )
20
- .catch(error => {
21
- console.log(`ERROR getting hover triggers (${error.message})`);
22
- return [];
23
- });
24
- // Identify the selectors of active elements likely to be disclosed by a hover.
25
- const targetSelectors = ['a', 'button', 'input', '[role=menuitem]', 'span']
26
- .map(selector => `${selector}:visible`)
27
- .join(', ');
28
- // Initialize the result.
29
- const data = {
30
- totals: {
31
- triggers: 0,
32
- madeVisible: 0,
33
- opacityChanged: 0,
34
- opacityAffected: 0,
35
- unhoverables: 0
16
+
17
+ // CONSTANTS
18
+
19
+ // Selectors of active elements likely to be disclosed by a hover.
20
+ const targetSelectors = ['a', 'button', 'input', '[role=menuitem]', 'span']
21
+ .map(selector => `${selector}:visible`)
22
+ .join(', ');
23
+ // Initialize the result.
24
+ const data = {
25
+ totals: {
26
+ triggers: 0,
27
+ madeVisible: 0,
28
+ opacityChanged: 0,
29
+ opacityAffected: 0,
30
+ unhoverables: 0
31
+ }
32
+ };
33
+
34
+ // VARIABLES
35
+
36
+ // Counter.
37
+ let elementsChecked = 0;
38
+
39
+ // FUNCTIONS
40
+
41
+ // Samples a population and returns the sample.
42
+ const getSample = (population, sampleSize) => {
43
+ const popSize = population.length;
44
+ if (sampleSize > 0 && sampleSize < popSize) {
45
+ const sample = new Set();
46
+ while (sample.size < sampleSize) {
47
+ const index = Math.floor(popSize * Math.random());
48
+ sample.add(population[index]);
36
49
  }
37
- };
50
+ return Array.from(sample);
51
+ }
52
+ else {
53
+ return [];
54
+ }
55
+ };
56
+ // Recursively finds and reports triggers and targets.
57
+ const find = async (withItems, page, triggers) => {
38
58
  if (withItems) {
39
59
  data.items = {
40
60
  triggers: [],
41
61
  unhoverables: []
42
62
  };
43
63
  }
44
- let triggerTag = '';
45
- // FUNCTION DEFINITION START
46
- // Recursively finds and reports triggers and targets.
47
- const find = async triggers => {
48
- // If any potential disclosure triggers remain:
49
- if (triggers.length) {
50
- // Identify the first of them.
51
- const firstTrigger = triggers[0];
52
- const tagNameJSHandle = await firstTrigger.getProperty('tagName')
53
- .catch(error => {
54
- console.log(`ERROR getting trigger tag name (${error.message})`);
55
- return '';
56
- });
57
- if (tagNameJSHandle) {
58
- const tagName = await tagNameJSHandle.jsonValue();
59
- // Identify the root of a subtree likely to contain disclosed elements.
60
- let root = firstTrigger;
61
- if (['A', 'BUTTON'].includes(tagName)) {
62
- const rootJSHandle = await page.evaluateHandle(
63
- firstTrigger => {
64
- const parent = firstTrigger.parentElement;
65
- if (parent) {
66
- return parent.parentElement || parent;
67
- }
68
- else {
69
- return firstTrigger;
70
- }
71
- },
72
- firstTrigger
73
- );
74
- root = rootJSHandle.asElement();
75
- }
76
- // Identify the visible active descendants of the root before the hover.
77
- const preVisibles = await root.$$(targetSelectors);
78
- // Identify all the descendants of the root.
79
- const descendants = await root.$$('*');
80
- // Identify their opacities before the hover.
81
- const preOpacities = await page.evaluate(
82
- elements => elements.map(el => window.getComputedStyle(el).opacity), descendants
83
- );
84
- try {
85
- // Hover over the potential trigger.
86
- await firstTrigger.hover({timeout: 700});
87
- // Identify whether it controls other elements.
88
- const isController = await page.evaluate(
89
- element => element.ariaHasPopup || element.hasAttribute('aria-controls'), firstTrigger
90
- );
91
- // Wait for any delayed and/or slowed hover reaction if likely.
92
- await page.waitForTimeout(
93
- elementsChecked++ < 10 || tagName !== triggerTag || isController ? 1200 : 200
94
- );
95
- await root.waitForElementState('stable');
96
- // Identify the visible active descendants of the root during the hover.
97
- const postVisibles = await root.$$(targetSelectors);
98
- // Identify the opacities of the descendants of the root during the hover.
99
- const postOpacities = await page.evaluate(
100
- elements => elements.map(el => window.getComputedStyle(el).opacity), descendants
101
- );
102
- // Identify the elements with opacity changes.
103
- const opacityTargets = descendants
104
- .filter((descendant, index) => postOpacities[index] !== preOpacities[index]);
105
- // Count them and their descendants.
106
- const opacityAffected = opacityTargets.length
107
- ? await page.evaluate(elements => elements.reduce(
108
- (total, current) => total + 1 + current.querySelectorAll('*').length, 0
109
- ), opacityTargets)
110
- : 0;
111
- // If hovering disclosed any element or changed any opacity:
112
- if (postVisibles.length > preVisibles.length || opacityAffected) {
113
- // Preserve the lengthened reaction wait, if any, for the next 5 tries.
114
- if (elementsChecked < 11) {
115
- elementsChecked = 5;
64
+ // If any potential disclosure triggers remain:
65
+ if (triggers.length) {
66
+ // Identify the first of them.
67
+ const firstTrigger = triggers[0];
68
+ const tagNameJSHandle = await firstTrigger.getProperty('tagName')
69
+ .catch(error => {
70
+ console.log(`ERROR getting trigger tag name (${error.message})`);
71
+ return '';
72
+ });
73
+ if (tagNameJSHandle) {
74
+ const tagName = await tagNameJSHandle.jsonValue();
75
+ // Identify the root of a subtree likely to contain disclosed elements.
76
+ let root = firstTrigger;
77
+ if (['A', 'BUTTON'].includes(tagName)) {
78
+ const rootJSHandle = await page.evaluateHandle(
79
+ firstTrigger => {
80
+ const parent = firstTrigger.parentElement;
81
+ if (parent) {
82
+ return parent.parentElement || parent;
116
83
  }
117
- // Hover over the upper-left corner of the page, to undo any hover reactions.
118
- await page.hover('body', {
119
- position: {
120
- x: 0,
121
- y: 0
122
- }
123
- });
124
- // Wait for any delayed and/or slowed hover reaction.
125
- await page.waitForTimeout(200);
126
- await root.waitForElementState('stable');
127
- // Increment the counts of triggers and targets.
128
- data.totals.triggers++;
129
- const madeVisible = Math.max(0, postVisibles.length - preVisibles.length);
130
- data.totals.madeVisible += madeVisible;
131
- data.totals.opacityChanged += opacityTargets.length;
132
- data.totals.opacityAffected += opacityAffected;
133
- // If details are to be reported:
134
- if (withItems) {
135
- // Report them.
136
- const triggerDataJSHandle = await page.evaluateHandle(args => {
137
- // Returns the text of an element.
138
- const textOf = (element, limit) => {
139
- const text = element.textContent.trim() || element.outerHTML.trim();
140
- return text.replace(/\s{2,}/sg, ' ').slice(0, limit);
141
- };
142
- const trigger = args[0];
143
- const preVisibles = args[1];
144
- const postVisibles = args[2];
145
- const madeVisible = postVisibles
146
- .filter(el => ! preVisibles.includes(el))
147
- .map(el => ({
148
- tagName: el.tagName,
149
- text: textOf(el, 50)
150
- }));
151
- const opacityChanged = args[3].map(el => ({
152
- tagName: el.tagName,
153
- text: textOf(el, 50)
154
- }));
155
- return {
156
- tagName: trigger.tagName,
157
- id: trigger.id || '',
158
- text: textOf(trigger, 50),
159
- madeVisible,
160
- opacityChanged
161
- };
162
- }, [firstTrigger, preVisibles, postVisibles, opacityTargets]);
163
- const triggerData = await triggerDataJSHandle.jsonValue();
164
- data.items.triggers.push(triggerData);
84
+ else {
85
+ return firstTrigger;
165
86
  }
87
+ },
88
+ firstTrigger
89
+ );
90
+ root = rootJSHandle.asElement();
91
+ }
92
+ // Identify the visible active descendants of the root before the hover.
93
+ const preVisibles = await root.$$(targetSelectors);
94
+ // Identify all the descendants of the root.
95
+ const descendants = await root.$$('*');
96
+ // Identify their opacities before the hover.
97
+ const preOpacities = await page.evaluate(
98
+ elements => elements.map(el => window.getComputedStyle(el).opacity), descendants
99
+ );
100
+ try {
101
+ // Hover over the potential trigger.
102
+ await firstTrigger.hover({timeout: 700});
103
+ // Identify whether it is coded as controlling other elements.
104
+ const isController = await page.evaluate(
105
+ element => element.ariaHasPopup || element.hasAttribute('aria-controls'), firstTrigger
106
+ );
107
+ // Wait for any delayed and/or slowed hover reaction, longer if coded as a controller.
108
+ await page.waitForTimeout(isController ? 1200 : 600);
109
+ await root.waitForElementState('stable');
110
+ // Identify the visible active descendants of the root during the hover.
111
+ const postVisibles = await root.$$(targetSelectors);
112
+ // Identify the opacities of the descendants of the root during the hover.
113
+ const postOpacities = await page.evaluate(
114
+ elements => elements.map(el => window.getComputedStyle(el).opacity), descendants
115
+ );
116
+ // Identify the elements with opacity changes.
117
+ const opacityTargets = descendants
118
+ .filter((descendant, index) => postOpacities[index] !== preOpacities[index]);
119
+ // Count them and their descendants.
120
+ const opacityAffected = opacityTargets.length
121
+ ? await page.evaluate(elements => elements.reduce(
122
+ (total, current) => total + 1 + current.querySelectorAll('*').length, 0
123
+ ), opacityTargets)
124
+ : 0;
125
+ // If hovering disclosed any element or changed any opacity:
126
+ if (postVisibles.length > preVisibles.length || opacityAffected) {
127
+ // Preserve the lengthened reaction wait, if any, for the next 5 tries.
128
+ if (elementsChecked < 11) {
129
+ elementsChecked = 5;
166
130
  }
167
- }
168
- catch (error) {
169
- console.log('ERROR hovering');
170
- // Returns the text of an element.
171
- const textOf = async (element, limit) => {
172
- let text = await element.textContent();
173
- text = text.trim() || await element.innerHTML();
174
- return text.trim().replace(/\s*/sg, '').slice(0, limit);
175
- };
176
- data.totals.unhoverables++;
131
+ // Hover over the upper-left corner of the page, to undo any hover reactions.
132
+ await page.hover('body', {
133
+ position: {
134
+ x: 0,
135
+ y: 0
136
+ }
137
+ });
138
+ // Wait for any delayed and/or slowed hover reaction.
139
+ await page.waitForTimeout(200);
140
+ await root.waitForElementState('stable');
141
+ // Increment the counts of triggers and targets.
142
+ data.totals.triggers++;
143
+ const madeVisible = Math.max(0, postVisibles.length - preVisibles.length);
144
+ data.totals.madeVisible += madeVisible;
145
+ data.totals.opacityChanged += opacityTargets.length;
146
+ data.totals.opacityAffected += opacityAffected;
147
+ // If details are to be reported:
177
148
  if (withItems) {
178
- data.items.unhoverables.push({
179
- tagName: tagName,
180
- id: firstTrigger.id || '',
181
- text: await textOf(firstTrigger, 50)
182
- });
149
+ // Report them.
150
+ const triggerDataJSHandle = await page.evaluateHandle(args => {
151
+ // Returns the text of an element.
152
+ const textOf = (element, limit) => {
153
+ const text = element.textContent.trim() || element.outerHTML.trim();
154
+ return text.replace(/\s{2,}/sg, ' ').slice(0, limit);
155
+ };
156
+ const trigger = args[0];
157
+ const preVisibles = args[1];
158
+ const postVisibles = args[2];
159
+ const madeVisible = postVisibles
160
+ .filter(el => ! preVisibles.includes(el))
161
+ .map(el => ({
162
+ tagName: el.tagName,
163
+ text: textOf(el, 50)
164
+ }));
165
+ const opacityChanged = args[3].map(el => ({
166
+ tagName: el.tagName,
167
+ text: textOf(el, 50)
168
+ }));
169
+ return {
170
+ tagName: trigger.tagName,
171
+ id: trigger.id || '',
172
+ text: textOf(trigger, 50),
173
+ madeVisible,
174
+ opacityChanged
175
+ };
176
+ }, [firstTrigger, preVisibles, postVisibles, opacityTargets]);
177
+ const triggerData = await triggerDataJSHandle.jsonValue();
178
+ data.items.triggers.push(triggerData);
183
179
  }
184
180
  }
185
- triggerTag = tagName;
186
181
  }
187
- // Process the remaining potential triggers.
188
- await find(triggers.slice(1));
182
+ catch (error) {
183
+ console.log('ERROR hovering');
184
+ // Returns the text of an element.
185
+ const textOf = async (element, limit) => {
186
+ let text = await element.textContent();
187
+ text = text.trim() || await element.innerHTML();
188
+ return text.trim().replace(/\s*/sg, '').slice(0, limit);
189
+ };
190
+ data.totals.unhoverables++;
191
+ if (withItems) {
192
+ data.items.unhoverables.push({
193
+ tagName: tagName,
194
+ id: firstTrigger.id || '',
195
+ text: await textOf(firstTrigger, 50)
196
+ });
197
+ }
198
+ }
189
199
  }
190
- };
200
+ // Process the remaining potential triggers.
201
+ await find(withItems, page, triggers.slice(1));
202
+ }
203
+ };
204
+ exports.reporter = async (page, withItems) => {
205
+ // Identify the triggers.
206
+ const selectors = [
207
+ 'body a:visible',
208
+ 'body button:visible',
209
+ 'body li:visible, body [onmouseenter]:visible',
210
+ 'body [onmouseover]:visible'
211
+ ];
212
+ const triggers = await page.$$(selectors.join(', '))
213
+ .catch(error => {
214
+ console.log(`ERROR getting hover triggers (${error.message})`);
215
+ return [];
216
+ });
217
+ // If they number more than the sample size limit, sample them.
218
+ const triggerCount = triggers.length;
219
+ const sampleSize = 15;
220
+ const triggerSample = triggerCount > sampleSize ? getSample(triggers, 15) : triggers;
191
221
  // Find and document the hover-triggered disclosures.
192
- await find(triggers);
222
+ await find(withItems, page, triggerSample);
223
+ // If the triggers were sampled:
224
+ if (triggerCount > sampleSize) {
225
+ // Change the totals to population estimates.
226
+ const multiplier = triggerCount / sampleSize;
227
+ Object.keys(data.totals).forEach(key => {
228
+ data.totals[key] = Math.round(multiplier * data.totals[key]);
229
+ });
230
+ }
193
231
  // Return the result.
194
232
  return {result: data};
195
233
  };
@@ -4,6 +4,10 @@
4
4
  const fs = require('fs').promises;
5
5
  const {handleRequest} = require(`${__dirname}/../../index`);
6
6
  const validateTests = async () => {
7
+ const totals = {
8
+ attempts: 0,
9
+ successes: 0
10
+ };
7
11
  const scriptFileNames = await fs.readdir(`${__dirname}/../tests/scripts`);
8
12
  for (const scriptFileName of scriptFileNames) {
9
13
  const rawScriptJSON = await fs
@@ -33,21 +37,28 @@ const validateTests = async () => {
33
37
  : true
34
38
  )
35
39
  ) {
40
+ totals.attempts++;
41
+ totals.successes++;
36
42
  console.log('Success: Reports have been correctly populated');
37
43
  if (reports[0].acts.every(
38
44
  act => act.type === 'test' ? act.result.failureCount === 0 : true
39
45
  )) {
46
+ totals.attempts++;
47
+ totals.successes++;
40
48
  console.log('Success: No failures');
41
49
  }
42
50
  else {
51
+ totals.attempts++;
43
52
  console.log('Failure: At least one test has at least one failure');
44
53
  console.log(JSON.stringify(reports, null, 2));
45
54
  }
46
55
  }
47
56
  else {
57
+ totals.attempts++;
48
58
  console.log('Failure: Reports empty or invalid');
49
59
  console.log(JSON.stringify(reports, null, 2));
50
60
  }
51
61
  }
62
+ console.log(`Grand totals: attempts ${totals.attempts}, successes ${totals.successes}`);
52
63
  };
53
64
  validateTests();