testaro 4.13.0 → 4.15.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
@@ -180,7 +180,11 @@ exports.commands = {
180
180
  hover: [
181
181
  'Perform a hover test',
182
182
  {
183
- sampleSize: [false, 'number', '', 'number of triggers to sample, if fewer than all'],
183
+ headSize: [false, 'number', '', 'count of first triggers to sample separately, if any'],
184
+ headSampleSize: [false, 'number', '', 'size of the head sample to be drawn, if any'],
185
+ tailSampleSize: [
186
+ false, 'number', '', 'size of the non-head sample to be drawn, if not all'
187
+ ],
184
188
  withItems: [true, 'boolean']
185
189
  }
186
190
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "4.13.0",
3
+ "version": "4.15.0",
4
4
  "description": "Automation of accessibility testing",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/run.js CHANGED
@@ -289,9 +289,12 @@ const launch = async typeName => {
289
289
  browserContext = await browser.newContext(viewport);
290
290
  // When a page is added to the browser context:
291
291
  browserContext.on('page', page => {
292
- // Make its console messages get reported and appear in the Playwright console.
292
+ // Make abbreviations of its console messages get reported in the Playwright console.
293
293
  page.on('console', msg => {
294
- const msgText = msg.text();
294
+ let msgText = msg.text();
295
+ if (msgText.length > 300) {
296
+ msgText = `${msgText.slice(0, 150)} ... ${msgText.slice(-150)}`;
297
+ }
295
298
  console.log(`[${msgText}]`);
296
299
  const msgTextLC = msgText.toLowerCase();
297
300
  const msgLength = msgText.length;
package/tests/aatt.js CHANGED
@@ -95,7 +95,7 @@ exports.reporter = async (page, waitLong, tryLimit = 4) => {
95
95
  warnings
96
96
  },
97
97
  report: nonNotices,
98
- preventionCount: tryLimit - triesLeft - 1
98
+ preventionCount: tryLimit - triesLeft
99
99
  }
100
100
  };
101
101
  }
package/tests/hover.js CHANGED
@@ -1,63 +1,71 @@
1
1
  /*
2
2
  hover
3
- This test reports unexpected effects of hovering. The effects include elements that are made
4
- visible, elements whose opacities are changed, elements with ancestors whose opacities are
5
- changed, and elements that cannot be hovered over. Only Playwright-visible elements in the
6
- DOM that have 'A', 'BUTTON', and 'LI' tag names or have 'onmouseenter' or 'onmouseover' attributes
7
- are considered as hovering targets. The elements considered when the effects of hovering are
8
- examined are the descendants of the grandparent of the element hovered over if that element
9
- has the tag name 'A' or 'BUTTON' or otherwise the descendants of the element. The only
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. The test waits 700 ms after
12
- each hover in case of delayed effects. Despite this delay, the test can make the execution
13
- time practical by randomly sampling targets instead of hovering over all of them. When
14
- sampling is performed, the results may vary from one execution to another. An element is
15
- reported as unhoverable when it fails the Playwright actionability checks for hovering, i.e.
16
- when it fails to be attached to the DOM, visible, stable (not or no longer animating), and
17
- able to receive events. All target candidates satisfy the first two conditions, so only the
18
- last two might fail. Playwright defines the ability to receive events as being the target of
19
- an action on the location where the center of the element is, rather than some other element
20
- with a higher zIndex value in the same location being the target.
3
+ This test reports unexpected impacts of hovering. The effects include additions and removals
4
+ of visible elements, opacity changes, and unhoverable elements. The elements that are
5
+ subjected to hovering (called “triggers”) are the Playwright-visible elements that have 'A',
6
+ 'BUTTON', or 'LI' tag names or have 'onmouseenter' or 'onmouseover' attributes. When such an
7
+ element is hovered over, the test examines the impacts on descendants of the great grandparents
8
+ of the elements with tag names 'A' and 'BUTTON', grandparents of elements with tag name 'LI',
9
+ and otherwise the descendants of the elements themselves. Four impacts are counted: (1) an
10
+ element is added or becomes visible, (2) an element is removed or becomes invisible, (3) the
11
+ opacity of an element changes, and (4) the element is a descendant of an element whose opacity
12
+ changes. The test checks up to 4 times for hovering impacts at intervals of 0.3 second.
13
+
14
+ Despite this delay, the test can make the execution time practical by randomly sampling triggers
15
+ instead of hovering over all of them. When sampling is performed, the results may vary from one
16
+ execution to another. Because hover impacts typically occur near the beginning of a page,
17
+ sampling is governed by three optional parameters (defaults in parentheses):
18
+ headSize (0): the size of an initial subset of triggers (“head”)
19
+ headSampleSize (-1): the size of the sample to be drawn from the head
20
+ tailSampleSize (-1): the size of the sample to be drawn from the remainder of the page
21
+ A sample size of -1 means that there is no sampling, and the entire population is tested.
22
+
23
+ An element is reported as unhoverable when it fails the Playwright actionability checks for
24
+ hovering, i.e. fails to be attached to the DOM, visible, stable (not or no longer animating), and
25
+ able to receive events. All triggers satisfy the first two conditions, so only the last two might
26
+ fail. Playwright defines the ability to receive events as being the target of an action on the
27
+ location where the center of the element is, rather than some other element with a higher zIndex
28
+ value in the same location being the target.
21
29
  */
22
30
 
23
31
  // CONSTANTS
24
32
 
25
- // Selectors of active elements likely to be disclosed by a hover.
26
- const targetSelectors = ['a', 'button', 'input', '[role=menuitem]', 'span']
27
- .map(selector => `${selector}:visible`)
28
- .join(', ');
29
33
  // Initialize the result.
30
34
  const data = {
31
- populationSize: 0,
32
35
  totals: {
33
36
  triggers: 0,
34
- madeVisible: 0,
35
- opacityChanged: 0,
36
- opacityAffected: 0,
37
+ headTriggers: 0,
38
+ tailTriggers: 0,
39
+ impactTriggers: 0,
40
+ additions: 0,
41
+ removals: 0,
42
+ opacityChanges: 0,
43
+ opacityImpact: 0,
37
44
  unhoverables: 0
38
45
  }
39
46
  };
40
47
 
41
- // VARIABLES
42
-
43
- // Counter.
44
- let elementsChecked = 0;
45
-
46
48
  // FUNCTIONS
47
49
 
48
50
  // Samples a population and returns the sample.
49
51
  const getSample = (population, sampleSize) => {
50
52
  const popSize = population.length;
51
- if (sampleSize > 0 && sampleSize < popSize) {
52
- const sample = new Set();
53
- while (sample.size < sampleSize) {
54
- const index = Math.floor(popSize * Math.random());
55
- sample.add(population[index]);
56
- }
57
- return Array.from(sample);
53
+ if (sampleSize === 0) {
54
+ return [];
55
+ }
56
+ else if (sampleSize > 0 && sampleSize < popSize) {
57
+ const popData = [];
58
+ for (const trigger of population) {
59
+ popData.push({
60
+ trigger,
61
+ sorter: Math.random()
62
+ });
63
+ };
64
+ popData.sort((a, b) => a.sorter - b.sorter);
65
+ return popData.slice(0, sampleSize).map(obj => obj.trigger);
58
66
  }
59
67
  else {
60
- return [];
68
+ return population;
61
69
  }
62
70
  };
63
71
  // Returns the text of an element.
@@ -66,12 +74,12 @@ const textOf = async (element, limit) => {
66
74
  text = text.trim() || await element.innerHTML();
67
75
  return text.trim().replace(/\s*/sg, '').slice(0, limit);
68
76
  };
69
- // Recursively finds and reports triggers and targets.
70
- const find = async (withItems, page, triggers) => {
71
- // If any potential disclosure triggers remain:
72
- if (triggers.length) {
77
+ // Recursively reports impacts of hovering over triggers.
78
+ const find = async (withItems, page, region, sample, popRatio) => {
79
+ // If any potential triggers remain:
80
+ if (sample.length) {
73
81
  // Identify the first of them.
74
- const firstTrigger = triggers[0];
82
+ const firstTrigger = sample[0];
75
83
  const tagNameJSHandle = await firstTrigger.getProperty('tagName')
76
84
  .catch(error => {
77
85
  console.log(`ERROR getting trigger tag name (${error.message})`);
@@ -79,59 +87,86 @@ const find = async (withItems, page, triggers) => {
79
87
  });
80
88
  if (tagNameJSHandle) {
81
89
  const tagName = await tagNameJSHandle.jsonValue();
82
- // Identify the root of a subtree likely to contain disclosed elements.
90
+ // Identify the root of a subtree likely to contain impacted elements.
83
91
  let root = firstTrigger;
84
- if (['A', 'BUTTON'].includes(tagName)) {
92
+ if (['A', 'BUTTON', 'LI'].includes(tagName)) {
85
93
  const rootJSHandle = await page.evaluateHandle(
86
94
  firstTrigger => {
87
95
  const parent = firstTrigger.parentElement || firstTrigger;
88
96
  const grandparent = parent.parentElement || parent;
89
- return grandparent;
97
+ const greatGrandparent = grandparent.parentElement || parent;
98
+ return firstTrigger.tagName === 'LI' ? grandparent : greatGrandparent;
90
99
  },
91
100
  firstTrigger
92
101
  );
93
102
  root = rootJSHandle.asElement();
94
103
  }
95
- // Identify the visible active descendants of the root before the hover.
96
- const preVisibles = await root.$$(targetSelectors);
97
104
  // Identify all the descendants of the root.
98
- const descendants = await root.$$('*');
99
- // Identify their opacities before the hover.
100
- const preOpacities = await page.evaluate(
101
- elements => elements.map(el => window.getComputedStyle(el).opacity), descendants
102
- );
105
+ const preDescendants = await root.$$('*');
106
+ // Identify their opacities.
107
+ const preOpacities = await page.evaluate(elements => elements.map(
108
+ element => window.getComputedStyle(element).opacity
109
+ ), preDescendants);
103
110
  try {
104
- // Hover over the potential trigger.
105
- await firstTrigger.hover({timeout: 700});
106
- // Identify whether it is coded as controlling other elements.
107
- const isController = await page.evaluate(
108
- element => element.ariaHasPopup || element.hasAttribute('aria-controls'), firstTrigger
109
- );
110
- // Wait for any delayed and/or slowed hover reaction, longer if coded as a controller.
111
- await page.waitForTimeout(isController ? 1200 : 600);
112
- await root.waitForElementState('stable');
113
- // Identify the visible active descendants of the root during the hover.
114
- const postVisibles = await root.$$(targetSelectors);
115
- // Identify the opacities of the descendants of the root during the hover.
116
- const postOpacities = await page.evaluate(
117
- elements => elements.map(el => window.getComputedStyle(el).opacity), descendants
118
- );
119
- // Identify the elements with opacity changes.
120
- const opacityTargets = descendants
121
- .filter((descendant, index) => postOpacities[index] !== preOpacities[index]);
122
- // Count them and their descendants.
123
- const opacityAffected = opacityTargets.length
124
- ? await page.evaluate(elements => elements.reduce(
125
- (total, current) => total + 1 + current.querySelectorAll('*').length, 0
126
- ), opacityTargets)
127
- : 0;
128
- // If hovering disclosed any element or changed any opacity:
129
- if (postVisibles.length > preVisibles.length || opacityAffected) {
130
- // Preserve the lengthened reaction wait, if any, for the next 5 tries.
131
- if (elementsChecked < 11) {
132
- elementsChecked = 5;
111
+ // Hover over the trigger.
112
+ await firstTrigger.hover({
113
+ timeout: 500,
114
+ noWaitAfter: true
115
+ });
116
+ // Repeatedly seeks impacts.
117
+ const getImpacts = async (interval, triesLeft) => {
118
+ if (triesLeft--) {
119
+ const postDescendants = await root.$$('*');
120
+ const remainerIndexes = await page.evaluate(args => {
121
+ const preDescendants = args[0];
122
+ const postDescendants = args[1];
123
+ const remainerIndexes = preDescendants
124
+ .map((element, index) => postDescendants.includes(element) ? index : -1)
125
+ .filter(index => index > -1);
126
+ return remainerIndexes;
127
+ }, [preDescendants, postDescendants]);
128
+ const additionCount = postDescendants.length - remainerIndexes.length;
129
+ const removalCount = preDescendants.length - remainerIndexes.length;
130
+ const remainers = [];
131
+ for (const index of remainerIndexes) {
132
+ remainers.push({
133
+ element: preDescendants[index],
134
+ preOpacity: preOpacities[index],
135
+ postOpacity: await page.evaluate(
136
+ element => window.getComputedStyle(element).opacity, preDescendants[index]
137
+ )
138
+ });
139
+ }
140
+ const opacityChangers = remainers
141
+ .filter(remainer => remainer.postOpacity !== remainer.preOpacity);
142
+ const opacityImpact = opacityChangers ? await page.evaluate(changers => changers.reduce(
143
+ (total, current) => total + current.element.querySelectorAll('*').length, 0
144
+ ), opacityChangers) : 0;
145
+ if (additionCount || removalCount || opacityChangers.length) {
146
+ return {
147
+ additionCount,
148
+ removalCount,
149
+ opacityChangers,
150
+ opacityImpact
151
+ };
152
+ }
153
+ else {
154
+ return await new Promise(resolve => {
155
+ setTimeout(() => {
156
+ resolve(getImpacts(interval, triesLeft));
157
+ }, interval);
158
+ });
159
+ }
160
+ }
161
+ else {
162
+ return null;
133
163
  }
134
- // Hover over the upper-left corner of the page, to undo any hover reactions.
164
+ };
165
+ // Repeatedly seek impacts of the hover at intervals.
166
+ const impacts = await getImpacts(300, 4);
167
+ // If there were any:
168
+ if (impacts) {
169
+ // Hover over the upper-left corner of the page, to undo any impacts.
135
170
  await page.hover('body', {
136
171
  position: {
137
172
  x: 0,
@@ -141,100 +176,92 @@ const find = async (withItems, page, triggers) => {
141
176
  // Wait for any delayed and/or slowed hover reaction.
142
177
  await page.waitForTimeout(200);
143
178
  await root.waitForElementState('stable');
144
- // Increment the counts of triggers and targets.
145
- data.totals.triggers++;
146
- const madeVisible = Math.max(0, postVisibles.length - preVisibles.length);
147
- data.totals.madeVisible += madeVisible;
148
- data.totals.opacityChanged += opacityTargets.length;
149
- data.totals.opacityAffected += opacityAffected;
179
+ // Increment the counts of triggers and impacts.
180
+ const {additionCount, removalCount, opacityChangers, opacityImpact} = impacts;
181
+ data.totals.impactTriggers += popRatio;
182
+ data.totals.additions += popRatio * additionCount;
183
+ data.totals.removals += popRatio * removalCount;
184
+ data.totals.opacityChanges += popRatio * opacityChangers.length;
185
+ data.totals.opacityImpact += popRatio * opacityImpact;
150
186
  // If details are to be reported:
151
187
  if (withItems) {
152
188
  // Report them.
153
- const triggerDataJSHandle = await page.evaluateHandle(args => {
154
- // Returns the text of an element.
155
- const textOf = (element, limit) => {
156
- const text = element.textContent.trim() || element.outerHTML.trim();
157
- return text.replace(/\s{2,}/sg, ' ').slice(0, limit);
158
- };
159
- const trigger = args[0];
160
- const preVisibles = args[1];
161
- const postVisibles = args[2];
162
- const madeVisible = postVisibles
163
- .filter(el => ! preVisibles.includes(el))
164
- .map(el => ({
165
- tagName: el.tagName,
166
- text: textOf(el, 50)
167
- }));
168
- const opacityChanged = args[3].map(el => ({
169
- tagName: el.tagName,
170
- text: textOf(el, 50)
171
- }));
172
- return {
173
- tagName: trigger.tagName,
174
- id: trigger.id || '',
175
- text: textOf(trigger, 50),
176
- madeVisible,
177
- opacityChanged
178
- };
179
- }, [firstTrigger, preVisibles, postVisibles, opacityTargets]);
180
- const triggerData = await triggerDataJSHandle.jsonValue();
181
- data.items.triggers.push(triggerData);
189
+ data.items[region].impactTriggers.push({
190
+ tagName,
191
+ text: await textOf(firstTrigger, 50),
192
+ additions: additionCount,
193
+ removals: removalCount,
194
+ opacityChanges: opacityChangers.length,
195
+ opacityImpact
196
+ });
182
197
  }
183
198
  }
184
199
  }
185
200
  catch (error) {
186
- console.log('ERROR hovering');
201
+ console.log(`ERROR hovering (${error.message})`);
187
202
  data.totals.unhoverables++;
188
203
  if (withItems) {
189
- data.items.unhoverables.push({
190
- tagName: tagName,
191
- id: firstTrigger.id || '',
204
+ const id = await firstTrigger.getAttribute('id');
205
+ data.items[region].unhoverables.push({
206
+ tagName,
207
+ id: id || '',
192
208
  text: await textOf(firstTrigger, 50)
193
209
  });
194
210
  }
195
211
  }
196
212
  }
197
213
  // Process the remaining potential triggers.
198
- await find(withItems, page, triggers.slice(1));
214
+ await find(withItems, page, region, sample.slice(1), popRatio);
199
215
  }
200
216
  };
201
- // Performs hover test and reports results.
202
- exports.reporter = async (page, sampleSize = Infinity, withItems) => {
217
+ // Performs the hover test and reports results.
218
+ exports.reporter = async (
219
+ page, headSize = 0, headSampleSize = -1, tailSampleSize = -1, withItems
220
+ ) => {
203
221
  // If details are to be reported:
204
222
  if (withItems) {
205
223
  // Add properties for details to the initialized result.
206
224
  data.items = {
207
- triggers: [],
208
- unhoverables: []
225
+ head: {
226
+ impactTriggers: [],
227
+ unhoverables: []
228
+ },
229
+ tail: {
230
+ impactTriggers: [],
231
+ unhoverables: []
232
+ }
209
233
  };
210
234
  }
211
235
  // Identify the triggers.
212
- const selectors = [
213
- 'body a:visible',
214
- 'body button:visible',
215
- 'body li:visible, body [onmouseenter]:visible',
216
- 'body [onmouseover]:visible'
217
- ];
218
- const triggers = await page.$$(selectors.join(', '))
236
+ const selectors = ['a', 'button', 'li', '[onmouseenter]', '[onmouseover]'];
237
+ const triggers = await page.$$(selectors.map(selector => `body ${selector}:visible`).join(', '))
219
238
  .catch(error => {
220
239
  console.log(`ERROR getting hover triggers (${error.message})`);
221
240
  data.prevented = true;
222
241
  return [];
223
242
  });
224
- // If they number more than the sample size limit, sample them.
225
- const triggerCount = triggers.length;
226
- data.populationSize = triggerCount;
227
- const triggerSample = triggerCount > sampleSize ? getSample(triggers, sampleSize) : triggers;
228
- // Find and document the hover-triggered disclosures.
229
- await find(withItems, page, triggerSample);
230
- // If the triggers were sampled:
231
- if (triggerCount > sampleSize) {
232
- // Change the totals to population estimates.
233
- const multiplier = triggerCount / sampleSize;
234
- Object.keys(data.totals).forEach(key => {
235
- data.totals[key] = Math.round(multiplier * data.totals[key]);
236
- });
243
+ // Classify them into head and tail triggers.
244
+ const headTriggers = triggers.slice(0, headSize);
245
+ const tailTriggers = triggers.slice(headSize);
246
+ const headTriggerCount = headTriggers.length;
247
+ const tailTriggerCount = tailTriggers.length;
248
+ data.totals.triggers = headTriggerCount + tailTriggerCount;
249
+ data.totals.headTriggers = headTriggerCount;
250
+ data.totals.tailTriggers = tailTriggerCount;
251
+ // Get the head and tail samples.
252
+ const headSample = getSample(headTriggers, headSampleSize);
253
+ const tailSample = tailSampleSize === -1 ? tailTriggers : getSample(tailTriggers, tailSampleSize);
254
+ // Find and document the impacts.
255
+ if (headSample.length) {
256
+ await find(withItems, page, 'head', headSample, headTriggerCount / headSample.length);
257
+ }
258
+ if (tailSample.length) {
259
+ await find(withItems, page, 'tail', tailSample, tailTriggerCount / tailSample.length);
237
260
  }
261
+ // Round the reported totals.
262
+ Object.keys(data.totals).forEach(key => {
263
+ data.totals[key] = Math.round(data.totals[key]);
264
+ });
238
265
  // Return the result.
239
266
  return {result: data};
240
267
  };
package/tests/ibm.js CHANGED
@@ -25,7 +25,6 @@ const run = async content => {
25
25
  const nowLabel = (new Date()).toISOString().slice(0, 19);
26
26
  // Return the result of a test.
27
27
  const ibmReport = await getCompliance(content, nowLabel);
28
- await close();
29
28
  return ibmReport;
30
29
  };
31
30
  // Trims an IBM report.
@@ -100,7 +99,7 @@ exports.reporter = async (page, withItems, withNewContent) => {
100
99
  // If a test with existing content is to be performed:
101
100
  const result = {};
102
101
  if (! withNewContent) {
103
- const timeLimit = 15;
102
+ const timeLimit = 20;
104
103
  const typeContent = await page.content();
105
104
  result.content = await doTest(typeContent, withItems, timeLimit);
106
105
  if (result.content.prevented) {
@@ -118,5 +117,6 @@ exports.reporter = async (page, withItems, withNewContent) => {
118
117
  console.log('ERROR: Getting ibm test report from URL took too long');
119
118
  }
120
119
  }
120
+ await close();
121
121
  return {result};
122
122
  };