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 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
- Version 16 or later of [Node.js](https://nodejs.org/en/).
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": "59.2.11",
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 = require('playwright')[browserID];
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' && {args: ['--disable-dev-shm-usage']})
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
- browserContext = await browser.newContext(device.windowOptions);
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. browser tab) is added to the browser context (i.e. browser window):
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] || 15000}
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 timeout and the error listeners.
388
- clearTimeout(timeout);
373
+ // Clear the error listeners.
389
374
  if (page && ! page.isClosed() && crashHandler) {
390
375
  page.off('crash', crashHandler);
391
376
  }