testaro 78.0.6 → 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 (361) 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 +25 -19
  7. package/CONTAINERS.md +10 -6
  8. package/CONTRIBUTING.md +29 -3
  9. package/Dockerfile +1 -1
  10. package/README.md +75 -20
  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 +373 -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 +313 -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 +63 -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/getSource.js +1 -0
  38. package/procs/job.js +116 -1
  39. package/procs/launch.d.ts +40 -0
  40. package/procs/launch.js +239 -108
  41. package/procs/nu.d.ts +48 -0
  42. package/procs/nu.js +1 -0
  43. package/procs/scope.js +196 -0
  44. package/procs/shoot.d.ts +33 -0
  45. package/procs/shoot.js +3 -1
  46. package/procs/standard.d.ts +18 -0
  47. package/procs/standard.js +83 -0
  48. package/procs/standard.ts +126 -0
  49. package/procs/testAct.js +145 -0
  50. package/procs/testaro.d.ts +20 -0
  51. package/procs/testaro.js +237 -189
  52. package/procs/testaro.ts +321 -0
  53. package/procs/userPath.js +126 -0
  54. package/procs/xPath.d.ts +4 -0
  55. package/procs/xPath.js +82 -64
  56. package/procs/xPath.ts +106 -0
  57. package/procs/xPathScript.d.ts +6 -0
  58. package/procs/xPathScript.js +70 -0
  59. package/run.js +3 -2
  60. package/surea11y/README.md +44 -0
  61. package/surea11y/surea11y.browser.js +14 -0
  62. package/testaro/adbID.d.ts +3 -0
  63. package/testaro/adbID.js +39 -41
  64. package/testaro/adbID.ts +66 -0
  65. package/testaro/allCapStyle.d.ts +3 -0
  66. package/testaro/allCapStyle.js +32 -34
  67. package/testaro/allCapStyle.ts +55 -0
  68. package/testaro/allCaps.d.ts +16 -0
  69. package/testaro/allCaps.js +179 -151
  70. package/testaro/allCaps.ts +228 -0
  71. package/testaro/allHidden.d.ts +13 -0
  72. package/testaro/allHidden.js +30 -30
  73. package/testaro/allHidden.ts +50 -0
  74. package/testaro/allSlanted.d.ts +3 -0
  75. package/testaro/allSlanted.js +31 -33
  76. package/testaro/allSlanted.ts +54 -0
  77. package/testaro/altScheme.d.ts +3 -0
  78. package/testaro/altScheme.js +28 -30
  79. package/testaro/altScheme.ts +50 -0
  80. package/testaro/attVal.d.ts +3 -0
  81. package/testaro/attVal.js +21 -21
  82. package/testaro/attVal.ts +52 -0
  83. package/testaro/autocomplete.d.ts +3 -0
  84. package/testaro/autocomplete.js +60 -75
  85. package/testaro/autocomplete.ts +101 -0
  86. package/testaro/bulk.d.ts +13 -0
  87. package/testaro/bulk.js +33 -33
  88. package/testaro/bulk.ts +55 -0
  89. package/testaro/buttonMenu.d.ts +9 -0
  90. package/testaro/buttonMenu.js +318 -319
  91. package/testaro/buttonMenu.ts +391 -0
  92. package/testaro/captionLoc.d.ts +3 -0
  93. package/testaro/captionLoc.js +18 -20
  94. package/testaro/captionLoc.ts +40 -0
  95. package/testaro/datalistRef.d.ts +3 -0
  96. package/testaro/datalistRef.js +34 -36
  97. package/testaro/datalistRef.ts +55 -0
  98. package/testaro/distortion.d.ts +3 -0
  99. package/testaro/distortion.js +58 -26
  100. package/testaro/distortion.ts +81 -0
  101. package/testaro/docType.d.ts +15 -0
  102. package/testaro/docType.js +26 -25
  103. package/testaro/docType.ts +45 -0
  104. package/testaro/dupAtt.d.ts +16 -0
  105. package/testaro/dupAtt.js +114 -104
  106. package/testaro/dupAtt.ts +144 -0
  107. package/testaro/elements.d.ts +6 -0
  108. package/testaro/elements.js +153 -153
  109. package/testaro/elements.ts +215 -0
  110. package/testaro/embAc.d.ts +3 -0
  111. package/testaro/embAc.js +20 -20
  112. package/testaro/embAc.ts +40 -0
  113. package/testaro/focAll.d.ts +13 -0
  114. package/testaro/focAll.js +186 -191
  115. package/testaro/focAll.ts +217 -0
  116. package/testaro/focAndOp.d.ts +3 -0
  117. package/testaro/focAndOp.js +101 -104
  118. package/testaro/focAndOp.ts +128 -0
  119. package/testaro/focInd.d.ts +3 -0
  120. package/testaro/focInd.js +65 -66
  121. package/testaro/focInd.ts +96 -0
  122. package/testaro/focVis.d.ts +3 -0
  123. package/testaro/focVis.js +29 -30
  124. package/testaro/focVis.ts +52 -0
  125. package/testaro/headEl.d.ts +10 -0
  126. package/testaro/headEl.js +62 -62
  127. package/testaro/headEl.ts +82 -0
  128. package/testaro/headingAmb.d.ts +3 -0
  129. package/testaro/headingAmb.js +55 -63
  130. package/testaro/headingAmb.ts +72 -0
  131. package/testaro/hovInd.d.ts +12 -0
  132. package/testaro/hovInd.js +164 -130
  133. package/testaro/hovInd.ts +199 -0
  134. package/testaro/hover.d.ts +3 -0
  135. package/testaro/hover.js +156 -126
  136. package/testaro/hover.ts +154 -0
  137. package/testaro/hr.d.ts +3 -0
  138. package/testaro/hr.js +15 -17
  139. package/testaro/hr.ts +36 -0
  140. package/testaro/imageLink.d.ts +3 -0
  141. package/testaro/imageLink.js +18 -20
  142. package/testaro/imageLink.ts +42 -0
  143. package/testaro/labClash.d.ts +3 -0
  144. package/testaro/labClash.js +32 -33
  145. package/testaro/labClash.ts +54 -0
  146. package/testaro/legendLoc.d.ts +3 -0
  147. package/testaro/legendLoc.js +18 -20
  148. package/testaro/legendLoc.ts +42 -0
  149. package/testaro/lineHeight.d.ts +3 -0
  150. package/testaro/lineHeight.js +44 -48
  151. package/testaro/lineHeight.ts +70 -0
  152. package/testaro/linkAmb.d.ts +7 -0
  153. package/testaro/linkAmb.js +80 -80
  154. package/testaro/linkAmb.ts +105 -0
  155. package/testaro/linkExt.d.ts +3 -0
  156. package/testaro/linkExt.js +14 -16
  157. package/testaro/linkExt.ts +35 -0
  158. package/testaro/linkOldAtt.d.ts +3 -0
  159. package/testaro/linkOldAtt.js +26 -28
  160. package/testaro/linkOldAtt.ts +48 -0
  161. package/testaro/linkTo.d.ts +3 -0
  162. package/testaro/linkTo.js +23 -22
  163. package/testaro/linkTo.ts +43 -0
  164. package/testaro/linkUl.d.ts +3 -0
  165. package/testaro/linkUl.js +31 -34
  166. package/testaro/linkUl.ts +54 -0
  167. package/testaro/miniText.d.ts +3 -0
  168. package/testaro/miniText.js +42 -44
  169. package/testaro/miniText.ts +68 -0
  170. package/testaro/motion.d.ts +10 -0
  171. package/testaro/motion.js +93 -96
  172. package/testaro/motion.ts +125 -0
  173. package/testaro/nonTable.d.ts +3 -0
  174. package/testaro/nonTable.js +40 -45
  175. package/testaro/nonTable.ts +66 -0
  176. package/testaro/optRoleSel.d.ts +3 -0
  177. package/testaro/optRoleSel.js +17 -19
  178. package/testaro/optRoleSel.ts +41 -0
  179. package/testaro/phOnly.d.ts +3 -0
  180. package/testaro/phOnly.js +19 -21
  181. package/testaro/phOnly.ts +43 -0
  182. package/testaro/pseudoP.d.ts +3 -0
  183. package/testaro/pseudoP.js +37 -38
  184. package/testaro/pseudoP.ts +59 -0
  185. package/testaro/radioSet.d.ts +3 -0
  186. package/testaro/radioSet.js +58 -59
  187. package/testaro/radioSet.ts +79 -0
  188. package/testaro/registry.d.ts +63 -0
  189. package/testaro/registry.js +67 -0
  190. package/testaro/registry.ts +141 -0
  191. package/testaro/role.d.ts +3 -0
  192. package/testaro/role.js +28 -29
  193. package/testaro/role.ts +53 -0
  194. package/testaro/secHeading.d.ts +3 -0
  195. package/testaro/secHeading.js +31 -33
  196. package/testaro/secHeading.ts +53 -0
  197. package/testaro/styleDiff.d.ts +25 -0
  198. package/testaro/styleDiff.js +249 -252
  199. package/testaro/styleDiff.ts +303 -0
  200. package/testaro/tabNav.d.ts +33 -0
  201. package/testaro/tabNav.js +273 -342
  202. package/testaro/tabNav.ts +454 -0
  203. package/testaro/targetsNear.d.ts +9 -0
  204. package/testaro/targetsNear.js +131 -132
  205. package/testaro/targetsNear.ts +160 -0
  206. package/testaro/textNodes.d.ts +6 -0
  207. package/testaro/textNodes.js +139 -135
  208. package/testaro/textNodes.ts +185 -0
  209. package/testaro/textSem.d.ts +3 -0
  210. package/testaro/textSem.js +26 -28
  211. package/testaro/textSem.ts +47 -0
  212. package/testaro/title.d.ts +9 -0
  213. package/testaro/title.js +16 -13
  214. package/testaro/title.ts +31 -0
  215. package/testaro/titledEl.d.ts +3 -0
  216. package/testaro/titledEl.js +16 -18
  217. package/testaro/titledEl.ts +38 -0
  218. package/testaro/zIndex.d.ts +3 -0
  219. package/testaro/zIndex.js +20 -22
  220. package/testaro/zIndex.ts +42 -0
  221. package/tests/alfa.d.ts +45 -0
  222. package/tests/alfa.js +139 -141
  223. package/tests/alfa.ts +214 -0
  224. package/tests/aslint.d.ts +33 -0
  225. package/tests/aslint.js +273 -249
  226. package/tests/aslint.ts +301 -0
  227. package/tests/axe.d.ts +27 -0
  228. package/tests/axe.js +200 -200
  229. package/tests/axe.ts +277 -0
  230. package/tests/ed11y.d.ts +28 -0
  231. package/tests/ed11y.js +142 -99
  232. package/tests/ed11y.ts +178 -0
  233. package/tests/htmlcs.d.ts +21 -0
  234. package/tests/htmlcs.js +175 -140
  235. package/tests/htmlcs.ts +181 -0
  236. package/tests/ibm.d.ts +52 -0
  237. package/tests/ibm.js +165 -166
  238. package/tests/ibm.ts +251 -0
  239. package/tests/nuVal.d.ts +13 -0
  240. package/tests/nuVal.js +107 -112
  241. package/tests/nuVal.ts +145 -0
  242. package/tests/nuVnu.d.ts +15 -0
  243. package/tests/nuVnu.js +142 -111
  244. package/tests/nuVnu.ts +144 -0
  245. package/tests/pour.d.ts +31 -0
  246. package/tests/pour.js +242 -0
  247. package/tests/pour.ts +273 -0
  248. package/tests/qualWeb.d.ts +39 -0
  249. package/tests/qualWeb.js +303 -272
  250. package/tests/qualWeb.ts +415 -0
  251. package/tests/surea11y.d.ts +33 -0
  252. package/tests/surea11y.js +288 -0
  253. package/tests/surea11y.ts +334 -0
  254. package/tests/testaro.d.ts +25 -0
  255. package/tests/testaro.js +746 -652
  256. package/tests/testaro.ts +862 -0
  257. package/tests/wave.d.ts +46 -0
  258. package/tests/wave.js +167 -177
  259. package/tests/wave.ts +252 -0
  260. package/tsconfig.json +17 -0
  261. package/types.d.ts +243 -0
  262. package/types.js +10 -0
  263. package/types.ts +376 -0
  264. package/validation/act/README.md +46 -0
  265. package/validation/act/capture.js +424 -0
  266. package/validation/act/chromium-issue-draft.md +66 -0
  267. package/validation/act/fp-triage-2026-08-22.md +86 -0
  268. package/validation/act/isolation-notes.md +159 -0
  269. package/validation/act/playwright-issue-draft.md +73 -0
  270. package/validation/act/propose-mappings.js +0 -0
  271. package/validation/act/repro-cdp-raw.js +105 -0
  272. package/validation/act/repro-metarefresh.js +65 -0
  273. package/validation/act/score.js +184 -0
  274. package/validation/act/stage3a-stress-report.md +69 -0
  275. package/validation/act/stage3b-mapping-proposals.md +94 -0
  276. package/validation/act/stage3b-triage-draft.md +138 -0
  277. package/validation/act/surea11y-track-a-2026-09-01.md +50 -0
  278. package/validation/executors/netWatch.js +180 -90
  279. package/validation/executors/test.js +17 -2
  280. package/validation/executors/tests.js +118 -10
  281. package/validation/jobs/reports/raw/260901T1000-surea11y-validation.json +964 -0
  282. package/validation/jobs/todo/240101T1200-simple-example.json +14 -6
  283. package/validation/jobs/todo/240101T1300-shoot-example.json +2 -1
  284. package/validation/jobs/todo/260821T1900-pour-validation.json +45 -0
  285. package/validation/jobs/todo/260901T1000-surea11y-validation.json +45 -0
  286. package/validation/knownFailures.json +1 -0
  287. package/validation/tests/jobProperties/adbID.json +27 -2
  288. package/validation/tests/jobProperties/{focOp.json → allCapStyle.json} +56 -53
  289. package/validation/tests/jobProperties/allCaps.json +25 -0
  290. package/validation/tests/jobProperties/allHidden.json +133 -13
  291. package/validation/tests/jobProperties/allSlanted.json +3 -3
  292. package/validation/tests/jobProperties/altScheme.json +23 -8
  293. package/validation/tests/jobProperties/attVal.json +57 -57
  294. package/validation/tests/jobProperties/autocomplete.json +11 -1
  295. package/validation/tests/jobProperties/bulk.json +6 -1
  296. package/validation/tests/jobProperties/buttonMenu.json +63 -42
  297. package/validation/tests/jobProperties/captionLoc.json +1 -6
  298. package/validation/tests/jobProperties/checkpoint-browser.json +401 -0
  299. package/validation/tests/jobProperties/checkpoint-page.json +397 -0
  300. package/validation/tests/jobProperties/checkpoint.json +395 -0
  301. package/validation/tests/jobProperties/datalistRef.json +20 -5
  302. package/validation/tests/jobProperties/distortion.json +28 -3
  303. package/validation/tests/jobProperties/docType.json +2 -2
  304. package/validation/tests/jobProperties/dupAtt.json +48 -33
  305. package/validation/tests/jobProperties/elements.json +28 -28
  306. package/validation/tests/jobProperties/embAc.json +36 -31
  307. package/validation/tests/jobProperties/focAndOp.json +284 -0
  308. package/validation/tests/jobProperties/focInd.json +34 -34
  309. package/validation/tests/jobProperties/focVis.json +1 -1
  310. package/validation/tests/jobProperties/hover.json +39 -37
  311. package/validation/tests/jobProperties/hr.json +13 -3
  312. package/validation/tests/jobProperties/imageLink.json +0 -5
  313. package/validation/tests/jobProperties/labClash.json +68 -33
  314. package/validation/tests/jobProperties/legendLoc.json +1 -6
  315. package/validation/tests/jobProperties/lineHeight.json +2 -2
  316. package/validation/tests/jobProperties/linkAmb.json +13 -18
  317. package/validation/tests/jobProperties/linkExt.json +1 -1
  318. package/validation/tests/jobProperties/linkOldAtt.json +12 -2
  319. package/validation/tests/jobProperties/linkTo.json +1 -1
  320. package/validation/tests/jobProperties/linkUl.json +65 -65
  321. package/validation/tests/jobProperties/miniText.json +7 -2
  322. package/validation/tests/jobProperties/motion.json +4 -50
  323. package/validation/tests/jobProperties/nonTable.json +48 -3
  324. package/validation/tests/jobProperties/optRoleSel.json +12 -2
  325. package/validation/tests/jobProperties/phOnly.json +6 -16
  326. package/validation/tests/jobProperties/pseudoP.json +17 -2
  327. package/validation/tests/jobProperties/radioSet.json +30 -30
  328. package/validation/tests/jobProperties/role.json +25 -5
  329. package/validation/tests/jobProperties/secHeading.json +26 -21
  330. package/validation/tests/jobProperties/styleDiff.json +35 -5
  331. package/validation/tests/jobProperties/tabNav.json +3 -1
  332. package/validation/tests/jobProperties/targetsNear.json +237 -0
  333. package/validation/tests/jobProperties/textNodes.json +29 -29
  334. package/validation/tests/jobProperties/textSem.json +26 -1
  335. package/validation/tests/jobProperties/title.json +12 -2
  336. package/validation/tests/jobProperties/titledEl.json +39 -9
  337. package/validation/tests/jobProperties/userPath.json +317 -0
  338. package/validation/tests/jobProperties/zIndex.json +40 -35
  339. package/validation/tests/targets/allCapStyle/index.html +26 -0
  340. package/validation/tests/targets/checkpoint/index.html +38 -0
  341. package/validation/tests/targets/datalistRef/index.html +1 -1
  342. package/validation/tests/targets/focAndOp/bad.html +29 -0
  343. package/validation/tests/targets/{focOp → focAndOp}/good.html +3 -1
  344. package/validation/tests/targets/focInd/bad.html +2 -1
  345. package/validation/tests/targets/headEl/index.html +10 -1
  346. package/validation/tests/targets/{targetSmall → targetsNear}/index.html +15 -1
  347. package/validation/tests/targets/userPath/index.html +42 -0
  348. package/validation/validateTest.js +62 -10
  349. package/.claude/settings.local.json +0 -11
  350. package/.eslintrc.json +0 -41
  351. package/htmlcs/.eslintrc.json +0 -67
  352. package/memory/MEMORY.md +0 -3
  353. package/memory/project_validation_pause.md +0 -10
  354. package/validation/tests/jobProperties/linkTitle.json +0 -127
  355. package/validation/tests/jobProperties/opFoc.json +0 -164
  356. package/validation/tests/jobProperties/targetSmall.json +0 -152
  357. package/validation/tests/jobProperties/targetTiny.json +0 -142
  358. package/validation/tests/targets/focOp/bad.html +0 -25
  359. package/validation/tests/targets/linkTitle/index.html +0 -24
  360. package/validation/tests/targets/opFoc/bad.html +0 -26
  361. package/validation/tests/targets/opFoc/good.html +0 -23
