testaro 28.1.8 → 28.2.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
@@ -826,13 +826,12 @@ You may store environment variables in an untracked `.env` file if you wish, and
826
826
  ```conf
827
827
  URL_INJECT=yes
828
828
  WAVE_KEY=yourwavekey
829
- PROTOCOL=https
830
- JOB_URL=yourserver.tld/job
831
- REPORT_URL=yourserver.tld/report
829
+ JOB_URLs=https://yourserver.tld/job+http://localhost:3004/testapp
832
830
  JOBDIR=../testing/jobs/ThisWorkstation
833
831
  REPORTDIR=../testing/reports
834
832
  AGENT=ThisWorkstation
835
833
  DEBUG=false
834
+ WAITS=0
836
835
  ```
837
836
 
838
837
  ## Validation
package/call.js CHANGED
@@ -70,14 +70,14 @@ const callWatch = async (isDirWatch, isForever, interval, watchee = null) => {
70
70
  if (fn === 'run' && fnArgs.length === 1) {
71
71
  callRun(fnArgs)
72
72
  .then(() => {
73
- console.log('Execution completed');
73
+ console.log('Execution completed\n');
74
74
  process.exit(0);
75
75
  });
76
76
  }
77
77
  else if (fn === 'watch' && fnArgs.length === 3) {
78
78
  callWatch(... fnArgs)
79
79
  .then(() => {
80
- console.log('Execution completed');
80
+ console.log('Execution completed\n');
81
81
  process.exit(0);
82
82
  });
83
83
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "28.1.8",
3
+ "version": "28.2.0",
4
4
  "description": "Run 960 web accessibility tests from 9 tools and get a standardized report",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/procs/nav.js CHANGED
@@ -127,7 +127,6 @@ const browserClose = async () => {
127
127
  }
128
128
  await browser.close();
129
129
  browser = null;
130
- console.log(`${browserType} browser closed`);
131
130
  }
132
131
  };
133
132
  // Launches a browser, navigates to a URL, and returns the status.
@@ -41,91 +41,91 @@ const getIdentifiers = code => {
41
41
  };
42
42
  // Specifies conversions of rule IDs of aslint based on what substrings.
