testaro 5.7.2 → 5.7.6

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
@@ -93,6 +93,7 @@ A script is a JSON file with the properties:
93
93
 
94
94
  ```json
95
95
  {
96
+ "id": "string consisting of lower-case ASCII letters and digits",
96
97
  "what": "string: description of the script",
97
98
  "strict": "boolean: whether redirections should be treated as failures",
98
99
  "timeLimit": "number: limit in seconds on the execution of this script",
@@ -100,32 +101,33 @@ A script is a JSON file with the properties:
100
101
  }
101
102
  ```
102
103
 
103
- The `timeLimit` property is optional. If it is omitted, a default of 300 seconds (5 minutes) is set.
104
+ The `timeLimit` property is optional. If it is omitted, a default of 300 seconds (5 minutes) is set.
104
105
 
105
106
  ### Example
106
107
 
107
108
  Here is an example of a script:
108
109
 
109
- ```javascript
110
+ ```json
110
111
  {
111
- what: 'Test example.com with alfa',
112
+ "id": "samplescript",
113
+ what: "Test example.com with alfa",
112
114
  strict: true,
113
- timeLimit: 15,
115
+ timeLimit: 65,
114
116
  commands: [
115
117
  {
116
- type: 'launch',
117
- which: 'chromium',
118
- what: 'Chromium browser'
118
+ type: "launch",
119
+ which: "chromium",
120
+ what: "Chromium browser"
119
121
  },
120
122
  {
121
- type: 'url',
122
- which: 'https://example.com/',
123
- what: 'page with a few accessibility defects'
123
+ type: "url",
124
+ which: "https://example.com/",
125
+ what: "page with a few accessibility defects"
124
126
  },
125
127
  {
126
- type: 'test',
127
- which: 'alfa',
128
- what: 'Siteimprove alfa package'
128
+ type: "test",
129
+ which: "alfa",
130
+ what: "Siteimprove alfa package"
129
131
  }
130
132
  ]
131
133
  }
@@ -302,6 +304,10 @@ In case you want to perform more than one `tenon` test with the same script, you
302
304
 
303
305
  Tenon recommends giving it a public URL rather than giving it the content of a page, if possible. So, it is best to give the `withNewContent` property of the `tenonRequest` command the value `true`, unless the page is not public.
304
306
 
307
+ ###### Continuum
308
+
309
+ The `continuum` test makes use of the files in the `continuum` directory. The test inserts the contents of all three files into the page as scripts and then uses them to perform the tests of the Continuum package.
310
+
305
311
  ###### HTML CodeSniffer
306
312
 
307
313
  The `htmlcs` test makes use of the`htmlcs/HTMLCS.js` file. That file was created, and can be recreated if necessary, as follows:
@@ -336,7 +342,7 @@ The changes in `htmlcs/HTMLCS.js` are:
336
342
 
337
343
  ###### BBC Accessibility Standards Checker
338
344
 
339
- The BBC Accessibility Standards Checker has obsolete dependencies with security vulnerabilities. Therefore, it is not used as a dependency of Testaro. Instead, 6 of its tests were reimplemented, in some case with revisions, as Testaro tests. They were drawn from the 18 automated tests of the Checker. The other 12 tests were found too duplicative of other tests to justify reimplementation.
345
+ The BBC Accessibility Standards Checker has obsolete dependencies with security vulnerabilities. Therefore, it is not used as a dependency of Testaro. Instead, 6 of its tests are reimplemented, in some case with revisions, as Testaro tests. They are drawn from the 18 automated tests of the Checker. The other 12 tests were found too duplicative of other tests to justify reimplementation.
340
346
 
341
347
  ##### Branching
342
348
 
@@ -436,18 +442,18 @@ A typical use for an `expect` property is checking the correctness of a Testaro
436
442
 
437
443
  You may wish to have Testaro perform the same sequence of tests on multiple web pages. In that case, you can create a _batch_, with the following structure:
438
444
 
