testaro 59.2.5 → 59.2.7

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": "59.2.5",
3
+ "version": "59.2.7",
4
4
  "description": "Run 1000 web accessibility tests from 11 tools and get a standardized report",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -611,24 +611,16 @@ const convert = (toolName, data, result, standardResult) => {
611
611
  rules.forEach(rule => {
612
612
  // Copy its instances to the standard result.
613
613
  const ruleResult = result[rule];
614
- if (ruleResult.standardInstances) {
615
- standardResult.instances.push(... ruleResult.standardInstances);
616
- }
617
- else {
618
- console.log(`ERROR: Testaro rule ${rule} result has no standardInstances property`);
619
- }
614
+ ruleResult.standardInstances ??= [];
615
+ standardResult.instances.push(... ruleResult.standardInstances);
620
616
  // Initialize a record of its sample-ratio-weighted totals.
621
617
  data.ruleTotals[rule] = [0, 0, 0, 0];
622
618
  // Add those totals to the record and to the standard result.
623
- if (ruleResult.totals) {
624
- for (const index in ruleResult.totals) {
625
- const ruleTotal = ruleResult.totals[index];
626
- data.ruleTotals[rule][index] += ruleTotal;
627
- standardResult.totals[index] += ruleTotal;
628
- }
629
- }
630
- else {
631
- console.log(`ERROR: Testaro rule ${rule} result has no totals property`);
619
+ ruleResult.totals ??= [0, 0, 0, 0];
620
+ for (const index in ruleResult.totals) {
621
+ const ruleTotal = ruleResult.totals[index];
622
+ data.ruleTotals[rule][index] += ruleTotal;
623
+ standardResult.totals[index] += ruleTotal;
632
624
  }
633
625
  });
634
626
  const preventionCount = result.preventions && result.preventions.length;
package/run.js CHANGED
@@ -205,7 +205,9 @@ const goTo = async (report, page, url, timeout, waitUntil) => {
205
205
  }
206
206
  }
207
207
  catch(error) {
208
- console.log(`ERROR visiting ${url} (${error.message.slice(0, 200)})`);
208
+ if (debug) {
209
+ console.log(`ERROR visiting ${url} (${error.message.slice(0, 200)})`);
210
+ }
209
211
  return {
210
212
  success: false,
211
213
  error: 'noVisit'
@@ -258,7 +260,9 @@ const addError = (alsoLog, alsoAbort, report, actIndex, message) => {
258
260
  }
259
261
  };
260
262
  // Launches a browser and navigates to a URL.
261
- const launch = exports.launch = async (report, debug, waits, tempBrowserID, tempURL) => {
263
+ const launch = exports.launch = async (
264
+ report, debug, waits, tempBrowserID, tempURL, retries = 2
265
+ ) => {
262
266
  const act = report.acts[actIndex];
263
267
  const {device} = report;
264
268
  const deviceID = device && device.id;
@@ -276,22 +280,21 @@ const launch = exports.launch = async (report, debug, waits, tempBrowserID, temp
276
280
  const browserOptions = {
277
281
  logger: {
278
282
  isEnabled: () => false,
279
- log: (name, severity, message) => console.log(message.slice(0, 100))
280
- }
283
+ log: (name, severity, message) => {
284
+ if (['warning', 'error'].includes(severity)) {
285
+ console.log(`${severity.toUpperCase()}: ${message.slice(0, 200)}`);
286
+ }
287
+ }
288
+ },
289
+ headless: ! debug,
290
+ slowMo: waits || 0,
291
+ args: ['--disable-dev-shm-usage']
281
292
  };
282
- browserOptions.headless = ! debug;
283
- browserOptions.slowMo = waits || 0;
284
293
  try {
285
294
  // Replace the browser with a new one.
286
295
  browser = await browserType.launch(browserOptions);
287
- // Open a context (i.e. browser window).
288
- const browserContext = await browser.newContext(device.windowOptions);
289
- // Create a diagnostic listener for its unintentional closing.
290
- browserContext.on('close', () => {
291
- if (! browserCloseIntentional) {
292
- console.log('ERROR: Browser context unexpectedly closed');
293
- }
294
- });
296
+ // Redefine the context (i.e. browser window).
297
+ browserContext = await browser.newContext(device.windowOptions);
295
298
  // Prevent default timeouts.
296
299
  browserContext.setDefaultTimeout(0);
297
300
  // When a page (i.e. browser tab) is added to the browser context (i.e. browser window):
@@ -380,18 +383,38 @@ const launch = exports.launch = async (report, debug, waits, tempBrowserID, temp
380
383
  // Report this.
381
384
  addError(true, false, report, actIndex, 'status429');
382
385
  }
383
- // Otherwise, i.e. if the launch or navigation failed:
386
+ // Otherwise, i.e. if the launch or navigation failed for another reason:
384
387
  else {
385
- // Report this.
386
- addError(true, false, report, actIndex, `ERROR: Launch failed (${navResult.error})`);
387
- page = null;
388
+ // Cause another attempt to launch and navigate, if retries remain.
389
+ throw new Error(`Navigation failed (${navResult.error})`);
388
390
  }
389
391
  }
390
392
  // If an error occurred:
391
393
  catch(error) {
392
- // Report this.
393
- addError(true, false, report, actIndex, `ERROR launching or navigating ${error.message}`);
394
- page = null;
394
+ // If retries remain:
395
+ if (retries > 0) {
396
+ console.log(`WARNING: Retrying launch (${retries} retries left)`);
397
+ await wait(2000);
398
+ return launch(report, debug, waits, tempBrowserID, tempURL, retries - 1);
399
+ }
400
+ // Otherwise, i.e. if no retries remain:
401
+ else {
402
+ // Report this.
403
+ addError(
404
+ true, false, report, actIndex, `FINAL ERROR launching or navigating (${error.message})`
405
+ );
406
+ // If the browser was created, and thus not a context of it:
407
+ if (browser) {
408
+ // Report this.
409
+ console.log('ERROR: Browser was created but context creation failed');
410
+ // Close the browser.
411
+ await browser.close().catch(() => {
412
+ console.log('ERROR: Could not close browser after context creation failure');
413
+ });
414
+ browser = null;
415
+ }
416
+ page = null;
417
+ }
395
418
  };
396
419
  }
397
420
  // Otherwise, i.e. if the browser or device ID is invalid:
package/testaro/hovInd.js CHANGED
@@ -140,113 +140,104 @@ exports.reporter = async (page, withItems, sampleSize = 20) => {
140
140
  const sample = locsAll.filter((loc, index) => sampleIndexes.includes(index));
141
141
  // For each trigger in the sample:
142
142
  for (const loc of sample) {
143
- // Get its style properties.
144
- const preStyles = await getHoverStyles(loc);
145
- // Try to focus it.
146
143
  try {
144
+ // Get its style properties.
145
+ const preStyles = await getHoverStyles(loc);
146
+ // Focus it.
147
147
  await loc.focus({timeout: 500});
148
148
  // If focusing succeeds, get its style properties.
149
149
  const focStyles = await getHoverStyles(loc);
150
- // Try to blur it.
151
- try {
152
- await loc.blur({timeout: 500});
153
- // If blurring succeeds, try to hover over it.
154
- try {
155
- await loc.hover({timeout: 500});
156
- // If hovering succeeds, get its style properties.
157
- const hovStyles = await getHoverStyles(loc);
158
- // If all 3 style declarations belong to the same element:
159
- if ([focStyles, hovStyles].every(style => style.code === preStyles.code)) {
160
- // Get data on the element if itemization is required.
161
- const elData = withItems ? await getLocatorData(loc) : null;
162
- // If the hover cursor is nonstandard:
163
- const cursorData = getCursorData(hovStyles);
164
- if (! cursorData.ok) {
165
- // Add to the totals.
166
- totals[2] += psRatio;
167
- data.typeTotals.badCursor += psRatio;
168
- // If itemization is required:
169
- if (withItems) {
170
- // Add an instance to the result.
171
- standardInstances.push({
172
- ruleID: 'hovInd',
173
- what: `Element has a nonstandard hover cursor (${cursorData.cursor})`,
174
- ordinalSeverity: 2,
175
- tagName: elData.tagName,
176
- id: elData.id,
177
- location: elData.location,
178
- excerpt: elData.excerpt
179
- });
180
- }
181
- }
182
- // If the element is a button and the hover and default states are not distinct:
183
- if (hovStyles.tagName === 'BUTTON' && areAlike(preStyles, hovStyles)) {
184
- // Add to the totals.
185
- totals[1] += psRatio;
186
- data.typeTotals.hoverLikeDefault += psRatio;
187
- // If itemization is required:
188
- if (withItems) {
189
- // Add an instance to the result.
190
- standardInstances.push({
191
- ruleID: 'hovInd',
192
- what: 'Element border, outline, color, and background color do not change when hovered over',
193
- ordinalSeverity: 1,
194
- tagName: elData.tagName,
195
- id: elData.id,
196
- location: elData.location,
197
- excerpt: elData.excerpt
198
- });
199
- }
200
- }
201
- // If the hover and focus states are indistinct but differ from the default state:
202
- if (areAlike(hovStyles, focStyles) && ! areAlike(hovStyles, preStyles)) {
203
- // Add to the totals.
204
- totals[1] += psRatio;
205
- data.typeTotals.hoverLikeFocus += psRatio;
206
- // If itemization is required:
207
- if (withItems) {
208
- // Add an instance to the result.
209
- standardInstances.push({
210
- ruleID: 'hovInd',
211
- what: 'Element border, outline, color, and background color are alike on hover and focus',
212
- ordinalSeverity: 1,
213
- tagName: elData.tagName,
214
- id: elData.id,
215
- location: elData.location,
216
- excerpt: elData.excerpt
217
- });
218
- }
219
- }
150
+ // Blur it.
151
+ await loc.blur({timeout: 500});
152
+ // If blurring succeeds, try to hover over it.
153
+ await loc.hover({timeout: 500});
154
+ // If hovering succeeds, get its style properties.
155
+ const hovStyles = await getHoverStyles(loc);
156
+ // If all 3 style declarations belong to the same element:
157
+ if ([focStyles, hovStyles].every(style => style.code === preStyles.code)) {
158
+ // Get data on the element if itemization is required.
159
+ const elData = withItems ? await getLocatorData(loc) : null;
160
+ // If the hover cursor is nonstandard:
161
+ const cursorData = getCursorData(hovStyles);
162
+ if (! cursorData.ok) {
163
+ // Add to the totals.
164
+ totals[2] += psRatio;
165
+ data.typeTotals.badCursor += psRatio;
166
+ // If itemization is required:
167
+ if (withItems) {
168
+ // Add an instance to the result.
169
+ standardInstances.push({
170
+ ruleID: 'hovInd',
171
+ what: `Element has a nonstandard hover cursor (${cursorData.cursor})`,
172
+ ordinalSeverity: 2,
173
+ tagName: elData.tagName,
174
+ id: elData.id,
175
+ location: elData.location,
176
+ excerpt: elData.excerpt
177
+ });
220
178
  }
221
- // Otherwise, i.e. if the style properties do not all belong to the same element:
222
- else {
223
- // Report this and quit.
224
- data.prevented = true;
225
- data.error = 'ERROR: Page changes on focus or hover prevent test';
226
- break;
179
+ }
180
+ // If the element is a button and the hover and default states are not distinct:
181
+ if (hovStyles.tagName === 'BUTTON' && areAlike(preStyles, hovStyles)) {
182
+ // Add to the totals.
183
+ totals[1] += psRatio;
184
+ data.typeTotals.hoverLikeDefault += psRatio;
185
+ // If itemization is required:
186
+ if (withItems) {
187
+ // Add an instance to the result.
188
+ standardInstances.push({
189
+ ruleID: 'hovInd',
190
+ what: 'Element border, outline, color, and background color do not change when hovered over',
191
+ ordinalSeverity: 1,
192
+ tagName: elData.tagName,
193
+ id: elData.id,
194
+ location: elData.location,
195
+ excerpt: elData.excerpt
196
+ });
227
197
  }
228
198
  }
229
- // If hovering fails:
230
- catch(error) {
231
- // Report this.
232
- data.prevented = true;
233
- data.error = 'ERROR: Hovering failed';
234
- break;
199
+ // If the hover and focus states are indistinct but differ from the default state:
200
+ if (areAlike(hovStyles, focStyles) && ! areAlike(hovStyles, preStyles)) {
201
+ // Add to the totals.
202
+ totals[1] += psRatio;
203
+ data.typeTotals.hoverLikeFocus += psRatio;
204
+ // If itemization is required:
205
+ if (withItems) {
206
+ // Add an instance to the result.
207
+ standardInstances.push({
208
+ ruleID: 'hovInd',
209
+ what: 'Element border, outline, color, and background color are alike on hover and focus',
210
+ ordinalSeverity: 1,
211
+ tagName: elData.tagName,
212
+ id: elData.id,
213
+ location: elData.location,
214
+ excerpt: elData.excerpt
215
+ });
216
+ }
235
217
  }
236
218
  }
237
- // If blurring fails:
238
- catch(error) {
239
- // Report this.
219
+ // Otherwise, i.e. if the style properties do not all belong to the same element:
220
+ else {
221
+ // Report this and quit.
240
222
  data.prevented = true;
241
- data.error = 'ERROR: Blurring failed';
223
+ data.error = 'ERROR: Page changes on focus or hover prevent test';
242
224
  break;
243
225
  }
244
226
  }
245
- // If focusing fails:
246
227
  catch(error) {
247
- // Report this.
228
+ // If the page closed:
229
+ if (
230
+ ['Target page', 'detached', 'null', 'closed'].some(string => error.message.includes(string))
231
+ ) {
232
+ data.error = `ERROR during hovInd test: ${error.message}`;
233
+ }
234
+ else {
235
+ const elementText = loc ? await loc.textContent({timeout: 200}) : '';
236
+ const excerpt = elementText ? elementText.trim().slice(0, 100) : '<no text>';
237
+ data.error = `ERROR manipulating element (${excerpt}) during hovInd test`;
238
+ }
248
239
  data.prevented = true;
249
- data.error = 'ERROR: Focusing failed';
240
+ // Abort this test.
250
241
  break;
251
242
  }
252
243
  }
@@ -34,12 +34,10 @@ exports.reporter = async (page, withItems) => {
34
34
  const ruleData = {
35
35
  ruleID: 'imageLink',
36
36
  selector: 'a[href]',
37
- pruner: async (loc) => {
38
- return loc.evaluate(el => {
39
- const href = el.getAttribute('href') || '';
40
- return /\.(?:png|jpe?g|gif|svg|webp|ico)(?:$|[?#])/i.test(href);
41
- });
42
- },
37
+ pruner: async loc => await loc.evaluate(el => {
38
+ const href = el.getAttribute('href') || '';
39
+ return /\.(?:png|jpe?g|gif|svg|webp|ico)(?:$|[?#])/i.test(href);
40
+ }),
43
41
  complaints: {
44
42
  instance: 'Link destination is an image file',
45
43
  summary: 'Links have image files as their destinations'
@@ -34,20 +34,18 @@ exports.reporter = async (page, withItems) => {
34
34
  const ruleData = {
35
35
  ruleID: 'legendLoc',
36
36
  selector: 'legend',
37
- pruner: async (loc) => {
38
- return loc.evaluate(el => {
39
- const parent = el.parentElement;
40
- if (!parent) return true;
41
- if (parent.tagName.toUpperCase() !== 'FIELDSET') return true;
42
- // Check if this legend is the first element child of the fieldset
43
- for (const child of parent.children) {
44
- if (child.nodeType === 1) {
45
- return child !== el; // true if not first child
46
- }
37
+ pruner: async (loc) => await loc.evaluate(el => {
38
+ const parent = el.parentElement;
39
+ if (!parent) return true;
40
+ if (parent.tagName.toUpperCase() !== 'FIELDSET') return true;
41
+ // Check if this legend is the first element child of the fieldset
42
+ for (const child of parent.children) {
43
+ if (child.nodeType === 1) {
44
+ return child !== el; // true if not first child
47
45
  }
48
- return true;
49
- });
50
- },
46
+ }
47
+ return true;
48
+ }),
51
49
  complaints: {
52
50
  instance: 'Element is not the first child of a fieldset element',
53
51
  summary: 'legend elements are not the first children of fieldset elements'
@@ -1,5 +1,6 @@
1
1
  /*
2
2
  © 2023–2024 CVS Health and/or one of its affiliates. All rights reserved.
3
+ © 2025 Jonathan Robert Pool. All rights reserved.
3
4
 
4
5
  MIT License
5
6
 
@@ -31,10 +32,12 @@
31
32
  their subtrees are excluded.
32
33
  */
33
34
 
35
+ // IMPORTS
36
+
34
37
  // Module to perform common operations.
35
38
  const {init, report} = require('../procs/testaro');
36
39
 
37
- // ########## FUNCTIONS
40
+ // FUNCTIONS
38
41
 
39
42
  // Runs the test and returns the result.
40
43
  exports.reporter = async (page, withItems) => {
@@ -34,11 +34,9 @@ exports.reporter = async (page, withItems) => {
34
34
  const ruleData = {
35
35
  ruleID: 'optRoleSel',
36
36
  selector: '[role="option"]',
37
- pruner: async (loc) => {
38
- return loc.evaluate(el => {
39
- return ! el.hasAttribute('aria-selected');
40
- });
41
- },
37
+ pruner: async (loc) => await loc.evaluate(el => {
38
+ return ! el.hasAttribute('aria-selected');
39
+ }),
42
40
  complaints: {
43
41
  instance: 'Element has an explicit option role but no aria-selected attribute',
44
42
  summary: 'Elements with explicit option roles have no aria-selected attributes'
package/tests/testaro.js CHANGED
@@ -28,7 +28,7 @@
28
28
  This test implements the Testaro evaluative rules.
29
29
  */
30
30
 
31
- // ######## IMPORTS
31
+ // IMPORTS
32
32
 
33
33
  // Module to perform common operations.
34
34
  const {init, report} = require('../procs/testaro');
@@ -37,7 +37,7 @@ const {launch} = require('../run');
37
37
  // Module to handle files.
38
38
  const fs = require('fs/promises');
39
39
 
40
- // ######## CONSTANTS
40
+ // CONSTANTS
41
41
 
42
42
  // The validation job data for the tests listed below are in the pending directory.
43
43
  /*
@@ -146,12 +146,18 @@ const slowTestLimits = {
146
146
  hovInd: 10,
147
147
  labClash: 10,
148
148
  lineHeight: 10,
149
+ linkUl: 10,
149
150
  motion: 15,
150
151
  opFoc: 10,
151
152
  tabNav: 10,
152
153
  textSem: 10
153
154
  };
154
155
 
156
+ // ERROR HANDLER
157
+ process.on('unhandledRejection', reason => {
158
+ console.error(`ERROR: Unhandled Promise Rejection (${reason})`);
159
+ });
160
+
155
161
  // ######## FUNCTIONS
156
162
 
157
163
  // Conducts a JSON-defined test.
@@ -182,6 +188,7 @@ const wait = ms => {
182
188
  };
183
189
  // Conducts and reports Testaro tests.
184
190
  exports.reporter = async (page, report, actIndex) => {
191
+ // Report page crashes.
185
192
  const url = await page.url();
186
193
  const act = report.acts[actIndex];
187
194
  const {args, stopOnFail, withItems} = act;
@@ -216,17 +223,15 @@ exports.reporter = async (page, report, actIndex) => {
216
223
  let calledRules = rules[0] === 'y'
217
224
  ? rules.slice(1)
218
225
  : Object.keys(evalRules).filter(ruleID => ! rules.slice(1).includes(ruleID));
219
- const calledContaminators = calledRules.filter(rule => contaminators.includes(rule)).sort();
226
+ const calledContaminators = calledRules.filter(rule => contaminators.includes(rule));
220
227
  const firstCalledContaminator = calledContaminators[0];
221
- const calledBenignRules = calledRules.filter(rule => ! contaminators.includes(rule)).sort();
228
+ const calledBenignRules = calledRules.filter(rule => ! contaminators.includes(rule));
222
229
  const testTimes = [];
223
230
  let contaminatorsStarted = false;
224
231
  // Starting with the noncontaminators, for each rule invoked:
225
232
  for (const rule of calledBenignRules.concat(calledContaminators)) {
226
- console.log(`Starting rule ${rule}`);
227
- if (rule === firstCalledContaminator) {
228
- console.log(' It is the first contaminator');
229
- }
233
+ const contaminatorSuffix = rule === firstCalledContaminator ? ' (first contaminator)' : '';
234
+ console.log(`Starting rule ${rule}${contaminatorSuffix}`);
230
235
  const pageClosed = page ? page.isClosed() : true;
231
236
  const isContaminator = contaminators.includes(rule);
232
237
  // If it is a contaminator other than the first one or the page has closed:
@@ -250,6 +255,22 @@ exports.reporter = async (page, report, actIndex) => {
250
255
  if (isContaminator) {
251
256
  contaminatorsStarted = true;
252
257
  }
258
+ // Report crashes and disconnections during this test.
259
+ let crashHandler;
260
+ let disconnectHandler;
261
+ const {browser} = require('../run');
262
+ if (page && ! page.isClosed()) {
263
+ crashHandler = () => {
264
+ console.log(`ERROR: Page crashed during ${rule} test`);
265
+ };
266
+ page.on('crash', crashHandler);
267
+ }
268
+ if (browser) {
269
+ disconnectHandler = () => {
270
+ console.log(`ERROR: Browser disconnected during ${rule} test`);
271
+ };
272
+ browser.on('disconnected', disconnectHandler);
273
+ }
253
274
  // Initialize an argument array.
254
275
  const ruleArgs = [page, withItems];
255
276
  const ruleFileNames = await fs.readdir(`${__dirname}/../testaro`);
@@ -269,52 +290,103 @@ exports.reporter = async (page, report, actIndex) => {
269
290
  const what = evalRules[rule] || etcRules[rule];
270
291
  result[rule].what = what;
271
292
  const startTime = Date.now();
272
- try {
273
- // Apply a time limit to the test.
274
- const timeLimit = 1000 * (slowTestLimits[rule] ?? 5);
275
- let timeout;
276
- // If the time limit expires during the test:
277
- const timer = new Promise(resolve => {
278
- timeout = setTimeout(() => {
279
- // Add data about the test, including its prevention, to the result.
293
+ let timeout;
294
+ let testRetries = 2;
295
+ let testSuccess = false;
296
+ while (testRetries > 0 && ! testSuccess) {
297
+ try {
298
+ // Apply a time limit to the test.
299
+ const timeLimit = 1000 * (slowTestLimits[rule] ?? 5);
300
+ // If the time limit expires during the test:
301
+ const timer = new Promise(resolve => {
302
+ timeout = setTimeout(() => {
303
+ // Add data about the test, including its prevention, to the result.
304
+ const endTime = Date.now();
305
+ testTimes.push([rule, Math.round((endTime - startTime) / 1000)]);
306
+ data.rulePreventions.push(rule);
307
+ result[rule].totals = [0, 0, 0, 0];
308
+ result[rule].standardInstances = [];
309
+ console.log(`ERROR: Test of testaro rule ${rule} timed out`);
310
+ resolve({timedOut: true});
311
+ }, timeLimit);
312
+ });
313
+ // Perform the test, subject to the time limit.
314
+ const ruleReport = isJS
315
+ ? require(`../testaro/${rule}`).reporter(... ruleArgs)
316
+ : jsonTest(rule, ruleArgs);
317
+ // Get the test result or a timeout result.
318
+ const ruleOrTimeoutReport = await Promise.race([timer, ruleReport]);
319
+ // If the test was completed:
320
+ if (! ruleOrTimeoutReport.timedOut) {
321
+ // Add data from the test to the result.
280
322
  const endTime = Date.now();
281
323
  testTimes.push([rule, Math.round((endTime - startTime) / 1000)]);
324
+ Object.keys(ruleOrTimeoutReport).forEach(key => {
325
+ result[rule][key] = ruleOrTimeoutReport[key];
326
+ });
327
+ result[rule].totals = result[rule].totals.map(total => Math.round(total));
328
+ // Prevent a retry of the test.
329
+ testSuccess = true;
330
+ // If testing is to stop after a failure and the page failed the test:
331
+ if (stopOnFail && ruleOrTimeoutReport.totals.some(total => total)) {
332
+ // Stop testing.
333
+ break;
334
+ }
335
+ }
336
+ }
337
+ // If an error is thrown by the test:
338
+ catch(error) {
339
+ const isPageClosed = ['closed', 'Protocol error', 'Target page'].some(phrase =>
340
+ error.message.includes(phrase)
341
+ );
342
+ // If the page has closed and there are retries left:
343
+ if (isPageClosed && testRetries) {
344
+ // Report this and decrement the allowed retry count.
345
+ console.log(
346
+ `WARNING: Retry ${3 - testRetries--} of test ${rule} starting after page closed`
347
+ );
348
+ await wait(2000);
349
+ // Replace the browser and the page and navigate to the target.
350
+ await launch(
351
+ report,
352
+ process.env.DEBUG === 'true',
353
+ Number.parseInt(process.env.WAITS) || 0,
354
+ report.browserID,
355
+ url
356
+ );
357
+ page = require('../run').page;
358
+ // If the page replacement failed:
359
+ if (! page) {
360
+ // Report this.
361
+ console.log(`ERROR: Browser relaunch to retry test ${rule} failed`);
362
+ data.rulePreventions.push(rule);
363
+ data.rulePreventionMessages[rule] = 'Retry failure due to browser relaunch failure';
364
+ // Stop retrying the test.
365
+ break;
366
+ }
367
+ // Update the rule arguments with the current page.
368
+ ruleArgs[0] = page;
369
+ }
370
+ // Otherwise, i.e. if the page is open or it is closed but no retries are left:
371
+ else {
372
+ // Treat the test as prevented.
282
373
  data.rulePreventions.push(rule);
283
- result[rule].totals = [0, 0, 0, 0];
284
- result[rule].standardInstances = [];
285
- console.log(`ERROR: Test of testaro rule ${rule} timed out`);
286
- resolve({timedOut: true});
287
- }, timeLimit);
288
- });
289
- // Perform the test, subject to the time limit.
290
- const ruleReport = isJS
291
- ? require(`../testaro/${rule}`).reporter(... ruleArgs)
292
- : jsonTest(rule, ruleArgs);
293
- // Get the test result or a timeout result.
294
- const ruleOrTimeoutReport = await Promise.race([timer, ruleReport]);
295
- clearTimeout(timeout);
296
- // If the test was completed:
297
- if (! ruleOrTimeoutReport.timedOut) {
298
- // Add data from the test to the result.
299
- const endTime = Date.now();
300
- testTimes.push([rule, Math.round((endTime - startTime) / 1000)]);
301
- Object.keys(ruleOrTimeoutReport).forEach(key => {
302
- result[rule][key] = ruleOrTimeoutReport[key];
303
- });
304
- result[rule].totals = result[rule].totals.map(total => Math.round(total));
305
- // If testing is to stop after a failure and the page failed the test:
306
- if (stopOnFail && ruleOrTimeoutReport.totals.some(total => total)) {
307
- // Stop testing.
374
+ data.rulePreventionMessages[rule] = error.message;
375
+ console.log(`ERROR: Test of testaro rule ${rule} prevented (${error.message})`);
376
+ // Do not retry the test even if retries are left.
308
377
  break;
309
378
  }
310
379
  }
311
- }
312
- // If an error is thrown by the test:
313
- catch(error) {
314
- // Report the error.
315
- data.rulePreventions.push(rule);
316
- data.rulePreventionMessages[rule] = error.message;
317
- console.log(`ERROR: Test of testaro rule ${rule} prevented (${error.message})`);
380
+ finally {
381
+ // Clear the timeout and the error listeners.
382
+ clearTimeout(timeout);
383
+ if (page && ! page.isClosed() && crashHandler) {
384
+ page.off('crash', crashHandler);
385
+ }
386
+ if (browser && disconnectHandler) {
387
+ browser.off('disconnected', disconnectHandler);
388
+ }
389
+ }
318
390
  }
319
391
  }
320
392
  // Otherwise, i.e. if the rule is undefined or doubly defined:
@@ -322,6 +394,10 @@ exports.reporter = async (page, report, actIndex) => {
322
394
  // Report this.
323
395
  data.rulesInvalid.push(rule);
324
396
  console.log(`ERROR: Rule ${rule} not validly defined`);
397
+ // Clear the crash listener.
398
+ if (page && ! page.isClosed()) {
399
+ page.off('crash', crashHandler);
400
+ }
325
401
  }
326
402
  }
327
403
  // Record the test times in descending order.