43
43
  const aslintData = {
44
- 'misused-required-attribute': [
45
- ['not needed', 'misused-required-attributeR']
44
+ 'misused_required_attribute': [
45
+ ['not needed', 'misused_required_attributeR']
46
46
  ],
47
- 'accessible-svg': [
48
- ['associated', 'accessible-svgI'],
49
- ['tabindex', 'accessible-svgT']
47
+ 'accessible_svg': [
48
+ ['associated', 'accessible_svgI'],
49
+ ['tabindex', 'accessible_svgT']
50
50
  ],
51
- 'audio-alternative': [
52
- ['track', 'audio-alternativeT'],
53
- ['alternative', 'audio-alternativeA'],
54
- ['bgsound', 'audio-alternativeB']
51
+ 'audio_alternative': [
52
+ ['track', 'audio_alternativeT'],
53
+ ['alternative', 'audio_alternativeA'],
54
+ ['bgsound', 'audio_alternativeB']
55
55
  ],
56
- 'table-missing-description': [
57
- ['describedby', 'associated', 'table-missing-descriptionDM'],
58
- ['labeledby', 'associated', 'table-missing-descriptionLM'],
59
- ['caption', 'not been defined', 'table-missing-descriptionC'],
60
- ['summary', 'empty', 'table-missing-descriptionS'],
61
- ['describedby', 'empty', 'table-missing-descriptionDE'],
62
- ['labeledby', 'empty', 'table-missing-descriptionLE'],
63
- ['caption', 'no content', 'table-missing-descriptionE']
56
+ 'table_missing_description': [
57
+ ['describedby', 'associated', 'table_missing_descriptionDM'],
58
+ ['labeledby', 'associated', 'table_missing_descriptionLM'],
59
+ ['caption', 'not been defined', 'table_missing_descriptionC'],
60
+ ['summary', 'empty', 'table_missing_descriptionS'],
61
+ ['describedby', 'empty', 'table_missing_descriptionDE'],
62
+ ['labeledby', 'empty', 'table_missing_descriptionLE'],
63
+ ['caption', 'no content', 'table_missing_descriptionE']
64
64
  ],
65
- 'label-implicitly-associated': [
66
- ['only whice spaces', 'label-implicitly-associatedW'],
67
- ['more than one', 'label-implicitly-associatedM']
65
+ 'label_implicitly_associated': [
66
+ ['only whice spaces', 'label_implicitly_associatedW'],
67
+ ['more than one', 'label_implicitly_associatedM']
68
68
  ],
69
- 'label-inappropriate-association': [
70
- ['Missing', 'label-inappropriate-associationM'],
71
- ['non-form', 'label-inappropriate-associationN']
69
+ 'label_inappropriate_association': [
70
+ ['Missing', 'label_inappropriate_associationM'],
71
+ ['non-form', 'label_inappropriate_associationN']
72
72
  ],
73
- 'table-row-and-column-headers': [
74
- ['headers', 'table-row-and-column-headersRC'],
75
- ['Content', 'table-row-and-column-headersB'],
76
- ['head of the columns', 'table-row-and-column-headersH']
73
+ 'table_row_and_column_headers': [
74
+ ['headers', 'table_row_and_column_headersRC'],
75
+ ['Content', 'table_row_and_column_headersB'],
76
+ ['head of the columns', 'table_row_and_column_headersH']
77
77
  ],
78
- 'color-contrast-state-pseudo-classes-abstract': [
79
- ['position: fixed', 'color-contrast-state-pseudo-classes-abstractF'],
80
- ['transparent', 'color-contrast-state-pseudo-classes-abstractB'],
81
- ['least 3:1', 'color-contrast-state-pseudo-classes-abstract3'],
82
- ['least 4.5:1', 'color-contrast-state-pseudo-classes-abstract4']
78
+ 'color_contrast_state_pseudo_classes_abstract': [
79
+ ['position: fixed', 'color_contrast_state_pseudo_classes_abstractF'],
80
+ ['transparent', 'color_contrast_state_pseudo_classes_abstractB'],
81
+ ['least 3:1', 'color_contrast_state_pseudo_classes_abstract3'],
82
+ ['least 4.5:1', 'color_contrast_state_pseudo_classes_abstract4']
83
83
  ],
84
- 'color-contrast-state-pseudo-classes-active': [
85
- ['position: fixed', 'color-contrast-state-pseudo-classes-abstractF'],
86
- ['transparent', 'color-contrast-state-pseudo-classes-abstractB'],
87
- ['least 3:1', 'color-contrast-state-pseudo-classes-abstract3'],
88
- ['least 4.5:1', 'color-contrast-state-pseudo-classes-abstract4']
84
+ 'color_contrast_state_pseudo_classes_active': [
85
+ ['position: fixed', 'color_contrast_state_pseudo_classes_abstractF'],
86
+ ['transparent', 'color_contrast_state_pseudo_classes_abstractB'],
87
+ ['least 3:1', 'color_contrast_state_pseudo_classes_abstract3'],
88
+ ['least 4.5:1', 'color_contrast_state_pseudo_classes_abstract4']
89
89
  ],
90
- 'color-contrast-state-pseudo-classes-focus': [
91
- ['position: fixed', 'color-contrast-state-pseudo-classes-abstractF'],
92
- ['transparent', 'color-contrast-state-pseudo-classes-abstractB'],
93
- ['least 3:1', 'color-contrast-state-pseudo-classes-abstract3'],
94
- ['least 4.5:1', 'color-contrast-state-pseudo-classes-abstract4']
90
+ 'color_contrast_state_pseudo_classes_focus': [
91
+ ['position: fixed', 'color_contrast_state_pseudo_classes_abstractF'],
92
+ ['transparent', 'color_contrast_state_pseudo_classes_abstractB'],
93
+ ['least 3:1', 'color_contrast_state_pseudo_classes_abstract3'],
94
+ ['least 4.5:1', 'color_contrast_state_pseudo_classes_abstract4']
95
95
  ],
96
- 'color-contrast-state-pseudo-classes-hover': [
97
- ['position: fixed', 'color-contrast-state-pseudo-classes-abstractF'],
98
- ['transparent', 'color-contrast-state-pseudo-classes-abstractB'],
99
- ['least 3:1', 'color-contrast-state-pseudo-classes-abstract3'],
100
- ['least 4.5:1', 'color-contrast-state-pseudo-classes-abstract4']
96
+ 'color_contrast_state_pseudo_classes_hover': [
97
+ ['position: fixed', 'color_contrast_state_pseudo_classes_abstractF'],
98
+ ['transparent', 'color_contrast_state_pseudo_classes_abstractB'],
99
+ ['least 3:1', 'color_contrast_state_pseudo_classes_abstract3'],
100
+ ['least 4.5:1', 'color_contrast_state_pseudo_classes_abstract4']
101
101
  ],
102
- 'color-contrast-aaa': [
103
- ['transparent', 'color-contrast-aaaB'],
104
- ['least 4.5:1', 'color-contrast-aaa4'],
105
- ['least 7:1', 'color-contrast-aaa7']
102
+ 'color_contrast_aaa': [
103
+ ['transparent', 'color_contrast_aaaB'],
104
+ ['least 4.5:1', 'color_contrast_aaa4'],
105
+ ['least 7:1', 'color_contrast_aaa7']
106
106
  ],
107
107
  'animation': [
108
108
  ['duration', 'animationD'],
109
109
  ['iteration', 'animationI'],
110
110
  ['mechanism', 'animationM']
111
111
  ],
112
- 'page-title': [
113
- ['empty', 'page-titleN'],
114
- ['not identify', 'page-titleU']
112
+ 'page_title': [
113
+ ['empty', 'page_titleN'],
114
+ ['not identify', 'page_titleU']
115
115
  ],
116
- 'aria-labelledby-association': [
117
- ['exist', 'aria-labelledby-associationN'],
118
- ['empty', 'aria-labelledby-associationE']
116
+ 'aria_labelledby_association': [
117
+ ['exist', 'aria_labelledby_associationN'],
118
+ ['empty', 'aria_labelledby_associationE']
119
119
  ],
120
- 'html-lang-attr': [
121
- ['parameters', 'html-lang-attrP'],
122
- ['nothing', 'html-lang-attrN'],
123
- ['empty', 'html-lang-attrE']
120
+ 'html_lang_attr': [
121
+ ['parameters', 'html_lang_attrP'],
122
+ ['nothing', 'html_lang_attrN'],
123
+ ['empty', 'html_lang_attrE']
124
124
  ],
125
- 'missing-label': [
126
- ['associated', 'missing-labelI'],
127
- ['defined', 'missing-labelN'],
128
- ['multiple labels', 'missing-labelM']
125
+ 'missing_label': [
126
+ ['associated', 'missing_labelI'],
127
+ ['defined', 'missing_labelN'],
128
+ ['multiple labels', 'missing_labelM']
129
129
  ],
130
130
  'orientation': [
131
131
  ['loaded', 'orientationT']
@@ -353,47 +353,65 @@ const convert = (toolName, result, standardResult) => {
353
353
  }
354
354
  // aslint
355
355
  else if (toolName === 'aslint' && result.summary && result.summary.byIssueType) {
356
+ // Get the totals.
356
357
  standardResult.totals = [
357
358
  result.summary.byIssueType.warning, 0, 0, result.summary.byIssueType.error
358
359
  ];
360
+ // For each rule:
359
361
  Object.keys(result.rules).forEach(ruleID => {
360
- const ruleResults = result.rules[ruleID].results;
361
- if (ruleResults && ruleResults.length) {
362
- ruleResults.forEach(ruleResult => {
363
- const what = ruleResult.message.actual.description;
364
- const {issueType} = result.rules[ruleID];
365
- const xpath = ruleResult.element && ruleResult.element.xpath || '';
366
- const tagName = xpath && xpath.replace(/^.*\//, '').replace(/[^-\w].*$/, '').toUpperCase()
367
- || '';
368
- const excerpt = ruleResult.element && ruleResult.element.html || '';
369
- const idDraft = excerpt && excerpt.replace(/^[^[>]+id="/, 'id=').replace(/".*$/, '');
370
- const id = idDraft && idDraft.length > 3 && idDraft.startsWith('id=')
371
- ? idDraft.slice(3)
372
- : '';
373
- const ruleData = aslintData[ruleID];
374
- if (ruleData) {
375
- const changer = ruleData.find(
376
- specs => specs.slice(0, -1).every(matcher => what.includes(matcher))
377
- );
378
- if (changer) {
379
- ruleID = changer[1];
362
+ // If it has a valid issue type:
363
+ const {issueType} = result.rules[ruleID];
364
+ if (issueType && ['warning', 'error'].includes(issueType)) {
365
+ // If there are any violations:
366
+ const ruleResults = result.rules[ruleID].results;
367
+ if (ruleResults && ruleResults.length) {
368
+ // For each violation:
369
+ ruleResults.forEach(ruleResult => {
370
+ // If it has a description:
371
+ if (
372
+ ruleResult.message
373
+ && ruleResult.message.actual
374
+ && ruleResult.message.actual.description
375
+ ) {
376
+
377
+ const what = ruleResult.message.actual.description;
378
+ // Get its differentiated ID if any.
379
+ const ruleData = aslintData[ruleID];
380
+ let finalRuleID = ruleID;
381
+ if (ruleData) {
382
+ const changer = ruleData.find(
383
+ specs => specs.slice(0, -1).every(matcher => what.includes(matcher))
384
+ );
385
+ if (changer) {
386
+ finalRuleID = changer[changer.length - 1];
387
+ }
388
+ }
389
+ const xpath = ruleResult.element && ruleResult.element.xpath || '';
390
+ const tagName = xpath
391
+ && xpath.replace(/^.*\//, '').replace(/[^-\w].*$/, '').toUpperCase()
392
+ || '';
393
+ const excerpt = ruleResult.element && ruleResult.element.html || '';
394
+ const idDraft = excerpt && excerpt.replace(/^[^[>]+id="/, 'id=').replace(/".*$/, '');
395
+ const id = idDraft && idDraft.length > 3 && idDraft.startsWith('id=')
396
+ ? idDraft.slice(3)
397
+ : '';
398
+ const instance = {
399
+ ruleID: finalRuleID,
400
+ what,
401
+ ordinalSeverity: ['warning', 0, 0, 'error'].indexOf(issueType),
402
+ tagName,
403
+ id,
404
+ location: {
405
+ doc: 'dom',
406
+ type: 'xpath',
407
+ spec: xpath
408
+ },
409
+ excerpt
410
+ };
411
+ standardResult.instances.push(instance);
380
412
  }
381
- }
382
- const instance = {
383
- ruleID,
384
- what,
385
- ordinalSeverity: ['warning', 0, 0, 'error'].indexOf(issueType),
386
- tagName,
387
- id,
388
- location: {
389
- doc: 'dom',
390
- type: 'xpath',
391
- spec: xpath
392
- },
393
- excerpt
394
- };
395
- standardResult.instances.push(instance);
396
- });
413
+ });
414
+ }
397
415
  }
398
416
  });
399
417
  }
package/run.js CHANGED
@@ -13,6 +13,9 @@ const {actSpecs} = require('./actSpecs');
13
13
  const {browserClose, getNonce, goTo, launch} = require('./procs/nav');
14
14
  // Module to standardize report formats.
15
15
  const {standardize} = require('./procs/standardize');
16
+ // HTTP and HTTPS clients.
17
+ const http = require('http');
18
+ const https = require('https');
16
19
 
17
20
  // ########## CONSTANTS
18
21
 
@@ -45,8 +48,10 @@ const tests = {
45
48
  testaro: 'Testaro',
46
49
  wave: 'WAVE',
47
50
  };
48
- // Items that may be waited for.
49
- const waitables = ['url', 'title', 'body'];
51
+ // Observation items.
52
+ const httpClient = require('http');
53
+ const httpsClient = require('https');
54
+ const agent = process.env.AGENT;
50
55
 
51
56
  // ########## VARIABLES
52
57
 
@@ -448,12 +453,6 @@ const doActs = async (report, actIndex, page) => {
448
453
  // Report this.
449
454
  console.log('ERROR: Job aborted');
450
455
  };
451
- process.on('message', message => {
452
- if (message === 'interrupt') {
453
- console.log('ERROR: Terminal interrupted doActs');
454
- process.exit();
455
- }
456
- });
457
456
  const {acts} = report;
458
457
  // If any more acts are to be performed:
459
458
  if (actIndex > -1 && actIndex < acts.length) {
@@ -471,7 +470,26 @@ const doActs = async (report, actIndex, page) => {
471
470
  }
472
471
  }
473
472
  const whichSuffix = actInfo ? ` (${actInfo})` : '';
474
- console.log(`>>>> ${act.type}${whichSuffix}`);
473
+ // If granular reporting has been specified:
474
+ let granularSuffix = '';
475
+ if (report.observe) {
476
+ // Notify the server of the act.
477
+ const observer = report.sources.sendReportTo.replace(/report$/, 'granular');
478
+ const whoParams = `agent=${agent}&jobID=${report.id || ''}`;
479
+ const actParams = `act=${act.type}&which=${act.which || ''}`;
480
+ const wholeURL = `${observer}?${whoParams}&${actParams}`;
481
+ const client = wholeURL.startsWith('https://') ? httpsClient : httpClient;
482
+ const request = client.request(wholeURL);
483
+ // If the notification threw an error:
484
+ request.on('error', error => {
485
+ // Report the error.
486
+ const errorMessage = `ERROR notifying the server of an act`;
487
+ console.log(`${errorMessage} (${error.message})`);
488
+ });
489
+ request.end();
490
+ granularSuffix = ' with notice to server';
491
+ console.log(`>>>> ${act.type}${whichSuffix}${granularSuffix}`);
492
+ }
475
493
  // Increment the count of acts performed.
476
494
  actCount++;
477
495
  act.startTime = Date.now();
@@ -1266,6 +1284,12 @@ exports.doJob = async report => {
1266
1284
  report.jobData.presses = 0;
1267
1285
  report.jobData.amountRead = 0;
1268
1286
  report.jobData.toolTimes = {};
1287
+ process.on('message', message => {
1288
+ if (message === 'interrupt') {
1289
+ console.log('ERROR: Terminal interrupted the job');
1290
+ process.exit();
1291
+ }
1292
+ });
1269
1293
  // Recursively perform the acts.
1270
1294
  await doActs(report, 0, null);
1271
1295
  // Add the end time and duration to the report.
package/tests/testaro.js CHANGED
@@ -12,17 +12,25 @@ const fs = require('fs/promises');
12
12
 
13
13
  // ######## CONSTANTS
14
14
 
15
- const evalRules = {
15
+ const futureEvalRules = {
16
16
  adbID: 'elements with ambiguous or missing referenced descriptions',
17
+ altScheme: 'img elements with alt attributes having URLs as their entire values',
18
+ captionLoc: 'caption elements that are not first children of table elements',
19
+ datalistRef: 'elements with ambiguous or missing referenced datalist elements',
20
+ imageLink: 'links with image files as their destinations',
21
+ legendLoc: 'legend elements that are not first children of fieldset elements',
22
+ optRoleSel: 'Non-option elements with option roles that have no aria-selected attributes',
23
+ phOnly: 'input elements with placeholders but no accessible names',
24
+ secHeading: 'headings that violate the logical level order in their sectioning containers',
25
+ textSem: 'semantically vague elements i, b, and/or small',
26
+ };
27
+ const evalRules = {
17
28
  allCaps: 'leaf elements with entirely upper-case text longer than 7 characters',
18
29
  allHidden: 'page that is entirely or mostly hidden',
19
30
  allSlanted: 'leaf elements with entirely italic or oblique text longer than 39 characters',
20
- altScheme: 'img elements with alt attributes having URLs as their entire values',
21
31
  autocomplete: 'name and email inputs without autocomplete attributes',
22
32
  bulk: 'large count of visible elements',
23
33
  buttonMenu: 'nonstandard keyboard navigation between items of button-controlled menus',
24
- captionLoc: 'caption elements that are not first children of table elements',
25
- datalistRef: 'elements with ambiguous or missing referenced datalist elements',
26
34
  distortion: 'distorted text',
27
35
  docType: 'document without a doctype property',
28
36
  dupAtt: 'elements with duplicate attributes',
@@ -37,9 +45,7 @@ const evalRules = {
37
45
  hover: 'hover-caused content changes',
38
46
  hovInd: 'hover indication nonstandard',
39
47
  hr: 'hr element instead of styles used for vertical segmentation',
40
- imageLink: 'links with image files as their destinations',
41
48
  labClash: 'labeling inconsistencies',
42
- legendLoc: 'legend elements that are not first children of fieldset elements',
43
49
  lineHeight: 'text with a line height less than 1.5 times its font size',
44
50
  linkExt: 'links that automatically open new windows',
45
51
  linkAmb: 'links with identical texts but different destinations',
@@ -51,16 +57,12 @@ const evalRules = {
51
57
  motion: 'motion without user request',
52
58
  nonTable: 'table elements used for layout',
53
59
  opFoc: 'Operable elements that are not Tab-focusable',
54
- optRoleSel: 'Non-option elements with option roles that have no aria-selected attributes',
55
- phOnly: 'input elements with placeholders but no accessible names',
56
60
  pseudoP: 'adjacent br elements suspected of nonsemantically simulating p elements',
57
61
  radioSet: 'radio buttons not grouped into standard field sets',
58
62
  role: 'invalid and native-replacing explicit roles',
59
- secHeading: 'headings that violate the logical level order in their sectioning containers',
60
63
  styleDiff: 'style inconsistencies',
61
64
  tabNav: 'nonstandard keyboard navigation between elements with the tab role',
62
65
  targetSize: 'buttons, inputs, and non-inline links smaller than 44 pixels wide and high',
63
- textSem: 'semantically vague elements i, b, and/or small',
64
66
  titledEl: 'title attributes on inappropriate elements',
65
67
  zIndex: 'non-default Z indexes'
66
68
  };
@@ -8,7 +8,6 @@ const fs = require('fs/promises');
8
8
  // CONSTANTS
9
9
 
10
10
  // Override cycle environment variables with validation-specific ones.
11
- process.env.PROTOCOL = 'http';
12
11
  process.env.JOBDIR = `${__dirname}/../watch`;
13
12
  process.env.REPORTDIR = `${__dirname}/../../temp`;
14
13
  const jobID = '00000-simple-example';
@@ -8,13 +8,11 @@ const fs = require('fs/promises');
8
8
  // CONSTANTS
9
9
 
10
10
  // Override cycle environment variables with validation-specific ones.
11
- process.env.PROTOCOL = 'http';
12
11
  const jobDir = `${__dirname}/../jobs/todo`;
13
- process.env.JOB_URL = 'localhost:3007/api/job';
14
- process.env.REPORT_URL = 'localhost:3007/api';
12
+ process.env.JOB_URL = 'http://localhost:3007/api/job';
15
13
  process.env.AGENT = 'testarauth';
16
14
  const {cycle} = require('../../watch');
17
- const client = require(process.env.PROTOCOL);
15
+ const client = require('http');
18
16
  const jobID = '00000-simple-example';
19
17
 
20
18
  // OPERATION
package/watch.js CHANGED
@@ -11,15 +11,17 @@ require('dotenv').config();
11
11
  const fs = require('fs/promises');
12
12
  // Module to perform tests.
13
13
  const {doJob} = require('./run');
14
+ // HTTP and HTTPS clients.
15
+ const http = require('http');
16
+ const https = require('https');
14
17
 
15
18
  // ########## CONSTANTS
16
19
 
17
- const protocol = process.env.PROTOCOL || 'https';
18
- const client = require(protocol);
19
- const jobURL = process.env.JOB_URL;
20
+ const httpClient = require('http');
21
+ const httpsClient = require('https');
22
+ const jobURLs = process.env.JOB_URLs;
20
23
  const agent = process.env.AGENT;
21
24
  const jobDir = process.env.JOBDIR;
22
- const reportURL = process.env.REPORT_URL;
23
25
  const reportDir = process.env.REPORTDIR;
24
26
 
25
27
  // ########## FUNCTIONS
@@ -55,48 +57,94 @@ const checkDirJob = async watchee => {
55
57
  return {};
56
58
  }
57
59
  };
58
- // Checks for and returns a network job.
60
+ // Checks for and, if obtained, returns a network job.
59
61
  const checkNetJob = async watchee => {
60
- const watchJobURL = watchee || jobURL;
61
- const job = await new Promise(resolve => {
62
- const wholeURL = `${protocol}://${watchJobURL}?agent=${agent}`;
63
- const request = client.request(wholeURL, response => {
64
- const chunks = [];
65
- response.on('data', chunk => {
66
- chunks.push(chunk);
67
- });
68
- response.on('end', () => {
69
- try {
70
- const jobJSON = chunks.join('');
71
- const job = JSON.parse(jobJSON);
72
- // Return it.
73
- resolve(job);
74
- }
75
- catch(error) {
76
- resolve({
77
- error: 'ERROR: Response was not JSON',
62
+ let watchJobURLs = [watchee];
63
+ if (! watchJobURLs[0]) {
64
+ watchJobURLs = jobURLs
65
+ .split('+')
66
+ .map(url => [Math.random(), url])
67
+ .sort((a, b) => a[0] - b[0])
68
+ .map(pair => pair[1]);
69
+ }
70
+ // For each watchee:
71
+ for (const watchJobURL of watchJobURLs) {
72
+ const job = await new Promise(resolve => {
73
+ // Request a job from it.
74
+ const wholeURL = `${watchJobURL}?agent=${agent}`;
75
+ const client = wholeURL.startsWith('https://') ? httpsClient : httpClient;
76
+ const request = client.request(wholeURL, {timeout: 1000}, response => {
77
+ const chunks = [];
78
+ response.on('data', chunk => {
79
+ chunks.push(chunk);
80
+ })
81
+ // When response arrives:
82
+ .on('end', () => {
83
+ // If the response was JSON-formatted:
84
+ try {
85
+ const jobJSON = chunks.join('');
86
+ const job = JSON.parse(jobJSON);
87
+ // Make it the response of the watchee.
88
+ return resolve(job);
89
+ }
90
+ // Otherwise, i.e. if the response was not JSON-formatted:
91
+ catch(error) {
92
+ // Make an error report the response of the watchee.
93
+ const errorMessage = `ERROR: Response of ${watchJobURL} was not JSON`;
94
+ console.log(errorMessage);
95
+ return resolve({
96
+ error: errorMessage,
97
+ message: error.message,
98
+ status: response.statusCode
99
+ });
100
+ }
101
+ })
102
+ .on('error', error => {
103
+ return resolve({
104
+ error: 'ERROR getting network job',
78
105
  message: error.message,
79
106
  status: response.statusCode
80
107
  });
81
- }
108
+ });
82
109
  });
110
+ // If the check threw an error:
111
+ request.on('error', error => {
112
+ // Make an error report the response of the watchee.
113
+ const errorMessage = `ERROR checking ${watchJobURL} for a network job`;
114
+ console.log(`${errorMessage} (${error.message})`);
115
+ return resolve({
116
+ error: errorMessage,
117
+ message: error.message
118
+ });
119
+ })
120
+ .on('timeout', () => {
121
+ const errorMessage = `ERROR: Request to ${watchJobURL} timed out`;
122
+ console.log(errorMessage);
123
+ return resolve({
124
+ error: errorMessage
125
+ });
126
+ });
127
+ request.end();
83
128
  });
84
- request.on('error', error => {
85
- console.log(`ERROR checking for a network job (${error.message})`);
86
- resolve({});
87
- });
88
- request.end();
89
- });
90
- if (job.id) {
91
- console.log(`Network job ${job.id} received (${nowString()})`);
92
- }
93
- else if (job.message) {
94
- console.log(job.message);
95
- }
96
- else {
97
- console.log(`No network job to do (${nowString()})`);
129
+ // If the watchee sent a job:
130
+ if (job.id) {
131
+ // Stop checking and return it.
132
+ console.log(`Network job ${job.id} received from ${watchJobURL} (${nowString()})`);
133
+ return job;
134
+ }
135
+ // Otherwise, if the watchee sent a message:
136
+ else if (job.message) {
137
+ // Report it and continue checking.
138
+ console.log(job.message);
139
+ }
140
+ // Otherwise, i.e. if the watchee sent neither a job nor a message:
141
+ else {
142
+ // Report this and continue checking.
143
+ console.log(`No network job at ${watchJobURL} to do (${nowString()})`);
144
+ }
98
145
  }
99
- return job;
146
+ // If no watchee sent a job, return this.
147
+ return {};
100
148
  };
101
149
  // Writes a directory report.
102
150
  const writeDirReport = async report => {
@@ -121,49 +169,60 @@ const writeDirReport = async report => {
121
169
  const writeNetReport = async report => {
122
170
  const ack = await new Promise(resolve => {
123
171
  if (report.sources) {
124
- // Get the report destination from the report or the environment.
172
+ // If the report specifies where to send it:
125
173
  const destination = report.sources.sendReportTo;
126
- const wholeURL = destination || `${process.env.PROTOCOL}://${reportURL}`;
127
- // Contact the server.
128
- const request = client.request(wholeURL, {method: 'POST'}, response => {
129
- const chunks = [];
130
- response.on('data', chunk => {
131
- chunks.push(chunk);
132
- });
133
- response.on('end', () => {
134
- const content = chunks.join('');
135
- try {
136
- resolve(JSON.parse(content));
137
- }
138
- catch(error) {
139
- resolve({
140
- error: 'ERROR: Response was not JSON',
141
- message: error.message,
142
- status: response.statusCode,
143
- content: content.slice(0, 3000)
144
- });
145
- }
174
+ if (destination) {
175
+ // Send it.
176
+ const client = destination.startsWith('https://') ? https : http;
177
+ const request = client.request(destination, {method: 'POST'}, response => {
178
+ const chunks = [];
179
+ response.on('data', chunk => {
180
+ chunks.push(chunk);
181
+ });
182
+ response.on('end', () => {
183
+ const content = chunks.join('');
184
+ try {
185
+ resolve(JSON.parse(content));
186
+ }
187
+ catch(error) {
188
+ resolve({
189
+ error: 'ERROR: Response was not JSON',
190
+ message: error.message,
191
+ status: response.statusCode,
192
+ content: content.slice(0, 3000)
193
+ });
194
+ }
195
+ });
146
196
  });
147
- });
148
- report.jobData.agent = agent;
149
- request.on('error', error => {
150
- console.log(`ERROR submitting job report (${error.message})`);
151
- resolve({
152
- error: 'ERROR: Job report submission failed',
153
- message: error.message
197
+ report.jobData.agent = agent;
198
+ request.on('error', error => {
199
+ console.log(`ERROR submitting job report (${error.message})`);
200
+ resolve({
201
+ error: 'ERROR: Job report submission failed',
202
+ message: error.message
203
+ });
154
204
  });
155
- });
156
- // Send the report to the server.
157
- const reportJSON = JSON.stringify(report, null, 2);
158
- request.write(reportJSON);
159
- request.end();
160
- console.log(`Report ${report.id} submitted (${nowString()})`);
205
+ const reportJSON = JSON.stringify(report, null, 2);
206
+ request.end(reportJSON);
207
+ console.log(`Report ${report.id} submitted (${nowString()})`);
208
+ }
209
+ // Otherwise, i.e. if the report does not specify where to send it:
210
+ else {
211
+ // Report this.
212
+ console.log('ERROR: Report specifies no submission destination');
213
+ }
161
214
  }
162
215
  else {
163
216
  console.log('ERROR: Report has no sources property');
164
217
  }
165
218
  });
166
219
  // Return the server response.
220
+ if (ack) {
221
+ return ack.message || ack;
222
+ }
223
+ else {
224
+ return '';
225
+ }
167
226
  return ack;
168
227
  };
169
228
  // Archives a job.
@@ -198,7 +257,7 @@ const runJob = async (job, isDirWatch) => {
198
257
  else {
199
258
  // Send the report to the server.
200
259
  const ack = await writeNetReport(job);
201
- console.log(JSON.stringify(ack, null, 2));
260
+ console.log(ack);
202
261
  }
203
262
  }
204
263
  // If the job failed: