testaro 67.0.0 → 68.0.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.
Files changed (95) hide show
  1. package/LICENSE +4 -16
  2. package/README.md +10 -2
  3. package/UPGRADES.md +1 -1
  4. package/dirWatch.js +2 -3
  5. package/ed11y/editoria11y.min.js +109 -690
  6. package/ed11y/editoria11y210.min.js +747 -0
  7. package/netWatch.js +6 -6
  8. package/package.json +1 -1
  9. package/procs/aslint.js +2 -2
  10. package/procs/catalog.js +190 -0
  11. package/procs/{dateOf.js → dateTime.js} +6 -4
  12. package/procs/doActs.js +1227 -0
  13. package/procs/doTestAct.js +63 -29
  14. package/procs/error.js +53 -0
  15. package/procs/job.js +64 -38
  16. package/procs/launch.js +596 -0
  17. package/procs/nu.js +3 -18
  18. package/procs/shoot.js +18 -2
  19. package/procs/testaro.js +102 -125
  20. package/procs/xPath.js +62 -0
  21. package/run.js +42 -1938
  22. package/scratch/README.md +9 -0
  23. package/testaro/adbID.js +3 -3
  24. package/testaro/allCaps.js +4 -5
  25. package/testaro/allHidden.js +19 -18
  26. package/testaro/allSlanted.js +4 -5
  27. package/testaro/altScheme.js +3 -3
  28. package/testaro/attVal.js +19 -35
  29. package/testaro/autocomplete.js +65 -62
  30. package/testaro/bulk.js +21 -20
  31. package/testaro/buttonMenu.js +112 -33
  32. package/testaro/captionLoc.js +3 -3
  33. package/testaro/datalistRef.js +4 -5
  34. package/testaro/distortion.js +3 -3
  35. package/testaro/docType.js +6 -9
  36. package/testaro/dupAtt.js +12 -25
  37. package/testaro/elements.js +4 -3
  38. package/testaro/embAc.js +4 -2
  39. package/testaro/focAll.js +6 -13
  40. package/testaro/focAndOp.js +3 -3
  41. package/testaro/focInd.js +3 -3
  42. package/testaro/focVis.js +4 -3
  43. package/testaro/headEl.js +5 -12
  44. package/testaro/headingAmb.js +45 -88
  45. package/testaro/hovInd.js +5 -5
  46. package/testaro/hover.js +44 -8
  47. package/testaro/hr.js +4 -4
  48. package/testaro/imageLink.js +3 -3
  49. package/testaro/labClash.js +3 -3
  50. package/testaro/legendLoc.js +3 -3
  51. package/testaro/lineHeight.js +3 -3
  52. package/testaro/linkAmb.js +25 -17
  53. package/testaro/linkExt.js +5 -5
  54. package/testaro/linkOldAtt.js +4 -3
  55. package/testaro/linkTo.js +4 -3
  56. package/testaro/linkUl.js +4 -5
  57. package/testaro/miniText.js +4 -3
  58. package/testaro/motion.js +3 -22
  59. package/testaro/nonTable.js +4 -5
  60. package/testaro/optRoleSel.js +3 -3
  61. package/testaro/phOnly.js +3 -3
  62. package/testaro/pseudoP.js +5 -5
  63. package/testaro/radioSet.js +4 -5
  64. package/testaro/role.js +4 -5
  65. package/testaro/secHeading.js +4 -5
  66. package/testaro/shoot0.js +3 -2
  67. package/testaro/shoot1.js +3 -2
  68. package/testaro/styleDiff.js +5 -12
  69. package/testaro/tabNav.js +30 -118
  70. package/testaro/targetSmall.js +30 -15
  71. package/testaro/textNodes.js +3 -1
  72. package/testaro/textSem.js +4 -5
  73. package/testaro/title.js +4 -2
  74. package/testaro/titledEl.js +3 -3
  75. package/testaro/zIndex.js +3 -3
  76. package/tests/alfa.js +28 -54
  77. package/tests/aslint.js +20 -53
  78. package/tests/axe.js +76 -13
  79. package/tests/ed11y.js +69 -141
  80. package/tests/htmlcs.js +69 -38
  81. package/tests/ibm.js +54 -9
  82. package/tests/nuVal.js +65 -12
  83. package/tests/nuVnu.js +76 -26
  84. package/tests/qualWeb.js +89 -44
  85. package/tests/testaro.js +288 -273
  86. package/tests/wave.js +142 -117
  87. package/tests/wax.js +61 -42
  88. package/procs/getLocatorData.js +0 -192
  89. package/procs/identify.js +0 -250
  90. package/procs/isInlineLink.js +0 -42
  91. package/procs/screenShot.js +0 -32
  92. package/procs/standardize.js +0 -524
  93. package/procs/target.js +0 -90
  94. package/procs/tellServer.js +0 -43
  95. package/scripts/dumpAlts.js +0 -28
package/tests/testaro.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  © 2023–2025 CVS Health and/or one of its affiliates. All rights reserved.
3
- © 2025 Jonathan Robert Pool.
3
+ © 2025–2026 Jonathan Robert Pool.
4
4
 
