testaro 4.13.0 → 4.14.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 (2) hide show
  1. package/package.json +1 -1
  2. package/tests/hover.js +118 -120
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "4.13.0",
3
+ "version": "4.14.0",
4
4
  "description": "Automation of accessibility testing",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/tests/hover.js CHANGED
@@ -1,48 +1,43 @@
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 targets
15
+ instead of hovering over all of them. When sampling is performed, the results may vary from one
16
+ execution to another.
17
+
18
+ An element is reported as unhoverable when it fails the Playwright actionability checks for
19
+ hovering, i.e. fails to be attached to the DOM, visible, stable (not or no longer animating), and
20
+ able to receive events. All triggers satisfy the first two conditions, so only the last two might
21
+ fail. Playwright defines the ability to receive events as being the target of an action on the
22
+ location where the center of the element is, rather than some other element with a higher zIndex
23
+ value in the same location being the target.
21
24
  */
22
25
 
23
26
  // CONSTANTS
24
27
 
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
28
  // Initialize the result.
30
29
  const data = {
31
30
  populationSize: 0,
32
31
  totals: {
33
- triggers: 0,
34
- madeVisible: 0,
35
- opacityChanged: 0,
36
- opacityAffected: 0,
32
+ impactTriggers: 0,
33
+ additions: 0,
34
+ removals: 0,
35
+ opacityChanges: 0,
36
+ opacityEffects: 0,
37
37
  unhoverables: 0
38
38
  }
39
39
  };
40
40
 
41
- // VARIABLES
42
-
43
- // Counter.
44
- let elementsChecked = 0;
45
-
46
41
  // FUNCTIONS
47
42
 
48
43
  // Samples a population and returns the sample.
@@ -66,9 +61,9 @@ const textOf = async (element, limit) => {
66
61
  text = text.trim() || await element.innerHTML();
67
62
  return text.trim().replace(/\s*/sg, '').slice(0, limit);
68
63
  };