@@ -0,0 +1,17 @@
1
+ import type { Page } from 'playwright';
2
+ import type { Catalog, Report } from '../types';
3
+ export interface CatalogSnapshot {
4
+ entries: Catalog;
5
+ pathIDs: Record<string, string>;
6
+ firstIndex: number;
7
+ nextIndex: number;
8
+ elementCount: number;
9
+ }
10
+ export declare const getAriaSnapshot: (page: Page) => Promise<string>;
11
+ export declare const catalogPage: (page: Page, report: Report, { checkpoint, restoreDetails }: {
12
+ checkpoint: number;
13
+ restoreDetails: boolean;
14
+ }) => Promise<CatalogSnapshot>;
15
+ export declare const getCatalog: (report: Report) => Promise<Catalog>;
16
+ export declare const pruneCheckpoint: (report: Report, checkpoint: number) => number;
17
+ export declare const pruneCatalog: (report: Report) => void;
package/procs/catalog.js CHANGED
@@ -1,99 +1,56 @@
1
+ "use strict";
1
2
  /*
3
+ © 2026 Jeff Witt.
2
4
  © 2025–2026 Jonathan Robert Pool.
3
5
 
4
6
  Licensed under the MIT License. See LICENSE file at the project root or https://opensource.org/license/mit/ for details.
5
7
 
6
8
  SPDX-License-Identifier: MIT
7
9
  */
