testilo 37.0.1 → 38.0.1

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
@@ -199,9 +199,22 @@ Here is a script:
199
199
  isolate: true,
200
200
  standard: 'also',
201
201
  observe: false,
202
- deviceID: 'Kindle Fire HDX',
202
+ device: {
203
+ id: 'iPhone 8',
204
+ windowOptions: {
205
+ reducedMotion: 'no-preference',
206
+ userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/17.4 Mobile/15A372 Safari/604.1',
207
+ viewport: {
208
+ width: 375,
209
+ height: 667
210
+ },
211
+ deviceScaleFactor: 2,
212
+ isMobile: true,
213
+ hasTouch: true,
214
+ defaultBrowserType: 'webkit'
215
+ }
216
+ },
203
217
  browserID: 'webkit',
204
- lowMotion: false,
205
218
  timeLimit: 80,
206
219
  creationTimeStamp: ''
207
220
  executionTimeStamp: '',
@@ -249,9 +262,8 @@ A script has several properties that specify facts about the jobs to be created.
249
262
  - `standard`: When Testaro performs a job, every tool produces its own report. Testaro can convert the test results of each tool report to standard results. The `standard` property specifies how to handle standardization. If `also`, Testaro will include in its reports both the original results of the tests of tools and the Testaro-standardized results. If `only`, reports will include only the standardized test results. If `no`, reports will include only the original results, without standardization.
250
263
  - `observe`: Testaro jobs can allow granular observation. If `true`, the job will do so. If `false`, Testaro will not report job progress, but will send a report to the server only when the report is completed. It is generally user-friendly to allow granular observation, and for user applications to implement it, if they make users wait while jobs are assigned and performed, since that process typically takes a few minutes.
251
264
  - `timeLimit`: This specifies the maximum duration, in seconds, of a job. Testaro will abort jobs that are not completed within that time.
252
- - `deviceID`: This specifies the default device type of the job.
265
+ - `device`: This specifies the ID of a device and properties that each new browser context (window) will have that correspond to that device. The permitted devices are those (about 125 in number) [recognized by Playwright](https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json), as well as `'default'`.
253
266
  - `browserID`: This specifies the default browser type (`'chromium'`, `'firefox'`, or `'webkit'`) of the job.