69
- // Recursively finds and reports triggers and targets.
64
+ // Recursively reports impacts of hovering over triggers.
70
65
  const find = async (withItems, page, triggers) => {
71
- // If any potential disclosure triggers remain:
66
+ // If any potential triggers remain:
72
67
  if (triggers.length) {
73
68
  // Identify the first of them.
74
69
  const firstTrigger = triggers[0];
@@ -79,59 +74,86 @@ const find = async (withItems, page, triggers) => {
79
74
  });
80
75
  if (tagNameJSHandle) {
81
76
  const tagName = await tagNameJSHandle.jsonValue();
82
- // Identify the root of a subtree likely to contain disclosed elements.
77
+ // Identify the root of a subtree likely to contain impacted elements.
83
78
  let root = firstTrigger;
84
- if (['A', 'BUTTON'].includes(tagName)) {
79
+ if (['A', 'BUTTON', 'LI'].includes(tagName)) {
85
80
  const rootJSHandle = await page.evaluateHandle(
86
81
  firstTrigger => {
87
82
  const parent = firstTrigger.parentElement || firstTrigger;
88
83
  const grandparent = parent.parentElement || parent;
89
- return grandparent;
84
+ const greatGrandparent = grandparent.parentElement || parent;
85
+ return firstTrigger.tagName === 'LI' ? grandparent : greatGrandparent;
90
86
  },
91
87
  firstTrigger
92
88
  );
93
89
  root = rootJSHandle.asElement();
94
90
  }
95
- // Identify the visible active descendants of the root before the hover.
96
- const preVisibles = await root.$$(targetSelectors);
97
91
  // 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
- );
92
+ const preDescendants = await root.$$('*');
93
+ // Identify their opacities.
94
+ const preOpacities = await page.evaluate(elements => elements.map(
95
+ element => window.getComputedStyle(element).opacity
96
+ ), preDescendants);
103
97
  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;
98
+ // Hover over the trigger.
99
+ await firstTrigger.hover({
100
+ timeout: 500,
101
+ noWaitAfter: true
102
+ });
103
+ // Repeatedly seeks impacts.
104
+ const getImpacts = async (interval, triesLeft) => {
105
+ if (triesLeft--) {
106
+ const postDescendants = await root.$$('*');
107
+ const remainerIndexes = await page.evaluate(args => {
108
+ const preDescendants = args[0];
109
+ const postDescendants = args[1];
110
+ const remainerIndexes = preDescendants
111
+ .map((element, index) => postDescendants.includes(element) ? index : -1)
112
+ .filter(index => index > -1);
113
+ return remainerIndexes;
114
+ }, [preDescendants, postDescendants]);
115
+ const additionCount = postDescendants.length - remainerIndexes.length;
116
+ const removalCount = preDescendants.length - remainerIndexes.length;
117
+ const remainers = [];
118
+ for (const index of remainerIndexes) {
119
+ remainers.push({
120
+ element: preDescendants[index],
121
+ preOpacity: preOpacities[index],
122
+ postOpacity: await page.evaluate(
123
+ element => window.getComputedStyle(element).opacity, preDescendants[index]
124
+ )
125
+ });
126
+ }
127
+ const opacityChangers = remainers
128
+ .filter(remainer => remainer.postOpacity !== remainer.preOpacity);
129
+ const opacityImpact = await page.evaluate(changers => changers.reduce(
130
+ (total, current) => total + current.element.querySelectorAll('*').length, 0
131
+ ), opacityChangers);
132
+ if (additionCount || removalCount || opacityChangers.length) {
133
+ return {
134
+ additionCount,
135
+ removalCount,
136
+ opacityChangers,
137
+ opacityImpact
138
+ };
139
+ }
140
+ else {
141
+ return await new Promise(resolve => {
142
+ setTimeout(() => {
143
+ resolve(getImpacts(interval, triesLeft));
144
+ }, interval);
145
+ });
146
+ }
147
+ }
148
+ else {
149
+ return null;
133
150
  }
134
- // Hover over the upper-left corner of the page, to undo any hover reactions.
151
+ };
152
+ // Repeatedly seek impacts of the hover at intervals.
153
+ const impacts = await getImpacts(300, 4);
154
+ // If there were any:
155
+ if (impacts) {
156
+ // Hover over the upper-left corner of the page, to undo any impacts.
135
157
  await page.hover('body', {
136
158
  position: {
137
159
  x: 0,
@@ -141,54 +163,35 @@ const find = async (withItems, page, triggers) => {
141
163
  // Wait for any delayed and/or slowed hover reaction.
142
164
  await page.waitForTimeout(200);
143
165
  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;
166
+ // Increment the counts of triggers and impacts.
167
+ const {additionCount, removalCount, opacityChangers, opacityImpact} = impacts;
168
+ data.totals.impactTriggers++;
169
+ data.totals.additions += additionCount;
170
+ data.totals.removals += removalCount;
171
+ data.totals.opacityChanges += opacityChangers.length;
172
+ data.totals.opacityImpact += opacityImpact;
150
173
  // If details are to be reported:
151
174
  if (withItems) {
152
175
  // 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);
176
+ data.items.impactTriggers.push({
177
+ tagName,
178
+ text: await textOf(firstTrigger, 50),
179
+ additions: additionCount,
180
+ removals: removalCount,
181
+ opacityChanges: opacityChangers.length,
182
+ opacityImpact
183
+ });
182
184
  }
183
185
  }
184
186
  }
185
187
  catch (error) {
186
- console.log('ERROR hovering');
188
+ console.log(`ERROR hovering (${error.message})`);
187
189
  data.totals.unhoverables++;
188
190
  if (withItems) {
191
+ const id = await firstTrigger.getAttribute('id');
189
192
  data.items.unhoverables.push({
190
- tagName: tagName,
191
- id: firstTrigger.id || '',
193
+ tagName,
194
+ id: id || '',
192
195
  text: await textOf(firstTrigger, 50)
193
196
  });
194
197
  }
@@ -204,18 +207,13 @@ exports.reporter = async (page, sampleSize = Infinity, withItems) => {
204
207
  if (withItems) {
205
208
  // Add properties for details to the initialized result.
206
209
  data.items = {
207
- triggers: [],
210
+ impactTriggers: [],
208
211
  unhoverables: []
209
212
  };
210
213
  }
211
214
  // 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(', '))
215
+ const selectors = ['a', 'button', 'li', '[onmouseenter]', '[onmouseover]'];
216
+ const triggers = await page.$$(selectors.map(selector => `body ${selector}:visible`).join(', '))
219
217
  .catch(error => {
220
218
  console.log(`ERROR getting hover triggers (${error.message})`);
221
219
  data.prevented = true;
@@ -225,7 +223,7 @@ exports.reporter = async (page, sampleSize = Infinity, withItems) => {
225
223
  const triggerCount = triggers.length;
226
224
  data.populationSize = triggerCount;
227
225
  const triggerSample = triggerCount > sampleSize ? getSample(triggers, sampleSize) : triggers;
228
- // Find and document the hover-triggered disclosures.
226
+ // Find and document the hover-triggered impacts.
229
227
  await find(withItems, page, triggerSample);
230
228
  // If the triggers were sampled:
231
229
  if (triggerCount > sampleSize) {