testaro 16.1.0 → 16.3.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/testaro/hover.js CHANGED
@@ -1,427 +1,149 @@
1
1
  /*
2
2
  hover
3
- This test reports unexpected impacts of hovering. The effects include additions and removals
4
- of visible elements, opacity changes, unhoverable elements, and nonstandard hover cursors.
3
+ This test reports unexpected impacts of hovering on the visible page. Impacts are measured by
4
+ pixel changes outside the hovered element and by unhoverability.
5
+
5
6
  The elements that are subjected to hovering (called “triggers”) are the Playwright-visible
6
7
  elements that have 'A', 'BUTTON', or (if not with role=menuitem) 'LI' tag names or have
7
8
  'onmouseenter' or 'onmouseover' attributes.
8
9
 
9
- When a trigger is hovered over, the test examines the impacts on descendants of the great
10
- grandparents of triggers with tag names 'A' and 'BUTTON', grandparents of triggers with tag
11
- name 'LI', and otherwise the descendants of the triggers themselves. Four impacts are counted:
12
- (1) an element is added or becomes visible, (2) an element is removed or becomes invisible, (3)
13
- the opacity of an element changes, and (4) the element is a descendant of an element whose opacity
14
- changes. The test checks up to 4 times for hovering impacts at intervals of 0.3 second.
15
-
16
10
  Despite the delay, the test can make the execution time practical by randomly sampling triggers
17
11
  instead of hovering over all of them. When sampling is performed, the results may vary from one
18
12
  execution to another. Because hover impacts typically occur near the beginning of a page with
19
13
  navigation menus, the probability of the inclusion of a trigger in a sample decreases with the
20
14
  index of the trigger.
21
15
 
22
- An element is reported as unhoverable when it fails the Playwright actionability checks for
23
- hovering, i.e. fails to be attached to the DOM, visible, stable (not or no longer animating), and
24
- able to receive events. All triggers satisfy the first two conditions, so only the last two might
25
- fail. Playwright defines the ability to receive events as being the target of an action on the
26
- location where the center of the element is, rather than some other element with a higher zIndex
27
- value in the same location being the target.
16
+ Pixel changes: If no pixel changes occur immediately after an element is hovered over, the page
17
+ is examined once more, after 0.5 second. The greater the fraction of changed pixels, the greater
18
+ the ordinal severity.
19
+
20
+ Unhoverability: An element is reported as unhoverable when it fails the Playwright actionability
21
+ checks for hovering, i.e. fails to be attached to the DOM, visible, stable (not or no longer
22
+ animating), and able to receive events. All triggers satisfy the first two conditions, so only the
23
+ last two might fail. Playwright defines the ability to receive events as being the target of an
24
+ action on the location where the center of the element is, rather than some other element with a
25
+ higher zIndex value in the same location being the target.
28
26
 
29
- An alternative strategy meriting consideration is to take screen shots and measure pixel changes.
27
+ WARNING: This test uses the Playwright page.screenshot method, which is not implemented for the
28
+ firefox browser type.
30
29
  */
31
30
 
32
- // VARIABLES
31
+ // IMPORTS
33
32
 
34
- let hasTimedOut = false;
33
+ // Module to get locator data.
34
+ const {getLocatorData} = require('../procs/getLocatorData');
35
+ // Module to draw a sample.
36
+ const {getSample} = require('../procs/sample');
37
+ // Module to get pixel changes between two times.
38
+ const {visChange} = require('../procs/visChange');
35
39
 
36
40
  // FUNCTIONS
37
41
 
