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
package/procs/launch.js CHANGED
@@ -17,6 +17,8 @@
17
17
  // IMPORTS
18
18
 
19
19
  const {addError} = require('./error');
20
+ const fs = require('fs');
21
+ const path = require('path');
20
22
  const {posix: posixPath} = require('path');
21
23
  const headedBrowser = process.env.HEADED_BROWSER === 'true';
22
24
  // Two flavors of Playwright:
@@ -30,6 +32,10 @@ const headedBrowser = process.env.HEADED_BROWSER === 'true';
30
32
  const playwrightCore = require('playwright');
31
33
  const playwrightExtra = require('playwright-extra');
32
34
  const {isBrowserID, isDeviceID, isURL, isValidJob} = require('./job');
35
+ // The in-page script defining window.getXPath.
36
+ const {getXPathSource} = require('./xPathScript');
37
+ // Deployment options (navigation, scanner identity, pre-scan scroll).
38
+ const {getNavigation, getScannerId, getScroll, waitStates} = require('./config');
33
39
 
34
40
  // CONSTANTS
35
41
 
@@ -81,6 +87,43 @@ const wait = exports.wait = ms => {
81
87
  // Removes any trailing slashes from a URL, for redirection comparison.
82
88
  const deSlash = url => (url || '').replace(/\/+$/, '');
83
89
  // Close a browser context and/or its browser, if they exist.
90
+ // Grace period for browser-close operations. Chromium can acknowledge a page
91
+ // close without performing it when the close races a navigation commit
92
+ // (https://issues.chromium.org/issues/536385539, observed on pages that
93
+ // navigate via meta refresh during testing); the close promise then never
94
+ // settles — it neither resolves nor rejects — so an unguarded await would
95
+ // hang the act forever. Errors are discarded, as before.
96
+ const CLOSE_GRACE_MS = 10000;
97
+ const settleWithin = promise => Promise.race([
98
+ promise.catch(error => {}),
99
+ new Promise(resolve => {
100
+ const timer = setTimeout(resolve, CLOSE_GRACE_MS);
101
+ // Do not keep the process alive for an abandoned close.
102
+ if (typeof timer.unref === 'function') {
103
+ timer.unref();
104
+ }
105
+ })
106
+ ]);
107
+ /*
108
+ The browser shared by the launches of a job under browser or page isolation, if any. When set,
109
+ launchOnce creates contexts in it instead of launching browsers, and browserClose closes only
110
+ the context of a page it owns. The job closes it with closeSharedBrowser at its end.
111
+ */
112
+ let sharedBrowser = null;
113
+ let sharedBrowserID = '';
114
+ exports.setSharedBrowser = (browser, browserID) => {
115
+ sharedBrowser = browser;
116
+ sharedBrowserID = browserID;
117
+ };
118
+ exports.getSharedBrowser = () => sharedBrowser;
119
+ exports.closeSharedBrowser = async () => {
120
+ if (sharedBrowser) {
121
+ const browser = sharedBrowser;
122
+ sharedBrowser = null;
123
+ sharedBrowserID = '';
124
+ await settleWithin(browser.close());
125
+ }
126
+ };
84
127
  const browserClose = exports.browserClose = async page => {
85
128
  if (page) {
86
129
  // Get the context (i.e. window) of the page and the browser of the context. These are methods, not properties; referencing them as properties made this function silently fail to close anything, because a function object has no close method and the resulting TypeError was caught and discarded.
@@ -88,15 +131,10 @@ const browserClose = exports.browserClose = async page => {
88
131
  if (browserContext) {
89
132
  // The browser is null for a context not owned by a browser, such as a persistent context.
90
133
  const browser = browserContext.browser();
91
- try {
92
- await browserContext.close();
93
- }
94
- catch(error) {}
95
- if (browser) {
96
- try {
97
- await browser.close();
98
- }
99
- catch(error) {}
134
+ await settleWithin(browserContext.close());
135
+ // Close the browser too, unless the job shares it.
136
+ if (browser && browser !== sharedBrowser) {
137
+ await settleWithin(browser.close());
100
138
  }
101
139
  }
102
140
  }
@@ -130,10 +168,18 @@ const normalizeURL = url => {
130
168
  };
131
169
  // Visits a URL and returns the response of the server.
132
170
  const goTo = exports.goTo = async (report, page, url, timeout, waitUntil) => {
133
- // If the URL is a file path relative to the project root:
171
+ // If the URL is that of a local file:
134
172
  if (url.startsWith('file://')) {
135
- // Make it the absolute path to the specified file.
136
- url = url.replace('file://', `file://${__dirname}/../`);
173
+ const filePath = url.replace(/^file:\/+/, '/');
174
+ const projectRoot = path.resolve(__dirname, '..');
175
+ // If the path is not absolute (inside the project or existing on disk), it is relative to
176
+ // the project root (file://validation/…), as job files write it: make it absolute.
177
+ if (! filePath.startsWith(`${projectRoot}/`) && ! fs.existsSync(filePath)) {
178
+ url = `file://${projectRoot}/${filePath.replace(/^\/+/, '')}`;
179
+ }
180
+ else {
181
+ url = `file://${filePath}`;
182
+ }
137
183
  }
138
184
  // Visit the URL.
139
185
  const startTime = Date.now();
@@ -209,8 +255,8 @@ const goTo = exports.goTo = async (report, page, url, timeout, waitUntil) => {
209
255
  let waitSeconds = 5;
210
256
  if (retryHeader) {
211
257
  waitSeconds = Number.isNaN(Number(retryHeader))
212
- ? Math.ceil((new Date(retryHeader) - new Date()) / 1000)
213
- : Number(retryHeader);
258
+ ? Math.ceil((new Date(retryHeader) - new Date()) / 1000)
259
+ : Number(retryHeader);
214
260
  }
215
261
  // Return this.
216
262
  console.log(
@@ -272,6 +318,63 @@ const getNonce = exports.getNonce = async response => {
272
318
  // Return the nonce, if any.
273
319
  return nonce;
274
320
  };
321
+ // Path of the dom-accessibility-api bundle.
322
+ const nameComputationPath = require.resolve('../dist/nameComputation.js');
323
+ // Defines the accessible-name window methods in the page. Runs inside the page: closure-free.
324
+ const installAccessibleName = () => {
325
+ // Add a window method to compute the accessible name of an element.
326
+ window.getAccessibleName = element => {
327
+ const nameIsComputable = element?.nodeType === Node.ELEMENT_NODE
328
+ && typeof window.computeAccessibleName === 'function';
329
+ return nameIsComputable ? window.computeAccessibleName(element) : '';
330
+ };
331
+ // Add a window method to return a standard proto-instance.
332
+ window.getProtoInstance = (
333
+ element, ruleID, what, count = 1, ordinalSeverity, summaryTagName = '', outcome = 'failed'
334
+ ) => {
335
+ // If an element has been specified:
336
+ if (element) {
337
+ // Get its properties.
338
+ return {
339
+ ruleID,
340
+ what,
341
+ count,
342
+ ordinalSeverity,
343
+ outcome,
344
+ pathID: window.getXPath(element)
345
+ };
346
+ }
347
+ // Otherwise, i.e. if no element has been specified, return a summary instance.
348
+ return {
349
+ ruleID,
350
+ what,
351
+ count,
352
+ ordinalSeverity,
353
+ outcome
354
+ };
355
+ };
356
+ };
357
+ const accessibleNameSource = `(${installAccessibleName.toString()})();`;
358
+ // Prepares an already loaded page (the live page of a checkpoint, under page isolation) for a
359
+ // tool, as launchOnce prepares a page it creates: XPath script or attributes, and accessible
360
+ // names. Evaluates scripts rather than adding script elements, so the DOM is unchanged.
361
+ exports.preparePage = async (page, {xPathNeed = 'script', needsAccessibleName = false} = {}) => {
362
+ if (xPathNeed !== 'none') {
363
+ await page.evaluate(getXPathSource);
364
+ }
365
+ if (xPathNeed === 'attribute') {
366
+ await page.evaluate(() => {
367
+ document.querySelectorAll('*').forEach(element => {
368
+ element.setAttribute('data-xpath', window.getXPath(element));
369
+ });
370
+ });
371
+ }
372
+ if (needsAccessibleName) {
373
+ const nameComputationSource = await fs.promises.readFile(nameComputationPath, 'utf8');
374
+ await page.evaluate(nameComputationSource);
375
+ await page.evaluate(accessibleNameSource);
376
+ }
377
+ };
275
378
  // Creates a browser, context, and page; navigates to a URL; and returns the page.
276
379
  const launchOnce = async opts => {
277
380
  // Get the arguments.
@@ -286,7 +389,9 @@ const launchOnce = async opts => {
286
389
  needsAccessibleName = false,
287
390
  // Extra Playwright context options (e.g. deviceScaleFactor for device-pixel page
288
391
  // images), spread last so they win over the defaults.
289
- contextOverrides = {}
392
+ contextOverrides = {},
393
+ // Whether to replay the active checkpoint's acts after navigating (test-act launches).
394
+ replay = true
290
395
  } = opts;
291
396
  const act = report.acts[actIndex] ?? {};
292
397
  const {device} = report;
@@ -360,10 +465,29 @@ const launchOnce = async opts => {
360
465
  // Disable the sandbox.
361
466
  browserOptions.chromiumSandbox = false;
362
467
  }
468
+ // If the job specifies a branded Chromium channel (chrome or msedge), run that installed
469
+ // browser instead of the bundled Chromium build; bundled or absent keeps the default.
470
+ const {browserChannel} = report;
471
+ if (browserID === 'chromium' && browserChannel && browserChannel !== 'bundled') {
472
+ browserOptions.channel = browserChannel;
473
+ }
363
474
  let browser, browserContext;
364
475
  try {
365
- // Create a browser of the specified type.
366
- browser = await browserType.launch(browserOptions);
476
+ // Use the job's shared browser if it exists and is of the specified type; otherwise
477
+ // create a browser of the specified type, and share it if the job shares browsers.
478
+ if (sharedBrowser && sharedBrowserID === browserID && sharedBrowser.isConnected()) {
479
+ browser = sharedBrowser;
480
+ }
481
+ else {
482
+ browser = await browserType.launch(browserOptions);
483
+ if (report.jobData?.isolation && report.jobData.isolation !== 'process') {
484
+ if (sharedBrowser) {
485
+ await settleWithin(sharedBrowser.close());
486
+ }
487
+ sharedBrowser = browser;
488
+ sharedBrowserID = browserID;
489
+ }
490
+ }
367
491
  // Create a context (i.e. window) for it.
368
492
  const contextOptions = {
369
493
  ...device.windowOptions,
@@ -377,7 +501,9 @@ const launchOnce = async opts => {
377
501
  'Accept-Language': 'en-US,en;q=0.9',
378
502
  'Accept-Encoding': 'gzip, deflate, br',
379
503
  'DNT': '1',
380
- 'Upgrade-Insecure-Requests': '1'
504
+ 'Upgrade-Insecure-Requests': '1',
505
+ // A stable scanner identity for firewalls, if configured (job scannerId or SCANNER_ID).
506
+ ...(getScannerId(report) ? {'X-YRA-Scanner': getScannerId(report)} : {})
381
507
  },
382
508
  // Caller-specified context options (see contextOverrides above).
383
509
  ...contextOverrides
@@ -445,99 +571,76 @@ const launchOnce = async opts => {
445
571
  });
446
572
  // If an XPath computation script is required:
447
573
  if (xPathNeed !== 'none') {
448
- // Add a script to the page to add a window method to get the XPath of an element.
449
- await page.addInitScript(() => {
450
- window.getXPath = element => {
451
- if (! element || element.nodeType !== Node.ELEMENT_NODE) {
452
- return '';
453
- }
454
- const segments = [];
455
- // As long as the current node is an element:
456
- while (element && element.nodeType === Node.ELEMENT_NODE) {
457
- const tag = element.tagName.toLowerCase();
458
- // If it is the html element:
459
- if (element === document.documentElement) {
460
- // Prepend it to the segment array
461
- segments.unshift('html');
462
- // Stop traversing.
463
- break;
464
- }
465
- // Otherwise, get its parent node.
466
- const parent = element.parentNode;
467
- // If (abnormally) the parent node is not an element:
468
- if (! parent || parent.nodeType !== Node.ELEMENT_NODE) {
469
- // Prepend the element (not the parent) to the segment array.
470
- segments.unshift(tag);
471
- // Stop traversing, leaving the segment array partial.
472
- break;
473
- }
474
- // Get the subscript of the element if it is not the body element.
475
- const cohort = Array
476
- .from(parent.childNodes)
477
- .filter(
478
- childNode => childNode.nodeType === Node.ELEMENT_NODE
479
- && childNode.tagName === element.tagName
480
- );
481
- const subscript = tag === 'body' ? '' : `[${cohort.indexOf(element) + 1}]`;
482
- // Prepend the element identifier to the segment array.
483
- segments.unshift(`${tag}${subscript}`);
484
- // Continue the traversal with the parent of the current element.
485
- element = parent;
486
- }
487
- // Return the XPath.
488
- return `/${segments.join('/')}`;
489
- };
490
- });
574
+ // Add the shared script to the page to add a window method to get the XPath of an element.
575
+ await page.addInitScript({content: getXPathSource});
491
576
  }
492
577
  // If an accessible-name computation script is needed:
493
578
  if (needsAccessibleName) {
494
579
  // Add the dom-accessibility-api script to the page to compute an accessible name.
495
- await page.addInitScript({path: require.resolve('../dist/nameComputation.js')});
496
- // Add a script to the page to:
497
- await page.addInitScript(() => {
498
- // Add a window method to compute the accessible name of an element.
499
- window.getAccessibleName = element => {
500
- const nameIsComputable = element?.nodeType === Node.ELEMENT_NODE
501
- && typeof window.computeAccessibleName === 'function';
502
- return nameIsComputable ? window.computeAccessibleName(element) : '';
503
- };
504
- // Add a window method to return a standard proto-instance.
505
- window.getProtoInstance = (
506
- element, ruleID, what, count = 1, ordinalSeverity, summaryTagName = ''
507
- ) => {
508
- // If an element has been specified:
509
- if (element) {
510
- // Get its properties.
511
- return {
512
- ruleID,
513
- what,
514
- count,
515
- ordinalSeverity,
516
- pathID: window.getXPath(element)
517
- };
518
- }
519
- // Otherwise, i.e. if no element has been specified, return a summary instance.
520
- return {
521
- ruleID,
522
- what,
523
- count,
524
- ordinalSeverity
525
- };
526
- };
527
- });
580
+ await page.addInitScript({path: nameComputationPath});
581
+ // Add the script defining the accessible-name window methods.
582
+ await page.addInitScript({content: accessibleNameSource});
528
583
  }
529
- // Base the wait on the need of the tool and the retry history.
530
- let waitUntil = xPathNeed === 'none' ? 'domcontentloaded' : 'networkidle';
531
- if (relaxWait === 'partly' && waitUntil === 'networkidle') {
532
- waitUntil = 'load';
584
+ // Base the wait on the need of the tool, the configured load state (job navigation
585
+ // property or NAV_WAIT_UNTIL; networkidle by default), and the retry history: a partial
586
+ // relaxation steps one load state looser, a full one waits only for domcontentloaded.
587
+ const navigation = getNavigation(report);
588
+ let waitUntil = xPathNeed === 'none' ? 'domcontentloaded' : navigation.waitUntil;
589
+ if (relaxWait === 'partly') {
590
+ waitUntil = waitStates[Math.min(waitStates.indexOf(waitUntil) + 1, waitStates.length - 1)];
533
591
  }
534
592
  if (relaxWait === 'fully') {
535
593
  waitUntil = 'domcontentloaded';
536
594
  }
537
595
  // Navigate to the specified URL and wait for the stability required by the next action.
538
- const navResult = await goTo(report, page, url, 10000, waitUntil);
596
+ const navResult = await goTo(report, page, url, navigation.timeout, waitUntil);
539
597
  // If the navigation succeeded:
540
598
  if (navResult.success) {
599
+ // If the job asks for a full-height scroll (job scroll property or PRESCAN_SCROLL), scroll
600
+ // the page in viewport steps so lazily loaded content is present before cataloguing,
601
+ // imaging, and each tool, then return to the top. Bounded in steps so an infinite scroll
602
+ // cannot hang the launch; never throws.
603
+ if (getScroll(report)) {
604
+ try {
605
+ const viewportHeight = (await page.evaluate(() => window.innerHeight)) || 800;
606
+ for (let step = 0, y = 0; step < 25; step++, y += viewportHeight) {
607
+ const atBottom = await page.evaluate(dy => {
608
+ window.scrollTo(0, dy);
609
+ return dy + window.innerHeight >= document.body.scrollHeight;
610
+ }, y);
611
+ await page.waitForTimeout(120);
612
+ if (atBottom) {
613
+ break;
614
+ }
615
+ }
616
+ await page.evaluate(() => window.scrollTo(0, 0));
617
+ await page.waitForTimeout(250);
618
+ }
619
+ catch(error) {
620
+ console.log(`ERROR: Pre-scan scroll failed (${error.message})`);
621
+ }
622
+ }
623
+ // If the launch is for a test act at a checkpoint reached by interaction:
624
+ const checkpoint = report.checkpoints?.[report.activeCheckpoint];
625
+ if (checkpoint && checkpoint.kind === 'interaction' && replay) {
626
+ // Re-enact the acts that reached the checkpoint, before any XPath stamping, so that
627
+ // elements the acts reveal or create are stamped like the rest. A failure closes
628
+ // the page and reports the act, so launch() can retry or give up.
629
+ const {replayActs} = require('./actDo');
630
+ const {getDomDigest} = require('./checkpoint');
631
+ const replayResult = await replayActs(page, report, checkpoint);
632
+ page = replayResult.page;
633
+ const digest = await getDomDigest(page);
634
+ act.data ??= {};
635
+ act.data.replay = {
636
+ checkpoint: checkpoint.index,
637
+ acts: replayResult.actCount,
638
+ elapsedMs: replayResult.elapsedMs,
639
+ fidelity: checkpoint.domDigest
640
+ ? (digest === checkpoint.domDigest ? 'exact' : 'divergent')
641
+ : 'unknown'
642
+ };
643
+ }
541
644
  // If XPath attributes are needed:
542
645
  if (xPathNeed === 'attribute') {
543
646
  // Use the added script to add them.
@@ -565,8 +668,8 @@ const launchOnce = async opts => {
565
668
  else {
566
669
  const {rejectionData} = navResult;
567
670
  const addendum = rejectionData
568
- ? ` (rejection data: ${JSON.stringify(rejectionData, null, 2)})`
569
- : '';
671
+ ? ` (rejection data: ${JSON.stringify(rejectionData, null, 2)})`
672
+ : '';
570
673
  // Throw an error.
571
674
  throw new Error(`Navigation failed: ${navResult.error}${addendum}`);
572
675
  }
@@ -598,18 +701,26 @@ const launchOnce = async opts => {
598
701
  };
599
702
  // Manages browser launching and navigating and returns a page.
600
703
  exports.launch = async (opts = {}) => {
601
- let {tempBrowserID = ''} = opts;
704
+ let {tempBrowserID = '', tempURL = ''} = opts;
602
705
  const {
603
706
  report = {},
604
707
  actIndex = 0,
605
- tempURL = '',
606
708
  headEmulation = 'high',
607
709
  xPathNeed = 'script',
608
710
  needsAccessibleName = false,
609
711
  retries = 2,
610
712
  // Extra Playwright context options, passed through to launchOnce.
611
- contextOverrides = {}
713
+ contextOverrides = {},
714
+ // Whether a test-act launch replays the active checkpoint's acts (launches for
715
+ // interaction acts and for the catalog pass do not).
716
+ replay = actIndex !== null
612
717
  } = opts;
718
+ // If the launch is for a test act at a later checkpoint, navigate to that checkpoint's
719
+ // origin: its URL if navigation reached it, else the URL its replayed acts start from.
720
+ const checkpoint = report.checkpoints?.[report.activeCheckpoint];
721
+ if (replay && checkpoint && report.activeCheckpoint > 0) {
722
+ tempURL = checkpoint.kind === 'navigation' ? checkpoint.url : checkpoint.launchURL;
723
+ }
613
724
  // If the report is valid:
614
725
  const jobValidation = isValidJob(report);
615
726
  if (jobValidation.isValid) {
@@ -625,7 +736,8 @@ exports.launch = async (opts = {}) => {
625
736
  headEmulation,
626
737
  xPathNeed,
627
738
  needsAccessibleName,
628
- contextOverrides
739
+ contextOverrides,
740
+ replay
629
741
  }
630
742
  );
631
743
  // If the launch and navigation succeeded:
@@ -636,8 +748,21 @@ exports.launch = async (opts = {}) => {
636
748
  // Otherwise, i.e. if the launch or navigation failed:
637
749
  else {
638
750
  let unusedBrowserIDs = ['chromium', 'webkit', 'firefox'].filter(id => id !== tempBrowserID);
639
- let retriesLeft = retries;
640
751
  let {error} = launchResult;
752
+ // A checkpoint replay failure is deterministic: do not retry it.
753
+ let retriesLeft = error.includes('checkpoint replay failed') ? 0 : retries;
754
+ // If configured (job navigation.failFast4xx or NAV_FAIL_FAST_4XX), a 4xx response other
755
+ // than 408 (request timeout) and 429 (rate limited) is a definitive refusal (firewall
756
+ // block, authentication wall): retrying and switching browsers cannot change it and, under
757
+ // the testaro tool's per-rule relaunch, compounds into long hangs. Stop at once.
758
+ const navStatus = Number((/status(\d{3})/.exec(error) || [])[1]);
759
+ if (
760
+ getNavigation(report).failFast4xx
761
+ && navStatus >= 400 && navStatus < 500 && navStatus !== 408 && navStatus !== 429
762
+ ) {
763
+ retriesLeft = 0;
764
+ unusedBrowserIDs = [];
765
+ }
641
766
  // As long as retries remain, decrement the allowed retry count and:
642
767
  while (retriesLeft) {
643
768
  // Prepare to wait 1 second before a retry.
@@ -668,7 +793,8 @@ exports.launch = async (opts = {}) => {
668
793
  headEmulation,
669
794
  xPathNeed,
670
795
  needsAccessibleName,
671
- contextOverrides
796
+ contextOverrides,
797
+ replay
672
798
  }
673
799
  );
674
800
  // If the launch and navigation succeeded:
@@ -681,6 +807,11 @@ exports.launch = async (opts = {}) => {
681
807
  error = launchResult.error;
682
808
  // Report this.
683
809
  console.log(`WARNING: Retry failed (${error})`);
810
+ // A checkpoint replay failure is deterministic: stop retrying.
811
+ if (error.includes('checkpoint replay failed')) {
812
+ retriesLeft = 0;
813
+ break;
814
+ }
684
815
  // If a browser type was specified, retries are exhausted, and browser types are not:
685
816
  if (tempBrowserID && unusedBrowserIDs.length && ! retriesLeft) {
686
817
  // Change the browser type.
@@ -700,7 +831,7 @@ exports.launch = async (opts = {}) => {
700
831
  actIndex === null ? true : abortAssertively,
701
832
  report,
702
833
  actIndex,
703
- `Launch or navigation failed; retries and browser types exhausted`
834
+ `Launch or navigation failed; retries and browser types exhausted (${error})`
704
835
  );
705
836
  }
706
837
  // Return a failure.
package/procs/nu.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ /*
2
+ © 2026 Jeff Witt.
3
+
4
+ Licensed under the MIT License. See LICENSE file at the project root or
5
+ https://opensource.org/license/mit/ for details.
6
+
7
+ SPDX-License-Identifier: MIT
8
+ */
9
+
10
+ /*
11
+ nu (hand-written declarations)
12
+ Minimal typings for the surface of procs/nu.js that converted TypeScript
13
+ files use. Delete this file when nu.js itself is converted (issue #73).
14
+ */
15
+
16
+ import type {Page} from 'playwright';
17
+
18
+ // The content obtained for a nuVal or nuVnu test.
19
+ export interface NuContent {
20
+ withSource: boolean | undefined;
21
+ testTarget: string | null;
22
+ prevented?: boolean;
23
+ error?: string;
24
+ }
25
+ // One message of a curated Nu Html Checker result.
26
+ export interface NuMessage {
27
+ type: string;
28
+ subType?: string;
29
+ message: string;
30
+ extract?: string;
31
+ [key: string]: unknown;
32
+ }
33
+ // A curated Nu Html Checker result; curate guarantees the messages array.
34
+ export interface NuResult {
35
+ messages: NuMessage[];
36
+ [key: string]: unknown;
37
+ }
38
+
39
+ // Gets the content for a nuVal or nuVnu test.
40
+ export function getContent(page: Page, withSource: boolean | undefined): Promise<NuContent>;
41
+ // Postprocesses a result from nuVal or nuVnu tests; undefined when nuData is falsy.
42
+ export function curate(
43
+ data: {prevented?: boolean; error?: string},
44
+ nuData: unknown,
45
+ rules: string[] | undefined
46
+ ): Promise<NuResult | undefined>;
47
+ // Gets an excerpt of a message extract, or null if the catalog reports the element.
48
+ export function getExtractExcerpt(extract: string | null | undefined): string | null;