254
- - `lowMotion`: This is true if the browser is to create tabs with the `reduce-motion` option set to `reduce` instead of `no-preference`. This makes the browser act as if the user has chosen a [motion-reduction option in the settings of the operating system or browser](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion#user_preferences). Motions on pages are, however, often immune to this setting.
255
267
  - `creationTimeStamp` and `executionTimeStamp`: These properties will have values assigned to them when jobs are created from the script.
256
268
  - `sendReportTo`: This is a URL that Testaro is to send its job reports to, or `''` if the jobs will not be network jobs.
257
269
  - `target`: This object contains blank `url` and `what` properties, which will be populated each time the script is converted to a job.
@@ -269,9 +281,10 @@ You can use the `script()` function of the `script` module to simplify the creat
269
281
 
270
282
  #### Without options
271
283
 
272
- In its simplest form, `script()` requires two string arguments:
273
- - An ID for the script
274
- - A description of the script
284
+ In its simplest form, `script()` requires 3 string arguments:
285
+ 1. An ID for the script
286
+ 1. A description of the script
287
+ 1. A device ID
275
288
 
276
289
  Called in this way, `script()` produces a script that tells Testaro to perform the tests for all of the evaluation rules defined by all of the tools integrated by Testaro.
277
290
 
@@ -320,16 +333,16 @@ A module can invoke `script()` in one of these ways:
320
333
 
321
334
  ```javaScript
322
335
  const {script} = require('testilo/script');
323
- const scriptObj = script('monthly', 'landmarks');
336
+ const scriptObj = script('monthly', 'landmarks', 'default');
324
337
  ```
325
338
 
326
339
  ```javaScript
327
340
  const {script} = require('testilo/script');
328
341
  const options = {…};
329
- const scriptObj = script('monthly', 'landmarks', options);
342
+ const scriptObj = script('monthly', 'landmarks', 'default', options);
330
343
  ```
331
344
 
332
- In this example, the script will have `'monthly'` as its ID and `'landmarks'` as its description. It will tell Testaro to test for all evaluation rules if the first form is used, or for all rules specified by the options argument if the second form is used.
345
+ In this example, the script will have `'monthly'` as its ID, `'landmarks'` as its description, and `'default'` as its device. It will tell Testaro to test for all evaluation rules if the first form is used, or for all rules specified by the options argument if the second form is used.
333
346
 
334
347
  The invoking module can further modify and use the script (`scriptObj`) as needed.
335
348
 
@@ -338,9 +351,9 @@ The invoking module can further modify and use the script (`scriptObj`) as neede
338
351
  A user can invoke `script()` by executing one of these statements in the Testilo project directory:
339
352
 
340
353
  ```javascript
341
- node call script id what
342
- node call script id what tools toolID0 toolID1 toolID2 …
343
- node call script id what issues tic99 issueID0 issueID1 …
354
+ node call script id what deviceID
355
+ node call script id what deviceID tools toolID0 toolID1 toolID2 …
356
+ node call script id what deviceID issues tic99 issueID0 issueID1 …
344
357
  ```
345
358
 
346
359
  The first form will create a script with no restrictions.
@@ -349,7 +362,7 @@ The second form will create a script that prescribes tests for all the rules of
349
362
 
350
363
  The third form will create a script that prescribes tests for all the rules classified by the named issue-classification file into any of the specified issues.
351
364
 
352
- In this statement, replace `id` with an ID for the script, such as `headings1`, consisting of ASCII alphanumeric characters, or with `''` if you want Testilo to create an ID. Replace `what` with a string describing the script, or with `''` if you want Testilo to create a generic description.
365
+ In this statement, replace `id` with an ID for the script, such as `headings1`, consisting of ASCII alphanumeric characters, or with `''` if you want Testilo to create an ID. Replace `what` with a string describing the script, or with `''` if you want Testilo to create a generic description. Replace `device` with a valid device ID.
353
366
 
354
367
  The `call` module will retrieve the named classification, if any.
355
368
  The `script` module will create a script.
@@ -362,7 +375,7 @@ When the `script` module creates a script for you, it does not ask you for all o
362
375
  - `isolate`: `true`
363
376
  - `standard`: `'only'`
364
377
  - `observe`: `false`
365
- - `deviceID`: `'default'`
378
+ - `device.windowOptions.reduce-motion`: `'no-preference'`
366
379
  - `browserID`: `'webkit'`
367
380
  - `lowMotion`: `false`
368
381
  - `timeLimit`: 50 plus 30 per tool
@@ -375,6 +388,8 @@ When the `script` module creates a script for you, it does not ask you for all o
375
388
  - `testaro` test act: `withItems` = true, `stopOnFail` = `false`
376
389
  - `wave` test act: `reportType` = 4
377
390
 
391
+ The `device.windowOptions` object has, in addition to `reducedMotion`, other properties shown in the above script example. The `script` module will set them according to the specified device. If you specify `'default'` as the device ID, the only property will be `reducedMotion`.
392
+
378
393
  The `webkit` browser type is selected because the other browser types corrupt some tests. The `ibm` test is performed on the existing page content because some targets cause HTTP2 protocol errors when the `ibm` tool tries to visit them.
379
394
 
380
395
  After you invoke `script`, you can edit the script that it creates to revise any of these options.
package/call.js CHANGED
@@ -39,13 +39,13 @@
39
39
  // Module to keep secrets.
40
40
  require('dotenv').config();
41
41
  // Module to perform common operations.
42
- const {getFileID, getNowStamp, getRandomString} = require('./procs/util');
42
+ const {getFileID, getNowStamp, getRandomString, isToolID} = require('./procs/util');
43
43
  // Function to process files.
44
44
  const fs = require('fs/promises');
45
45
  // Function to process a list-to-batch conversion.
46
46
  const {batch} = require('./batch');
47
47
  // Function to create a script from rule specifications.
48
- const {script, toolIDs} = require('./script');
48
+ const {script} = require('./script');
49
49
  // Function to process a merger.
50
50
  const {merge} = require('./merge');
51
51
  // Function to score reports.
@@ -126,7 +126,7 @@ const callBatch = async (id, what) => {
126
126
  }
127
127
  };
