testaro 78.0.8 → 78.4.0

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