testaro 72.4.3 → 72.5.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/CLAUDE.md CHANGED
@@ -82,7 +82,7 @@ Key variables:
82
82
  - `WAVE_KEY` — API key for the WAVE subscription API
83
83
  - `JOBDIR` / `REPORTDIR` — root directories for job files and report output
84
84
  - `AGENT` — instance name used in network-watch mode
85
- - `NETWATCH_URL_<N>_JOB/OBSERVE/REPORT/AUTH`, `NETWATCH_URLS` — server polling configuration
85
+ - `NETWATCH_URL_<N>_JOB/REPORT/AUTH`, `NETWATCH_URLS` — server polling configuration
86
86
  - `TIMEOUT_MULTIPLIER` — scales all per-tool time limits (default 1)
87
87
 
88
88
  ## Code style
package/README.md CHANGED
@@ -162,8 +162,6 @@ AGENT=agentabc
162
162
  # When Testaro polls a network host to ask for new jobs, data on the host.
163
163
  # URL to poll.
164
164
  NETWATCH_JOB=http://localhost:3000/api/assignJob/agentabc
165
- # URL to which to send progress reports during jobs.
166
- NETWATCH_OBSERVE=http://localhost:3000/api/granular/agentabc
167
165
  # URL to which to send completed job reports.
168
166
  NETWATCH_REPORT=http://localhost:3000/api/takeReport/agentabc
169
167
  # Password to give to the host to authenticate this instance.
@@ -184,7 +182,6 @@ Here is a sample job, showing properties that you can set:
184
182
  what: 'monthly health check', // Job description
185
183
  strict: true, // Whether to reject redirections from the target URL
186
184
  standard: 'also', // or 'only' or 'no' (whether to report a standard result)
