testaro 60.2.1 → 60.4.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/tests/testaro.js CHANGED
@@ -31,7 +31,7 @@
31
31
  // IMPORTS
32
32
 
33
33
  // Module to perform common operations.
34
- const {init, report} = require('../procs/testaro');
34
+ const {init, getRuleResult} = require('../procs/testaro');
35
35
  // Function to launch a browser.
36
36
  const {launch} = require('../run');
37
37
  // Module to handle files.
@@ -39,100 +39,415 @@ const fs = require('fs/promises');
39
39
 
40
40
  // CONSTANTS
41
41
 
42
- // The validation job data for the tests listed below are in the pending directory.
43
- const futureRules = new Set([]);
44
- const evalRules = {
45
- adbID: 'elements with ambiguous or missing referenced descriptions',
46
- allCaps: 'leaf elements with entirely upper-case text longer than 7 characters',
47
- allHidden: 'page that is entirely or mostly hidden',
48
- allSlanted: 'leaf elements with entirely italic or oblique text longer than 39 characters',
49
- altScheme: 'img elements with alt attributes having URLs as their entire values',
50
- attVal: 'duplicate attribute values',
51
- autocomplete: 'name and email inputs without autocomplete attributes',
52
- bulk: 'large count of visible elements',
53
- buttonMenu: 'nonstandard keyboard navigation between items of button-controlled menus',
54
- captionLoc: 'caption elements that are not first children of table elements',
55
- datalistRef: 'elements with ambiguous or missing referenced datalist elements',
56
- distortion: 'distorted text',
57
- docType: 'document without a doctype property',
58
- dupAtt: 'elements with duplicate attributes',
59
- embAc: 'active elements embedded in links or buttons',
60
- focAll: 'discrepancies between focusable and Tab-focused elements',
61
- focInd: 'missing and nonstandard focus indicators',
62
- focOp: 'Tab-focusable elements that are not operable',
63
- focVis: 'links that are not entirely visible when focused',
64
- headEl: 'invalid elements within the head',
65
- headingAmb: 'same-level sibling headings with identical texts',
66
- hover: 'hover-caused content changes',
67
- hovInd: 'hover indication nonstandard',
68
- hr: 'hr element instead of styles used for vertical segmentation',
69
- imageLink: 'links with image files as their destinations',
70
- labClash: 'labeling inconsistencies',
71
- legendLoc: 'legend elements that are not first children of fieldset elements',
72
- lineHeight: 'text with a line height less than 1.5 times its font size',
73
- linkAmb: 'links with identical texts but different destinations',
74
- linkExt: 'links that automatically open new windows',
75
- linkOldAtt: 'links with deprecated attributes',
76
- linkTitle: 'links with title attributes repeating text content',
77
- linkTo: 'links without destinations',
78
- linkUl: 'missing underlines on inline links',
79
- miniText: 'text smaller than 11 pixels',
80
- motion: 'motion without user request',
81
- nonTable: 'table elements used for layout',
82
- opFoc: 'operable elements that are not Tab-focusable',
83
- optRoleSel: 'Non-option elements with option roles that have no aria-selected attributes',
84
- phOnly: 'input elements with placeholders but no accessible names',
85
- pseudoP: 'adjacent br elements suspected of nonsemantically simulating p elements',
86
- radioSet: 'radio buttons not grouped into standard field sets',
87
- role: 'native-replacing explicit roles',
88
- secHeading: 'headings that violate the logical level order in their sectioning containers',
89
- styleDiff: 'style inconsistencies',
90
- tabNav: 'nonstandard keyboard navigation between elements with the tab role',
91
- targetSmall: 'buttons, inputs, and non-inline links smaller than 44 pixels wide and high',
92
- targetTiny: 'buttons, inputs, and non-inline links smaller than 24 pixels wide and high',
93
- textSem: 'semantically vague elements i, b, and/or small',
94
- titledEl: 'title attributes on inappropriate elements',
95
- zIndex: 'non-default Z indexes'
96
- };
97
- const etcRules = {
98
- attVal: 'elements with attributes having illicit values',
99
- elements: 'data on specified elements',
100
- textNodes: 'data on specified text nodes',
101
- title: 'page title',
102
- };
103
- // Tests that modify the page.
104
- const contaminators = [
105
- 'buttonMenu',
106
- 'elements',
107
- 'focAll',
108
- 'focOp',
109
- 'focInd',
110
- 'hover',
111
- 'hovInd',
112
- 'motion',
113
- 'opFoc',
114
- 'tabNav',
115
- 'textNodes'
42
+ // Metadata of all rules in default execution order.
43
+ const allRules = [
44
+ {
45
+ id: 'shoot',
46
+ what: 'page screenshot',
47
+ contaminator: false,
48
+ timeOut: 5,
49
+ defaultOn: true
50
+ },
51
+ {
52
+ id: 'adbID',
53
+ what: 'elements with ambiguous or missing referenced descriptions',
54
+ contaminator: false,
55
+ timeOut: 5,
56
+ defaultOn: true
57
+ },
58
+ {
59
+ id: 'allCaps',
60
+ what: 'leaf elements with entirely upper-case text longer than 7 characters',
61
+ contaminator: false,
62
+ timeOut: 10,
63
+ defaultOn: true
64
+ },
65
+ {
66
+ id: 'allHidden',
67
+ what: 'page that is entirely or mostly hidden',
68
+ contaminator: false,
69
+ timeOut: 5,
70
+ defaultOn: true
71
+ },
72
+ {
73
+ id: 'allSlanted',
74
+ what: 'leaf elements with entirely italic or oblique text longer than 39 characters',
75
+ contaminator: false,
76
+ timeOut: 5,
77
+ defaultOn: true
78
+ },
79
+ {
80
+ id: 'altScheme',
81
+ what: 'img elements with alt attributes having URLs as their entire values',
82
+ contaminator: false,
83
+ timeOut: 5,
84
+ defaultOn: true
85
+ },
86
+ {
87
+ id: 'attVal',
88
+ what: 'elements with attributes having illicit values',
89
+ contaminator: false,
90
+ timeOut: 5,
91
+ defaultOn: false
92
+ },
93
+ {
94
+ id: 'dupAtt',
95
+ what: 'duplicate attribute values',
96
+ contaminator: false,
97
+ timeOut: 5,
98
+ defaultOn: true
99
+ },
100
+ {
101
+ id: 'autocomplete',
102
+ what: 'name and email inputs without autocomplete attributes',
103
+ contaminator: false,
104
+ timeOut: 5,
105
+ defaultOn: true
106
+ },
107
+ {
108
+ id: 'bulk',
109
+ what: 'large count of visible elements',
110
+ contaminator: false,
111
+ timeOut: 5,
112
+ defaultOn: true
113
+ },
114
+ {
115
+ id: 'captionLoc',
116
+ what: 'caption elements that are not first children of table elements',
117
+ contaminator: false,
118
+ timeOut: 5,
119
+ defaultOn: true
120
+ },
121
+ {
122
+ id: 'datalistRef',
123
+ what: 'elements with ambiguous or missing referenced datalist elements',
124
+ contaminator: false,
125
+ timeOut: 5,
126
+ defaultOn: true
127
+ },
128
+ {
129
+ id: 'distortion',
130
+ what: 'distorted text',
131
+ contaminator: false,
132
+ timeOut: 10,
133
+ defaultOn: true
134
+ },
135
+ {
136
+ id: 'docType',
137
+ what: 'document without a doctype property',
138
+ contaminator: false,
139
+ timeOut: 10,
140
+ defaultOn: true
141
+ },
142
+ {
143
+ id: 'dupAtt',
144
+ what: 'elements with duplicate attributes',
145
+ contaminator: false,
146
+ timeOut: 5,
147
+ defaultOn: true
148
+ },
149
+ {
150
+ id: 'embAc',
151
+ what: 'active elements embedded in links or buttons',
152
+ contaminator: false,
153
+ timeOut: 5,
154
+ defaultOn: true
155
+ },
156
+ {
157
+ id: 'headEl',
158
+ what: 'invalid elements within the head',
159
+ contaminator: false,
160
+ timeOut: 5,
161
+ defaultOn: true
162
+ },
163
+ {
164
+ id: 'headingAmb',
165
+ what: 'same-level sibling headings with identical texts',
166
+ contaminator: false,
167
+ timeOut: 5,
168
+ defaultOn: true
169
+ },
170
+ {
171
+ id: 'hr',
172
+ what: 'hr element instead of styles used for vertical segmentation',
173
+ contaminator: false,
174
+ timeOut: 5,
175
+ defaultOn: true
176
+ },
177
+ {
178
+ id: 'imageLink',
179
+ what: 'links with image files as their destinations',
180
+ contaminator: false,
181
+ timeOut: 5,
182
+ defaultOn: true
183
+ },
184
+ {
185
+ id: 'labClash',
186
+ what: 'labeling inconsistencies',
187
+ contaminator: false,
188
+ timeOut: 10,
189
+ defaultOn: true
190
+ },
191
+ {
192
+ id: 'legendLoc',
193
+ what: 'legend elements that are not first children of fieldset elements',
194
+ contaminator: false,
195
+ timeOut: 5,
196
+ defaultOn: true
197
+ },
198
+ {
199
+ id: 'lineHeight',
200
+ what: 'text with a line height less than 1.5 times its font size',
201
+ contaminator: false,
202
+ timeOut: 10,
203
+ defaultOn: true
204
+ },
205
+ {
206
+ id: 'linkAmb',
207
+ what: 'links with identical texts but different destinations',
208
+ contaminator: false,
209
+ timeOut: 5,
210
+ defaultOn: true
211
+ },
212
+ {
213
+ id: 'linkExt',
214
+ what: 'links that automatically open new windows',
215
+ contaminator: false,
216
+ timeOut: 5,
217
+ defaultOn: true
218
+ },
219
+ {
220
+ id: 'linkOldAtt',
221
+ what: 'links with deprecated attributes',
222
+ contaminator: false,
223
+ timeOut: 5,
224
+ defaultOn: true
225
+ },
226
+ {
227
+ id: 'linkTitle',
228
+ what: 'links with title attributes repeating text content',
229
+ contaminator: false,
230
+ timeOut: 5,
231
+ defaultOn: true
232
+ },
233
+ {
234
+ id: 'linkTo',
235
+ what: 'links without destinations',
236
+ contaminator: false,
237
+ timeOut: 5,
238
+ defaultOn: true
239
+ },
240
+ {
241
+ id: 'linkUl',
242
+ what: 'missing underlines on inline links',
243
+ contaminator: false,
244
+ timeOut: 10,
245
+ defaultOn: true
246
+ },
247
+ {
248
+ id: 'miniText',
249
+ what: 'text smaller than 11 pixels',
250
+ contaminator: false,
251
+ timeOut: 5,
252
+ defaultOn: true
253
+ },
254
+ {
255
+ id: 'nonTable',
256
+ what: 'table elements used for layout',
257
+ contaminator: false,
258
+ timeOut: 5,
259
+ defaultOn: true
260
+ },
261
+ {
262
+ id: 'optRoleSel',
263
+ what: 'Non-option elements with option roles that have no aria-selected attributes',
264
+ contaminator: false,
265
+ timeOut: 5,
266
+ defaultOn: true
267
+ },
268
+ {
269
+ id: 'phOnly',
270
+ what: 'input elements with placeholders but no accessible names',
271
+ contaminator: false,
272
+ timeOut: 5,
273
+ defaultOn: true
274
+ },
275
+ {
276
+ id: 'pseudoP',
277
+ what: 'adjacent br elements suspected of nonsemantically simulating p elements',
278
+ contaminator: false,
279
+ timeOut: 5,
280
+ defaultOn: true
281
+ },
282
+ {
283
+ id: 'radioSet',
284
+ what: 'radio buttons not grouped into standard field sets',
285
+ contaminator: false,
286
+ timeOut: 5,
287
+ defaultOn: true
288
+ },
289
+ {
290
+ id: 'role',
291
+ what: 'native-replacing explicit roles',
292
+ contaminator: false,
293
+ timeOut: 5,
294
+ defaultOn: true
295
+ },
296
+ {
297
+ id: 'secHeading',
298
+ what: 'headings that violate the logical level order in their sectioning containers',
299
+ contaminator: false,
300
+ timeOut: 5,
301
+ defaultOn: true
302
+ },
303
+ {
304
+ id: 'styleDiff',
305
+ what: 'style inconsistencies',
306
+ contaminator: false,
307
+ timeOut: 5,
308
+ defaultOn: true
309
+ },
310
+ {
311
+ id: 'targetSmall',
312
+ what: 'buttons, inputs, and non-inline links smaller than 44 pixels wide and high',
313
+ contaminator: false,
314
+ timeOut: 5,
315
+ defaultOn: true
316
+ },
317
+ {
318
+ id: 'targetTiny',
319
+ what: 'buttons, inputs, and non-inline links smaller than 24 pixels wide and high',
320
+ contaminator: false,
321
+ timeOut: 5,
322
+ defaultOn: true
323
+ },
324
+ {
325
+ id: 'textSem',
326
+ what: 'semantically vague elements i, b, and/or small',
327
+ contaminator: false,
328
+ timeOut: 10,
329
+ defaultOn: true
330
+ },
331
+ {
332
+ id: 'title',
333
+ what: 'page title',
334
+ contaminator: false,
335
+ timeOut: 5,
336
+ defaultOn: false
337
+ },
338
+ {
339
+ id: 'titledEl',
340
+ what: 'title attributes on inappropriate elements',
341
+ contaminator: false,
342
+ timeOut: 5,
343
+ defaultOn: true
344
+ },
345
+ {
346
+ id: 'zIndex',
347
+ what: 'non-default Z indexes',
348
+ contaminator: false,
349
+ timeOut: 5,
350
+ defaultOn: true
351
+ },
352
+ {
353
+ id: 'shoot',
354
+ what: 'page screenshot',
355
+ contaminator: false,
356
+ timeOut: 5,
357
+ defaultOn: true
358
+ },
359
+ {
360
+ id: 'motion',
361
+ what: 'motion without user request, measured across tests',
362
+ contaminator: false,
363
+ timeOut: 5,
364
+ defaultOn: true
365
+ },
366
+ {
367
+ id: 'buttonMenu',
368
+ what: 'nonstandard keyboard navigation between items of button-controlled menus',
369
+ contaminator: true,
370
+ timeOut: 15,
371
+ defaultOn: true
372
+ },
373
+ {
374
+ id: 'elements',
375
+ what: 'data on specified elements',
376
+ contaminator: true,
377
+ timeOut: 10,
378
+ defaultOn: false
379
+ },
380
+ {
381
+ id: 'focAll',
382
+ what: 'discrepancies between focusable and Tab-focused elements',
383
+ contaminator: true,
384
+ timeOut: 10,
385
+ defaultOn: true
386
+ },
387
+ {
388
+ id: 'focInd',
389
+ what: 'missing and nonstandard focus indicators',
390
+ contaminator: true,
391
+ timeOut: 10,
392
+ defaultOn: true
393
+ },
394
+ {
395
+ id: 'focOp',
396
+ what: 'Tab-focusable elements that are not operable',
397
+ contaminator: true,
398
+ timeOut: 5,
399
+ defaultOn: true
400
+ },
401
+ {
402
+ id: 'focVis',
403
+ what: 'links that are not entirely visible when focused',
404
+ contaminator: true,
405
+ timeOut: 10,
406
+ defaultOn: true
407
+ },
408
+ {
409
+ id: 'hover',
410
+ what: 'hover-caused content changes',
411
+ contaminator: true,
412
+ timeOut: 10,
413
+ defaultOn: true
414
+ },
415
+ {
416
+ id: 'hovInd',
417
+ what: 'hover indication nonstandard',
418
+ contaminator: true,
419
+ timeOut: 10,
420
+ defaultOn: true
421
+ },
422
+ {
423
+ id: 'motionSolo',
424
+ what: 'motion without user request, measured within this test',
425
+ contaminator: true,
426
+ timeOut: 15,
427
+ defaultOn: false
428
+ },
429
+ {
430
+ id: 'opFoc',
431
+ what: 'operable elements that are not Tab-focusable',
432
+ contaminator: true,
433
+ timeOut: 10,
434
+ defaultOn: true
435
+ },
436
+ {
437
+ id: 'tabNav',
438
+ what: 'nonstandard keyboard navigation between elements with the tab role',
439
+ contaminator: true,
440
+ timeOut: 10,
441
+ defaultOn: true
442
+ },
443
+ {
444
+ id: 'textNodes',
445
+ what: 'data on specified text nodes',
446
+ contaminator: true,
447
+ timeOut: 10,
448
+ defaultOn: false
449
+ }
116
450
  ];
117
- // Extraordinary time limits on rules.
118
- const slowTestLimits = {
119
- allCaps: 10,
120
- buttonMenu: 15,
121
- distortion: 10,
122
- docType: 10,
123
- focAll: 10,
124
- focInd: 10,
125
- focVis: 10,
126
- hover: 10,
127
- hovInd: 10,
128
- labClash: 10,
129
- lineHeight: 10,
130
- linkUl: 10,
131
- motion: 15,
132
- opFoc: 10,
133
- tabNav: 10,
134
- textSem: 10
135
- };
136
451
  const timeoutMultiplier = Number.parseFloat(process.env.TIMEOUT_MULTIPLIER) || 1;
137
452
 
138
453
  // ERROR HANDLER
@@ -156,7 +471,7 @@ const jsonTest = async (ruleID, ruleArgs) => {
156
471
  ruleObj.complaints.instance,
157
472
  ruleObj.complaints.summary
158
473
  ];
159
- return await report(
474
+ return await getRuleResult(
160
475
  withItems, all, ruleObj.ruleID, whats, ruleObj.ordinalSeverity, ruleObj.summaryTagName
161
476
  );
162
477
  };
@@ -170,13 +485,16 @@ const wait = ms => {
170
485
  };
171
486
  // Conducts and reports Testaro tests.
172
487
  exports.reporter = async (page, report, actIndex) => {
173
- // Report page crashes.
174
488
  const url = await page.url();
175
489
  const act = report.acts[actIndex];
176
490
  const {args, stopOnFail, withItems} = act;
491
+ const launchOptions = act.launch;
492
+ const browserID = launchOptions ? launchOptions.browserID || report.browserID : report.browserID;
177
493
  const argRules = args ? Object.keys(args) : null;
178
- const rules = act.rules || ['y', ... Object.keys(evalRules)];
179
- // Initialize the act report.
494
+ // Get the specification of rules to be tested for.
495
+ const ruleSpec = act.rules
496
+ || ['y', ... allRules.filter(rule => rule.defaultOn).map(rule => rule.id)];
497
+ // Initialize the act data and result.
180
498
  const data = {
181
499
  prevented: false,
182
500
  error: '',
@@ -185,222 +503,216 @@ exports.reporter = async (page, report, actIndex) => {
185
503
  rulesInvalid: [],
186
504
  ruleTestTimes: {}
187
505
  };
188
- const result = {};
189
- // If the rule specification is valid:
190
- if (
191
- rules.length > 1
192
- && ['y', 'n'].includes(rules[0])
193
- && rules.slice(1).every(rule => {
194
- if (evalRules[rule] || etcRules[rule] || futureRules.has(rule)) {
195
- return true;
196
- }
197
- else {
198
- console.log(`ERROR: Testaro rule ${rule} invalid`);
199
- return false;
200
- }
201
- })
202
- ) {
203
- // Wait 1 second to prevent out-of-order logging with granular reporting.
204
- await wait(1000);
205
- let calledRules = rules[0] === 'y'
206
- ? rules.slice(1)
207
- : Object.keys(evalRules).filter(ruleID => ! rules.slice(1).includes(ruleID));
208
- const calledContaminators = calledRules.filter(rule => contaminators.includes(rule));
209
- const firstCalledContaminator = calledContaminators[0];
210
- const calledBenignRules = calledRules.filter(rule => ! contaminators.includes(rule));
211
- const testTimes = [];
212
- let contaminatorsStarted = false;
213
- // Starting with the noncontaminators, for each rule invoked:
214
- for (const rule of calledBenignRules.concat(calledContaminators)) {
215
- const contaminatorSuffix = rule === firstCalledContaminator ? ' (first contaminator)' : '';
216
- console.log(`Starting rule ${rule}${contaminatorSuffix}`);
217
- const pageClosed = page ? page.isClosed() : true;
218
- const isContaminator = contaminators.includes(rule);
219
- // If it is a contaminator other than the first one or the page has closed:
220
- if (contaminatorsStarted || pageClosed) {
221
- // If the page has closed:
222
- if (pageClosed) {
223
- // Report this.
224
- console.log(`WARNING: Relaunching browser for test ${rule} after abnormal closure`);
225
- }
226
- // Replace the browser and the page and navigate to the target.
227
- await launch(
228
- report,
229
- process.env.DEBUG === 'true',
230
- Number.parseInt(process.env.WAITS) || 0,
231
- report.browserID,
232
- url
233
- );
234
- page = require('../run').page;
235
- }
236
- // If it is a contaminator, ensure that future tests use new browsers.
237
- if (isContaminator) {
238
- contaminatorsStarted = true;
239
- }
240
- // Report crashes and disconnections during this test.
241
- let crashHandler;
242
- let disconnectHandler;
243
- const {browser} = require('../run');
244
- if (page && ! page.isClosed()) {
245
- crashHandler = () => {
246
- console.log(`ERROR: Page crashed during ${rule} test`);
247
- };
248
- page.on('crash', crashHandler);
506
+ const result = exports.result = {};
507
+ const allRuleIDs = allRules.map(rule => rule.id);
508
+ // If the rule specification is invalid:
509
+ if (! (
510
+ ruleSpec.length > 1
511
+ && ['y', 'n'].includes(ruleSpec[0])
512
+ && ruleSpec.slice(1).every(ruleID => allRuleIDs.includes(ruleID))
513
+ )) {
514
+ // Report this and stop testing.
515
+ data.prevented = true;
516
+ data.error = 'ERROR: Testaro rule specification invalid';
517
+ console.log('ERROR: Testaro rule specification invalid');
518
+ return {
519
+ data,
520
+ result
521
+ };
522
+ }
523
+ // Wait 1 second to prevent out-of-order logging with granular reporting.
524
+ await wait(1000);
525
+ // Get the rules to be tested for and their execution order.
526
+ const jobRuleIDs = ruleSpec[0] === 'y'
527
+ ? ruleSpec.slice(1)
528
+ : allRules.filter(rule => rule.defaultOn && ! allRuleIDs.includes(rule.id));
529
+ const jobRules = allRules.filter(rule => jobRuleIDs.includes(rule.id));
530
+ const testTimes = [];
531
+ let contaminatorsStarted = false;
532
+ // For each rule to be tested for:
533
+ for (const rule of jobRules) {
534
+ const ruleID = rule.id;
535
+ console.log(`Starting rule ${ruleID}`);
536
+ const pageClosed = page ? page.isClosed() : true;
537
+ const isContaminator = rule.contaminator;
538
+ // If it is a contaminator other than the first one or the page has closed:
539
+ if (contaminatorsStarted || pageClosed) {
540
+ // If the page has closed:
541
+ if (pageClosed) {
542
+ // Report this.
543
+ console.log(`WARNING: Relaunching browser for test ${rule} after abnormal closure`);
249
544
  }
250
- if (browser) {
251
- disconnectHandler = () => {
252
- console.log(`ERROR: Browser disconnected during ${rule} test`);
253
- };
254
- browser.on('disconnected', disconnectHandler);
545
+ // Replace the browser and the page and navigate to the target.
546
+ await launch(
547
+ report,
548
+ process.env.DEBUG === 'true',
549
+ Number.parseInt(process.env.WAITS) || 0,
550
+ browserID,
551
+ url
552
+ );
553
+ page = require('../run').page;
554
+ }
555
+ // If the rule is a contaminator, ensure that future tests use new browsers.
556
+ if (isContaminator) {
557
+ contaminatorsStarted = true;
558
+ }
559
+ // Report crashes and disconnections during this test.
560
+ let crashHandler;
561
+ let disconnectHandler;
562
+ const {browser} = require('../run');
563
+ if (page && ! page.isClosed()) {
564
+ crashHandler = () => {
565
+ console.log(`ERROR: Page crashed during ${rule} test`);
566
+ };
567
+ page.on('crash', crashHandler);
568
+ }
569
+ if (browser) {
570
+ disconnectHandler = () => {
571
+ console.log(`ERROR: Browser disconnected during ${rule} test`);
572
+ };
573
+ browser.on('disconnected', disconnectHandler);
574
+ }
575
+ // Initialize an argument array.
576
+ const ruleArgs = [page, withItems];
577
+ const ruleFileNames = await fs.readdir(`${__dirname}/../testaro`);
578
+ const isJS = ruleFileNames.includes(`${ruleID}.js`);
579
+ const isJSON = ruleFileNames.includes(`${ruleID}.json`);
580
+ // If the rule is defined with JavaScript or JSON but not both:
581
+ if ((isJS || isJSON) && ! (isJS && isJSON)) {
582
+ // If with JavaScript and it has extra arguments:
583
+ if (isJS && argRules && argRules.includes(ruleID)) {
584
+ // Add them to the argument array.
585
+ ruleArgs.push(... args[ruleID]);
255
586
  }
256
- // Initialize an argument array.
257
- const ruleArgs = [page, withItems];
258
- const ruleFileNames = await fs.readdir(`${__dirname}/../testaro`);
259
- const isJS = ruleFileNames.includes(`${rule}.js`);
260
- const isJSON = ruleFileNames.includes(`${rule}.json`);
261
- // If the rule is defined with JavaScript or JSON but not both:
262
- if ((isJS || isJSON) && ! (isJS && isJSON)) {
263
- // If with JavaScript and it has extra arguments:
264
- if (isJS && argRules && argRules.includes(rule)) {
265
- // Add them to the argument array.
266
- ruleArgs.push(... args[rule]);
267
- }
268
- // Test the page.
269
- if (! result[rule]) {
270
- result[rule] = {};
271
- }
272
- const what = evalRules[rule] || etcRules[rule];
273
- result[rule].what = what;
274
- const startTime = Date.now();
275
- let timeout;
276
- let testRetries = 2;
277
- let testSuccess = false;
278
- while (testRetries > 0 && ! testSuccess) {
279
- try {
280
- // Apply a time limit to the test.
281
- const timeLimit = 1000 * timeoutMultiplier * (slowTestLimits[rule] ?? 5);
282
- // If the time limit expires during the test:
283
- const timer = new Promise(resolve => {
284
- timeout = setTimeout(() => {
285
- // Add data about the test, including its prevention, to the result.
286
- const endTime = Date.now();
287
- testTimes.push([rule, Math.round((endTime - startTime) / 1000)]);
288
- data.rulePreventions.push(rule);
289
- data.rulePreventionMessages[rule] = 'Timeout';
290
- result[rule].totals = [0, 0, 0, 0];
291
- result[rule].standardInstances = [];
292
- console.log(`ERROR: Test of testaro rule ${rule} timed out`);
293
- resolve({timedOut: true});
294
- }, timeLimit);
295
- });
296
- // Perform the test, subject to the time limit.
297
- const ruleReport = isJS
298
- ? require(`../testaro/${rule}`).reporter(... ruleArgs)
299
- : jsonTest(rule, ruleArgs);
300
- // Get the test result or a timeout result.
301
- const ruleOrTimeoutReport = await Promise.race([timer, ruleReport]);
302
- // If the test was completed:
303
- if (! ruleOrTimeoutReport.timedOut) {
304
- // Add data from the test to the result.
587
+ result[ruleID] ??= {};
588
+ const {what} = rule;
589
+ result[ruleID].what = what || '';
590
+ const startTime = Date.now();
591
+ let timeout;
592
+ let testRetries = 2;
593
+ let testSuccess = false;
594
+ while (testRetries > 0 && ! testSuccess) {
595
+ try {
596
+ // Apply a time limit to the test.
597
+ const timeLimit = 1000 * timeoutMultiplier * rule.timeOut;
598
+ // If the time limit expires during the test:
599
+ const timer = new Promise(resolve => {
600
+ timeout = setTimeout(() => {
601
+ // Add data about the test, including its prevention, to the result.
305
602
  const endTime = Date.now();
306
603
  testTimes.push([rule, Math.round((endTime - startTime) / 1000)]);
307
- Object.keys(ruleOrTimeoutReport).forEach(key => {
308
- result[rule][key] = ruleOrTimeoutReport[key];
309
- });
310
- result[rule].totals = result[rule].totals.map(total => Math.round(total));
311
- // Prevent a retry of the test.
312
- testSuccess = true;
313
- // If testing is to stop after a failure and the page failed the test:
314
- if (stopOnFail && ruleOrTimeoutReport.totals.some(total => total)) {
315
- // Stop testing.
316
- break;
317
- }
604
+ data.rulePreventions.push(rule);
605
+ data.rulePreventionMessages[ruleID] = 'Timeout';
606
+ result[ruleID].totals = [0, 0, 0, 0];
607
+ result[ruleID].standardInstances = [];
608
+ console.log(`ERROR: Test of testaro rule ${ruleID} timed out`);
609
+ resolve({timedOut: true});
610
+ }, timeLimit);
611
+ });
612
+ // Perform the test, subject to the time limit.
613
+ const ruleReport = isJS
614
+ ? require(`../testaro/${ruleID}`).reporter(... ruleArgs)
615
+ : jsonTest(ruleID, ruleArgs);
616
+ // Get the test result or a timeout result.
617
+ const ruleOrTimeoutReport = await Promise.race([timer, ruleReport]);
618
+ // If the test was completed:
619
+ if (! ruleOrTimeoutReport.timedOut) {
620
+ // Add data from the test to the result.
621
+ const endTime = Date.now();
622
+ testTimes.push([ruleID, Math.round((endTime - startTime) / 1000)]);
623
+ Object.keys(ruleOrTimeoutReport).forEach(key => {
624
+ result[ruleID][key] = ruleOrTimeoutReport[key];
625
+ });
626
+ // If the result includes totals:
627
+ if (result[ruleID].totals) {
628
+ // Round them.
629
+ result[ruleID].totals = result[ruleID].totals.map(total => Math.round(total));
318
630
  }
319
- // Otherwise, i.e. if the test timed out:
320
- else {
321
- // Stop retrying the test.
631
+ // Prevent a retry of the test.
632
+ testSuccess = true;
633
+ // If testing is to stop after a failure and the page failed the test:
634
+ if (stopOnFail && ruleOrTimeoutReport.totals.some(total => total)) {
635
+ // Stop testing.
322
636
  break;
323
637
  }
324
638
  }
325
- // If an error is thrown by the test:
326
- catch(error) {
327
- const isPageClosed = ['closed', 'Protocol error', 'Target page'].some(phrase =>
328
- error.message.includes(phrase)
639
+ // Otherwise, i.e. if the test timed out:
640
+ else {
641
+ // Stop retrying the test.
642
+ break;
643
+ }
644
+ }
645
+ // If an error is thrown by the test:
646
+ catch(error) {
647
+ const isPageClosed = ['closed', 'Protocol error', 'Target page'].some(phrase =>
648
+ error.message.includes(phrase)
649
+ );
650
+ // If the page has closed and there are retries left:
651
+ if (isPageClosed && testRetries) {
652
+ // Report this and decrement the allowed retry count.
653
+ console.log(
654
+ `WARNING: Retry ${3 - testRetries--} of test ${ruleID} starting after page closed`
329
655
  );
330
- // If the page has closed and there are retries left:
331
- if (isPageClosed && testRetries) {
332
- // Report this and decrement the allowed retry count.
333
- console.log(
334
- `WARNING: Retry ${3 - testRetries--} of test ${rule} starting after page closed`
335
- );
336
- await wait(2000);
337
- // Replace the browser and the page and navigate to the target.
338
- await launch(
339
- report,
340
- process.env.DEBUG === 'true',
341
- Number.parseInt(process.env.WAITS) || 0,
342
- report.browserID,
343
- url
344
- );
345
- page = require('../run').page;
346
- // If the page replacement failed:
347
- if (! page) {
348
- // Report this.
349
- console.log(`ERROR: Browser relaunch to retry test ${rule} failed`);
350
- data.rulePreventions.push(rule);
351
- data.rulePreventionMessages[rule] = 'Retry failure due to browser relaunch failure';
352
- // Stop retrying the test.
353
- break;
354
- }
355
- // Update the rule arguments with the current page.
356
- ruleArgs[0] = page;
357
- }
358
- // Otherwise, i.e. if the page is open or it is closed but no retries are left:
359
- else {
360
- // Treat the test as prevented.
361
- data.rulePreventions.push(rule);
362
- data.rulePreventionMessages[rule] = error.message;
363
- console.log(`ERROR: Test of testaro rule ${rule} prevented (${error.message})`);
364
- // Do not retry the test even if retries are left.
656
+ await wait(2000);
657
+ // Replace the browser and the page and navigate to the target.
658
+ await launch(
659
+ report,
660
+ process.env.DEBUG === 'true',
661
+ Number.parseInt(process.env.WAITS) || 0,
662
+ report.browserID,
663
+ url
664
+ );
665
+ page = require('../run').page;
666
+ // If the page replacement failed:
667
+ if (! page) {
668
+ // Report this.
669
+ console.log(`ERROR: Browser relaunch to retry test ${ruleID} failed`);
670
+ data.rulePreventions.push(ruleID);
671
+ data.rulePreventionMessages[ruleID] = 'Retry failure due to browser relaunch failure';
672
+ // Stop retrying the test.
365
673
  break;
366
674
  }
675
+ // Update the rule arguments with the current page.
676
+ ruleArgs[0] = page;
367
677
  }
368
- finally {
369
- // Clear the timeout.
370
- clearTimeout(timeout);
678
+ // Otherwise, i.e. if the page is open or it is closed but no retries are left:
679
+ else {
680
+ // Treat the test as prevented.
681
+ data.rulePreventions.push(ruleID);
682
+ data.rulePreventionMessages[ruleID] = error.message;
683
+ console.log(`ERROR: Test of testaro rule ${ruleID} prevented (${error.message})`);
684
+ // Do not retry the test even if retries are left.
685
+ break;
371
686
  }
372
687
  }
373
- // Clear the error listeners.
374
- if (page && ! page.isClosed() && crashHandler) {
375
- page.off('crash', crashHandler);
376
- }
377
- if (browser && disconnectHandler) {
378
- browser.off('disconnected', disconnectHandler);
688
+ finally {
689
+ // Clear the timeout.
690
+ clearTimeout(timeout);
379
691
  }
380
692
  }
381
- // Otherwise, i.e. if the rule is undefined or doubly defined:
382
- else {
383
- // Report this.
384
- data.rulesInvalid.push(rule);
385
- console.log(`ERROR: Rule ${rule} not validly defined`);
386
- // Clear the crash listener.
387
- if (page && ! page.isClosed()) {
388
- page.off('crash', crashHandler);
389
- }
693
+ // Clear the error listeners.
694
+ if (page && ! page.isClosed() && crashHandler) {
695
+ page.off('crash', crashHandler);
696
+ }
697
+ if (browser && disconnectHandler) {
698
+ browser.off('disconnected', disconnectHandler);
390
699
  }
391
700
  }
392
- // Record the test times in descending order.
393
- testTimes.sort((a, b) => b[1] - a[1]).forEach(pair => {
394
- data.ruleTestTimes[pair[0]] = pair[1];
395
- });
396
- }
397
- // Otherwise, i.e. if the rule specification is invalid:
398
- else {
399
- const message = 'ERROR: Testaro rule specification invalid';
400
- console.log(message);
401
- data.prevented = true;
402
- data.error = message;
403
- }
701
+ // Otherwise, i.e. if the rule is undefined or doubly defined:
702
+ else {
703
+ // Report this.
704
+ data.rulesInvalid.push(rule);
705
+ console.log(`ERROR: Rule ${rule.id} not validly defined`);
706
+ // Clear the crash listener.
707
+ if (page && ! page.isClosed()) {
708
+ page.off('crash', crashHandler);
709
+ }
710
+ }
711
+ };
712
+ // Record the test times in descending order.
713
+ testTimes.sort((a, b) => b[1] - a[1]).forEach(pair => {
714
+ data.ruleTestTimes[pair[0]] = pair[1];
715
+ });
404
716
  return {
405
717
  data,
406
718
  result