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.
- package/.gitattributes +94 -0
- package/.github/workflows/ci.yml +58 -0
- package/.github/workflows/publish.yml +49 -0
- package/.github/workflows/typescript.yml +35 -0
- package/AGENTS.md +2 -2
- package/CLAUDE.md +24 -18
- package/CONTAINERS.md +9 -6
- package/CONTRIBUTING.md +29 -3
- package/Dockerfile +1 -1
- package/README.md +73 -19
- package/UPGRADES.md +4 -0
- package/actSpecs-doc.md +13 -1
- package/actSpecs.js +29 -5
- package/call.js +6 -5
- package/docker-compose.yml +8 -2
- package/docs/checkpoint-scanning.md +199 -0
- package/docs/standard-result-outcome.md +165 -0
- package/env.example +44 -10
- package/eslint.config.mjs +101 -0
- package/netWatch.js +372 -222
- package/package.json +46 -31
- package/pour/README.md +47 -0
- package/pour/pour.min.js +8 -0
- package/procs/actDo.js +808 -0
- package/procs/catalog.d.ts +17 -0
- package/procs/catalog.js +312 -221
- package/procs/catalog.ts +398 -0
- package/procs/checkpoint.js +110 -0
- package/procs/config.d.ts +35 -0
- package/procs/config.js +62 -0
- package/procs/dateTime.js +2 -1
- package/procs/doActs.js +442 -849
- package/procs/doTestAct.js +27 -121
- package/procs/flow.js +221 -0
- package/procs/generateRuleRegistry.js +94 -0
- package/procs/getSource.d.ts +26 -0
- package/procs/job.js +115 -1
- package/procs/launch.d.ts +40 -0
- package/procs/launch.js +239 -108
- package/procs/nu.d.ts +48 -0
- package/procs/scope.js +196 -0
- package/procs/shoot.d.ts +33 -0
- package/procs/standard.d.ts +18 -0
- package/procs/standard.js +83 -0
- package/procs/standard.ts +126 -0
- package/procs/testAct.js +145 -0
- package/procs/testaro.d.ts +20 -0
- package/procs/testaro.js +236 -189
- package/procs/testaro.ts +321 -0
- package/procs/userPath.js +126 -0
- package/procs/xPath.d.ts +4 -0
- package/procs/xPath.js +81 -69
- package/procs/xPath.ts +106 -0
- package/procs/xPathScript.d.ts +6 -0
- package/procs/xPathScript.js +70 -0
- package/run.js +3 -2
- package/surea11y/README.md +44 -0
- package/surea11y/surea11y.browser.js +14 -0
- package/testaro/adbID.d.ts +3 -0
- package/testaro/adbID.js +38 -41
- package/testaro/adbID.ts +66 -0
- package/testaro/allCapStyle.d.ts +3 -0
- package/testaro/allCapStyle.js +31 -34
- package/testaro/allCapStyle.ts +55 -0
- package/testaro/allCaps.d.ts +16 -0
- package/testaro/allCaps.js +179 -151
- package/testaro/allCaps.ts +228 -0
- package/testaro/allHidden.d.ts +13 -0
- package/testaro/allHidden.js +29 -30
- package/testaro/allHidden.ts +50 -0
- package/testaro/allSlanted.d.ts +3 -0
- package/testaro/allSlanted.js +30 -33
- package/testaro/allSlanted.ts +54 -0
- package/testaro/altScheme.d.ts +3 -0
- package/testaro/altScheme.js +27 -30
- package/testaro/altScheme.ts +50 -0
- package/testaro/attVal.d.ts +3 -0
- package/testaro/attVal.js +20 -21
- package/testaro/attVal.ts +52 -0
- package/testaro/autocomplete.d.ts +3 -0
- package/testaro/autocomplete.js +59 -75
- package/testaro/autocomplete.ts +101 -0
- package/testaro/bulk.d.ts +13 -0
- package/testaro/bulk.js +32 -33
- package/testaro/bulk.ts +55 -0
- package/testaro/buttonMenu.d.ts +9 -0
- package/testaro/buttonMenu.js +317 -319
- package/testaro/buttonMenu.ts +391 -0
- package/testaro/captionLoc.d.ts +3 -0
- package/testaro/captionLoc.js +17 -20
- package/testaro/captionLoc.ts +40 -0
- package/testaro/datalistRef.d.ts +3 -0
- package/testaro/datalistRef.js +33 -36
- package/testaro/datalistRef.ts +55 -0
- package/testaro/distortion.d.ts +3 -0
- package/testaro/distortion.js +57 -26
- package/testaro/distortion.ts +81 -0
- package/testaro/docType.d.ts +15 -0
- package/testaro/docType.js +25 -25
- package/testaro/docType.ts +45 -0
- package/testaro/dupAtt.d.ts +16 -0
- package/testaro/dupAtt.js +113 -104
- package/testaro/dupAtt.ts +144 -0
- package/testaro/elements.d.ts +6 -0
- package/testaro/elements.js +153 -153
- package/testaro/elements.ts +215 -0
- package/testaro/embAc.d.ts +3 -0
- package/testaro/embAc.js +19 -20
- package/testaro/embAc.ts +40 -0
- package/testaro/focAll.d.ts +13 -0
- package/testaro/focAll.js +185 -191
- package/testaro/focAll.ts +217 -0
- package/testaro/focAndOp.d.ts +3 -0
- package/testaro/focAndOp.js +100 -104
- package/testaro/focAndOp.ts +128 -0
- package/testaro/focInd.d.ts +3 -0
- package/testaro/focInd.js +64 -66
- package/testaro/focInd.ts +96 -0
- package/testaro/focVis.d.ts +3 -0
- package/testaro/focVis.js +28 -30
- package/testaro/focVis.ts +52 -0
- package/testaro/headEl.d.ts +10 -0
- package/testaro/headEl.js +61 -62
- package/testaro/headEl.ts +82 -0
- package/testaro/headingAmb.d.ts +3 -0
- package/testaro/headingAmb.js +54 -63
- package/testaro/headingAmb.ts +72 -0
- package/testaro/hovInd.d.ts +12 -0
- package/testaro/hovInd.js +163 -130
- package/testaro/hovInd.ts +199 -0
- package/testaro/hover.d.ts +3 -0
- package/testaro/hover.js +155 -126
- package/testaro/hover.ts +154 -0
- package/testaro/hr.d.ts +3 -0
- package/testaro/hr.js +14 -17
- package/testaro/hr.ts +36 -0
- package/testaro/imageLink.d.ts +3 -0
- package/testaro/imageLink.js +17 -20
- package/testaro/imageLink.ts +42 -0
- package/testaro/labClash.d.ts +3 -0
- package/testaro/labClash.js +31 -33
- package/testaro/labClash.ts +54 -0
- package/testaro/legendLoc.d.ts +3 -0
- package/testaro/legendLoc.js +17 -20
- package/testaro/legendLoc.ts +42 -0
- package/testaro/lineHeight.d.ts +3 -0
- package/testaro/lineHeight.js +43 -48
- package/testaro/lineHeight.ts +70 -0
- package/testaro/linkAmb.d.ts +7 -0
- package/testaro/linkAmb.js +79 -80
- package/testaro/linkAmb.ts +105 -0
- package/testaro/linkExt.d.ts +3 -0
- package/testaro/linkExt.js +13 -16
- package/testaro/linkExt.ts +35 -0
- package/testaro/linkOldAtt.d.ts +3 -0
- package/testaro/linkOldAtt.js +25 -28
- package/testaro/linkOldAtt.ts +48 -0
- package/testaro/linkTo.d.ts +3 -0
- package/testaro/linkTo.js +22 -22
- package/testaro/linkTo.ts +43 -0
- package/testaro/linkUl.d.ts +3 -0
- package/testaro/linkUl.js +30 -34
- package/testaro/linkUl.ts +54 -0
- package/testaro/miniText.d.ts +3 -0
- package/testaro/miniText.js +41 -44
- package/testaro/miniText.ts +68 -0
- package/testaro/motion.d.ts +10 -0
- package/testaro/motion.js +92 -96
- package/testaro/motion.ts +125 -0
- package/testaro/nonTable.d.ts +3 -0
- package/testaro/nonTable.js +39 -45
- package/testaro/nonTable.ts +66 -0
- package/testaro/optRoleSel.d.ts +3 -0
- package/testaro/optRoleSel.js +16 -19
- package/testaro/optRoleSel.ts +41 -0
- package/testaro/phOnly.d.ts +3 -0
- package/testaro/phOnly.js +18 -21
- package/testaro/phOnly.ts +43 -0
- package/testaro/pseudoP.d.ts +3 -0
- package/testaro/pseudoP.js +36 -38
- package/testaro/pseudoP.ts +59 -0
- package/testaro/radioSet.d.ts +3 -0
- package/testaro/radioSet.js +57 -59
- package/testaro/radioSet.ts +79 -0
- package/testaro/registry.d.ts +63 -0
- package/testaro/registry.js +67 -0
- package/testaro/registry.ts +141 -0
- package/testaro/role.d.ts +3 -0
- package/testaro/role.js +27 -29
- package/testaro/role.ts +53 -0
- package/testaro/secHeading.d.ts +3 -0
- package/testaro/secHeading.js +30 -33
- package/testaro/secHeading.ts +53 -0
- package/testaro/styleDiff.d.ts +25 -0
- package/testaro/styleDiff.js +248 -252
- package/testaro/styleDiff.ts +303 -0
- package/testaro/tabNav.d.ts +33 -0
- package/testaro/tabNav.js +272 -342
- package/testaro/tabNav.ts +454 -0
- package/testaro/targetsNear.d.ts +9 -0
- package/testaro/targetsNear.js +130 -132
- package/testaro/targetsNear.ts +160 -0
- package/testaro/textNodes.d.ts +6 -0
- package/testaro/textNodes.js +139 -135
- package/testaro/textNodes.ts +185 -0
- package/testaro/textSem.d.ts +3 -0
- package/testaro/textSem.js +25 -28
- package/testaro/textSem.ts +47 -0
- package/testaro/title.d.ts +9 -0
- package/testaro/title.js +16 -13
- package/testaro/title.ts +31 -0
- package/testaro/titledEl.d.ts +3 -0
- package/testaro/titledEl.js +15 -18
- package/testaro/titledEl.ts +38 -0
- package/testaro/zIndex.d.ts +3 -0
- package/testaro/zIndex.js +19 -22
- package/testaro/zIndex.ts +42 -0
- package/tests/alfa.d.ts +45 -0
- package/tests/alfa.js +138 -141
- package/tests/alfa.ts +214 -0
- package/tests/aslint.d.ts +33 -0
- package/tests/aslint.js +272 -249
- package/tests/aslint.ts +301 -0
- package/tests/axe.d.ts +27 -0
- package/tests/axe.js +199 -200
- package/tests/axe.ts +277 -0
- package/tests/ed11y.d.ts +28 -0
- package/tests/ed11y.js +141 -99
- package/tests/ed11y.ts +178 -0
- package/tests/htmlcs.d.ts +21 -0
- package/tests/htmlcs.js +174 -140
- package/tests/htmlcs.ts +181 -0
- package/tests/ibm.d.ts +52 -0
- package/tests/ibm.js +164 -166
- package/tests/ibm.ts +251 -0
- package/tests/nuVal.d.ts +13 -0
- package/tests/nuVal.js +107 -112
- package/tests/nuVal.ts +145 -0
- package/tests/nuVnu.d.ts +15 -0
- package/tests/nuVnu.js +141 -111
- package/tests/nuVnu.ts +144 -0
- package/tests/pour.d.ts +31 -0
- package/tests/pour.js +242 -0
- package/tests/pour.ts +273 -0
- package/tests/qualWeb.d.ts +39 -0
- package/tests/qualWeb.js +302 -272
- package/tests/qualWeb.ts +415 -0
- package/tests/surea11y.d.ts +33 -0
- package/tests/surea11y.js +288 -0
- package/tests/surea11y.ts +334 -0
- package/tests/testaro.d.ts +25 -0
- package/tests/testaro.js +745 -652
- package/tests/testaro.ts +862 -0
- package/tests/wave.d.ts +46 -0
- package/tests/wave.js +166 -177
- package/tests/wave.ts +252 -0
- package/tsconfig.json +17 -0
- package/types.d.ts +243 -0
- package/types.js +10 -0
- package/types.ts +376 -0
- package/validation/act/README.md +46 -0
- package/validation/act/capture.js +424 -0
- package/validation/act/chromium-issue-draft.md +66 -0
- package/validation/act/fp-triage-2026-08-22.md +86 -0
- package/validation/act/isolation-notes.md +159 -0
- package/validation/act/playwright-issue-draft.md +73 -0
- package/validation/act/propose-mappings.js +0 -0
- package/validation/act/repro-cdp-raw.js +105 -0
- package/validation/act/repro-metarefresh.js +65 -0
- package/validation/act/score.js +184 -0
- package/validation/act/stage3a-stress-report.md +69 -0
- package/validation/act/stage3b-mapping-proposals.md +94 -0
- package/validation/act/stage3b-triage-draft.md +138 -0
- package/validation/act/surea11y-track-a-2026-09-01.md +50 -0
- package/validation/executors/netWatch.js +180 -90
- package/validation/executors/test.js +17 -2
- package/validation/executors/tests.js +118 -10
- package/validation/jobs/reports/raw/260901T1000-surea11y-validation.json +964 -0
- package/validation/jobs/todo/240101T1200-simple-example.json +14 -6
- package/validation/jobs/todo/240101T1300-shoot-example.json +2 -1
- package/validation/jobs/todo/260821T1900-pour-validation.json +45 -0
- package/validation/jobs/todo/260901T1000-surea11y-validation.json +45 -0
- package/validation/knownFailures.json +1 -0
- package/validation/tests/jobProperties/adbID.json +27 -2
- package/validation/tests/jobProperties/{focOp.json → allCapStyle.json} +56 -53
- package/validation/tests/jobProperties/allCaps.json +25 -0
- package/validation/tests/jobProperties/allHidden.json +133 -13
- package/validation/tests/jobProperties/allSlanted.json +3 -3
- package/validation/tests/jobProperties/altScheme.json +23 -8
- package/validation/tests/jobProperties/attVal.json +57 -57
- package/validation/tests/jobProperties/autocomplete.json +11 -1
- package/validation/tests/jobProperties/bulk.json +6 -1
- package/validation/tests/jobProperties/buttonMenu.json +63 -42
- package/validation/tests/jobProperties/captionLoc.json +1 -6
- package/validation/tests/jobProperties/checkpoint-browser.json +401 -0
- package/validation/tests/jobProperties/checkpoint-page.json +397 -0
- package/validation/tests/jobProperties/checkpoint.json +395 -0
- package/validation/tests/jobProperties/datalistRef.json +20 -5
- package/validation/tests/jobProperties/distortion.json +28 -3
- package/validation/tests/jobProperties/docType.json +2 -2
- package/validation/tests/jobProperties/dupAtt.json +48 -33
- package/validation/tests/jobProperties/elements.json +28 -28
- package/validation/tests/jobProperties/embAc.json +36 -31
- package/validation/tests/jobProperties/focAndOp.json +284 -0
- package/validation/tests/jobProperties/focInd.json +34 -34
- package/validation/tests/jobProperties/focVis.json +1 -1
- package/validation/tests/jobProperties/hover.json +39 -37
- package/validation/tests/jobProperties/hr.json +13 -3
- package/validation/tests/jobProperties/imageLink.json +0 -5
- package/validation/tests/jobProperties/labClash.json +68 -33
- package/validation/tests/jobProperties/legendLoc.json +1 -6
- package/validation/tests/jobProperties/lineHeight.json +2 -2
- package/validation/tests/jobProperties/linkAmb.json +13 -18
- package/validation/tests/jobProperties/linkExt.json +1 -1
- package/validation/tests/jobProperties/linkOldAtt.json +12 -2
- package/validation/tests/jobProperties/linkTo.json +1 -1
- package/validation/tests/jobProperties/linkUl.json +65 -65
- package/validation/tests/jobProperties/miniText.json +7 -2
- package/validation/tests/jobProperties/motion.json +4 -50
- package/validation/tests/jobProperties/nonTable.json +48 -3
- package/validation/tests/jobProperties/optRoleSel.json +12 -2
- package/validation/tests/jobProperties/phOnly.json +6 -16
- package/validation/tests/jobProperties/pseudoP.json +17 -2
- package/validation/tests/jobProperties/radioSet.json +30 -30
- package/validation/tests/jobProperties/role.json +25 -5
- package/validation/tests/jobProperties/secHeading.json +26 -21
- package/validation/tests/jobProperties/styleDiff.json +35 -5
- package/validation/tests/jobProperties/tabNav.json +3 -1
- package/validation/tests/jobProperties/targetsNear.json +237 -0
- package/validation/tests/jobProperties/textNodes.json +29 -29
- package/validation/tests/jobProperties/textSem.json +26 -1
- package/validation/tests/jobProperties/title.json +12 -2
- package/validation/tests/jobProperties/titledEl.json +39 -9
- package/validation/tests/jobProperties/userPath.json +317 -0
- package/validation/tests/jobProperties/zIndex.json +40 -35
- package/validation/tests/targets/allCapStyle/index.html +26 -0
- package/validation/tests/targets/checkpoint/index.html +38 -0
- package/validation/tests/targets/datalistRef/index.html +1 -1
- package/validation/tests/targets/focAndOp/bad.html +29 -0
- package/validation/tests/targets/{focOp → focAndOp}/good.html +3 -1
- package/validation/tests/targets/focInd/bad.html +2 -1
- package/validation/tests/targets/headEl/index.html +10 -1
- package/validation/tests/targets/{targetSmall → targetsNear}/index.html +15 -1
- package/validation/tests/targets/userPath/index.html +42 -0
- package/validation/validateTest.js +61 -9
- package/.claude/settings.local.json +0 -11
- package/.eslintrc.json +0 -41
- package/htmlcs/.eslintrc.json +0 -67
- package/validation/tests/jobProperties/linkTitle.json +0 -127
- package/validation/tests/jobProperties/opFoc.json +0 -164
- package/validation/tests/jobProperties/targetSmall.json +0 -152
- package/validation/tests/jobProperties/targetTiny.json +0 -142
- package/validation/tests/targets/focOp/bad.html +0 -25
- package/validation/tests/targets/linkTitle/index.html +0 -24
- package/validation/tests/targets/opFoc/bad.html +0 -26
- package/validation/tests/targets/opFoc/good.html +0 -23
package/procs/catalog.ts
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/*
|
|
2
|
+
© 2026 Jeff Witt.
|
|
3
|
+
© 2025–2026 Jonathan Robert Pool.
|
|
4
|
+
|
|
5
|
+
Licensed under the MIT License. See LICENSE file at the project root or https://opensource.org/license/mit/ for details.
|
|
6
|
+
|
|
7
|
+
SPDX-License-Identifier: MIT
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// IMPORTS
|
|
11
|
+
|
|
12
|
+
import type {Page} from 'playwright';
|
|
13
|
+
// Module to close and launch browsers.
|
|
14
|
+
import {browserClose, launch} from './launch';
|
|
15
|
+
import {shoot} from './shoot';
|
|
16
|
+
// Function to define window.getXPath on a live page.
|
|
17
|
+
import {defineGetXPath} from './xPathScript';
|
|
18
|
+
import type {Catalog, CatalogEntry, Checkpoint, Report, StandardResult} from '../types';
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
catalog
|
|
22
|
+
Creates and returns a catalog of a target. Compiled to catalog.js by tsc
|
|
23
|
+
(issue #73); edit this file, not the emitted one.
|
|
24
|
+
|
|
25
|
+
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 9 properties:
|
|
26
|
+
- tagName
|
|
27
|
+
- id
|
|
28
|
+
- startTag
|
|
29
|
+
- text
|
|
30
|
+
- textLinkable
|
|
31
|
+
- boxID
|
|
32
|
+
- pathID
|
|
33
|
+
- headingIndex
|
|
34
|
+
- checkpoint: the index of the checkpoint (page state) whose snapshot the entry belongs to
|
|
35
|
+
|
|
36
|
+
The catalog of checkpoint 0 (the job target as launched) is made by getCatalog before any act. A checkpoint act adds the entries of a later page state with catalogPage, on the live page, keyed from report.catalogNextIndex so indexes never collide. report.pathIDs maps each checkpoint's XPaths to catalog indexes.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
// TYPES
|
|
40
|
+
|
|
41
|
+
// What catalogPage returns.
|
|
42
|
+
export interface CatalogSnapshot {
|
|
43
|
+
entries: Catalog;
|
|
44
|
+
pathIDs: Record<string, string>;
|
|
45
|
+
firstIndex: number;
|
|
46
|
+
nextIndex: number;
|
|
47
|
+
elementCount: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// CONSTANTS
|
|
51
|
+
|
|
52
|
+
// The ARIA snapshot of a page, for checkpoints; empty if the browser cannot make one.
|
|
53
|
+
export const getAriaSnapshot = async (page: Page): Promise<string> => {
|
|
54
|
+
try {
|
|
55
|
+
return await page.locator('body').ariaSnapshot();
|
|
56
|
+
}
|
|
57
|
+
catch(error) {
|
|
58
|
+
console.log(`ERROR: ARIA snapshot failed (${(error as Error).message})`);
|
|
59
|
+
return '';
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// FUNCTIONS
|
|
64
|
+
|
|
65
|
+
// Catalogs the elements of a live page and returns the snapshot. Expands closed details
|
|
66
|
+
// elements first, so box measurements agree with a page image taken in the same state; with
|
|
67
|
+
// restoreDetails, closes them again afterward so a live page keeps its state.
|
|
68
|
+
export const catalogPage = async (
|
|
69
|
+
page: Page, report: Report, {checkpoint, restoreDetails}: {checkpoint: number; restoreDetails: boolean}
|
|
70
|
+
): Promise<CatalogSnapshot> => {
|
|
71
|
+
// Ensure the page can compute XPaths (a page launched for acts lacks the script).
|
|
72
|
+
await defineGetXPath(page);
|
|
73
|
+
// Expand closed details elements before the page image and the box measurements, so
|
|
74
|
+
// both see the same fully disclosed state. Chromium lays out the content of a closed
|
|
75
|
+
// details element (content-visibility: hidden) without painting it and without
|
|
76
|
+
// shifting the content after it, so getBoundingClientRect returns coordinates that
|
|
77
|
+
// overlap unrelated visible elements, making box IDs disagree with the page image.
|
|
78
|
+
await page.evaluate(() => {
|
|
79
|
+
document.querySelectorAll('details:not([open])').forEach(details => {
|
|
80
|
+
details.setAttribute('open', '');
|
|
81
|
+
details.setAttribute('data-testaro-opened', '');
|
|
82
|
+
});
|
|
83
|
+
}).catch(error => {
|
|
84
|
+
console.log(`ERROR: Expanding details elements failed (${error.message})`);
|
|
85
|
+
});
|
|
86
|
+
const startIndex = report.catalogNextIndex ?? 0;
|
|
87
|
+
// Get a catalog of the elements in the page and a map of path IDs to catalog indexes.
|
|
88
|
+
console.log(`Creating catalog for checkpoint ${checkpoint}`);
|
|
89
|
+
const {cat: entries, pathIDs, elementCount} = await page.evaluate(({startIndex, checkpoint}) => {
|
|
90
|
+
// HTMLElement covers the innerText reads below; SVG-only members are guarded.
|
|
91
|
+
const elements = Array.from(document.querySelectorAll<HTMLElement>('*'));
|
|
92
|
+
// Initialize a catalog.
|
|
93
|
+
const cat: Record<string, CatalogEntry> = {};
|
|
94
|
+
// Initialize a map of path IDs to catalog indexes.
|
|
95
|
+
const pathIDs: Record<string, string> = {};
|
|
96
|
+
// Initialize a directory of text fragments.
|
|
97
|
+
const texts: Record<string, string[]> = {};
|
|
98
|
+
// Initialize the index of the current heading.
|
|
99
|
+
let headingIndex = '';
|
|
100
|
+
// For each element in the page:
|
|
101
|
+
// Iterate by numeric index rather than `for...in`. `for...in` over this
|
|
102
|
+
// array also enumerates any enumerable members added to Array.prototype
|
|
103
|
+
// by the TARGET page's own scripts (e.g. legacy MooTools/Prototype-style
|
|
104
|
+
// extensions); `element` then becomes that injected value and the
|
|
105
|
+
// element.closest(...) call below throws "closest is not a function",
|
|
106
|
+
// aborting the entire catalog and the job. The indexed loop sees only
|
|
107
|
+
// real elements. `index` is kept as a string so the catalog keys
|
|
108
|
+
// (cat[index], texts[...].push(index)) are unchanged.
|
|
109
|
+
for (let i = 0; i < elements.length; i++) {
|
|
110
|
+
const index = String(startIndex + i);
|
|
111
|
+
const element = elements[i];
|
|
112
|
+
// Get its ID and tag name.
|
|
113
|
+
const {id, tagName} = element;
|
|
114
|
+
// Get its start tag.
|
|
115
|
+
const startTag = element.outerHTML?.replace(/^.*?</s, '<').replace(/>.*$/s, '>') ?? '';
|
|
116
|
+
// Get whether it is eligible for text-fragment acquisition.
|
|
117
|
+
const isTextable = element.closest('body')
|
|
118
|
+
&& ! element.closest('svg')
|
|
119
|
+
&& ! ['SCRIPT', 'STYLE', 'svg'].includes(element.tagName);
|
|
120
|
+
const innerText = isTextable
|
|
121
|
+
? element.innerText.trim() || (element.parentElement?.innerText?.trim() ?? '')
|
|
122
|
+
: '';
|
|
123
|
+
let text = '';
|
|
124
|
+
// If it is eligible and has an inner text:
|
|
125
|
+
if (innerText) {
|
|
126
|
+
const segments = innerText?.split('\n') ?? [];
|
|
127
|
+
const tidySegments = segments.map(segment => segment.trim().replace(/\s+/g, ' '));
|
|
128
|
+
const neededSegments = tidySegments.filter(segment => segment.length);
|
|
129
|
+
neededSegments.splice(1, neededSegments.length - 2);
|
|
130
|
+
// Get its text fragments.
|
|
131
|
+
text = neededSegments.join('\n');
|
|
132
|
+
// Add its index to the directory of text fragments.
|
|
133
|
+
texts[text] ??= [];
|
|
134
|
+
texts[text].push(index);
|
|
135
|
+
}
|
|
136
|
+
// Get its bounding box, but only if the element is painted. Chromium reports
|
|
137
|
+
// plausible nonzero boxes for laid-out but unpainted content (visibility: hidden
|
|
138
|
+
// and content-visibility: hidden subtrees), and such a box disagrees with the
|
|
139
|
+
// page image, overlapping unrelated visible elements.
|
|
140
|
+
const isVisible = typeof element.checkVisibility === 'function'
|
|
141
|
+
? element.checkVisibility({checkVisibilityCSS: true, visibilityProperty: true})
|
|
142
|
+
: true;
|
|
143
|
+
const domRect = isVisible ? element.getBoundingClientRect() : null;
|
|
144
|
+
// Get its box ID.
|
|
145
|
+
const boxID = domRect
|
|
146
|
+
? (['x', 'y', 'width', 'height'] as const).map(key => Math.round(domRect[key])).join(':')
|
|
147
|
+
: '';
|
|
148
|
+
// Get its path ID.
|
|
149
|
+
const pathID = window.getXPath(element) ?? '/html';
|
|
150
|
+
// If it is a heading that nullifies an existing current heading index:
|
|
151
|
+
if (
|
|
152
|
+
headingIndex
|
|
153
|
+
&& ['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(tagName)
|
|
154
|
+
&& cat[headingIndex].tagName >= tagName
|
|
155
|
+
) {
|
|
156
|
+
// Nullify the current heading index.
|
|
157
|
+
headingIndex = '';
|
|
158
|
+
}
|
|
159
|
+
// Add an entry for it to the catalog.
|
|
160
|
+
cat[index] = {
|
|
161
|
+
tagName,
|
|
162
|
+
id: id || '',
|
|
163
|
+
startTag,
|
|
164
|
+
text,
|
|
165
|
+
textLinkable: false,
|
|
166
|
+
boxID,
|
|
167
|
+
pathID,
|
|
168
|
+
headingIndex,
|
|
169
|
+
checkpoint
|
|
170
|
+
};
|
|
171
|
+
// If the element is a heading:
|
|
172
|
+
if (['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(tagName)) {
|
|
173
|
+
// Assign its index to the current heading index.
|
|
174
|
+
headingIndex = index;
|
|
175
|
+
}
|
|
176
|
+
// Add the path ID to the map of path IDs.
|
|
177
|
+
pathIDs[pathID] = index;
|
|
178
|
+
}
|
|
179
|
+
// For each text in the catalog:
|
|
180
|
+
Object.keys(texts).forEach(text => {
|
|
181
|
+
const textElementIndexes = texts[text].sort((a, b) => Number(a) - Number(b));
|
|
182
|
+
// If every element that has it is in the same subtree, so the text is page-unique:
|
|
183
|
+
if (
|
|
184
|
+
textElementIndexes.slice(0, -1).every(
|
|
185
|
+
(elementIndex, index) => cat[textElementIndexes[index + 1]]
|
|
186
|
+
.pathID
|
|
187
|
+
.startsWith(cat[elementIndex].pathID)
|
|
188
|
+
)
|
|
189
|
+
) {
|
|
190
|
+
// For each element that has it:
|
|
191
|
+
textElementIndexes.forEach(index => {
|
|
192
|
+
// If it is not in the head, a script, a style, or a noscript element:
|
|
193
|
+
if (
|
|
194
|
+
! ['/head[1]', '/script[', '/style[', '/noscript[']
|
|
195
|
+
.some(excluder => cat[index].pathID.includes(excluder))
|
|
196
|
+
) {
|
|
197
|
+
// Mark it as linkable in the element data in the catalog.
|
|
198
|
+
cat[index].textLinkable = true;
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
return {cat, pathIDs, elementCount: elements.length};
|
|
204
|
+
}, {startIndex, checkpoint});
|
|
205
|
+
// If the page is to keep its state, close the details elements that were opened.
|
|
206
|
+
if (restoreDetails) {
|
|
207
|
+
await page.evaluate(() => {
|
|
208
|
+
document.querySelectorAll('details[data-testaro-opened]').forEach(details => {
|
|
209
|
+
details.removeAttribute('open');
|
|
210
|
+
details.removeAttribute('data-testaro-opened');
|
|
211
|
+
});
|
|
212
|
+
}).catch(() => {});
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
entries,
|
|
216
|
+
pathIDs,
|
|
217
|
+
firstIndex: startIndex,
|
|
218
|
+
nextIndex: startIndex + elementCount,
|
|
219
|
+
elementCount
|
|
220
|
+
};
|
|
221
|
+
};
|
|
222
|
+
// Creates and returns a catalog.
|
|
223
|
+
export const getCatalog = async (report: Report): Promise<Catalog> => {
|
|
224
|
+
const {browserID} = report;
|
|
225
|
+
const targetURL = report.target?.url;
|
|
226
|
+
// Image scale factor (report.imageScale). When greater than 1, the catalog context
|
|
227
|
+
// runs at that deviceScaleFactor and a supplemental page image is captured at device
|
|
228
|
+
// scale, i.e. with imageScale times the pixels of the CSS layout. Box IDs are
|
|
229
|
+
// CSS-pixel regardless, because getBoundingClientRect reports CSS pixels by
|
|
230
|
+
// definition; consumers map them onto the supplemental image by multiplying by
|
|
231
|
+
// imageScale. Omitted, 1, or invalid values keep the behavior identical to before
|
|
232
|
+
// this option existed.
|
|
233
|
+
const imageScale = Number.isFinite(report.imageScale) && (report.imageScale as number) > 1
|
|
234
|
+
? report.imageScale as number
|
|
235
|
+
: 1;
|
|
236
|
+
// If the report specifies a global browser ID and a global target URL:
|
|
237
|
+
if (browserID && targetURL) {
|
|
238
|
+
// Launch a browser and visit the target, or abort the job on failure.
|
|
239
|
+
const page = await launch({
|
|
240
|
+
report,
|
|
241
|
+
actIndex: null,
|
|
242
|
+
tempBrowserID: browserID,
|
|
243
|
+
tempURL: targetURL,
|
|
244
|
+
contextOverrides: imageScale > 1 ? {deviceScaleFactor: imageScale} : {}
|
|
245
|
+
});
|
|
246
|
+
// If the launch and navigation succeeded:
|
|
247
|
+
if (page) {
|
|
248
|
+
const startTime = Date.now();
|
|
249
|
+
// If a page image is required:
|
|
250
|
+
if ([0, 2, 4, 6].includes(report.imageColor as number)) {
|
|
251
|
+
// Expand closed details elements first, so the image and the box measurements below
|
|
252
|
+
// see the same fully disclosed state (catalogPage expands them again, idempotently).
|
|
253
|
+
await page.evaluate(() => {
|
|
254
|
+
document.querySelectorAll('details:not([open])').forEach(details => {
|
|
255
|
+
details.setAttribute('open', '');
|
|
256
|
+
});
|
|
257
|
+
}).catch(error => {
|
|
258
|
+
console.log(`ERROR: Expanding details elements failed (${error.message})`);
|
|
259
|
+
});
|
|
260
|
+
// Create one at CSS-pixel scale and add it to the report as images[0]. This
|
|
261
|
+
// scale is invariant to imageScale and to the context's deviceScaleFactor, so
|
|
262
|
+
// the testaro motion rule, which compares its own CSS-scale screenshot with
|
|
263
|
+
// images[0], is unaffected by the imageScale option.
|
|
264
|
+
console.log('Creating page image');
|
|
265
|
+
await shoot(page, report, {
|
|
266
|
+
exclusionSelector: '',
|
|
267
|
+
colorType: report.imageColor,
|
|
268
|
+
action: 'report'
|
|
269
|
+
});
|
|
270
|
+
// If a supersampled page image is also required:
|
|
271
|
+
if (imageScale > 1) {
|
|
272
|
+
// Create one at device-pixel scale and add it to the report as images[1].
|
|
273
|
+
console.log(`Creating page image at ${imageScale}x device scale`);
|
|
274
|
+
await shoot(page, report, {
|
|
275
|
+
exclusionSelector: '',
|
|
276
|
+
colorType: report.imageColor,
|
|
277
|
+
action: 'report',
|
|
278
|
+
scale: 'device'
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// Snapshot the page as checkpoint 0.
|
|
283
|
+
const snapshot = await catalogPage(page, report, {checkpoint: 0, restoreDetails: false});
|
|
284
|
+
report.catalog = snapshot.entries;
|
|
285
|
+
report.pathIDs = {0: snapshot.pathIDs};
|
|
286
|
+
report.catalogNextIndex = snapshot.nextIndex;
|
|
287
|
+
// Record the checkpoint.
|
|
288
|
+
const imageIndexes = (report.images ?? []).map((image, index) => index);
|
|
289
|
+
const checkpoint: Checkpoint = {
|
|
290
|
+
index: 0,
|
|
291
|
+
name: 'start',
|
|
292
|
+
implicit: false,
|
|
293
|
+
actIndex: null,
|
|
294
|
+
launchActIndex: null,
|
|
295
|
+
launchURL: page.url(),
|
|
296
|
+
replay: [],
|
|
297
|
+
interaction: {modality: 'efficient'},
|
|
298
|
+
kind: 'navigation',
|
|
299
|
+
url: page.url(),
|
|
300
|
+
title: await page.title().catch(() => ''),
|
|
301
|
+
imageIndexes,
|
|
302
|
+
catalogRange: [snapshot.firstIndex, snapshot.nextIndex - 1],
|
|
303
|
+
elementCount: snapshot.elementCount,
|
|
304
|
+
ariaSnapshot: await getAriaSnapshot(page),
|
|
305
|
+
elapsedMs: Date.now() - startTime,
|
|
306
|
+
testActs: []
|
|
307
|
+
};
|
|
308
|
+
report.checkpoints = [checkpoint];
|
|
309
|
+
const catalog = report.catalog;
|
|
310
|
+
// Close the browser and its context.
|
|
311
|
+
await browserClose(page);
|
|
312
|
+
// Return the catalog.
|
|
313
|
+
return catalog;
|
|
314
|
+
}
|
|
315
|
+
// Otherwise, i.e. if the launch or navigation failed, report and return this.
|
|
316
|
+
console.log('ERROR: Launch or navigation failure prevented cataloguing and aborted job');
|
|
317
|
+
return {};
|
|
318
|
+
}
|
|
319
|
+
// Otherwise, i.e. if the report specification is incomplete, report and return this.
|
|
320
|
+
console.log('ERROR: Job omits browser ID or target URL, preventing catalog creation');
|
|
321
|
+
return {};
|
|
322
|
+
};
|
|
323
|
+
// Returns the catalog indexes cited by the standard instances of the test acts (all of them,
|
|
324
|
+
// or those of one checkpoint), plus the heading indexes of the cited entries.
|
|
325
|
+
const getCitedIndexes = (report: Report, checkpoint: number | null = null): Set<string> => {
|
|
326
|
+
const {acts, catalog} = report as Report & {catalog: Catalog};
|
|
327
|
+
const citedElementIndexes = new Set<string>();
|
|
328
|
+
// For each act in the report:
|
|
329
|
+
acts.forEach(act => {
|
|
330
|
+
// If it is a test (of the checkpoint, if one is specified) with a standard result:
|
|
331
|
+
if (
|
|
332
|
+
act.type === 'test' && act.result?.standardResult
|
|
333
|
+
&& (checkpoint === null || act.checkpoint === checkpoint)
|
|
334
|
+
) {
|
|
335
|
+
// The guard above makes the fallback unreachable and instances present; the
|
|
336
|
+
// expression is kept verbatim from the JavaScript original.
|
|
337
|
+
const {instances} = (act.result?.standardResult ?? []) as Required<StandardResult>;
|
|
338
|
+
// For each instance of the standard result:
|
|
339
|
+
instances.forEach(instance => {
|
|
340
|
+
const catalogIndex = instance?.catalogIndex;
|
|
341
|
+
// If the instance has a catalog index (an index of '0' counts, so test for presence):
|
|
342
|
+
if (catalogIndex !== undefined && catalogIndex !== '') {
|
|
343
|
+
// Ensure the index is classified as cited, as a string so it matches the
|
|
344
|
+
// catalog keys tested below (Object.keys() yields strings).
|
|
345
|
+
citedElementIndexes.add(String(catalogIndex));
|
|
346
|
+
const {headingIndex} = catalog[catalogIndex] ?? {};
|
|
347
|
+
// If the catalog item has a heading index:
|
|
348
|
+
if (headingIndex) {
|
|
349
|
+
// Ensure it, too, is classified as cited.
|
|
350
|
+
citedElementIndexes.add(String(headingIndex));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
return citedElementIndexes;
|
|
357
|
+
};
|
|
358
|
+
/*
|
|
359
|
+
Prunes the entries of one checkpoint that no test act of that checkpoint cites, and drops
|
|
360
|
+
its job-time XPath map. Called when the next checkpoint is created: test acts belong to the
|
|
361
|
+
latest checkpoint, so no later act can cite the earlier one, and pruning it then bounds the
|
|
362
|
+
temporary report that every test act reads. The structure diff with the next checkpoint is
|
|
363
|
+
computed before this runs (procs/checkpoint.js).
|
|
364
|
+
*/
|
|
365
|
+
export const pruneCheckpoint = (report: Report, checkpoint: number): number => {
|
|
366
|
+
const {catalog} = report as Report & {catalog: Catalog};
|
|
367
|
+
const cited = getCitedIndexes(report, checkpoint);
|
|
368
|
+
let prunedCount = 0;
|
|
369
|
+
Object.keys(catalog).forEach(elementIndex => {
|
|
370
|
+
if (catalog[elementIndex].checkpoint === checkpoint && ! cited.has(elementIndex)) {
|
|
371
|
+
delete catalog[elementIndex];
|
|
372
|
+
prunedCount++;
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
if (report.pathIDs) {
|
|
376
|
+
delete report.pathIDs[checkpoint];
|
|
377
|
+
}
|
|
378
|
+
return prunedCount;
|
|
379
|
+
};
|
|
380
|
+
// Prunes a catalog.
|
|
381
|
+
export const pruneCatalog = (report: Report): void => {
|
|
382
|
+
console.log('Pruning catalog');
|
|
383
|
+
// The catalog always exists by pruning time (getCatalog ran at job start).
|
|
384
|
+
const {catalog} = report as Report & {catalog: Catalog};
|
|
385
|
+
const citedElementIndexes = getCitedIndexes(report);
|
|
386
|
+
// Delete the temporary job-time properties of the report.
|
|
387
|
+
delete report.pathIDs;
|
|
388
|
+
delete report.catalogNextIndex;
|
|
389
|
+
delete report.activeCheckpoint;
|
|
390
|
+
// For each element in the catalog:
|
|
391
|
+
Object.keys(catalog).forEach(elementIndex => {
|
|
392
|
+
// If it is not cited by any instance or by any cited element:
|
|
393
|
+
if (! citedElementIndexes.has(elementIndex)) {
|
|
394
|
+
// Delete it in the catalog.
|
|
395
|
+
delete catalog[elementIndex];
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/*
|
|
2
|
+
© 2026 Jeff Witt.
|
|
3
|
+
|
|
4
|
+
Licensed under the MIT License. See LICENSE file at the project root or
|
|
5
|
+
https://opensource.org/license/mit/ for details.
|
|
6
|
+
|
|
7
|
+
SPDX-License-Identifier: MIT
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
checkpoint.js
|
|
12
|
+
Creates checkpoints: snapshots of page states reached by a job's acts. See
|
|
13
|
+
docs/checkpoint-scanning.md.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// IMPORTS
|
|
17
|
+
|
|
18
|
+
const {createHash} = require('crypto');
|
|
19
|
+
const {catalogPage, getAriaSnapshot, pruneCheckpoint} = require('./catalog');
|
|
20
|
+
const {getStructureDiff} = require('./scope');
|
|
21
|
+
const {shoot} = require('./shoot');
|
|
22
|
+
|
|
23
|
+
// FUNCTIONS
|
|
24
|
+
|
|
25
|
+
// Returns a digest of the page's DOM, ignoring the attributes Testaro adds and whitespace, so
|
|
26
|
+
// that a replayed page can be compared with the snapshot it re-enacts.
|
|
27
|
+
const getDomDigest = exports.getDomDigest = async page => {
|
|
28
|
+
try {
|
|
29
|
+
const content = await page.content();
|
|
30
|
+
const normalized = content
|
|
31
|
+
.replace(/ data-xpath="[^"]*"/g, '')
|
|
32
|
+
.replace(/ data-testaro-opened=""/g, '')
|
|
33
|
+
.replace(/\s+/g, ' ');
|
|
34
|
+
return createHash('sha1').update(normalized).digest('hex');
|
|
35
|
+
}
|
|
36
|
+
catch(error) {
|
|
37
|
+
console.log(`ERROR: DOM digest failed (${error.message})`);
|
|
38
|
+
return '';
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
// Creates a checkpoint from the live page, adds it to the report, and returns it.
|
|
42
|
+
exports.makeCheckpoint = async ({
|
|
43
|
+
page, report, name, actIndex, implicit, launchActIndex, launchURL, replay, interaction
|
|
44
|
+
}) => {
|
|
45
|
+
const startTime = Date.now();
|
|
46
|
+
const index = report.checkpoints.length;
|
|
47
|
+
const imageIndexes = [];
|
|
48
|
+
// If page images are required:
|
|
49
|
+
if ([0, 2, 4, 6].includes(report.imageColor)) {
|
|
50
|
+
const imageScale = Number.isFinite(report.imageScale) && report.imageScale > 1
|
|
51
|
+
? report.imageScale
|
|
52
|
+
: 1;
|
|
53
|
+
// Create one at CSS scale, and one at device scale if the job asks for it.
|
|
54
|
+
const cssIndex = await shoot(page, report, {
|
|
55
|
+
exclusionSelector: '',
|
|
56
|
+
colorType: report.imageColor,
|
|
57
|
+
action: 'report'
|
|
58
|
+
});
|
|
59
|
+
if (typeof cssIndex === 'number') {
|
|
60
|
+
imageIndexes.push(cssIndex);
|
|
61
|
+
}
|
|
62
|
+
if (imageScale > 1) {
|
|
63
|
+
const deviceIndex = await shoot(page, report, {
|
|
64
|
+
exclusionSelector: '',
|
|
65
|
+
colorType: report.imageColor,
|
|
66
|
+
action: 'report',
|
|
67
|
+
scale: 'device'
|
|
68
|
+
});
|
|
69
|
+
if (typeof deviceIndex === 'number') {
|
|
70
|
+
imageIndexes.push(deviceIndex);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Catalog the page as this checkpoint, keeping the live page's state.
|
|
75
|
+
const snapshot = await catalogPage(page, report, {checkpoint: index, restoreDetails: true});
|
|
76
|
+
Object.assign(report.catalog, snapshot.entries);
|
|
77
|
+
report.pathIDs ??= {};
|
|
78
|
+
report.pathIDs[index] = snapshot.pathIDs;
|
|
79
|
+
report.catalogNextIndex = snapshot.nextIndex;
|
|
80
|
+
const checkpoint = {
|
|
81
|
+
index,
|
|
82
|
+
name,
|
|
83
|
+
implicit,
|
|
84
|
+
actIndex,
|
|
85
|
+
launchActIndex,
|
|
86
|
+
launchURL,
|
|
87
|
+
replay,
|
|
88
|
+
interaction,
|
|
89
|
+
kind: replay.length ? 'interaction' : 'navigation',
|
|
90
|
+
url: page.url(),
|
|
91
|
+
title: await page.title().catch(() => ''),
|
|
92
|
+
imageIndexes,
|
|
93
|
+
catalogRange: [snapshot.firstIndex, snapshot.nextIndex - 1],
|
|
94
|
+
elementCount: snapshot.elementCount,
|
|
95
|
+
ariaSnapshot: await getAriaSnapshot(page),
|
|
96
|
+
domDigest: await getDomDigest(page),
|
|
97
|
+
elapsedMs: Date.now() - startTime,
|
|
98
|
+
testActs: []
|
|
99
|
+
};
|
|
100
|
+
report.checkpoints.push(checkpoint);
|
|
101
|
+
// If there is a previous checkpoint, record what changed since it (job-time; report.flow
|
|
102
|
+
// takes it over at job end), then prune the previous checkpoint's uncited entries, which
|
|
103
|
+
// no later test act can cite.
|
|
104
|
+
if (index > 0) {
|
|
105
|
+
checkpoint.structure = getStructureDiff(report, index - 1, index);
|
|
106
|
+
const prunedCount = pruneCheckpoint(report, index - 1);
|
|
107
|
+
console.log(`Pruned ${prunedCount} uncited catalog entries of checkpoint ${index - 1}`);
|
|
108
|
+
}
|
|
109
|
+
return checkpoint;
|
|
110
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/*
|
|
2
|
+
© 2026 Jeff Witt.
|
|
3
|
+
|
|
4
|
+
Licensed under the MIT License. See LICENSE file at the project root or
|
|
5
|
+
https://opensource.org/license/mit/ for details.
|
|
6
|
+
|
|
7
|
+
SPDX-License-Identifier: MIT
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
config (hand-written declarations)
|
|
12
|
+
Minimal typings for the surface of procs/config.js that converted TypeScript
|
|
13
|
+
files use. Delete this file when config.js itself is converted (issue #73).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// Multiplies a base timeout by the configured timeout multiplier.
|
|
17
|
+
export function applyMultiplier(baseTimeout: number): number;
|
|
18
|
+
// Whether an environment variable is set to a true value, or the default if unset.
|
|
19
|
+
export function envFlag(name: string, defaultValue?: boolean): boolean;
|
|
20
|
+
// Load states a navigation can wait for, from strictest to loosest.
|
|
21
|
+
export const waitStates: string[];
|
|
22
|
+
// Navigation options of a job (its navigation property over the environment defaults).
|
|
23
|
+
export function getNavigation(report: unknown): {
|
|
24
|
+
waitUntil: 'networkidle' | 'load' | 'domcontentloaded';
|
|
25
|
+
timeout: number;
|
|
26
|
+
failFast4xx: boolean;
|
|
27
|
+
};
|
|
28
|
+
// The scanner identity header value of a job, or '' for none.
|
|
29
|
+
export function getScannerId(report: unknown): string;
|
|
30
|
+
// Whether a job scrolls the full page after navigation.
|
|
31
|
+
export function getScroll(report: unknown): boolean;
|
|
32
|
+
// Launch retries per rule of the testaro tool.
|
|
33
|
+
export const ruleLaunchRetries: number;
|
|
34
|
+
// Defaults for the qualWeb tool's browser.
|
|
35
|
+
export const qualWebDefaults: {stealth: boolean; adBlock: boolean};
|
package/procs/config.js
CHANGED
|
@@ -17,3 +17,65 @@
|
|
|
17
17
|
const timeoutMultiplier = Number.parseFloat(process.env.TIMEOUT_MULTIPLIER) || 1;
|
|
18
18
|
// Multiplies a time limit by the configured amount.
|
|
19
19
|
exports.applyMultiplier = (baseTimeout) => Math.round(baseTimeout * timeoutMultiplier);
|
|
20
|
+
|
|
21
|
+
// Returns whether an environment variable is set to a true value, or the default if unset.
|
|
22
|
+
const envFlag = (name, defaultValue = false) => {
|
|
23
|
+
const value = process.env[name];
|
|
24
|
+
if (value === undefined || value === '') {
|
|
25
|
+
return defaultValue;
|
|
26
|
+
}
|
|
27
|
+
return ['true', '1', 'yes', 'on'].includes(value.toLowerCase());
|
|
28
|
+
};
|
|
29
|
+
exports.envFlag = envFlag;
|
|
30
|
+
|
|
31
|
+
/*
|
|
32
|
+
Deployment options. Each is a job property when a job sets it, else an environment
|
|
33
|
+
variable, else the historical default, so an operator can set a fleet-wide policy and a job
|
|
34
|
+
can override it. See README.md (Jobs) and env.example.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
// Load states a navigation can wait for, from strictest to loosest.
|
|
38
|
+
const waitStates = exports.waitStates = ['networkidle', 'load', 'domcontentloaded'];
|
|
39
|
+
// Defaults for navigation: the load state to wait for (NAV_WAIT_UNTIL; networkidle, the
|
|
40
|
+
// historical default, never arrives on pages whose network never quiets, so a deployment may
|
|
41
|
+
// prefer load), the time limit in ms (NAV_TIMEOUT), and whether a 4xx response ends launch
|
|
42
|
+
// retries at once (NAV_FAIL_FAST_4XX; a client error is a definitive refusal that retrying
|
|
43
|
+
// and switching browsers cannot change).
|
|
44
|
+
const navTimeout = Number.parseInt(process.env.NAV_TIMEOUT);
|
|
45
|
+
const navigationDefaults = {
|
|
46
|
+
waitUntil: waitStates.includes(process.env.NAV_WAIT_UNTIL) ? process.env.NAV_WAIT_UNTIL : 'networkidle',
|
|
47
|
+
timeout: navTimeout > 0 ? navTimeout : 10000,
|
|
48
|
+
failFast4xx: envFlag('NAV_FAIL_FAST_4XX')
|
|
49
|
+
};
|
|
50
|
+
// Returns the navigation options of a job: its navigation property over the defaults.
|
|
51
|
+
exports.getNavigation = report => {
|
|
52
|
+
const own = report && report.navigation && typeof report.navigation === 'object'
|
|
53
|
+
? report.navigation
|
|
54
|
+
: {};
|
|
55
|
+
return {
|
|
56
|
+
waitUntil: waitStates.includes(own.waitUntil) ? own.waitUntil : navigationDefaults.waitUntil,
|
|
57
|
+
timeout: Number.isInteger(own.timeout) && own.timeout > 0 ? own.timeout : navigationDefaults.timeout,
|
|
58
|
+
failFast4xx: typeof own.failFast4xx === 'boolean' ? own.failFast4xx : navigationDefaults.failFast4xx
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
// The scanner identity sent as the X-YRA-Scanner request header, so that web application
|
|
62
|
+
// firewalls can recognize the scanner by a stable value (unlike the emulated User-Agent):
|
|
63
|
+
// the job's scannerId, else SCANNER_ID, else none.
|
|
64
|
+
exports.getScannerId = report => (report && report.scannerId) || process.env.SCANNER_ID || '';
|
|
65
|
+
// Whether to scroll the full height of a page after navigation, before cataloguing, imaging,
|
|
66
|
+
// and each tool, so that lazily loaded content is present: the job's scroll property, else
|
|
67
|
+
// PRESCAN_SCROLL, else false (the as-loaded page, comparable with the WebAIM Million).
|
|
68
|
+
exports.getScroll = report => (
|
|
69
|
+
report && typeof report.scroll === 'boolean' ? report.scroll : envFlag('PRESCAN_SCROLL')
|
|
70
|
+
);
|
|
71
|
+
// Launch retries per rule of the testaro tool, which relaunches the browser per contaminating
|
|
72
|
+
// rule: TESTARO_RULE_RETRIES, else 2. Non-Chromium browsers navigate flakily on relaunch, so
|
|
73
|
+
// a deployment testing them may raise this.
|
|
74
|
+
const ruleRetries = Number.parseInt(process.env.TESTARO_RULE_RETRIES);
|
|
75
|
+
exports.ruleLaunchRetries = ruleRetries >= 0 ? ruleRetries : 2;
|
|
76
|
+
// Defaults for the qualWeb tool's browser: stealth evasions (QUALWEB_STEALTH) and ad and
|
|
77
|
+
// tracker blocking (QUALWEB_ADBLOCK), both on by default as the tool was designed.
|
|
78
|
+
exports.qualWebDefaults = {
|
|
79
|
+
stealth: envFlag('QUALWEB_STEALTH', true),
|
|
80
|
+
adBlock: envFlag('QUALWEB_ADBLOCK', true)
|
|
81
|
+
};
|
package/procs/dateTime.js
CHANGED
|
@@ -28,7 +28,8 @@ exports.dateOf = timeStamp => {
|
|
|
28
28
|
const dateString = punctuate(timeStamp.slice(0, 6), '-', 2);
|
|
29
29
|
const timeString = punctuate(timeStamp.slice(7, 11), ':', 2);
|
|
30
30
|
return new Date(`20${dateString}T${timeString}Z`);
|
|
31
|
-
}
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
32
33
|
return null;
|
|
33
34
|
}
|
|
34
35
|
};
|