testaro 78.0.8 → 78.6.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 (357) hide show
  1. package/.gitattributes +94 -0
  2. package/.github/workflows/ci.yml +58 -0
  3. package/.github/workflows/publish.yml +50 -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 +143 -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/dist/nameComputation.js +1 -0
  16. package/docker-compose.yml +8 -2
  17. package/docs/checkpoint-scanning.md +199 -0
  18. package/docs/standard-result-outcome.md +165 -0
  19. package/env.example +49 -10
  20. package/eslint.config.mjs +101 -0
  21. package/netWatch.js +372 -222
  22. package/package.json +46 -31
  23. package/pour/README.md +47 -0
  24. package/pour/pour.min.js +8 -0
  25. package/procs/actDo.js +808 -0
  26. package/procs/catalog.d.ts +17 -0
  27. package/procs/catalog.js +312 -221
  28. package/procs/catalog.ts +398 -0
  29. package/procs/checkpoint.js +110 -0
  30. package/procs/config.d.ts +41 -0
  31. package/procs/config.js +98 -0
  32. package/procs/dateTime.js +2 -1
  33. package/procs/doActs.js +442 -849
  34. package/procs/doTestAct.js +27 -121
  35. package/procs/flow.js +221 -0
  36. package/procs/generateRuleRegistry.js +94 -0
  37. package/procs/getSource.d.ts +26 -0
  38. package/procs/job.js +115 -1
  39. package/procs/launch.d.ts +40 -0
  40. package/procs/launch.js +243 -108
  41. package/procs/nu.d.ts +48 -0
  42. package/procs/scope.js +196 -0
  43. package/procs/shoot.d.ts +33 -0
  44. package/procs/standard.d.ts +18 -0
  45. package/procs/standard.js +83 -0
  46. package/procs/standard.ts +126 -0
  47. package/procs/testAct.js +145 -0
  48. package/procs/testaro.d.ts +20 -0
  49. package/procs/testaro.js +236 -189
  50. package/procs/testaro.ts +321 -0
  51. package/procs/userPath.js +126 -0
  52. package/procs/xPath.d.ts +4 -0
  53. package/procs/xPath.js +81 -69
  54. package/procs/xPath.ts +106 -0
  55. package/procs/xPathScript.d.ts +6 -0
  56. package/procs/xPathScript.js +70 -0
  57. package/run.js +3 -2
  58. package/surea11y/README.md +44 -0
  59. package/surea11y/surea11y.browser.js +14 -0
  60. package/testaro/adbID.d.ts +3 -0
  61. package/testaro/adbID.js +38 -41
  62. package/testaro/adbID.ts +66 -0
  63. package/testaro/allCapStyle.d.ts +3 -0
  64. package/testaro/allCapStyle.js +31 -34
  65. package/testaro/allCapStyle.ts +55 -0
  66. package/testaro/allCaps.d.ts +16 -0
  67. package/testaro/allCaps.js +179 -151
  68. package/testaro/allCaps.ts +228 -0
  69. package/testaro/allHidden.d.ts +13 -0
  70. package/testaro/allHidden.js +29 -30
  71. package/testaro/allHidden.ts +50 -0
  72. package/testaro/allSlanted.d.ts +3 -0
  73. package/testaro/allSlanted.js +30 -33
  74. package/testaro/allSlanted.ts +54 -0
  75. package/testaro/altScheme.d.ts +3 -0
  76. package/testaro/altScheme.js +27 -30
  77. package/testaro/altScheme.ts +50 -0
  78. package/testaro/attVal.d.ts +3 -0
  79. package/testaro/attVal.js +20 -21
  80. package/testaro/attVal.ts +52 -0
  81. package/testaro/autocomplete.d.ts +3 -0
  82. package/testaro/autocomplete.js +59 -75
  83. package/testaro/autocomplete.ts +101 -0
  84. package/testaro/bulk.d.ts +13 -0
  85. package/testaro/bulk.js +32 -33
  86. package/testaro/bulk.ts +55 -0
  87. package/testaro/buttonMenu.d.ts +9 -0
  88. package/testaro/buttonMenu.js +317 -319
  89. package/testaro/buttonMenu.ts +391 -0
  90. package/testaro/captionLoc.d.ts +3 -0
  91. package/testaro/captionLoc.js +17 -20
  92. package/testaro/captionLoc.ts +40 -0
  93. package/testaro/datalistRef.d.ts +3 -0
  94. package/testaro/datalistRef.js +33 -36
  95. package/testaro/datalistRef.ts +55 -0
  96. package/testaro/distortion.d.ts +3 -0
  97. package/testaro/distortion.js +57 -26
  98. package/testaro/distortion.ts +81 -0
  99. package/testaro/docType.d.ts +15 -0
  100. package/testaro/docType.js +25 -25
  101. package/testaro/docType.ts +45 -0
  102. package/testaro/dupAtt.d.ts +16 -0
  103. package/testaro/dupAtt.js +113 -104
  104. package/testaro/dupAtt.ts +144 -0
  105. package/testaro/elements.d.ts +6 -0
  106. package/testaro/elements.js +153 -153
  107. package/testaro/elements.ts +215 -0
  108. package/testaro/embAc.d.ts +3 -0
  109. package/testaro/embAc.js +19 -20
  110. package/testaro/embAc.ts +40 -0
  111. package/testaro/focAll.d.ts +13 -0
  112. package/testaro/focAll.js +185 -191
  113. package/testaro/focAll.ts +217 -0
  114. package/testaro/focAndOp.d.ts +3 -0
  115. package/testaro/focAndOp.js +100 -104
  116. package/testaro/focAndOp.ts +128 -0
  117. package/testaro/focInd.d.ts +3 -0
  118. package/testaro/focInd.js +64 -66
  119. package/testaro/focInd.ts +96 -0
  120. package/testaro/focVis.d.ts +3 -0
  121. package/testaro/focVis.js +28 -30
  122. package/testaro/focVis.ts +52 -0
  123. package/testaro/headEl.d.ts +10 -0
  124. package/testaro/headEl.js +61 -62
  125. package/testaro/headEl.ts +82 -0
  126. package/testaro/headingAmb.d.ts +3 -0
  127. package/testaro/headingAmb.js +54 -63
  128. package/testaro/headingAmb.ts +72 -0
  129. package/testaro/hovInd.d.ts +12 -0
  130. package/testaro/hovInd.js +163 -130
  131. package/testaro/hovInd.ts +199 -0
  132. package/testaro/hover.d.ts +3 -0
  133. package/testaro/hover.js +155 -126
  134. package/testaro/hover.ts +154 -0
  135. package/testaro/hr.d.ts +3 -0
  136. package/testaro/hr.js +14 -17
  137. package/testaro/hr.ts +36 -0
  138. package/testaro/imageLink.d.ts +3 -0
  139. package/testaro/imageLink.js +17 -20
  140. package/testaro/imageLink.ts +42 -0
  141. package/testaro/labClash.d.ts +3 -0
  142. package/testaro/labClash.js +31 -33
  143. package/testaro/labClash.ts +54 -0
  144. package/testaro/legendLoc.d.ts +3 -0
  145. package/testaro/legendLoc.js +17 -20
  146. package/testaro/legendLoc.ts +42 -0
  147. package/testaro/lineHeight.d.ts +3 -0
  148. package/testaro/lineHeight.js +43 -48
  149. package/testaro/lineHeight.ts +70 -0
  150. package/testaro/linkAmb.d.ts +7 -0
  151. package/testaro/linkAmb.js +79 -80
  152. package/testaro/linkAmb.ts +105 -0
  153. package/testaro/linkExt.d.ts +3 -0
  154. package/testaro/linkExt.js +13 -16
  155. package/testaro/linkExt.ts +35 -0
  156. package/testaro/linkOldAtt.d.ts +3 -0
  157. package/testaro/linkOldAtt.js +25 -28
  158. package/testaro/linkOldAtt.ts +48 -0
  159. package/testaro/linkTo.d.ts +3 -0
  160. package/testaro/linkTo.js +22 -22
  161. package/testaro/linkTo.ts +43 -0
  162. package/testaro/linkUl.d.ts +3 -0
  163. package/testaro/linkUl.js +30 -34
  164. package/testaro/linkUl.ts +54 -0
  165. package/testaro/miniText.d.ts +3 -0
  166. package/testaro/miniText.js +41 -44
  167. package/testaro/miniText.ts +68 -0
  168. package/testaro/motion.d.ts +10 -0
  169. package/testaro/motion.js +92 -96
  170. package/testaro/motion.ts +125 -0
  171. package/testaro/nonTable.d.ts +3 -0
  172. package/testaro/nonTable.js +39 -45
  173. package/testaro/nonTable.ts +66 -0
  174. package/testaro/optRoleSel.d.ts +3 -0
  175. package/testaro/optRoleSel.js +16 -19
  176. package/testaro/optRoleSel.ts +41 -0
  177. package/testaro/phOnly.d.ts +3 -0
  178. package/testaro/phOnly.js +18 -21
  179. package/testaro/phOnly.ts +43 -0
  180. package/testaro/pseudoP.d.ts +3 -0
  181. package/testaro/pseudoP.js +36 -38
  182. package/testaro/pseudoP.ts +59 -0
  183. package/testaro/radioSet.d.ts +3 -0
  184. package/testaro/radioSet.js +57 -59
  185. package/testaro/radioSet.ts +79 -0
  186. package/testaro/registry.d.ts +63 -0
  187. package/testaro/registry.js +67 -0
  188. package/testaro/registry.ts +141 -0
  189. package/testaro/role.d.ts +3 -0
  190. package/testaro/role.js +27 -29
  191. package/testaro/role.ts +53 -0
  192. package/testaro/secHeading.d.ts +3 -0
  193. package/testaro/secHeading.js +30 -33
  194. package/testaro/secHeading.ts +53 -0
  195. package/testaro/styleDiff.d.ts +25 -0
  196. package/testaro/styleDiff.js +248 -252
  197. package/testaro/styleDiff.ts +303 -0
  198. package/testaro/tabNav.d.ts +33 -0
  199. package/testaro/tabNav.js +272 -342
  200. package/testaro/tabNav.ts +454 -0
  201. package/testaro/targetsNear.d.ts +9 -0
  202. package/testaro/targetsNear.js +130 -132
  203. package/testaro/targetsNear.ts +160 -0
  204. package/testaro/textNodes.d.ts +6 -0
  205. package/testaro/textNodes.js +139 -135
  206. package/testaro/textNodes.ts +185 -0
  207. package/testaro/textSem.d.ts +3 -0
  208. package/testaro/textSem.js +25 -28
  209. package/testaro/textSem.ts +47 -0
  210. package/testaro/title.d.ts +9 -0
  211. package/testaro/title.js +16 -13
  212. package/testaro/title.ts +31 -0
  213. package/testaro/titledEl.d.ts +3 -0
  214. package/testaro/titledEl.js +15 -18
  215. package/testaro/titledEl.ts +38 -0
  216. package/testaro/zIndex.d.ts +3 -0
  217. package/testaro/zIndex.js +19 -22
  218. package/testaro/zIndex.ts +42 -0
  219. package/tests/alfa.d.ts +45 -0
  220. package/tests/alfa.js +138 -141
  221. package/tests/alfa.ts +214 -0
  222. package/tests/aslint.d.ts +33 -0
  223. package/tests/aslint.js +272 -249
  224. package/tests/aslint.ts +301 -0
  225. package/tests/axe.d.ts +27 -0
  226. package/tests/axe.js +199 -200
  227. package/tests/axe.ts +277 -0
  228. package/tests/ed11y.d.ts +28 -0
  229. package/tests/ed11y.js +141 -99
  230. package/tests/ed11y.ts +178 -0
  231. package/tests/htmlcs.d.ts +21 -0
  232. package/tests/htmlcs.js +174 -140
  233. package/tests/htmlcs.ts +181 -0
  234. package/tests/ibm.d.ts +57 -0
  235. package/tests/ibm.js +182 -175
  236. package/tests/ibm.ts +287 -0
  237. package/tests/nuVal.d.ts +13 -0
  238. package/tests/nuVal.js +107 -112
  239. package/tests/nuVal.ts +145 -0
  240. package/tests/nuVnu.d.ts +15 -0
  241. package/tests/nuVnu.js +141 -111
  242. package/tests/nuVnu.ts +144 -0
  243. package/tests/pour.d.ts +31 -0
  244. package/tests/pour.js +242 -0
  245. package/tests/pour.ts +273 -0
  246. package/tests/qualWeb.d.ts +39 -0
  247. package/tests/qualWeb.js +262 -273
  248. package/tests/qualWeb.ts +344 -0
  249. package/tests/surea11y.d.ts +33 -0
  250. package/tests/surea11y.js +288 -0
  251. package/tests/surea11y.ts +334 -0
  252. package/tests/testaro.d.ts +25 -0
  253. package/tests/testaro.js +745 -652
  254. package/tests/testaro.ts +862 -0
  255. package/tests/wave.d.ts +46 -0
  256. package/tests/wave.js +166 -177
  257. package/tests/wave.ts +252 -0
  258. package/tsconfig.json +17 -0
  259. package/types.d.ts +243 -0
  260. package/types.js +10 -0
  261. package/types.ts +376 -0
  262. package/validation/act/README.md +46 -0
  263. package/validation/act/capture.js +877 -0
  264. package/validation/act/chromium-issue-draft.md +66 -0
  265. package/validation/act/fp-triage-2026-08-22.md +86 -0
  266. package/validation/act/isolation-notes.md +159 -0
  267. package/validation/act/playwright-issue-draft.md +73 -0
  268. package/validation/act/propose-mappings.js +0 -0
  269. package/validation/act/repro-cdp-raw.js +105 -0
  270. package/validation/act/repro-metarefresh.js +65 -0
  271. package/validation/act/score.js +184 -0
  272. package/validation/act/stage3a-stress-report.md +69 -0
  273. package/validation/act/stage3b-mapping-proposals.md +94 -0
  274. package/validation/act/stage3b-triage-draft.md +138 -0
  275. package/validation/act/surea11y-track-a-2026-09-01.md +50 -0
  276. package/validation/executors/netWatch.js +180 -90
  277. package/validation/executors/test.js +17 -2
  278. package/validation/executors/tests.js +118 -10
  279. package/validation/jobs/reports/raw/260901T1000-surea11y-validation.json +964 -0
  280. package/validation/jobs/todo/240101T1200-simple-example.json +14 -6
  281. package/validation/jobs/todo/240101T1300-shoot-example.json +2 -1
  282. package/validation/jobs/todo/260821T1900-pour-validation.json +45 -0
  283. package/validation/jobs/todo/260901T1000-surea11y-validation.json +45 -0
  284. package/validation/knownFailures.json +1 -0
  285. package/validation/tests/jobProperties/adbID.json +27 -2
  286. package/validation/tests/jobProperties/{focOp.json → allCapStyle.json} +56 -53
  287. package/validation/tests/jobProperties/allCaps.json +25 -0
  288. package/validation/tests/jobProperties/allHidden.json +133 -13
  289. package/validation/tests/jobProperties/allSlanted.json +3 -3
  290. package/validation/tests/jobProperties/altScheme.json +23 -8
  291. package/validation/tests/jobProperties/attVal.json +57 -57
  292. package/validation/tests/jobProperties/autocomplete.json +11 -1
  293. package/validation/tests/jobProperties/bulk.json +6 -1
  294. package/validation/tests/jobProperties/buttonMenu.json +63 -42
  295. package/validation/tests/jobProperties/captionLoc.json +1 -6
  296. package/validation/tests/jobProperties/checkpoint-browser.json +401 -0
  297. package/validation/tests/jobProperties/checkpoint-page.json +397 -0
  298. package/validation/tests/jobProperties/checkpoint.json +395 -0
  299. package/validation/tests/jobProperties/datalistRef.json +20 -5
  300. package/validation/tests/jobProperties/distortion.json +28 -3
  301. package/validation/tests/jobProperties/docType.json +2 -2
  302. package/validation/tests/jobProperties/dupAtt.json +48 -33
  303. package/validation/tests/jobProperties/elements.json +28 -28
  304. package/validation/tests/jobProperties/embAc.json +36 -31
  305. package/validation/tests/jobProperties/focAndOp.json +284 -0
  306. package/validation/tests/jobProperties/focInd.json +34 -34
  307. package/validation/tests/jobProperties/focVis.json +1 -1
  308. package/validation/tests/jobProperties/hover.json +39 -37
  309. package/validation/tests/jobProperties/hr.json +13 -3
  310. package/validation/tests/jobProperties/imageLink.json +0 -5
  311. package/validation/tests/jobProperties/labClash.json +68 -33
  312. package/validation/tests/jobProperties/legendLoc.json +1 -6
  313. package/validation/tests/jobProperties/lineHeight.json +2 -2
  314. package/validation/tests/jobProperties/linkAmb.json +13 -18
  315. package/validation/tests/jobProperties/linkExt.json +1 -1
  316. package/validation/tests/jobProperties/linkOldAtt.json +12 -2
  317. package/validation/tests/jobProperties/linkTo.json +1 -1
  318. package/validation/tests/jobProperties/linkUl.json +65 -65
  319. package/validation/tests/jobProperties/miniText.json +7 -2
  320. package/validation/tests/jobProperties/motion.json +4 -50
  321. package/validation/tests/jobProperties/nonTable.json +48 -3
  322. package/validation/tests/jobProperties/optRoleSel.json +12 -2
  323. package/validation/tests/jobProperties/phOnly.json +6 -16
  324. package/validation/tests/jobProperties/pseudoP.json +17 -2
  325. package/validation/tests/jobProperties/radioSet.json +30 -30
  326. package/validation/tests/jobProperties/role.json +25 -5
  327. package/validation/tests/jobProperties/secHeading.json +26 -21
  328. package/validation/tests/jobProperties/styleDiff.json +35 -5
  329. package/validation/tests/jobProperties/tabNav.json +3 -1
  330. package/validation/tests/jobProperties/targetsNear.json +237 -0
  331. package/validation/tests/jobProperties/textNodes.json +29 -29
  332. package/validation/tests/jobProperties/textSem.json +26 -1
  333. package/validation/tests/jobProperties/title.json +12 -2
  334. package/validation/tests/jobProperties/titledEl.json +39 -9
  335. package/validation/tests/jobProperties/userPath.json +317 -0
  336. package/validation/tests/jobProperties/zIndex.json +40 -35
  337. package/validation/tests/targets/allCapStyle/index.html +26 -0
  338. package/validation/tests/targets/checkpoint/index.html +38 -0
  339. package/validation/tests/targets/datalistRef/index.html +1 -1
  340. package/validation/tests/targets/focAndOp/bad.html +29 -0
  341. package/validation/tests/targets/{focOp → focAndOp}/good.html +3 -1
  342. package/validation/tests/targets/focInd/bad.html +2 -1
  343. package/validation/tests/targets/headEl/index.html +10 -1
  344. package/validation/tests/targets/{targetSmall → targetsNear}/index.html +15 -1
  345. package/validation/tests/targets/userPath/index.html +42 -0
  346. package/validation/validateTest.js +61 -9
  347. package/.claude/settings.local.json +0 -11
  348. package/.eslintrc.json +0 -41
  349. package/htmlcs/.eslintrc.json +0 -67
  350. package/validation/tests/jobProperties/linkTitle.json +0 -127
  351. package/validation/tests/jobProperties/opFoc.json +0 -164
  352. package/validation/tests/jobProperties/targetSmall.json +0 -152
  353. package/validation/tests/jobProperties/targetTiny.json +0 -142
  354. package/validation/tests/targets/focOp/bad.html +0 -25
  355. package/validation/tests/targets/linkTitle/index.html +0 -24
  356. package/validation/tests/targets/opFoc/bad.html +0 -26
  357. 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, outbound proxy).
