testaro 5.16.2 → 5.17.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
@@ -196,11 +196,7 @@ exports.commands = {
196
196
  hover: [
197
197
  'Perform a hover test',
198
198
  {
199
- headSize: [false, 'number', '', 'count of first triggers to sample separately, if any'],
200
- headSampleSize: [false, 'number', '', 'size of the head sample to be drawn, if any'],
201
- tailSampleSize: [
202
- false, 'number', '', 'size of the non-head sample to be drawn, if not all'
203
- ],
199
+ sampleSize: [false, 'number', '', 'limit on sample size of triggers, if any'],
204
200
  withItems: [true, 'boolean']
205
201
  }
206
202
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "5.16.2",
3
+ "version": "5.17.0",
4
4
  "description": "Automation of accessibility testing",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -87,9 +87,7 @@
87
87
  {
88
88
  "type": "test",
89
89
  "which": "hover",
90
- "headSize": 40,
91
- "headSampleSize": 15,
92
- "tailSampleSize": 10,
90
+ "sampleSize": 20,
93
91
  "withItems": true,
94
92
  "what": "hover impacts"
95
93
  },
package/tests/hover.js CHANGED
@@ -13,12 +13,8 @@
13
13
 
14
14
  Despite this delay, the test can make the execution time practical by randomly sampling triggers
15
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.
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.
22
18
 
23
19
  An element is reported as unhoverable when it fails the Playwright actionability checks for
24
20
  hovering, i.e. fails to be attached to the DOM, visible, stable (not or no longer animating), and
@@ -34,25 +30,29 @@ let hasTimedOut = false;
34
30
 
35
31
  // FUNCTIONS
36
32
 
37
- // Samples a population and returns the sample.
33
+ // Samples a population and returns the sample and each member’s sampling probability.
38
34
  const getSample = (population, sampleSize) => {
39
35
  const popSize = population.length;
40
- if (sampleSize === 0) {
41
- return [];
42
- }
43
- else if (sampleSize > 0 && sampleSize < popSize) {
44
- const popData = [];
45
- for (const trigger of population) {
46
- popData.push({
47
- trigger,
48
- sorter: Math.random()
49
- });
50
- }
51
- popData.sort((a, b) => a.sorter - b.sorter);
52
- return popData.slice(0, sampleSize).map(obj => obj.trigger);
36
+ // If the sample is at least as large as the population:
37
+ if (sampleSize >= popSize) {
38
+ // Return the population as the sample.
39
+ return population.map(trigger => [trigger, 1]);
53
40
  }
41
+ // Otherwise, i.e. if the sample is smaller than the population:
54
42
  else {
55
- return population;
43
+ // Force the sample size to be an integer and at least 1.
44
+ sampleSize = Math.floor(Math.max(1, sampleSize));
45
+ 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
+ // Get the sample.
51
+ const sample = population.map((trigger, index) => {
52
+ const itemProb = samProb(index);
53
+ return [trigger, itemProb];
54
+ }).filter(pair => pair[1] > Math.random());
55
+ return sample;
56
56
  }
57
57
  };
58
58
  // Returns the text of an element.
@@ -62,28 +62,28 @@ const textOf = async (element, limit) => {
62
62
  return text.trim().replace(/\s*/sg, '').slice(0, limit);
63
63
  };
64
64
  // Recursively reports impacts of hovering over triggers.
65
- const find = async (data, withItems, page, region, sample, popRatio) => {
66
- // If any potential triggers remain and the test has not timed out:
65
+ const find = async (data, withItems, page, sample) => {
66
+ // If any triggers remain and the test has not timed out:
67
67
  if (sample.length && ! hasTimedOut) {
68
68
  // Get and report the impacts until and unless the test times out.
69
69
  try {
70
- // Identify the first of them.
70
+ // Identify the first trigger and its sampling probability.
71
71
  const firstTrigger = sample[0];
72
- const tagNameJSHandle = await firstTrigger.getProperty('tagName')
72
+ const tagNameJSHandle = await firstTrigger[0].getProperty('tagName')
73
73
  .catch(() => '');
74
74
  if (tagNameJSHandle) {
75
75
  const tagName = await tagNameJSHandle.jsonValue();
76
76
  // Identify the root of a subtree likely to contain impacted elements.
77
- let root = firstTrigger;
77
+ let root = firstTrigger[0];
78
78
  if (['A', 'BUTTON', 'LI'].includes(tagName)) {
79
79
  const rootJSHandle = await page.evaluateHandle(
80
- firstTrigger => {
81
- const parent = firstTrigger.parentElement || firstTrigger;
80
+ trigger => {
81
+ const parent = trigger.parentElement || trigger;
82
82
  const grandparent = parent.parentElement || parent;
83
83
  const greatGrandparent = grandparent.parentElement || parent;
84
- return firstTrigger.tagName === 'LI' ? grandparent : greatGrandparent;
84
+ return trigger.tagName === 'LI' ? grandparent : greatGrandparent;
85
85
  },
86
- firstTrigger
86
+ firstTrigger[0]
87
87
  );
88
88
  root = rootJSHandle.asElement();
89
89
  }
@@ -95,7 +95,7 @@ const find = async (data, withItems, page, region, sample, popRatio) => {
95
95
  ), preDescendants);
96
96
  try {
97
97
  // Hover over the trigger.
98
- await firstTrigger.hover({
98
+ await firstTrigger[0].hover({
99
99
  timeout: 500,
100
100
  noWaitAfter: true
101
101
  });
@@ -115,7 +115,7 @@ const find = async (data, withItems, page, region, sample, popRatio) => {
115
115
  .filter(index => index > -1);
116
116
  return remainerIndexes;
117
117
  }, [preDescendants, postDescendants]);
118
- // Get the count of elements added by the hover event.
118
+ // Get the impacts of the hover event.
119
119
  const additionCount = postDescendants.length - remainerIndexes.length;
120
120
  const removalCount = preDescendants.length - remainerIndexes.length;
121
121
  const remainers = [];
@@ -130,18 +130,24 @@ const find = async (data, withItems, page, region, sample, popRatio) => {
130
130
  }
131
131
  const opacityChangers = remainers
132
132
  .filter(remainer => remainer.postOpacity !== remainer.preOpacity);
133
- const opacityImpact = opacityChangers ? await page.evaluate(changers => changers.reduce(
133
+ const opacityImpact = opacityChangers
134
+ ? await page.evaluate(changers => changers.reduce(
134
135
  (total, current) => total + current.element.querySelectorAll('*').length, 0
135
- ), opacityChangers) : 0;
136
+ ), opacityChangers)
137
+ : 0;
138
+ // If there are any impacts:
136
139
  if (additionCount || removalCount || opacityChangers.length) {
140
+ // Return them as estimated population impacts.
137
141
  return {
138
- additionCount,
139
- removalCount,
140
- opacityChangers,
141
- opacityImpact
142
+ additionCount: additionCount / firstTrigger[1],
143
+ removalCount: removalCount / firstTrigger[1],
144
+ opacityChanges: opacityChangers.length / firstTrigger[1],
145
+ opacityImpact: opacityImpact / firstTrigger[1]
142
146
  };
143
147
  }
148
+ // Otherwise, i.e. if there are no impacts:
144
149
  else {
150
+ // Try again.
145
151
  return await new Promise(resolve => {
146
152
  setTimeout(() => {
147
153
  resolve(getImpacts(interval, triesLeft));
@@ -149,7 +155,9 @@ const find = async (data, withItems, page, region, sample, popRatio) => {
149
155
  });
150
156
  }
151
157
  }
158
+ // Otherwise, i.e. if the allowed trial count has been exhausted:
152
159
  else {
160
+ // Report non-impact.
153
161
  return null;
154
162
  }
155
163
  };
@@ -171,27 +179,27 @@ const find = async (data, withItems, page, region, sample, popRatio) => {
171
179
  // Wait for any delayed and/or slowed hover reaction.
172
180
  await page.waitForTimeout(200);
173
181
  await root.waitForElementState('stable');
174
- // Increment the counts of triggers and impacts.
175
- const {additionCount, removalCount, opacityChangers, opacityImpact} = impacts;
182
+ // Increment the estimated counts of triggers and impacts.
183
+ const {additionCount, removalCount, opacityChanges, opacityImpact} = impacts;
176
184
  if (hasTimedOut) {
177
185
  return Promise.resolve('');
178
186
  }
179
187
  else {
180
- data.totals.impactTriggers += popRatio;
181
- data.totals.additions += popRatio * additionCount;
182
- data.totals.removals += popRatio * removalCount;
183
- data.totals.opacityChanges += popRatio * opacityChangers.length;
184
- data.totals.opacityImpact += popRatio * opacityImpact;
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;
185
193
  // If details are to be reported:
186
194
  if (withItems) {
187
- // Report them.
188
- data.items[region].impactTriggers.push({
195
+ // Report them, with probability weighting removed.
196
+ data.items.impactTriggers.push({
189
197
  tagName,
190
- text: await textOf(firstTrigger, 50),
191
- additions: additionCount,
192
- removals: removalCount,
193
- opacityChanges: opacityChangers.length,
194
- opacityImpact
198
+ 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]
195
203
  });
196
204
  }
197
205
  }
@@ -203,14 +211,14 @@ const find = async (data, withItems, page, region, sample, popRatio) => {
203
211
  return Promise.resolve('');
204
212
  }
205
213
  else {
206
- data.totals.unhoverables++;
214
+ data.totals.unhoverables += 1 / firstTrigger[1];
207
215
  if (withItems) {
208
216
  try {
209
- const id = await firstTrigger.getAttribute('id');
210
- data.items[region].unhoverables.push({
217
+ const id = await firstTrigger[0].getAttribute('id');
218
+ data.items.unhoverables.push({
211
219
  tagName,
212
220
  id: id || '',
213
- text: await textOf(firstTrigger, 50)
221
+ text: await textOf(firstTrigger[0], 50)
214
222
  });
215
223
  }
216
224
  catch(error) {
@@ -221,7 +229,7 @@ const find = async (data, withItems, page, region, sample, popRatio) => {
221
229
  }
222
230
  }
223
231
  // Process the remaining potential triggers.
224
- await find(data, withItems, page, region, sample.slice(1), popRatio);
232
+ await find(data, withItems, page, sample.slice(1));
225
233
  }
226
234
  catch(error) {
227
235
  console.log(`ERROR: Test quit when remaining sample size was ${sample.length}`);
@@ -232,15 +240,12 @@ const find = async (data, withItems, page, region, sample, popRatio) => {
232
240
  }
233
241
  };
234
242
  // Performs the hover test and reports results.
235
- exports.reporter = async (
236
- page, headSize = 0, headSampleSize = -1, tailSampleSize = -1, withItems
237
- ) => {
243
+ exports.reporter = async (page, sampleSize = -1, withItems) => {
238
244
  // Initialize the result.
239
245
  let data = {
240
246
  totals: {
241
247
  triggers: 0,
242
- headTriggers: 0,
243
- tailTriggers: 0,
248
+ triggerSample: 0,
244
249
  impactTriggers: 0,
245
250
  additions: 0,
246
251
  removals: 0,
@@ -253,14 +258,8 @@ exports.reporter = async (
253
258
  if (withItems) {
254
259
  // Add properties for details to the initialized result.
255
260
  data.items = {
256
- head: {
257
- impactTriggers: [],
258
- unhoverables: []
259
- },
260
- tail: {
261
- impactTriggers: [],
262
- unhoverables: []
263
- }
261
+ impactTriggers: [],
262
+ unhoverables: []
264
263
  };
265
264
  }
266
265
  // Identify the triggers.
@@ -271,19 +270,12 @@ exports.reporter = async (
271
270
  data.prevented = true;
272
271
  return [];
273
272
  });
274
- // Classify them into head and tail triggers.
275
- const headTriggers = triggers.slice(0, headSize);
276
- const tailTriggers = triggers.slice(headSize);
277
- const headTriggerCount = headTriggers.length;
278
- const tailTriggerCount = tailTriggers.length;
279
- data.totals.triggers = headTriggerCount + tailTriggerCount;
280
- data.totals.headTriggers = headTriggerCount;
281
- data.totals.tailTriggers = tailTriggerCount;
282
- // Get the head and tail samples.
283
- const headSample = getSample(headTriggers, headSampleSize);
284
- const tailSample = tailSampleSize === -1 ? tailTriggers : getSample(tailTriggers, tailSampleSize);
273
+ data.totals.triggers = triggers.length;
274
+ // Get the sample.
275
+ const sample = getSample(triggers, sampleSize);
276
+ data.sampleSize = sample.length;
285
277
  // Set a time limit to cover possible 1.9 seconds per trigger.
286
- const timeLimit = Math.round(2.2 * (headSample.length + tailSample.length));
278
+ const timeLimit = Math.round(2.2 * data.sampleSize);
287
279
  const timeout = setTimeout(async () => {
288
280
  await page.close();
289
281
  console.log(
@@ -297,11 +289,8 @@ exports.reporter = async (
297
289
  clearTimeout(timeout);
298
290
  }, 1000 * timeLimit);
299
291
  // Find and document the impacts.
300
- if (headSample.length && ! hasTimedOut) {
301
- await find(data, withItems, page, 'head', headSample, headTriggerCount / headSample.length);
302
- }
303
- if (tailSample.length && ! hasTimedOut) {
304
- await find(data, withItems, page, 'tail', tailSample, tailTriggerCount / tailSample.length);
292
+ if (data.sampleSize && ! hasTimedOut) {
293
+ await find(data, withItems, page, sample);
305
294
  }
306
295
  clearTimeout(timeout);
307
296
  // Round the reported totals.
@@ -16,20 +16,17 @@
16
16
  "type": "test",
17
17
  "which": "hover",
18
18
  "what": "hover",
19
- "headSize": 3,
20
- "headSampleSize": 3,
21
- "tailSampleSize": 30,
19
+ "sampleSize": 2,
22
20
  "withItems": true,
23
21
  "expect": [
24
22
  ["totals.triggers", "=", 2],
25
- ["totals.headTriggers", "=", 2],
26
- ["totals.tailTriggers", "=", 0],
27
23
  ["totals.impactTriggers", "=", 0],
28
24
  ["totals.additions", "=", 0],
29
25
  ["totals.removals", "=", 0],
30
26
  ["totals.opacityChanges", "=", 0],
31
27
  ["totals.opacityImpact", "=", 0],
32
- ["totals.unhoverables", "=", 0]
28
+ ["totals.unhoverables", "=", 0],
29
+ ["items.impactTriggers.0"]
33
30
  ]
34
31
  },
35
32
  {
@@ -41,20 +38,49 @@
41
38
  "type": "test",
42
39
  "which": "hover",
43
40
  "what": "hover",
44
- "headSize": 3,
45
- "headSampleSize": 3,
46
- "tailSampleSize": 30,
41
+ "sampleSize": 5,
47
42
  "withItems": true,
48
43
  "expect": [
49
44
  ["totals.triggers", "=", 4],
50
- ["totals.headTriggers", "=", 3],
51
- ["totals.tailTriggers", "=", 1],
52
45
  ["totals.impactTriggers", "=", 2],
53
46
  ["totals.additions", "=", 3],
54
47
  ["totals.removals", "=", 0],
55
48
  ["totals.opacityChanges", "=", 1],
56
49
  ["totals.opacityImpact", "=", 1],
57
- ["totals.unhoverables", "=", 1]
50
+ ["totals.unhoverables", "=", 1],
51
+ ["items.impactTriggers.0.tagName", "=", "A"],
52
+ ["items.impactTriggers.1.tagName", "=", "BUTTON"],
53
+ ["items.impactTriggers.0.text", "=", "information"],
54
+ ["items.impactTriggers.0.additions", "=", 0],
55
+ ["items.impactTriggers.1.additions", "=", 3],
56
+ ["items.impactTriggers.0.opacityChanges", "=", 1],
57
+ ["items.impactTriggers.1.opacityImpact", "=", 0],
58
+ ["items.unhoverables.0.tagName", "=", "BUTTON"],
59
+ ["items.unhoverables.0.id", "=", "smallButton"],
60
+ ["items.unhoverables.0.text", "=", "button"]
61
+ ]
62
+ },
63
+ {
64
+ "type": "url",
65
+ "which": "__targets__/hover/large.html",
66
+ "what": "page with deviant hover behavior"
67
+ },
68
+ {
69
+ "type": "test",
70
+ "which": "hover",
71
+ "what": "hover",
72
+ "sampleSize": 10,
73
+ "withItems": true,
74
+ "expect": [
75
+ ["totals.triggers", "=", 20],
76
+ ["totals.impactTriggers", ">", -1],
77
+ ["totals.impactTriggers", "<", 6],
78
+ ["totals.additions", ">", -1],
79
+ ["totals.additions", "<", 6],
80
+ ["totals.removals", "=", 0],
81
+ ["totals.opacityChanges", "=", 0],
82
+ ["totals.opacityImpact", "=", 0],
83
+ ["totals.unhoverables", "=", 0]
58
84
  ]
59
85
  }
60
86
  ]
@@ -13,7 +13,7 @@
13
13
  <p id="hiddenP" style="display: none">The button, when hovered over, makes this paragraph, and therefore this <a href="https://en.wikipedia.org/wiki/Web_accessibility">link on web accessibility</a> and this <button type="button">new button</button>, visible.</p>
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
- <p style="position: relative"><button style="position: absolute; left: 10rem">button</button><button style="position: absolute; left: 11rem; top: -0.5rem; font-size: x-large">bigger button</button></p>
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
17
  </main>
18
18
  </body>
19
19
  </html>
@@ -0,0 +1,27 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en-US">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Page with many hover triggers</title>
6
+ <meta name="description" content="tester">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
+ </head>
9
+ <body>
10
+ <main>
11
+ <h1>Page with many hover triggers</h1>
12
+ <p>This paragraph contains a link to <a href="https://en.wikipedia.org" onmouseover="document.getElementById('english').removeAttribute('hidden')">information</a> and a <button type="button">button</button>.</p>
13
+ <p>This paragraph contains a link to <a href="https://fr.wikipedia.org" onmouseover="document.getElementById('french').removeAttribute('hidden')">information</a> and a <button type="button">button</button>.</p>
14
+ <p>This paragraph contains a link to <a href="https://ru.wikipedia.org">information</a> and a <button type="button">button</button>.</p>
15
+ <p>This paragraph contains a link to <a href="https://es.wikipedia.org">information</a> and a <button type="button">button</button>.</p>
16
+ <p>This paragraph contains a link to <a href="https://eo.wikipedia.org">information</a> and a <button type="button">button</button>.</p>
17
+ <p>This paragraph contains a link to <a href="https://fi.wikipedia.org">information</a> and a <button type="button">button</button>.</p>
18
+ <p>This paragraph contains a link to <a href="https://de.wikipedia.org" onmouseover="document.getElementById('german').removeAttribute('hidden')">information</a> and a <button type="button">button</button>.</p>
19
+ <p>This paragraph contains a link to <a href="https://hu.wikipedia.org">information</a> and a <button type="button">button</button>.</p>
20
+ <p>This paragraph contains a link to <a href="https://hi.wikipedia.org">information</a> and a <button type="button">button</button>.</p>
21
+ <p>This paragraph contains a link to <a href="https://ja.wikipedia.org">information</a> and a <button type="button">button</button>.</p>
22
+ <p id="english" hidden>English</p>
23
+ <p id="french" hidden>French</p>
24
+ <p id="german" hidden>German</p>
25
+ </main>
26
+ </body>
27
+ </html>