testaro 67.0.0 → 68.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.
Files changed (95) hide show
  1. package/LICENSE +4 -16
  2. package/README.md +10 -2
  3. package/UPGRADES.md +1 -1
  4. package/dirWatch.js +2 -3
  5. package/ed11y/editoria11y.min.js +109 -690
  6. package/ed11y/editoria11y210.min.js +747 -0
  7. package/netWatch.js +6 -6
  8. package/package.json +1 -1
  9. package/procs/aslint.js +2 -2
  10. package/procs/catalog.js +190 -0
  11. package/procs/{dateOf.js → dateTime.js} +6 -4
  12. package/procs/doActs.js +1227 -0
  13. package/procs/doTestAct.js +63 -29
  14. package/procs/error.js +53 -0
  15. package/procs/job.js +64 -38
  16. package/procs/launch.js +596 -0
  17. package/procs/nu.js +3 -18
  18. package/procs/shoot.js +18 -2
  19. package/procs/testaro.js +102 -125
  20. package/procs/xPath.js +62 -0
  21. package/run.js +42 -1938
  22. package/scratch/README.md +9 -0
  23. package/testaro/adbID.js +3 -3
  24. package/testaro/allCaps.js +4 -5
  25. package/testaro/allHidden.js +19 -18
  26. package/testaro/allSlanted.js +4 -5
  27. package/testaro/altScheme.js +3 -3
  28. package/testaro/attVal.js +19 -35
  29. package/testaro/autocomplete.js +65 -62
  30. package/testaro/bulk.js +21 -20
  31. package/testaro/buttonMenu.js +112 -33
  32. package/testaro/captionLoc.js +3 -3
  33. package/testaro/datalistRef.js +4 -5
  34. package/testaro/distortion.js +3 -3
  35. package/testaro/docType.js +6 -9
  36. package/testaro/dupAtt.js +12 -25
  37. package/testaro/elements.js +4 -3
  38. package/testaro/embAc.js +4 -2
  39. package/testaro/focAll.js +6 -13
  40. package/testaro/focAndOp.js +3 -3
  41. package/testaro/focInd.js +3 -3
  42. package/testaro/focVis.js +4 -3
  43. package/testaro/headEl.js +5 -12
  44. package/testaro/headingAmb.js +45 -88
  45. package/testaro/hovInd.js +5 -5
  46. package/testaro/hover.js +44 -8
  47. package/testaro/hr.js +4 -4
  48. package/testaro/imageLink.js +3 -3
  49. package/testaro/labClash.js +3 -3
  50. package/testaro/legendLoc.js +3 -3
  51. package/testaro/lineHeight.js +3 -3
  52. package/testaro/linkAmb.js +25 -17
  53. package/testaro/linkExt.js +5 -5
  54. package/testaro/linkOldAtt.js +4 -3
  55. package/testaro/linkTo.js +4 -3
  56. package/testaro/linkUl.js +4 -5
  57. package/testaro/miniText.js +4 -3
  58. package/testaro/motion.js +3 -22
  59. package/testaro/nonTable.js +4 -5
  60. package/testaro/optRoleSel.js +3 -3
  61. package/testaro/phOnly.js +3 -3
  62. package/testaro/pseudoP.js +5 -5
  63. package/testaro/radioSet.js +4 -5
  64. package/testaro/role.js +4 -5
  65. package/testaro/secHeading.js +4 -5
  66. package/testaro/shoot0.js +3 -2
  67. package/testaro/shoot1.js +3 -2
  68. package/testaro/styleDiff.js +5 -12
  69. package/testaro/tabNav.js +30 -118
  70. package/testaro/targetSmall.js +30 -15
  71. package/testaro/textNodes.js +3 -1
  72. package/testaro/textSem.js +4 -5
  73. package/testaro/title.js +4 -2
  74. package/testaro/titledEl.js +3 -3
  75. package/testaro/zIndex.js +3 -3
  76. package/tests/alfa.js +28 -54
  77. package/tests/aslint.js +20 -53
  78. package/tests/axe.js +76 -13
  79. package/tests/ed11y.js +69 -141
  80. package/tests/htmlcs.js +69 -38
  81. package/tests/ibm.js +54 -9
  82. package/tests/nuVal.js +65 -12
  83. package/tests/nuVnu.js +76 -26
  84. package/tests/qualWeb.js +89 -44
  85. package/tests/testaro.js +288 -273
  86. package/tests/wave.js +142 -117
  87. package/tests/wax.js +61 -42
  88. package/procs/getLocatorData.js +0 -192
  89. package/procs/identify.js +0 -250
  90. package/procs/isInlineLink.js +0 -42
  91. package/procs/screenShot.js +0 -32
  92. package/procs/standardize.js +0 -524
  93. package/procs/target.js +0 -90
  94. package/procs/tellServer.js +0 -43
  95. package/scripts/dumpAlts.js +0 -28