5
5
  Licensed under the MIT License. See LICENSE file at the project root or
6
6
  https://opensource.org/license/mit/ for details.
@@ -16,387 +16,436 @@
16
16
  // IMPORTS
17
17
 
18
18
  // Function to launch a browser.
19
- const {launch} = require('../run');
19
+ const {launch} = require('../procs/launch');
20
20
 
21
21
  // CONSTANTS
22
22
 
23
- // Metadata of all rules in default execution order.
23
+ /*
24
+ Metadata of all rules in default execution order.
25
+ Property needsLaunch is true if the rule is first or the previous one contaminates the page.
26
+ */
24
27
  const allRules = [
25
28
  {
26
29
  id: 'shoot0',
27
30
  what: 'first page screenshot',
28
- launchRole: 'owner',
31
+ contaminates: false,
32
+ needsAccessibleName: false,
29
33
  timeOut: 5,
30
34
  defaultOn: true
31
35
  },
32
36
  {
33
37
  id: 'adbID',
34
38
  what: 'elements with ambiguous or missing referenced descriptions',
35
- launchRole: 'sharer',
39
+ contaminates: false,
40
+ needsAccessibleName: false,
36
41
  timeOut: 5,
37
42
  defaultOn: true
38
43
  },
39
44
  {
40
45
  id: 'allCaps',
41
46
  what: 'leaf elements with entirely upper-case text longer than 7 characters',
42
- launchRole: 'sharer',
47
+ contaminates: false,
48
+ needsAccessibleName: false,
43
49
  timeOut: 5,
44
50
  defaultOn: true
45
51
  },
46
52
  {
47
53
  id: 'allHidden',
48
54
  what: 'page that is entirely or mostly hidden',
49
- launchRole: 'sharer',
55
+ contaminates: false,
56
+ needsAccessibleName: false,
50
57
  timeOut: 5,
51
58
  defaultOn: true
52
59
  },
53
60
  {
54
61
  id: 'allSlanted',
55
62
  what: 'leaf elements with entirely italic or oblique text longer than 39 characters',
56
- launchRole: 'sharer',
63
+ contaminates: false,
64
+ needsAccessibleName: false,
57
65
  timeOut: 5,
58
66
  defaultOn: true
59
67
  },
60
68
  {
61
69
  id: 'altScheme',
62
70
  what: 'img elements with alt attributes having URLs as their entire values',
63
- launchRole: 'sharer',
71
+ contaminates: false,
72
+ needsAccessibleName: false,
64
73
  timeOut: 5,
65
74
  defaultOn: true
66
75
  },
67
76
  {
68
77
  id: 'attVal',
69
78
  what: 'elements with attributes having illicit values',
70
- launchRole: 'sharer',
79
+ contaminates: false,
80
+ needsAccessibleName: false,
71
81
  timeOut: 5,
72
82
  defaultOn: false
73
83
  },
74
- {
75
- id: 'dupAtt',
76
- what: 'duplicate attribute values',
77
- launchRole: 'sharer',
78
- timeOut: 5,
79
- defaultOn: true
80
- },
81
- {
82
- id: 'autocomplete',
83
- what: 'name and email inputs without autocomplete attributes',
84
- launchRole: 'sharer',
85
- timeOut: 5,
86
- defaultOn: true
87
- },
88
84
  {
89
85
  id: 'bulk',
90
86
  what: 'large count of visible elements',
91
- launchRole: 'sharer',
87
+ contaminates: false,
88
+ needsAccessibleName: false,
92
89
  timeOut: 5,
93
90
  defaultOn: true
94
91
  },
95
92
  {
96
93
  id: 'captionLoc',
97
94
  what: 'caption elements that are not first children of table elements',
98
- launchRole: 'sharer',
95
+ contaminates: false,
96
+ needsAccessibleName: false,
99
97
  timeOut: 5,
100
98
  defaultOn: true
101
99
  },
102
100
  {
103
101
  id: 'datalistRef',
104
102
  what: 'elements with ambiguous or missing referenced datalist elements',
105
- launchRole: 'sharer',
103
+ contaminates: false,
104
+ needsAccessibleName: false,
106
105
  timeOut: 5,
107
106
  defaultOn: true
108
107
  },
109
108
  {
110
109
  id: 'distortion',
111
110
  what: 'distorted text',
112
- launchRole: 'sharer',
111
+ contaminates: false,
112
+ needsAccessibleName: false,
113
113
  timeOut: 5,
114
114
  defaultOn: true
115
115
  },
116
116
  {
117
117
  id: 'docType',
118
118
  what: 'document without a doctype property',
119
- launchRole: 'sharer',
119
+ contaminates: false,
120
+ needsAccessibleName: false,
120
121
  timeOut: 5,
121
122
  defaultOn: true
122
123
  },
123
124
  {
124
125
  id: 'dupAtt',
125
- what: 'elements with duplicate attributes',
126
- launchRole: 'sharer',
126
+ what: 'duplicate attribute values',
127
+ contaminates: false,
128
+ needsAccessibleName: false,
127
129
  timeOut: 5,
128
130
  defaultOn: true
129
131
  },
130
132
  {
131
133
  id: 'embAc',
132
134
  what: 'active elements embedded in links or buttons',
133
- launchRole: 'sharer',
135
+ contaminates: false,
136
+ needsAccessibleName: false,
134
137
  timeOut: 5,
135
138
  defaultOn: true
136
139
  },
137
140
  {
138
141
  id: 'headEl',
139
142
  what: 'invalid elements within the head',
140
- launchRole: 'sharer',
143
+ contaminates: false,
144
+ needsAccessibleName: false,
141
145
  timeOut: 5,
142
146
  defaultOn: true
143
147
  },
144
148
  {
145
149
  id: 'headingAmb',
146
150
  what: 'same-level sibling headings with identical texts',
147
- launchRole: 'sharer',
151
+ contaminates: false,
152
+ needsAccessibleName: false,
148
153
  timeOut: 5,
149
154
  defaultOn: true
150
155
  },
151
156
  {
152
157
  id: 'hr',
153
158
  what: 'hr element instead of styles used for vertical segmentation',
154
- launchRole: 'sharer',
159
+ contaminates: false,
160
+ needsAccessibleName: false,
155
161
  timeOut: 5,
156
162
  defaultOn: true
157
163
  },
158
164
  {
159
165
  id: 'imageLink',
160
166
  what: 'links with image files as their destinations',
161
- launchRole: 'sharer',
167
+ contaminates: false,
168
+ needsAccessibleName: false,
162
169
  timeOut: 5,
163
170
  defaultOn: true
164
171
  },
165
172
  {
166
173
  id: 'labClash',
167
174
  what: 'labeling inconsistencies',
168
- launchRole: 'sharer',
175
+ contaminates: false,
176
+ needsAccessibleName: false,
169
177
  timeOut: 5,
170
178
  defaultOn: true
171
179
  },
172
180
  {
173
181
  id: 'legendLoc',
174
182
  what: 'legend elements that are not first children of fieldset elements',
175
- launchRole: 'sharer',
183
+ contaminates: false,
184
+ needsAccessibleName: false,
176
185
  timeOut: 5,
177
186
  defaultOn: true
178
187
  },
179
188
  {
180
189
  id: 'lineHeight',
181
190
  what: 'text with a line height less than 1.5 times its font size',
182
- launchRole: 'sharer',
191
+ contaminates: false,
192
+ needsAccessibleName: false,
183
193
  timeOut: 5,
184
194
  defaultOn: true
185
195
  },
186
196
  {
187
197
  id: 'linkAmb',
188
198
  what: 'links with identical texts but different destinations',
189
- launchRole: 'sharer',
199
+ contaminates: false,
200
+ needsAccessibleName: false,
190
201
  timeOut: 20,
191
202
  defaultOn: true
192
203
  },
193
204
  {
194
205
  id: 'linkExt',
195
206
  what: 'links that automatically open new windows',
196
- launchRole: 'sharer',
207
+ contaminates: false,
208
+ needsAccessibleName: false,
197
209
  timeOut: 5,
198
210
  defaultOn: true
199
211
  },
200
212
  {
201
213
  id: 'linkOldAtt',
202
214
  what: 'links with deprecated attributes',
203
- launchRole: 'sharer',
215
+ contaminates: false,
216
+ needsAccessibleName: false,
204
217
  timeOut: 5,
205
218
  defaultOn: true
206
219
  },
207
220
  {
208
221
  id: 'linkTo',
209
222
  what: 'links without destinations',
210
- launchRole: 'sharer',
223
+ contaminates: false,
224
+ needsAccessibleName: false,
211
225
  timeOut: 5,
212
226
  defaultOn: true
213
227
  },
214
228
  {
215
229
  id: 'linkUl',
216
230
  what: 'missing underlines on inline links',
217
- launchRole: 'sharer',
231
+ contaminates: false,
232
+ needsAccessibleName: false,
218
233
  timeOut: 5,
219
234
  defaultOn: true
220
235
  },
221
236
  {
222
237
  id: 'miniText',
223
238
  what: 'text smaller than 11 pixels',
224
- launchRole: 'sharer',
239
+ contaminates: false,
240
+ needsAccessibleName: false,
225
241
  timeOut: 5,
226
242
  defaultOn: true
227
243
  },
228
244
  {
229
245
  id: 'nonTable',
230
246
  what: 'table elements used for layout',
231
- launchRole: 'sharer',
247
+ contaminates: false,
248
+ needsAccessibleName: false,
232
249
  timeOut: 5,
233
250
  defaultOn: true
234
251
  },
235
252
  {
236
253
  id: 'optRoleSel',
237
254
  what: 'Non-option elements with option roles that have no aria-selected attributes',
238
- launchRole: 'sharer',
239
- timeOut: 5,
240
- defaultOn: true
241
- },
242
- {
243
- id: 'phOnly',
244
- what: 'input elements with placeholders but no accessible names',
245
- launchRole: 'sharer',
255
+ contaminates: false,
256
+ needsAccessibleName: false,
246
257
  timeOut: 5,
247
258
  defaultOn: true
248
259
  },
249
260
  {
250
261
  id: 'pseudoP',
251
262
  what: 'adjacent br elements suspected of nonsemantically simulating p elements',
252
- launchRole: 'sharer',
263
+ contaminates: false,
264
+ needsAccessibleName: false,
253
265
  timeOut: 5,
254
266
  defaultOn: true
255
267
  },
256
268
  {
257
269
  id: 'radioSet',
258
270
  what: 'radio buttons not grouped into standard field sets',
259
- launchRole: 'sharer',
271
+ contaminates: false,
272
+ needsAccessibleName: false,
260
273
  timeOut: 5,
261
274
  defaultOn: true
262
275
  },
263
276
  {
264
277
  id: 'role',
265
278
  what: 'native-replacing explicit roles',
266
- launchRole: 'sharer',
279
+ contaminates: false,
280
+ needsAccessibleName: false,
267
281
  timeOut: 20,
268
282
  defaultOn: true
269
283
  },
270
284
  {
271
285
  id: 'secHeading',
272
286
  what: 'headings that violate the logical level order in their sectioning containers',
273
- launchRole: 'sharer',
287
+ contaminates: false,
288
+ needsAccessibleName: false,
274
289
  timeOut: 5,
275
290
  defaultOn: true
276
291
  },
277
292
  {
278
293
  id: 'styleDiff',
279
294
  what: 'style inconsistencies',
280
- launchRole: 'sharer',
295
+ contaminates: false,
296
+ needsAccessibleName: false,
281
297
  timeOut: 5,
282
298
  defaultOn: true
283
299
  },
284
300
  {
285
301
  id: 'targetSmall',
286
302
  what: 'labels, buttons, inputs, and links too near each other',
287
- launchRole: 'sharer',
303
+ contaminates: false,
304
+ needsAccessibleName: false,
288
305
  timeOut: 5,
289
306
  defaultOn: true
290
307
  },
291
308
  {
292
309
  id: 'textSem',
293
310
  what: 'semantically vague elements i, b, and/or small',
294
- launchRole: 'sharer',
311
+ contaminates: false,
312
+ needsAccessibleName: false,
295
313
  timeOut: 5,
296
314
  defaultOn: true
297
315
  },
298
316
  {
299
317
  id: 'title',
300
318
  what: 'page title',
301
- launchRole: 'sharer',
319
+ contaminates: false,
320
+ needsAccessibleName: false,
302
321
  timeOut: 5,
303
322
  defaultOn: false
304
323
  },
305
324
  {
306
325
  id: 'titledEl',
307
326
  what: 'title attributes on inappropriate elements',
308
- launchRole: 'sharer',
327
+ contaminates: false,
328
+ needsAccessibleName: false,
309
329
  timeOut: 5,
310
330
  defaultOn: true
311
331
  },
312
332
  {
313
333
  id: 'zIndex',
314
334
  what: 'non-default Z indexes',
315
- launchRole: 'sharer',
335
+ contaminates: false,
336
+ needsAccessibleName: false,
316
337
  timeOut: 5,
317
338
  defaultOn: true
318
339
  },
319
340
  {
320
341
  id: 'shoot1',
321
342
  what: 'second page screenshot',
322
- launchRole: 'owner',
343
+ contaminates: false,
344
+ needsAccessibleName: false,
323
345
  timeOut: 5,
324
346
  defaultOn: true
325
347
  },
326
348
  {
327
349
  id: 'motion',
328
350
  what: 'motion without user request, measured across tests',
329
- launchRole: 'sharer',
351
+ contaminates: false,
352
+ needsAccessibleName: false,
353
+ timeOut: 5,
354
+ defaultOn: true
355
+ },
356
+ {
357
+ id: 'autocomplete',
358
+ what: 'name and email inputs without autocomplete attributes',
359
+ contaminates: false,
360
+ needsAccessibleName: true,
361
+ timeOut: 5,
362
+ defaultOn: true
363
+ },
364
+ {
365
+ id: 'phOnly',
366
+ what: 'input elements with placeholders but no accessible names',
367
+ contaminates: false,
368
+ needsAccessibleName: true,
330
369
  timeOut: 5,
331
370
  defaultOn: true
332
371
  },
333
372
  {
334
373
  id: 'buttonMenu',
335
374
  what: 'nonstandard keyboard navigation between items of button-controlled menus',
336
- launchRole: 'waster',
375
+ contaminates: true,
376
+ needsAccessibleName: false,
337
377
  timeOut: 15,
338
378
  defaultOn: true
339
379
  },
340
380
  {
341
381
  id: 'elements',
342
382
  what: 'data on specified elements',
343
- launchRole: 'waster',
383
+ contaminates: true,
384
+ needsAccessibleName: false,
344
385
  timeOut: 10,
345
386
  defaultOn: false
346
387
  },
347
388
  {
348
389
  id: 'focAll',
349
390
  what: 'discrepancies between focusable and Tab-focused elements',
350
- launchRole: 'waster',
391
+ contaminates: true,
392
+ needsAccessibleName: false,
351
393
  timeOut: 10,
352
394
  defaultOn: true
353
395
  },
354
396
  {
355
397
  id: 'focAndOp',
356
398
  what: 'Tab-focusable elements that are not operable or vice versa',
357
- launchRole: 'waster',
399
+ contaminates: true,
400
+ needsAccessibleName: false,
358
401
  timeOut: 5,
359
402
  defaultOn: true
360
403
  },
361
404
  {
362
405
  id: 'focInd',
363
406
  what: 'missing and nonstandard focus indicators',
364
- launchRole: 'waster',
407
+ contaminates: true,
408
+ needsAccessibleName: false,
365
409
  timeOut: 10,
366
410
  defaultOn: true
367
411
  },
368
412
  {
369
413
  id: 'focVis',
370
414
  what: 'links that are not entirely visible when focused',
371
- launchRole: 'waster',
415
+ contaminates: true,
416
+ needsAccessibleName: false,
372
417
  timeOut: 10,
373
418
  defaultOn: true
374
419
  },
375
420
  {
376
421
  id: 'hover',
377
422
  what: 'hover-caused content changes',
378
- launchRole: 'waster',
423
+ contaminates: true,
424
+ needsAccessibleName: false,
379
425
  timeOut: 20,
380
426
  defaultOn: true
381
427
  },
382
428
  {
383
429
  id: 'hovInd',
384
430
  what: 'hover indication nonstandard',
385
- launchRole: 'waster',
431
+ contaminates: true,
432
+ needsAccessibleName: false,
386
433
  timeOut: 10,
387
434
  defaultOn: true
388
435
  },
389
436
  {
390
437
  id: 'tabNav',
391
438
  what: 'nonstandard keyboard navigation between elements with the tab role',
392
- launchRole: 'waster',
439
+ contaminates: true,
440
+ needsAccessibleName: false,
393
441
  timeOut: 10,
394
442
  defaultOn: true
395
443
  },
396
444
  {
397
445
  id: 'textNodes',
398
446
  what: 'data on specified text nodes',
399
- launchRole: 'waster',
447
+ contaminates: true,
448
+ needsAccessibleName: false,
400
449
  timeOut: 10,
401
450
  defaultOn: false
402
451
  }
@@ -426,244 +475,210 @@ exports.reporter = async (page, report, actIndex) => {
426
475
  const url = target.url;
427
476
  const browserID = act.launch ? act.launch.browserID || report.browserID : report.browserID;
428
477
  const argRules = args ? Object.keys(args) : null;
429
- // Get the specification of rules to be tested for.
478
+ // Get the specification of rules to be tested for or, by default, all rules with defaultOn true.
430
479
  const ruleSpec = act.rules
431
480
  || ['y', ... allRules.filter(rule => rule.defaultOn).map(rule => rule.id)];
432
- // Initialize the act data and result.
481
+ // Initialize the act data.
433
482
  const data = {
434
483
  prevented: false,
435
484
  error: '',
436
- rulePreventions: [],
437
- rulePreventionMessages: {},
485
+ rulePreventions: {},
438
486
  rulesInvalid: [],
439
- ruleTestTimes: {},
487
+ ruleTestTimes: [],
440
488
  ruleData: {}
441
489
  };
442
- const result = {};
490
+ // Initialize the act result.
491
+ const result = {
492
+ nativeResult: {},
493
+ standardResult: {
494
+ prevented: false,
495
+ totals: [0, 0, 0, 0],
496
+ instances: []
497
+ }
498
+ };
499
+ const {standardResult} = result;
443
500
  const allRuleIDs = allRules.map(rule => rule.id);
444
- // If the rule specification is invalid:
445
- if (! (
501
+ // If the rule specification is valid:
502
+ if (
446
503
  ruleSpec.length > 1
447
504
  && ['y', 'n'].includes(ruleSpec[0])
448
505
  && ruleSpec.slice(1).every(ruleID => allRuleIDs.includes(ruleID))
449
- )) {
450
- // Report this and stop testing.
451
- data.prevented = true;
452
- data.error = 'ERROR: Testaro rule specification invalid';
453
- console.log('ERROR: Testaro rule specification invalid');
454
- return {
455
- data,
456
- result
457
- };
458
- }
459
- // Wait 1 second to prevent out-of-order logging with granular reporting.
460
- await wait(1000);
461
- // Get the rules to be tested for and their execution order.
462
- const jobRuleIDs = ruleSpec[0] === 'y'
463
- ? ruleSpec.slice(1)
464
- : allRules.filter(rule => rule.defaultOn && ! allRuleIDs.includes(rule.id));
465
- const jobRules = allRules.filter(rule => jobRuleIDs.includes(rule.id));
466
- const testTimes = [];
467
- // For each rule to be tested for:
468
- for (const ruleIndexString in jobRules) {
469
- const ruleIndex = Number.parseInt(ruleIndexString);
470
- const rule = jobRules[ruleIndex];
471
- const ruleID = rule.id;
472
- console.log(`Starting rule ${ruleID}`);
473
- // Make the browser emulate headedness in all cases, because performance does not suffer.
474
- const headEmulation = ruleID.startsWith('shoot') ? 'high' : 'high';
475
- // Get whether it needs a new browser launched.
476
- const needsLaunch = ruleIndex
477
- && jobRules[ruleIndex - 1].launchRole !== 'sharer'
478
- && rule.launchRole !== 'owner'
479
- || ! ruleIndex;
480
- const pageClosed = page && page.isClosed();
481
- // If it does, or if the page has closed:
482
- if (needsLaunch || pageClosed) {
483
- // If the page has closed when it is expected to be open:
484
- if (pageClosed && ! needsLaunch) {
485
- // Report this.
486
- console.log(`WARNING: Relaunching browser for test ${rule} after abnormal closure`);
487
- }
488
- // Replace the browser and the page and navigate to the target.
489
- await launch(
490
- report,
491
- actIndex,
492
- headEmulation,
493
- browserID,
494
- url
495
- );
496
- page = require('../run').page;
497
- }
498
- // Get the current browser.
499
- const {browser} = require('../run');
500
- // Report crashes and disconnections during this test.
501
- let crashHandler;
502
- let disconnectHandler;
503
- if (page && ! page.isClosed()) {
504
- crashHandler = () => {
505
- console.log(`ERROR: Page crashed during ${rule} test`);
506
+ ) {
507
+ // Wait 1 second to prevent out-of-order logging with granular reporting.
508
+ await wait(1000);
509
+ // Get the rules to be tested for and their execution order.
510
+ const jobRuleIDs = ruleSpec[0] === 'y'
511
+ ? ruleSpec.slice(1)
512
+ : allRules.filter(rule => rule.defaultOn && ! allRuleIDs.includes(rule.id));
513
+ const jobRules = allRules.filter(rule => jobRuleIDs.includes(rule.id));
514
+ // For each rule to be tested for:
515
+ for (let ruleIndex = 0; ruleIndex < jobRules.length; ruleIndex++) {
516
+ const rule = jobRules[ruleIndex];
517
+ // Initialize the rule result.
518
+ const ruleResult = {
519
+ id: rule.id,
520
+ prevented: false,
521
+ error: '',
522
+ data: {},
523
+ totals: [0, 0, 0, 0],
524
+ instances: [],
525
+ elapsedTime: 0
506
526
  };
507
- page.on('crash', crashHandler);
508
- }
509
- if (browser) {
510
- disconnectHandler = () => {
511
- console.log(`ERROR: Browser disconnected during ${rule} test`);
512
- };
513
- browser.on('disconnected', disconnectHandler);
514
- }
515
- // Initialize an argument array for the reporter.
516
- const ruleArgs = [page, withItems];
517
- // If the rule has extra arguments:
518
- if (argRules && argRules.includes(ruleID)) {
519
- // Add them to the argument array.
520
- ruleArgs.push(... args[ruleID]);
521
- }
522
- // Initialize the rule result.
523
- result[ruleID] ??= {};
524
- const ruleResult = result[ruleID];
525
- ruleResult.what = rule.what || '';
526
- const startTime = Date.now();
527
- let timeout;
528
- let testRetries = 2;
529
- let testSuccess = false;
530
- // Until all permitted retries are exhausted or the test succeeds:
531
- while (testRetries > 0 && ! testSuccess) {
527
+ console.log(`Starting rule ${ruleResult.id}`);
528
+ // Make the browser emulate headedness in all cases, because performance does not suffer.
529
+ const headEmulation = ruleResult.id.startsWith('shoot') ? 'high' : 'high';
530
+ // Get whether the rule needs a new browser launched.
531
+ const needsLaunch = ruleIndex === 0
532
+ || jobRules[ruleIndex - 1].contaminates
533
+ || jobRules[ruleIndex].needsAccessibleName && ! jobRules[ruleIndex - 1].needsAccessibleName;
534
+ const pageClosed = page && page.isClosed();
535
+ // If it does, or if the page has closed:
536
+ if (needsLaunch || pageClosed) {
537
+ // If the page has closed when it is expected to be open:
538
+ if (pageClosed && ! needsLaunch) {
539
+ // Report this.
540
+ console.log(`WARNING: Relaunching browser for test ${rule} after abnormal closure`);
541
+ }
542
+ // Create a browser, replace the page, and visit the target, retrying twice if necessary.
543
+ page = await launch({
544
+ report,
545
+ actIndex,
546
+ tempBrowserID: browserID,
547
+ tempURL: url,
548
+ xPathNeed: 'script',
549
+ needsAccessibleName: jobRules[ruleIndex].needsAccessibleName,
550
+ retries: 2
551
+ });
552
+ }
553
+ // Report crashes and disconnections during this test.
554
+ let crashHandler;
555
+ let disconnectHandler;
556
+ if (page && ! page.isClosed()) {
557
+ crashHandler = () => {
558
+ console.log(`ERROR: Page crashed during ${rule} test`);
559
+ };
560
+ page.on('crash', crashHandler);
561
+ }
562
+ const browser = page.context().browser();
563
+ if (browser) {
564
+ disconnectHandler = () => {
565
+ console.log(`ERROR: Browser disconnected during ${rule} test`);
566
+ };
567
+ browser.on('disconnected', disconnectHandler);
568
+ }
569
+ // Initialize an argument array for the reporter.
570
+ const ruleArgs = [page, report.catalog, withItems];
571
+ // If the rule has extra arguments:
572
+ if (argRules?.includes(ruleResult.id)) {
573
+ // Add them to the argument array.
574
+ ruleArgs.push(... args[ruleResult.id]);
575
+ }
576
+ const startTime = Date.now();
577
+ let timer;
532
578
  try {
533
579
  // Apply a time limit to the test.
534
580
  const timeLimit = 1000 * timeoutMultiplier * rule.timeOut;
581
+ let timeout;
535
582
  // If the time limit expires during the test:
536
- const timer = new Promise(resolve => {
583
+ timer = new Promise(resolve => {
537
584
  timeout = setTimeout(() => {
538
- // Add data about the test, including its prevention, to the result.
539
- const endTime = Date.now();
540
- testTimes.push([rule, Math.round((endTime - startTime) / 1000)]);
541
- data.rulePreventions.push(ruleID);
542
- data.rulePreventionMessages[ruleID] = 'Timeout';
543
- ruleResult.totals = [0, 0, 0, 0];
544
- ruleResult.standardInstances = [];
545
- console.log(`ERROR: Test of testaro rule ${ruleID} timed out`);
585
+ // Add data about the timeout to the rule result.
586
+ ruleResult.prevented = true;
587
+ ruleResult.error = 'Timeout';
588
+ console.log(`ERROR: Test of testaro rule ${ruleResult.id} timed out`);
546
589
  resolve({timedOut: true});
547
590
  }, timeLimit);
548
591
  });
549
- // Perform the test, subject to the time limit.
550
- const ruleReport = require(`../testaro/${ruleID}`).reporter(... ruleArgs);
551
- // Get the rule report or a timeout report.
552
- const ruleOrTimeoutReport = await Promise.race([timer, ruleReport]);
553
- // If the test was completed:
554
- if (! ruleOrTimeoutReport.timedOut) {
555
- // Add data from the rule report to the tool result.
556
- const endTime = Date.now();
557
- testTimes.push([ruleID, Math.round((endTime - startTime) / 1000)]);
558
- Object.keys(ruleOrTimeoutReport).forEach(key => {
559
- ruleResult[key] = ruleOrTimeoutReport[key];
560
- });
561
- // If the test was prevented:
562
- if (ruleResult.data?.prevented && ruleResult.data.error) {
563
- // Add this to the tool result.
564
- data.rulePreventions.push(ruleID);
565
- data.rulePreventionMessages[ruleID] = ruleResult.data.error;
592
+ // Try to perform the test and get a test report.
593
+ const testReport = require(`../testaro/${ruleResult.id}`).reporter(... ruleArgs);
594
+ // Get a test or timeout report.
595
+ const ruleReport = await Promise.race([timer, testReport]);
596
+ clearTimeout(timeout);
597
+ // If it was a test report:
598
+ if (! ruleReport.timedOut) {
599
+ // Add the rule-report properties to the rule result.
600
+ ruleResult.data = ruleReport.data;
601
+ ruleResult.totals = ruleReport.totals;
602
+ ruleResult.instances = ruleReport.standardInstances;
603
+ // Add the rule-result properties to the result.
604
+ if (Object.keys(ruleReport.data).length) {
605
+ data.ruleData[ruleResult.id] = ruleResult.data;
566
606
  }
567
- // If the result includes totals:
568
607
  if (ruleResult.totals) {
569
- // Round them.
570
- ruleResult.totals = ruleResult.totals.map(total => Math.round(total));
608
+ ruleResult.totals.forEach((total, index) => {
609
+ standardResult.totals[index] += Math.round(total);
610
+ });
611
+ }
612
+ if (ruleResult.instances) {
613
+ standardResult.instances.push(... ruleResult.instances);
571
614
  }
572
- const ruleDataMiscKeys = Object
573
- .keys(ruleResult.data)
574
- .filter(key => ! ['prevented', 'error'].includes(key));
575
- // For any other property of the rule report data object:
576
- ruleDataMiscKeys.forEach(key => {
577
- data.ruleData[ruleID] ??= {};
578
- // Add it to the tool result.
579
- data.ruleData[ruleID][key] = ruleResult.data[key];
580
- });
581
- // Prevent a retry of the test.
582
- testSuccess = true;
583
615
  // If testing is to stop after a failure and the page failed the test:
584
- if (stopOnFail && ruleResult.totals && ruleResult.totals.some(total => total)) {
585
- // Stop testing.
616
+ if (stopOnFail && ruleReport.totals?.some(total => total)) {
617
+ // Test for no more rules.
586
618
  break;
587
619
  }
588
620
  }
589
- // Otherwise, i.e. if the test timed out:
590
- else {
591
- // Report this.
592
- data.rulePreventions.push(ruleID);
593
- data.rulePreventionMessages[ruleID] = 'Timeout';
594
- // Stop retrying the test.
595
- break;
596
- }
597
621
  }
598
622
  // If an error is thrown by the test:
599
623
  catch(error) {
600
624
  const isPageClosed = ['closed', 'Protocol error', 'Target page'].some(phrase =>
601
625
  error.message.includes(phrase)
602
626
  );
603
- // If the page has closed and there are retries left:
604
- if (isPageClosed && testRetries) {
605
- // Report this and decrement the allowed retry count.
606
- console.log(
607
- `WARNING: Retry ${3 - testRetries--} of test ${ruleID} starting after page closed`
608
- );
609
- await wait(2000);
610
- // Replace the browser and the page in the run module and navigate to the target.
611
- await launch(
612
- report,
613
- actIndex,
614
- headEmulation,
615
- report.browserID,
616
- url
617
- );
618
- page = require('../run').page;
619
- // If the page replacement failed:
620
- if (! page) {
621
- // Report this.
622
- console.log(`ERROR: Browser relaunch to retry test ${ruleID} failed`);
623
- data.rulePreventions.push(ruleID);
624
- data.rulePreventionMessages[ruleID] = 'Retry failure due to browser relaunch failure';
625
- // Stop retrying the test.
626
- break;
627
- }
628
- // Update the rule arguments with the current page.
629
- ruleArgs[0] = page;
627
+ // If the page has closed:
628
+ if (isPageClosed) {
629
+ // Report this.
630
+ console.log(`ERROR: Test ${ruleResult.id} failed because page closed`);
630
631
  }
631
- // Otherwise, i.e. if the page is open or it is closed but no retries are left:
632
+ // Otherwise, i.e. if the page is open:
632
633
  else {
633
- // Treat the test as prevented.
634
- data.rulePreventions.push(ruleID);
635
- data.rulePreventionMessages[ruleID] = error.message;
636
- console.log(`ERROR: Test of testaro rule ${ruleID} prevented (${error.message})`);
637
- // Do not retry the test even if retries are left.
638
- break;
634
+ // Add this to the rule result.
635
+ ruleResult.prevented = true;
636
+ ruleResult.error = error.message;
637
+ console.log(
638
+ `ERROR: Test of testaro rule ${ruleResult.id} prevented (${error.message})`
639
+ );
639
640
  }
640
641
  }
641
642
  finally {
642
- // Clear the timeout.
643
- clearTimeout(timeout);
643
+ // Add the elapsed time to the rule result.
644
+ ruleResult.elapsedTime = Math.round((Date.now() - startTime) / 1000);
645
+ // Add the elapsed time to the data.
646
+ data.ruleTestTimes.push([ruleResult.id, ruleResult.elapsedTime]);
647
+ // If the test timed out or otherwise failed:
648
+ if (ruleResult.prevented) {
649
+ // Add this and the error to the data.
650
+ data.rulePreventions[ruleResult.id] = ruleResult.error;
651
+ }
644
652
  }
645
- }
646
- // Clear the error listeners.
647
- if (page && ! page.isClosed() && crashHandler) {
648
- page.off('crash', crashHandler);
649
- crashHandler = null;
650
- }
651
- if (browser && disconnectHandler) {
652
- browser.off('disconnected', disconnectHandler);
653
- disconnectHandler = null;
654
- }
655
- // Force a garbage collection.
656
- try {
657
- if (global.gc) {
658
- global.gc();
653
+ // Sort the rule test times.
654
+ data.ruleTestTimes.sort((a, b) => b[1] - a[1]);
655
+ // Clear the error listeners.
656
+ if (page && ! page.isClosed() && crashHandler) {
657
+ page.off('crash', crashHandler);
658
+ crashHandler = null;
659
659
  }
660
- }
661
- catch(error) {}
662
- };
663
- // Record the test times in descending order.
664
- testTimes.sort((a, b) => b[1] - a[1]).forEach(pair => {
665
- data.ruleTestTimes[pair[0]] = pair[1];
666
- });
660
+ if (browser && disconnectHandler) {
661
+ browser.off('disconnected', disconnectHandler);
662
+ disconnectHandler = null;
663
+ }
664
+ // Force a garbage collection.
665
+ try {
666
+ if (global.gc) {
667
+ global.gc();
668
+ }
669
+ }
670
+ catch(error) {}
671
+ };
672
+ }
673
+ // Otherwise, i.e. if the rule specification is invalid:
674
+ else {
675
+ // Report this and stop testing.
676
+ standardResult.prevented = true;
677
+ data.prevented = true;
678
+ const message = 'ERROR: Testaro rule specification invalid';
679
+ data.error = message;
680
+ console.log(message);
681
+ }
667
682
  return {
668
683
  data,
669
684
  result