439
- ```javascript
445
+ ```json
440
446
  {
441
- what: 'Web leaders',
447
+ what: "Web leaders",
442
448
  hosts: {
443
- id: 'w3c',
444
- which: 'https://www.w3.org/',
445
- what: 'W3C'
449
+ id: "w3c",
450
+ which: "https://www.w3.org/",
451
+ what: "W3C"
446
452
  },
447
453
  {
448
- id: 'wikimedia',
449
- which: 'https://www.wikimedia.org/',
450
- what: 'Wikimedia'
454
+ id: "wikimedia",
455
+ which: "https://www.wikimedia.org/",
456
+ what: "Wikimedia"
451
457
  }
452
458
  }
453
459
  ```
@@ -487,7 +493,7 @@ The argument of `require` is a path relative to the directory of the module in w
487
493
 
488
494
  Another Node.js package that has Testaro as a dependency can execute the same statements, except changing `'./run'` to `'testaro/run'`.
489
495
 
490
- Testaro will run the script and populate the `log` and `acts` arrays of the `report` object. When Testaro finishes, the `log` and `acts` properties will contain the results. The final statement can further process the `report` object as desired in the `then` callback.
496
+ Testaro will run the script and modify the properties of the `report` object. When Testaro finishes, the `log`, `acts`, and other properties of `report` will contain the results. The final statement can further process the `report` object as desired in the `then` callback.
491
497
 
492
498
  #### High-level
493
499
 
@@ -500,7 +506,7 @@ Relative paths must be relative to the Testaro project directory. For example, i
500
506
 
501
507
  Also ensure that Testaro can read all those directories and write to `REPORTDIR`.
502
508
 
503
- Place a script into `SCRIPTDIR` and, optionally, a batch into `BATCHDIR`. Each should be named `idvalue.json`, where `idvalue` is replaced with the value of its `id` property. That value must consist of only lower-case ASCII letters and digits.
509
+ Place a script into `SCRIPTDIR` and, optionally, a batch into `BATCHDIR`. Each should be named with a `.json` extension., where `idvalue` is replaced with the value of its `id` property. That value must consist of only lower-case ASCII letters and digits.
504
510
 
505
511
  Then execute the statement `node high scriptID` or `node high scriptID batchID`, replacing `scriptID` and `batchID` with the `id` values of the script and the batch, respectively.
506
512
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "5.7.2",
3
+ "version": "5.7.6",
4
4
  "description": "Automation of accessibility testing",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/run.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*
2
- index.js
2
+ run.js
3
3
  testaro main script.
4
4
  */
5
5
 
@@ -103,8 +103,10 @@ let errorLogSize = 0;
103
103
  let prohibitedCount = 0;
104
104
  let visitTimeoutCount = 0;
105
105
  let visitRejectionCount = 0;
106
+ let visitLatency = 0;
106
107
  let actCount = 0;
107
108
  // Facts about the current browser.
109
+ let browser;
108
110
  let browserContext;
109
111
  let browserTypeName;
110
112
  let requestedURL = '';
@@ -256,12 +258,24 @@ const isValidReport = async report => {
256
258
 
257
259
  // ########## OTHER FUNCTIONS
258
260
 
261
+ // Closes the current browser.
262
+ const browserClose = async () => {
263
+ if (browser) {
264
+ const contexts = browser.contexts();
265
+ for (const context of contexts) {
266
+ await context.close();
267
+ }
268
+ await browser.close();
269
+ }
270
+ };
259
271
  // Launches a browser.
260
272
  const launch = async typeName => {
261
273
  const browserType = require('playwright')[typeName];
262
274
  // If the specified browser type exists:
263
275
  if (browserType) {
264
- // Launch a browser of the specified type.
276
+ // Close the current browser, if any.
277
+ await browserClose();
278
+ // Launch a browser of that type.
265
279
  const browserOptions = {};
266
280
  if (debug) {
267
281
  browserOptions.headless = false;
@@ -270,7 +284,7 @@ const launch = async typeName => {
270
284
  browserOptions.slowMo = waits;
271
285
  }
272
286
  let healthy = true;
273
- const browser = await browserType.launch(browserOptions)
287
+ browser = await browserType.launch(browserOptions)
274
288
  .catch(error => {
275
289
  healthy = false;
276
290
  console.log(`ERROR launching browser: ${error.message.replace(/\n.+/s, '')}`);
@@ -511,6 +525,7 @@ const goto = async (page, url, timeout, waitUntil, isStrict) => {
511
525
  url = url.replace('file://', `file://${__dirname}/`);
