testaro 16.1.0 → 16.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "16.1.0",
3
+ "version": "16.2.0",
4
4
  "description": "Automation of accessibility testing",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -1,6 +1,7 @@
1
1
  /*
2
2
  visChange
3
- This procedure reports a change in the visible content of a page between two times.
3
+ This procedure reports a change in the visible content of a page between two times, optionally
4
+ hovering over a locator-defined element immediately after the first time.
4
5
  WARNING: This test uses the Playwright page.screenshot method, which produces incorrect results
5
6
  when the browser type is chromium and is not implemented for the firefox browser type. The only
6
7
  browser type usable with this test is webkit.
@@ -17,15 +18,15 @@ const {PNG} = require('pngjs');
17
18
  const shoot = async (page, exclusion = null) => {
18
19
  // Make a screenshot as a buffer.
19
20
  const options = {
20
- fullPage: false,
21
+ fullPage: true,
21
22
  omitBackground: true,
22
23
  timeout: 2000
23
24
  };
24
25
  if (exclusion) {
26
+ const exclusionText = await exclusion.textContent();
25
27
  options.mask = [exclusion];
26
28
  }
27
- return await page.screenshot({
28
- })
29
+ return await page.screenshot(options)
29
30
  .catch(error => {
30
31
  console.log(`ERROR: Screenshot failed (${error.message})`);
31
32
  return '';
@@ -37,10 +38,37 @@ exports.visChange = async (page, options = {}) => {
37
38
  if (delayBefore) {
38
39
  await page.waitForTimeout(delayBefore);
39
40
  }
40
- // Make a screenshot.
41
+ // If an exclusion was specified:
42
+ if (exclusion) {
43
+ // Hover over the upper-left corner of the page for test isolation.
44
+ const docLoc = page.locator('html');
45
+ await docLoc.hover({
46
+ position: {
47
+ x: 0,
48
+ y: 0
49
+ }
50
+ });
51
+ }
52
+ // Make a screenshot, excluding an element if specified.
41
53
  const shot0 = await shoot(page, exclusion);
42
54
  // If it succeeded:
43
55
  if (shot0.length) {
56
+ // If an exclusion was specified:
57
+ if (exclusion) {
58
+ // Hover over it.
59
+ try {
60
+ await exclusion.hover({
61
+ timeout: 500,
62
+ noWaitAfter: true
63
+ });
64
+ }
65
+ catch(error) {
66
+ return {
67
+ success: false,
68
+ error: 'Hovering failed'
69
+ };
70
+ }
71
+ }
44
72
  // Wait as specified, or 3 seconds.
45
73
  await page.waitForTimeout(delayBetween || 3000);
46
74
  // Make another screenshot.
@@ -54,7 +82,7 @@ exports.visChange = async (page, options = {}) => {
54
82
  // Get the count of differing pixels between the shots.
55
83
  const pixelChanges = pixelmatch(pngs[0].data, pngs[1].data, null, width, height);
56
84
  // Get the ratio of differing to all pixels as a percentage.
57
- const changePercent = Math.round(100 * pixelChanges / (width * height));
85
+ const changePercent = 100 * pixelChanges / (width * height);
58
86
  // Return this.
59
87
  return {
60
88
  success: true,
@@ -68,7 +96,8 @@ exports.visChange = async (page, options = {}) => {
68
96
  else {
69
97
  // Return this.
70
98
  return {
71
- success: false
99
+ success: false,
100
+ error: 'Second screenshot failed'
72
101
  };
73
102
  }
74
103
  }
@@ -76,7 +105,8 @@ exports.visChange = async (page, options = {}) => {
76
105
  else {
77
106
  // Return this.
78
107
  return {
79
- success: false
108
+ success: false,
109
+ error: 'First screenshot failed'
80
110
  };
81
111
  }
82
112
  };
package/testaro/hover.js CHANGED
@@ -1,427 +1,170 @@
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 get pixel changes between two times.
36
+ const {visChange} = require('../procs/visChange');
35
37
 
36
38
  // FUNCTIONS
37
39
 
38
40
  // Draws a location-weighted sample of triggers.
39
41
  const getSample = (population, sampleSize) => {
40
42
  const popSize = population.length;
43
+ // If the sample is smaller than the population:
41
44
  if (sampleSize < popSize) {
45
+ // Assign to each trigger a priority randomly decreasing with its index.
42
46
  const WeightedPopulation = population.map((trigger, index) => {
43
47
  const weight = 1 + Math.sin(Math.PI * index / popSize + Math.PI / 2);
44
48
  const priority = weight * Math.random();
45
- return [trigger, index, priority];
49
+ return [index, priority];
46
50
  });
47
- const sortedPopulation = WeightedPopulation.sort((a, b) => b[2] - a[2]);
51
+ // Return the indexes of the triggers with the highest priorities.
52
+ const sortedPopulation = WeightedPopulation.sort((a, b) => b[1] - a[1]);
48
53
  const sample = sortedPopulation.slice(0, sampleSize);
49
- const domOrderSample = sample.sort((a, b) => a[1] - b[1]);
54
+ const domOrderSample = sample.sort((a, b) => a[0] - b[0]);
50
55
  return domOrderSample.map(trigger => trigger[0]);
51
56
  }
57
+ // Otherwise, i.e. if the sample is at least as large as the population:
52
58
  else {
53
- return population;
59
+ // Return the population indexes.
60
+ return population.map((trigger, index) => index);
54
61
  }
55
62
  };
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
- };
63
+ // Performs the hover test and reports results.
64
+ exports.reporter = async (page, withItems, sampleSize = 20) => {
65
+ // Initialize the result.
66
+ const data = {};
67
+ const totals = [0, 0, 0, 0];
68
+ const standardInstances = [];
69
+ // Identify the triggers.
70
+ const selectors = ['a', 'button', 'li:not([role=menuitem])', '[onmouseenter]', '[onmouseover]'];
71
+ const selectorString = selectors.map(selector => `body ${selector}:visible`).join(', ');
72
+ const locAll = page.locator(selectorString);
73
+ const locsAll = await locAll.all();
74
+ // Get the population-to-sample ratio.
75
+ const psRatio = Math.max(1, locsAll.length / sampleSize);
76
+ // Get a sample of the triggers.
77
+ const sampleIndexes = getSample(locsAll, sampleSize);
78
+ const sample = locsAll.filter((loc, index) => sampleIndexes.includes(index));
79
+ // For each trigger in the sample:
80
+ for (const loc of sample) {
81
+ // Hover over it and get the fractional pixel change.
82
+ const hoverData = await visChange(page, {
83
+ delayBefore: 0,
84
+ delayBetween: 500,
85
+ exclusion: loc
86
+ });
87
+ // If the hovering and measurement succeeded:
88
+ if (hoverData.success) {
89
+ // If any pixels changed:
90
+ if (hoverData.changePercent) {
91
+ // Get the ordinal severity from the fractional pixel change.
92
+ const ordinalSeverity = Math.floor(Math.min(3, 0.4 * Math.sqrt(hoverData.changePercent)));
93
+ // Add to the totals.
94
+ totals[ordinalSeverity] += 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: 'Hovering over the element changes the page',
103
+ ordinalSeverity,
104
+ tagName: elData.tagName,
105
+ id: elData.id,
106
+ location: elData.location,
107
+ excerpt: elData.excerpt
108
+ });
109
+ }
110
+ }
108
111
  }
109
- // Otherwise, i.e. if there are no impacts:
112
+ // Otherwise, i.e. if hovering and measurement failed:
110
113
  else {
111
- // Try again.
112
- return await new Promise(resolve => {
113
- setTimeout(() => {
114
- resolve(getImpacts(interval, triesLeft, root, page, preDescendants, preOpacities));
115
- }, interval);
116
- });
114
+ // Add to the totals.
115
+ totals[3] += psRatio;
116
+ // If itemization is required:
117
+ if (withItems) {
118
+ // Get data on the trigger.
119
+ const elData = await getLocatorData(loc);
120
+ // Add an instance to the result.
121
+ standardInstances.push({
122
+ ruleID: 'hover',
123
+ what: 'Element is not hoverable',
124
+ ordinalSeverity: 3,
125
+ tagName: elData.tagName,
126
+ id: elData.id,
127
+ location: elData.location,
128
+ excerpt: elData.excerpt
129
+ });
130
+ }
117
131
  }
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.
132
+ // Reload the page to preserve locator integrity.
142
133
  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));
134
+ await page.reload({timeout: 5000});
283
135
  }
284
136
  catch(error) {
285
- console.log(`ERROR: Test quit when remaining sample size was ${sample.length}`);
137
+ console.log('ERROR: page reload timed out');
286
138
  }
287
139
  }
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
310
- }
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
- }
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 => {
140
+ // If itemization is not required:
141
+ if (! withItems) {
142
+ // For each ordinal severity:
143
+ for (const index in totals) {
144
+ // If there were any instances with it:
145
+ if (totals[index]) {
146
+ // Add a summary instance to the result.
386
147
  standardInstances.push({
387
148
  ruleID: 'hover',
388
- what: what[category],
389
- ordinalSeverity: severity[category],
390
- tagName: item.tagName,
391
- id: item.id,
149
+ what: 'Hovering over elements changes the page or fails',
150
+ ordinalSeverity: index,
151
+ count: Math.round(totals[index]),
152
+ tagName: '',
153
+ id: '',
392
154
  location: {
393
155
  doc: '',
394
156
  type: '',
395
157
  spec: ''
396
158
  },
397
- excerpt: item.text
159
+ excerpt: ''
398
160
  });
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');
161
+ }
162
+ }
424
163
  }
164
+ // Round the totals.
165
+ totals.forEach((total, index) => {
166
+ totals[index] = Math.round(totals[index]);
167
+ });
425
168
  // Return the result.
426
169
  return {
427
170
  data,