8
-
9
- /*
10
- catalog
11
- Creates and returns a catalog of a target.
12
-
13
- A catalog is an object with one property per element in the target. Each property has the element index as its key and has an object as its value, with 7 properties:
14
- - tagName
15
- - id
16
- - startTag
17
- - text
18
- - textLinkable
19
- - boxID
20
- - pathID
21
- - headingIndex
22
- */
23
-
24
- // IMPORTS
25
-
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.pruneCatalog = exports.pruneCheckpoint = exports.getCatalog = exports.catalogPage = exports.getAriaSnapshot = void 0;
26
12
  // Module to close and launch browsers.
27
- const {browserClose, launch} = require('./launch');
28
- const {shoot} = require('./shoot');
29
-
13
+ const launch_1 = require("./launch");
14
+ const shoot_1 = require("./shoot");
15
+ // Function to define window.getXPath on a live page.
16
+ const xPathScript_1 = require("./xPathScript");
17
+ // CONSTANTS
18
+ // The ARIA snapshot of a page, for checkpoints; empty if the browser cannot make one.
19
+ const getAriaSnapshot = async (page) => {
20
+ try {
21
+ return await page.locator('body').ariaSnapshot();
22
+ }
23
+ catch (error) {
24
+ console.log(`ERROR: ARIA snapshot failed (${error.message})`);
25
+ return '';
26
+ }
27
+ };
28
+ exports.getAriaSnapshot = getAriaSnapshot;
30
29
  // FUNCTIONS
