testaro 75.2.2 → 76.0.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/.dockerignore ADDED
@@ -0,0 +1,12 @@
1
+ .git
2
+ node_modules
3
+ temp
4
+ tmp
5
+ watch
6
+ .env
7
+ .npmrc
8
+ .DS_Store
9
+ .vscode
10
+ validation/tests/old
11
+ Dockerfile
12
+ docker-compose.yml
package/AGENTS.md CHANGED
@@ -25,6 +25,11 @@
25
25
  - **Error handling**: Try-catch blocks, report failures via `prevented: true` in results
26
26
  - **Comments**: Explain complex logic, but keep concise
27
27
 
28
+ ## Assistant Mode Behavior
29
+
30
+ - The user deliberately selects Code, Ask, or Plan mode for each session; do not suggest or invite switching modes (e.g., “switch to Code mode so I can…”).
31
+ - In Ask mode, propose exact changes/commands for the user to apply without prompting a mode change.
32
+
28
33
  ## License
29
34
 
30
35
  © 2025–2026 Jonathan Robert Pool.
package/CONTAINERS.md ADDED
@@ -0,0 +1,88 @@
1
+ # Containerized deployment
2
+
3
+ ## Introduction
4
+
5
+ This document describes how to deploy Testaro in a container, using the reference `Dockerfile` and `docker-compose.yml` at the project root. It accompanies the evaluation requested in [issue 32](https://github.com/jrpool/testaro/issues/32).
6
+
7
+ Containerization is practical. The reference image runs every Testaro tool:
8
+
9
+ - The image is based on the official Playwright image (`mcr.microsoft.com/playwright`), which contains the Chromium, Firefox, and WebKit browsers and their system libraries, plus standard fonts, which keep page rendering consistent across hosts.
10
+ - QualWeb’s own (Puppeteer) Chromium is downloaded at image build time, pinned by `package-lock.json`.
11
+ - A headless Java runtime is installed for the `nuVnu` test, so `vnu-jar` does not download one over the network on first use.
12
+ - The `nuVal` test is pure HTTP; the reference Compose stack pairs Testaro with a self-hosted Nu Html Checker sidecar (`ghcr.io/validator/validator`) via the `TESTARO_NU_URL` environment variable, so it does not depend on the public W3C service (which is rate-limited and rejects large documents).
13
+ - The `wave` test calls an external API and needs only a `WAVE_KEY`.
14
+
15
+ ## Version coupling
16
+
17
+ The tag of the base image in the `Dockerfile` (`mcr.microsoft.com/playwright:v1.60.0-noble`) must match the version of the `playwright` package in `package-lock.json`, so that the browsers baked into the image are the ones the installed Playwright expects. When upgrading Playwright, change both together.
18
+
19
+ ## The Chromium sandbox
20
+
21
+ Chromium’s sandbox requires unprivileged user-namespace cloning, which the default Docker seccomp policy blocks (and which some hardened hosts prohibit even outside containers). There are two ways to run:
22
+
23
+ 1. **Without the sandbox** (the image default): the `TESTARO_CHROMIUM_NO_SANDBOX=true` environment variable makes Testaro launch Playwright’s Chromium with `chromiumSandbox: false` and QualWeb’s Chromium with `--no-sandbox`. This is the usual choice when the container itself is the isolation boundary and only untrusted web content is being visited.
24
+ 2. **With the sandbox**: set `TESTARO_CHROMIUM_NO_SANDBOX=false` and run the container with a seccomp profile that permits user-namespace cloning, such as [the profile published by Playwright](https://github.com/microsoft/playwright/blob/main/utils/docker/seccomp_profile.json):
25
+
26
+ ```bash
27
+ docker run --security-opt seccomp=seccomp_profile.json …
28
+ ```
29
+
30
+ WebKit and Firefox are unaffected either way.
31
+
32
+ ## Quick start with Compose
33
+
34
+ ```bash
35
+ mkdir -p watch/jobs/todo watch/jobs/done watch/reports/raw
36
+ chmod -R a+w watch
37
+ docker compose up --build
38
+ ```
39
+
40
+ This builds the image, starts the Nu Html Checker sidecar, and starts Testaro watching `watch/jobs/todo` (checking every 300 seconds, per the default command `node call dirWatch true 300`). Drop a job file into `watch/jobs/todo` on the host; the report appears in `watch/reports/raw`, and the job file moves to `watch/jobs/done`.
41
+
42
+ The container runs as the unprivileged `pwuser` from the Playwright base image, so the mounted host directories must be writable by that user (the `chmod` above, or `chown` to the matching UID).
43
+
44
+ ## One-shot job
45
+
46
+ To perform a single job and exit:
47
+
48
+ ```bash
49
+ docker compose run --rm testaro node call run
50
+ ```
51
+
52
+ Or without Compose (no Nu sidecar; `nuVal` then uses the public W3C service):
53
+
54
+ ```bash
55
+ docker build -t testaro .
56
+ docker run --rm --init \
57
+ -v "$PWD/watch/jobs:/home/pwuser/testaro/watch/jobs" \
58
+ -v "$PWD/watch/reports:/home/pwuser/testaro/watch/reports" \
59
+ testaro node call run
60
+ ```
61
+
62
+ ## Server polling
63
+
64
+ To poll a server for jobs instead of watching a directory, override the command and provide the `netWatch` variables:
65
+
66
+ ```bash
67
+ docker run --rm --init \
68
+ -e AGENT=agent1 \
69
+ -e NETWATCH_URL_JOB=https://example.com/api/testaro-agent/job \
70
+ -e NETWATCH_URL_REPORT=https://example.com/api/testaro-agent/report \
71
+ -e NETWATCH_URL_AUTH=password \
72
+ testaro node call netWatch true 300 true
73
+ ```
74
+
75
+ ## Operational notes
76
+
77
+ - **Init process**: Testaro forks a child process per test act, and some tools relaunch browsers repeatedly, so zombie reaping matters more than for typical browser-automation applications. Always run with an init process (`docker run --init`; in Compose, `init: true`).
78
+ - **Memory**: allow headroom for browser relaunch spikes; multiple tools’ browsers can briefly coexist with the catalog pass. The reference Compose stack allows 4 GB.
79
+ - **`/dev/shm`**: Testaro launches Chromium with `--disable-dev-shm-usage`, so the small default `/dev/shm` of containers is not a problem and no `--shm-size` option is needed.
80
+ - **Environment variables**: all variables in `env.example` work as usual; pass them with `-e`/`--env-file` or the Compose `environment` key rather than baking an `.env` file into the image.
81
+
82
+ ## License
83
+
84
+ © 2026 Jeff Witt.
85
+
86
+ Licensed under the [MIT License](https://opensource.org/license/mit/). See [LICENSE](LICENSE) file at the project root for details.
87
+
88
+ SPDX-License-Identifier: MIT
package/Dockerfile ADDED
@@ -0,0 +1,53 @@
1
+ # © 2026 Jeff Witt.
2
+ #
3
+ # Licensed under the MIT License. See LICENSE file at the project root or
4
+ # https://opensource.org/license/mit/ for details.
5
+ #
6
+ # SPDX-License-Identifier: MIT
7
+
8
+ # Reference container image for Testaro. See CONTAINERS.md for usage.
9
+ #
10
+ # The base-image tag must match the version of the playwright package in
11
+ # package-lock.json, so that the browsers baked into the image are the ones
12
+ # the installed Playwright expects.
13
+ FROM mcr.microsoft.com/playwright:v1.60.0-noble
14
+
15
+ # Install a headless Java runtime for the nuVnu test (vnu-jar); baking it in
16
+ # prevents vnu-jar from downloading a Temurin runtime over the network on
17
+ # first use. Also install unzip, which Puppeteer's browser downloader needs
18
+ # during `npm ci` and which the base image lacks.
19
+ RUN apt-get update \
20
+ && apt-get install -y --no-install-recommends openjdk-17-jre-headless unzip \
21
+ && rm -rf /var/lib/apt/lists/*
22
+
23
+ # Run as the unprivileged user provided by the Playwright base image.
24
+ USER pwuser
25
+ WORKDIR /home/pwuser/testaro
26
+
27
+ # Install the dependencies first, for build-layer caching. QualWeb's
28
+ # Puppeteer downloads its own Chromium during this step (into
29
+ # ~/.cache/puppeteer), so that browser is also baked into the image, pinned
30
+ # by package-lock.json. Playwright's browsers are already in the base image.
31
+ COPY --chown=pwuser:pwuser package.json package-lock.json ./
32
+ RUN npm ci
33
+
34
+ # Copy the application.
35
+ COPY --chown=pwuser:pwuser . .
36
+
37
+ # Directories for jobs and reports when watching a directory; mount volumes
38
+ # here (see CONTAINERS.md and docker-compose.yml).
39
+ ENV JOBDIR=/home/pwuser/testaro/watch/jobs \
40
+ REPORTDIR=/home/pwuser/testaro/watch/reports
41
+ RUN mkdir -p watch/jobs/todo watch/jobs/done watch/reports/raw
42
+
43
+ # Chromium's sandbox requires unprivileged user-namespace cloning, which the
44
+ # default Docker seccomp policy blocks. This default disables the sandbox
45
+ # (for Playwright's Chromium and QualWeb's). To keep the sandbox instead,
46
+ # run the container with a seccomp profile that permits user-namespace
47
+ # cloning and set this variable to false. See CONTAINERS.md.
48
+ ENV TESTARO_CHROMIUM_NO_SANDBOX=true
49
+
50
+ # Testaro forks a child process per test act, and some tools relaunch
51
+ # browsers repeatedly, so zombie reaping matters: run the container with an
52
+ # init process (`docker run --init`; in Compose, `init: true`).
53
+ CMD ["node", "call", "dirWatch", "true", "300"]
package/README.md CHANGED
@@ -575,7 +575,7 @@ The behavior of Testaro as a dependency of an application deployed on a virtual
575
575
 
576
576
  ### Containerized deployment
577
577
 
578
- Experimental deployments of Testaro as a dependency in a containerized application has sometimes resulted in thrown errors that are not thrown when the same application is deployed without containerization.
578
+ A reference container image for stand-alone deployment, in which all tools run, is defined by the `Dockerfile` and `docker-compose.yml` files at the project root and documented in [CONTAINERS.md](CONTAINERS.md).
579
579
 
580
580
  ### Headless browser fidelity
581
581
 
package/call.js CHANGED
@@ -53,13 +53,14 @@ const callRun = async jobIDStart => {
53
53
  // Get it.
54
54
  const jobJSON = await fs.readFile(`${todoDir}/${jobFileName}`, 'utf8');
55
55
  let report = JSON.parse(jobJSON);
56
+ const {id} = report;
56
57
  // Run the job.
57
58
  report = await doJob(report);
58
59
  // Archive it.
59
- await fs.rename(`${todoDir}/${jobFileName}`, `${jobDir}/done/${jobFileName}`);
60
+ await fs.rename(`${todoDir}/${jobFileName}`, `${jobDir}/done/${id}.json`);
60
61
  // Save the report.
61
- await fs.writeFile(`${rawDir}/${jobFileName}`, JSON.stringify(report, null, 2));
62
- console.log(`Job completed and report ${report.id}.json saved in ${rawDir}`);
62
+ await fs.writeFile(`${rawDir}/${id}.json`, `${JSON.stringify(report, null, 2)}\n`);
63
+ console.log(`Job completed and report ${id} saved in ${rawDir}`);
63
64
  }
64
65
  // Otherwise, i.e. if the job does not exist.
65
66
  else {
package/dirWatch.js CHANGED
@@ -52,7 +52,7 @@ const writeDirReport = async report => {
52
52
  }
53
53
  };
54
54
  // Archives a job.
55
- const archiveJob = async (job, isFile) => {
55
+ const archiveJob = async (job, todoFileName) => {
56
56
  // Save the job in the done subdirectory.
57
57
  const {id} = job;
58
58
  const jobJSON = JSON.stringify(job, null, 2);
@@ -60,11 +60,11 @@ const archiveJob = async (job, isFile) => {
60
60
  await fs.mkdir(doneDir, {recursive: true});
61
61
  await fs.writeFile(`${doneDir}/${id}.json`, `${jobJSON}\n`);
62
62
  // If the job had been saved as a file in the todo subdirectory:
63
- if (isFile) {
63
+ if (todoFileName) {
64
64
  // Delete the file.
65
- await fs.rm(`${jobDir}/todo/${id}.json`);
65
+ await fs.rm(`${jobDir}/todo/${todoFileName}`);
66
66
  }
67
- console.log(`Job ${id} archived in ${doneDir} (${nowString()})`);
67
+ console.log(`Job ${id} from ${todoFileName} archived in ${doneDir} (${nowString()})`);
68
68
  };
69
69
  // Waits.
70
70
  const wait = ms => {
@@ -92,11 +92,12 @@ exports.dirWatch = async (isForever, intervalInSeconds) => {
92
92
  const toDoFileNames = await fs.readdir(`${jobDir}/todo`);
93
93
  const jobFileNames = toDoFileNames.filter(fileName => fileName.endsWith('.json'));
94
94
  if (jobFileNames.length) {
95
+ const jobFileName = jobFileNames[0];
95
96
  // If the first one is ready to do:
96
- const firstJobTimeStamp = jobFileNames[0].replace(/-.+$/, '');
97
+ const firstJobTimeStamp = jobFileName.replace(/-.+$/, '');
97
98
  if (Date.now() > dateOf(firstJobTimeStamp)) {
98
99
  // Get it.
99
- const jobJSON = await fs.readFile(`${jobDir}/todo/${jobFileNames[0]}`, 'utf8');
100
+ const jobJSON = await fs.readFile(`${jobDir}/todo/${jobFileName}`, 'utf8');
100
101
  try {
101
102
  const job = JSON.parse(jobJSON);
102
103
  let report = JSON.parse(jobJSON);
@@ -108,7 +109,7 @@ exports.dirWatch = async (isForever, intervalInSeconds) => {
108
109
  // Save the report.
109
110
  await writeDirReport(report);
110
111
  // Archive the job.
111
- await archiveJob(job, true);
112
+ await archiveJob(job, jobFileName);
112
113
  }
113
114
  catch(error) {
114
115
  console.log(`ERROR processing directory job (${error.message})`);
@@ -1,7 +1,11 @@
1
1
  var computeAccessibleName = (() => {
2
2
  var __getOwnPropNames = Object.getOwnPropertyNames;
3
3
  var __commonJS = (cb, mod) => function __require() {
4
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
4
+ try {
5
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
6
+ } catch (e) {
7
+ throw mod = 0, e;
8
+ }
5
9
  };
6
10
 
7
11
  // node_modules/dom-accessibility-api/dist/polyfills/array.from.js
@@ -0,0 +1,49 @@
1
+ # © 2026 Jeff Witt.
2
+ #
3
+ # Licensed under the MIT License. See LICENSE file at the project root or
4
+ # https://opensource.org/license/mit/ for details.
5
+ #
6
+ # SPDX-License-Identifier: MIT
7
+
8
+ # Reference Compose stack for Testaro: the Testaro image watching a job
9
+ # directory, plus a self-hosted Nu Html Checker as a sidecar so the nuVal
10
+ # test does not depend on the public W3C service (which is rate-limited and
11
+ # rejects large documents). See CONTAINERS.md for usage.
12
+ services:
13
+ testaro:
14
+ build: .
15
+ # Reap zombie processes from forked test acts and browser relaunches.
16
+ init: true
17
+ environment:
18
+ # Disable the Chromium sandbox (default seccomp blocks the user
19
+ # namespaces it needs). Alternative: set this to "false" and run with
20
+ # a seccomp profile that permits user-namespace cloning, e.g.
21
+ # security_opt:
22
+ # - seccomp:seccomp_profile.json
23
+ # using the profile published by Playwright:
24
+ # https://github.com/microsoft/playwright/blob/main/utils/docker/seccomp_profile.json
25
+ TESTARO_CHROMIUM_NO_SANDBOX: "true"
26
+ # Send nuVal requests to the sidecar instead of the W3C service.
27
+ TESTARO_NU_URL: "http://nu:8888/?parser=html&out=json"
28
+ # JODBIR and REPORTDIR from Dockerfile override those from .env.
29
+ # Import other specific environment variables from .env file.
30
+ AGENT: ${AGENT}
31
+ WAVE_KEY: ${WAVE_KEY}
32
+ ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
33
+ volumes:
34
+ # Host directories for jobs and reports. The container runs as the
35
+ # unprivileged pwuser; the host directories must be writable by it
36
+ # (see CONTAINERS.md).
37
+ - ./watch/jobs:/home/pwuser/testaro/watch/jobs
38
+ - ./watch/reports:/home/pwuser/testaro/watch/reports
39
+ depends_on:
40
+ - nu
41
+ # Headroom for browser relaunch spikes: multiple tools' browsers can
42
+ # briefly coexist with the catalog pass.
43
+ mem_limit: 4g
44
+
45
+ nu:
46
+ image: ghcr.io/validator/validator:latest
47
+ # Uncomment to also reach the checker from the host:
48
+ # ports:
49
+ # - "8888:8888"
package/env.example CHANGED
@@ -31,6 +31,11 @@ TIMEOUT_MULTIPLIER=1
31
31
  ABORT_ASSERTIVELY=false
32
32
  # URL of any non-default (e.g., self-hosted) Nu Html Checker API (see ghcr.io/validator/validator).
33
33
  TESTARO_NU_URL=__placeholder__
34
+ # Whether to launch Chromium (Playwright's and QualWeb's) without its sandbox (normally false).
35
+ # The sandbox requires unprivileged user-namespace cloning, which default container
36
+ # seccomp policies and some hardened hosts prohibit. Set this to true in containers,
37
+ # or run the container with a seccomp profile that permits user-namespace cloning.
38
+ TESTARO_CHROMIUM_NO_SANDBOX=false
34
39
 
35
40
  ## License
36
41
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "75.2.2",
3
+ "version": "76.0.0",
4
4
  "description": "Run 1300 web accessibility tests from 10 tools and get a standardized report",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -55,5 +55,16 @@
55
55
  "dom-accessibility-api": "*",
56
56
  "esbuild": "*",
57
57
  "eslint": "*"
58
+ },
59
+ "overrides": {
60
+ "adm-zip": "^0.6.0"
61
+ },
62
+ "allowScripts": {
63
+ "accessibility-checker": true,
64
+ "puppeteer": true,
65
+ "chromedriver": true,
66
+ "esbuild": true,
67
+ "fsevents": true,
68
+ "vnu-jar": true
58
69
  }
59
70
  }
package/procs/launch.js CHANGED
@@ -18,6 +18,14 @@
18
18
 
19
19
  const {addError} = require('./error');
20
20
  const headedBrowser = process.env.HEADED_BROWSER === 'true';
21
+ // Whether to launch Chromium without its sandbox. The sandbox requires
22
+ // unprivileged user-namespace cloning, which the default container seccomp
23
+ // policies and some hardened hosts prohibit. Setting
24
+ // TESTARO_CHROMIUM_NO_SANDBOX=true permits Chromium to run in such
25
+ // environments; the alternative is to run the container with a seccomp
26
+ // profile that permits user-namespace cloning. Applies only to Chromium;
27
+ // WebKit and Firefox have no equivalent option.
28
+ const chromiumNoSandbox = process.env.TESTARO_CHROMIUM_NO_SANDBOX === 'true';
21
29
  // Two flavors of Playwright:
22
30
  // - `playwrightCore`: the upstream Playwright SDK with no plugins attached.
23
31
  // - `playwrightExtra`: the playwright-extra wrapper. `run.js` registers
@@ -320,6 +328,11 @@ const launchOnce = async opts => {
320
328
  slowMo: waits || 0,
321
329
  args: browserOptionArgs
322
330
  };
331
+ // If launching Chromium without its sandbox was specified:
332
+ if (browserID === 'chromium' && chromiumNoSandbox) {
333
+ // Disable the sandbox.
334
+ browserOptions.chromiumSandbox = false;
335
+ }
323
336
  let browser, browserContext;
324
337
  try {
325
338
  // Create a browser of the specified type.
package/tests/qualWeb.js CHANGED
@@ -72,11 +72,19 @@ exports.reporter = async (page, report, actIndex, timeLimit) => {
72
72
  instances: []
73
73
  };
74
74
  }
75
+ // Specify the options for the Puppeteer browser launched by QualWeb.
76
+ const puppeteerOptions = {
77
+ headless: true
78
+ };
79
+ // If launching Chromium without its sandbox was specified (e.g., in a
80
+ // container or on a host that restricts unprivileged user namespaces):
81
+ if (process.env.TESTARO_CHROMIUM_NO_SANDBOX === 'true') {
82
+ // Disable the sandbox of the QualWeb browser, too.
83
+ puppeteerOptions.args = ['--no-sandbox'];
84
+ }
75
85
  try {
76
86
  // Start the QualWeb core engine.
77
- await qualWeb.start(clusterOptions, {
78
- headless: true
79
- });
87
+ await qualWeb.start(clusterOptions, puppeteerOptions);
80
88
  }
81
89
  // If the start fails:
82
90
  catch(error) {
@@ -1,29 +0,0 @@
1
- {
2
- "id": "240101T1200-simple-example",
3
- "what": "Test example.edu with bulk",
4
- "strict": true,
5
- "timeLimit": 10,
6
- "acts": [
7
- {
8
- "type": "launch",
9
- "which": "chromium",
10
- "url": "https://example.edu/",
11
- "what": "Chromium browser"
12
- },
13
- {
14
- "type": "test",
15
- "which": "testaro",
16
- "withItems": false,
17
- "stopOnFail": true,
18
- "rules": [
19
- "y",
20
- "bulk"
21
- ]
22
- }
23
- ],
24
- "sources": {},
25
- "standard": "only",
26
- "timeStamp": "240101T1500",
27
- "creationTimeStamp": "240101T1200",
28
- "sendReportTo": ""
29
- }
@@ -1,12 +0,0 @@
1
- # done
2
-
3
- Directory watched by executor `watchDir`.
4
-
5
- ## License
6
-
7
- © 2023 CVS Health and/or one of its affiliates. All rights reserved.
8
-
9
- Licensed under the [MIT License](https://opensource.org/license/mit/). See [LICENSE](../../LICENSE) file
10
- at the project root for details.
11
-
12
- SPDX-License-Identifier: MIT
@@ -1,12 +0,0 @@
1
- # todo
2
-
3
- Directory watched by executor `watchDir`.
4
-
5
- ## License
6
-
7
- © 2023 CVS Health and/or one of its affiliates. All rights reserved.
8
-
9
- Licensed under the [MIT License](https://opensource.org/license/mit/). See [LICENSE](../../LICENSE) file
10
- at the project root for details.
11
-
12
- SPDX-License-Identifier: MIT