testaro 78.0.8 → 78.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.
Files changed (356) hide show
  1. package/.gitattributes +94 -0
  2. package/.github/workflows/ci.yml +58 -0
  3. package/.github/workflows/publish.yml +49 -0
  4. package/.github/workflows/typescript.yml +35 -0
  5. package/AGENTS.md +2 -2
  6. package/CLAUDE.md +24 -18
  7. package/CONTAINERS.md +9 -6
  8. package/CONTRIBUTING.md +29 -3
  9. package/Dockerfile +1 -1
  10. package/README.md +73 -19
  11. package/UPGRADES.md +4 -0
  12. package/actSpecs-doc.md +13 -1
  13. package/actSpecs.js +29 -5
  14. package/call.js +6 -5
  15. package/docker-compose.yml +8 -2
  16. package/docs/checkpoint-scanning.md +199 -0
  17. package/docs/standard-result-outcome.md +165 -0
  18. package/env.example +44 -10
  19. package/eslint.config.mjs +101 -0
  20. package/netWatch.js +372 -222
  21. package/package.json +46 -31
  22. package/pour/README.md +47 -0
  23. package/pour/pour.min.js +8 -0
  24. package/procs/actDo.js +808 -0
  25. package/procs/catalog.d.ts +17 -0
  26. package/procs/catalog.js +312 -221
  27. package/procs/catalog.ts +398 -0
  28. package/procs/checkpoint.js +110 -0
  29. package/procs/config.d.ts +35 -0
  30. package/procs/config.js +62 -0
  31. package/procs/dateTime.js +2 -1
  32. package/procs/doActs.js +442 -849
  33. package/procs/doTestAct.js +27 -121
  34. package/procs/flow.js +221 -0
  35. package/procs/generateRuleRegistry.js +94 -0
  36. package/procs/getSource.d.ts +26 -0
  37. package/procs/job.js +115 -1
  38. package/procs/launch.d.ts +40 -0
  39. package/procs/launch.js +239 -108
  40. package/procs/nu.d.ts +48 -0
  41. package/procs/scope.js +196 -0
  42. package/procs/shoot.d.ts +33 -0
  43. package/procs/standard.d.ts +18 -0
  44. package/procs/standard.js +83 -0
  45. package/procs/standard.ts +126 -0
  46. package/procs/testAct.js +145 -0
  47. package/procs/testaro.d.ts +20 -0
  48. package/procs/testaro.js +236 -189
  49. package/procs/testaro.ts +321 -0
  50. package/procs/userPath.js +126 -0
  51. package/procs/xPath.d.ts +4 -0
  52. package/procs/xPath.js +81 -69
  53. package/procs/xPath.ts +106 -0
  54. package/procs/xPathScript.d.ts +6 -0
  55. package/procs/xPathScript.js +70 -0
  56. package/run.js +3 -2
  57. package/surea11y/README.md +44 -0
  58. package/surea11y/surea11y.browser.js +14 -0
  59. package/testaro/adbID.d.ts +3 -0
  60. package/testaro/adbID.js +38 -41
  61. package/testaro/adbID.ts +66 -0
  62. package/testaro/allCapStyle.d.ts +3 -0
  63. package/testaro/allCapStyle.js +31 -34
  64. package/testaro/allCapStyle.ts +55 -0
  65. package/testaro/allCaps.d.ts +16 -0
  66. package/testaro/allCaps.js +179 -151
  67. package/testaro/allCaps.ts +228 -0
  68. package/testaro/allHidden.d.ts +13 -0
  69. package/testaro/allHidden.js +29 -30
  70. package/testaro/allHidden.ts +50 -0
  71. package/testaro/allSlanted.d.ts +3 -0
  72. package/testaro/allSlanted.js +30 -33
  73. package/testaro/allSlanted.ts +54 -0
  74. package/testaro/altScheme.d.ts +3 -0
  75. package/testaro/altScheme.js +27 -30
  76. package/testaro/altScheme.ts +50 -0
  77. package/testaro/attVal.d.ts +3 -0
  78. package/testaro/attVal.js +20 -21
  79. package/testaro/attVal.ts +52 -0
  80. package/testaro/autocomplete.d.ts +3 -0
  81. package/testaro/autocomplete.js +59 -75
  82. package/testaro/autocomplete.ts +101 -0
  83. package/testaro/bulk.d.ts +13 -0
  84. package/testaro/bulk.js +32 -33
  85. package/testaro/bulk.ts +55 -0
  86. package/testaro/buttonMenu.d.ts +9 -0
  87. package/testaro/buttonMenu.js +317 -319
  88. package/testaro/buttonMenu.ts +391 -0
  89. package/testaro/captionLoc.d.ts +3 -0
  90. package/testaro/captionLoc.js +17 -20
  91. package/testaro/captionLoc.ts +40 -0
  92. package/testaro/datalistRef.d.ts +3 -0
  93. package/testaro/datalistRef.js +33 -36
  94. package/testaro/datalistRef.ts +55 -0
  95. package/testaro/distortion.d.ts +3 -0
  96. package/testaro/distortion.js +57 -26
  97. package/testaro/distortion.ts +81 -0
  98. package/testaro/docType.d.ts +15 -0
  99. package/testaro/docType.js +25 -25
  100. package/testaro/docType.ts +45 -0
  101. package/testaro/dupAtt.d.ts +16 -0
  102. package/testaro/dupAtt.js +113 -104
  103. package/testaro/dupAtt.ts +144 -0
  104. package/testaro/elements.d.ts +6 -0
  105. package/testaro/elements.js +153 -153
  106. package/testaro/elements.ts +215 -0
  107. package/testaro/embAc.d.ts +3 -0
  108. package/testaro/embAc.js +19 -20
  109. package/testaro/embAc.ts +40 -0
  110. package/testaro/focAll.d.ts +13 -0
  111. package/testaro/focAll.js +185 -191
  112. package/testaro/focAll.ts +217 -0
  113. package/testaro/focAndOp.d.ts +3 -0
  114. package/testaro/focAndOp.js +100 -104
  115. package/testaro/focAndOp.ts +128 -0
  116. package/testaro/focInd.d.ts +3 -0
  117. package/testaro/focInd.js +64 -66
  118. package/testaro/focInd.ts +96 -0
  119. package/testaro/focVis.d.ts +3 -0
  120. package/testaro/focVis.js +28 -30
  121. package/testaro/focVis.ts +52 -0
  122. package/testaro/headEl.d.ts +10 -0
  123. package/testaro/headEl.js +61 -62
  124. package/testaro/headEl.ts +82 -0
  125. package/testaro/headingAmb.d.ts +3 -0
  126. package/testaro/headingAmb.js +54 -63
  127. package/testaro/headingAmb.ts +72 -0
  128. package/testaro/hovInd.d.ts +12 -0
  129. package/testaro/hovInd.js +163 -130
  130. package/testaro/hovInd.ts +199 -0
  131. package/testaro/hover.d.ts +3 -0
  132. package/testaro/hover.js +155 -126
  133. package/testaro/hover.ts +154 -0
  134. package/testaro/hr.d.ts +3 -0
  135. package/testaro/hr.js +14 -17
  136. package/testaro/hr.ts +36 -0
  137. package/testaro/imageLink.d.ts +3 -0
  138. package/testaro/imageLink.js +17 -20
  139. package/testaro/imageLink.ts +42 -0
  140. package/testaro/labClash.d.ts +3 -0
  141. package/testaro/labClash.js +31 -33
  142. package/testaro/labClash.ts +54 -0
  143. package/testaro/legendLoc.d.ts +3 -0
  144. package/testaro/legendLoc.js +17 -20
  145. package/testaro/legendLoc.ts +42 -0
  146. package/testaro/lineHeight.d.ts +3 -0
  147. package/testaro/lineHeight.js +43 -48
  148. package/testaro/lineHeight.ts +70 -0
  149. package/testaro/linkAmb.d.ts +7 -0
  150. package/testaro/linkAmb.js +79 -80
  151. package/testaro/linkAmb.ts +105 -0
  152. package/testaro/linkExt.d.ts +3 -0
  153. package/testaro/linkExt.js +13 -16
  154. package/testaro/linkExt.ts +35 -0
  155. package/testaro/linkOldAtt.d.ts +3 -0
  156. package/testaro/linkOldAtt.js +25 -28
  157. package/testaro/linkOldAtt.ts +48 -0
  158. package/testaro/linkTo.d.ts +3 -0
  159. package/testaro/linkTo.js +22 -22
  160. package/testaro/linkTo.ts +43 -0
  161. package/testaro/linkUl.d.ts +3 -0
  162. package/testaro/linkUl.js +30 -34
  163. package/testaro/linkUl.ts +54 -0
  164. package/testaro/miniText.d.ts +3 -0
  165. package/testaro/miniText.js +41 -44
  166. package/testaro/miniText.ts +68 -0
  167. package/testaro/motion.d.ts +10 -0
  168. package/testaro/motion.js +92 -96
  169. package/testaro/motion.ts +125 -0
  170. package/testaro/nonTable.d.ts +3 -0
  171. package/testaro/nonTable.js +39 -45
  172. package/testaro/nonTable.ts +66 -0
  173. package/testaro/optRoleSel.d.ts +3 -0
  174. package/testaro/optRoleSel.js +16 -19
  175. package/testaro/optRoleSel.ts +41 -0
  176. package/testaro/phOnly.d.ts +3 -0
  177. package/testaro/phOnly.js +18 -21
  178. package/testaro/phOnly.ts +43 -0
  179. package/testaro/pseudoP.d.ts +3 -0
  180. package/testaro/pseudoP.js +36 -38
  181. package/testaro/pseudoP.ts +59 -0
  182. package/testaro/radioSet.d.ts +3 -0
  183. package/testaro/radioSet.js +57 -59
  184. package/testaro/radioSet.ts +79 -0
  185. package/testaro/registry.d.ts +63 -0
  186. package/testaro/registry.js +67 -0
  187. package/testaro/registry.ts +141 -0
  188. package/testaro/role.d.ts +3 -0
  189. package/testaro/role.js +27 -29
  190. package/testaro/role.ts +53 -0
  191. package/testaro/secHeading.d.ts +3 -0
  192. package/testaro/secHeading.js +30 -33
  193. package/testaro/secHeading.ts +53 -0
  194. package/testaro/styleDiff.d.ts +25 -0
  195. package/testaro/styleDiff.js +248 -252
  196. package/testaro/styleDiff.ts +303 -0
  197. package/testaro/tabNav.d.ts +33 -0
  198. package/testaro/tabNav.js +272 -342
  199. package/testaro/tabNav.ts +454 -0
  200. package/testaro/targetsNear.d.ts +9 -0
  201. package/testaro/targetsNear.js +130 -132
  202. package/testaro/targetsNear.ts +160 -0
  203. package/testaro/textNodes.d.ts +6 -0
  204. package/testaro/textNodes.js +139 -135
  205. package/testaro/textNodes.ts +185 -0
  206. package/testaro/textSem.d.ts +3 -0
  207. package/testaro/textSem.js +25 -28
  208. package/testaro/textSem.ts +47 -0
  209. package/testaro/title.d.ts +9 -0
  210. package/testaro/title.js +16 -13
  211. package/testaro/title.ts +31 -0
  212. package/testaro/titledEl.d.ts +3 -0
  213. package/testaro/titledEl.js +15 -18
  214. package/testaro/titledEl.ts +38 -0
  215. package/testaro/zIndex.d.ts +3 -0
  216. package/testaro/zIndex.js +19 -22
  217. package/testaro/zIndex.ts +42 -0
  218. package/tests/alfa.d.ts +45 -0
  219. package/tests/alfa.js +138 -141
  220. package/tests/alfa.ts +214 -0
  221. package/tests/aslint.d.ts +33 -0
  222. package/tests/aslint.js +272 -249
  223. package/tests/aslint.ts +301 -0
  224. package/tests/axe.d.ts +27 -0
  225. package/tests/axe.js +199 -200
  226. package/tests/axe.ts +277 -0
  227. package/tests/ed11y.d.ts +28 -0
  228. package/tests/ed11y.js +141 -99
  229. package/tests/ed11y.ts +178 -0
  230. package/tests/htmlcs.d.ts +21 -0
  231. package/tests/htmlcs.js +174 -140
  232. package/tests/htmlcs.ts +181 -0
  233. package/tests/ibm.d.ts +52 -0
  234. package/tests/ibm.js +164 -166
  235. package/tests/ibm.ts +251 -0
  236. package/tests/nuVal.d.ts +13 -0
  237. package/tests/nuVal.js +107 -112
  238. package/tests/nuVal.ts +145 -0
  239. package/tests/nuVnu.d.ts +15 -0
  240. package/tests/nuVnu.js +141 -111
  241. package/tests/nuVnu.ts +144 -0
  242. package/tests/pour.d.ts +31 -0
  243. package/tests/pour.js +242 -0
  244. package/tests/pour.ts +273 -0
  245. package/tests/qualWeb.d.ts +39 -0
  246. package/tests/qualWeb.js +302 -272
  247. package/tests/qualWeb.ts +415 -0
  248. package/tests/surea11y.d.ts +33 -0
  249. package/tests/surea11y.js +288 -0
  250. package/tests/surea11y.ts +334 -0
  251. package/tests/testaro.d.ts +25 -0
  252. package/tests/testaro.js +745 -652
  253. package/tests/testaro.ts +862 -0
  254. package/tests/wave.d.ts +46 -0
  255. package/tests/wave.js +166 -177
  256. package/tests/wave.ts +252 -0
  257. package/tsconfig.json +17 -0
  258. package/types.d.ts +243 -0
  259. package/types.js +10 -0
  260. package/types.ts +376 -0
  261. package/validation/act/README.md +46 -0
  262. package/validation/act/capture.js +424 -0
  263. package/validation/act/chromium-issue-draft.md +66 -0
  264. package/validation/act/fp-triage-2026-08-22.md +86 -0
  265. package/validation/act/isolation-notes.md +159 -0
  266. package/validation/act/playwright-issue-draft.md +73 -0
  267. package/validation/act/propose-mappings.js +0 -0
  268. package/validation/act/repro-cdp-raw.js +105 -0
  269. package/validation/act/repro-metarefresh.js +65 -0
  270. package/validation/act/score.js +184 -0
  271. package/validation/act/stage3a-stress-report.md +69 -0
  272. package/validation/act/stage3b-mapping-proposals.md +94 -0
  273. package/validation/act/stage3b-triage-draft.md +138 -0
  274. package/validation/act/surea11y-track-a-2026-09-01.md +50 -0
  275. package/validation/executors/netWatch.js +180 -90
  276. package/validation/executors/test.js +17 -2
  277. package/validation/executors/tests.js +118 -10
  278. package/validation/jobs/reports/raw/260901T1000-surea11y-validation.json +964 -0
  279. package/validation/jobs/todo/240101T1200-simple-example.json +14 -6
  280. package/validation/jobs/todo/240101T1300-shoot-example.json +2 -1
  281. package/validation/jobs/todo/260821T1900-pour-validation.json +45 -0
  282. package/validation/jobs/todo/260901T1000-surea11y-validation.json +45 -0
  283. package/validation/knownFailures.json +1 -0
  284. package/validation/tests/jobProperties/adbID.json +27 -2
  285. package/validation/tests/jobProperties/{focOp.json → allCapStyle.json} +56 -53
  286. package/validation/tests/jobProperties/allCaps.json +25 -0
  287. package/validation/tests/jobProperties/allHidden.json +133 -13
  288. package/validation/tests/jobProperties/allSlanted.json +3 -3
  289. package/validation/tests/jobProperties/altScheme.json +23 -8
  290. package/validation/tests/jobProperties/attVal.json +57 -57
  291. package/validation/tests/jobProperties/autocomplete.json +11 -1
  292. package/validation/tests/jobProperties/bulk.json +6 -1
  293. package/validation/tests/jobProperties/buttonMenu.json +63 -42
  294. package/validation/tests/jobProperties/captionLoc.json +1 -6
  295. package/validation/tests/jobProperties/checkpoint-browser.json +401 -0
  296. package/validation/tests/jobProperties/checkpoint-page.json +397 -0
  297. package/validation/tests/jobProperties/checkpoint.json +395 -0
  298. package/validation/tests/jobProperties/datalistRef.json +20 -5
  299. package/validation/tests/jobProperties/distortion.json +28 -3
  300. package/validation/tests/jobProperties/docType.json +2 -2
  301. package/validation/tests/jobProperties/dupAtt.json +48 -33
  302. package/validation/tests/jobProperties/elements.json +28 -28
  303. package/validation/tests/jobProperties/embAc.json +36 -31
  304. package/validation/tests/jobProperties/focAndOp.json +284 -0
  305. package/validation/tests/jobProperties/focInd.json +34 -34
  306. package/validation/tests/jobProperties/focVis.json +1 -1
  307. package/validation/tests/jobProperties/hover.json +39 -37
  308. package/validation/tests/jobProperties/hr.json +13 -3
  309. package/validation/tests/jobProperties/imageLink.json +0 -5
  310. package/validation/tests/jobProperties/labClash.json +68 -33
  311. package/validation/tests/jobProperties/legendLoc.json +1 -6
  312. package/validation/tests/jobProperties/lineHeight.json +2 -2
  313. package/validation/tests/jobProperties/linkAmb.json +13 -18
  314. package/validation/tests/jobProperties/linkExt.json +1 -1
  315. package/validation/tests/jobProperties/linkOldAtt.json +12 -2
  316. package/validation/tests/jobProperties/linkTo.json +1 -1
  317. package/validation/tests/jobProperties/linkUl.json +65 -65
  318. package/validation/tests/jobProperties/miniText.json +7 -2
  319. package/validation/tests/jobProperties/motion.json +4 -50
  320. package/validation/tests/jobProperties/nonTable.json +48 -3
  321. package/validation/tests/jobProperties/optRoleSel.json +12 -2
  322. package/validation/tests/jobProperties/phOnly.json +6 -16
  323. package/validation/tests/jobProperties/pseudoP.json +17 -2
  324. package/validation/tests/jobProperties/radioSet.json +30 -30
  325. package/validation/tests/jobProperties/role.json +25 -5
  326. package/validation/tests/jobProperties/secHeading.json +26 -21
  327. package/validation/tests/jobProperties/styleDiff.json +35 -5
  328. package/validation/tests/jobProperties/tabNav.json +3 -1
  329. package/validation/tests/jobProperties/targetsNear.json +237 -0
  330. package/validation/tests/jobProperties/textNodes.json +29 -29
  331. package/validation/tests/jobProperties/textSem.json +26 -1
  332. package/validation/tests/jobProperties/title.json +12 -2
  333. package/validation/tests/jobProperties/titledEl.json +39 -9
  334. package/validation/tests/jobProperties/userPath.json +317 -0
  335. package/validation/tests/jobProperties/zIndex.json +40 -35
  336. package/validation/tests/targets/allCapStyle/index.html +26 -0
  337. package/validation/tests/targets/checkpoint/index.html +38 -0
  338. package/validation/tests/targets/datalistRef/index.html +1 -1
  339. package/validation/tests/targets/focAndOp/bad.html +29 -0
  340. package/validation/tests/targets/{focOp → focAndOp}/good.html +3 -1
  341. package/validation/tests/targets/focInd/bad.html +2 -1
  342. package/validation/tests/targets/headEl/index.html +10 -1
  343. package/validation/tests/targets/{targetSmall → targetsNear}/index.html +15 -1
  344. package/validation/tests/targets/userPath/index.html +42 -0
  345. package/validation/validateTest.js +61 -9
  346. package/.claude/settings.local.json +0 -11
  347. package/.eslintrc.json +0 -41
  348. package/htmlcs/.eslintrc.json +0 -67
  349. package/validation/tests/jobProperties/linkTitle.json +0 -127
  350. package/validation/tests/jobProperties/opFoc.json +0 -164
  351. package/validation/tests/jobProperties/targetSmall.json +0 -152
  352. package/validation/tests/jobProperties/targetTiny.json +0 -142
  353. package/validation/tests/targets/focOp/bad.html +0 -25
  354. package/validation/tests/targets/linkTitle/index.html +0 -24
  355. package/validation/tests/targets/opFoc/bad.html +0 -26
  356. package/validation/tests/targets/opFoc/good.html +0 -23
