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 +17 -3
- package/package.json +2 -2
- package/procs/catalog.js +12 -0
- package/run.js +3 -1
- package/testaro/motion.js +54 -39
- package/tests/testaro.js +1 -20
- package/testaro/shoot0.js +0 -36
- package/testaro/shoot1.js +0 -41
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
|
-
- [
|
|
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: '
|
|
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
|
|
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.
|
|
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
|
-
//
|
|
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
|
|
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
|
|
19
|
-
const
|
|
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 (
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
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 (
|
|
47
|
-
const fromSize = `${width}×${height}`;
|
|
48
|
-
const toSize = `${
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
97
|
+
// Otherwise, i.e. if it failed:
|
|
83
98
|
else {
|
|
84
99
|
// Report this.
|
|
85
100
|
data.prevented = true;
|
|
86
|
-
data.error = '
|
|
101
|
+
data.error = 'Image creation failed';
|
|
87
102
|
}
|
|
88
103
|
}
|
|
89
|
-
//
|
|
90
|
-
|
|
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 =
|
|
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
|
|
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
|
-
};
|