testaro 41.0.0 → 41.0.2

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/dirWatch.js CHANGED
@@ -33,6 +33,8 @@ require('dotenv').config();
33
33
  const fs = require('fs/promises');
34
34
  // Module to perform jobs.
35
35
  const {doJob} = require('./run');
36
+ // Module to get dates from time stamps.
37
+ const {dateOf} = require('./procs/dateOf');
36
38
 
37
39
  // ########## CONSTANTS
38
40
 
@@ -45,13 +47,6 @@ const reportDir = process.env.REPORTDIR;
45
47
  const tsPart = (timeStamp, startIndex) => timeStamp.slice(startIndex, startIndex + 2);
46
48
  // Returns a string representing the date and time.
47
49
  const nowString = () => (new Date()).toISOString().slice(2, 16);
48
- // Gets date of a timestamp.
49
- const dateOf = ts => {
50
- const dateString = `20${tsPart(ts, 0)}-${tsPart(ts, 2)}-${tsPart(ts, 4)}`;
51
- const timeString = `${tsPart(ts, 7)}:${tsPart(ts, 9)}:00`;
52
- const dateTimeString = `${dateString}T${timeString}Z`;
53
- return new Date(dateTimeString);
54
- };
55
50
  // Writes a directory report.
56
51
  const writeDirReport = async report => {
57
52
  const jobID = report && report.id;
package/netWatch.js CHANGED
@@ -29,6 +29,8 @@
29
29
 
30
30
  // Module to keep secrets.
31
31
  require('dotenv').config();
32
+ // Module to validate jobs.
33
+ const {isValidJob} = require('./procs/job');
32
34
  // Modules to make requests to servers.
33
35
  const httpClient = require('http');
34
36
  const httpsClient = require('https');
@@ -132,6 +134,7 @@ exports.netWatch = async (isForever, intervalInSeconds, isCertTolerant = true) =
132
134
  try {
133
135
  // If there was no job to do:
134
136
  let contentObj = JSON.parse(content);
137
+ let jobInvalidity = '';
135
138
  if (! Object.keys(contentObj).length) {
136
139
  // Report this.
137
140
  console.log(`No job to do at ${url}`);
@@ -139,7 +142,8 @@ exports.netWatch = async (isForever, intervalInSeconds, isCertTolerant = true) =
139
142
  }
140
143
  // Otherwise, i.e. if there was a job or a message:
141
144
  else {
142
- const {id, message, sendReportTo, sources} = contentObj;
145
+ const {id, message, sources} = contentObj;
146
+ const sendReportTo = sources ? sources.sendReportTo : '';
143
147
  // If the server sent a message, not a job:
144
148
  if (message) {
145
149
  // Report it.
@@ -147,98 +151,89 @@ exports.netWatch = async (isForever, intervalInSeconds, isCertTolerant = true) =
147
151
  resolve(true);
148
152
  }
149
153
  // Otherwise, if the server sent a valid job:
150
- else if (id && sources) {
154
+ else if (
155
+ id && sendReportTo && sources && ! (jobInvalidity = isValidJob(contentObj))
156
+ ) {
151
157
  // Restart the cycle.
152
158
  cycleIndex = -1;
153
159
  // Prevent further watching, if unwanted.
154
160
  noJobYet = false;
155
161
  // Add the agent to the job.
156
162
  sources.agent = agent;
157
- // If the job specifies a report destination:
158
- if (sendReportTo) {
159
- // Perform the job, adding result data to it.
160
- console.log(`${logStart}job ${id} (${nowString()})`);
161
- console.log(`>> It will send report to ${sendReportTo}`);
162
- await doJob(contentObj);
163
- let reportJSON = JSON.stringify(contentObj, null, 2);
164
- console.log(`Job ${id} finished (${nowString()})`);
165
- // Send the report to the specified server.
166
- console.log(`Sending report ${id} to ${sendReportTo}`);
167
- const reportClient = sendReportTo.startsWith('https://')
168
- ? httpsClient
169
- : httpClient;
170
- const reportLogStart = `Sent report ${id} to ${sendReportTo} and got `;
171
- reportClient.request(sendReportTo, {method: 'POST'}, repResponse => {
172
- const chunks = [];
173
- repResponse
174
- // If the response to the report threw an error:
175
- .on('error', async error => {
176
- // Report this.
177
- console.log(`${reportLogStart}error message ${error.message}\n`);
178
- resolve(true);
179
- })
180
- .on('data', chunk => {
181
- chunks.push(chunk);
182
- })
183
- // When the response arrives:
184
- .on('end', async () => {
185
- const content = chunks.join('');
186
- try {
187
- // If the server sent a message, as expected:
188
- const ackObj = JSON.parse(content);
189
- const {message} = ackObj;
190
- if (message) {
191
- // Report it.
192
- console.log(`${reportLogStart}message ${message}\n`);
193
- // Free the memory used by the report.
194
- reportJSON = '';
195
- contentObj = {};
196
- resolve(true);
197
- }
198
- // Otherwise, i.e. if the server sent anything else:
199
- else {
200
- // Report it.
201
- console.log(
202
- `ERROR: ${reportLogStart}status ${repResponse.statusCode} and error message ${JSON.stringify(ackObj, null, 2)}\n`
203
- );
204
- resolve(true);
205
- }
163
+ // Perform the job, adding result data to it.
164
+ console.log(`${logStart}job ${id} (${nowString()})`);
165
+ console.log(`>> It will send report to ${sendReportTo}`);
166
+ await doJob(contentObj);
167
+ let reportJSON = JSON.stringify(contentObj, null, 2);
168
+ console.log(`Job ${id} finished (${nowString()})`);
169
+ // Send the report to the specified server.
170
+ console.log(`Sending report ${id} to ${sendReportTo}`);
171
+ const reportClient = sendReportTo.startsWith('https://')
172
+ ? httpsClient
173
+ : httpClient;
174
+ const reportLogStart = `Sent report ${id} to ${sendReportTo} and got `;
175
+ reportClient.request(sendReportTo, {method: 'POST'}, repResponse => {
176
+ const chunks = [];
177
+ repResponse
178
+ // If the response to the report threw an error:
179
+ .on('error', async error => {
180
+ // Report this.
181
+ console.log(`${reportLogStart}error message ${error.message}\n`);
182
+ resolve(true);
183
+ })
184
+ .on('data', chunk => {
185
+ chunks.push(chunk);
186
+ })
187
+ // When the response arrives:
188
+ .on('end', async () => {
189
+ const content = chunks.join('');
190
+ try {
191
+ // If the server sent a message, as expected:
192
+ const ackObj = JSON.parse(content);
193
+ const {message} = ackObj;
194
+ if (message) {
195
+ // Report it.
196
+ console.log(`${reportLogStart}message ${message}\n`);
197
+ // Free the memory used by the report.
198
+ reportJSON = '';
199
+ contentObj = {};
200
+ resolve(true);
206
201
  }
207
- // If processing the server message throws an error:
208
- catch(error) {
202
+ // Otherwise, i.e. if the server sent anything else:
203
+ else {
209
204
  // Report it.
210
205
  console.log(
211
- `ERROR: ${reportLogStart}status ${repResponse.statusCode}, error message ${error.message}, and response ${content.slice(0, 1000)}\n`
206
+ `ERROR: ${reportLogStart}status ${repResponse.statusCode} and error message ${JSON.stringify(ackObj, null, 2)}\n`
212
207
  );
213
208
  resolve(true);
214
209
  }
215
- });
216
- })
217
- // If the report submission throws an error:
218
- .on('error', async error => {
219
- // Report this.
220
- console.log(
221
- `ERROR in report submission: ${reportLogStart}error message ${error.message}\n`
222
- );
223
- resolve(true);
224
- })
225
- // Finish submitting the report.
226
- .end(reportJSON);
227
- }
228
- // Otherwise, i.e. if the job specifies no report destination:
229
- else {
210
+ }
211
+ // If processing the server message throws an error:
212
+ catch(error) {
213
+ // Report it.
214
+ console.log(
215
+ `ERROR: ${reportLogStart}status ${repResponse.statusCode}, error message ${error.message}, and response ${content.slice(0, 1000)}\n`
216
+ );
217
+ resolve(true);
218
+ }
219
+ });
220
+ })
221
+ // If the report submission throws an error:
222
+ .on('error', async error => {
230
223
  // Report this.
231
- const message = `ERROR: ${logStart}job with no report destination`;
232
- console.log(message);
233
- serveObject({message}, response);
224
+ console.log(
225
+ `ERROR in report submission: ${reportLogStart}error message ${error.message}\n`
226
+ );
234
227
  resolve(true);
235
- }
228
+ })
229
+ // Finish submitting the report.
230
+ .end(reportJSON);
236
231
  }
237
232
  // Otherwise, i.e. if the server sent an invalid job:
238
233
  else {
239
234
  // Report this.
240
- const message
241
- = `ERROR: ${logStart}invalid job:\n${JSON.stringify(contentObj, null, 2)}`;
235
+ const errorSuffix = jobInvalidity ? ` (${jobInvalidity})` : '';
236
+ const message = `ERROR: ${logStart}invalid job${errorSuffix}`;
242
237
  console.log(message);
243
238
  serveObject({message}, response);
244
239
  resolve(true);
@@ -248,9 +243,7 @@ exports.netWatch = async (isForever, intervalInSeconds, isCertTolerant = true) =
248
243
  // If processing the server response throws an error:
249
244
  catch(error) {
250
245
  // Report this.
251
- console.log(
252
- `ERROR processing server response: ${error.message} (response ${content.slice(0, 1000)})`
253
- );
246
+ console.log(`ERROR processing server response: ${error.message})`);
254
247
  resolve(true);
255
248
  }
256
249
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "41.0.0",
3
+ "version": "41.0.2",
4
4
  "description": "Run 1000 web accessibility tests from 10 tools and get a standardized report",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -0,0 +1,47 @@
1
+ /*
2
+ © 2024 CVS Health and/or one of its affiliates. All rights reserved.
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
21
+ */
22
+
23
+ /*
24
+ dateOf
25
+ Returns the date represented by a time stamp.
26
+ */
27
+
28
+ // Inserts a character periodically in a string.
29
+ const punctuate = (string, insertion, chunkSize) => {
30
+ const segments = [];
31
+ let startIndex = 0;
32
+ while (startIndex < string.length) {
33
+ segments.push(string.slice(startIndex, startIndex + chunkSize));
34
+ startIndex += chunkSize;
35
+ }
36
+ return segments.join(insertion);
37
+ };
38
+ // Gets the date of a timestamp.
39
+ exports.dateOf = timeStamp => {
40
+ if (/^\d{6}T\d{4}$/.test(timeStamp)) {
41
+ const dateString = punctuate(timeStamp.slice(0, 6), '-', 2);
42
+ const timeString = punctuate(timeStamp.slice(7, 11), ':', 2);
43
+ return new Date(`20${dateString}T${timeString}Z`);
44
+ } else {
45
+ return null;
46
+ }
47
+ };
package/procs/job.js ADDED
@@ -0,0 +1,268 @@
1
+ /*
2
+ © 2024 CVS Health and/or one of its affiliates. All rights reserved.
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
21
+ */
22
+
23
+ /*
24
+ job
25
+ Utilities about jobs and acts.
26
+ */
27
+
28
+ // IMPORTS
29
+
30
+ // Requirements for acts.
31
+ const {actSpecs} = require('../actSpecs');
32
+ // Module to validate device IDs.
33
+ const {isDeviceID} = require('./device');
34
+ // Module to get dates from time stamps.
35
+ const {dateOf} = require('./dateOf');
36
+
37
+ // CONSTANTS
38
+
39
+ // Names and descriptions of tools.
40
+ const tools = exports.tools = {
41
+ alfa: 'alfa',
42
+ aslint: 'ASLint',
43
+ axe: 'Axe',
44
+ ed11y: 'Editoria11y',
45
+ htmlcs: 'HTML CodeSniffer WCAG 2.1 AA ruleset',
46
+ ibm: 'IBM Accessibility Checker',
47
+ nuVal: 'Nu Html Checker',
48
+ qualWeb: 'QualWeb',
49
+ testaro: 'Testaro',
50
+ wave: 'WAVE',
51
+ };
52
+
53
+ // FUNCTIONS
54
+
55
+ // Returns whether a variable has a specified type.
56
+ const hasType = (variable, type) => {
57
+ if (type === 'string') {
58
+ return typeof variable === 'string';
59
+ }
60
+ else if (type === 'array') {
61
+ return Array.isArray(variable);
62
+ }
63
+ else if (type === 'boolean') {
64
+ return typeof variable === 'boolean';
65
+ }
66
+ else if (type === 'number') {
67
+ return typeof variable === 'number';
68
+ }
69
+ else if (type === 'object') {
70
+ return typeof variable === 'object' && ! Array.isArray(variable);
71
+ }
72
+ else {
73
+ return false;
74
+ }
75
+ };
76
+ // Returns whether a variable has a specified subtype.
77
+ const hasSubtype = (variable, subtype) => {
78
+ if (subtype) {
79
+ if (subtype === 'hasLength') {
80
+ return variable.length > 0;
81
+ }
82
+ else if (subtype === 'isURL') {
83
+ return isURL(variable);
84
+ }
85
+ else if (subtype === 'isDeviceID') {
86
+ return isDeviceID(variable);
87
+ }
88
+ else if (subtype === 'isBrowserID') {
89
+ return isBrowserID(variable);
90
+ }
91
+ else if (subtype === 'isFocusable') {
92
+ return isFocusable(variable);
93
+ }
94
+ else if (subtype === 'isTest') {
95
+ return tools[variable];
96
+ }
97
+ else if (subtype === 'isWaitable') {
98
+ return ['url', 'title', 'body'].includes(variable);
99
+ }
100
+ else if (subtype === 'areNumbers') {
101
+ return areNumbers(variable);
102
+ }
103
+ else if (subtype === 'areStrings') {
104
+ return areStrings(variable);
105
+ }
106
+ else if (subtype === 'areArrays') {
107
+ return areArrays(variable);
108
+ }
109
+ else if (subtype === 'isState') {
110
+ return isState(variable);
111
+ }
112
+ else {
113
+ console.log(`ERROR: ${subtype} not a known subtype`);
114
+ return false;
115
+ }
116
+ }
117
+ else {
118
+ return true;
119
+ }
120
+ };
121
+ // Validates a browser type.
122
+ const isBrowserID = type => ['chromium', 'firefox', 'webkit'].includes(type);
123
+ // Validates a load state.
124
+ const isState = string => ['loaded', 'idle'].includes(string);
125
+ // Validates a URL.
126
+ const isURL = string => /^(?:https?|file):\/\/[^\s]+$/.test(string);
127
+ // Validates a focusable tag name.
128
+ const isFocusable = string => ['a', 'button', 'input', 'select'].includes(string);
129
+ // Returns whether all elements of an array are numbers.
130
+ const areNumbers = array => array.every(element => typeof element === 'number');
131
+ // Returns whether all elements of an array are strings.
132
+ const areStrings = array => array.every(element => typeof element === 'string');
133
+ // Returns whether all properties of an object have array values.
134
+ const areArrays = object => Object.values(object).every(value => Array.isArray(value));
135
+ // Validates an act by reference to actSpecs.js.
136
+ const isValidAct = exports.isValidAct = act => {
137
+ // Identify the type of the act.
138
+ const type = act.type;
139
+ // If the type exists and is known:
140
+ if (type && actSpecs.etc[type]) {
141
+ // Copy the validator of the type for possible expansion.
142
+ const validator = Object.assign({}, actSpecs.etc[type][1]);
143
+ // If the type is test:
144
+ if (type === 'test') {
145
+ // Identify the test.
146
+ const toolName = act.which;
147
+ // If one was specified and is known:
148
+ if (toolName && tools[toolName]) {
149
+ // If it has special properties:
150
+ if (actSpecs.tools[toolName]) {
151
+ // Expand the validator by adding them.
152
+ Object.assign(validator, actSpecs.tools[toolName][1]);
153
+ }
154
+ }
155
+ // Otherwise, i.e. if no or an unknown test was specified:
156
+ else {
157
+ // Return invalidity.
158
+ return false;
159
+ }
160
+ }
161
+ // Return whether the act is valid.
162
+ return Object.keys(validator).every(property => {
163
+ if (property === 'name') {
164
+ return true;
165
+ }
166
+ else {
167
+ const vP = validator[property];
168
+ const aP = act[property];
169
+ // If it is optional and omitted or is present and valid:
170
+ const optAndNone = ! vP[0] && ! aP;
171
+ const isValid = aP !== undefined && hasType(aP, vP[1]) && hasSubtype(aP, vP[2]);
172
+ return optAndNone || isValid;
173
+ }
174
+ });
175
+ }
176
+ // Otherwise, i.e. if the act has an unknown or no type:
177
+ else {
178
+ // Return invalidity.
179
+ return false;
180
+ }
181
+ };
182
+ // Returns blank if a job is valid, or an error message.
183
+ exports.isValidJob = job => {
184
+ // If any job was provided:
185
+ if (job) {
186
+ // Get its properties.
187
+ const {
188
+ id,
189
+ strict,
190
+ isolate,
191
+ standard,
192
+ observe,
193
+ deviceID,
194
+ browserID,
195
+ lowMotion,
196
+ timeLimit,
197
+ creationTimeStamp,
198
+ executionTimeStamp,
199
+ sources,
200
+ acts
201
+ } = job;
202
+ // Return an error for the first missing or invalid property.
203
+ if (! id || typeof id !== 'string') {
204
+ return 'Bad job ID';
205
+ }
206
+ if (typeof strict !== 'boolean') {
207
+ return 'Bad job strict';
208
+ }
209
+ if (typeof isolate !== 'boolean') {
210
+ return 'Bad job isolate';
211
+ }
212
+ if (! ['also', 'only', 'no'].includes(standard)) {
213
+ return 'Bad job standard';
214
+ }
215
+ if (typeof observe !== 'boolean') {
216
+ return 'Bad job observe';
217
+ }
218
+ if (! isDeviceID(deviceID)) {
219
+ return 'Bad job deviceID';
220
+ }
221
+ if (! ['chromium', 'firefox', 'webkit'].includes(browserID)) {
222
+ return 'Bad job browserID';
223
+ }
224
+ if (typeof lowMotion !== 'boolean') {
225
+ return 'Bad job lowMotion';
226
+ }
227
+ if (typeof timeLimit !== 'number' || timeLimit < 1) {
228
+ return 'Bad job timeLimit';
229
+ }
230
+ if (
231
+ ! (creationTimeStamp && typeof creationTimeStamp === 'string' && dateOf(creationTimeStamp))
232
+ ) {
233
+ return 'bad job creationTimeStamp';
234
+ }
235
+ if (
236
+ ! (executionTimeStamp && typeof executionTimeStamp === 'string') && dateOf(executionTimeStamp)
237
+ ) {
238
+ return 'bad job executionTimeStamp';
239
+ }
240
+ if (
241
+ ! sources
242
+ || typeof sources !== 'object'
243
+ || ! ['script', 'batch', 'target'].every(key => sources[key])
244
+ || ! ['what', 'url'].every(key => sources.target[key])
245
+ ) {
246
+ return 'Bad job sources';
247
+ }
248
+ if (
249
+ ! acts
250
+ || ! Array.isArray(acts)
251
+ || acts.length < 2
252
+ || ! acts.every(act => act.type && typeof act.type === 'string')
253
+ || acts[0].type !== 'launch'
254
+ ) {
255
+ return 'Bad job acts';
256
+ }
257
+ const invalidAct = acts.find(act => ! isValidAct(act));
258
+ if (invalidAct) {
259
+ return `Invalid act:\n${JSON.stringify(invalidAct, null, 2)}`;
260
+ }
261
+ return '';
262
+ }
263
+ // Otherwise, i.e. if no job was provided:
264
+ else {
265
+ // Return this.
266
+ return 'no job';
267
+ }
268
+ };