@@ -0,0 +1,596 @@
1
+ /*
2
+ © 2021–2025 CVS Health and/or one of its affiliates. All rights reserved.
3
+ © 2025–2026 Jonathan Robert Pool.
4
+
5
+ Licensed under the MIT License. See LICENSE file at the project root or
6
+ https://opensource.org/license/mit/ for details.
7
+
8
+ SPDX-License-Identifier: MIT
9
+ */
10
+
11
+ /*
12
+ launch.js
13
+ Creates a browser, context, and page, navigates, and acts.
14
+ */
15
+
16
+ // IMPORTS
17
+
18
+ // Module to handle errors.
19
+ const {addError} = require('./error');
20
+ const headedBrowser = process.env.HEADED_BROWSER === 'true';
21
+ const {chromium, webkit, firefox} = require('playwright-extra');
22
+ const {isBrowserID, isDeviceID, isURL, isValidJob} = require('./job');
23
+
24
+ // CONSTANTS
25
+
26
+ // Whether to log page-context log messages.
27
+ const debug = process.env.DEBUG === 'true';
28
+ // Playwright browser types.
29
+ const playwrightBrowsers = {chromium, webkit, firefox};
30
+ // Strings in log messages indicating errors.
31
+ const errorWords = [
32
+ 'but not used',
33
+ 'content security policy',
34
+ 'deprecated',
35
+ 'error',
36
+ 'exception',
37
+ 'expected',
38
+ 'failed',
39
+ 'invalid',
40
+ 'missing',
41
+ 'non-standard',
42
+ 'not supported',
43
+ 'refused',
44
+ 'requires',
45
+ 'sorry',
46
+ 'suspicious',
47
+ 'unrecognized',
48
+ 'violates',
49
+ 'warning'
50
+ ];
51
+ // Seconds to wait between actions.
52
+ const waits = Number(process.env.WAITS) ?? 0;
53
+
54
+ // FUNCTIONS
55
+
56
+ // Waits.
57
+ const wait = exports.wait = ms => {
58
+ return new Promise(resolve => {
59
+ setTimeout(() => {
60
+ resolve('');
61
+ }, ms);
62
+ });
63
+ };
64
+ // Close a browser context and/or its browser, if they exist.
65
+ const browserClose = exports.browserClose = async page => {
66
+ if (page) {
67
+ const browserContext = page.context;
68
+ if (browserContext) {
69
+ const {browser} = browserContext;
70
+ try {
71
+ await browserContext.close();
72
+ }
73
+ catch(error) {}
74
+ if (browser) {
75
+ try {
76
+ await browser.close();
77
+ }
78
+ catch(error) {}
79
+ }
80
+ }
81
+ }
82
+ };
83
+ // Visits a URL and returns the response of the server.
84
+ const goTo = exports.goTo = async (report, page, url, timeout, waitUntil) => {
85
+ // If the URL is a file path:
86
+ if (url.startsWith('file://')) {
87
+ // Make it absolute.
88
+ url = url.replace('file://', `file://${__dirname}/`);
89
+ }
90
+ // Visit the URL.
91
+ const startTime = Date.now();
92
+ try {
93
+ const response = await page.goto(url, {
94
+ timeout,
95
+ waitUntil
96
+ });
97
+ report.jobData.visitLatency += Math.round((Date.now() - startTime) / 1000);
98
+ const httpStatus = response.status();
99
+ // If the response status was normal:
100
+ if ([200, 304].includes(httpStatus) || url.startsWith('file:')) {
101
+ const actualURL = page.url();
102
+ const actualNorm = actualURL.startsWith('file:') ? normalizeFile(actualURL) : actualURL;
103
+ const urlNorm = url.startsWith('file:') ? normalizeFile(url) : url;
104
+ // If the browser was redirected in violation of a strictness requirement:
105
+ if (report.strict && deSlash(actualNorm) !== deSlash(urlNorm)) {
106
+ // Return an error.
107
+ console.log(`ERROR: Visit to ${url} redirected to ${actualURL}`);
108
+ return {
109
+ success: false,
110
+ error: 'badRedirection'
111
+ };
112
+ }
113
+ // Otherwise, i.e. if no prohibited redirection occurred:
114
+ else {
115
+ // Press the Escape key to dismiss any modal dialog.
116
+ await page.keyboard.press('Escape');
117
+ // Return the result of the navigation.
118
+ return {
119
+ success: true,
120
+ response
121
+ };
122
+ }
123
+ }
124
+ // Otherwise, if the response status was prohibition:
125
+ else if (httpStatus === 403) {
126
+ // Return this.
127
+ console.log(`ERROR: Visit to ${url} prohibited (status 403)`);
128
+ return {
129
+ success: false,
130
+ error: 'status403'
131
+ };
132
+ }
133
+ // Otherwise, if the response status was rejection of excessive requests:
134
+ else if (httpStatus === 429) {
135
+ const retryHeader = response.headers()['retry-after'];
136
+ let waitSeconds = 5;
137
+ if (retryHeader) {
138
+ waitSeconds = Number.isNaN(Number(retryHeader))
139
+ ? Math.ceil((new Date(retryHeader) - new Date()) / 1000)
140
+ : Number(retryHeader);
141
+ }
142
+ // Return this.
143
+ console.log(
144
+ `ERROR: Visit to ${url} rate-limited (status 429); retry after ${waitSeconds} sec.`
145
+ );
146
+ return {
147
+ success: false,
148
+ error: `status429/retryAfterSeconds=${waitSeconds}`
149
+ };
150
+ }
151
+ // Otherwise, i.e. if the response status was otherwise abnormal:
152
+ else {
153
+ // Return an error.
154
+ console.log(`ERROR: Visit to ${url} got status ${httpStatus}`);
155
+ report.jobData.visitRejectionCount++;
156
+ return {
157
+ success: false,
158
+ error: 'badStatus'
159
+ };
160
+ }
161
+ }
162
+ catch(error) {
163
+ if (debug) {
164
+ console.log(`ERROR visiting ${url} (${error.message.slice(0, 200)})`);
165
+ }
166
+ return {
167
+ success: false,
168
+ error: 'noVisit'
169
+ };
170
+ }
171
+ };
172
+ // Gets the script nonce from a response.
173
+ const getNonce = exports.getNonce = async response => {
174
+ let nonce = '';
175
+ // If the response includes a content security policy:
176
+ const headers = await response.allHeaders();
177
+ const cspWithQuotes = headers && headers['content-security-policy'];
178
+ if (cspWithQuotes) {
179
+ // If it requires scripts to have a nonce:
180
+ const csp = cspWithQuotes.replace(/'/g, '');
181
+ const directives = csp.split(/ *; */).map(directive => directive.split(/ +/));
182
+ const scriptDirective = directives.find(dir => dir[0] === 'script-src');
183
+ if (scriptDirective) {
184
+ const nonceSpec = scriptDirective.find(valPart => valPart.startsWith('nonce-'));
185
+ if (nonceSpec) {
186
+ // Return the nonce.
187
+ nonce = nonceSpec.replace(/^nonce-/, '');
188
+ }
189
+ }
190
+ }
191
+ // Return the nonce, if any.
192
+ return nonce;
193
+ };
194
+ // Creates a browser, context, and page; navigates to a URL; and returns the page.
195
+ const launchOnce = async opts => {
196
+ // Get the arguments.
197
+ const {
198
+ relaxWait = 'no',// no, partly, fully
199
+ report = {},
200
+ actIndex = 0,
201
+ tempBrowserID = '',
202
+ tempURL = '',
203
+ headEmulation = 'high',// low, high
204
+ xPathNeed = 'script',// own, script, attribute, none
205
+ needsAccessibleName = false
206
+ } = opts;
207
+ const act = report.acts[actIndex] ?? {};
208
+ const {device} = report;
209
+ const deviceID = device?.id;
210
+ const browserID = tempBrowserID || report.browserID || '';
211
+ const url = tempURL || report.target?.url || '';
212
+ let page;
213
+ // If the specified browser and device types and URL are valid:
214
+ if (isBrowserID(browserID) && isDeviceID(deviceID) && isURL(url)) {
215
+ // Replace the report target URL with this URL.
216
+ report.target.url = url;
217
+ // Create a browser of the specified or default type.
218
+ const browserType = playwrightBrowsers[browserID];
219
+ // Define the browser-option args, depending on the browser type and head-emulation level.
220
+ const browserOptionArgs = [];
221
+ if (browserID === 'chromium') {
222
+ browserOptionArgs.push(
223
+ '--disable-dev-shm-usage', '--disable-blink-features=AutomationControlled'
224
+ );
225
+ if (headEmulation === 'high') {
226
+ browserOptionArgs.push(
227
+ '--disable-gpu',
228
+ '--disable-software-rasterizer',
229
+ '--force-device-scale-factor=1',
230
+ '--disable-default-apps',
231
+ '--disable-extensions',
232
+ '--disable-sync',
233
+ '--disable-background-timer-throttling',
234
+ '--disable-backgrounding-occluded-windows',
235
+ '--disable-renderer-backgrounding',
236
+ '--disable-background-networking',
237
+ '--force-color-profile=srgb',
238
+ '--disable-features=TranslateUI,VizDisplayCompositor',
239
+ '--disable-ipc-flooding-protection',
240
+ '--disable-logging',
241
+ '--disable-permissions-api',
242
+ '--disable-notifications',
243
+ '--disable-popup-blocking'
244
+ );
245
+ }
246
+ }
247
+ // Define the browser options.
248
+ const browserOptions = {
249
+ logger: {
250
+ isEnabled: () => false,
251
+ log: (name, severity, message) => {
252
+ if (['warning', 'error'].includes(severity)) {
253
+ console.log(`${severity.toUpperCase()}: ${message.slice(0, 200)}`);
254
+ }
255
+ }
256
+ },
257
+ headless: ! headedBrowser,
258
+ slowMo: waits || 0,
259
+ args: browserOptionArgs
260
+ };
261
+ let browser, browserContext;
262
+ try {
263
+ // Create a browser of the specified type.
264
+ browser = await browserType.launch(browserOptions);
265
+ // Create a context (i.e. window) for it.
266
+ const contextOptions = {
267
+ ...device.windowOptions,
268
+ 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',
269
+ viewport: device.windowOptions.viewport || {width: 1920, height: 1080},
270
+ locale: 'en-US',
271
+ timezoneId: 'America/Los_Angeles',
272
+ extraHTTPHeaders: {
273
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
274
+ 'Accept-Language': 'en-US,en;q=0.9',
275
+ 'Accept-Encoding': 'gzip, deflate, br',
276
+ 'DNT': '1',
277
+ 'Upgrade-Insecure-Requests': '1'
278
+ }
279
+ };
280
+ browserContext = await browser.newContext(contextOptions);
281
+ // Prevent default timeouts.
282
+ browserContext.setDefaultTimeout(0);
283
+ // When a page (i.e. tab) is added to the browser context (i.e. window):
284
+ browserContext.on('page', async page => {
285
+ // Ensure the report has a jobData property.
286
+ report.jobData ??= {};
287
+ const {jobData} = report;
288
+ jobData.logCount ??= 0;
289
+ jobData.logSize ??= 0;
290
+ jobData.errorLogCount ??= 0;
291
+ // When an error is thrown, increment the count of logging errors.
292
+ page.on('crash', () => {
293
+ jobData.errorLogCount++;
294
+ console.log('Page crashed');
295
+ });
296
+ page.on('pageerror', () => {
297
+ jobData.errorLogCount++;
298
+ });
299
+ page.on('requestfailed', () => {
300
+ jobData.errorLogCount++;
301
+ });
302
+ // When the page emits a message:
303
+ page.on('console', msg => {
304
+ const msgText = msg.text();
305
+ // If debugging is on:
306
+ if (debug) {
307
+ // Log the start of the message on the console.
308
+ console.log(`\n${msgText.slice(0, 3000)}`);
309
+ }
310
+ // Add statistics on the message to the report.
311
+ const msgTextLC = msgText.toLowerCase();
312
+ const msgLength = msgText.length;
313
+ jobData.logCount++;
314
+ jobData.logSize += msgLength;
315
+ if (errorWords.some(word => msgTextLC.includes(word))) {
316
+ jobData.errorLogCount++;
317
+ jobData.errorLogSize += msgLength;
318
+ }
319
+ const msgLC = msgText.toLowerCase();
320
+ if (
321
+ msgText.includes('403') && (msgLC.includes('status')
322
+ || msgLC.includes('prohibited'))
323
+ ) {
324
+ jobData.prohibitedCount++;
325
+ }
326
+ });
327
+ });
328
+ // Create a page (tab) of the context (window).
329
+ page = await browserContext.newPage();
330
+ // Add a script to the page to mask automation detection.
331
+ await page.addInitScript(() => {
332
+ Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
333
+ window.chrome = {runtime: {}};
334
+ Object.defineProperty(navigator, 'plugins', {
335
+ get: () => [1, 2, 3, 4, 5]
336
+ });
337
+ Object.defineProperty(navigator, 'languages', {
338
+ get: () => ['en-US', 'en']
339
+ });
340
+ });
341
+ // If an XPath computation script is required:
342
+ if (xPathNeed !== 'none') {
343
+ // Add a script to the page to add a window method to get the XPath of an element.
344
+ await page.addInitScript(() => {
345
+ window.getXPath = element => {
346
+ if (! element || element.nodeType !== Node.ELEMENT_NODE) {
347
+ return '';
348
+ }
349
+ const segments = [];
350
+ // As long as the current node is an element:
351
+ while (element && element.nodeType === Node.ELEMENT_NODE) {
352
+ const tag = element.tagName.toLowerCase();
353
+ // If it is the html element:
354
+ if (element === document.documentElement) {
355
+ // Prepend it to the segment array
356
+ segments.unshift('html');
357
+ // Stop traversing.
358
+ break;
359
+ }
360
+ // Otherwise, get its parent node.
361
+ const parent = element.parentNode;
362
+ // If (abnormally) the parent node is not an element:
363
+ if (! parent || parent.nodeType !== Node.ELEMENT_NODE) {
364
+ // Prepend the element (not the parent) to the segment array.
365
+ segments.unshift(tag);
366
+ // Stop traversing, leaving the segment array partial.
367
+ break;
368
+ }
369
+ // Get the subscript of the element if it is not the body element.
370
+ const cohort = Array
371
+ .from(parent.childNodes)
372
+ .filter(
373
+ childNode => childNode.nodeType === Node.ELEMENT_NODE
374
+ && childNode.tagName === element.tagName
375
+ );
376
+ const subscript = tag === 'body' ? '' : `[${cohort.indexOf(element) + 1}]`;
377
+ // Prepend the element identifier to the segment array.
378
+ segments.unshift(`${tag}${subscript}`);
379
+ // Continue the traversal with the parent of the current element.
380
+ element = parent;
381
+ }
382
+ // Return the XPath.
383
+ return `/${segments.join('/')}`;
384
+ };
385
+ });
386
+ }
387
+ // If an accessible-name computation script is needed:
388
+ if (needsAccessibleName) {
389
+ // Add the dom-accessibility-api script to the page to compute an accessible name.
390
+ await page.addInitScript({path: require.resolve('../dist/nameComputation.js')});
391
+ // Add a script to the page to:
392
+ await page.addInitScript(() => {
393
+ // Add a window method to compute the accessible name of an element.
394
+ window.getAccessibleName = element => {
395
+ const nameIsComputable = element?.nodeType === Node.ELEMENT_NODE
396
+ && typeof window.computeAccessibleName === 'function';
397
+ return nameIsComputable ? window.computeAccessibleName(element) : '';
398
+ };
399
+ // Add a window method to return a standard proto-instance.
400
+ window.getProtoInstance = (
401
+ element, ruleID, what, count = 1, ordinalSeverity, summaryTagName = ''
402
+ ) => {
403
+ // If an element has been specified:
404
+ if (element) {
405
+ // Get its properties.
406
+ return {
407
+ ruleID,
408
+ what,
409
+ count,
410
+ ordinalSeverity,
411
+ pathID: window.getXPath(element)
412
+ };
413
+ }
414
+ // Otherwise, i.e. if no element has been specified, return a summary instance.
415
+ return {
416
+ ruleID,
417
+ what,
418
+ count,
419
+ ordinalSeverity
420
+ };
421
+ };
422
+ });
423
+ }
424
+ // Base the wait on the need of the tool and the retry history.
425
+ let waitUntil = xPathNeed === 'none' ? 'domcontentloaded' : 'networkidle';
426
+ if (relaxWait !== 'no' && waitUntil === 'networkidle') {
427
+ waitUntil = 'domcontentloaded';
428
+ }
429
+ if (relaxWait === 'fully') {
430
+ waitUntil = 'load';
431
+ }
432
+ // Navigate to the specified URL and wait for the stability required by the next action.
433
+ const navResult = await goTo(report, page, url, 10000, waitUntil);
434
+ // If the navigation succeeded:
435
+ if (navResult.success) {
436
+ // If XPath attributes are needed:
437
+ if (xPathNeed === 'attribute') {
438
+ // Use the added script to add them.
439
+ await page.evaluate(() => {
440
+ const elements = document.querySelectorAll('*');
441
+ elements.forEach(element => {
442
+ element.setAttribute('data-xpath', window.getXPath(element));
443
+ });
444
+ });
445
+ }
446
+ // If the launch was for an act:
447
+ if (act) {
448
+ // Add the actual URL to the act.
449
+ act.actualURL = page.url();
450
+ // Get the response of the target server.
451
+ const {response} = navResult;
452
+ // Add the script nonce, if any, to the act.
453
+ const scriptNonce = await getNonce(response);
454
+ if (scriptNonce) {
455
+ report.jobData.lastScriptNonce = scriptNonce;
456
+ }
457
+ }
458
+ }
459
+ // Otherwise, i.e. if the navigation failed:
460
+ else {
461
+ // Throw an error.
462
+ throw new Error(`Navigation failed (${navResult.error})`);
463
+ }
464
+ }
465
+ // If an error occurred:
466
+ catch(error) {
467
+ // Report this.
468
+ console.log(`ERROR launching or navigating (${error.message})`);
469
+ // Close the browser and its context, if they exist.
470
+ await browserClose(page);
471
+ // Return a failure.
472
+ return {
473
+ success: false,
474
+ error: error.message
475
+ };
476
+ }
477
+ }
478
+ // If the launch and navigation succeeded, return the page.
479
+ return {
480
+ success: true,
481
+ page
482
+ };
483
+ };
484
+ // Normalizes a file URL in case it has the Windows path format.
485
+ const normalizeFile = u => {
486
+ if (!u) return u;
487
+ if (!u.toLowerCase().startsWith('file:')) return u;
488
+ // Ensure forward slashes and three slashes after file:
489
+ let path = u.replace(/^file:\/+/i, '');
490
+ path = path.replace(/\\/g, '/');
491
+ return 'file:///' + path.replace(/^\//, '');
492
+ };
493
+ // Manages browser launching and navigating and returns a page.
494
+ exports.launch = async (opts = {}) => {
495
+ const {
496
+ report = {},
497
+ actIndex = 0,
498
+ tempBrowserID = '',
499
+ tempURL = '',
500
+ headEmulation = 'high',
501
+ xPathNeed = 'script',
502
+ needsAccessibleName = false,
503
+ retries = 2
504
+ } = opts;
505
+ // If the report is valid:
506
+ const jobValidation = isValidJob(report);
507
+ if (jobValidation.isValid) {
508
+ // Try to launch a browser and navigate to the specified URL.
509
+ let launchResult = await launchOnce(
510
+ {
511
+ relaxWait: 'no',
512
+ priorTries: false,
513
+ report,
514
+ actIndex,
515
+ tempBrowserID,
516
+ tempURL,
517
+ headEmulation,
518
+ xPathNeed,
519
+ needsAccessibleName
520
+ }
521
+ );
522
+ // If the launch and navigation succeeded:
523
+ if (launchResult.success) {
524
+ // Return the page.
525
+ return launchResult.page;
526
+ }
527
+ // Otherwise, i.e. if the launch or navigation failed:
528
+ else {
529
+ let retriesLeft = retries;
530
+ // As long as retries remain, decrement the allowed retry count and:
531
+ while (retriesLeft--) {
532
+ const {error} = launchResult;
533
+ // Prepare to wait 1 second before a retry.
534
+ let waitSeconds = 1;
535
+ // If the error was a visit failure due to rate limiting:
536
+ if (error.includes('status429/retryAfterSeconds=')) {
537
+ const waitSecondsRequest = Number(error.replace(/^.+=|\)$/g, ''));
538
+ // If the requested wait is less than 10 seconds:
539
+ if (! Number.isNaN(waitSecondsRequest) && waitSecondsRequest < 10) {
540
+ // Change the wait to the requested one.
541
+ waitSeconds = waitSecondsRequest;
542
+ }
543
+ }
544
+ // Report the wait.
545
+ console.log(
546
+ `WARNING: Waiting ${waitSeconds} sec. before retrying (retries left: ${retries})`
547
+ );
548
+ // Wait as specified.
549
+ await wait(1000 * waitSeconds);
550
+ // Retry the launch and navigation.
551
+ launchResult = await launchOnce(
552
+ {
553
+ relaxWait: retriesLeft === 0 ? 'fully' : 'partly',
554
+ report,
555
+ actIndex,
556
+ tempBrowserID,
557
+ tempURL,
558
+ headEmulation,
559
+ xPathNeed,
560
+ needsAccessibleName
561
+ }
562
+ );
563
+ // If the launch and navigation succeeded:
564
+ if (launchResult.success) {
565
+ // Return the page.
566
+ return launchResult.page;
567
+ }
568
+ // Otherwise, i.e. if the launch or navigation failed:
569
+ else {
570
+ // Report this.
571
+ console.log(`WARNING: Retry failed; retries left: ${retries}`);
572
+ }
573
+ }
574
+ // If the retries were exhausted:
575
+ if (retriesLeft === -1) {
576
+ // Report this.
577
+ addError(true, false, report, actIndex, 'ERROR: No retries left');
578
+ }
579
+ // Return a failure.
580
+ return null;
581
+ }
582
+ }
583
+ // Otherwise, i.e. if the report is invalid:
584
+ else {
585
+ // Report this.
586
+ addError(
587
+ true,
588
+ false,
589
+ report,
590
+ actIndex,
591
+ `ERROR: Cannot launch browser for invalid job (${jobValidation.error})`
592
+ );
593
+ // Return a failure.
594
+ return null;
595
+ }
596
+ };
package/procs/nu.js CHANGED
@@ -1,8 +1,7 @@
1
1
  /*
2
- © 2025 Jonathan Robert Pool.
2
+ © 2025–2026 Jonathan Robert Pool.
3
3
 
4
- Licensed under the MIT License. See LICENSE file at the project root or
5
- https://opensource.org/license/mit/ for details.
4
+ Licensed under the MIT License. See LICENSE file at the project root or https://opensource.org/license/mit/ for details.
6
5
 
7
6
  SPDX-License-Identifier: MIT
8
7
  */
@@ -14,10 +13,6 @@
14
13
 
15
14
  // ########## IMPORTS
16
15
 
17
- // Module to add Testaro IDs to elements.
18
- const {addTestaroIDs} = require('./testaro');
19
- // Module to get location data from an element.
20
- const {getElementData} = require('./getLocatorData');
21
16
  // Module to get the document source.
22
17
  const {getSource} = require('./getSource');
23
18
 
@@ -47,8 +42,6 @@ exports.getContent = async (page, withSource) => {
47
42
  }
48
43
  // Otherwise, i.e. if the specified content type was the Playwright page content:
49
44
  else {
50
- // Annotate all elements on the page with unique identifiers.
51
- await addTestaroIDs(page);
52
45
  // Add the annotated page content to the data.
53
46
  data.testTarget = await page.content();
54
47
  }
@@ -56,9 +49,7 @@ exports.getContent = async (page, withSource) => {
56
49
  return data;
57
50
  };
58
51
  // Postprocesses a result from nuVal or nuVnu tests.
59
- exports.curate = async (page, data, nuData, rules) => {
60
- // Delete most of the test target from the data.
61
- data.testTarget = `${data.testTarget.slice(0, 200)}…`;
52
+ exports.curate = async (data, nuData, rules) => {
62
53
  let result;
63
54
  // If a result was obtained:
64
55
  if (nuData) {
@@ -90,12 +81,6 @@ exports.curate = async (page, data, nuData, rules) => {
90
81
  result.messages = result.messages.filter(
91
82
  message => ! badMessages.has(message.message)
92
83
  );
93
- // For each message:
94
- for (const message of result.messages) {
95
- const {extract} = message;
96
- // Add location data for the element to the message.
97
- message.elementLocation = await getElementData(page, extract);
98
- }
99
84
  }
100
85
  // Return the result.
101
86
  return result;
package/procs/shoot.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*
2
- © 2025 Jonathan Robert Pool.
2
+ © 2025–2026 Jonathan Robert Pool.
3
3
  Licensed under the MIT License. See LICENSE file for details.
4
4
  */
5
5
 
@@ -14,7 +14,6 @@ const fs = require('fs/promises');
14
14
  const os = require('os');
15
15
  const path = require('path');
16
16
  const {PNG} = require('pngjs');
17
- const {screenShot} = require('./screenShot');
18
17
 
19
18
  // CONSTANTS
20
19
 
@@ -22,6 +21,23 @@ const tmpDir = os.tmpdir();
22
21
 
23
22
  // FUNCTIONS
24
23
 
24
+ // Creates and returns a screenshot.
25
+ const screenShot = async (page, exclusion = null) => {
26
+ const options = {
27
+ fullPage: true,
28
+ omitBackground: true,
29
+ timeout: 4000
30
+ };
31
+ if (exclusion) {
32
+ options.mask = [exclusion];
33
+ }
34
+ // Make and return a screenshot as a buffer.
35
+ return await page.screenshot(options)
36
+ .catch(error => {
37
+ console.log(`ERROR: Screenshot failed (${error.message})`);
38
+ return '';
39
+ });
40
+ };
25
41
  exports.shoot = async (page, index) => {
26
42
  // Make and get a screenshot as a buffer.
27
43
  let shot = await screenShot(page);