testaro 5.17.3 → 5.18.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "5.17.3",
3
+ "version": "5.18.1",
4
4
  "description": "Automation of accessibility testing",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/tests/hover.js CHANGED
@@ -1,20 +1,26 @@
1
1
  /*
2
2
  hover
3
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
4
+ of visible elements, opacity changes, unhoverable elements, and nonstandard hover indication.
5
+ The elements that are subjected to hovering (called “triggers”) are the Playwright-visible
6
+ elements that have 'A', 'BUTTON', or 'LI' tag names or have 'onmouseenter' or 'onmouseover'
7
+ attributes.
8
+
9
+ The test examines how the hover event is indicated to the user with the mouse cursor and with
10
+ changes of the styles of the trigger.
11
+
12
+ When a trigger is hovered over, the test also examines the impacts on descendants of the great
13
+ grandparents of triggers with tag names 'A' and 'BUTTON', grandparents of triggers with tag
14
+ name 'LI', and otherwise the descendants of the triggers themselves. Four impacts are counted:
15
+ (1) an element is added or becomes visible, (2) an element is removed or becomes invisible, (3)
16
+ the opacity of an element changes, and (4) the element is a descendant of an element whose opacity
12
17
  changes. The test checks up to 4 times for hovering impacts at intervals of 0.3 second.
13
18
 
14
- Despite this delay, the test can make the execution time practical by randomly sampling triggers
19
+ Despite the delay, the test can make the execution time practical by randomly sampling triggers
15
20
  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, the
17
- probability of the inclusion of a trigger in a sample decreases with the index of the trigger.
21
+ execution to another. Because hover impacts typically occur near the beginning of a page with
22
+ navigation menus, the probability of the inclusion of a trigger in a sample decreases with the
23
+ index of the trigger.
18
24
 
19
25
  An element is reported as unhoverable when it fails the Playwright actionability checks for
20
26
  hovering, i.e. fails to be attached to the DOM, visible, stable (not or no longer animating), and
@@ -30,7 +36,10 @@ let hasTimedOut = false;
30
36
 
31
37
  // FUNCTIONS
32
38
 
33
- // Samples a population and returns the sample and each member’s sampling probability.
39
+ // Gets the probability of a trigger being sampled.
40
+ const samProb = (index, popSize, sampleRatio) =>
41
+ sampleRatio * (1 + Math.sin(Math.PI * index / popSize + Math.PI / 2));
42
+ // Samples the trigger population and returns the sample and each member’s sampling probability.
34
43
  const getSample = (population, sampleSize) => {
35
44
  const popSize = population.length;
36
45
  // If the sample is at least as large as the population:
@@ -43,13 +52,9 @@ const getSample = (population, sampleSize) => {
43
52
  // Force the sample size to be an integer and at least 1.
44
53
  sampleSize = Math.floor(Math.max(1, sampleSize));
45
54
  const sampleRatio = sampleSize / popSize;
46
- // FUNCTION DEFINITION START
47
- // Gets the probability of a trigger being sampled.
48
- const samProb = index => (1 + Math.sin(Math.PI * index / popSize + Math.PI / 2)) * sampleRatio;
49
- // FUNCTION DEFINITION END
50
55
  // Get the sample.
51
56
  const sample = population.map((trigger, index) => {
52
- const itemProb = samProb(index);
57
+ const itemProb = samProb(index, popSize, sampleRatio);
53
58
  return [trigger, itemProb];
54
59
  }).filter(pair => pair[1] > Math.random());
55
60
  return sample;
@@ -59,9 +64,84 @@ const getSample = (population, sampleSize) => {
59
64
  const textOf = async (element, limit) => {
60
65
  let text = await element.textContent();
61
66
  text = text.trim() || await element.innerHTML();
62
- return text.trim().replace(/\s*/sg, '').slice(0, limit);
67
+ return text.trim().replace(/\s+/sg, ' ').slice(0, limit);
68
+ };
69
+ // Returns the impacts of hovering over a sampled trigger.
70
+ const getImpacts = async (
71
+ interval, triesLeft, root, page, preDescendants, preOpacities
72
+ ) => {
73
+ // If the allowed trial count has not yet been exhausted:
74
+ if (triesLeft-- && ! hasTimedOut) {
75
+ // Get the collection of descendants of the root.
76
+ const postDescendants = await root.$$(':visible');
77
+ // Identify the prior descendants of the root still in existence.
78
+ const remainerIndexes = await page.evaluate(args => {
79
+ const preDescendants = args[0];
80
+ const postDescendants = args[1];
81
+ const remainerIndexes = preDescendants
82
+ .map((element, index) => postDescendants.includes(element) ? index : -1)
83
+ .filter(index => index > -1);
84
+ return remainerIndexes;
85
+ }, [preDescendants, postDescendants]);
86
+ // Get the impacts of the hover event.
87
+ const additions = postDescendants.length - remainerIndexes.length;
88
+ const removals = preDescendants.length - remainerIndexes.length;
89
+ const remainers = [];
90
+ for (const index of remainerIndexes) {
91
+ remainers.push({
92
+ element: preDescendants[index],
93
+ preOpacity: preOpacities[index],
94
+ postOpacity: await page.evaluate(
95
+ element => window.getComputedStyle(element).opacity, preDescendants[index]
96
+ )
97
+ });
98
+ }
99
+ const opacityChangers = remainers
100
+ .filter(remainer => remainer.postOpacity !== remainer.preOpacity);
101
+ const opacityImpact = opacityChangers
102
+ ? await page.evaluate(changers => changers.reduce(
103
+ (total, current) => total + current.element.querySelectorAll('*').length, 0
104
+ ), opacityChangers)
105
+ : 0;
106
+ // If there are any impacts:
107
+ if (additions || removals || opacityChangers.length) {
108
+ // Return them.
109
+ return {
110
+ additions,
111
+ removals,
112
+ opacityChanges: opacityChangers.length,
113
+ opacityImpact
114
+ };
115
+ }
116
+ // Otherwise, i.e. if there are no impacts:
117
+ else {
118
+ // Try again.
119
+ return await new Promise(resolve => {
120
+ setTimeout(() => {
121
+ resolve(getImpacts(interval, triesLeft, root, page, preDescendants, preOpacities));
122
+ }, interval);
123
+ });
124
+ }
125
+ }
126
+ // Otherwise, i.e. if the allowed trial count has been exhausted:
127
+ else {
128
+ // Report non-impact.
129
+ return null;
130
+ }
63
131
  };