512
526
  }
513
527
  // Visit the URL.
528
+ const startTime = Date.now();
514
529
  const response = await page.goto(url, {
515
530
  timeout,
516
531
  waitUntil
@@ -520,6 +535,7 @@ const goto = async (page, url, timeout, waitUntil, isStrict) => {
520
535
  visitTimeoutCount++;
521
536
  return 'error';
522
537
  });
538
+ visitLatency += Math.round((Date.now() - startTime) / 1000);
523
539
  // If the visit succeeded:
524
540
  if (typeof response !== 'string') {
525
541
  const httpStatus = response.status();
@@ -733,7 +749,7 @@ const doActs = async (report, actIndex, page) => {
733
749
  // If the command is a url:
734
750
  if (act.type === 'url') {
735
751
  // Visit it and wait until it is stable.
736
- page = await visit(act, page, report.strict);
752
+ page = await visit(act, page, report.isStrict);
737
753
  }
738
754
  // Otherwise, if the act is a wait for text:
739
755
  else if (act.type === 'wait') {
@@ -1335,6 +1351,7 @@ const doScript = async (report) => {
1335
1351
  report.prohibitedCount = prohibitedCount;
1336
1352
  report.visitTimeoutCount = visitTimeoutCount;
1337
1353
  report.visitRejectionCount = visitRejectionCount;
1354
+ report.visitLatency = visitLatency;
1338
1355
  // Add the end time and duration to the report.
1339
1356
  const endTime = new Date();
1340
1357
  report.endTime = endTime.toISOString().slice(0, 19);
package/tests/hover.js CHANGED
@@ -28,6 +28,10 @@
28
28
  value in the same location being the target.
29
29
  */
30
30
 
31
+ // VARIABLES
32
+
33
+ let hasTimedOut = false;
34
+
31
35
  // FUNCTIONS
32
36
 
33
37
  // Samples a population and returns the sample.
@@ -59,153 +63,172 @@ const textOf = async (element, limit) => {
59
63
  };
60
64
  // Recursively reports impacts of hovering over triggers.
61
65
  const find = async (data, withItems, page, region, sample, popRatio) => {
62
- // If any potential triggers remain:
63
- if (sample.length) {
64
- // Identify the first of them.
65
- const firstTrigger = sample[0];
66
- const tagNameJSHandle = await firstTrigger.getProperty('tagName')
67
- .catch(error => {
68
- return '';
69
- });
70
- if (tagNameJSHandle) {
71
- const tagName = await tagNameJSHandle.jsonValue();
72
- // Identify the root of a subtree likely to contain impacted elements.
73
- let root = firstTrigger;
74
- if (['A', 'BUTTON', 'LI'].includes(tagName)) {
75
- const rootJSHandle = await page.evaluateHandle(
76
- firstTrigger => {
77
- const parent = firstTrigger.parentElement || firstTrigger;
78
- const grandparent = parent.parentElement || parent;
79
- const greatGrandparent = grandparent.parentElement || parent;
80
- return firstTrigger.tagName === 'LI' ? grandparent : greatGrandparent;
81
- },
82
- firstTrigger
83
- );
84
- root = rootJSHandle.asElement();
85
- }
86
- // Identify all the descendants of the root.
87
- const preDescendants = await root.$$(':visible');
88
- // Identify their opacities.
89
- const preOpacities = await page.evaluate(elements => elements.map(
90
- element => window.getComputedStyle(element).opacity
91
- ), preDescendants);
92
- try {
93
- // Hover over the trigger.
94
- await firstTrigger.hover({
95
- timeout: 500,
96
- noWaitAfter: true
97
- });
98
- // Repeatedly seeks impacts.
99
- const getImpacts = async (interval, triesLeft) => {
100
- // If the allowed trial count has not yet been exhausted:
101
- if (triesLeft--) {
102
- // Get the collection of descendants of the root.
103
- const postDescendants = await root.$$(':visible');
104
- // Identify the prior descandants of the root still in existence.
105
- const remainerIndexes = await page.evaluate(args => {
106
- const preDescendants = args[0];
107
- const postDescendants = args[1];
108
- const remainerIndexes = preDescendants
109
- .map((element, index) => postDescendants.includes(element) ? index : -1)
110
- .filter(index => index > -1);
111
- return remainerIndexes;
112
- }, [preDescendants, postDescendants]);
113
- // Get the count of elements added by the hover event.
114
- const additionCount = postDescendants.length - remainerIndexes.length;
115
- const removalCount = preDescendants.length - remainerIndexes.length;
116
- const remainers = [];
117
- for (const index of remainerIndexes) {
118
- remainers.push({
119
- element: preDescendants[index],
120
- preOpacity: preOpacities[index],
121
- postOpacity: await page.evaluate(
122
- element => window.getComputedStyle(element).opacity, preDescendants[index]
123
- )
124
- });
125
- }
126
- const opacityChangers = remainers
127
- .filter(remainer => remainer.postOpacity !== remainer.preOpacity);
128
- const opacityImpact = opacityChangers ? await page.evaluate(changers => changers.reduce(
129
- (total, current) => total + current.element.querySelectorAll('*').length, 0
130
- ), opacityChangers) : 0;
131
- if (additionCount || removalCount || opacityChangers.length) {
132
- return {
133
- additionCount,
134
- removalCount,
135
- opacityChangers,
136
- opacityImpact
137
- };
138
- }
139
- else {
140
- return await new Promise(resolve => {
141
- setTimeout(() => {
142
- resolve(getImpacts(interval, triesLeft));
143
- }, interval);
144
- });
145
- }
146
- }
147
- else {
148
- return null;
149
- }
150
- };
151
- // Repeatedly seek impacts of the hover at intervals.
152
- const impacts = await getImpacts(300, 4);
153
- // If there were any:
154
- if (impacts) {
155
- // Hover over the upper-left corner of the page, to undo any impacts.
156
- await page.hover('body', {
157
- position: {
158
- x: 0,
159
- y: 0
66
+ // If any potential triggers remain and the test has not timed out:
67
+ if (sample.length && ! hasTimedOut) {
68
+ // Get and report the impacts until and unless the test times out.
69
+ try {
70
+ // Identify the first of them.
71
+ const firstTrigger = sample[0];
72
+ const tagNameJSHandle = await firstTrigger.getProperty('tagName')
73
+ .catch(error => '');
74
+ if (tagNameJSHandle) {
75
+ const tagName = await tagNameJSHandle.jsonValue();
76
+ // Identify the root of a subtree likely to contain impacted elements.
77
+ let root = firstTrigger;
78
+ if (['A', 'BUTTON', 'LI'].includes(tagName)) {
79
+ const rootJSHandle = await page.evaluateHandle(
80
+ firstTrigger => {
81
+ const parent = firstTrigger.parentElement || firstTrigger;
82
+ const grandparent = parent.parentElement || parent;
83
+ const greatGrandparent = grandparent.parentElement || parent;
84
+ return firstTrigger.tagName === 'LI' ? grandparent : greatGrandparent;
160
85
  },
86
+ firstTrigger
87
+ );
88
+ root = rootJSHandle.asElement();
89
+ }
90
+ // Identify all the visible descendants of the root.
91
+ const preDescendants = await root.$$(':visible');
92
+ // Identify their opacities.
93
+ const preOpacities = await page.evaluate(elements => elements.map(
94
+ element => window.getComputedStyle(element).opacity
95
+ ), preDescendants);
96
+ try {
97
+ // Hover over the trigger.
98
+ await firstTrigger.hover({
161
99
  timeout: 500,
162
- force: true,
163
100
  noWaitAfter: true
164
101
  });
165
- // Wait for any delayed and/or slowed hover reaction.
166
- await page.waitForTimeout(200);
167
- await root.waitForElementState('stable');
168
- // Increment the counts of triggers and impacts.
169
- const {additionCount, removalCount, opacityChangers, opacityImpact} = impacts;
170
- data.totals.impactTriggers += popRatio;
171
- data.totals.additions += popRatio * additionCount;
172
- data.totals.removals += popRatio * removalCount;
173
- data.totals.opacityChanges += popRatio * opacityChangers.length;
174
- data.totals.opacityImpact += popRatio * opacityImpact;
175
- // If details are to be reported:
176
- if (withItems) {
177
- // Report them.
178
- data.items[region].impactTriggers.push({
179
- tagName,
180
- text: await textOf(firstTrigger, 50),
181
- additions: additionCount,
182
- removals: removalCount,
183
- opacityChanges: opacityChangers.length,
184
- opacityImpact
102
+ // FUNCTION DEFINITION START
103
+ // Repeatedly seeks impacts.
104
+ const getImpacts = async (interval, triesLeft) => {
105
+ // If the allowed trial count has not yet been exhausted:
106
+ if (triesLeft-- && ! hasTimedOut) {
107
+ // Get the collection of descendants of the root.
108
+ const postDescendants = await root.$$(':visible');
109
+ // Identify the prior descandants of the root still in existence.
110
+ const remainerIndexes = await page.evaluate(args => {
111
+ const preDescendants = args[0];
112
+ const postDescendants = args[1];
113
+ const remainerIndexes = preDescendants
114
+ .map((element, index) => postDescendants.includes(element) ? index : -1)
115
+ .filter(index => index > -1);
116
+ return remainerIndexes;
117
+ }, [preDescendants, postDescendants]);
118
+ // Get the count of elements added by the hover event.
119
+ const additionCount = postDescendants.length - remainerIndexes.length;
120
+ const removalCount = preDescendants.length - remainerIndexes.length;
121
+ const remainers = [];
122
+ for (const index of remainerIndexes) {
123
+ remainers.push({
124
+ element: preDescendants[index],
125
+ preOpacity: preOpacities[index],
126
+ postOpacity: await page.evaluate(
127
+ element => window.getComputedStyle(element).opacity, preDescendants[index]
128
+ )
129
+ });
130
+ }
131
+ const opacityChangers = remainers
132
+ .filter(remainer => remainer.postOpacity !== remainer.preOpacity);
133
+ const opacityImpact = opacityChangers ? await page.evaluate(changers => changers.reduce(
134
+ (total, current) => total + current.element.querySelectorAll('*').length, 0
135
+ ), opacityChangers) : 0;
136
+ if (additionCount || removalCount || opacityChangers.length) {
137
+ return {
138
+ additionCount,
139
+ removalCount,
140
+ opacityChangers,
141
+ opacityImpact
142
+ };
143
+ }
144
+ else {
145
+ return await new Promise(resolve => {
146
+ setTimeout(() => {
147
+ resolve(getImpacts(interval, triesLeft));
148
+ }, interval);
149
+ });
150
+ }
151
+ }
152
+ else {
153
+ return null;
154
+ }
155
+ };
156
+ // FUNCTION DEFINITION END
157
+ // Repeatedly seek impacts of the hover at intervals.
158
+ const impacts = await getImpacts(300, 4);
159
+ // If there were any:
160
+ if (impacts) {
161
+ // Hover over the upper-left corner of the page, to undo any impacts.
162
+ await page.hover('body', {
163
+ position: {
164
+ x: 0,
165
+ y: 0
166
+ },
167
+ timeout: 500,
168
+ force: true,
169
+ noWaitAfter: true
185
170
  });
171
+ // Wait for any delayed and/or slowed hover reaction.
172
+ await page.waitForTimeout(200);
173
+ await root.waitForElementState('stable');
174
+ // Increment the counts of triggers and impacts.
175
+ const {additionCount, removalCount, opacityChangers, opacityImpact} = impacts;
176
+ if (hasTimedOut) {
177
+ return Promise.resolve('');
178
+ }
179
+ else {
180
+ data.totals.impactTriggers += popRatio;
181
+ data.totals.additions += popRatio * additionCount;
182
+ data.totals.removals += popRatio * removalCount;
183
+ data.totals.opacityChanges += popRatio * opacityChangers.length;
184
+ data.totals.opacityImpact += popRatio * opacityImpact;
185
+ // If details are to be reported:
186
+ if (withItems) {
187
+ // Report them.
188
+ data.items[region].impactTriggers.push({
189
+ tagName,
190
+ text: await textOf(firstTrigger, 50),
191
+ additions: additionCount,
192
+ removals: removalCount,
193
+ opacityChanges: opacityChangers.length,
194
+ opacityImpact
195
+ });
196
+ }
197
+ }
186
198
  }
187
199
  }
188
- }
189
- catch (error) {
190
- console.log(`ERROR hovering (${error.message.replace(/\n.+/s, '')})`);
191
- data.totals.unhoverables++;
192
- if (withItems) {
193
- try {
194
- const id = await firstTrigger.getAttribute('id');
195
- data.items[region].unhoverables.push({
196
- tagName,
197
- id: id || '',
198
- text: await textOf(firstTrigger, 50)
199
- });
200
+ catch (error) {
201
+ console.log(`ERROR hovering (${error.message.replace(/\n.+/s, '')})`);
202
+ if (hasTimedOut) {
203
+ return Promise.resolve('');
200
204
  }
201
- catch(error) {
202
- console.log('ERROR itemizing unhoverable element');
205
+ else {
206
+ data.totals.unhoverables++;
207
+ if (withItems) {
208
+ try {
209
+ const id = await firstTrigger.getAttribute('id');
210
+ data.items[region].unhoverables.push({
211
+ tagName,
212
+ id: id || '',
213
+ text: await textOf(firstTrigger, 50)
214
+ });
215
+ }
216
+ catch(error) {
217
+ console.log('ERROR itemizing unhoverable element');
218
+ }
219
+ }
203
220
  }
204
221
  }
205
222
  }
223
+ // Process the remaining potential triggers.
224
+ await find(data, withItems, page, region, sample.slice(1), popRatio);
206
225
  }
207
- // Process the remaining potential triggers.
208
- await find(data, withItems, page, region, sample.slice(1), popRatio);
226
+ catch(error) {
227
+ console.log(`ERROR: Test quit when remaining sample size was ${sample.length}`);
228
+ }
229
+ }
230
+ else {
231
+ return Promise.resolve('');
209
232
  }
210
233
  };
211
234
  // Performs the hover test and reports results.
@@ -213,7 +236,7 @@ exports.reporter = async (
213
236
  page, headSize = 0, headSampleSize = -1, tailSampleSize = -1, withItems
214
237
  ) => {
215
238
  // Initialize the result.
216
- const data = {
239
+ let data = {
217
240
  totals: {
218
241
  triggers: 0,
219
242
  headTriggers: 0,
@@ -259,17 +282,34 @@ exports.reporter = async (
259
282
  // Get the head and tail samples.
260
283
  const headSample = getSample(headTriggers, headSampleSize);
261
284
  const tailSample = tailSampleSize === -1 ? tailTriggers : getSample(tailTriggers, tailSampleSize);
285
+ // Set a time limit to handle pages that slow the operations of this test.
286
+ const timeLimit = Math.round(1.3 * (headSample.length + tailSample.length));
287
+ const timeout = setTimeout(async () => {
288
+ await page.close();
289
+ console.log(
290
+ `ERROR: hover test timed out at ${timeLimit} seconds; page closed`
291
+ );
292
+ hasTimedOut = true;
293
+ data = {
294
+ prevented: true,
295
+ error: 'ERROR: hover test timed out'
296
+ };
297
+ clearTimeout(timeout);
298
+ }, 1000 * timeLimit);
262
299
  // Find and document the impacts.
263
- if (headSample.length) {
300
+ if (headSample.length && ! hasTimedOut) {
264
301
  await find(data, withItems, page, 'head', headSample, headTriggerCount / headSample.length);
265
302
  }
266
- if (tailSample.length) {
303
+ if (tailSample.length && ! hasTimedOut) {
267
304
  await find(data, withItems, page, 'tail', tailSample, tailTriggerCount / tailSample.length);
268
305
  }
306
+ clearTimeout(timeout);
269
307
  // Round the reported totals.
270
- Object.keys(data.totals).forEach(key => {
271
- data.totals[key] = Math.round(data.totals[key]);
272
- });
308
+ if (! hasTimedOut) {
309
+ Object.keys(data.totals).forEach(key => {
310
+ data.totals[key] = Math.round(data.totals[key]);
311
+ });
312
+ }
273
313
  // Return the result.
274
314
  return {result: data};
275
315
  };
package/tests/nuVal.js CHANGED
@@ -1,16 +1,24 @@
1
1
  /*
2
2
  nuVal
3
3
  This test subjects a page to the Nu Html Checker.
4
+ That API erratically replaces left and right double quotation marks with invalid UTF-8, which
5
+ appears as 2 or 3 successive instances of the replacement character (U+fffd). Therefore, this
6
+ test removes all such quotation marks and the replacement character. That causes
7
+ 'Bad value “” for' to become 'Bad value for'. Since the corruption of quotation marks is
8
+ erratic, no better solution is known.
4
9
  */
5
10
  const https = require('https');
6
11
  exports.reporter = async page => {
7
12
  const pageContent = await page.content();
8
13
  // Get the data from a Nu validation.
9
- const data = await new Promise((resolve, reject) => {
14
+ const dataPromise = new Promise(resolve => {
10
15
  try {
11
16
  const request = https.request(
12
17
  {
13
- // Alternatives (more timeout-prone): host=validator.nu; path=/?parser=html@out=json
18
+ /*
19
+ Alternatives (more timeout-prone): host=validator.nu; path=/?parser=html@out=json
20
+ That host crashes instead of ending with a fatal error.
21
+ */
14
22
  host: 'validator.w3.org',
15
23
  path: '/nu/?parser=html&out=json',
16
24
  method: 'POST',
@@ -27,12 +35,12 @@ exports.reporter = async page => {
27
35
  // When the data arrive:
28
36
  response.on('end', async () => {
29
37
  try {
30
- // Delete unnecessary properties.
31
- const result = JSON.parse(report);
38
+ // Delete left and right quotation marks and their erratic invalid replacements.
39
+ const result = JSON.parse(report.replace(/[\u{fffd}“”]/ug, ''));
32
40
  return resolve(result);
33
41
  }
34
42
  catch (error) {
35
- console.log(`Validation failed (${error.message})`);
43
+ console.log(`ERROR: Validation failed (${error.message})`);
36
44
  return resolve({
37
45
  prevented: true,
38
46
  error: error.message,
@@ -46,7 +54,7 @@ exports.reporter = async page => {
46
54
  request.end();
47
55
  request.on('error', error => {
48
56
  console.log(error.message);
49
- return reject({
57
+ return resolve({
50
58
  prevented: true,
51
59
  error: error.message
52
60
  });
@@ -54,11 +62,22 @@ exports.reporter = async page => {
54
62
  }
55
63
  catch(error) {
56
64
  console.log(error.message);
57
- return reject({
65
+ return resolve({
58
66
  prevented: true,
59
67
  error: error.message
60
68
  });
61
69
  }
62
70
  });
71
+ const timeoutPromise = new Promise(resolve => {
72
+ const timeLimit = 12;
73
+ const timeoutID = setTimeout(() => {
74
+ resolve({
75
+ prevented: true,
76
+ error: `ERROR: Validation timed out at ${timeLimit} seconds`
77
+ });
78
+ clearTimeout(timeoutID);
79
+ }, 1000 * timeLimit);
80
+ });
81
+ const data = await Promise.race([dataPromise, timeoutPromise]);
63
82
  return {result: data};
64
83
  };