testaro 59.2.11 → 60.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/README.md +2 -1
- package/package.json +5 -2
- package/run.js +60 -15
- package/tests/testaro.js +7 -22
package/README.md
CHANGED
|
@@ -42,6 +42,7 @@ One software product that performs some such functions is [Testilo](https://www.
|
|
|
42
42
|
|
|
43
43
|
Testaro uses:
|
|
44
44
|
- [Playwright](https://playwright.dev/) to launch browsers, perform user actions in them, and perform tests
|
|
45
|
+
- [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 their requests more likely to succeed
|
|
45
46
|
- [playwright-dompath](https://www.npmjs.com/package/playwright-dompath) to retrieve XPaths of elements
|
|
46
47
|
- [pixelmatch](https://www.npmjs.com/package/pixelmatch) to measure motion
|
|
47
48
|
|
|
@@ -91,7 +92,7 @@ The main directories containing code files are:
|
|
|
91
92
|
|
|
92
93
|
## System requirements
|
|
93
94
|
|
|
94
|
-
|
|
95
|
+
The latest long-term-support version of [Node.js](https://nodejs.org/en/).
|
|
95
96
|
|
|
96
97
|
## Installation
|
|
97
98
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "testaro",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "60.0.0",
|
|
4
4
|
"description": "Run 1000 web accessibility tests from 11 tools and get a standardized report",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -41,7 +41,10 @@
|
|
|
41
41
|
"dotenv": "*",
|
|
42
42
|
"pixelmatch": "*",
|
|
43
43
|
"playwright": "*",
|
|
44
|
-
"playwright-dompath": "*"
|
|
44
|
+
"playwright-dompath": "*",
|
|
45
|
+
"playwright-extra": "*",
|
|
46
|
+
"playwright-extra-plugin-stealth": "*",
|
|
47
|
+
"puppeteer-extra-plugin-stealth": "*"
|
|
45
48
|
},
|
|
46
49
|
"devDependencies": {
|
|
47
50
|
"eslint": "*"
|
package/run.js
CHANGED
|
@@ -36,6 +36,13 @@ const fs = require('fs/promises');
|
|
|
36
36
|
require('dotenv').config({quiet: true});
|
|
37
37
|
// Module to validate jobs.
|
|
38
38
|
const {isBrowserID, isDeviceID, isURL, isValidJob, tools} = require('./procs/job');
|
|
39
|
+
// Module to evade automation detection.
|
|
40
|
+
const {chromium, webkit, firefox} = require('playwright-extra');
|
|
41
|
+
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
|
|
42
|
+
chromium.use(StealthPlugin());
|
|
43
|
+
webkit.use(StealthPlugin());
|
|
44
|
+
firefox.use(StealthPlugin());
|
|
45
|
+
const playwrightBrowsers = {chromium, webkit, firefox};
|
|
39
46
|
// Module to standardize report formats.
|
|
40
47
|
const {standardize} = require('./procs/standardize');
|
|
41
48
|
// Module to identify element bounding boxes.
|
|
@@ -92,6 +99,8 @@ const timeLimits = {
|
|
|
92
99
|
ibm: 30,
|
|
93
100
|
testaro: 150 + Math.round(6 * process.env.WAITS / 1000)
|
|
94
101
|
};
|
|
102
|
+
// Timeout multiplier.
|
|
103
|
+
const timeoutMultiplier = Number.parseFloat(process.env.TIMEOUT_MULTIPLIER) || 1;
|
|
95
104
|
|
|
96
105
|
// Temporary directory
|
|
97
106
|
const tmpDir = os.tmpdir();
|
|
@@ -134,6 +143,15 @@ const getNonce = async response => {
|
|
|
134
143
|
// Return the nonce, if any.
|
|
135
144
|
return nonce;
|
|
136
145
|
};
|
|
146
|
+
// Normalizes a file URL in case it has the Windows path format.
|
|
147
|
+
const normalizeFile = u => {
|
|
148
|
+
if (!u) return u;
|
|
149
|
+
if (!u.toLowerCase().startsWith('file:')) return u;
|
|
150
|
+
// Ensure forward slashes and three slashes after file:
|
|
151
|
+
let path = u.replace(/^file:\/+/i, '');
|
|
152
|
+
path = path.replace(/\\/g, '/');
|
|
153
|
+
return 'file:///' + path.replace(/^\//, '');
|
|
154
|
+
};
|
|
137
155
|
// Visits a URL and returns the response of the server.
|
|
138
156
|
const goTo = async (report, page, url, timeout, waitUntil) => {
|
|
139
157
|
// If the URL is a file path:
|
|
@@ -152,19 +170,10 @@ const goTo = async (report, page, url, timeout, waitUntil) => {
|
|
|
152
170
|
const httpStatus = response.status();
|
|
153
171
|
// If the response status was normal:
|
|
154
172
|
if ([200, 304].includes(httpStatus) || url.startsWith('file:')) {
|
|
155
|
-
// If the browser was redirected in violation of a strictness requirement:
|
|
156
173
|
const actualURL = page.url();
|
|
157
|
-
// Normalize file:// URLs for comparison (handles Windows path formats)
|
|
158
|
-
const normalizeFile = u => {
|
|
159
|
-
if (!u) return u;
|
|
160
|
-
if (!u.toLowerCase().startsWith('file:')) return u;
|
|
161
|
-
// Ensure forward slashes and three slashes after file:
|
|
162
|
-
let path = u.replace(/^file:\/+/i, '');
|
|
163
|
-
path = path.replace(/\\/g, '/');
|
|
164
|
-
return 'file:///' + path.replace(/^\//, '');
|
|
165
|
-
};
|
|
166
174
|
const actualNorm = actualURL.startsWith('file:') ? normalizeFile(actualURL) : actualURL;
|
|
167
175
|
const urlNorm = url.startsWith('file:') ? normalizeFile(url) : url;
|
|
176
|
+
// If the browser was redirected in violation of a strictness requirement:
|
|
168
177
|
if (report.strict && deSlash(actualNorm) !== deSlash(urlNorm)) {
|
|
169
178
|
// Return an error.
|
|
170
179
|
console.log(`ERROR: Visit to ${url} redirected to ${actualURL}`);
|
|
@@ -184,6 +193,15 @@ const goTo = async (report, page, url, timeout, waitUntil) => {
|
|
|
184
193
|
};
|
|
185
194
|
}
|
|
186
195
|
}
|
|
196
|
+
// Otherwise, if the response status was prohibition:
|
|
197
|
+
else if (httpStatus === 403) {
|
|
198
|
+
// Return this.
|
|
199
|
+
console.log(`ERROR: Visit to ${url} prohibited (status 403)`);
|
|
200
|
+
return {
|
|
201
|
+
success: false,
|
|
202
|
+
error: 'status403'
|
|
203
|
+
};
|
|
204
|
+
}
|
|
187
205
|
// Otherwise, if the response status was rejection of excessive requests:
|
|
188
206
|
else if (httpStatus === 429) {
|
|
189
207
|
// Return this.
|
|
@@ -273,7 +291,7 @@ const launch = exports.launch = async (
|
|
|
273
291
|
// Replace the report target URL with this URL.
|
|
274
292
|
report.target.url = url;
|
|
275
293
|
// Create a browser of the specified or default type.
|
|
276
|
-
const browserType =
|
|
294
|
+
const browserType = playwrightBrowsers[browserID];
|
|
277
295
|
// Close the current browser, if any.
|
|
278
296
|
await browserClose();
|
|
279
297
|
// Define browser options.
|
|
@@ -288,17 +306,44 @@ const launch = exports.launch = async (
|
|
|
288
306
|
},
|
|
289
307
|
headless: ! debug,
|
|
290
308
|
slowMo: waits || 0,
|
|
291
|
-
...(browserID === 'chromium' && {
|
|
309
|
+
...(browserID === 'chromium' && {
|
|
310
|
+
args: ['--disable-dev-shm-usage', '--disable-blink-features=AutomationControlled']
|
|
311
|
+
})
|
|
292
312
|
};
|
|
293
313
|
try {
|
|
294
314
|
// Replace the browser with a new one.
|
|
295
315
|
browser = await browserType.launch(browserOptions);
|
|
296
316
|
// Redefine the context (i.e. browser window).
|
|
297
|
-
|
|
317
|
+
const contextOptions = {
|
|
318
|
+
...device.windowOptions,
|
|
319
|
+
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
|
320
|
+
viewport: device.windowOptions.viewport || {width: 1920, height: 1080},
|
|
321
|
+
locale: 'en-US',
|
|
322
|
+
timezoneId: 'America/Los_Angeles',
|
|
323
|
+
extraHTTPHeaders: {
|
|
324
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
|
325
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
326
|
+
'Accept-Encoding': 'gzip, deflate, br',
|
|
327
|
+
'DNT': '1',
|
|
328
|
+
'Upgrade-Insecure-Requests': '1'
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
browserContext = await browser.newContext(contextOptions);
|
|
298
332
|
// Prevent default timeouts.
|
|
299
333
|
browserContext.setDefaultTimeout(0);
|
|
300
|
-
// When a page (i.e.
|
|
334
|
+
// When a page (i.e. tab) is added to the browser context (i.e. browser window):
|
|
301
335
|
browserContext.on('page', async page => {
|
|
336
|
+
// Mask automation detection
|
|
337
|
+
await page.addInitScript(() => {
|
|
338
|
+
Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
|
|
339
|
+
window.chrome = {runtime: {}};
|
|
340
|
+
Object.defineProperty(navigator, 'plugins', {
|
|
341
|
+
get: () => [1, 2, 3, 4, 5]
|
|
342
|
+
});
|
|
343
|
+
Object.defineProperty(navigator, 'languages', {
|
|
344
|
+
get: () => ['en-US', 'en']
|
|
345
|
+
});
|
|
346
|
+
});
|
|
302
347
|
// Ensure the report has a jobData property.
|
|
303
348
|
report.jobData ??= {};
|
|
304
349
|
const {jobData} = report;
|
|
@@ -721,7 +766,7 @@ const doActs = async (report, opts = {}) => {
|
|
|
721
766
|
const actResult = await new Promise(resolve => {
|
|
722
767
|
let closed = false;
|
|
723
768
|
const child = fork(
|
|
724
|
-
`${__dirname}/procs/doTestAct`, [actIndex], {timeout: 1000 * timeLimits[act.which] ||
|
|
769
|
+
`${__dirname}/procs/doTestAct`, [actIndex], {timeout: timeoutMultiplier * 1000 * (timeLimits[act.which] || 15)}
|
|
725
770
|
);
|
|
726
771
|
child.on('message', message => {
|
|
727
772
|
if (! closed) {
|
package/tests/testaro.js
CHANGED
|
@@ -40,25 +40,6 @@ const fs = require('fs/promises');
|
|
|
40
40
|
// CONSTANTS
|
|
41
41
|
|
|
42
42
|
// The validation job data for the tests listed below are in the pending directory.
|
|
43
|
-
/*
|
|
44
|
-
const futureEvalRulesTraining = {
|
|
45
|
-
altScheme: 'img elements with alt attributes having URLs as their entire values',
|
|
46
|
-
captionLoc: 'caption elements that are not first children of table elements',
|
|
47
|
-
datalistRef: 'elements with ambiguous or missing referenced datalist elements',
|
|
48
|
-
secHeading: 'headings that violate the logical level order in their sectioning containers',
|
|
49
|
-
textSem: 'semantically vague elements i, b, and/or small'
|
|
50
|
-
};
|
|
51
|
-
const futureEvalRulesCleanRoom = {
|
|
52
|
-
adbID: 'elements with ambiguous or missing referenced descriptions',
|
|
53
|
-
imageLink: 'links with image files as their destinations',
|
|
54
|
-
legendLoc: 'legend elements that are not first children of fieldset elements',
|
|
55
|
-
optRoleSel: 'Non-option elements with option roles that have no aria-selected attributes',
|
|
56
|
-
phOnly: 'input elements with placeholders but no accessible names'
|
|
57
|
-
};
|
|
58
|
-
*/
|
|
59
|
-
// The following were previously marked as future (clean-room) rules.
|
|
60
|
-
// For local validation runs they are included in evalRules below. Remove from futureRules
|
|
61
|
-
// when preparing clean-room submissions.
|
|
62
43
|
const futureRules = new Set([]);
|
|
63
44
|
const evalRules = {
|
|
64
45
|
adbID: 'elements with ambiguous or missing referenced descriptions',
|
|
@@ -152,6 +133,7 @@ const slowTestLimits = {
|
|
|
152
133
|
tabNav: 10,
|
|
153
134
|
textSem: 10
|
|
154
135
|
};
|
|
136
|
+
const timeoutMultiplier = Number.parseFloat(process.env.TIMEOUT_MULTIPLIER) || 1;
|
|
155
137
|
|
|
156
138
|
// ERROR HANDLER
|
|
157
139
|
process.on('unhandledRejection', reason => {
|
|
@@ -296,7 +278,7 @@ exports.reporter = async (page, report, actIndex) => {
|
|
|
296
278
|
while (testRetries > 0 && ! testSuccess) {
|
|
297
279
|
try {
|
|
298
280
|
// Apply a time limit to the test.
|
|
299
|
-
const timeLimit = 1000 * (slowTestLimits[rule] ?? 5);
|
|
281
|
+
const timeLimit = 1000 * timeoutMultiplier * (slowTestLimits[rule] ?? 5);
|
|
300
282
|
// If the time limit expires during the test:
|
|
301
283
|
const timer = new Promise(resolve => {
|
|
302
284
|
timeout = setTimeout(() => {
|
|
@@ -383,9 +365,12 @@ exports.reporter = async (page, report, actIndex) => {
|
|
|
383
365
|
break;
|
|
384
366
|
}
|
|
385
367
|
}
|
|
368
|
+
finally {
|
|
369
|
+
// Clear the timeout.
|
|
370
|
+
clearTimeout(timeout);
|
|
371
|
+
}
|
|
386
372
|
}
|
|
387
|
-
// Clear the
|
|
388
|
-
clearTimeout(timeout);
|
|
373
|
+
// Clear the error listeners.
|
|
389
374
|
if (page && ! page.isClosed() && crashHandler) {
|
|
390
375
|
page.off('crash', crashHandler);
|
|
391
376
|
}
|