38
+ const {getNavigation, getProxyLaunchOptions, 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,33 @@ const launchOnce = async opts => {
360
465
  // Disable the sandbox.
361
466
  browserOptions.chromiumSandbox = false;
362
467
  }
468
+ // Route the browser through the outbound proxy, if one is configured.
469
+ const {args: proxyArgs = [], ...proxyOptions} = getProxyLaunchOptions(browserID);
470
+ browserOptionArgs.push(...proxyArgs);
471
+ Object.assign(browserOptions, proxyOptions);
472
+ // If the job specifies a branded Chromium channel (chrome or msedge), run that installed
473
+ // browser instead of the bundled Chromium build; bundled or absent keeps the default.
474
+ const {browserChannel} = report;
475
+ if (browserID === 'chromium' && browserChannel && browserChannel !== 'bundled') {
476
+ browserOptions.channel = browserChannel;
477
+ }
363
478
  let browser, browserContext;
364
479
  try {
365
- // Create a browser of the specified type.
366
- browser = await browserType.launch(browserOptions);
480
+ // Use the job's shared browser if it exists and is of the specified type; otherwise
481
+ // create a browser of the specified type, and share it if the job shares browsers.
482
+ if (sharedBrowser && sharedBrowserID === browserID && sharedBrowser.isConnected()) {
483
+ browser = sharedBrowser;
484
+ }
485
+ else {
486
+ browser = await browserType.launch(browserOptions);
487
+ if (report.jobData?.isolation && report.jobData.isolation !== 'process') {
488
+ if (sharedBrowser) {
489
+ await settleWithin(sharedBrowser.close());
490
+ }
491
+ sharedBrowser = browser;
492
+ sharedBrowserID = browserID;
493
+ }
494
+ }
367
495
  // Create a context (i.e. window) for it.
368
496
  const contextOptions = {
369
497
  ...device.windowOptions,
@@ -377,7 +505,9 @@ const launchOnce = async opts => {
377
505
  'Accept-Language': 'en-US,en;q=0.9',
378
506
  'Accept-Encoding': 'gzip, deflate, br',
379
507
  'DNT': '1',
380
- 'Upgrade-Insecure-Requests': '1'
508
+ 'Upgrade-Insecure-Requests': '1',
509
+ // A stable scanner identity for firewalls, if configured (job scannerId or SCANNER_ID).
510
+ ...(getScannerId(report) ? {'X-YRA-Scanner': getScannerId(report)} : {})
381
511
  },
382
512
  // Caller-specified context options (see contextOverrides above).
383
513
  ...contextOverrides
@@ -445,99 +575,76 @@ const launchOnce = async opts => {
445
575
  });
446
576
  // If an XPath computation script is required:
447
577
  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
- });
578
+ // Add the shared script to the page to add a window method to get the XPath of an element.
579
+ await page.addInitScript({content: getXPathSource});
491
580
  }
