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/actDo.js ADDED
@@ -0,0 +1,808 @@
1
+ /*
2
+ © 2021–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 https://opensource.org/license/mit/ for details.
7
+
8
+ SPDX-License-Identifier: MIT
9
+ */
10
+
11
+ /*
12
+ actDo.js
13
+ Performs the acts that operate on a live page: navigation, waiting, revealing, screenshots,
14
+ moves (button, checkbox, focus, link, radio, search, select, text), and key presses. The acts
15
+ loop (doActs.js) calls doInteractionAct for each such act, and the launcher (launch.js) calls
16
+ replayActs to re-enact the acts that reached a checkpoint before a tool tests it.
17
+
18
+ The `interaction` option selects the event modality; only 'efficient' (Playwright's composed
19
+ clicks and key presses) exists today. Recorded on each checkpoint, it lets a replay use the
20
+ same modality the original acts used.
21
+ */
22
+
23
+ // IMPORTS
24
+
25
+ const {addError} = require('./error');
26
+ // Function to take a full-page screenshot.
27
+ const {shoot} = require('./shoot');
28
+
29
+ // CONSTANTS
30
+
31
+ // CSS selectors for targets of moves.
32
+ const moves = exports.moves = {
33
+ button: 'button, [role=button], input[type=submit]',
34
+ checkbox: 'input[type=checkbox]',
35
+ focus: true,
36
+ link: 'a, [role=link]',
37
+ radio: 'input[type=radio]',
38
+ search: 'input[type=search], input[aria-label*=search i], input[placeholder*=search i]',
39
+ select: 'select',
40
+ text: 'input'
41
+ };
42
+ // Act types that doInteractionAct performs (every act type that needs a live page).
43
+ exports.interactionTypes = [
44
+ 'url', 'wait', 'state', 'page', 'reveal', 'shoot', 'press', 'presses', ... Object.keys(moves)
45
+ ];
46
+ // The default interaction options.
47
+ exports.defaultInteraction = {modality: 'efficient'};
48
+
49
+ // FUNCTIONS
50
+
51
+ // The launcher requires this module, so require it lazily to avoid a circular load.
52
+ const getLaunch = () => require('./launch');
53
+ // Normalizes spacing characters and cases in a string.
54
+ const debloat = exports.debloat = string => string
55
+ .replace(/\s/g, ' ')
56
+ .trim()
57
+ .replace(/ {2,}/g, ' ')
58
+ .toLowerCase();
59
+ // Returns the first line of an error message.
60
+ const errorStart = error => error.message.replace(/\n.+/s, '');
61
+ // Returns the text of an element, lower-cased.
62
+ const textOf = async (page, element) => {
63
+ if (element) {
64
+ const tagNameJSHandle = await element.getProperty('tagName');
65
+ const tagName = await tagNameJSHandle.jsonValue();
66
+ let totalText = '';
67
+ // If the element is a link, button, input, or select list:
68
+ if (['A', 'BUTTON', 'INPUT', 'SELECT'].includes(tagName)) {
69
+ // Return its visible labels, descriptions, and legend if the first input in a fieldset.
70
+ totalText = await page.evaluate(element => {
71
+ const {tagName, ariaLabel} = element;
72
+ let ownText = '';
73
+ if (['A', 'BUTTON'].includes(tagName)) {
74
+ ownText = element.textContent;
75
+ }
76
+ else if (tagName === 'INPUT' && element.type === 'submit') {
77
+ ownText = element.value;
78
+ }
79
+ // HTML link elements have no labels property.
80
+ const labels = tagName !== 'A' ? Array.from(element.labels) : [];
81
+ const labelTexts = labels.map(label => label.textContent);
82
+ if (ariaLabel) {
83
+ labelTexts.push(ariaLabel);
84
+ }
85
+ const refIDs = new Set([
86
+ element.getAttribute('aria-labelledby') || '',
87
+ element.getAttribute('aria-describedby') || ''
88
+ ].join(' ').split(/\s+/));
89
+ if (refIDs.size) {
90
+ refIDs.forEach(id => {
91
+ const labeler = document.getElementById(id);
92
+ if (labeler) {
93
+ const labelerText = labeler.textContent.trim();
94
+ if (labelerText.length) {
95
+ labelTexts.push(labelerText);
96
+ }
97
+ }
98
+ });
99
+ }
100
+ let legendText = '';
101
+ if (tagName === 'INPUT') {
102
+ const fieldsets = Array.from(document.body.querySelectorAll('fieldset'));
103
+ const inputFieldsets = fieldsets.filter(fieldset => {
104
+ const inputs = Array.from(fieldset.querySelectorAll('input'));
105
+ return inputs.length && inputs[0] === element;
106
+ });
107
+ const inputFieldset = inputFieldsets[0] || null;
108
+ if (inputFieldset) {
109
+ const legend = inputFieldset.querySelector('legend');
110
+ if (legend) {
111
+ legendText = legend.textContent;
112
+ }
113
+ }
114
+ }
115
+ return [legendText].concat(labelTexts, ownText).join(' ');
116
+ }, element);
117
+ }
118
+ // Otherwise, if it is an option:
119
+ else if (tagName === 'OPTION') {
120
+ // Return its text content, prefixed with the text of its select parent if the first option.
121
+ const ownText = await element.textContent();
122
+ const indexJSHandle = await element.getProperty('index');
123
+ const index = await indexJSHandle.jsonValue();
124
+ if (index) {
125
+ totalText = ownText;
126
+ }
127
+ else {
128
+ const selectJSHandle = await page.evaluateHandle(
129
+ element => element.parentElement, element
130
+ );
131
+ const select = await selectJSHandle.asElement();
132
+ if (select) {
133
+ const selectText = await textOf(page, select);
134
+ totalText = [ownText, selectText].join(' ');
135
+ }
136
+ else {
137
+ totalText = ownText;
138
+ }
139
+ }
140
+ }
141
+ // Otherwise, i.e. if it is not an input, select, or option:
142
+ else {
143
+ // Get its text content.
144
+ totalText = await element.textContent();
145
+ }
146
+ return debloat(totalText);
147
+ }
148
+ else {
149
+ return null;
150
+ }
151
+ };
152
+ // Adds a wait error result to an act.
153
+ const waitError = (page, act, error, what) => {
154
+ console.log(`ERROR waiting for ${what} (${error.message})`);
155
+ act.result.found = false;
156
+ act.result.url = page.url();
157
+ act.result.error = `ERROR waiting for ${what}`;
158
+ return false;
159
+ };
160
+ // Performs one act on a live page and returns the page (which a page act replaces).
161
+ exports.doInteractionAct = async ({
162
+ page, report, act, actIndex, actCount = 0, interaction = exports.defaultInteraction
163
+ }) => {
164
+ const {goTo, getNonce, wait} = getLaunch();
165
+ const {type} = act;
166
+ // The modality is recorded for the future modalities; only 'efficient' exists today.
167
+ act.modality = interaction.modality;
168
+ if (type === 'url') {
169
+ // Identify the URL.
170
+ const requestedURL = act.which.replace('__dirname', __dirname);
171
+ // Visit it and wait until the DOM is loaded.
172
+ const navResult = await goTo(report, page, requestedURL, 15000, 'domcontentloaded');
173
+ // If the visit succeeded:
174
+ if (navResult.success) {
175
+ // Revise the temporary report URL to this URL.
176
+ report.target.url = requestedURL;
177
+ // Add the script nonce, if any, to the job data.
178
+ const {response} = navResult;
179
+ const scriptNonce = await getNonce(response);
180
+ if (scriptNonce) {
181
+ report.jobData.lastScriptNonce = scriptNonce;
182
+ }
183
+ // Add the resulting URL to the act.
184
+ act.result = {success: true, url: page.url()};
185
+ }
186
+ // Otherwise, i.e. if the visit failed (goTo reports a prohibited redirection as a failure):
187
+ else {
188
+ // Report this.
189
+ addError(true, false, report, actIndex, `ERROR: Visit failed (${navResult.error})`);
190
+ }
191
+ }
192
+ // Otherwise, if the act is a wait for text:
193
+ else if (type === 'wait') {
194
+ const {what, which} = act;
195
+ console.log(`>> ${what}`);
196
+ const result = act.result = {};
197
+ // If the text is to be the URL:
198
+ if (what === 'url') {
199
+ // Wait for the URL to be the exact text.
200
+ try {
201
+ await page.waitForURL(which, {timeout: 15000});
202
+ result.found = true;
203
+ result.url = page.url();
204
+ }
205
+ // If the wait times out:
206
+ catch(error) {
207
+ // Report this.
208
+ waitError(page, act, error, 'text in the URL');
209
+ }
210
+ }
211
+ // Otherwise, if the text is to be a substring of the page title:
212
+ else if (what === 'title') {
213
+ // Wait for the page title to include the text, case-insensitively.
214
+ try {
215
+ await page.waitForFunction(
216
+ text => document
217
+ && document.title
218
+ && document.title.toLowerCase().includes(text.toLowerCase()),
219
+ which,
220
+ {
221
+ polling: 1000,
222
+ timeout: 5000
223
+ }
224
+ );
225
+ result.found = true;
226
+ result.title = await page.title();
227
+ }
228
+ // If the wait times out:
229
+ catch(error) {
230
+ // Report this.
231
+ waitError(page, act, error, 'text in the title');
232
+ }
233
+ }
234
+ // Otherwise, if the text is to be a substring of the text of the page body:
235
+ else if (what === 'body') {
236
+ // Wait for the body to include the text, case-insensitively.
237
+ try {
238
+ await page.waitForFunction(
239
+ text => document
240
+ && document.body
241
+ && document.body.innerText.toLowerCase().includes(text.toLowerCase()),
242
+ which,
243
+ {
244
+ polling: 2000,
245
+ timeout: 15000
246
+ }
247
+ );
248
+ result.found = true;
249
+ }
250
+ // If the wait times out:
251
+ catch(error) {
252
+ // Report this.
253
+ waitError(page, act, error, 'text in the body');
254
+ }
255
+ }
256
+ }
257
+ // Otherwise, if the act is a wait for a state:
258
+ else if (type === 'state') {
259
+ // Wait for it.
260
+ const stateIndex = ['loaded', 'idle'].indexOf(act.which);
261
+ let reached = true;
262
+ await page.waitForLoadState(
263
+ ['domcontentloaded', 'networkidle'][stateIndex], {timeout: [10000, 15000][stateIndex]}
264
+ )
265
+ // If the wait times out:
266
+ .catch(async error => {
267
+ // Report this.
268
+ reached = false;
269
+ console.log(`ERROR waiting for page to be ${act.which} (${error.message})`);
270
+ addError(true, false, report, actIndex, `ERROR waiting for page to be ${act.which}`);
271
+ });
272
+ // Add state data to the act.
273
+ act.result = {
274
+ success: reached,
275
+ state: act.which
276
+ };
277
+ }
278
+ // Otherwise, if the act is a page switch:
279
+ else if (type === 'page') {
280
+ const context = page.context();
281
+ page = await context.waitForEvent('page');
282
+ // Wait until it is idle.
283
+ await page.waitForLoadState('networkidle', {timeout: 15000});
284
+ // Add the resulting URL to the act.
285
+ const result = {
286
+ url: page.url()
287
+ };
288
+ act.result = result;
289
+ }
290
+ // Otherwise, if the page has a URL:
291
+ else if (page.url() && page.url() !== 'about:blank') {
292
+ const url = page.url();
293
+ // Add the URL to the act.
294
+ act.actualURL = url;
295
+ // If the act is a revelation:
296
+ if (type === 'reveal') {
297
+ act.result = {
298
+ success: true
299
+ };
300
+ // Make all elements in the page visible.
301
+ await page.$$eval('body *', elements => {
302
+ elements.forEach(element => {
303
+ const styleDec = window.getComputedStyle(element);
304
+ if (styleDec.display === 'none') {
305
+ element.style.display = 'initial';
306
+ }
307
+ if (['hidden', 'collapse'].includes(styleDec.visibility)) {
308
+ element.style.visibility = 'inherit';
309
+ }
310
+ });
311
+ })
312
+ .catch(error => {
313
+ console.log(`ERROR making all elements visible (${error.message})`);
314
+ act.result.success = false;
315
+ });
316
+ }
317
+ // Otherwise, if the act is a screenshot:
318
+ else if (type === 'shoot') {
319
+ const {exclusionSelector, colorType, action} = act;
320
+ // Make and dispose of a full-page screenshot.
321
+ const shotInfo = await shoot(page, report, {
322
+ exclusion: exclusionSelector ? page.locator(exclusionSelector) : null,
323
+ colorType: [0, 2, 4, 6].includes(colorType) ? colorType : null,
324
+ action: ['return', 'report', 'file'].includes(action) ? action : 'report'
325
+ });
326
+ // Add the PNG base-64 encoding, image index, or file path to the act result.
327
+ act.result = shotInfo !== ''
328
+ ? {success: true, shotInfo}
329
+ : {success: false, prevented: true};
330
+ }
331
+ // Otherwise, if the act is a move:
332
+ else if (moves[type]) {
333
+ // The act's own selector (any Playwright selector) replaces the type's element selector.
334
+ const selector = act.selector || (typeof moves[type] === 'string' ? moves[type] : act.what);
335
+ // Try up to 5 times to:
336
+ act.result = {found: false};
337
+ let selection = {};
338
+ let tries = 0;
339
+ const slimText = act.which ? debloat(act.which) : '';
340
+ while (tries++ < 5 && ! act.result.found) {
341
+ if (page) {
342
+ // Identify the elements of the specified type.
343
+ const selections = await page.$$(selector);
344
+ // If there are any:
345
+ if (selections.length) {
346
+ // If there are enough to make a match possible:
347
+ if ((act.index || 0) < selections.length) {
348
+ // For each element of the specified type:
349
+ let matchCount = 0;
350
+ const selectionTexts = [];
351
+ for (selection of selections) {
352
+ // Add its lower-case text or an empty string to the list of element texts.
353
+ const selectionText = slimText ? await textOf(page, selection) : '';
354
+ selectionTexts.push(selectionText);
355
+ // If its text includes any specified text, case-insensitively:
356
+ if (selectionText.includes(slimText)) {
357
+ // If the element has the specified index among such elements:
358
+ if (matchCount++ === (act.index || 0)) {
359
+ // Report it as the matching element and stop checking.
360
+ act.result.found = true;
361
+ act.result.textSpec = slimText;
362
+ act.result.textContent = selectionText;
363
+ break;
364
+ }
365
+ }
366
+ }
367
+ // If no element satisfied the specifications:
368
+ if (! act.result.found) {
369
+ // Add the failure data to the temporary report.
370
+ act.result.success = false;
371
+ act.result.error = 'exhausted';
372
+ act.result.typeElementCount = selections.length;
373
+ if (slimText) {
374
+ act.result.textElementCount = --matchCount;
375
+ }
376
+ act.result.message = 'Not enough specified elements exist';
377
+ act.result.candidateTexts = selectionTexts;
378
+ }
379
+ }
380
+ // Otherwise, i.e. if there are too few such elements to make a match possible:
381
+ else {
382
+ // Add the failure data to the temporary report.
383
+ act.result.success = false;
384
+ act.result.error = 'fewer';
385
+ act.result.typeElementCount = selections.length;
386
+ act.result.message = 'Elements of specified type too few';
387
+ }
388
+ }
389
+ // Otherwise, i.e. if there are no elements of the specified type:
390
+ else {
391
+ // Add the failure data to the temporary report.
392
+ act.result.success = false;
393
+ act.result.error = 'none';
394
+ act.result.typeElementCount = 0;
395
+ act.result.message = 'No elements of specified type found';
396
+ }
397
+ }
398
+ // Otherwise, i.e. if the page no longer exists:
399
+ else {
400
+ // Add the failure data to the temporary report.
401
+ act.result.success = false;
402
+ act.result.error = 'gone';
403
+ act.result.message = 'Page gone';
404
+ }
405
+ if (! act.result.found) {
406
+ await wait(2000);
407
+ }
408
+ }
409
+ // If a match was found:
410
+ if (act.result.found) {
411
+ // FUNCTION DEFINITION START
412
+ // Performs a click or Enter keypress and waits for the network to be idle.
413
+ const doAndWait = async isClick => {
414
+ // Perform and report the move.
415
+ const move = isClick ? 'click' : 'Enter keypress';
416
+ try {
417
+ await isClick
418
+ ? selection.click({timeout: 4000})
419
+ : selection.press('Enter', {timeout: 4000});
420
+ act.result.success = true;
421
+ act.result.move = move;
422
+ }
423
+ // If the move fails:
424
+ catch(error) {
425
+ // Report this.
426
+ addError(true, false, report, actIndex, `ERROR: ${move} failed`);
427
+ }
428
+ if (act.result.success) {
429
+ try {
430
+ await page.context().waitForEvent('networkidle', {timeout: 10000});
431
+ act.result.idleTimely = true;
432
+ }
433
+ catch(error) {
434
+ console.log(`ERROR: Network busy after ${move} (${errorStart(error)})`);
435
+ act.result.idleTimely = false;
436
+ }
437
+ // Add the page URL to the result.
438
+ act.result.newURL = page.url();
439
+ }
440
+ };
441
+ // FUNCTION DEFINITION END
442
+ // If the move is a button click, perform it.
443
+ if (type === 'button') {
444
+ await selection.click({timeout: 3000});
445
+ act.result.success = true;
446
+ act.result.move = 'clicked';
447
+ }
448
+ // Otherwise, if it is checking a radio button or checkbox, perform it.
449
+ else if (['checkbox', 'radio'].includes(type)) {
450
+ await selection.waitForElementState('stable', {timeout: 2000})
451
+ .catch(error => {
452
+ console.log(`ERROR waiting for stable ${type} (${error.message})`);
453
+ act.result.success = false;
454
+ act.result.error = `ERROR waiting for stable ${type}`;
455
+ });
456
+ if (! act.result.error) {
457
+ const isEnabled = await selection.isEnabled();
458
+ if (isEnabled) {
459
+ await selection.check({
460
+ force: true,
461
+ timeout: 2000
462
+ })
463
+ .catch(error => {
464
+ console.log(`ERROR checking ${type} (${error.message})`);
465
+ act.result.success = false;
466
+ act.result.error = `ERROR checking ${type}`;
467
+ });
468
+ if (! act.result.error) {
469
+ act.result.success = true;
470
+ act.result.move = 'checked';
471
+ }
472
+ }
473
+ else {
474
+ const message = `ERROR: could not check ${type} because disabled`;
475
+ act.result.success = false;
476
+ act.result.error = message;
477
+ }
478
+ }
479
+ }
480
+ // Otherwise, if it is focusing the element, perform it.
481
+ else if (type === 'focus') {
482
+ await selection.focus({timeout: 2000});
483
+ act.result.success = true;
484
+ act.result.move = 'focused';
485
+ }
486
+ // Otherwise, if it is clicking a link:
487
+ else if (type === 'link') {
488
+ const href = await selection.getAttribute('href');
489
+ const target = await selection.getAttribute('target');
490
+ act.result.href = href || 'NONE';
491
+ act.result.target = target || 'DEFAULT';
492
+ // If the destination is a new page:
493
+ if (target && target !== '_self') {
494
+ // Click the link and wait for the network to be idle.
495
+ doAndWait(true);
496
+ }
497
+ // Otherwise, i.e. if the destination is in the current page:
498
+ else {
499
+ // Click the link and wait for the resulting navigation.
500
+ try {
501
+ await selection.click({timeout: 5000});
502
+ // Wait for the new content to load.
503
+ await page.waitForLoadState('domcontentloaded', {timeout: 6000});
504
+ act.result.success = true;
505
+ act.result.move = 'clicked';
506
+ act.result.newURL = page.url();
507
+ }
508
+ // If the click or load failed:
509
+ catch(error) {
510
+ // Report this.
511
+ console.log(`ERROR clicking link (${errorStart(error)})`);
512
+ act.result.success = false;
513
+ act.result.error = 'unclickable';
514
+ act.result.message = 'ERROR: click or load timed out';
515
+ }
516
+ // If the link click succeeded:
517
+ if (! act.result.error) {
518
+ // Add success data to the temporary report.
519
+ act.result.success = true;
520
+ act.result.move = 'clicked';
521
+ }
522
+ }
523
+ }
524
+ // Otherwise, if it is selecting an option in a select list, perform it.
525
+ else if (type === 'select') {
526
+ const options = await selection.$$('option');
527
+ let optionText = '';
528
+ if (options && Array.isArray(options) && options.length) {
529
+ const optionTexts = [];
530
+ for (const option of options) {
531
+ const optionText = await option.textContent();
532
+ optionTexts.push(optionText);
533
+ }
534
+ const matchTexts = optionTexts.map(
535
+ (text, index) => text.includes(act.what) ? index : -1
536
+ );
537
+ const index = matchTexts.filter(text => text > -1)[act.index || 0];
538
+ if (index !== undefined) {
539
+ await selection.selectOption({index});
540
+ optionText = optionTexts[index];
541
+ }
542
+ // If no option text matches, try the specification as an option value or label.
543
+ else {
544
+ const selected = await selection.selectOption(act.what, {timeout: 2000})
545
+ .catch(() => []);
546
+ if (selected.length) {
547
+ optionText = act.what;
548
+ }
549
+ }
550
+ }
551
+ act.result.success = true;
552
+ act.result.move = 'selected';
553
+ act.result.option = optionText;
554
+ }
555
+ // Otherwise, if it is entering text in an input element:
556
+ else if (['text', 'search'].includes(type)) {
557
+ act.result.attributes = {};
558
+ const {attributes} = act.result;
559
+ const type = await selection.getAttribute('type');
560
+ const label = await selection.getAttribute('aria-label');
561
+ const labelRefs = await selection.getAttribute('aria-labelledby');
562
+ attributes.type = type || '';
563
+ attributes.label = label || '';
564
+ attributes.labelRefs = labelRefs || '';
565
+ // If the text contains a placeholder for an environment variable:
566
+ let {what} = act;
567
+ if (/__[A-Z]+__/.test(what)) {
568
+ // Replace it.
569
+ const envKey = /__([A-Z]+)__/.exec(what)[1];
570
+ const envValue = process.env[envKey];
571
+ what = what.replace(/__[A-Z]+__/, envValue);
572
+ }
573
+ // Enter the text, replacing any existing value if the act says so.
574
+ if (act.clear) {
575
+ await selection.fill(what);
576
+ }
577
+ else {
578
+ await selection.type(what);
579
+ }
580
+ report.jobData.presses += what.length;
581
+ act.result.success = true;
582
+ act.result.move = 'entered';
583
+ // If the input is a search input:
584
+ if (type === 'search') {
585
+ // Press the Enter key and wait for a network to be idle.
586
+ doAndWait(false);
587
+ }
588
+ }
589
+ // Otherwise, i.e. if the move is unknown, add the failure to the act.
590
+ else {
591
+ // Report the error.
592
+ const message = 'ERROR: move unknown';
593
+ act.result.success = false;
594
+ act.result.error = message;
595
+ }
596
+ }
597
+ // Otherwise, i.e. if no match was found:
598
+ else {
599
+ // RLeport this.
600
+ act.result.success = false;
601
+ act.result.error = 'absent';
602
+ act.result.message = 'ERROR: specified element not found';
603
+ console.log('ERROR: Specified element not found');
604
+ }
605
+ }
606
+ // Otherwise, if the act is a keypress:
607
+ else if (type === 'press') {
608
+ // Identify the number of times to press the key.
609
+ let times = 1 + (act.again || 0);
610
+ report.jobData.presses += times;
611
+ const key = act.which;
612
+ // Press the key.
613
+ while (times--) {
614
+ await page.keyboard.press(key);
615
+ }
616
+ const qualifier = act.again ? `${1 + act.again} times` : 'once';
617
+ act.result = {
618
+ success: true,
619
+ message: `pressed ${qualifier}`
620
+ };
621
+ }
622
+ // Otherwise, if it is a repetitive keyboard navigation:
623
+ else if (type === 'presses') {
624
+ const {navKey, what, which, withItems} = act;
625
+ const matchTexts = which ? which.map(text => debloat(text)) : [];
626
+ // Initialize the loop variables.
627
+ let status = 'more';
628
+ let presses = 0;
629
+ let amountRead = 0;
630
+ let items = [];
631
+ let matchedText;
632
+ // As long as a matching element has not been reached:
633
+ while (status === 'more') {
634
+ // Press the Escape key to dismiss any modal dialog.
635
+ await page.keyboard.press('Escape');
636
+ // Press the specified navigation key.
637
+ await page.keyboard.press(navKey);
638
+ presses++;
639
+ // Identify the newly current element or a failure.
640
+ const currentJSHandle = await page.evaluateHandle(actCount => {
641
+ // Initialize it as the focused element.
642
+ let currentElement = document.activeElement;
643
+ // If it exists in the page:
644
+ if (currentElement && currentElement.tagName !== 'BODY') {
645
+ // Change it, if necessary, to its active descendant.
646
+ if (currentElement.hasAttribute('aria-activedescendant')) {
647
+ currentElement = document.getElementById(
648
+ currentElement.getAttribute('aria-activedescendant')
649
+ );
650
+ }
651
+ // Or change it, if necessary, to its selected option.
652
+ else if (currentElement.tagName === 'SELECT') {
653
+ const currentIndex = Math.max(0, currentElement.selectedIndex);
654
+ const options = currentElement.querySelectorAll('option');
655
+ currentElement = options[currentIndex];
656
+ }
657
+ // Or change it, if necessary, to its active shadow-DOM element.
658
+ else if (currentElement.shadowRoot) {
659
+ currentElement = currentElement.shadowRoot.activeElement;
660
+ }
661
+ // If there is a current element:
662
+ if (currentElement) {
663
+ // If it was already reached within this act:
664
+ if (currentElement.dataset.pressesReached === actCount.toString(10)) {
665
+ // Report the error.
666
+ console.log(`ERROR: ${currentElement.tagName} element reached again`);
667
+ status = 'ERROR';
668
+ return 'ERROR: locallyExhausted';
669
+ }
670
+ // Otherwise, i.e. if it is newly reached within this act:
671
+ else {
672
+ // Mark and return it.
673
+ currentElement.dataset.pressesReached = actCount;
674
+ return currentElement;
675
+ }
676
+ }
677
+ // Otherwise, i.e. if there is no current element:
678
+ else {
679
+ // Report the error.
680
+ status = 'ERROR';
681
+ return 'noActiveElement';
682
+ }
683
+ }
684
+ // Otherwise, i.e. if there is no focus in the page:
685
+ else {
686
+ // Report the error.
687
+ status = 'ERROR';
688
+ return 'ERROR: globallyExhausted';
689
+ }
690
+ }, actCount);
691
+ // If the current element exists:
692
+ const currentElement = currentJSHandle.asElement();
693
+ if (currentElement) {
694
+ // Update the data.
695
+ const tagNameJSHandle = await currentElement.getProperty('tagName');
696
+ const tagName = await tagNameJSHandle.jsonValue();
697
+ const text = await textOf(page, currentElement);
698
+ // If the text of the current element was found:
699
+ if (text !== null) {
700
+ const textLength = text.length;
701
+ // If it is non-empty and there are texts to match:
702
+ if (matchTexts.length && textLength) {
703
+ // Identify the matching text.
704
+ matchedText = matchTexts.find(matchText => text.includes(matchText));
705
+ }
706
+ // Update the item data if required.
707
+ if (withItems) {
708
+ const itemData = {
709
+ tagName,
710
+ text,
711
+ textLength
712
+ };
713
+ if (matchedText) {
714
+ itemData.matchedText = matchedText;
715
+ }
716
+ items.push(itemData);
717
+ }
718
+ amountRead += textLength;
719
+ // If there is no text-match failure:
720
+ if (matchedText || ! matchTexts.length) {
721
+ // If the element has any specified tag name:
722
+ if (! what || tagName === what) {
723
+ // Change the status.
724
+ status = 'done';
725
+ // Perform the action.
726
+ const inputText = act.text;
727
+ if (inputText) {
728
+ await page.keyboard.type(inputText);
729
+ presses += inputText.length;
730
+ }
731
+ if (act.action) {
732
+ presses++;
733
+ await page.keyboard.press(act.action);
734
+ await page.waitForLoadState();
735
+ }
736
+ }
737
+ }
738
+ }
739
+ else {
740
+ status = 'ERROR';
741
+ }
742
+ }
743
+ // Otherwise, i.e. if there was a failure:
744
+ else {
745
+ // Update the status.
746
+ status = await currentJSHandle.jsonValue();
747
+ }
748
+ }
749
+ // Add the result to the act.
750
+ act.result = {
751
+ success: true,
752
+ status,
753
+ totals: {
754
+ presses,
755
+ amountRead
756
+ }
757
+ };
758
+ if (status === 'done' && matchedText) {
759
+ act.result.matchedText = matchedText;
760
+ }
761
+ if (withItems) {
762
+ act.result.items = items;
763
+ }
764
+ // Add the totals to the temporary report.
765
+ report.jobData.presses += presses;
766
+ report.jobData.amountRead += amountRead;
767
+ }
768
+ // Otherwise, i.e. if the act type is unknown:
769
+ else {
770
+ // Report this.
771
+ addError(true, false, report, actIndex, 'ERROR: Invalid act type');
772
+ }
773
+ }
774
+ // Otherwise, a page URL is required but does not exist, so:
775
+ else {
776
+ // Report this.
777
+ addError(true, false, report, actIndex, 'ERROR: Page has no URL');
778
+ }
779
+ return page;
780
+ };
781
+ // Re-enacts the acts that reached a checkpoint, on a freshly navigated page, and returns the
782
+ // page. Throws if an act fails, naming the act. The recorded acts are not modified: clones are
783
+ // performed, against a copy of the report whose counters are discarded.
784
+ exports.replayActs = async (page, report, checkpoint) => {
785
+ const startTime = Date.now();
786
+ const replayReport = {
787
+ ... report,
788
+ target: {... report.target},
789
+ jobData: {... report.jobData, presses: 0, amountRead: 0},
790
+ acts: report.acts.map(act => ({... act}))
791
+ };
792
+ const interaction = checkpoint.interaction || exports.defaultInteraction;
793
+ for (const actIndex of checkpoint.replay) {
794
+ const act = replayReport.acts[actIndex];
795
+ delete act.result;
796
+ delete act.actualURL;
797
+ page = await exports.doInteractionAct({
798
+ page, report: replayReport, act, actIndex, actCount: actIndex, interaction
799
+ });
800
+ const failed = act.result && (act.result.success === false || act.result.found === false);
801
+ if (failed || replayReport.jobData.aborted) {
802
+ const reason = act.result?.error || act.result?.message || replayReport.jobData.abortMessage
803
+ || 'unknown';
804
+ throw new Error(`checkpoint replay failed at act ${actIndex} (${act.type}: ${reason})`);
805
+ }
806
+ }
807
+ return {page, elapsedMs: Date.now() - startTime, actCount: checkpoint.replay.length};
808
+ };