testaro 59.3.0 → 60.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
@@ -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.3.0",
3
+ "version": "60.1.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.
@@ -136,6 +143,15 @@ const getNonce = async response => {
136
143
  // Return the nonce, if any.
137
144
  return nonce;
138
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
+ };
139
155
  // Visits a URL and returns the response of the server.
140
156
  const goTo = async (report, page, url, timeout, waitUntil) => {
141
157
  // If the URL is a file path:
@@ -154,19 +170,10 @@ const goTo = async (report, page, url, timeout, waitUntil) => {
154
170
  const httpStatus = response.status();
155
171
  // If the response status was normal:
156
172
  if ([200, 304].includes(httpStatus) || url.startsWith('file:')) {
157
- // If the browser was redirected in violation of a strictness requirement:
158
173
  const actualURL = page.url();
159
- // Normalize file:// URLs for comparison (handles Windows path formats)
160
- const normalizeFile = u => {
161
- if (!u) return u;
162
- if (!u.toLowerCase().startsWith('file:')) return u;
163
- // Ensure forward slashes and three slashes after file:
164
- let path = u.replace(/^file:\/+/i, '');
165
- path = path.replace(/\\/g, '/');
166
- return 'file:///' + path.replace(/^\//, '');
167
- };
168
174
  const actualNorm = actualURL.startsWith('file:') ? normalizeFile(actualURL) : actualURL;
169
175
  const urlNorm = url.startsWith('file:') ? normalizeFile(url) : url;
176
+ // If the browser was redirected in violation of a strictness requirement:
170
177
  if (report.strict && deSlash(actualNorm) !== deSlash(urlNorm)) {
171
178
  // Return an error.
172
179
  console.log(`ERROR: Visit to ${url} redirected to ${actualURL}`);
@@ -186,6 +193,15 @@ const goTo = async (report, page, url, timeout, waitUntil) => {
186
193
  };
187
194
  }
188
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
+ }
189
205
  // Otherwise, if the response status was rejection of excessive requests:
190
206
  else if (httpStatus === 429) {
191
207
  // Return this.
@@ -275,7 +291,7 @@ const launch = exports.launch = async (
275
291
  // Replace the report target URL with this URL.
276
292
  report.target.url = url;
277
293
  // Create a browser of the specified or default type.
278
- const browserType = require('playwright')[browserID];
294
+ const browserType = playwrightBrowsers[browserID];
279
295
  // Close the current browser, if any.
280
296
  await browserClose();
281
297
  // Define browser options.
@@ -290,17 +306,44 @@ const launch = exports.launch = async (
290
306
  },
291
307
  headless: ! debug,
292
308
  slowMo: waits || 0,
293
- ...(browserID === 'chromium' && {args: ['--disable-dev-shm-usage']})
309
+ ...(browserID === 'chromium' && {
310
+ args: ['--disable-dev-shm-usage', '--disable-blink-features=AutomationControlled']
311
+ })
294
312
  };
295
313
  try {
296
314
  // Replace the browser with a new one.
297
315
  browser = await browserType.launch(browserOptions);
298
316
  // Redefine the context (i.e. browser window).
299
- 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);
300
332
  // Prevent default timeouts.
301
333
  browserContext.setDefaultTimeout(0);
302
- // 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):
303
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
+ });
304
347
  // Ensure the report has a jobData property.
305
348
  report.jobData ??= {};
306
349
  const {jobData} = report;
@@ -69,12 +69,20 @@ const linksByType = async page => await page.evaluateHandle(() => {
69
69
  }
70
70
  };
71
71
  // FUNCTION DEFINITIONS END
72
- // Identify the list links in the page.
72
+ // Identify the lists in the page.
73
73
  const lists = Array.from(document.body.querySelectorAll('ul, ol'));
74
+ // Initialize an array of list links.
74
75
  const listLinks = [];
76
+ // For each one:
75
77
  lists.forEach(list => {
78
+ // If it is a list of links:
76
79
  if (isLinkList(list)) {
77
- listLinks.push(... Array.from(list.querySelectorAll('a')));
80
+ // Choose one of the links randomly, assuming they have uniform styles.
81
+ const links = Array.from(list.querySelectorAll('a'));
82
+ const randomIndex = Math.floor(Math.random() * links.length);
83
+ const randomLink = links[randomIndex];
84
+ // Add it to the array.
85
+ listLinks.push(randomLink);
78
86
  }
79
87
  });
80
88
  // Identify the inline links in the page.
@@ -163,6 +171,7 @@ exports.reporter = async (page, withItems) => {
163
171
  // Add its elements to the object.
164
172
  elements.headings[tagName] = Array.from(body.getElementsByTagName(tagName));
165
173
  });
174
+ // FUNCTION DEFINITION START
166
175
  // Tabulates the distribution of style properties for elements of a type.
167
176
  const tallyStyles = (typeName, elements, typeStyles, withItems) => {
168
177
  // If there are any elements:
@@ -226,6 +235,7 @@ exports.reporter = async (page, withItems) => {
226
235
  }
227
236
  }
228
237
  };
238
+ // FUNCTION DEFINITION END
229
239
  // Report the style-property distributions for the element types.
230
240
  tallyStyles('button', elements.buttons, buttonStyles, withItems);
231
241
  tallyStyles('adjacentLink', elements.links.adjacent, [], withItems);