testaro 75.0.0 → 75.1.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/README.md CHANGED
@@ -41,7 +41,7 @@ Testaro uses:
41
41
  - [Playwright](https://playwright.dev/) to launch browsers, perform user actions in them, and perform tests
42
42
  - [playwright-extra](https://www.npmjs.com/package/playwright-extra) and [puppeteer-extra-plugin-stealth](https://www.npmjs.com/package/puppeteer-extra-plugin-stealth) to make a Playwright-controlled browser more indistinguishable from a human-operated browser and thus make its requests more likely to succeed
43
43
  - [playwright-dompath](https://www.npmjs.com/package/playwright-dompath) to retrieve XPaths of elements
44
- - [BlazeDiff](https://blazediff.dev/) to measure motion
44
+ - [pixelmatch](https://www.npmjs.com/package/pixelmatch) to measure motion
45
45
  - [dotenv](https://www.npmjs.com/package/dotenv) to load environment variables
46
46
 
47
47
  Testaro can perform tests of these _tools_:
@@ -142,7 +142,8 @@ Here is a sample job, showing properties that you can set:
142
142
  id: 'healthcheck2611', // Job identifier
143
143
  what: 'monthly health check', // Job description
144
144
  strict: true, // Whether to reject redirections from the target URL
145
- standard: 'also', // or 'only' or 'no' (whether to report a standard result)
145
+ standard: 'only', // Report native (no), standard (only), or both (also) results
146
+ imageColor: 0, // Color type (0, 2, 4, 6) of the page image, if one is to be created along with a catalog
146
147
  device: { // Device to emulate
147
148
  id: 'iPhone 8',
148
149
  windowOptions: {
@@ -288,13 +289,18 @@ A report is a job with information about the results of the performance of the j
288
289
 
289
290
  ### Whole-job data
290
291
 
291
- As Testaro performs a job, information about the job as a whole is inserted into the job. That information is organized into one or two properties:
292
+ As Testaro performs a job, information about the job as a whole is inserted into the job. That information is organized into one, two, or three properties:
292
293
 
293
294
  - `jobData`: Facts about the performance of the job
294
295
  - `catalog`: A collection of data about the HTML elements of the target that are relevant to any test failures
296
+ - `images`: A collection of page images captured during the job
297
+
298
+ #### `jobData`
295
299
 
296
300
  Testaro inserts the `jobData` property into every job.
297
301
 
302
+ #### `catalog`
303
+
298
304
  Testaro inserts the `catalog` property only into jobs that instruct Testaro to produce standard results. The catalog is an inventory of HTML elements in the DOM of the target.
299
305
 
300
306
  The `catalog` property has an object value. Here is an example:
@@ -336,6 +342,12 @@ In some cases no catalog entry can be found. The reasons may include:
336
342
  - The element is inside a `noscript` element and therefore not considered an element in the DOM.
337
343
  - The violation is not ascribed to a single element.
338
344
 
345
+ #### `images`
346
+
347
+ Testaro inserts an `images` array property if necessary to store page images in the report. If the job has an `imageColor` property with `0`, `2`, `4`, or `6` as its value and Testaro will insert a `catalog` property, then Testaro also creates a page image with that color type and makes its base64-encoded PNG the first item in the `images` array.
348
+
349
+ There is a `shoot` act type that can be used to make additional page images during a job.
350
+
339
351
  ### Act data
340
352
 
341
353
  As Testaro performs the acts of a job, information about the result of each act is inserted into that act. For acts of type `test`, the added properties are:
@@ -542,6 +554,8 @@ Test targets employ mechanisms to prevent scraping, multiple requests within a s
542
554
 
543
555
  Some targets prohibit the execution of alien scripts unless the client can demonstrate that it is the requester of the page. Failure to provide that evidence results in the script being blocked and an error message being logged, saying “Refused to execute a script because its hash, its nonce, or unsafe-inline does not appear in the script-src directive of the Content Security Policy”. This mechanism affects tools that insert scripts into a target in order to test it. To comply with this requirement, Testaro obtains a _nonce_ from the response that serves the target. Then the file that runs the tool adds that nonce to the script as the value of a `nonce` attribute when it inserts its script into the target.
544
556
 
557
+ Some targets have been found erratically to prevent the creation of page images. When page images have been created, during the `motion` test in `testaro` some targets have been found to prevent their comparison by BlazeDiff, but comparison by `pixelmatch` has succeeded. For this reason, although reportedly slower, `pixelmatch` is the library used for image comparison.
558
+
545
559
  ### Tool duplicativity
546
560
 
547
561
  Tools sometimes do redundant testing, in that two or more tools test for the same defects, although such duplications are not necessarily perfect. This fact creates problems:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "75.0.0",
3
+ "version": "75.1.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": {
@@ -30,7 +30,6 @@
30
30
  },
31
31
  "homepage": "https://github.com/jrpool/testaro#readme",
32
32
  "dependencies": {
33
- "@blazediff/core": "*",
34
33
  "@qualweb/core": "*",
35
34
  "@qualweb/act-rules": "*",
36
35
  "@qualweb/wcag-techniques": "*",
@@ -43,6 +42,7 @@
43
42
  "aslint-testaro": "*",
44
43
  "axe-playwright": "*",
45
44
  "dotenv": "*",
45
+ "pixelmatch": "*",
46
46
  "playwright": "*",
47
47
  "playwright-dompath": "*",
48
48
  "playwright-extra": "*",
package/procs/catalog.js CHANGED
@@ -25,6 +25,7 @@
25
25
 
26
26
  // Module to close and launch browsers.
27
27
  const {browserClose, launch} = require('./launch');
28
+ const {shoot} = require('./shoot');
28
29
 
29
30
  // FUNCTIONS
30
31
 
@@ -43,7 +44,18 @@ exports.getCatalog = async report => {
43
44
  });
44
45
  // If the launch and navigation succeeded:
45
46
  if (page) {
47
+ // If a page image is required:
48
+ if ([0, 2, 4, 6].includes(report.imageColor)) {
49
+ // Create one and add it to the report.
50
+ console.log('Creating page image');
51
+ await shoot(page, report, {
52
+ exclusionSelector: '',
53
+ colorType: report.imageColor,
54
+ action: 'report'
55
+ });
56
+ }
46
57
  // Get a catalog of the elements in the page.
58
+ console.log('Creating catalog');
47
59
  const catalog = await page.evaluate(() => {
48
60
  const elements = Array.from(document.querySelectorAll('*'));
49
61
  // Initialize a catalog.
package/run.js CHANGED
@@ -126,7 +126,9 @@ exports.doJob = async (job, opts = {}) => {
126
126
  });
127
127
  // If the job specifies a browser ID and a target and requires standardization:
128
128
  if (job.browserID && job.target && job.standard !== 'no') {
129
- // Create a catalog of the target and add it to the report.
129
+ // Initialize a catalog so it precedes any page images in the report.
130
+ report.catalog = {};
131
+ // Add a catalog of the target, and a page image if required, to the report.
130
132
  report.catalog = await getCatalog(report);
131
133
  }
132
134
  // Perform the acts and revise the report.
package/testaro/motion.js CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  /*
7
7
  motion
8
- This test reports motion in a page by comparing the first and last of the screenshots previously made by the shoot0 and shoot1 tests.
8
+ This test reports motion in a page by making a page image and comparing it with the initial one, i.e. the one made by the catalog proc.
9
9
 
10
10
  For minimal accessibility, standards require motion to be brief, or else stoppable by the user. But stopping motion can be difficult or impossible, and, by the time a user manages to stop motion, the motion may have caused annoyance or harm. For superior accessibility, a page contains no motion until and unless the user authorizes it. The test reports a rule violation if any pixels differ between the screenshots. The larger the change fraction, the greater the ordinal severity.
11
11
 
@@ -15,54 +15,69 @@
15
15
  // IMPORTS
16
16
 
17
17
  const {getXPathCatalogIndex} = require('../procs/xPath');
18
- const fs = require('fs/promises');
19
- const blazediff = require('@blazediff/core').diff;
18
+ const {shoot} = require('../procs/shoot');
19
+ const pixelmatch = require('pixelmatch').default;
20
20
  const {PNG} = require('pngjs');
21
21
 
22
22
  // FUNCTIONS
23
23
 
24
24
  // Runs the test and returns the result.
25
- exports.reporter = async (_0, report, _1, _2, tmpDir) => {
25
+ exports.reporter = async (page, report) => {
26
26
  // Initialize the totals and standard instances.
27
27
  const data = {};
28
28
  const totals = [0, 0, 0, 0];
29
29
  const standardInstances = [];
30
- let violationWhat = '';
31
- let ordinalSeverity = 0;
32
- try {
33
- // Get the screenshot PNG buffers made by the shoot0 and shoot1 tests.
34
- let shoot0PNGBuffer = await fs.readFile(report.jobData.testaroShoot0);
35
- let shoot1PNGBuffer = await fs.readFile(report.jobData.testaroShoot1);
36
- // Delete the buffer files.
37
- await fs.unlink(report.jobData.testaroShoot0);
38
- await fs.unlink(report.jobData.testaroShoot1);
39
- // If both buffers exist:
40
- if (shoot0PNGBuffer && shoot1PNGBuffer) {
41
- // Parse them into PNG objects.
42
- let shoot0PNG = PNG.sync.read(shoot0PNGBuffer);
43
- let shoot1PNG = PNG.sync.read(shoot1PNGBuffer);
44
- const {width, height} = shoot0PNG;
30
+ // If the initial image exists:
31
+ if (report.images?.length) {
32
+ let violationWhat = '';
33
+ let ordinalSeverity = 0;
34
+ // Make an image with the same color type as the initial one and get its base64 encoding.
35
+ const png = await shoot(page, report, {
36
+ exclusionSelector: null,
37
+ colorType: report.imageColor,
38
+ action: 'return'
39
+ });
40
+ // If this succeeded:
41
+ if (png) {
42
+ // Parse both base64 encodings into PNG objects.
43
+ const initialPNG = PNG.sync.read(Buffer.from(report.images[0], 'base64'));
44
+ const finalPNG = PNG.sync.read(Buffer.from(png, 'base64'));
45
45
  // If their dimensions differ:
46
- if (shoot1PNG.width !== width || shoot1PNG.height !== height) {
47
- const fromSize = `${width}×${height}`;
48
- const toSize = `${shoot1PNG.width}×${shoot1PNG.height}`;
46
+ if (finalPNG.width !== initialPNG.width || finalPNG.height !== initialPNG.height) {
47
+ const fromSize = `${initialPNG.width}×${initialPNG.height}`;
48
+ const toSize = `${finalPNG.width}×${finalPNG.height}`;
49
49
  // Describe the violation.
50
50
  violationWhat = `Page size changes spontaneously (from ${fromSize} to ${toSize})`;
51
51
  }
52
52
  // Otherwise, i.e. if their dimensions are identical:
53
53
  else {
54
- // Get the count of differing pixels between the shots.
55
- const pixelChanges = blazediff(shoot0PNG.data, shoot1PNG.data, null, width, height);
56
- // Get the ratio of differing to all pixels as a percentage.
57
- const changePercent = Math.round(100 * pixelChanges / (width * height));
58
- // Free the memory used by screenshots.
59
- shoot0PNG = shoot1PNG = shoot0PNGBuffer = shoot1PNGBuffer = null;
60
- // If any pixels were changed:
61
- if (pixelChanges) {
62
- // Describe the violation.
63
- violationWhat = `Content changes spontaneously (${changePercent}% of pixels changed)`;
64
- // Get the ordinal severity from the fractional pixel change.
65
- ordinalSeverity = Math.floor(Math.min(3, 0.4 * Math.sqrt(changePercent)));
54
+ // Get the count of differing pixels between the images, using the default sensitivity.
55
+ try {
56
+ const pixelChanges = pixelmatch(
57
+ initialPNG.data,
58
+ finalPNG.data,
59
+ null,
60
+ initialPNG.width,
61
+ initialPNG.height,
62
+ {
63
+ threshold: 0.1
64
+ }
65
+ );
66
+ // Get the ratio of differing to all pixels as a percentage.
67
+ const changePercent = Math.round(
68
+ 100 * pixelChanges / (initialPNG.width * initialPNG.height)
69
+ );
70
+ // If any pixels were changed:
71
+ if (pixelChanges) {
72
+ // Describe the violation.
73
+ violationWhat = `Content changes spontaneously (${changePercent}% of pixels changed)`;
74
+ // Get the ordinal severity from the fractional pixel change.
75
+ ordinalSeverity = Math.floor(Math.min(3, 0.4 * Math.sqrt(changePercent)));
76
+ }
77
+ } catch (err) {
78
+ console.log(`pixelmatch error: ${err.message}, ${err.stack}`);
79
+ data.prevented = true;
80
+ data.error = `Pixel comparison failed: ${err.message}`;
66
81
  }
67
82
  }
68
83
  // If there was a violation:
@@ -79,18 +94,18 @@ exports.reporter = async (_0, report, _1, _2, tmpDir) => {
79
94
  });
80
95
  }
81
96
  }
82
- // Otherwise, i.e. if they do not both exist:
97
+ // Otherwise, i.e. if it failed:
83
98
  else {
84
99
  // Report this.
85
100
  data.prevented = true;
86
- data.error = 'At least 1 screenshot missing';
101
+ data.error = 'Image creation failed';
87
102
  }
88
103
  }
89
- // If getting or deleting either buffer file failed:
90
- catch(error) {
104
+ // Otherwise, i.e. if the initial image does not exist:
105
+ else {
91
106
  // Report this.
92
107
  data.prevented = true;
93
- data.error = `Screenshot file error (${error.message})`;
108
+ data.error = 'Initial image missing';
94
109
  }
95
110
  // Return the result.
96
111
  return {
package/tests/testaro.js CHANGED
@@ -23,15 +23,6 @@ const {launch} = require('../procs/launch');
23
23
 
24
24
  // Metadata of all rules in default execution order.
25
25
  const allRules = [
26
- {
27
- id: 'shoot0',
28
- what: 'first page screenshot',
29
- contaminates: false,
30
- needsAccessibleName: false,
31
- needsTmpDir: true,
32
- timeOut: 5,
33
- defaultOn: true
34
- },
35
26
  {
36
27
  id: 'adbID',
37
28
  what: 'elements with ambiguous or missing referenced descriptions',
@@ -328,21 +319,11 @@ const allRules = [
328
319
  timeOut: 5,
329
320
  defaultOn: true
330
321
  },
331
- {
332
- id: 'shoot1',
333
- what: 'second page screenshot',
334
- contaminates: false,
335
- needsAccessibleName: false,
336
- needsTmpDir: true,
337
- timeOut: 5,
338
- defaultOn: true
339
- },
340
322
  {
341
323
  id: 'motion',
342
- what: 'motion without user request, measured across tests',
324
+ what: 'motion without user request',
343
325
  contaminates: false,
344
326
  needsAccessibleName: false,
345
- needsTmpDir: true,
346
327
  timeOut: 5,
347
328
  defaultOn: true
348
329
  },
package/testaro/shoot0.js DELETED
@@ -1,36 +0,0 @@
1
- /*
2
- © 2025–2026 Jonathan Robert Pool.
3
- Licensed under the MIT License. See LICENSE file for details.
4
- */
5
-
6
- /*
7
- shoot0
8
- This test makes and saves the first of two screenshots.
9
- */
10
-
11
- // IMPORTS
12
-
13
- const {shoot} = require('../procs/shoot');
14
-
15
- // FUNCTIONS
16
-
17
- // Makes and saves the first screenshot.
18
- exports.reporter = async (page, report) => {
19
- // Make and save the screenshot.
20
- const pngPath = await shoot(page, report, {
21
- exclusionSelector: '',
22
- colorType: 0,
23
- action: 'file'
24
- });
25
- // If this succeeded:
26
- if (pngPath) {
27
- // Add the file path to the report.
28
- report.jobData.testaroShoot0 = pngPath;
29
- }
30
- // Return whether the screenshot was prevented.
31
- return {
32
- data: {
33
- prevented: ! pngPath
34
- }
35
- };
36
- };
package/testaro/shoot1.js DELETED
@@ -1,41 +0,0 @@
1
- /*
2
- © 2025–2026 Jonathan Robert Pool.
3
- Licensed under the MIT License. See LICENSE file for details.
4
- */
5
-
6
- /*
7
- shoot1
8
- This test makes and saves the second of two screenshots. It aborts if the first screenshot was prevented.
9
- */
10
-
11
- // IMPORTS
12
-
13
- const fs = require('fs/promises');
14
- const {shoot} = require('../procs/shoot');
15
-
16
- // FUNCTIONS
17
-
18
- // Make and save the second screenshot.
19
- exports.reporter = async (page, report) => {
20
- let pngPath = '';
21
- // If there is a shoot0 file:
22
- if (report.jobData.testaroShoot0) {
23
- // Make and save the screenshot.
24
- pngPath = await shoot(page, report, {
25
- exclusionSelector: null,
26
- colorType: 0,
27
- action: 'file'
28
- });
29
- // If this succeeded:
30
- if (pngPath) {
31
- // Add the file path to the report.
32
- report.jobData.testaroShoot1 = pngPath;
33
- }
34
- }
35
- // Return whether the screenshot was prevented.
36
- return {
37
- data: {
38
- prevented: ! pngPath
39
- }
40
- };
41
- };