@@ -0,0 +1,862 @@
1
+ /*
2
+ © 2023–2025 CVS Health and/or one of its affiliates. All rights reserved.
3
+ © 2026 Jeff Witt.
4
+ © 2025–2026 Jonathan Robert Pool.
5
+
6
+ Licensed under the MIT License. See LICENSE file at the project root or
7
+ https://opensource.org/license/mit/ for details.
8
+
9
+ SPDX-License-Identifier: MIT
10
+ */
11
+
12
+ /*
13
+ testaro
14
+ Implements the Testaro evaluative rules.
15
+ */
16
+
17
+ // IMPORTS
18
+
19
+ // Shared configuration for timeout multiplier.
20
+ import type {Page} from 'playwright';
21
+ import {applyMultiplier, ruleLaunchRetries} from '../procs/config';
22
+ import {browserClose, launch} from '../procs/launch';
23
+ // Function to get an empty standard result.
24
+ import {getStandardResult} from '../procs/standard';
25
+ import {ruleModules} from '../testaro/registry';
26
+ import type {RuleID, RuleModule} from '../testaro/registry';
27
+ import type {Act, BrowserID, Outcome, Report, StandardInstance, UncertaintyCode} from '../types';
28
+
29
+ // TYPES
30
+
31
+ // Metadata of one rule.
32
+ interface RuleMeta {
33
+ id: RuleID;
34
+ what: string;
35
+ contaminates: boolean;
36
+ needsAccessibleName: boolean;
37
+ // Default outcome of the rule's violations ('failed' unless specified): 'cantTell' for a
38
+ // heuristic rule whose violations need human confirmation; a violation may override it.
39
+ outcome?: Outcome;
40
+ // Default uncertainty code of the rule's cantTell violations.
41
+ uncertainty?: UncertaintyCode;
42
+ // Whether the rule's verdict on an element depends only on that element's subtree and
43
+ // what it references, so a changed-scope test act may restrict the rule to changed subtrees.
44
+ // Page-level rules (heading order, landmarks, duplicate IDs, focus order, hover, motion, …)
45
+ // are not local: any change can alter their verdicts, so they always test the whole page.
46
+ local: boolean;
47
+ timeOut: number;
48
+ defaultOn: boolean;
49
+ // Read below but defined by no rule, so the temporary-directory branch never runs;
50
+ // verbatim from the original.
51
+ needsTmpDir?: boolean;
52
+ }
53
+ // The testaro-act properties this reporter consumes.
54
+ interface TestaroAct extends Act {
55
+ target?: {url: string; what?: string};
56
+ args?: Record<string, unknown[]>;
57
+ stopOnFail?: boolean;
58
+ withItems?: boolean;
59
+ rules?: string[];
60
+ launch?: {browserID?: BrowserID};
61
+ scope?: 'page' | 'changed';
62
+ // Launch retries per rule (default TESTARO_RULE_RETRIES, else 2).
63
+ retries?: number;
64
+ }
65
+ // The result of one rule test.
66
+ interface RuleTestResult {
67
+ id: RuleID;
68
+ prevented: boolean;
69
+ error: string;
70
+ data: unknown;
71
+ totals: number[];
72
+ instances: StandardInstance[];
73
+ elapsedTime: number;
74
+ }
75
+
76
+ // CONSTANTS
77
+
78
+ // Metadata of all rules in default execution order.
79
+ const allRules: RuleMeta[] = [
80
+ {
81
+ id: 'adbID',
82
+ what: 'elements with ambiguous or missing referenced descriptions',
83
+ contaminates: false,
84
+ needsAccessibleName: false,
85
+ local: true,
86
+ timeOut: 5,
87
+ defaultOn: true
88
+ },
89
+ {
90
+ id: 'allCapStyle',
91
+ what: 'elements with all-capital text transformation styles',
92
+ contaminates: false,
93
+ needsAccessibleName: false,
94
+ local: true,
95
+ timeOut: 5,
96
+ defaultOn: false
97
+ },
98
+ {
99
+ id: 'allCaps',
100
+ what: 'elements with unnecessarily all-capital text substrings',
101
+ contaminates: false,
102
+ needsAccessibleName: false,
103
+ local: true,
104
+ outcome: 'cantTell',
105
+ uncertainty: 'judgement-required',
106
+ timeOut: 30,
107
+ defaultOn: true
108
+ },
109
+ {
110
+ id: 'allHidden',
111
+ what: 'page that is entirely or mostly hidden',
112
+ contaminates: false,
113
+ needsAccessibleName: false,
114
+ local: false,
115
+ timeOut: 5,
116
+ defaultOn: true
117
+ },
118
+ {
119
+ id: 'allSlanted',
120
+ what: 'leaf elements with entirely italic or oblique text longer than 39 characters',
121
+ contaminates: false,
122
+ needsAccessibleName: false,
123
+ local: true,
124
+ timeOut: 5,
125
+ defaultOn: true
126
+ },
127
+ {
128
+ id: 'altScheme',
129
+ what: 'img elements with alt attributes having URLs as their entire values',
130
+ contaminates: false,
131
+ needsAccessibleName: false,
132
+ local: true,
133
+ timeOut: 5,
134
+ defaultOn: true
135
+ },
136
+ {
137
+ id: 'attVal',
138
+ what: 'elements with attributes having illicit values',
139
+ contaminates: false,
140
+ needsAccessibleName: false,
141
+ local: true,
142
+ timeOut: 5,
143
+ defaultOn: false
144
+ },
145
+ {
146
+ id: 'bulk',
147
+ what: 'large count of visible elements',
148
+ contaminates: false,
149
+ needsAccessibleName: false,
150
+ local: false,
151
+ timeOut: 5,
152
+ defaultOn: true
153
+ },
154
+ {
155
+ id: 'captionLoc',
156
+ what: 'caption elements that are not first children of table elements',
157
+ contaminates: false,
158
+ needsAccessibleName: false,
159
+ local: true,
160
+ timeOut: 5,
161
+ defaultOn: true
162
+ },
163
+ {
164
+ id: 'datalistRef',
165
+ what: 'elements with ambiguous or missing referenced datalist elements',
166
+ contaminates: false,
167
+ needsAccessibleName: false,
168
+ local: true,
169
+ timeOut: 5,
170
+ defaultOn: true
171
+ },
172
+ {
173
+ id: 'distortion',
174
+ what: 'distorted text',
175
+ contaminates: false,
176
+ needsAccessibleName: false,
177
+ local: true,
178
+ timeOut: 5,
179
+ defaultOn: true
180
+ },
181
+ {
182
+ id: 'docType',
183
+ what: 'document without a doctype property',
184
+ contaminates: false,
185
+ needsAccessibleName: false,
186
+ local: false,
187
+ timeOut: 5,
188
+ defaultOn: true
189
+ },
190
+ {
191
+ id: 'embAc',
192
+ what: 'active elements embedded in links or buttons',
193
+ contaminates: false,
194
+ needsAccessibleName: false,
195
+ local: true,
196
+ timeOut: 5,
197
+ defaultOn: true
198
+ },
199
+ {
200
+ id: 'headEl',
201
+ what: 'invalid elements within the head',
202
+ contaminates: false,
203
+ needsAccessibleName: false,
204
+ local: false,
205
+ timeOut: 5,
206
+ defaultOn: true
207
+ },
208
+ {
209
+ id: 'headingAmb',
210
+ what: 'same-level sibling headings with identical texts',
211
+ contaminates: false,
212
+ needsAccessibleName: false,
213
+ local: false,
214
+ timeOut: 5,
215
+ defaultOn: true
216
+ },
217
+ {
218
+ id: 'hr',
219
+ what: 'hr element instead of styles used for vertical segmentation',
220
+ contaminates: false,
221
+ needsAccessibleName: false,
222
+ local: true,
223
+ timeOut: 5,
224
+ defaultOn: true
225
+ },
226
+ {
227
+ id: 'imageLink',
228
+ what: 'links with image files as their destinations',
229
+ contaminates: false,
230
+ needsAccessibleName: false,
231
+ local: true,
232
+ timeOut: 5,
233
+ defaultOn: true
234
+ },
235
+ {
236
+ id: 'labClash',
237
+ what: 'labeling inconsistencies',
238
+ contaminates: false,
239
+ needsAccessibleName: false,
240
+ local: false,
241
+ timeOut: 5,
242
+ defaultOn: true
243
+ },
244
+ {
245
+ id: 'legendLoc',
246
+ what: 'legend elements that are not first children of fieldset elements',
247
+ contaminates: false,
248
+ needsAccessibleName: false,
249
+ local: true,
250
+ timeOut: 5,
251
+ defaultOn: true
252
+ },
253
+ {
254
+ id: 'lineHeight',
255
+ what: 'text with a line height less than 1.5 times its font size',
256
+ contaminates: false,
257
+ needsAccessibleName: false,
258
+ local: true,
259
+ timeOut: 5,
260
+ defaultOn: true
261
+ },
262
+ {
263
+ id: 'linkAmb',
264
+ what: 'links with identical texts but different destinations',
265
+ contaminates: false,
266
+ needsAccessibleName: false,
267
+ local: false,
268
+ timeOut: 20,
269
+ defaultOn: true
270
+ },
271
+ {
272
+ id: 'linkExt',
273
+ what: 'links that automatically open new windows',
274
+ contaminates: false,
275
+ needsAccessibleName: false,
276
+ local: true,
277
+ timeOut: 5,
278
+ defaultOn: true
279
+ },
280
+ {
281
+ id: 'linkOldAtt',
282
+ what: 'links with deprecated attributes',
283
+ contaminates: false,
284
+ needsAccessibleName: false,
285
+ local: true,
286
+ timeOut: 5,
287
+ defaultOn: true
288
+ },
289
+ {
290
+ id: 'linkTo',
291
+ what: 'links without destinations',
292
+ contaminates: false,
293
+ needsAccessibleName: false,
294
+ local: true,
295
+ timeOut: 5,
296
+ defaultOn: true
297
+ },
298
+ {
299
+ id: 'linkUl',
300
+ what: 'missing underlines on inline links',
301
+ contaminates: false,
302
+ needsAccessibleName: false,
303
+ local: true,
304
+ timeOut: 5,
305
+ defaultOn: true
306
+ },
307
+ {
308
+ id: 'miniText',
309
+ what: 'text smaller than 11 pixels',
310
+ contaminates: false,
311
+ needsAccessibleName: false,
312
+ local: true,
313
+ timeOut: 5,
314
+ defaultOn: true
315
+ },
316
+ {
317
+ id: 'nonTable',
318
+ what: 'table elements used for layout',
319
+ contaminates: false,
320
+ needsAccessibleName: false,
321
+ local: true,
322
+ timeOut: 5,
323
+ defaultOn: true
324
+ },
325
+ {
326
+ id: 'optRoleSel',
327
+ what: 'Non-option elements with option roles that have no aria-selected attributes',
328
+ contaminates: false,
329
+ needsAccessibleName: false,
330
+ local: true,
331
+ timeOut: 5,
332
+ defaultOn: true
333
+ },
334
+ {
335
+ id: 'pseudoP',
336
+ what: 'adjacent br elements suspected of nonsemantically simulating p elements',
337
+ contaminates: false,
338
+ needsAccessibleName: false,
339
+ local: true,
340
+ timeOut: 5,
341
+ defaultOn: true
342
+ },
343
+ {
344
+ id: 'radioSet',
345
+ what: 'radio buttons not grouped into standard field sets',
346
+ contaminates: false,
347
+ needsAccessibleName: false,
348
+ local: false,
349
+ timeOut: 5,
350
+ defaultOn: true
351
+ },
352
+ {
353
+ id: 'role',
354
+ what: 'native-replacing explicit roles',
355
+ contaminates: false,
356
+ needsAccessibleName: false,
357
+ local: false,
358
+ timeOut: 20,
359
+ defaultOn: true
360
+ },
361
+ {
362
+ id: 'secHeading',
363
+ what: 'headings that violate the logical level order in their sectioning containers',
364
+ contaminates: false,
365
+ needsAccessibleName: false,
366
+ local: false,
367
+ timeOut: 5,
368
+ defaultOn: true
369
+ },
370
+ {
371
+ id: 'styleDiff',
372
+ what: 'style inconsistencies',
373
+ contaminates: false,
374
+ needsAccessibleName: false,
375
+ local: false,
376
+ timeOut: 5,
377
+ defaultOn: true
378
+ },
379
+ {
380
+ id: 'targetsNear',
381
+ what: 'labels, buttons, inputs, and links too near each other',
382
+ contaminates: false,
383
+ needsAccessibleName: false,
384
+ local: false,
385
+ timeOut: 5,
386
+ defaultOn: true
387
+ },
388
+ {
389
+ id: 'textSem',
390
+ what: 'semantically vague elements i, b, and/or small',
391
+ contaminates: false,
392
+ needsAccessibleName: false,
393
+ local: true,
394
+ timeOut: 5,
395
+ defaultOn: true
396
+ },
397
+ {
398
+ id: 'title',
399
+ what: 'page title',
400
+ contaminates: false,
401
+ needsAccessibleName: false,
402
+ local: false,
403
+ timeOut: 5,
404
+ defaultOn: false
405
+ },
406
+ {
407
+ id: 'titledEl',
408
+ what: 'title attributes on inappropriate elements',
409
+ contaminates: false,
410
+ needsAccessibleName: false,
411
+ local: true,
412
+ timeOut: 5,
413
+ defaultOn: true
414
+ },
415
+ {
416
+ id: 'zIndex',
417
+ what: 'non-default Z indexes',
418
+ contaminates: false,
419
+ needsAccessibleName: false,
420
+ local: true,
421
+ timeOut: 5,
422
+ defaultOn: true
423
+ },
424
+ {
425
+ id: 'motion',
426
+ what: 'motion without user request',
427
+ contaminates: false,
428
+ needsAccessibleName: false,
429
+ local: false,
430
+ // The budget must cover a full-page screenshot (itself allowed 4 seconds in
431
+ // procs/shoot.js), decoding two full-page PNGs, and a pixel comparison; 5
432
+ // seconds made the rule time out whenever an initial image existed.
433
+ timeOut: 30,
434
+ defaultOn: true
435
+ },
436
+ {
437
+ id: 'dupAtt',
438
+ what: 'duplicate attribute values',
439
+ contaminates: false,
440
+ needsAccessibleName: false,
441
+ local: false,
442
+ timeOut: 5,
443
+ defaultOn: true
444
+ },
445
+ {
446
+ id: 'autocomplete',
447
+ what: 'name and email inputs without autocomplete attributes',
448
+ contaminates: false,
449
+ needsAccessibleName: true,
450
+ local: true,
451
+ timeOut: 5,
452
+ defaultOn: true
453
+ },
454
+ {
455
+ id: 'phOnly',
456
+ what: 'input elements with placeholders but no accessible names',
457
+ contaminates: false,
458
+ needsAccessibleName: true,
459
+ local: true,
460
+ timeOut: 5,
461
+ defaultOn: true
462
+ },
463
+ {
464
+ id: 'buttonMenu',
465
+ what: 'nonstandard keyboard navigation between items of button-controlled menus',
466
+ contaminates: true,
467
+ needsAccessibleName: false,
468
+ local: false,
469
+ timeOut: 15,
470
+ defaultOn: true
471
+ },
472
+ {
473
+ id: 'elements',
474
+ what: 'data on specified elements',
475
+ contaminates: true,
476
+ needsAccessibleName: false,
477
+ local: false,
478
+ timeOut: 10,
479
+ defaultOn: false
480
+ },
481
+ {
482
+ id: 'focAll',
483
+ what: 'discrepancies between focusable and Tab-focused elements',
484
+ contaminates: true,
485
+ needsAccessibleName: false,
486
+ local: false,
487
+ timeOut: 10,
488
+ defaultOn: true
489
+ },
490
+ {
491
+ id: 'focAndOp',
492
+ what: 'Tab-focusable elements that are not operable or vice versa',
493
+ contaminates: true,
494
+ needsAccessibleName: false,
495
+ local: false,
496
+ timeOut: 5,
497
+ defaultOn: true
498
+ },
499
+ {
500
+ id: 'focInd',
501
+ what: 'missing and nonstandard focus indicators',
502
+ contaminates: true,
503
+ needsAccessibleName: false,
504
+ local: false,
505
+ timeOut: 10,
506
+ defaultOn: true
507
+ },
508
+ {
509
+ id: 'focVis',
510
+ what: 'links that are not entirely visible when focused',
511
+ contaminates: true,
512
+ needsAccessibleName: false,
513
+ local: false,
514
+ timeOut: 10,
515
+ defaultOn: true
516
+ },
517
+ {
518
+ id: 'hover',
519
+ what: 'hover-caused content changes',
520
+ contaminates: true,
521
+ needsAccessibleName: false,
522
+ local: false,
523
+ timeOut: 20,
524
+ defaultOn: true
525
+ },
526
+ {
527
+ id: 'hovInd',
528
+ what: 'hover indication nonstandard',
529
+ contaminates: true,
530
+ needsAccessibleName: false,
531
+ local: false,
532
+ timeOut: 10,
533
+ defaultOn: true
534
+ },
535
+ {
536
+ id: 'tabNav',
537
+ what: 'nonstandard keyboard navigation between elements with the tab role',
538
+ contaminates: true,
539
+ needsAccessibleName: false,
540
+ local: false,
541
+ timeOut: 10,
542
+ defaultOn: true
543
+ },
544
+ {
545
+ id: 'textNodes',
546
+ what: 'data on specified text nodes',
547
+ contaminates: true,
548
+ needsAccessibleName: false,
549
+ local: false,
550
+ timeOut: 10,
551
+ defaultOn: false
552
+ }
553
+ ];
554
+
555
+ // ERROR HANDLER
556
+ process.on('unhandledRejection', reason => {
557
+ console.error(`ERROR: Unhandled Promise Rejection (${reason})`);
558
+ });
559
+
560
+ // FUNCTIONS
561
+
562
+ // Conducts and reports Testaro tests.
563
+ export const reporter = async (page: Page | undefined, report: Report, actIndex: number) => {
564
+ const act = report.acts[actIndex] as TestaroAct;
565
+ const givenPage = page;
566
+ const {args, stopOnFail, withItems} = act;
567
+ // A testaro act always has a target on itself or the report; verbatim from the original.
568
+ const target = (act.target || report.target)!;
569
+ const url = target.url;
570
+ const browserID = act.launch ? act.launch.browserID || report.browserID : report.browserID;
571
+ const argRules = args ? Object.keys(args) : null;
572
+ // Get the specification of rules to be tested for or, by default, all rules with defaultOn true.
573
+ const ruleSpec = act.rules
574
+ || ['y', ... allRules.filter(rule => rule.defaultOn).map(rule => rule.id)];
575
+ // Initialize the act data.
576
+ const data: {
577
+ prevented: boolean;
578
+ error: string;
579
+ rulePreventions: Record<string, string>;
580
+ rulesInvalid: string[];
581
+ ruleTestTimes: [string, number][];
582
+ ruleData: Record<string, unknown>;
583
+ scope?: {localRules: string[]; pageRules: string[]};
584
+ } = {
585
+ prevented: false,
586
+ error: '',
587
+ rulePreventions: {},
588
+ rulesInvalid: [],
589
+ ruleTestTimes: [],
590
+ ruleData: {}
591
+ };
592
+ // The changed subtree roots the act is scoped to, if any (report.scope, set by the acts loop).
593
+ const scopeRoots = report.scope?.roots ?? null;
594
+ if (scopeRoots) {
595
+ data.scope = {localRules: [], pageRules: []};
596
+ }
597
+ // Initialize the act result.
598
+ const result: {
599
+ nativeResult: Record<string, unknown>;
600
+ standardResult: {
601
+ prevented: boolean;
602
+ totals: number[];
603
+ outcomeTotals: Record<Outcome, number>;
604
+ instances: StandardInstance[];
605
+ };
606
+ } = {
607
+ nativeResult: {},
608
+ standardResult: getStandardResult() as {
609
+ prevented: boolean;
610
+ totals: number[];
611
+ outcomeTotals: Record<Outcome, number>;
612
+ instances: StandardInstance[];
613
+ }
614
+ };
615
+ const {standardResult} = result;
616
+ const allRuleIDs = allRules.map(rule => rule.id);
617
+ if (
618
+ // If the rule specification has at least 2 items
619
+ ruleSpec.length > 1
620
+ // and the first item is y or n
621
+ && ['y', 'n'].includes(ruleSpec[0])
622
+ // and all subsequent items are rule IDs to be included (if y) or excluded (if n):
623
+ && ruleSpec.slice(1).every(ruleID => (allRuleIDs as string[]).includes(ruleID))
624
+ ) {
625
+ // Get the rules to be tested for and their execution order.
626
+ const excludeIDs = ruleSpec.slice(1);
627
+ const jobRuleIDs = ruleSpec[0] === 'y'
628
+ ? excludeIDs
629
+ : allRules
630
+ .filter(rule => rule.defaultOn && ! excludeIDs.includes(rule.id))
631
+ .map(rule => rule.id);
632
+ const jobRules = allRules.filter(rule => jobRuleIDs.includes(rule.id));
633
+ let justPrevented = false;
634
+ // For each rule to be tested for:
635
+ for (let ruleIndex = 0; ruleIndex < jobRules.length; ruleIndex++) {
636
+ const rule = jobRules[ruleIndex];
637
+ // Initialize the rule result.
638
+ const ruleResult: RuleTestResult = {
639
+ id: rule.id,
640
+ prevented: false,
641
+ error: '',
642
+ data: {},
643
+ totals: [0, 0, 0, 0],
644
+ instances: [],
645
+ elapsedTime: 0
646
+ };
647
+ console.log(`Starting rule ${ruleResult.id}`);
648
+ // Make the browser emulate headedness in all cases, because performance does not suffer.
649
+ const headEmulation = ruleResult.id.startsWith('shoot') ? 'high' : 'high';
650
+ // Get whether the rule needs a new browser launched. Under page isolation the first rules
651
+ // run on the live page provided; a contaminating rule still gets a fresh page.
652
+ const previousRule = ruleIndex > 0 ? jobRules[ruleIndex - 1] : null;
653
+ const needsLaunch = (ruleIndex === 0 && ! (page && report.jobData?.isolation === 'page'))
654
+ || justPrevented
655
+ || Boolean(previousRule?.contaminates)
656
+ || jobRules[ruleIndex].needsAccessibleName && ! previousRule?.needsAccessibleName
657
+ && ! (ruleIndex === 0 && page);
658
+ const pageClosed = page && page.isClosed();
659
+ // If it does, or if the page has closed:
660
+ if (needsLaunch || pageClosed) {
661
+ // If the page has closed when it is expected to be open:
662
+ if (pageClosed && ! needsLaunch) {
663
+ // Report this.
664
+ console.log(`WARNING: Relaunching browser for test ${rule} after abnormal closure`);
665
+ }
666
+ // Create a browser, replace the page, and visit the target, retrying twice if necessary.
667
+ page = await launch({
668
+ report,
669
+ actIndex,
670
+ tempBrowserID: browserID,
671
+ tempURL: url,
672
+ headEmulation,
673
+ xPathNeed: 'script',
674
+ needsAccessibleName: jobRules[ruleIndex].needsAccessibleName,
675
+ retries: Number.isInteger(act.retries) && (act.retries as number) >= 0
676
+ ? act.retries as number
677
+ : ruleLaunchRetries
678
+ });
679
+ }
680
+ // If no page exists, the launch (or the replay of a checkpoint's acts) failed: the
681
+ // target is unreachable, so prevent the act and stop testing rules.
682
+ if (! page) {
683
+ const message = String(
684
+ report.jobData?.abortMessage
685
+ || `Launch or checkpoint replay failed before rule ${ruleResult.id}`
686
+ );
687
+ ruleResult.prevented = true;
688
+ ruleResult.error = message;
689
+ data.rulePreventions[ruleResult.id] = message;
690
+ data.prevented = true;
691
+ data.error = message;
692
+ standardResult.prevented = true;
693
+ console.log(`ERROR: ${message}`);
694
+ break;
695
+ }
696
+ // Report crashes and disconnections during this test.
697
+ let crashHandler: (() => void) | null | undefined;
698
+ let disconnectHandler: (() => void) | null | undefined;
699
+ if (page && ! page.isClosed()) {
700
+ crashHandler = () => {
701
+ console.log(`ERROR: Page crashed during ${rule} test`);
702
+ };
703
+ page.on('crash', crashHandler);
704
+ }
705
+ // The page exists whenever this runs; verbatim from the original.
706
+ const browser = page!.context().browser();
707
+ if (browser) {
708
+ disconnectHandler = () => {
709
+ console.log(`ERROR: Browser disconnected during ${rule} test`);
710
+ };
711
+ browser.on('disconnected', disconnectHandler);
712
+ }
713
+ // Restrict an element-local rule to the scope roots; a page-level rule tests the page.
714
+ if (scopeRoots) {
715
+ report.ruleScopeRoots = rule.local ? scopeRoots : null;
716
+ (rule.local ? data.scope!.localRules : data.scope!.pageRules).push(rule.id);
717
+ }
718
+ // Initialize an argument array for the reporter.
719
+ const ruleArgs: unknown[] = [page, report, actIndex, withItems];
720
+ // If the rule needs a temporary directory:
721
+ if (rule.needsTmpDir) {
722
+ // Add its path to the argument array.
723
+ ruleArgs.push(report.jobData!.tmpDir);
724
+ }
725
+ // If the testaro test act specifies extra arguments for this rule:
726
+ if (argRules?.includes(ruleResult.id)) {
727
+ // Add them to the argument array.
728
+ ruleArgs.push(... args![ruleResult.id]);
729
+ }
730
+ const startTime = Date.now();
731
+ let timer: Promise<{timedOut: true}>;
732
+ try {
733
+ // Apply a time limit to the test.
734
+ const timeLimit = applyMultiplier(1000 * rule.timeOut);
735
+ let timeout: NodeJS.Timeout;
736
+ // If the time limit expires during the test:
737
+ timer = new Promise(resolve => {
738
+ timeout = setTimeout(() => {
739
+ // Add data about the timeout to the rule result.
740
+ justPrevented = true;
741
+ ruleResult.prevented = true;
742
+ ruleResult.error = 'Timeout';
743
+ console.log(`ERROR: Test of testaro rule ${ruleResult.id} timed out`);
744
+ resolve({timedOut: true});
745
+ }, timeLimit);
746
+ });
747
+ // Try to perform the test and get a test report, loading the rule via the registry.
748
+ // The RuleModule cast widens the per-rule signature union to the registry contract.
749
+ const testReport = (ruleModules[ruleResult.id]() as RuleModule).reporter(... ruleArgs);
750
+ // Get a test or timeout report.
751
+ const ruleReport: {
752
+ timedOut?: boolean;
753
+ data?: unknown;
754
+ totals?: number[];
755
+ standardInstances?: unknown[];
756
+ } = await Promise.race([timer, testReport]);
757
+ clearTimeout(timeout!);
758
+ // If it was a test report:
759
+ if (! ruleReport.timedOut) {
760
+ // Add the rule-report properties to the rule result.
761
+ ruleResult.data = ruleReport.data;
762
+ ruleResult.totals = ruleReport.totals as number[];
763
+ ruleResult.instances = ruleReport.standardInstances as StandardInstance[];
764
+ // Add the rule-result properties to the result.
765
+ if (Object.keys(ruleReport.data as Record<string, unknown>).length) {
766
+ data.ruleData[ruleResult.id] = ruleResult.data;
767
+ }
768
+ if (ruleResult.totals) {
769
+ ruleResult.totals.forEach((total, index) => {
770
+ standardResult.totals[index] += Math.round(total);
771
+ });
772
+ }
773
+ if (ruleResult.instances?.length) {
774
+ // Apply the rule's default outcome to instances without their own.
775
+ ruleResult.instances.forEach(instance => {
776
+ instance.outcome ??= rule.outcome ?? 'failed';
777
+ if (instance.outcome === 'cantTell' && rule.uncertainty) {
778
+ instance.uncertainty ??= rule.uncertainty;
779
+ }
780
+ standardResult.outcomeTotals[instance.outcome] += instance.count || 1;
781
+ });
782
+ standardResult.instances.push(... ruleResult.instances);
783
+ }
784
+ justPrevented = false;
785
+ // If testing is to stop after a failure and the page failed the test:
786
+ if (stopOnFail && ruleReport.totals?.some(total => total)) {
787
+ // Test for no more rules.
788
+ break;
789
+ }
790
+ }
791
+ }
792
+ // If an error is thrown by the test:
793
+ catch(error) {
794
+ ruleResult.prevented = true;
795
+ justPrevented = true;
796
+ const isPageClosed = ['closed', 'Protocol error', 'Target page'].some(phrase =>
797
+ (error as Error).message.includes(phrase)
798
+ );
799
+ // If the page has closed:
800
+ if (isPageClosed) {
801
+ // Report this.
802
+ console.log(`ERROR: Test ${ruleResult.id} failed because page closed`);
803
+ }
804
+ // Otherwise, i.e. if the page is open:
805
+ else {
806
+ // Add this to the rule result.
807
+ ruleResult.error = (error as Error).message;
808
+ console.log(
809
+ `ERROR: Test of testaro rule ${ruleResult.id} prevented (${(error as Error).message})`
810
+ );
811
+ }
812
+ }
813
+ finally {
814
+ // Add the elapsed time to the rule result.
815
+ ruleResult.elapsedTime = Math.round((Date.now() - startTime) / 1000);
816
+ // Add the elapsed time to the data.
817
+ data.ruleTestTimes.push([ruleResult.id, ruleResult.elapsedTime]);
818
+ // If the test timed out or otherwise failed:
819
+ if (ruleResult.prevented) {
820
+ // Add this and the error to the data.
821
+ data.rulePreventions[ruleResult.id] = ruleResult.error;
822
+ }
823
+ }
824
+ // Sort the rule test times.
825
+ data.ruleTestTimes.sort((a, b) => b[1] - a[1]);
826
+ // Clear the error listeners.
827
+ if (page && ! page.isClosed() && crashHandler) {
828
+ page.off('crash', crashHandler);
829
+ crashHandler = null;
830
+ }
831
+ if (browser && disconnectHandler) {
832
+ browser.off('disconnected', disconnectHandler);
833
+ disconnectHandler = null;
834
+ }
835
+ // Force a garbage collection.
836
+ try {
837
+ if (global.gc) {
838
+ global.gc();
839
+ }
840
+ }
841
+ catch(error) {}
842
+ };
843
+ }
844
+ // Otherwise, i.e. if the rule specification is invalid:
845
+ else {
846
+ // Report this and stop testing.
847
+ standardResult.prevented = true;
848
+ data.prevented = true;
849
+ const message = 'ERROR: Testaro rule specification invalid';
850
+ data.error = message;
851
+ console.log(message);
852
+ }
853
+ // Close the last page the tool launched, unless it is the live page it was given.
854
+ if (page && page !== givenPage) {
855
+ await browserClose(page);
856
+ }
857
+ delete report.ruleScopeRoots;
858
+ return {
859
+ data,
860
+ result
861
+ };
862
+ };