128
128
  // Fulfills a script-creation request.
129
- const callScript = async (scriptID, what, optionType, ... optionDetails) => {
129
+ const callScript = async (scriptID, what, deviceID, optionType, ... optionDetails) => {
130
130
  // Sanitize the script ID.
131
131
  scriptID = scriptID.replace(/[^a-zA-Z0-9]/g, '');
132
132
  if (scriptID === '') {
@@ -138,7 +138,7 @@ const callScript = async (scriptID, what, optionType, ... optionDetails) => {
138
138
  if (optionType === 'tools') {
139
139
  if (
140
140
  optionDetails.length === new Set(optionDetails).size
141
- && optionDetails.every(toolID => toolIDs.includes(toolID))
141
+ && optionDetails.every(toolID => isToolID(toolID))
142
142
  ) {
143
143
  optionArg.type = 'tools';
144
144
  optionArg.specs = optionDetails;
@@ -186,17 +186,22 @@ const callScript = async (scriptID, what, optionType, ... optionDetails) => {
186
186
  return;
187
187
  }
188
188
  }
189
- // Create a script with default device and browser IDs.
190
- const scriptObj = script(scriptID, undefined, undefined, what, optionArg);
191
- try {
192
- // Save it.
193
- const scriptJSON = JSON.stringify(scriptObj, null, 2);
194
- const scriptPath = `${specDir}/scripts/${scriptID}.json`;
195
- await fs.writeFile(scriptPath, `${scriptJSON}\n`);
196
- console.log(`Script created and saved as ${scriptPath}`);
189
+ // Create a script with specified device ID and default browser ID.
190
+ const scriptObj = script(scriptID, what, deviceID, optionArg);
191
+ if (scriptObj) {
192
+ try {
193
+ // Save it.
194
+ const scriptJSON = JSON.stringify(scriptObj, null, 2);
195
+ const scriptPath = `${specDir}/scripts/${scriptID}.json`;
196
+ await fs.writeFile(scriptPath, `${scriptJSON}\n`);
197
+ console.log(`Script created and saved as ${scriptPath}`);
198
+ }
199
+ catch(error) {
200
+ console.log(`ERROR saving script (${error.message})`);
201
+ }
197
202
  }
198
- catch(error) {
199
- console.log(`ERROR saving script (${error.message})`);
203
+ else {
204
+ console.log('ERROR: Script creation failed');
200
205
  }
201
206
  };
202
207
  // Fulfills a merging request.
@@ -487,7 +492,7 @@ if (fn === 'batch' && fnArgs.length === 2) {
487
492
  console.log('Execution completed');
488
493
  });
489
494
  }
490
- else if (fn === 'script' && (fnArgs.length === 2 || fnArgs.length > 3)) {
495
+ else if (fn === 'script' && (fnArgs.length === 3 || fnArgs.length > 4)) {
491
496
  callScript(... fnArgs)
492
497
  .then(() => {
493
498
  console.log('Execution completed');
package/merge.js CHANGED
@@ -50,7 +50,7 @@ const mergeIDLength = 2;
50
50
 
51
51
  // Merges a script and a batch and returns jobs.
52
52
  exports.merge = (script, batch, executionTimeStamp) => {
53
- // If a time stamp was specified:
53
+ // If an execution time stamp was specified:
54
54
  if (executionTimeStamp) {
55
55
  // If it is invalid:
56
56
  if (! dateOf(executionTimeStamp)) {
@@ -100,44 +100,39 @@ exports.merge = (script, batch, executionTimeStamp) => {
100
100
  }
101
101
  // Initialize an array of jobs.
102
102
  const jobs = [];
103
- // For each target in the batch:
104
103
  const {targets} = batch;
105
104
  const targetIDs = Object.keys(targets);
105
+ // For each target in the batch:
106
106
  targetIDs.forEach((what, index) => {
107
- // If the target has the required identifiers:
108
107
  const {actGroups, url} = targets[what];
108
+ // If the target has the required identifiers:
109
109
  if (actGroups && url) {
110
- // Initialize a job.
110
+ // Initialize a job as a copy of the template.
111
111
  const job = JSON.parse(JSON.stringify(protoJob));
112
112
  const {sources, target} = job;
113
113
  // Make the job ID unique.
114
114
  const targetSuffix = alphaNumOf(index);
115
115
  job.id = `${executionTimeStamp}-${sources.mergeID}-${targetSuffix}`;
116
- // If the target is the last one:
117
- if (index === targetIDs.length - 1) {
118
- // Add that fact to the sources property of the job.
119
- sources.lastTarget = true;
120
- }
121
116
  // Populate the target-specific properties of the job.
122
117
  target.what = what;
123
118
  target.url = url;
124
- // Replace each placeholder object in the job with the named act group of the target.
119
+ // Replace each placeholder in the job with a copy of the named act group of the target.
125
120
  let {acts} = job;
126
121
  for (const actIndex in acts) {
127
122
  const act = acts[actIndex];
128
123
  if (act.type === 'placeholder') {
129
124
  const replacerName = act.which;
130
- const replacerActs = actGroups[replacerName];
125
+ const replacerActs = JSON.parse(JSON.stringify(actGroups[replacerName]));
131
126
  if (replacerName && actGroups && replacerActs) {
132
- // Add properties to any launch act in the replacer.
127
+ // Add any override properties to any launch act in the replacer.
133
128
  for (const replacerAct of replacerActs) {
134
129
  if (replacerAct.type === 'launch') {
135
- if (act.deviceID) {
136
- replacerAct.deviceID = act.deviceID;
137
- }
138
130
  if (act.browserID) {
139
131
  replacerAct.browserID = act.browserID;
140
132
  }
133
+ if (act.target) {
134
+ replacerAct.target = act.target;
135
+ }
141
136
  }
142
137
  };
143
138
  acts[actIndex] = replacerActs;
@@ -146,7 +141,7 @@ exports.merge = (script, batch, executionTimeStamp) => {
146
141
  console.log(`ERROR: Placeholder for target ${what} not replaceable`);
147
142
  }
148
143
  }
149
- }
144
+ };
150
145
  // Flatten the acts.
151
146
  job.acts = acts.flat();
152
147
  // Append the job to the array of jobs.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testilo",
3
- "version": "37.0.1",
3
+ "version": "38.0.1",
4
4
  "description": "Prepares Testaro jobs and processes Testaro reports",
5
5
  "main": "call.js",
6
6
  "scripts": {
@@ -57,7 +57,9 @@ const getIssueScoreRow = (issueConstants, issueDetails) => {
57
57
  };
58
58
  // Adds parameters to a query for a digest.
59
59
  const populateQuery = (report, query) => {
60
- const {browserID, deviceID, id, isolate, lowMotion, score, sources, standard, strict, target} = report;
60
+ const {
61
+ browserID, device, id, isolate, lowMotion, score, sources, standard, strict, target
62
+ } = report;
61
63
  const {agent, requester, script} = sources;
62
64
  const {scoreProcID, summary, details} = score;
63
65
  query.ts = script;
@@ -74,7 +76,7 @@ const populateQuery = (report, query) => {
74
76
  = ['original', 'standard', 'original and standard'][['no', 'only', 'also'].indexOf(standard)];
75
77
  query.motion = lowMotion ? 'requested' : 'not requested';
76
78
  query.requester = requester;
77
- query.device = deviceID;
79
+ query.device = device.id;
78
80
  query.browser = browserID;
79
81
  query.agent = agent ? ` on agent ${agent}` : '';
80
82
  query.reportURL = process.env.SCORED_REPORT_URL.replace('__id__', id);
@@ -323,8 +323,20 @@ exports.scorer = report => {
323
323
  summary.tool = toolWeight * details.severity.total.reduce(
324
324
  (total, current, index) => total + severityWeights[index] * current, 0
325
325
  );
326
- // Add the summary element total to the score.
327
- summary.element = elementWeight * pathIDs.size;
326
+ // Get the minimum count of violating elements.
327
+ const actRuleIDs = testActs.map(
328
+ act => act.standardResult.instances.map(instance => `${act.which}:${instance.ruleID}`)
329
+ );
330
+ const allRuleIDs = actRuleIDs.flat();
331
+ const ruleCounts = Array
332
+ .from(new Set(allRuleIDs))
333
+ .map(ruleID => allRuleIDs.filter(id => id === ruleID).length);
334
+ /*
335
+ Add the summary element total to the score, based on the count of identified violating
336
+ elements or the largest count of instances of violations of any rule, whichever is
337
+ greater.
338
+ */
339
+ summary.element = elementWeight * Math.max(pathIDs.size, ... ruleCounts);
328
340
  // Add the summary prevention total to the score.
329
341
  summary.prevention = Object.values(details.prevention).reduce(
330
342
  (total, current) => total + current, 0
package/procs/util.js CHANGED
@@ -25,11 +25,6 @@
25
25
  Utility functions.
26
26
  */
27
27
 
28
- // IMPORTS
29
-
30
- // Devices recognized by Playwright.
31
- const {devices} = require('playwright');
32
-
33
28
  // CONSTANTS
34
29
 
35
30
  // Array of 62 alphanumeric characters.
@@ -39,6 +34,10 @@ const alphaNumChars = (() => {
39
34
  const lowers = Array(26).fill('').map((letter, index) => String.fromCodePoint(97 + index));
40
35
  return digits.concat(uppers, lowers);
41
36
  })();
37
+ // Tools.
38
+ const toolIDs = exports.toolIDs = [
39
+ 'alfa', 'aslint', 'axe', 'ed11y', 'htmlcs', 'ibm', 'nuVal', 'qualWeb', 'testaro', 'wave'
40
+ ];
42
41
 
43
42
  // FUNCTIONS
44
43
 
@@ -114,8 +113,5 @@ exports.getBarCell = (num, colMax, svgWidth, isRight = false) => {
114
113
  const cell = `<td aria-hidden="true"${rightClass}>${svg}</td>`;
115
114
  return cell;
116
115
  };
117
- // Returns whether a device ID is recognized by Playwright.
118
- exports.isValidDeviceID = deviceID => {
119
- const deviceIDs = Object.keys(devices);
120
- return deviceIDs.concat('default').includes(deviceID);
121
- };
116
+ // Returns whether a tool ID is the ID of a Testaro-integrated tool.
117
+ exports.isToolID = toolID => toolIDs.includes(toolID);
package/script.js CHANGED
@@ -25,180 +25,227 @@
25
25
  Creates and returns a script to perform the tests for issues.
26
26
  */
27
27
 
28
- // ########## VARIABLES
28
+ // IMPORTS
29
29
 
30
- // Testaro tool IDs.
31
- const toolIDs = exports.toolIDs = [
32
- 'alfa', 'aslint', 'axe', 'ed11y', 'htmlcs', 'ibm', 'nuVal', 'qualWeb', 'testaro', 'wave'
33
- ];
30
+ // Module to handle secrets.
31
+ require('dotenv').config();
32
+ // Devices recognized by Playwright.
33
+ const {devices} = require('playwright');
34
+ // Utility module.
35
+ const {isToolID, toolIDs} = require('./procs/util');
34
36
 
35
37
  // ########## FUNCTIONS
36
38
 
37
- // Creates and returns a script.
38
- exports.script = (id, what, options = {}) => {
39
- const toolsRulesData = {};
40
- // If options are specified:
41
- if (options.type && options.specs) {
42
- const {type, specs} = options;
43
- // If the option type is tools and is valid:
44
- if (
45
- type === 'tools'
46
- && Array.isArray(specs)
47
- && specs.length
48
- && specs.every(spec => toolIDs.includes(spec))
49
- ) {
50
- // Populate the data on tools and rules.
51
- specs.forEach(spec => {
52
- toolsRulesData[spec] = [];
53
- });
54
- }
55
- // Otherwise, if the option type is issues and is valid:
56
- else if (
57
- type === 'issues'
58
- && typeof specs === 'object'
59
- && specs.issues
60
- && specs.issueIDs
61
- && typeof specs.issues === 'object'
62
- && Array.isArray(specs.issueIDs)
63
- && specs.issueIDs.length
64
- ) {
65
- // For each specified issue:
66
- const {issueIDs, issues} = specs;
67
- issueIDs.forEach(issueID => {
68
- // If it exists in the classification:
69
- const issueData = issues[issueID];
70
- if (issueData) {
71
- // For each tool that tests for the issue:
72
- const issueToolIDs = Object.keys(issueData.tools);
73
- issueToolIDs.forEach(issueToolID => {
74
- // For each of the rules of the tool for the issue:
75
- toolsRulesData[issueToolID] ??= [];
76
- const toolRuleIDs = toolsRulesData[issueToolID];
77
- const toolData = issueData.tools[issueToolID];
78
- Object.keys(toolData).forEach(ruleID => {
79
- // Add the rule to the data on tools and rules.
80
- let rulePrefix = '';
81
- if (issueToolID === 'nuVal') {
82
- rulePrefix = toolData[ruleID].variable ? '~' : '=';
83
- }
84
- const fullRuleID = `${rulePrefix}${ruleID}`;
85
- if (! toolRuleIDs.includes(fullRuleID)) {
86
- toolRuleIDs.push(fullRuleID);
87
- }
88
- });
89
- });
90
- }
91
- // Otherwise, i.e. if it does not exist in the classification:
92
- else {
93
- // Report this and quit.
94
- console.log(`ERROR: Issue ${issueID} not in issue classification`);
95
- return {};
96
- }
97
- });
39
+ // Returns whether a device ID is recognized by Playwright.
40
+ const isDeviceID = exports.isDeviceID = deviceID => deviceID === 'default' || !! devices[deviceID];
41
+ // Returns options for a new browser context (window).
42
+ const getWindowOptions = deviceID => {
43
+ // If the argument is valid:
44
+ if (isDeviceID(deviceID)) {
45
+ // Set the reducedMotion option.
46
+ const options = {
47
+ reducedMotion: 'no-preference'
48
+ };
49
+ // If the default device was specified:
50
+ if (deviceID === 'default') {
51
+ // Return the set option.
52
+ return options;
98
53
  }
99
- // Otherwise, i.e. if the option specification is invalid:
54
+ // Otherwise, i.e. if a non-default device was specified:
100
55
  else {
101
- // Report this and quit.
102
- console.log(`ERROR: Options invalid`);
103
- return {};
56
+ // Get its properties.
57
+ const deviceProperties = devices[deviceID];
58
+ // Return the reduce-motion and device options.
59
+ return {
60
+ ... options,
61
+ ... deviceProperties
62
+ };
104
63
  }
105
64
  }
106
- // Otherwise, i.e. if options are not specified:
65
+ // Otherwise, i.e. if the arguments are not valid:
107
66
  else {
108
- // Populate the data on tools and rules.
109
- toolIDs.forEach(toolID => {
110
- toolsRulesData[toolID] = [];
111
- });
67
+ // Return this.
68
+ return null;
112
69
  }
113
- // Initialize a script.
114
- const scriptObj = {
115
- id,
116
- what,
117
- strict: false,
118
- isolate: true,
119
- standard: 'only',
120
- observe: false,
121
- deviceID: 'default',
122
- browserID: 'webkit',
123
- lowMotion: false,
124
- timeLimit: Math.round(50 + 30 * Object.keys(toolsRulesData).length),
125
- creationTimeStamp: '',
126
- executionTimeStamp: '',
127
- sendReportTo: process.env.SEND_REPORT_TO || '',
128
- target: {
129
- what: '',
130
- url: ''
131
- },
132
- sources: {
133
- script: id,
134
- batch: '',
135
- mergeID: '',
136
- requester: process.env.REQUESTER || ''
137
- },
138
- acts: [
139
- {
140
- type: 'placeholder',
141
- which: 'main'
70
+ };
71
+ // Creates and returns a script.
72
+ exports.script = (id, what, deviceID, options = {}) => {
73
+ // If the arguments are valid:
74
+ if (id && what && isDeviceID(deviceID)) {
75
+ const toolsRulesData = {};
76
+ // If options are specified:
77
+ const {type, specs} = options;
78
+ if (type && specs) {
79
+ // If the option type is tools and is valid:
80
+ if (
81
+ type === 'tools'
82
+ && Array.isArray(specs)
83
+ && specs.length
84
+ && specs.every(spec => isToolID(spec))
85
+ ) {
86
+ // Initialize the data on tools and rules.
87
+ specs.forEach(spec => {
88
+ toolsRulesData[spec] = [];
89
+ });
142
90
  }
143
- ]
144
- };
145
- // For each tool used:
146
- Object.keys(toolsRulesData).forEach(toolID => {
147
- // Initialize a test act for it.
148
- const toolAct = {
149
- type: 'test',
150
- which: toolID
151
- };
152
- // If rules were specified:
153
- const ruleIDs = toolsRulesData[toolID];
154
- if (ruleIDs.length) {
155
- // Add a rules array as a property to the act.
156
- toolAct.rules = ruleIDs;
157
- // If the tool is QualWeb:
158
- if (toolID === 'qualWeb') {
159
- // For each QualWeb module:
160
- const specs = [];
161
- const prefixes = {
162
- act: 'QW-ACT-R',
163
- wcag: 'QW-WCAG-T',
164
- best: 'QW-BP'
165
- };
166
- Object.keys(prefixes).forEach(prefix => {
167
- // Specify the rules of that module to be tested for.
168
- const ids = toolAct.rules.filter(id => id.startsWith(prefixes[prefix]));
169
- const integers = ids.map(id => id.slice(prefixes[prefix].length));
170
- specs.push(`${prefix}:${integers.join(',')}`);
91
+ // Otherwise, if the option type is issues and is valid:
92
+ else if (
93
+ type === 'issues'
94
+ && typeof specs === 'object'
95
+ && specs.issues
96
+ && specs.issueIDs
97
+ && typeof specs.issues === 'object'
98
+ && Array.isArray(specs.issueIDs)
99
+ && specs.issueIDs.length
100
+ ) {
101
+ const {issueIDs, issues} = specs;
102
+ // For each specified issue:
103
+ issueIDs.forEach(issueID => {
104
+ const issueData = issues[issueID];
105
+ // If it exists in the classification:
106
+ if (issueData) {
107
+ const issueToolIDs = Object.keys(issueData.tools);
108
+ // For each tool that tests for the issue:
109
+ issueToolIDs.forEach(issueToolID => {
110
+ toolsRulesData[issueToolID] ??= [];
111
+ const toolRuleIDs = toolsRulesData[issueToolID];
112
+ const toolData = issueData.tools[issueToolID];
113
+ // For each of the rules of the tool for the issue:
114
+ Object.keys(toolData).forEach(ruleID => {
115
+ // Add the rule to the data on tools and rules.
116
+ let rulePrefix = '';
117
+ if (issueToolID === 'nuVal') {
118
+ rulePrefix = toolData[ruleID].variable ? '~' : '=';
119
+ }
120
+ const fullRuleID = `${rulePrefix}${ruleID}`;
121
+ if (! toolRuleIDs.includes(fullRuleID)) {
122
+ toolRuleIDs.push(fullRuleID);
123
+ }
124
+ });
125
+ });
126
+ }
127
+ // Otherwise, i.e. if it does not exist in the classification:
128
+ else {
129
+ // Report this and quit.
130
+ console.log(`ERROR: Issue ${issueID} not in issue classification`);
131
+ return null;
132
+ }
171
133
  });
172
- // Replace the generic rule list with the QualWeb-format list.
173
- toolAct.rules = specs;
174
134
  }
175
- // Otherwise, if the tool is Testaro:
176
- else if (toolID === 'testaro') {
177
- // Prepend the inclusion option to the rule array.
178
- toolAct.rules.unshift('y');
135
+ // Otherwise, i.e. if the option specification is invalid:
136
+ else {
137
+ // Report this and quit.
138
+ console.log(`ERROR: Options invalid`);
139
+ return null;
179
140
  }
180
141
  }
181
- // Add any needed option defaults to the act.
182
- if (toolID === 'axe') {
183
- toolAct.detailLevel = 2;
184
- }
185
- else if (toolID === 'ibm') {
186
- toolAct.withItems = true;
187
- toolAct.withNewContent = false;
188
- }
189
- else if (toolID === 'qualWeb') {
190
- toolAct.withNewContent = false;
191
- }
192
- else if (toolID === 'testaro') {
193
- toolAct.withItems = true;
194
- toolAct.stopOnFail = false;
195
- }
196
- else if (toolID === 'wave') {
197
- toolAct.reportType = 4;
142
+ // Otherwise, i.e. if options are not specified:
143
+ else {
144
+ // Populate the data on tools and rules.
145
+ toolIDs.forEach(toolID => {
146
+ toolsRulesData[toolID] = [];
147
+ });
198
148
  }
199
- // Add the act to the script.
200
- scriptObj.acts.push(toolAct);
201
- });
202
- // Return the script.
203
- return scriptObj;
149
+ // Initialize a script.
150
+ const scriptObj = {
151
+ id,
152
+ what,
153
+ strict: false,
154
+ isolate: true,
155
+ standard: 'only',
156
+ observe: false,
157
+ device: {
158
+ id: deviceID,
159
+ windowOptions: {}
160
+ },
161
+ browserID: 'webkit',
162
+ timeLimit: Math.round(50 + 30 * Object.keys(toolsRulesData).length),
163
+ creationTimeStamp: '',
164
+ executionTimeStamp: '',
165
+ sendReportTo: process.env.SEND_REPORT_TO || '',
166
+ target: {
167
+ what: '',
168
+ url: ''
169
+ },
170
+ sources: {
171
+ script: id,
172
+ batch: '',
173
+ mergeID: '',
174
+ requester: process.env.REQUESTER || ''
175
+ },
176
+ acts: [
177
+ {
178
+ type: 'placeholder',
179
+ which: 'main'
180
+ }
181
+ ]
182
+ };
183
+ // Add the window options to the script.
184
+ scriptObj.device.windowOptions = getWindowOptions(deviceID);
185
+ // For each tool used:
186
+ Object.keys(toolsRulesData).forEach(toolID => {
187
+ // Initialize a test act for it.
188
+ const toolAct = {
189
+ type: 'test',
190
+ which: toolID
191
+ };
192
+ // If rules were specified:
193
+ const ruleIDs = toolsRulesData[toolID];
194
+ if (ruleIDs.length) {
195
+ // Add a rules array as a property to the act.
196
+ toolAct.rules = ruleIDs;
197
+ // If the tool is QualWeb:
198
+ if (toolID === 'qualWeb') {
199
+ // For each QualWeb module:
200
+ const specs = [];
201
+ const prefixes = {
202
+ act: 'QW-ACT-R',
203
+ wcag: 'QW-WCAG-T',
204
+ best: 'QW-BP'
205
+ };
206
+ Object.keys(prefixes).forEach(prefix => {
207
+ // Specify the rules of that module to be tested for.
208
+ const ids = toolAct.rules.filter(id => id.startsWith(prefixes[prefix]));
209
+ const integers = ids.map(id => id.slice(prefixes[prefix].length));
210
+ specs.push(`${prefix}:${integers.join(',')}`);
211
+ });
212
+ // Replace the generic rule list with the QualWeb-format list.
213
+ toolAct.rules = specs;
214
+ }
215
+ // Otherwise, if the tool is Testaro:
216
+ else if (toolID === 'testaro') {
217
+ // Prepend the inclusion option to the rule array.
218
+ toolAct.rules.unshift('y');
219
+ }
220
+ }
221
+ // Add any needed option defaults to the act.
222
+ if (toolID === 'axe') {
223
+ toolAct.detailLevel = 2;
224
+ }
225
+ else if (toolID === 'ibm') {
226
+ toolAct.withItems = true;
227
+ toolAct.withNewContent = false;
228
+ }
229
+ else if (toolID === 'qualWeb') {
230
+ toolAct.withNewContent = false;
231
+ }
232
+ else if (toolID === 'testaro') {
233
+ toolAct.withItems = true;
234
+ toolAct.stopOnFail = false;
235
+ }
236
+ else if (toolID === 'wave') {
237
+ toolAct.reportType = 4;
238
+ }
239
+ // Add the act to the script.
240
+ scriptObj.acts.push(toolAct);
241
+ });
242
+ // Return the script.
243
+ return scriptObj;
244
+ }
245
+ // Otherwise, i.e. if the arguments are not valid:
246
+ else {
247
+ // Report this and quit.
248
+ console.log(`ERROR: Arguments invalid`);
249
+ return null;
250
+ }
204
251
  }