38
- // Draws a location-weighted sample of triggers.
39
- const getSample = (population, sampleSize) => {
40
- const popSize = population.length;
41
- if (sampleSize < popSize) {
42
- const WeightedPopulation = population.map((trigger, index) => {
43
- const weight = 1 + Math.sin(Math.PI * index / popSize + Math.PI / 2);
44
- const priority = weight * Math.random();
45
- return [trigger, index, priority];
42
+ // Performs the hover test and reports results.
43
+ exports.reporter = async (page, withItems, sampleSize = 20) => {
44
+ // Initialize the result.
45
+ const data = {};
46
+ const totals = [0, 0, 0, 0];
47
+ const standardInstances = [];
48
+ // Identify the triggers.
49
+ const selectors = ['a', 'button', 'li:not([role=menuitem])', '[onmouseenter]', '[onmouseover]'];
50
+ const selectorString = selectors.map(selector => `body ${selector}:visible`).join(', ');
51
+ const locAll = page.locator(selectorString);
52
+ const locsAll = await locAll.all();
53
+ // Get the population-to-sample ratio.
54
+ const psRatio = Math.max(1, locsAll.length / sampleSize);
55
+ // Get a sample of the triggers.
56
+ const sampleIndexes = getSample(locsAll, sampleSize);
57
+ const sample = locsAll.filter((loc, index) => sampleIndexes.includes(index));
58
+ // For each trigger in the sample:
59
+ for (const loc of sample) {
60
+ // Hover over it and get the fractional pixel change.
61
+ const hoverData = await visChange(page, {
62
+ delayBefore: 0,
63
+ delayBetween: 500,
64
+ exclusion: loc
46
65
  });
47
- const sortedPopulation = WeightedPopulation.sort((a, b) => b[2] - a[2]);
48
- const sample = sortedPopulation.slice(0, sampleSize);
49
- const domOrderSample = sample.sort((a, b) => a[1] - b[1]);
50
- return domOrderSample.map(trigger => trigger[0]);
51
- }
52
- else {
53
- return population;
54
- }
55
- };
56
- // Returns the text of an element.
57
- const textOf = async (element, limit) => {
58
- let text = await element.textContent();
59
- text = text.trim() || await element.innerHTML();
60
- return text.trim().replace(/\s+/sg, ' ').slice(0, limit);
61
- };
62
- // Returns the impacts of hovering over a sampled trigger.
63
- const getImpacts = async (
64
- interval, triesLeft, root, page, preDescendants, preOpacities
65
- ) => {
66
- // If the allowed trial count has not yet been exhausted:
67
- if (triesLeft-- && ! hasTimedOut) {
68
- // Get the collection of descendants of the root.
69
- const postDescendants = await root.$$(':visible');
70
- // Identify the prior descendants of the root still in existence.
71
- const remainerIndexes = await page.evaluate(args => {
72
- const preDescendants = args[0];
73
- const postDescendants = args[1];
74
- const remainerIndexes = preDescendants
75
- .map((element, index) => postDescendants.includes(element) ? index : -1)
76
- .filter(index => index > -1);
77
- return remainerIndexes;
78
- }, [preDescendants, postDescendants]);
79
- // Get the impacts of the hover event.
80
- const additions = postDescendants.length - remainerIndexes.length;
81
- const removals = preDescendants.length - remainerIndexes.length;
82
- const remainers = [];
83
- for (const index of remainerIndexes) {
84
- remainers.push({
85
- element: preDescendants[index],
86
- preOpacity: preOpacities[index],
87
- postOpacity: await page.evaluate(
88
- element => window.getComputedStyle(element).opacity, preDescendants[index]
89
- )
90
- });
91
- }
92
- const opacityChangers = remainers
93
- .filter(remainer => remainer.postOpacity !== remainer.preOpacity);
94
- const opacityImpact = opacityChangers
95
- ? await page.evaluate(changers => changers.reduce(
96
- (total, current) => total + current.element.querySelectorAll('*').length, 0
97
- ), opacityChangers)
98
- : 0;
99
- // If there are any impacts:
100
- if (additions || removals || opacityChangers.length) {
101
- // Return them.
102
- return {
103
- additions,
104
- removals,
105
- opacityChanges: opacityChangers.length,
106
- opacityImpact
107
- };
66
+ // If the hovering and measurement succeeded:
67
+ if (hoverData.success) {
68
+ // If any pixels changed:
69
+ if (hoverData.changePercent) {
70
+ // Get the ordinal severity from the fractional pixel change.
71
+ const ordinalSeverity = Math.floor(Math.min(3, 0.4 * Math.sqrt(hoverData.changePercent)));
72
+ // Add to the totals.
73
+ totals[ordinalSeverity] += psRatio;
74
+ // If itemization is required:
75
+ if (withItems) {
76
+ // Get data on the trigger.
77
+ const elData = await getLocatorData(loc);
78
+ // Add an instance to the result.
79
+ standardInstances.push({
80
+ ruleID: 'hover',
81
+ what: 'Hovering over the element changes the page',
82
+ ordinalSeverity,
83
+ tagName: elData.tagName,
84
+ id: elData.id,
85
+ location: elData.location,
86
+ excerpt: elData.excerpt
87
+ });
88
+ }
89
+ }
108
90
  }
109
- // Otherwise, i.e. if there are no impacts:
91
+ // Otherwise, i.e. if hovering and measurement failed:
110
92
  else {
111
- // Try again.
112
- return await new Promise(resolve => {
113
- setTimeout(() => {
114
- resolve(getImpacts(interval, triesLeft, root, page, preDescendants, preOpacities));
115
- }, interval);
116
- });
93
+ // Add to the totals.
94
+ totals[3] += psRatio;
95
+ // If itemization is required:
96
+ if (withItems) {
97
+ // Get data on the trigger.
98
+ const elData = await getLocatorData(loc);
99
+ // Add an instance to the result.
100
+ standardInstances.push({
101
+ ruleID: 'hover',
102
+ what: 'Element is not hoverable',
103
+ ordinalSeverity: 3,
104
+ tagName: elData.tagName,
105
+ id: elData.id,
106
+ location: elData.location,
107
+ excerpt: elData.excerpt
108
+ });
109
+ }
117
110
  }
118
- }
119
- // Otherwise, i.e. if the allowed trial count has been exhausted:
120
- else {
121
- // Report non-impact.
122
- return null;
123
- }
124
- };
125
- // Returns the hover-related style properties of a trigger.
126
- const getHoverStyles = async (page, element) => await page.evaluate(
127
- element => {
128
- const {cursor, outline, color, backgroundColor} = window.getComputedStyle(element);
129
- return {
130
- cursor: cursor.replace(/^.+, */, ''),
131
- outline,
132
- color,
133
- backgroundColor
134
- };
135
- }, element
136
- );
137
- // Recursively adds estimated and itemized impacts of hovering over triggers to data.
138
- const find = async (data, withItems, page, sample) => {
139
- // If any triggers remain and the test has not timed out:
140
- if (sample.length && ! hasTimedOut) {
141
- // Get and report the impacts until and unless the test times out.
111
+ // Reload the page to preserve locator integrity.
142
112
  try {
143
- // Identify the first trigger.
144
- const firstTrigger = sample[0];
145
- const tagNameJSHandle = await firstTrigger.getProperty('tagName')
146
- .catch(() => '');
147
- if (tagNameJSHandle) {
148
- const tagName = await tagNameJSHandle.jsonValue();
149
- // Identify the root of a subtree likely to contain impacted elements.
150
- let root = firstTrigger;
151
- if (['A', 'BUTTON', 'LI'].includes(tagName)) {
152
- const rootJSHandle = await page.evaluateHandle(
153
- trigger => {
154
- const parent = trigger.parentElement || trigger;
155
- const grandparent = parent.parentElement || parent;
156
- const greatGrandparent = grandparent.parentElement || parent;
157
- return trigger.tagName === 'LI' ? grandparent : greatGrandparent;
158
- },
159
- firstTrigger
160
- );
161
- root = rootJSHandle.asElement();
162
- }
163
- // Identify all the visible descendants of the root.
164
- const preDescendants = await root.$$(':visible');
165
- // Identify their opacities.
166
- const preOpacities = await page.evaluate(elements => elements.map(
167
- element => window.getComputedStyle(element).opacity
168
- ), preDescendants);
169
- // Get the style properties of the trigger.
170
- const triggerPreStyles = await getHoverStyles(page, firstTrigger);
171
- const multiplier = data.sampling.triggers / data.sampling.triggerSample;
172
- const itemData = {
173
- tagName,
174
- id: (await firstTrigger.getAttribute('id')) || '',
175
- text: await textOf(firstTrigger, 100)
176
- };
177
- try {
178
- // Hover over the trigger.
179
- await firstTrigger.hover({
180
- timeout: 500,
181
- noWaitAfter: true
182
- });
183
- // Repeatedly seek impacts of the hover at intervals.
184
- const impacts = await getImpacts(
185
- 300, 4, root, page, preDescendants, preOpacities, firstTrigger
186
- );
187
- // Get the style properties of the trigger.
188
- const triggerPostStyles = await getHoverStyles(page, firstTrigger);
189
- // Add cursor and other style defects to the data.
190
- const cursor = triggerPreStyles.cursor;
191
- // If the trigger has no cursor:
192
- if (cursor === 'none') {
193
- // Add this fact to the data.
194
- data.totals.noCursors += multiplier;
195
- if (withItems) {
196
- data.items.noCursors.push(itemData);
197
- }
198
- }
199
- // If the trigger has an improper cursor:
200
- if (
201
- tagName === 'A' && cursor !== 'pointer'
202
- || tagName === 'BUTTON' && cursor !== 'default'
203
- ){
204
- // Add this fact to the data.
205
- data.totals.badCursors += multiplier;
206
- if (withItems) {
207
- data.items.badCursors.push(itemData);
208
- }
209
- }
210
- // If hover indication is illicit but is present:
211
- if (
212
- tagName === 'LI'
213
- && JSON.stringify(triggerPostStyles) !== JSON.stringify(triggerPreStyles)
214
- ) {
215
- // Add this fact to the data.
216
- data.totals.badIndicators += multiplier;
217
- if (withItems) {
218
- data.items.badIndicators.push(itemData);
219
- }
220
- }
221
- // If there were any impacts:
222
- if (impacts) {
223
- // Hover over the upper-left corner of the page, to undo any impacts.
224
- await page.hover('body', {
225
- position: {
226
- x: 0,
227
- y: 0
228
- },
229
- timeout: 500,
230
- force: true,
231
- noWaitAfter: true
232
- });
233
- // Wait for any delayed and/or slowed reaction.
234
- await page.waitForTimeout(200);
235
- await root.waitForElementState('stable');
236
- // Increment the estimated counts of triggers and impacts.
237
- const {additions, removals, opacityChanges, opacityImpact} = impacts;
238
- if (hasTimedOut) {
239
- return Promise.resolve('');
240
- }
241
- else {
242
- data.totals.impactTriggers += multiplier;
243
- data.totals.additions += additions * multiplier;
244
- data.totals.removals += removals * multiplier;
245
- data.totals.opacityChanges += opacityChanges * multiplier;
246
- data.totals.opacityImpact += opacityImpact * multiplier;
247
- // If details are to be reported:
248
- if (withItems) {
249
- // Add them to the data.
250
- data.items.impactTriggers.push({
251
- tagName,
252
- id: itemData.id,
253
- text: await textOf(firstTrigger, 100),
254
- additions,
255
- removals,
256
- opacityChanges,
257
- opacityImpact
258
- });
259
- }
260
- }
261
- }
262
- }
263
- catch (error) {
264
- console.log(`ERROR hovering (${error.message.replace(/\n.+/s, '')})`);
265
- if (hasTimedOut) {
266
- return Promise.resolve('');
267
- }
268
- else {
269
- data.totals.unhoverables += multiplier;
270
- if (withItems) {
271
- try {
272
- data.items.unhoverables.push(itemData);
273
- }
274
- catch(error) {
275
- console.log('ERROR itemizing unhoverable element');
276
- }
277
- }
278
- }
279
- }
280
- }
281
- // Process the remaining potential triggers.
282
- await find(data, withItems, page, sample.slice(1));
113
+ await page.reload({timeout: 5000});
283
114
  }
284
115
  catch(error) {
285
- console.log(`ERROR: Test quit when remaining sample size was ${sample.length}`);
286
- }
287
- }
288
- else {
289
- return Promise.resolve('');
290
- }
291
- };
292
- // Performs the hover test and reports results.
293
- exports.reporter = async (page, withItems, sampleSize = 20) => {
294
- // Initialize the result.
295
- let data = {
296
- sampling: {
297
- triggers: 0,
298
- triggerSample: 0,
299
- },
300
- totals: {
301
- impactTriggers: 0,
302
- additions: 0,
303
- removals: 0,
304
- opacityChanges: 0,
305
- opacityImpact: 0,
306
- unhoverables: 0,
307
- noCursors: 0,
308
- badCursors: 0,
309
- badIndicators: 0
116
+ console.log('ERROR: page reload timed out');
310
117
  }
311
- };
312
- // If details are to be reported:
313
- if (withItems) {
314
- // Add properties for details to the initialized result.
315
- data.items = {
316
- impactTriggers: [],
317
- unhoverables: [],
318
- noCursors: [],
319
- badCursors: [],
320
- badIndicators: []
321
- };
322
- }
323
- // Identify the triggers.
324
- const selectors = ['a', 'button', 'li:not([role=menuitem])', '[onmouseenter]', '[onmouseover]'];
325
- const triggers = await page.$$(selectors.map(selector => `body ${selector}:visible`).join(', '))
326
- .catch(error => {
327
- console.log(`ERROR getting hover triggers (${error.message})`);
328
- data.prevented = true;
329
- return [];
330
- });
331
- data.sampling.triggers = triggers.length;
332
- // Get the sample.
333
- const sample = getSample(triggers, sampleSize);
334
- data.sampling.triggerSample = sample.length;
335
- // Set a time limit to cover possible 2 seconds per trigger.
336
- const timeLimit = Math.round(2.8 * sample.length + 2);
337
- const timeout = setTimeout(async () => {
338
- await page.close();
339
- console.log(
340
- `ERROR: hover test on sample of ${sample.length} triggers timed out at ${timeLimit} seconds; page closed`
341
- );
342
- hasTimedOut = true;
343
- data = {
344
- prevented: true,
345
- error: 'ERROR: hover test timed out'
346
- };
347
- clearTimeout(timeout);
348
- }, 1000 * timeLimit);
349
- // Find and document the style defects and impacts of the sampled triggers.
350
- if (sample.length && ! hasTimedOut) {
351
- await find(data, withItems, page, sample);
352
- }
353
- clearTimeout(timeout);
354
- // Round the reported totals.
355
- if (! hasTimedOut) {
356
- Object.keys(data.totals).forEach(key => {
357
- data.totals[key] = Math.round(data.totals[key]);
358
- });
359
118
  }
360
- const severity = {
361
- impactTriggers: 3,
362
- additions: 1,
363
- removals: 2,
364
- opacityChanges: 1,
365
- opacityImpact: 0,
366
- unhoverables: 3,
367
- noCursors: 3,
368
- badCursors: 2,
369
- badIndicators: 2
370
- };
371
- const what = {
372
- impactTriggers: 'Hovering over the element has unexpected effects',
373
- unhoverables: 'Operable element cannot be hovered over',
374
- noCursors: 'Hoverable element hides the mouse cursor',
375
- badCursors: 'Link or button makes the hovering mouse cursor nonstandard',
376
- badIndicators: 'List item changes when hovered over'
377
- };
378
- const totals = [0, 0, 0, 0];
379
- Object.keys(data.totals).forEach(category => {
380
- totals[severity[category]] += data.totals[category];
381
- });
382
- const standardInstances = [];
383
- if (data.items) {
384
- Object.keys(data.items).forEach(category => {
385
- data.items[category].forEach(item => {
119
+ // If itemization is not required:
120
+ if (! withItems) {
121
+ // For each ordinal severity:
122
+ for (const index in totals) {
123
+ // If there were any instances with it:
124
+ if (totals[index]) {
125
+ // Add a summary instance to the result.
386
126
  standardInstances.push({
387
127
  ruleID: 'hover',
388
- what: what[category],
389
- ordinalSeverity: severity[category],
390
- tagName: item.tagName,
391
- id: item.id,
128
+ what: 'Hovering over elements changes the page or fails',
129
+ ordinalSeverity: index,
130
+ count: Math.round(totals[index]),
131
+ tagName: '',
132
+ id: '',
392
133
  location: {
393
134
  doc: '',
394
135
  type: '',
395
136
  spec: ''
396
137
  },
397
- excerpt: item.text
138
+ excerpt: ''
398
139
  });
399
- });
400
- });
401
- }
402
- else if (totals.some(total => total)) {
403
- standardInstances.push({
404
- ruleID: 'hover',
405
- what: 'Hovering behaves unexpectedly',
406
- ordinalSeverity: totals.reduce((max, current, index) => current ? index : max, 0),
407
- count: Object.values(data.totals).reduce((total, current) => total + current),
408
- tagName: '',
409
- id: '',
410
- location: {
411
- doc: '',
412
- type: '',
413
- spec: ''
414
- },
415
- excerpt: ''
416
- });
417
- }
418
- // Reload the page.
419
- try {
420
- await page.reload({timeout: 15000});
421
- }
422
- catch(error) {
423
- console.log('ERROR: page reload timed out');
140
+ }
141
+ }
424
142
  }
143
+ // Round the totals.
144
+ totals.forEach((total, index) => {
145
+ totals[index] = Math.round(totals[index]);
146
+ });
425
147
  // Return the result.
426
148
  return {
427
149
  data,
package/testaro/motion.js CHANGED
@@ -5,16 +5,16 @@
5
5
  by the time a user manages to stop motion, the motion may have caused annoyance or harm. For
6
6
  superior accessibility, a page contains no motion until and unless the user authorizes it. The
7
7
  test compares two screen shots of the viewport 2 seconds and 6 seconds after page load. It
8
- reports a rule violation if more than 1% of the pixels change. The larger the change fraction,
9
- the greater the ordinal severity.
8
+ reports a rule violation if any pixels change. The larger the change fraction, the greater the
9
+ ordinal severity.
10
10
 
11
- WARNING: This test uses the Playwright page.screenshot method, which produces incorrect results
12
- when the browser type is chromium and is not implemented for the firefox browser type. The only
13
- browser type usable with this test is webkit.
11
+ WARNING: This test uses the Playwright page.screenshot method, which is not implemented for the
12
+ firefox browser type.
14
13
  */
15
14
 
16
15
  // IMPORTS
17
16
 
17
+ // Module to get pixel changes between two times.
18
18
  const {visChange} = require('../procs/visChange');
19
19
 
20
20
  // FUNCTIONS
@@ -31,10 +31,10 @@ exports.reporter = async page => {
31
31
  });
32
32
  // If the screenshots succeeded:
33
33
  if (data.success) {
34
- // Get the ordinal severity from the fractional pixel change.
35
- const ordinalSeverity = Math.floor(Math.min(3, 0.4 * Math.sqrt(data.changePercent)));
36
34
  // If any pixels were changed:
37
35
  if (data.pixelChanges) {
36
+ // Get the ordinal severity from the fractional pixel change.
37
+ const ordinalSeverity = Math.floor(Math.min(3, 0.4 * Math.sqrt(data.changePercent)));
38
38
  // Add to the totals.
39
39
  totals[ordinalSeverity] = 1;
40
40
  // Get a summary standard instance.
package/tests/testaro.js CHANGED
@@ -22,6 +22,7 @@ const evalRules = {
22
22
  focOp: 'discrepancies between focusability and operability',
23
23
  focVis: 'links that are invisible when focused',
24
24
  hover: 'hover-caused content changes',
25
+ hovInd: 'hover indication nonstandard',
25
26
  labClash: 'labeling inconsistencies',
26
27
  lineHeight: 'text with a line height less than 1.5 times its font size',
27
28
  linkExt: 'links that automatically open new windows',