31
-
32
- // Creates and returns a catalog.
33
- exports.getCatalog = async report => {
34
- const {browserID} = report;
35
- const targetURL = report.target?.url;
36
- // Image scale factor (report.imageScale). When greater than 1, the catalog context
37
- // runs at that deviceScaleFactor and a supplemental page image is captured at device
38
- // scale, i.e. with imageScale times the pixels of the CSS layout. Box IDs are
39
- // CSS-pixel regardless, because getBoundingClientRect reports CSS pixels by
40
- // definition; consumers map them onto the supplemental image by multiplying by
41
- // imageScale. Omitted, 1, or invalid values keep the behavior identical to before
42
- // this option existed.
43
- const imageScale = Number.isFinite(report.imageScale) && report.imageScale > 1
44
- ? report.imageScale
45
- : 1;
46
- // If the report specifies a global browser ID and a global target URL:
47
- if (browserID && targetURL) {
48
- // Launch a browser and visit the target, or abort the job on failure.
49
- const page = await launch({
50
- report,
51
- actIndex: null,
52
- tempBrowserID: browserID,
53
- tempURL: targetURL,
54
- contextOverrides: imageScale > 1 ? {deviceScaleFactor: imageScale} : {}
55
- });
56
- // If the launch and navigation succeeded:
57
- if (page) {
58
- // Expand closed details elements before the page image and the box measurements, so
59
- // both see the same fully disclosed state. Chromium lays out the content of a closed
60
- // details element (content-visibility: hidden) without painting it and without
61
- // shifting the content after it, so getBoundingClientRect returns coordinates that
62
- // overlap unrelated visible elements, making box IDs disagree with the page image.
63
- await page.evaluate(() => {
30
+ // Catalogs the elements of a live page and returns the snapshot. Expands closed details
31
+ // elements first, so box measurements agree with a page image taken in the same state; with
32
+ // restoreDetails, closes them again afterward so a live page keeps its state.
33
+ const catalogPage = async (page, report, { checkpoint, restoreDetails }) => {
34
+ // Ensure the page can compute XPaths (a page launched for acts lacks the script).
35
+ await (0, xPathScript_1.defineGetXPath)(page);
36
+ // Expand closed details elements before the page image and the box measurements, so
37
+ // both see the same fully disclosed state. Chromium lays out the content of a closed
38
+ // details element (content-visibility: hidden) without painting it and without
39
+ // shifting the content after it, so getBoundingClientRect returns coordinates that
40
+ // overlap unrelated visible elements, making box IDs disagree with the page image.
41
+ await page.evaluate(() => {
64
42
  document.querySelectorAll('details:not([open])').forEach(details => {
65
- details.setAttribute('open', '');
43
+ details.setAttribute('open', '');
44
+ details.setAttribute('data-testaro-opened', '');
66
45
  });
67
- }).catch(error => {
46
+ }).catch(error => {
68
47
  console.log(`ERROR: Expanding details elements failed (${error.message})`);
69
- });
70
- // If a page image is required:
71
- if ([0, 2, 4, 6].includes(report.imageColor)) {
72
- // Create one at CSS-pixel scale and add it to the report as images[0]. This
73
- // scale is invariant to imageScale and to the context's deviceScaleFactor, so
74
- // the testaro motion rule, which compares its own CSS-scale screenshot with
75
- // images[0], is unaffected by the imageScale option.
76
- console.log('Creating page image');
77
- await shoot(page, report, {
78
- exclusionSelector: '',
79
- colorType: report.imageColor,
80
- action: 'report'
81
- });
82
- // If a supersampled page image is also required:
83
- if (imageScale > 1) {
84
- // Create one at device-pixel scale and add it to the report as images[1].
85
- console.log(`Creating page image at ${imageScale}x device scale`);
86
- await shoot(page, report, {
87
- exclusionSelector: '',
88
- colorType: report.imageColor,
89
- action: 'report',
90
- scale: 'device'
91
- });
92
- }
93
- }
94
- // Get a catalog of the elements in the page and a map of path IDs to catalog indexes.
95
- console.log('Creating catalog');
96
- const {cat: catalog, pathIDs} = await page.evaluate(() => {
48
+ });
49
+ const startIndex = report.catalogNextIndex ?? 0;
50
+ // Get a catalog of the elements in the page and a map of path IDs to catalog indexes.
51
+ console.log(`Creating catalog for checkpoint ${checkpoint}`);
52
+ const { cat: entries, pathIDs, elementCount } = await page.evaluate(({ startIndex, checkpoint }) => {
53
+ // HTMLElement covers the innerText reads below; SVG-only members are guarded.
97
54
  const elements = Array.from(document.querySelectorAll('*'));
98
55
  // Initialize a catalog.
99
56
  const cat = {};
@@ -113,151 +70,286 @@ exports.getCatalog = async report => {
113
70
  // real elements. `index` is kept as a string so the catalog keys
114
71
  // (cat[index], texts[...].push(index)) are unchanged.
115
72
  for (let i = 0; i < elements.length; i++) {
116
- const index = String(i);
117
- const element = elements[i];
118
- // Get its ID and tag name.
119
- const {id, tagName} = element;
120
- // Get its start tag.
121
- const startTag = element.outerHTML?.replace(/^.*?</s, '<').replace(/>.*$/s, '>') ?? '';
122
- // Get whether it is eligible for text-fragment acquisition.
123
- const isTextable = element.closest('body')
124
- && ! element.closest('svg')
125
- && ! ['SCRIPT', 'STYLE', 'svg'].includes(element.tagName);
126
- const innerText = isTextable
127
- ? element.innerText.trim() || (element.parentElement?.innerText?.trim() ?? '')
128
- : '';
129
- let text = '';
130
- // If it is eligible and has an inner text:
131
- if (innerText) {
132
- const segments = innerText?.split('\n') ?? [];
133
- const tidySegments = segments.map(segment => segment.trim().replace(/\s+/g, ' '));
134
- const neededSegments = tidySegments.filter(segment => segment.length);
135
- neededSegments.splice(1, neededSegments.length - 2);
136
- // Get its text fragments.
137
- text = neededSegments.join('\n');
138
- // Add its index to the directory of text fragments.
139
- texts[text] ??= [];
140
- texts[text].push(index);
141
- }
142
- // Get its bounding box, but only if the element is painted. Chromium reports
143
- // plausible nonzero boxes for laid-out but unpainted content (visibility: hidden
144
- // and content-visibility: hidden subtrees), and such a box disagrees with the
145
- // page image, overlapping unrelated visible elements.
146
- const isVisible = typeof element.checkVisibility === 'function'
147
- ? element.checkVisibility({checkVisibilityCSS: true, visibilityProperty: true})
148
- : true;
149
- const domRect = isVisible ? element.getBoundingClientRect() : null;
150
- // Get its box ID.
151
- const boxID = domRect
152
- ? ['x', 'y', 'width', 'height'].map(key => Math.round(domRect[key])).join(':')
153
- : '';
154
- // Get its path ID.
155
- const pathID = window.getXPath(element) ?? '/html';
156
- // If it is a heading that nullifies an existing current heading index:
157
- if (
158
- headingIndex
159
- && ['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(tagName)
160
- && cat[headingIndex].tagName >= tagName
161
- ) {
162
- // Nullify the current heading index.
163
- headingIndex = '';
164
- }
165
- // Add an entry for it to the catalog.
166
- cat[index] = {
167
- tagName,
168
- id: id || '',
169
- startTag,
170
- text,
171
- textLinkable: false,
172
- boxID,
173
- pathID,
174
- headingIndex
175
- };
176
- // If the element is a heading:
177
- if (['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(tagName)) {
178
- // Assign its index to the current heading index.
179
- headingIndex = index;
180
- }
181
- // Add the path ID to the map of path IDs.
182
- pathIDs[pathID] = index;
73
+ const index = String(startIndex + i);
74
+ const element = elements[i];
75
+ // Get its ID and tag name.
76
+ const { id, tagName } = element;
77
+ // Get its start tag.
78
+ const startTag = element.outerHTML?.replace(/^.*?</s, '<').replace(/>.*$/s, '>') ?? '';
79
+ // Get whether it is eligible for text-fragment acquisition.
80
+ const isTextable = element.closest('body')
81
+ && !element.closest('svg')
82
+ && !['SCRIPT', 'STYLE', 'svg'].includes(element.tagName);
83
+ const innerText = isTextable
84
+ ? element.innerText.trim() || (element.parentElement?.innerText?.trim() ?? '')
85
+ : '';
86
+ let text = '';
87
+ // If it is eligible and has an inner text:
88
+ if (innerText) {
89
+ const segments = innerText?.split('\n') ?? [];
90
+ const tidySegments = segments.map(segment => segment.trim().replace(/\s+/g, ' '));
91
+ const neededSegments = tidySegments.filter(segment => segment.length);
92
+ neededSegments.splice(1, neededSegments.length - 2);
93
+ // Get its text fragments.
94
+ text = neededSegments.join('\n');
95
+ // Add its index to the directory of text fragments.
96
+ texts[text] ??= [];
97
+ texts[text].push(index);
98
+ }
99
+ // Get its bounding box, but only if the element is painted. Chromium reports
100
+ // plausible nonzero boxes for laid-out but unpainted content (visibility: hidden
101
+ // and content-visibility: hidden subtrees), and such a box disagrees with the
102
+ // page image, overlapping unrelated visible elements.
103
+ const isVisible = typeof element.checkVisibility === 'function'
104
+ ? element.checkVisibility({ checkVisibilityCSS: true, visibilityProperty: true })
105
+ : true;
106
+ const domRect = isVisible ? element.getBoundingClientRect() : null;
107
+ // Get its box ID.
108
+ const boxID = domRect
109
+ ? ['x', 'y', 'width', 'height'].map(key => Math.round(domRect[key])).join(':')
110
+ : '';
111
+ // Get its path ID.
112
+ const pathID = window.getXPath(element) ?? '/html';
113
+ // If it is a heading that nullifies an existing current heading index:
114
+ if (headingIndex
115
+ && ['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(tagName)
116
+ && cat[headingIndex].tagName >= tagName) {
117
+ // Nullify the current heading index.
118
+ headingIndex = '';
119
+ }
120
+ // Add an entry for it to the catalog.
121
+ cat[index] = {
122
+ tagName,
123
+ id: id || '',
124
+ startTag,
125
+ text,
126
+ textLinkable: false,
127
+ boxID,
128
+ pathID,
129
+ headingIndex,
130
+ checkpoint
131
+ };
132
+ // If the element is a heading:
133
+ if (['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(tagName)) {
134
+ // Assign its index to the current heading index.
135
+ headingIndex = index;
136
+ }
137
+ // Add the path ID to the map of path IDs.
138
+ pathIDs[pathID] = index;
183
139
  }
184
140
  // For each text in the catalog:
185
141
  Object.keys(texts).forEach(text => {
186
- const textElementIndexes = texts[text].sort((a, b) => Number(a) - Number(b));
187
- // If every element that has it is in the same subtree, so the text is page-unique:
188
- if (
189
- textElementIndexes.slice(0, -1).every(
190
- (elementIndex, index) => cat[textElementIndexes[index + 1]]
191
- .pathID
192
- .startsWith(cat[elementIndex].pathID)
193
- )
194
- ) {
195
- // For each element that has it:
196
- textElementIndexes.forEach(index => {
197
- // If it is not in the head, a script, a style, or a noscript element:
198
- if (
199
- ! ['/head[1]', '/script[', '/style[', '/noscript[']
200
- .some(excluder => cat[index].pathID.includes(excluder))
201
- ) {
202
- // Mark it as linkable in the element data in the catalog.
203
- cat[index].textLinkable = true;
204
- }
142
+ const textElementIndexes = texts[text].sort((a, b) => Number(a) - Number(b));
143
+ // If every element that has it is in the same subtree, so the text is page-unique:
144
+ if (textElementIndexes.slice(0, -1).every((elementIndex, index) => cat[textElementIndexes[index + 1]]
145
+ .pathID
146
+ .startsWith(cat[elementIndex].pathID))) {
147
+ // For each element that has it:
148
+ textElementIndexes.forEach(index => {
149
+ // If it is not in the head, a script, a style, or a noscript element:
150
+ if (!['/head[1]', '/script[', '/style[', '/noscript[']
151
+ .some(excluder => cat[index].pathID.includes(excluder))) {
152
+ // Mark it as linkable in the element data in the catalog.
153
+ cat[index].textLinkable = true;
154
+ }
155
+ });
156
+ }
157
+ });
158
+ return { cat, pathIDs, elementCount: elements.length };
159
+ }, { startIndex, checkpoint });
160
+ // If the page is to keep its state, close the details elements that were opened.
161
+ if (restoreDetails) {
162
+ await page.evaluate(() => {
163
+ document.querySelectorAll('details[data-testaro-opened]').forEach(details => {
164
+ details.removeAttribute('open');
165
+ details.removeAttribute('data-testaro-opened');
205
166
  });
206
- }
167
+ }).catch(() => { });
168
+ }
169
+ return {
170
+ entries,
171
+ pathIDs,
172
+ firstIndex: startIndex,
173
+ nextIndex: startIndex + elementCount,
174
+ elementCount
175
+ };
176
+ };
177
+ exports.catalogPage = catalogPage;
178
+ // Creates and returns a catalog.
179
+ const getCatalog = async (report) => {
180
+ const { browserID } = report;
181
+ const targetURL = report.target?.url;
182
+ // Image scale factor (report.imageScale). When greater than 1, the catalog context
183
+ // runs at that deviceScaleFactor and a supplemental page image is captured at device
184
+ // scale, i.e. with imageScale times the pixels of the CSS layout. Box IDs are
185
+ // CSS-pixel regardless, because getBoundingClientRect reports CSS pixels by
186
+ // definition; consumers map them onto the supplemental image by multiplying by
187
+ // imageScale. Omitted, 1, or invalid values keep the behavior identical to before
188
+ // this option existed.
189
+ const imageScale = Number.isFinite(report.imageScale) && report.imageScale > 1
190
+ ? report.imageScale
191
+ : 1;
192
+ // If the report specifies a global browser ID and a global target URL:
193
+ if (browserID && targetURL) {
194
+ // Launch a browser and visit the target, or abort the job on failure.
195
+ const page = await (0, launch_1.launch)({
196
+ report,
197
+ actIndex: null,
198
+ tempBrowserID: browserID,
199
+ tempURL: targetURL,
200
+ contextOverrides: imageScale > 1 ? { deviceScaleFactor: imageScale } : {}
207
201
  });
208
- return {cat, pathIDs};
209
- });
210
- // Add the map of path IDs to the report as a temporary job-time property.
211
- report.pathIDs = pathIDs;
212
- // Close the browser and its context.
213
- await browserClose(page);
214
- // Return the catalog.
215
- return catalog;
202
+ // If the launch and navigation succeeded:
203
+ if (page) {
204
+ const startTime = Date.now();
205
+ // If a page image is required:
206
+ if ([0, 2, 4, 6].includes(report.imageColor)) {
207
+ // Expand closed details elements first, so the image and the box measurements below
208
+ // see the same fully disclosed state (catalogPage expands them again, idempotently).
209
+ await page.evaluate(() => {
210
+ document.querySelectorAll('details:not([open])').forEach(details => {
211
+ details.setAttribute('open', '');
212
+ });
213
+ }).catch(error => {
214
+ console.log(`ERROR: Expanding details elements failed (${error.message})`);
215
+ });
216
+ // Create one at CSS-pixel scale and add it to the report as images[0]. This
217
+ // scale is invariant to imageScale and to the context's deviceScaleFactor, so
218
+ // the testaro motion rule, which compares its own CSS-scale screenshot with
219
+ // images[0], is unaffected by the imageScale option.
220
+ console.log('Creating page image');
221
+ await (0, shoot_1.shoot)(page, report, {
222
+ exclusionSelector: '',
223
+ colorType: report.imageColor,
224
+ action: 'report'
225
+ });
226
+ // If a supersampled page image is also required:
227
+ if (imageScale > 1) {
228
+ // Create one at device-pixel scale and add it to the report as images[1].
229
+ console.log(`Creating page image at ${imageScale}x device scale`);
230
+ await (0, shoot_1.shoot)(page, report, {
231
+ exclusionSelector: '',
232
+ colorType: report.imageColor,
233
+ action: 'report',
234
+ scale: 'device'
235
+ });
236
+ }
237
+ }
238
+ // Snapshot the page as checkpoint 0.
239
+ const snapshot = await (0, exports.catalogPage)(page, report, { checkpoint: 0, restoreDetails: false });
240
+ report.catalog = snapshot.entries;
241
+ report.pathIDs = { 0: snapshot.pathIDs };
242
+ report.catalogNextIndex = snapshot.nextIndex;
243
+ // Record the checkpoint.
244
+ const imageIndexes = (report.images ?? []).map((image, index) => index);
245
+ const checkpoint = {
246
+ index: 0,
247
+ name: 'start',
248
+ implicit: false,
249
+ actIndex: null,
250
+ launchActIndex: null,
251
+ launchURL: page.url(),
252
+ replay: [],
253
+ interaction: { modality: 'efficient' },
254
+ kind: 'navigation',
255
+ url: page.url(),
256
+ title: await page.title().catch(() => ''),
257
+ imageIndexes,
258
+ catalogRange: [snapshot.firstIndex, snapshot.nextIndex - 1],
259
+ elementCount: snapshot.elementCount,
260
+ ariaSnapshot: await (0, exports.getAriaSnapshot)(page),
261
+ elapsedMs: Date.now() - startTime,
262
+ testActs: []
263
+ };
264
+ report.checkpoints = [checkpoint];
265
+ const catalog = report.catalog;
266
+ // Close the browser and its context.
267
+ await (0, launch_1.browserClose)(page);
268
+ // Return the catalog.
269
+ return catalog;
270
+ }
271
+ // Otherwise, i.e. if the launch or navigation failed, report and return this.
272
+ console.log('ERROR: Launch or navigation failure prevented cataloguing and aborted job');
273
+ return {};
216
274
  }
217
- // Otherwise, i.e. if the launch or navigation failed, report and return this.
218
- console.log('ERROR: Launch or navigation failure prevented cataloguing and aborted job');
275
+ // Otherwise, i.e. if the report specification is incomplete, report and return this.
276
+ console.log('ERROR: Job omits browser ID or target URL, preventing catalog creation');
219
277
  return {};
220
- }
221
- // Otherwise, i.e. if the report specification is incomplete, report and return this.
222
- console.log('ERROR: Job omits browser ID or target URL, preventing catalog creation');
223
- return {};
224
278
  };
225
- // Prunes a catalog.
226
- exports.pruneCatalog = report => {
227
- console.log('Pruning catalog');
228
- const {acts, catalog} = report;
229
- const citedElementIndexes = new Set();
230
- // For each act in the report:
231
- acts.forEach(act => {
232
- // If it is a test with a standard result:
233
- if (act.type === 'test' && act.result?.standardResult) {
234
- const {instances} = act.result?.standardResult ?? [];
235
- // For each instance of the standard result:
236
- instances.forEach(instance => {
237
- const catalogIndex = instance?.catalogIndex;
238
- // If the instance has a catalog index (an index of '0' counts, so test for presence):
239
- if (catalogIndex !== undefined && catalogIndex !== '') {
240
- // Ensure the index is classified as cited, as a string so it matches the
241
- // catalog keys tested below (Object.keys() yields strings).
242
- citedElementIndexes.add(String(catalogIndex));
243
- const {headingIndex} = catalog[catalogIndex] ?? {};
244
- // If the catalog item has a heading index:
245
- if (headingIndex) {
246
- // Ensure it, too, is classified as cited.
247
- citedElementIndexes.add(String(headingIndex));
248
- }
279
+ exports.getCatalog = getCatalog;
280
+ // Returns the catalog indexes cited by the standard instances of the test acts (all of them,
281
+ // or those of one checkpoint), plus the heading indexes of the cited entries.
282
+ const getCitedIndexes = (report, checkpoint = null) => {
283
+ const { acts, catalog } = report;
284
+ const citedElementIndexes = new Set();
285
+ // For each act in the report:
286
+ acts.forEach(act => {
287
+ // If it is a test (of the checkpoint, if one is specified) with a standard result:
288
+ if (act.type === 'test' && act.result?.standardResult
289
+ && (checkpoint === null || act.checkpoint === checkpoint)) {
290
+ // The guard above makes the fallback unreachable and instances present; the
291
+ // expression is kept verbatim from the JavaScript original.
292
+ const { instances } = (act.result?.standardResult ?? []);
293
+ // For each instance of the standard result:
294
+ instances.forEach(instance => {
295
+ const catalogIndex = instance?.catalogIndex;
296
+ // If the instance has a catalog index (an index of '0' counts, so test for presence):
297
+ if (catalogIndex !== undefined && catalogIndex !== '') {
298
+ // Ensure the index is classified as cited, as a string so it matches the
299
+ // catalog keys tested below (Object.keys() yields strings).
300
+ citedElementIndexes.add(String(catalogIndex));
301
+ const { headingIndex } = catalog[catalogIndex] ?? {};
302
+ // If the catalog item has a heading index:
303
+ if (headingIndex) {
304
+ // Ensure it, too, is classified as cited.
305
+ citedElementIndexes.add(String(headingIndex));
306
+ }
307
+ }
308
+ });
249
309
  }
250
- });
251
- }
252
- });
253
- // Delete the temporary path ID map of the report.
254
- delete report.pathIDs;
255
- // For each element in the catalog:
256
- Object.keys(catalog).forEach(elementIndex => {
257
- // If it is not cited by any instance or by any cited element:
258
- if (! citedElementIndexes.has(elementIndex)) {
259
- // Delete it in the catalog.
260
- delete catalog[elementIndex];
310
+ });
311
+ return citedElementIndexes;
312
+ };
313
+ /*
314
+ Prunes the entries of one checkpoint that no test act of that checkpoint cites, and drops
315
+ its job-time XPath map. Called when the next checkpoint is created: test acts belong to the
316
+ latest checkpoint, so no later act can cite the earlier one, and pruning it then bounds the
317
+ temporary report that every test act reads. The structure diff with the next checkpoint is
318
+ computed before this runs (procs/checkpoint.js).
319
+ */
320
+ const pruneCheckpoint = (report, checkpoint) => {
321
+ const { catalog } = report;
322
+ const cited = getCitedIndexes(report, checkpoint);
323
+ let prunedCount = 0;
324
+ Object.keys(catalog).forEach(elementIndex => {
325
+ if (catalog[elementIndex].checkpoint === checkpoint && !cited.has(elementIndex)) {
326
+ delete catalog[elementIndex];
327
+ prunedCount++;
328
+ }
329
+ });
330
+ if (report.pathIDs) {
331
+ delete report.pathIDs[checkpoint];
261
332
  }
262
- });
333
+ return prunedCount;
334
+ };
335
+ exports.pruneCheckpoint = pruneCheckpoint;
336
+ // Prunes a catalog.
337
+ const pruneCatalog = (report) => {
338
+ console.log('Pruning catalog');
339
+ // The catalog always exists by pruning time (getCatalog ran at job start).
340
+ const { catalog } = report;
341
+ const citedElementIndexes = getCitedIndexes(report);
342
+ // Delete the temporary job-time properties of the report.
343
+ delete report.pathIDs;
344
+ delete report.catalogNextIndex;
345
+ delete report.activeCheckpoint;
346
+ // For each element in the catalog:
347
+ Object.keys(catalog).forEach(elementIndex => {
348
+ // If it is not cited by any instance or by any cited element:
349
+ if (!citedElementIndexes.has(elementIndex)) {
350
+ // Delete it in the catalog.
351
+ delete catalog[elementIndex];
352
+ }
353
+ });
263
354
  };
355
+ exports.pruneCatalog = pruneCatalog;