492
581
  // If an accessible-name computation script is needed:
493
582
  if (needsAccessibleName) {
494
583
  // 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
- });
584
+ await page.addInitScript({path: nameComputationPath});
585
+ // Add the script defining the accessible-name window methods.
586
+ await page.addInitScript({content: accessibleNameSource});
528
587
  }
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';
588
+ // Base the wait on the need of the tool, the configured load state (job navigation
589
+ // property or NAV_WAIT_UNTIL; networkidle by default), and the retry history: a partial
590
+ // relaxation steps one load state looser, a full one waits only for domcontentloaded.
591
+ const navigation = getNavigation(report);
592
+ let waitUntil = xPathNeed === 'none' ? 'domcontentloaded' : navigation.waitUntil;
593
+ if (relaxWait === 'partly') {
594
+ waitUntil = waitStates[Math.min(waitStates.indexOf(waitUntil) + 1, waitStates.length - 1)];
533
595
  }
534
596
  if (relaxWait === 'fully') {
535
597
  waitUntil = 'domcontentloaded';
536
598
  }
537
599
  // 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);
600
+ const navResult = await goTo(report, page, url, navigation.timeout, waitUntil);
539
601
  // If the navigation succeeded:
540
602
  if (navResult.success) {
603
+ // If the job asks for a full-height scroll (job scroll property or PRESCAN_SCROLL), scroll
604
+ // the page in viewport steps so lazily loaded content is present before cataloguing,
605
+ // imaging, and each tool, then return to the top. Bounded in steps so an infinite scroll
606
+ // cannot hang the launch; never throws.
607
+ if (getScroll(report)) {
608
+ try {
609
+ const viewportHeight = (await page.evaluate(() => window.innerHeight)) || 800;
610
+ for (let step = 0, y = 0; step < 25; step++, y += viewportHeight) {
611
+ const atBottom = await page.evaluate(dy => {
612
+ window.scrollTo(0, dy);
613
+ return dy + window.innerHeight >= document.body.scrollHeight;
614
+ }, y);
615
+ await page.waitForTimeout(120);
616
+ if (atBottom) {
617
+ break;
618
+ }
619
+ }
620
+ await page.evaluate(() => window.scrollTo(0, 0));
621
+ await page.waitForTimeout(250);
622
+ }
623
+ catch(error) {
624
+ console.log(`ERROR: Pre-scan scroll failed (${error.message})`);
625
+ }
626
+ }
627
+ // If the launch is for a test act at a checkpoint reached by interaction:
628
+ const checkpoint = report.checkpoints?.[report.activeCheckpoint];
629
+ if (checkpoint && checkpoint.kind === 'interaction' && replay) {
630
+ // Re-enact the acts that reached the checkpoint, before any XPath stamping, so that
631
+ // elements the acts reveal or create are stamped like the rest. A failure closes
632
+ // the page and reports the act, so launch() can retry or give up.
633
+ const {replayActs} = require('./actDo');
634
+ const {getDomDigest} = require('./checkpoint');
635
+ const replayResult = await replayActs(page, report, checkpoint);
636
+ page = replayResult.page;
637
+ const digest = await getDomDigest(page);
638
+ act.data ??= {};
639
+ act.data.replay = {
640
+ checkpoint: checkpoint.index,
641
+ acts: replayResult.actCount,
642
+ elapsedMs: replayResult.elapsedMs,
643
+ fidelity: checkpoint.domDigest
644
+ ? (digest === checkpoint.domDigest ? 'exact' : 'divergent')
645
+ : 'unknown'
646
+ };
647
+ }
541
648
  // If XPath attributes are needed:
542
649
  if (xPathNeed === 'attribute') {
543
650
  // Use the added script to add them.
@@ -565,8 +672,8 @@ const launchOnce = async opts => {
565
672
  else {
566
673
  const {rejectionData} = navResult;
567
674
  const addendum = rejectionData
568
- ? ` (rejection data: ${JSON.stringify(rejectionData, null, 2)})`
569
- : '';
675
+ ? ` (rejection data: ${JSON.stringify(rejectionData, null, 2)})`
676
+ : '';
570
677
  // Throw an error.
571
678
  throw new Error(`Navigation failed: ${navResult.error}${addendum}`);
572
679
  }
@@ -598,18 +705,26 @@ const launchOnce = async opts => {
598
705
  };
599
706
  // Manages browser launching and navigating and returns a page.
600
707
  exports.launch = async (opts = {}) => {
601
- let {tempBrowserID = ''} = opts;
708
+ let {tempBrowserID = '', tempURL = ''} = opts;
602
709
  const {
603
710
  report = {},
604
711
  actIndex = 0,
605
- tempURL = '',
606
712
  headEmulation = 'high',
607
713
  xPathNeed = 'script',
608
714
  needsAccessibleName = false,
609
715
  retries = 2,
610
716
  // Extra Playwright context options, passed through to launchOnce.
611
- contextOverrides = {}
717
+ contextOverrides = {},
718
+ // Whether a test-act launch replays the active checkpoint's acts (launches for
719
+ // interaction acts and for the catalog pass do not).
720
+ replay = actIndex !== null
612
721
  } = opts;
722
+ // If the launch is for a test act at a later checkpoint, navigate to that checkpoint's
723
+ // origin: its URL if navigation reached it, else the URL its replayed acts start from.
724
+ const checkpoint = report.checkpoints?.[report.activeCheckpoint];
725
+ if (replay && checkpoint && report.activeCheckpoint > 0) {
726
+ tempURL = checkpoint.kind === 'navigation' ? checkpoint.url : checkpoint.launchURL;
727
+ }
613
728
  // If the report is valid:
614
729
  const jobValidation = isValidJob(report);
615
730
  if (jobValidation.isValid) {
@@ -625,7 +740,8 @@ exports.launch = async (opts = {}) => {
625
740
  headEmulation,
626
741
  xPathNeed,
627
742
  needsAccessibleName,
628
- contextOverrides
743
+ contextOverrides,
744
+ replay
629
745
  }
630
746
  );
631
747
  // If the launch and navigation succeeded:
@@ -636,8 +752,21 @@ exports.launch = async (opts = {}) => {
636
752
  // Otherwise, i.e. if the launch or navigation failed:
637
753
  else {
638
754
  let unusedBrowserIDs = ['chromium', 'webkit', 'firefox'].filter(id => id !== tempBrowserID);
639
- let retriesLeft = retries;
640
755
  let {error} = launchResult;
756
+ // A checkpoint replay failure is deterministic: do not retry it.
757
+ let retriesLeft = error.includes('checkpoint replay failed') ? 0 : retries;
758
+ // If configured (job navigation.failFast4xx or NAV_FAIL_FAST_4XX), a 4xx response other
759
+ // than 408 (request timeout) and 429 (rate limited) is a definitive refusal (firewall
760
+ // block, authentication wall): retrying and switching browsers cannot change it and, under
761
+ // the testaro tool's per-rule relaunch, compounds into long hangs. Stop at once.
762
+ const navStatus = Number((/status(\d{3})/.exec(error) || [])[1]);
763
+ if (
764
+ getNavigation(report).failFast4xx
765
+ && navStatus >= 400 && navStatus < 500 && navStatus !== 408 && navStatus !== 429
766
+ ) {
767
+ retriesLeft = 0;
768
+ unusedBrowserIDs = [];
769
+ }
641
770
  // As long as retries remain, decrement the allowed retry count and:
642
771
  while (retriesLeft) {
643
772
  // Prepare to wait 1 second before a retry.
@@ -668,7 +797,8 @@ exports.launch = async (opts = {}) => {
668
797
  headEmulation,
669
798
  xPathNeed,
670
799
  needsAccessibleName,
671
- contextOverrides
800
+ contextOverrides,
801
+ replay
672
802
  }
673
803
  );
674
804
  // If the launch and navigation succeeded:
@@ -681,6 +811,11 @@ exports.launch = async (opts = {}) => {
681
811
  error = launchResult.error;
682
812
  // Report this.
683
813
  console.log(`WARNING: Retry failed (${error})`);
814
+ // A checkpoint replay failure is deterministic: stop retrying.
815
+ if (error.includes('checkpoint replay failed')) {
816
+ retriesLeft = 0;
817
+ break;
818
+ }
684
819
  // If a browser type was specified, retries are exhausted, and browser types are not:
685
820
  if (tempBrowserID && unusedBrowserIDs.length && ! retriesLeft) {
686
821
  // Change the browser type.
@@ -700,7 +835,7 @@ exports.launch = async (opts = {}) => {
700
835
  actIndex === null ? true : abortAssertively,
701
836
  report,
702
837
  actIndex,
703
- `Launch or navigation failed; retries and browser types exhausted`
838
+ `Launch or navigation failed; retries and browser types exhausted (${error})`
704
839
  );
705
840
  }
706
841
  // 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;