187
- observe: false, // Whether to send progress notices to the requesting host
188
185
  device: { // Device to emulate
189
186
  id: 'iPhone 8',
190
187
  windowOptions: {
package/call.js CHANGED
@@ -56,8 +56,6 @@ const callRun = async jobIDStart => {
56
56
  // Get it.
57
57
  const jobJSON = await fs.readFile(`${todoDir}/${jobFileName}`, 'utf8');
58
58
  let report = JSON.parse(jobJSON);
59
- // Ensure it does not specify server properties.
60
- report.observe = false;
61
59
  // Run it.
62
60
  report = await doJob(report);
63
61
  // Archive it.
package/dirWatch.js CHANGED
@@ -100,9 +100,6 @@ exports.dirWatch = async (isForever, intervalInSeconds) => {
100
100
  try {
101
101
  const job = JSON.parse(jobJSON);
102
102
  let report = JSON.parse(jobJSON);
103
- // Ensure it has no server properties.
104
- job.observe = false;
105
- report.observe = false;
106
103
  const {id} = job;
107
104
  console.log(`\n\nDirectory job ${id} ready to do (${nowString()})`);
108
105
  // Perform it and get a report.
package/env.example ADDED
@@ -0,0 +1,32 @@
1
+ # You can get a WAVE API key at https://wave.webaim.org/api.
2
+ WAVE_KEY=__placeholder__
3
+ # You can get an Anthropic API key at https://console.anthropic.com/.
4
+ ANTHROPIC_API_KEY=__placeholder__
5
+ # Name to identify this Testaro instance to a server.
6
+ AGENT=__placeholder__
7
+ # Whether to make the browser visible (normally false).
8
+ HEADED_BROWSER=false
9
+ # Whether to output even forked-job logging to the primary log (normally false).
10
+ DEBUG=false
11
+ # Number of seconds to wait between actions (normally 0).
12
+ WAITS=0
13
+ # See https://nodejs.org/api/cli.html#options for all permitted options.
14
+ NODE_OPTIONS='--trace-uncaught --trace-warnings'
15
+ # Suppresses a routine warning from QualWeb.
16
+ PUPPETEER_DISABLE_HEADLESS_WARNING=true
17
+ # URL to poll for available jobs when watching the network.
18
+ NETWATCH_JOB=__placeholder__/api/testaro-agent/job
19
+ # URL to report results to when watching the network.
20
+ NETWATCH_REPORT=__placeholder__/api/testaro-agent/report
21
+ # Password of this Testaro agent
22
+ NETWATCH_AUTH=__placeholder__
23
+ # Directory (relative to project root) to watch for jobs.
24
+ JOBDIR=__placeholder__
25
+ # Directory (relative to project root) to write reports to when watching a directory for jobs.
26
+ REPORTDIR=__placeholder__
27
+ # Multiplier for time limits (normally 1).
28
+ TIMEOUT_MULTIPLIER=1
29
+ # Name of the directory at the project root used for temporary files.
30
+ TMPDIRNAME=scratch
31
+ # Whether to abort the job when any test act fork crashes (default false).
32
+ ABORT_ASSERTIVELY=false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "72.4.3",
3
+ "version": "72.5.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": {
package/procs/doActs.js CHANGED
@@ -14,22 +14,13 @@
14
14
 
15
15
  // IMPORTS
16
16
 
17
- // Module to handle errors.
18
- const {abortActs, addError} = require('./error');
19
- // Function to close a browser and/or its context.
17
+ const {addError} = require('./error');
20
18
  const {getNonce, goTo, launch, wait} = require('./launch');
21
- // Constant describing the tools.
22
19
  const {tools} = require('./job');
23
- // Module to create child processes.
24
20
  const {fork} = require('child_process');
25
21
  const os = require('os');
26
- // Function to prune a catalog.
27
22
  const {pruneCatalog} = require('./catalog');
28
- // Module to handle file system operations.
29
23
  const fs = require('fs/promises');
30
- const httpClient = require('http');
31
- const httpsClient = require('https');
32
- const agent = process.env.AGENT;
33
24
 
34
25
  // CONSTANTS
35
26
 
@@ -62,27 +53,11 @@ const timeLimits = {
62
53
  };
63
54
  // Timeout multiplier.
64
55
  const timeoutMultiplier = Number.parseFloat(process.env.TIMEOUT_MULTIPLIER) || 1;
56
+ // Abort aggressiveness.
57
+ const abortAssertively = process.env.ABORT_ASSERTIVELY === 'true';
65
58
 
66
59
  // FUNCTIONS
67
60
 
68
- // Sends a notice to a network observer.
69
- const tellServer = (report, messageParams, logMessage) => {
70
- const observerURL = process.env.NETWATCH_URL_OBSERVE;
71
- if (observerURL) {
72
- const whoParams = `agent=${agent}&jobID=${report.id || ''}`;
73
- const wholeURL = `${observerURL}?${whoParams}&${messageParams}`;
74
- const client = wholeURL.startsWith('https://') ? httpsClient : httpClient;
75
- client.request(wholeURL)
76
- // If the notification threw an error:
77
- .on('error', error => {
78
- // Report the error.
79
- const errorMessage = 'ERROR notifying the server';
80
- console.log(`${errorMessage} (${error.message})`);
81
- })
82
- .end();
83
- console.log(`${logMessage} (server notified)`);
84
- }
85
- };
86
61
  // Normalizes spacing characters and cases in a string.
87
62
  const debloat = string => string.replace(/\s/g, ' ').trim().replace(/ {2,}/g, ' ').toLowerCase();
88
63
  // Returns the first line of an error message.
@@ -249,83 +224,50 @@ const waitError = (page, act, error, what) => {
249
224
  return false;
250
225
  };
251
226
  // Performs the acts in a report and adds the results to the report.
252
- exports.doActs = async (report, opts = {}) => {
253
- // Make a local copy of the report.
254
- let localReport = JSON.parse(JSON.stringify(report));
227
+ exports.doActs = async report => {
228
+ // Make a temporary copy of the report. Precondition: report is valid.
229
+ let tempReport = JSON.parse(JSON.stringify(report));
255
230
  let page = null;
256
- let {acts} = localReport;
257
- // Get the granular observation options, if any.
258
- const {onProgress = null, signal = null} = opts;
231
+ let {acts} = tempReport;
259
232
  // Get the standardization specification.
260
- const standard = localReport.standard || 'only';
261
- // Set the temporary directory.
262
- let tmpDir = `${__dirname}/../${process.env.TMPDIRNAME || 'scratch'}`;
263
- try {
264
- await fs.access(tmpDir, fs.constants.W_OK);
265
- }
266
- catch(error) {
267
- console.log(`ERROR: ${tmpDir} is not writable`);
268
- tmpDir = os.tmpdir();
233
+ const standard = tempReport.standard || 'only';
234
+ const tmpDirs = [`${__dirname}/../${process.env.TMPDIRNAME || 'scratch'}`, os.tmpdir(), '/tmp'];
235
+ let tmpDir = null;
236
+ // For each potential temporary directory:
237
+ for (const tmpDirAlternative of tmpDirs) {
269
238
  try {
270
- await fs.access(tmpDir, fs.constants.W_OK);
239
+ // Verify that it is writable.
240
+ await fs.access(tmpDirAlternative, fs.constants.W_OK);
241
+ tmpDir = tmpDirAlternative;
242
+ break;
271
243
  }
244
+ // If it is not:
272
245
  catch(error) {
273
- console.log(`ERROR: ${tmpDir} is not writable`);
274
- tmpDir = '/tmp';
275
- try {
276
- await fs.access(tmpDir, fs.constants.W_OK);
277
- }
278
- catch(error) {
279
- console.log(`ERROR: ${tmpDir} is not writable; quitting`);
280
- process.exit(1);
281
- }
246
+ // Report this.
247
+ console.log(`ERROR: ${tmpDirAlternative} is not a writable directory for temporary reports`);
282
248
  }
283
249
  }
250
+ // If no writable temporary directory was found:
251
+ if (! tmpDir) {
252
+ // Report this.
253
+ console.log('ERROR: No writable temporary directory was found; quitting');
254
+ // Quit.
255
+ process.exit(1);
256
+ }
284
257
  // Get a path for temporary reports.
285
- const reportPath = `${tmpDir}/${localReport.id}.json`;
258
+ const reportPath = `${tmpDir}/${tempReport.id}.json`;
286
259
  // Initialize the count of completed acts.
287
260
  let actCount = 0;
288
- // For each act in the local report:
261
+ // For each act in the temporary report:
289
262
  for (const actIndex in acts) {
290
- // If the job has been aborted by a signal:
291
- if (signal && signal.aborted) {
292
- // Report this.
293
- throw new Error('doActs aborted');
294
- }
295
- // Otherwise, and if the job has not been aborted internally:
296
- if (localReport.jobData && ! localReport.jobData.aborted) {
263
+ // If the job has not been aborted:
264
+ if (tempReport?.jobData && ! tempReport.jobData.aborted) {
297
265
  let act = acts[actIndex];
298
266
  const {type, which} = act;
299
267
  const actSuffix = type === 'test' ? ` ${which}` : '';
300
268
  const message = `>>>> ${type}${actSuffix}`;
301
- // If granular reporting has been specified:
302
- if (localReport.observe) {
303
- const whichParam = which ? `&which=${which}` : '';
304
- const messageParams = `act=${type}${whichParam}`;
305
- // If a progress callback has been provided by a caller on this host:
306
- if (onProgress) {
307
- // Notify the observer of the act.
308
- try {
309
- onProgress({
310
- type,
311
- which
312
- });
313
- }
314
- catch (error) {
315
- console.log(`${message} (observer notification failed: ${errorStart(error)})`);
316
- }
317
- }
318
- // Otherwise, i.e. if no progress callback has been provided:
319
- else {
320
- // Notify the remote observer of the act and log it.
321
- tellServer(localReport, messageParams, message);
322
- }
323
- }
324
- // Otherwise, i.e. if granular reporting has not been specified:
325
- else {
326
- // Log the act.
327
- console.log(message);
328
- }
269
+ // Log the act.
270
+ console.log(message);
329
271
  // If the act is an index changer:
330
272
  if (type === 'next') {
331
273
  const condition = act.if;
@@ -347,7 +289,7 @@ exports.doActs = async (report, opts = {}) => {
347
289
  if (truth[1]) {
348
290
  // If the performance of acts is to stop:
349
291
  if (act.jump === 0) {
350
- // Quit.
292
+ // Stop processing acts.
351
293
  break;
352
294
  }
353
295
  // Otherwise, if there is a numerical jump:
@@ -366,16 +308,16 @@ exports.doActs = async (report, opts = {}) => {
366
308
  else if (type === 'launch') {
367
309
  // Launch a browser, navigate to a page, and add the result to the act.
368
310
  page = await launch({
369
- localReport,
311
+ tempReport,
370
312
  actIndex,
371
- tempBrowserID: getActBrowserID(localReport, actIndex),
372
- tempURL: getActTargetURL(localReport, actIndex),
313
+ tempBrowserID: getActBrowserID(tempReport, actIndex),
314
+ tempURL: getActTargetURL(tempReport, actIndex),
373
315
  xPathNeed: 'none'
374
316
  });
375
317
  // If this failed:
376
318
  if (! page) {
377
- // Add this to the act.
378
- addError(false, false, localReport, actIndex, page.error ?? '');
319
+ // Report this.
320
+ addError(false, false, tempReport, actIndex, page.error ?? '');
379
321
  }
380
322
  }
381
323
  // Otherwise, if the act is a test act:
@@ -386,9 +328,9 @@ exports.doActs = async (report, opts = {}) => {
386
328
  const startTime = Date.now();
387
329
  // Add it to the act.
388
330
  act.startTime = startTime;
389
- let localReportJSON = JSON.stringify(localReport);
390
- // Save a copy of the local report, which the child process will read.
391
- await fs.writeFile(reportPath, localReportJSON);
331
+ let tempReportJSON = JSON.stringify(tempReport);
332
+ // Save a copy of the temporary report, which the child process will read.
333
+ await fs.writeFile(reportPath, tempReportJSON);
392
334
  let timedOut = false;
393
335
  const limitMs = timeoutMultiplier * 1000 * (timeLimits[act.which] || 15);
394
336
  const actResult = await new Promise(resolve => {
@@ -459,55 +401,70 @@ exports.doActs = async (report, opts = {}) => {
459
401
  });
460
402
  // If the child process sent a message:
461
403
  if (actResult.kind === 'message') {
462
- // Get the revised localReport file.
463
- localReportJSON = await fs.readFile(reportPath, 'utf8');
404
+ // Get the revised tempReport file.
405
+ tempReportJSON = await fs.readFile(reportPath, 'utf8');
464
406
  try {
465
- // Reassign it to the local report.
466
- localReport = JSON.parse(localReportJSON);
467
- // Redefine the acts as those in the revised local report.
468
- ({acts} = localReport);
407
+ // Reassign it to the temporary report.
408
+ tempReport = JSON.parse(tempReportJSON);
409
+ // Redefine the acts as those in the revised temporary report.
410
+ ({acts} = tempReport);
469
411
  }
470
- // If the conversion fails, leaving the local report and its acts unchanged:
412
+ // If the reassignment fails, leaving the temporary report and its acts unchanged:
471
413
  catch (error) {
472
414
  // Report this.
473
415
  console.log(
474
- `ERROR: Tool sent message ${actResult.message}. Report is no longer JSON (${error.message}) but is instead a(n) ${typeof localReportJSON} of length ${localReportJSON.length}:\n${localReportJSON}`
416
+ `ERROR: Tool sent message ${actResult.message}. Report is no longer JSON (${error.message}) but is instead a(n) ${typeof tempReportJSON} of length ${tempReportJSON.length}:\n${tempReportJSON}`
475
417
  );
476
- // Add the error data to the act.
418
+ // Report this and that the job was aborted.
477
419
  addError(
478
420
  false,
479
- false,
480
- localReport,
421
+ true,
422
+ tempReport,
481
423
  actIndex,
482
- `Non-JSON local report file after message ${actResult.message}`
424
+ `Non-JSON temporary report file after message ${actResult.message}`
483
425
  );
426
+ // Stop processing acts.
427
+ break;
484
428
  }
485
429
  }
486
430
  // Otherwise, i.e. if the child process closed abnormally:
487
431
  else {
488
- // Add the error data to the act.
432
+ // Report this and, if so configured, that the job was aborted.
489
433
  const {code, error, kind, signal} = actResult;
490
434
  if (kind === 'close' && timedOut) {
491
435
  addError(
492
- false, false, localReport, actIndex, `Timed out at ${Math.round(limitMs / 1000)} seconds`
436
+ false,
437
+ abortAssertively,
438
+ tempReport,
439
+ actIndex,
440
+ `Timed out at ${Math.round(limitMs / 1000)} seconds`
493
441
  );
494
442
  }
495
443
  else if (kind === 'close') {
496
444
  addError(
497
- true, false, localReport, actIndex, `Closed with code ${code} and signal ${signal})`
445
+ true,
446
+ abortAssertively,
447
+ tempReport,
448
+ actIndex,
449
+ `Closed with code ${code} and signal ${signal})`
498
450
  );
499
451
  }
500
452
  else {
501
453
  addError(
502
- true, false, localReport, actIndex, `Terminated with error ${error}`
454
+ true, abortAssertively, tempReport, actIndex, `Terminated with error ${error}`
503
455
  );
504
456
  }
457
+ // If the job was aborted:
458
+ if (abortAssertively) {
459
+ // Stop processing acts.
460
+ break;
461
+ }
505
462
  }
506
463
  // Get the (usually revised) act.
507
464
  act = acts[actIndex];
508
- // Add the elapsed time of the tool to the local report.
465
+ // Add the elapsed time of the tool to the temporary report.
509
466
  const time = Math.round((Date.now() - startTime) / 1000);
510
- const {toolTimes} = localReport.jobData;
467
+ const {toolTimes} = tempReport.jobData;
511
468
  toolTimes[act.which] ??= 0;
512
469
  toolTimes[act.which] += time;
513
470
  // If the act was not prevented:
@@ -545,16 +502,16 @@ exports.doActs = async (report, opts = {}) => {
545
502
  const resolved = act.which.replace('__dirname', __dirname);
546
503
  requestedURL = resolved;
547
504
  // Visit it and wait until the DOM is loaded.
548
- const navResult = await goTo(localReport, page, requestedURL, 15000, 'domcontentloaded');
505
+ const navResult = await goTo(tempReport, page, requestedURL, 15000, 'domcontentloaded');
549
506
  // If the visit succeeded:
550
507
  if (navResult.success) {
551
- // Revise the local report URL to this URL.
552
- localReport.target.url = requestedURL;
508
+ // Revise the temporary report URL to this URL.
509
+ tempReport.target.url = requestedURL;
553
510
  // Add the script nonce, if any, to the act.
554
511
  const {response} = navResult;
555
512
  const scriptNonce = getNonce(response);
556
513
  if (scriptNonce) {
557
- localReport.jobData.lastScriptNonce = scriptNonce;
514
+ tempReport.jobData.lastScriptNonce = scriptNonce;
558
515
  }
559
516
  // Add the resulting URL to the act.
560
517
  if (! act.result) {
@@ -564,13 +521,13 @@ exports.doActs = async (report, opts = {}) => {
564
521
  // If a prohibited redirection occurred:
565
522
  if (response.exception === 'badRedirection') {
566
523
  // Report this.
567
- addError(true, false, localReport, actIndex, 'ERROR: Navigation illicitly redirected');
524
+ addError(true, false, tempReport, actIndex, 'ERROR: Navigation illicitly redirected');
568
525
  }
569
526
  }
570
527
  // Otherwise, i.e. if the visit failed:
571
528
  else {
572
529
  // Report this.
573
- addError(true, false, localReport, actIndex, 'ERROR: Visit failed');
530
+ addError(true, false, tempReport, actIndex, 'ERROR: Visit failed');
574
531
  }
575
532
  }
576
533
  // Otherwise, if the act is a wait for text:
@@ -588,8 +545,7 @@ exports.doActs = async (report, opts = {}) => {
588
545
  }
589
546
  // If the wait times out:
590
547
  catch(error) {
591
- // Quit.
592
- abortActs(localReport, actIndex);
548
+ // Report this.
593
549
  waitError(page, act, error, 'text in the URL');
594
550
  }
595
551
  }
@@ -612,8 +568,7 @@ exports.doActs = async (report, opts = {}) => {
612
568
  }
613
569
  // If the wait times out:
614
570
  catch(error) {
615
- // Quit.
616
- abortActs(localReport, actIndex);
571
+ // Report this.
617
572
  waitError(page, act, error, 'text in the title');
618
573
  }
619
574
  }
@@ -635,8 +590,7 @@ exports.doActs = async (report, opts = {}) => {
635
590
  }
636
591
  // If the wait times out:
637
592
  catch(error) {
638
- // Quit.
639
- abortActs(localReport, actIndex);
593
+ // Report this.
640
594
  waitError(page, act, error, 'text in the body');
641
595
  }
642
596
  }
@@ -650,13 +604,13 @@ exports.doActs = async (report, opts = {}) => {
650
604
  )
651
605
  // If the wait times out:
652
606
  .catch(async error => {
653
- // Report this and abort the job.
607
+ // Report this.
654
608
  console.log(`ERROR waiting for page to be ${act.which} (${error.message})`);
655
- addError(true, false, localReport, actIndex, `ERROR waiting for page to be ${act.which}`);
609
+ addError(true, false, tempReport, actIndex, `ERROR waiting for page to be ${act.which}`);
656
610
  });
657
611
  // If the wait succeeded:
658
612
  if (actIndex > -2) {
659
- // Add state data to the local report.
613
+ // Add state data to the temporary report.
660
614
  act.result = {
661
615
  success: true,
662
616
  state: act.which
@@ -739,7 +693,7 @@ exports.doActs = async (report, opts = {}) => {
739
693
  }
740
694
  // If no element satisfied the specifications:
741
695
  if (! act.result.found) {
742
- // Add the failure data to the local report.
696
+ // Add the failure data to the temporary report.
743
697
  act.result.success = false;
744
698
  act.result.error = 'exhausted';
745
699
  act.result.typeElementCount = selections.length;
@@ -752,7 +706,7 @@ exports.doActs = async (report, opts = {}) => {
752
706
  }
753
707
  // Otherwise, i.e. if there are too few such elements to make a match possible:
754
708
  else {
755
- // Add the failure data to the local report.
709
+ // Add the failure data to the temporary report.
756
710
  act.result.success = false;
757
711
  act.result.error = 'fewer';
758
712
  act.result.typeElementCount = selections.length;
@@ -761,7 +715,7 @@ exports.doActs = async (report, opts = {}) => {
761
715
  }
762
716
  // Otherwise, i.e. if there are no elements of the specified type:
763
717
  else {
764
- // Add the failure data to the local report.
718
+ // Add the failure data to the temporary report.
765
719
  act.result.success = false;
766
720
  act.result.error = 'none';
767
721
  act.result.typeElementCount = 0;
@@ -770,7 +724,7 @@ exports.doActs = async (report, opts = {}) => {
770
724
  }
771
725
  // Otherwise, i.e. if the page no longer exists:
772
726
  else {
773
- // Add the failure data to the local report.
727
+ // Add the failure data to the temporary report.
774
728
  act.result.success = false;
775
729
  act.result.error = 'gone';
776
730
  act.result.message = 'Page gone';
@@ -795,8 +749,8 @@ exports.doActs = async (report, opts = {}) => {
795
749
  }
796
750
  // If the move fails:
797
751
  catch(error) {
798
- // Add the error result to the act and abort the job.
799
- addError(true, false, localReport, actIndex, `ERROR: ${move} failed`);
752
+ // Report this.
753
+ addError(true, false, tempReport, actIndex, `ERROR: ${move} failed`);
800
754
  }
801
755
  if (act.result.success) {
802
756
  try {
@@ -880,16 +834,15 @@ exports.doActs = async (report, opts = {}) => {
880
834
  }
881
835
  // If the click or load failed:
882
836
  catch(error) {
883
- // Quit and add failure data to the local report.
837
+ // Report this.
884
838
  console.log(`ERROR clicking link (${errorStart(error)})`);
885
839
  act.result.success = false;
886
840
  act.result.error = 'unclickable';
887
841
  act.result.message = 'ERROR: click or load timed out';
888
- abortActs(localReport, actIndex);
889
842
  }
890
843
  // If the link click succeeded:
891
844
  if (! act.result.error) {
892
- // Add success data to the local report.
845
+ // Add success data to the temporary report.
893
846
  act.result.success = true;
894
847
  act.result.move = 'clicked';
895
848
  }
@@ -938,7 +891,7 @@ exports.doActs = async (report, opts = {}) => {
938
891
  }
939
892
  // Enter the text.
940
893
  await selection.type(what);
941
- localReport.jobData.presses += what.length;
894
+ tempReport.jobData.presses += what.length;
942
895
  act.result.success = true;
943
896
  act.result.move = 'entered';
944
897
  // If the input is a search input:
@@ -957,19 +910,18 @@ exports.doActs = async (report, opts = {}) => {
957
910
  }
958
911
  // Otherwise, i.e. if no match was found:
959
912
  else {
960
- // Quit and add failure data to the local report.
913
+ // RLeport this.
961
914
  act.result.success = false;
962
915
  act.result.error = 'absent';
963
916
  act.result.message = 'ERROR: specified element not found';
964
917
  console.log('ERROR: Specified element not found');
965
- abortActs(localReport, actIndex);
966
918
  }
967
919
  }
968
920
  // Otherwise, if the act is a keypress:
969
921
  else if (type === 'press') {
970
922
  // Identify the number of times to press the key.
971
923
  let times = 1 + (act.again || 0);
972
- localReport.jobData.presses += times;
924
+ tempReport.jobData.presses += times;
973
925
  const key = act.which;
974
926
  // Press the key.
975
927
  while (times--) {
@@ -1123,26 +1075,26 @@ exports.doActs = async (report, opts = {}) => {
1123
1075
  if (withItems) {
1124
1076
  act.result.items = items;
1125
1077
  }
1126
- // Add the totals to the local report.
1127
- localReport.jobData.presses += presses;
1128
- localReport.jobData.amountRead += amountRead;
1078
+ // Add the totals to the temporary report.
1079
+ tempReport.jobData.presses += presses;
1080
+ tempReport.jobData.amountRead += amountRead;
1129
1081
  }
1130
1082
  // Otherwise, i.e. if the act type is unknown:
1131
1083
  else {
1132
- // Add the error result to the act and abort the job.
1133
- addError(true, false, localReport, actIndex, 'ERROR: Invalid act type');
1084
+ // Report this.
1085
+ addError(true, false, tempReport, actIndex, 'ERROR: Invalid act type');
1134
1086
  }
1135
1087
  }
1136
1088
  // Otherwise, a page URL is required but does not exist, so:
1137
1089
  else {
1138
- // Add an error result to the act and abort the job.
1139
- addError(true, false, localReport, actIndex, 'ERROR: Page has no URL');
1090
+ // Report this.
1091
+ addError(true, false, tempReport, actIndex, 'ERROR: Page has no URL');
1140
1092
  }
1141
1093
  }
1142
1094
  // Otherwise, i.e. if no page exists:
1143
1095
  else {
1144
- // Add an error result to the act and abort the job.
1145
- addError(true, false, localReport, actIndex, 'ERROR: No page identified');
1096
+ // Report this.
1097
+ addError(true, false, tempReport, actIndex, 'ERROR: No page identified');
1146
1098
  }
1147
1099
  // Add the end time to the act.
1148
1100
  act.endTime = Date.now();
@@ -1156,20 +1108,20 @@ exports.doActs = async (report, opts = {}) => {
1156
1108
  // If the native results are not to be included in the report:
1157
1109
  if (standard === 'only') {
1158
1110
  // Remove them.
1159
- localReport.acts.forEach(act => {
1111
+ tempReport.acts.forEach(act => {
1160
1112
  if (act.result?.nativeResult) {
1161
1113
  delete act.result.nativeResult;
1162
1114
  }
1163
1115
  });
1164
1116
  }
1165
1117
  // If a catalog was created:
1166
- if (localReport.catalog) {
1167
- let {catalog} = localReport;
1118
+ if (tempReport.catalog) {
1119
+ let {catalog} = tempReport;
1168
1120
  // Get its element count.
1169
1121
  const elementCount = Object.keys(catalog).length;
1170
1122
  // Prune it, removing elements with no reported violations.
1171
- pruneCatalog(localReport);
1172
- ({catalog} = localReport);
1123
+ pruneCatalog(tempReport);
1124
+ ({catalog} = tempReport);
1173
1125
  // Get properties of the pruned catalog.
1174
1126
  const textCount = Object.values(catalog).filter(entry => entry.text).length;
1175
1127
  const linkableTextCount = Object.values(catalog).filter(entry => entry.textLinkable).length;
@@ -1186,7 +1138,7 @@ exports.doActs = async (report, opts = {}) => {
1186
1138
  },
1187
1139
  tools: {}
1188
1140
  };
1189
- const {acts} = localReport;
1141
+ const {acts} = tempReport;
1190
1142
  // For each act:
1191
1143
  for (const act of acts) {
1192
1144
  // If it is a test act:
@@ -1219,12 +1171,12 @@ exports.doActs = async (report, opts = {}) => {
1219
1171
  }
1220
1172
  }
1221
1173
  }
1222
- // Add the catalog data to the local report.
1223
- localReport.jobData.catalogData = catalogData;
1174
+ // Add the catalog data to the temporary report.
1175
+ tempReport.jobData.catalogData = catalogData;
1224
1176
  }
1225
1177
  }
1226
- // Delete the temporary local report file.
1178
+ // Delete the temporary temporary report file.
1227
1179
  await fs.rm(reportPath, {force: true});
1228
- // Return the local report.
1229
- return localReport;
1180
+ // Return the temporary report.
1181
+ return tempReport;
1230
1182
  };
@@ -73,9 +73,9 @@ const sendMessage = message => {
73
73
  );
74
74
  }
75
75
  };
76
- // Performs the tests of an act.
76
+ // Performs tests of a test act.
77
77
  const doTestAct = async (reportPath, actIndex) => {
78
- // Get the report from the temporary directory.
78
+ // Get the temporary report.
79
79
  const reportJSON = await fs.readFile(reportPath, 'utf8');
80
80
  const report = JSON.parse(reportJSON);
81
81
  // Get a reference to the act in the report.
package/procs/error.js CHANGED
@@ -12,14 +12,22 @@
12
12
  Handles errors.
13
13
  */
14
14
 
15
+ // IMPORTS
16
+
17
+ const {nowString} = require('./dateTime');
18
+
19
+ // FUNCTIONS
20
+
15
21
  // Reports a job being aborted.
16
- const abortActs = exports.abortActs = (report, actIndex) => {
22
+ const abortActs = (report, actIndex, message = '') => {
17
23
  // Add data on the aborted act to the report.
18
24
  report.jobData.abortTime = nowString();
19
- report.jobData.abortedAct = actIndex;
25
+ report.jobData.abortedAct = actIndex ?? 'none';
20
26
  report.jobData.aborted = true;
21
- // Report that the job is aborted.
22
- console.log(`ERROR: Job aborted on act ${actIndex}`);
27
+ report.jobData.abortMessage = message;
28
+ const abortedActString = typeof actIndex === 'number' ? ` on act ${actIndex}` : '';
29
+ // Log that the job is aborted.
30
+ console.log(`ERROR: Job aborted${abortedActString}`);
23
31
  };
24
32
  // Adds an error result to an act.
25
33
  exports.addError = (alsoLog, alsoAbort, report, actIndex, message) => {
@@ -48,6 +56,6 @@ exports.addError = (alsoLog, alsoAbort, report, actIndex, message) => {
48
56
  // If the job is to be aborted:
49
57
  if (alsoAbort) {
50
58
  // Add this to the report.
51
- abortActs(report, actIndex);
59
+ abortActs(report, actIndex, message);
52
60
  }
53
61
  };
package/procs/job.js CHANGED
@@ -175,7 +175,6 @@ exports.isValidJob = job => {
175
175
  id,
176
176
  strict,
177
177
  standard,
178
- observe,
179
178
  device,
180
179
  browserID,
181
180
  creationTimeStamp,
@@ -203,12 +202,6 @@ exports.isValidJob = job => {
203
202
  error: 'Bad job standard'
204
203
  };
205
204
  }
206
- if (typeof observe !== 'boolean') {
207
- return {
208
- isValid: false,
209
- error: 'Bad job observe'
210
- }
211
- }
212
205
  if (! isDeviceID(device.id)) {
213
206
  return {
214
207
  isValid: false,
package/procs/launch.js CHANGED
@@ -50,6 +50,7 @@ const errorWords = [
50
50
  ];
51
51
  // Seconds to wait between actions.
52
52
  const waits = Number(process.env.WAITS) ?? 0;
53
+ const abortAssertively = process.env.ABORT_ASSERTIVELY === 'true';
53
54
 
54
55
  // FUNCTIONS
55
56
 
@@ -80,6 +81,29 @@ const browserClose = exports.browserClose = async page => {
80
81
  }
81
82
  }
82
83
  };
84
+ // Normalizes a file URL in case it has the Windows path format.
85
+ const normalizeURL = url => {
86
+ // If a URL was provided:
87
+ if (url) {
88
+ // If it is that of a local file:
89
+ if (url.toLowerCase().startsWith('file:')) {
90
+ let path = url.replace(/^file:\/+/i, '');
91
+ path = path.replace(/\\/g, '/');
92
+ // Return the URL normalized.
93
+ return 'file:///' + path.replace(/^\//, '');
94
+ }
95
+ // Otherwise, i.e. if it is not that of a local file:
96
+ else {
97
+ // Return it.
98
+ return url;
99
+ }
100
+ }
101
+ // Otherwise, i.e. if no URL was provided:
102
+ else {
103
+ // Return this.
104
+ return undefined;
105
+ }
106
+ };
83
107
  // Visits a URL and returns the response of the server.
84
108
  const goTo = exports.goTo = async (report, page, url, timeout, waitUntil) => {
85
109
  // If the URL is a file path relative to the project root:
@@ -96,11 +120,11 @@ const goTo = exports.goTo = async (report, page, url, timeout, waitUntil) => {
96
120
  });
97
121
  report.jobData.visitLatency += Math.round((Date.now() - startTime) / 1000);
98
122
  const httpStatus = response.status();
99
- // If the response status was normal:
123
+ // If the response status was normal or the URL points to a local file:
100
124
  if ([200, 304].includes(httpStatus) || url.startsWith('file:')) {
101
125
  const actualURL = page.url();
102
- const actualNorm = actualURL.startsWith('file:') ? normalizeFile(actualURL) : actualURL;
103
- const urlNorm = url.startsWith('file:') ? normalizeFile(url) : url;
126
+ const actualNorm = actualURL.startsWith('file:') ? normalizeURL(actualURL) : actualURL;
127
+ const urlNorm = url.startsWith('file:') ? normalizeURL(url) : url;
104
128
  // If the browser was redirected in violation of a strictness requirement:
105
129
  if (report.strict && deSlash(actualNorm) !== deSlash(urlNorm)) {
106
130
  // Return an error.
@@ -151,11 +175,10 @@ const goTo = exports.goTo = async (report, page, url, timeout, waitUntil) => {
151
175
  // Otherwise, i.e. if the response status was otherwise abnormal:
152
176
  else {
153
177
  // Return an error.
154
- console.log(`ERROR: Visit to ${url} got status ${httpStatus}`);
155
178
  report.jobData.visitRejectionCount++;
156
179
  return {
157
180
  success: false,
158
- error: 'badStatus'
181
+ error: `ERROR: Visit to ${url} got status ${httpStatus}`
159
182
  };
160
183
  }
161
184
  }
@@ -165,7 +188,7 @@ const goTo = exports.goTo = async (report, page, url, timeout, waitUntil) => {
165
188
  }
166
189
  return {
167
190
  success: false,
168
- error: 'noVisit'
191
+ error: `ERROR visiting ${url} (${error.message.slice(0, 200)})`
169
192
  };
170
193
  }
171
194
  };
@@ -208,15 +231,13 @@ const launchOnce = async opts => {
208
231
  const {device} = report;
209
232
  const deviceID = device?.id;
210
233
  const browserID = tempBrowserID || report.browserID || '';
211
- const url = tempURL || report.target?.url || '';
234
+ const url = normalizeURL(tempURL || report.target?.url || '');
212
235
  let page;
213
236
  // If the specified browser and device types and URL are valid:
214
237
  if (isBrowserID(browserID) && isDeviceID(deviceID) && isURL(url)) {
215
- // Replace the report target URL with this URL.
238
+ // Replace the report target URL with the specified URL.
216
239
  report.target.url = url;
217
- // Create a browser of the specified or default type.
218
240
  const browserType = playwrightBrowsers[browserID];
219
- // Define the browser-option args, depending on the browser type and head-emulation level.
220
241
  const browserOptionArgs = [];
221
242
  if (browserID === 'chromium') {
222
243
  browserOptionArgs.push(
@@ -244,7 +265,7 @@ const launchOnce = async opts => {
244
265
  );
245
266
  }
246
267
  }
247
- // Define the browser options.
268
+ // Get the browser options.
248
269
  const browserOptions = {
249
270
  logger: {
250
271
  isEnabled: () => false,
@@ -462,34 +483,31 @@ const launchOnce = async opts => {
462
483
  throw new Error(`Navigation failed (${navResult.error})`);
463
484
  }
464
485
  }
465
- // If an error occurred:
486
+ // If the browser and page creation and navigation threw an error:
466
487
  catch(error) {
467
- // Report this.
468
- console.log(`ERROR launching or navigating (${error.message})`);
469
488
  // Close the browser and its context, if they exist.
470
489
  await browserClose(page);
471
- // Return a failure.
490
+ // Return the error.
472
491
  return {
473
492
  success: false,
474
493
  error: error.message
475
494
  };
476
495
  }
477
496
  }
478
- // If the launch and navigation succeeded, return the page.
497
+ // Otherwise, i.e. if the specified browser or device type or URL is invalid:
498
+ else {
499
+ // Return this.
500
+ return {
501
+ success: false,
502
+ error: 'Invalid browser, device type, or URL'
503
+ };
504
+ }
505
+ // If the browser and page creation and navigation succeeded, return the page.
479
506
  return {
480
507
  success: true,
481
508
  page
482
509
  };
483
510
  };
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
511
  // Manages browser launching and navigating and returns a page.
494
512
  exports.launch = async (opts = {}) => {
495
513
  const {
@@ -527,9 +545,9 @@ exports.launch = async (opts = {}) => {
527
545
  // Otherwise, i.e. if the launch or navigation failed:
528
546
  else {
529
547
  let retriesLeft = retries;
548
+ let {error} = launchResult;
530
549
  // As long as retries remain, decrement the allowed retry count and:
531
- while (retriesLeft--) {
532
- const {error} = launchResult;
550
+ while (retriesLeft) {
533
551
  // Prepare to wait 1 second before a retry.
534
552
  let waitSeconds = 1;
535
553
  // If the error was a visit failure due to rate limiting:
@@ -543,7 +561,7 @@ exports.launch = async (opts = {}) => {
543
561
  }
544
562
  // Report the wait.
545
563
  console.log(
546
- `WARNING: Waiting ${waitSeconds} sec. before retrying (retries left: ${retries})`
564
+ `WARNING: Waiting ${waitSeconds} sec. before retrying (retries left: ${retriesLeft--})`
547
565
  );
548
566
  // Wait as specified.
549
567
  await wait(1000 * waitSeconds);
@@ -567,14 +585,17 @@ exports.launch = async (opts = {}) => {
567
585
  }
568
586
  // Otherwise, i.e. if the launch or navigation failed:
569
587
  else {
588
+ error = launchResult.error;
570
589
  // Report this.
571
- console.log(`WARNING: Retry failed; retries left: ${retries}`);
590
+ console.log(`WARNING: Retry failed (${error})`);
572
591
  }
573
592
  }
574
593
  // If the retries were exhausted:
575
- if (retriesLeft === -1) {
576
- // Report this.
577
- addError(true, false, report, actIndex, 'ERROR: No retries left');
594
+ if (! retriesLeft) {
595
+ // Report this and, if so configured, that the job was aborted.
596
+ addError(
597
+ true, abortAssertively, report, actIndex, `Launch or navigation failed; retries exhausted`
598
+ );
578
599
  }
579
600
  // Return a failure.
580
601
  return null;
@@ -582,13 +603,13 @@ exports.launch = async (opts = {}) => {
582
603
  }
583
604
  // Otherwise, i.e. if the report is invalid:
584
605
  else {
585
- // Report this.
606
+ // Report this and that the job was aborted.
586
607
  addError(
587
608
  true,
588
- false,
609
+ true,
589
610
  report,
590
611
  actIndex,
591
- `ERROR: Cannot launch browser for invalid job (${jobValidation.error})`
612
+ `ERROR: Job invalid (${jobValidation.error})`
592
613
  );
593
614
  // Return a failure.
594
615
  return null;
package/run.js CHANGED
@@ -48,7 +48,7 @@ exports.doJob = async (job, opts = {}) => {
48
48
  console.log(`ERROR: ${jobInvalidity.error}`);
49
49
  jobData.aborted = true;
50
50
  jobData.abortedAct = null;
51
- jobData.abortError = jobInvalidity.error;
51
+ jobData.abortMessage = jobInvalidity.error;
52
52
  }
53
53
  // Otherwise, i.e. if it is valid:
54
54
  else {
@@ -69,6 +69,8 @@ exports.doJob = async (job, opts = {}) => {
69
69
  visitRejectionCount: 0,
70
70
  aborted: false,
71
71
  abortedAct: null,
72
+ abortTime: '',
73
+ abortMessage: '',
72
74
  presses: 0,
73
75
  amountRead: 0,
74
76
  toolTimes: {},
package/testaro/linkUl.js CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  /*
11
11
  linkUl
12
- This test reports failures to underline inline links. Underlining and color are the traditional style properties that identify links. Lists of links containing only links may be recognizable without underlines, but other links are difficult or impossible to distinguish visually from surrounding text if not underlined. Underlining adjacent links only on hover provides an indicator valuable only to mouse users, and even they must traverse the text with a mouse merely to discover which passages are links.
12
+ This test reports failures to underline links that have adjacent text. Underlining and color are the traditional style properties that identify links. A link whose text is the entire text of its block may be recognizable without underlining, but otherwise links are difficult or impossible to distinguish visually from surrounding text if not underlined. Underlining links only on hover provides an indicator valuable only to mouse users, and even they must traverse the text with a mouse merely to discover which passages are links.
13
13
  */
14
14
 
15
15
  // IMPORTS
@@ -21,24 +21,28 @@ const {doTest} = require('../procs/testaro');
21
21
  // Runs the test and returns the result.
22
22
  exports.reporter = async (page, catalog, withItems) => {
23
23
  const getBadWhat = element => {
24
- const liAncestor = element.closest('li');
25
- // If the element is not the only link inside a list item:
26
- if (! (liAncestor && liAncestor.getElementsByTagName('a').length === 1)) {
27
- const styleDec = window.getComputedStyle(element);
28
- const {textDecoration} = styleDec;
29
- // If the element text is not underlined:
30
- if (! textDecoration.includes('underline')) {
31
- const styleDec = window.getComputedStyle(element);
32
- const {display} = styleDec;
33
- // If the element has does not have a block display style:
34
- if (display !== 'block') {
35
- // Return a violation description.
36
- return 'Element is not a list item but is not underlined';
37
- }
24
+ let {display, textDecorationLine} = window.getComputedStyle(element);
25
+ // If the element is not underlined:
26
+ if (! textDecorationLine.includes('underline')) {
27
+ let ancestor = element;
28
+ // Get its closest non-inline ancestor.
29
+ while (display === 'inline') {
30
+ ancestor = ancestor.parentElement;
31
+ display = ancestor ? window.getComputedStyle(ancestor).display : null;
32
+ }
33
+ // Removes superfluous whitespace from a string.
34
+ const compact = string => string?.replace(/\s+/g, ' ')?.trim();
35
+ // If the rendered compacted text of that ancestor includes more than that of the element:
36
+ if (
37
+ compact(element.textContent) !== compact(ancestor?.textContent)
38
+ && compact(element.innerText) !== compact(ancestor?.innerText)
39
+ ) {
40
+ // Return a violation description.
41
+ return 'Element has adjacent text but is not underlined';
38
42
  }
39
43
  }
40
44
  };
41
- const whats = 'Links that are not list items are not underlined';
45
+ const whats = 'Links with adjacent text are not underlined';
42
46
  return await doTest(
43
47
  page, catalog, withItems, 'linkUl', 'body a', whats, 1, getBadWhat.toString()
44
48
  );
package/tests/testaro.js CHANGED
@@ -501,11 +501,7 @@ exports.reporter = async (page, report, actIndex) => {
501
501
  && ['y', 'n'].includes(ruleSpec[0])
502
502
  && ruleSpec.slice(1).every(ruleID => allRuleIDs.includes(ruleID))
503
503
  ) {
504
- // Wait 1 second to prevent out-of-order logging with granular reporting.
505
- await wait(1000);
506
- // Get the rules to be tested for and their execution order.
507
- // 'y' = include-list: run exactly the rules in ruleSpec.slice(1).
508
- // 'n' = exclude-list: run all defaultOn rules EXCEPT those in ruleSpec.slice(1).
504
+ // Get the rules to be (y) or not to be (n) tested for and their execution order.
509
505
  const excludeIDs = ruleSpec.slice(1);
510
506
  const jobRuleIDs = ruleSpec[0] === 'y'
511
507
  ? excludeIDs
@@ -23,7 +23,6 @@
23
23
  ],
24
24
  "sources": {},
25
25
  "standard": "only",
26
- "observe": false,
27
26
  "sendReportTo": "http://localhost:3007/api",
28
27
  "timeStamp": "240101T1500",
29
28
  "creationTimeStamp": "240101T1200"
@@ -26,7 +26,6 @@ const job = {
26
26
  what: '',
27
27
  strict: true,
28
28
  standard: 'only',
29
- observe: false,
30
29
  device: {
31
30
  id: 'default',
32
31
  windowOptions: {
@@ -23,7 +23,6 @@
23
23
  ],
24
24
  "sources": {},
25
25
  "standard": "only",
26
- "observe": false,
27
26
  "timeStamp": "240101T1500",
28
27
  "creationTimeStamp": "240101T1200",
29
28
  "sendReportTo": ""