64
- // Recursively reports impacts of hovering over triggers.
132
+ // Returns the hover-related style properties of a trigger.
133
+ const getHoverStyles = async (page, element) => await page.evaluate(
134
+ element => {
135
+ const {cursor, outline, color, backgroundColor} = window.getComputedStyle(element);
136
+ return {
137
+ cursor: cursor.replace(/^.+, */, ''),
138
+ outline,
139
+ color,
140
+ backgroundColor
141
+ };
142
+ }, element
143
+ );
144
+ // Recursively adds estimated and itemized impacts of hovering over triggers to data.
65
145
  const find = async (data, withItems, page, sample) => {
66
146
  // If any triggers remain and the test has not timed out:
67
147
  if (sample.length && ! hasTimedOut) {
@@ -69,6 +149,8 @@ const find = async (data, withItems, page, sample) => {
69
149
  try {
70
150
  // Identify the first trigger and its sampling probability.
71
151
  const firstTrigger = sample[0];
152
+ const onmouseenter = await firstTrigger[0].getAttribute('onmouseenter');
153
+ const onmouseover = await firstTrigger[0].getAttribute('onmouseover');
72
154
  const tagNameJSHandle = await firstTrigger[0].getProperty('tagName')
73
155
  .catch(() => '');
74
156
  if (tagNameJSHandle) {
@@ -93,78 +175,70 @@ const find = async (data, withItems, page, sample) => {
93
175
  const preOpacities = await page.evaluate(elements => elements.map(
94
176
  element => window.getComputedStyle(element).opacity
95
177
  ), preDescendants);
178
+ // Get the style properties of the trigger.
179
+ const triggerPreStyles = await getHoverStyles(page, firstTrigger[0]);
180
+ const totalEstimate = 1 / firstTrigger[1];
181
+ const itemData = {
182
+ tagName,
183
+ id: (await firstTrigger[0].getAttribute('id')) || '',
184
+ text: await textOf(firstTrigger[0], 50)
185
+ };
96
186
  try {
97
187
  // Hover over the trigger.
98
188
  await firstTrigger[0].hover({
99
189
  timeout: 500,
100
190
  noWaitAfter: true
101
191
  });
102
- // FUNCTION DEFINITION START
103
- // Repeatedly seeks impacts.
104
- const getImpacts = async (interval, triesLeft) => {
105
- // If the allowed trial count has not yet been exhausted:
106
- if (triesLeft-- && ! hasTimedOut) {
107
- // Get the collection of descendants of the root.
108
- const postDescendants = await root.$$(':visible');
109
- // Identify the prior descandants of the root still in existence.
110
- const remainerIndexes = await page.evaluate(args => {
111
- const preDescendants = args[0];
112
- const postDescendants = args[1];
113
- const remainerIndexes = preDescendants
114
- .map((element, index) => postDescendants.includes(element) ? index : -1)
115
- .filter(index => index > -1);
116
- return remainerIndexes;
117
- }, [preDescendants, postDescendants]);
118
- // Get the impacts of the hover event.
119
- const additionCount = postDescendants.length - remainerIndexes.length;
120
- const removalCount = preDescendants.length - remainerIndexes.length;
121
- const remainers = [];
122
- for (const index of remainerIndexes) {
123
- remainers.push({
124
- element: preDescendants[index],
125
- preOpacity: preOpacities[index],
126
- postOpacity: await page.evaluate(
127
- element => window.getComputedStyle(element).opacity, preDescendants[index]
128
- )
129
- });
130
- }
131
- const opacityChangers = remainers
132
- .filter(remainer => remainer.postOpacity !== remainer.preOpacity);
133
- const opacityImpact = opacityChangers
134
- ? await page.evaluate(changers => changers.reduce(
135
- (total, current) => total + current.element.querySelectorAll('*').length, 0
136
- ), opacityChangers)
137
- : 0;
138
- // If there are any impacts:
139
- if (additionCount || removalCount || opacityChangers.length) {
140
- // Return them as estimated population impacts.
141
- return {
142
- additionCount: additionCount / firstTrigger[1],
143
- removalCount: removalCount / firstTrigger[1],
144
- opacityChanges: opacityChangers.length / firstTrigger[1],
145
- opacityImpact: opacityImpact / firstTrigger[1]
146
- };
147
- }
148
- // Otherwise, i.e. if there are no impacts:
149
- else {
150
- // Try again.
151
- return await new Promise(resolve => {
152
- setTimeout(() => {
153
- resolve(getImpacts(interval, triesLeft));
154
- }, interval);
155
- });
156
- }
192
+ // Repeatedly seek impacts of the hover at intervals.
193
+ const impacts = await getImpacts(
194
+ 300, 4, root, page, preDescendants, preOpacities, firstTrigger
195
+ );
196
+ // Get the style properties of the trigger.
197
+ const triggerPostStyles = await getHoverStyles(page, firstTrigger[0]);
198
+ // Add cursor and other style defects to the data.
199
+ const cursor = triggerPreStyles.cursor;
200
+ // If the trigger has no cursor:
201
+ if (cursor === 'none') {
202
+ // Add this fact to the data.
203
+ data.totals.noCursors += totalEstimate;
204
+ if (withItems) {
205
+ data.items.noCursors.push(itemData);
157
206
  }
158
- // Otherwise, i.e. if the allowed trial count has been exhausted:
159
- else {
160
- // Report non-impact.
161
- return null;
207
+ }
208
+ // If the trigger has an improper cursor:
209
+ if (
210
+ tagName === 'A' && cursor !== 'pointer'
211
+ || tagName === 'BUTTON' && cursor !== 'default'
212
+ ){
213
+ // Add this fact to the data.
214
+ data.totals.badCursors += totalEstimate;
215
+ if (withItems) {
216
+ data.items.badCursors.push(itemData);
162
217
  }
163
- };
164
- // FUNCTION DEFINITION END
165
- // Repeatedly seek impacts of the hover at intervals.
166
- const impacts = await getImpacts(300, 4);
167
- // If there were any:
218
+ }
219
+ // If hover indication is required but is absent:
220
+ if (
221
+ (tagName === 'BUTTON' || onmouseenter || onmouseover)
222
+ && JSON.stringify(triggerPostStyles) === JSON.stringify(triggerPreStyles)
223
+ ) {
224
+ // Add this fact to the data.
225
+ data.totals.noIndicators += totalEstimate;
226
+ if (withItems) {
227
+ data.items.noIndicators.push(itemData);
228
+ }
229
+ }
230
+ // If hover indication is illicit but is present:
231
+ if (
232
+ tagName === 'LI'
233
+ && JSON.stringify(triggerPostStyles) !== JSON.stringify(triggerPreStyles)
234
+ ) {
235
+ // Add this fact to the data.
236
+ data.totals.badIndicators += totalEstimate;
237
+ if (withItems) {
238
+ data.items.badIndicators.push(itemData);
239
+ }
240
+ }
241
+ // If there were any impacts:
168
242
  if (impacts) {
169
243
  // Hover over the upper-left corner of the page, to undo any impacts.
170
244
  await page.hover('body', {
@@ -176,30 +250,30 @@ const find = async (data, withItems, page, sample) => {
176
250
  force: true,
177
251
  noWaitAfter: true
178
252
  });
179
- // Wait for any delayed and/or slowed hover reaction.
253
+ // Wait for any delayed and/or slowed reaction.
180
254
  await page.waitForTimeout(200);
181
255
  await root.waitForElementState('stable');
182
256
  // Increment the estimated counts of triggers and impacts.
183
- const {additionCount, removalCount, opacityChanges, opacityImpact} = impacts;
257
+ const {additions, removals, opacityChanges, opacityImpact} = impacts;
184
258
  if (hasTimedOut) {
185
259
  return Promise.resolve('');
186
260
  }
187
261
  else {
188
- data.totals.impactTriggers += 1 / firstTrigger[1];
189
- data.totals.additions += additionCount;
190
- data.totals.removals += removalCount;
191
- data.totals.opacityChanges += opacityChanges;
192
- data.totals.opacityImpact += opacityImpact;
262
+ data.totals.impactTriggers += totalEstimate;
263
+ data.totals.additions += additions / firstTrigger[1];
264
+ data.totals.removals += removals / firstTrigger[1];
265
+ data.totals.opacityChanges += opacityChanges / firstTrigger[1];
266
+ data.totals.opacityImpact += opacityImpact / firstTrigger[1];
193
267
  // If details are to be reported:
194
268
  if (withItems) {
195
- // Report them, with probability weighting removed.
269
+ // Add them to the data.
196
270
  data.items.impactTriggers.push({
197
271
  tagName,
198
272
  text: await textOf(firstTrigger[0], 50),
199
- additions: additionCount * firstTrigger[1],
200
- removals: removalCount * firstTrigger[1],
201
- opacityChanges: opacityChanges * firstTrigger[1],
202
- opacityImpact: opacityImpact * firstTrigger[1]
273
+ additions,
274
+ removals,
275
+ opacityChanges,
276
+ opacityImpact
203
277
  });
204
278
  }
205
279
  }
@@ -211,15 +285,10 @@ const find = async (data, withItems, page, sample) => {
211
285
  return Promise.resolve('');
212
286
  }
213
287
  else {
214
- data.totals.unhoverables += 1 / firstTrigger[1];
288
+ data.totals.unhoverables += totalEstimate;
215
289
  if (withItems) {
216
290
  try {
217
- const id = await firstTrigger[0].getAttribute('id');
218
- data.items.unhoverables.push({
219
- tagName,
220
- id: id || '',
221
- text: await textOf(firstTrigger[0], 50)
222
- });
291
+ data.items.unhoverables.push(itemData);
223
292
  }
224
293
  catch(error) {
225
294
  console.log('ERROR itemizing unhoverable element');
@@ -251,7 +320,11 @@ exports.reporter = async (page, sampleSize = -1, withItems) => {
251
320
  removals: 0,
252
321
  opacityChanges: 0,
253
322
  opacityImpact: 0,
254
- unhoverables: 0
323
+ unhoverables: 0,
324
+ noCursors: 0,
325
+ badCursors: 0,
326
+ noIndicators: 0,
327
+ badIndicators: 0
255
328
  }
256
329
  };
257
330
  // If details are to be reported:
@@ -259,7 +332,11 @@ exports.reporter = async (page, sampleSize = -1, withItems) => {
259
332
  // Add properties for details to the initialized result.
260
333
  data.items = {
261
334
  impactTriggers: [],
262
- unhoverables: []
335
+ unhoverables: [],
336
+ noCursors: [],
337
+ badCursors: [],
338
+ noIndicators: [],
339
+ badIndicators: []
263
340
  };
264
341
  }
265
342
  // Identify the triggers.
@@ -288,7 +365,7 @@ exports.reporter = async (page, sampleSize = -1, withItems) => {
288
365
  };
289
366
  clearTimeout(timeout);
290
367
  }, 1000 * timeLimit);
291
- // Find and document the impacts.
368
+ // Find and document the style defects and impacts of the sampled triggers.
292
369
  if (sample.length && ! hasTimedOut) {
293
370
  await find(data, withItems, page, sample);
294
371
  }
@@ -16,10 +16,10 @@
16
16
  "type": "test",
17
17
  "which": "hover",
18
18
  "what": "hover",
19
- "sampleSize": 2,
19
+ "sampleSize": 5,
20
20
  "withItems": true,
21
21
  "expect": [
22
- ["totals.triggers", "=", 2],
22
+ ["totals.triggers", "=", 4],
23
23
  ["totals.impactTriggers", "=", 0],
24
24
  ["totals.additions", "=", 0],
25
25
  ["totals.removals", "=", 0],
@@ -38,21 +38,23 @@
38
38
  "type": "test",
39
39
  "which": "hover",
40
40
  "what": "hover",
41
- "sampleSize": 5,
41
+ "sampleSize": 8,
42
42
  "withItems": true,
43
43
  "expect": [
44
- ["totals.triggers", "=", 4],
45
- ["totals.impactTriggers", "=", 2],
44
+ ["totals.triggers", "=", 6],
45
+ ["totals.impactTriggers", "=", 3],
46
46
  ["totals.additions", "=", 3],
47
- ["totals.removals", "=", 0],
47
+ ["totals.removals", "=", 1],
48
48
  ["totals.opacityChanges", "=", 1],
49
49
  ["totals.opacityImpact", "=", 1],
50
50
  ["totals.unhoverables", "=", 1],
51
51
  ["items.impactTriggers.0.tagName", "=", "A"],
52
52
  ["items.impactTriggers.1.tagName", "=", "BUTTON"],
53
+ ["items.impactTriggers.2.tagName", "=", "LI"],
53
54
  ["items.impactTriggers.0.text", "=", "information"],
54
55
  ["items.impactTriggers.0.additions", "=", 0],
55
56
  ["items.impactTriggers.1.additions", "=", 3],
57
+ ["items.impactTriggers.2.removals", "=", 1],
56
58
  ["items.impactTriggers.0.opacityChanges", "=", 1],
57
59
  ["items.impactTriggers.1.opacityImpact", "=", 0],
58
60
  ["items.unhoverables.0.tagName", "=", "BUTTON"],
@@ -70,7 +72,7 @@
70
72
  "which": "hover",
71
73
  "what": "hover",
72
74
  "sampleSize": 10,
73
- "withItems": true,
75
+ "withItems": false,
74
76
  "expect": [
75
77
  ["totals.triggers", "=", 20],
76
78
  ["totals.impactTriggers", ">", -1],
@@ -82,6 +84,26 @@
82
84
  ["totals.opacityImpact", "=", 0],
83
85
  ["totals.unhoverables", "=", 0]
84
86
  ]
87
+ },
88
+ {
89
+ "type": "url",
90
+ "which": "__targets__/hover/styleBad.html",
91
+ "what": "page with deviant trigger styles"
92
+ },
93
+ {
94
+ "type": "test",
95
+ "which": "hover",
96
+ "what": "hover",
97
+ "sampleSize": 5,
98
+ "withItems": true,
99
+ "expect": [
100
+ ["totals.noIndicators", "=", 1],
101
+ ["totals.noCursors", "=", 1],
102
+ ["totals.badIndicators", "=", 1],
103
+ ["items.noIndicators.0.text", "=", "button"],
104
+ ["items.noCursors.0.tagName", "=", "LI"],
105
+ ["items.badIndicators.0.id", "=", "li2"]
106
+ ]
85
107
  }
86
108
  ]
87
109
  }
@@ -14,6 +14,12 @@
14
14
  <p id="translucent" style="opacity: 0.4">The first link, when hovered over, changes the opacity of this paragraph from 0.4 to 1. That indirectly changes the opacity of this <span>word</span>, too.</p>
15
15
  <p>The small button is mostly covered by a large one here, preventing the small button from receiving a hover event.</p>
16
16
  <p style="position: relative"><button id="smallButton" style="position: absolute; left: 10rem">button</button><button style="position: absolute; left: 11rem; top: -0.5rem; font-size: x-large">bigger button</button></p>
17
+ <p>Hovering over the first of the following list items changes content.</p>
18
+ <ul>
19
+ <li onmouseover="document.getElementById('listDependent').setAttribute('hidden', true)">This is a list item.</li>
20
+ <li>This is another list item.</li>
21
+ </ul>
22
+ <p id="listDependent">This paragraph becomes invisible when you hover over the first list item.</p>
17
23
  </main>
18
24
  </body>
19
25
  </html>
@@ -10,6 +10,11 @@
10
10
  <main>
11
11
  <h1>Page with standard hover behavior</h1>
12
12
  <p>This paragraph contains a link to <a href="https://en.wikipedia.org">information</a> and a <button type="button">button</button>. Both of them can be hovered over, and hovering over either of them does not trigger any change in content.</p>
13
+ <p>Hovering over either of the following list items changes no content.</p>
14
+ <ul>
15
+ <li>This is a list item.</li>
16
+ <li>This is another list item.</li>
17
+ </ul>
13
18
  </main>
14
19
  </body>
15
20
  </html>
@@ -0,0 +1,35 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en-US">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Page with deviant hover indicators</title>
6
+ <meta name="description" content="tester">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
+ <style>
9
+ button, button:hover {
10
+ color: red;
11
+ background-color: white;
12
+ border: 2px solid black;
13
+ }
14
+ li.cursorless {
15
+ cursor: none;
16
+ }
17
+ li.hoverChanger:hover {
18
+ color: blue;
19
+ background-color: yellow;
20
+ }
21
+ </style>
22
+ </head>
23
+ <body>
24
+ <main>
25
+ <h1>Page with deviant hover indicators</h1>
26
+ <p>This page contains a link to <a href="https://en.wikipedia.org">information</a> and a <button type="button">button</button>. The default style change of the button is prevented.</p>
27
+ <p>This is a list.</p>
28
+ <ul>
29
+ <li>This is a normal list item.</li>
30
+ <li class="cursorless">This is a list item that loses its cursor when hovered over.</li>
31
+ <li id="li2" class="hoverChanger">This is a list item that changes when hovered over although it should not.</li>
32
+ </ul>
33
+ </main>
34
+ </body>
35
+ </html>