testaro 5.2.2 → 5.3.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
@@ -89,10 +89,13 @@ A script is a JSON file with the properties:
89
89
  {
90
90
  "what": "string: description of the script",
91
91
  "strict": "boolean: whether redirections should be treated as failures",
92
+ "timeLimit": "number: limit in seconds on the execution of this script",
92
93
  "commands": "array of objects: the commands to be performed"
93
94
  }
94
95
  ```
95
96
 
97
+ The `timeLimit` property is optional. If it is omitted, a default of 300 seconds (5 minutes) is set.
98
+
96
99
  ### Example
97
100
 
98
101
  Here is an example of a script:
@@ -101,6 +104,7 @@ Here is an example of a script:
101
104
  {
102
105
  what: 'Test example.com with alfa',
103
106
  strict: true,
107
+ timeLimit: 15,
104
108
  commands: [
105
109
  {
106
110
  type: 'launch',
@@ -486,13 +490,13 @@ Relative paths must be relative to the Testaro project directory. For example, i
486
490
 
487
491
  Also ensure that Testaro can read all those directories and write to `REPORTDIR`.
488
492
 
489
- 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.
493
+ 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.
490
494
 
491
495
  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.
492
496
 
493
497
  The `high` module will call the `runJob` function of the `create` module, which in turn will call the `handleRequest` function of the `run` module. The results will be saved in report files in the `REPORTDIR` directory.
494
498
 
495
- If there is no batch, the report file will be named with a unique timestamp, suffixed with a `.json` extension. If there is a batch, then the base of each report file’s name will be the same timestamp, suffixed with `-hostID`, where `hostID` is the value of the `id` property of the `host` object in the batch file. For example, if you execute `node create script01 wikis`, you might get these report files deposited into `REPORTDIR`:
499
+ If there is no batch, the report file will be named with a unique timestamp, suffixed with a `.json` extension. If there is a batch, then the base of each report file’s name will be the same timestamp, suffixed with `-hostid`, where `hostid` is the value of the `id` property of the `host` object in the batch file. For example, if you execute `node create script01 wikis`, you might get these report files deposited into `REPORTDIR`:
496
500
  - `enp46j-wikipedia.json`
497
501
  - `enp45j-wiktionary.json`
498
502
  - `enp45j-wikidata.json`
@@ -602,6 +606,16 @@ The rationales motivating the Testaro-defined tests can be found in comments wit
602
606
 
603
607
  ## Testing challenges
604
608
 
609
+ ### Abnormal termination
610
+
611
+ On rare occasions a test throws an error that terminates the Node process and cannot be handled with a `try`-`catch` structure. It has been observed, for example, that the `ibm` test does this when run on the host at `https://zenythgroup.com/index` or `https://monsido.com`.
612
+
613
+ If a single process performed all of the commands in a batch-based script, the process could perform tens of thousands of commands, and such an error could stop the process at any point.
614
+
615
+ To handle this risk, Testaro processes batch-based jobs by forking a new process for each host. If such an error occurs, it crashes the child process, preventing a report for that host from being written. The parent process waits for the report to appear in the `REPORTDIR` directory until the time limit. When it fails to appear, the parent process continues to the next host.
616
+
617
+ If you are using high-level invocation, your terminal will show the standard output of the parent process and, if there is a batch, the current child process, too. If you interrupt the process with `CTRL-c`, you will send a `SIGINT` signal to the parent process, which will handle it by sending a message to the child process telling it to terminate itself, and then the parent process will terminate by skipping the remaining hosts.
618
+
605
619
  ### Activation
606
620
 
607
621
  Testing to determine what happens when a control or link is activated is straightforward, except in the context of a comprehensive set of tests of a single page. There, activating a control or link can change the page or navigate away from it, interfering with the remaining planned tests of the page.
package/create.js CHANGED
@@ -9,6 +9,7 @@
9
9
  require('dotenv').config();
10
10
  // Module to read and write files.
11
11
  const fs = require('fs/promises');
12
+ const {fork} = require('child_process');
12
13
  const {handleRequest} = require('./run');
13
14
  // Module to convert a script and a batch to a batch-based array of scripts.
14
15
  const {batchify} = require('./batchify');
@@ -21,10 +22,10 @@ const reportDir = process.env.REPORTDIR;
21
22
  // ########## FUNCTIONS
22
23
 
23
24
  // Runs one script and writes a report file.
24
- const runHost = async (id, script, host = {}) => {
25
+ const runHost = async (id, script) => {
25
26
  const report = {
26
27
  id,
27
- host,
28
+ host: {},
28
29
  log: [],
29
30
  script,
30
31
  acts: []
@@ -35,10 +36,19 @@ const runHost = async (id, script, host = {}) => {
35
36
  };
36
37
  // Runs a file-based job and writes a report file for the script or each host.
37
38
  exports.runJob = async (scriptID, batchID) => {
39
+ let healthy = true;
40
+ let childAlive = true;
41
+ process.on('SIGINT', () => {
42
+ console.log('ERROR: Terminal interrupted runJob');
43
+ healthy = false;
44
+ });
38
45
  if (scriptID) {
39
46
  try {
40
47
  const scriptJSON = await fs.readFile(`${scriptDir}/${scriptID}.json`, 'utf8');
41
48
  const script = JSON.parse(scriptJSON);
49
+ // Get the time limit of the script or, if none, set it to 5 minutes.
50
+ let {timeLimit} = script;
51
+ timeLimit = timeLimit || 300;
42
52
  // Identify the start time and a timestamp.
43
53
  const timeStamp = Math.floor((Date.now() - Date.UTC(2022, 1)) / 2000).toString(36);
44
54
  // If there is a batch:
@@ -48,28 +58,89 @@ exports.runJob = async (scriptID, batchID) => {
48
58
  const batchJSON = await fs.readFile(`${batchDir}/${batchID}.json`, 'utf8');
49
59
  batch = JSON.parse(batchJSON);
50
60
  const specs = batchify(script, batch, timeStamp);
51
- // For each host script:
52
- while (specs.length) {
53
- const spec = specs.shift();
54
- const {id, host, script} = spec;
55
- // Run it and save the result with a host-suffixed ID.
56
- await runHost(id, script, host);
57
- }
61
+ const batchSize = specs.length;
62
+ const sizedRep = `${batchSize} report${batchSize > 1 ? 's' : ''}`;
63
+ const timeoutHosts = [];
64
+ const crashHosts = [];
65
+ // FUNCTION DEFINITION START
66
+ // Recursively runs host scripts.
67
+ const runHosts = specs => {
68
+ // If any scripts remain to be run and the process has not been interrupted:
69
+ if (specs.length && healthy) {
70
+ childAlive = true;
71
+ // Run the first one and save the report with a host-suffixed ID.
72
+ const spec = specs.shift();
73
+ const {id, host, script} = spec;
74
+ const subprocess = fork(
75
+ 'runHost', [id, JSON.stringify(script), JSON.stringify(host)],
76
+ {
77
+ detached: true,
78
+ stdio: [0, 1, 'ipc']
79
+ }
80
+ );
81
+ subprocess.on('exit', () => {
82
+ childAlive = false;
83
+ });
84
+ const startTime = Date.now();
85
+ // At 5-second intervals:
86
+ const reCheck = setInterval(async () => {
87
+ // If the user has not interrupted the process:
88
+ if (healthy) {
89
+ // If there is no need to keep checking:
90
+ const reportNames = await fs.readdir(reportDir);
91
+ const timedOut = Date.now() - startTime > 1000 * timeLimit;
92
+ if (timedOut || reportNames.includes(`${id}.json`) || ! childAlive) {
93
+ // Stop checking.
94
+ clearInterval(reCheck);
95
+ // If the cause is a timeout:
96
+ if (timedOut) {
97
+ // Add the host to the array of timed-out hosts.
98
+ timeoutHosts.push(id);
99
+ }
100
+ // Otherwise, if the cause is a child crash:
101
+ else if (! childAlive) {
102
+ // Add the host to the array of crashed hosts.
103
+ crashHosts.push(id);
104
+ }
105
+ // Run the script of the next host.
106
+ runHosts(specs);
107
+ }
108
+ }
109
+ // Otherwise, i.e. if the user has interrupted the process:
110
+ else {
111
+ // Tell the script run to quit.
112
+ subprocess.send('interrupt');
113
+ // Stop checking.
114
+ clearInterval(reCheck);
115
+ }
116
+ }, 5000);
117
+ }
118
+ else {
119
+ console.log(`${sizedRep} ${timeStamp}-....json in ${process.env.REPORTDIR}`);
120
+ if (timeoutHosts.length) {
121
+ console.log(`Reports not created:\n${JSON.stringify(timeoutHosts), null, 2}`);
122
+ }
123
+ if (crashHosts.length) {
124
+ console.log(`Hosts crashed:\n${JSON.stringify(crashHosts), null, 2}`);
125
+ }
126
+ }
127
+ };
128
+ // FUNCTION DEFINITION END
129
+ // Recursively run each host script and save the reports.
130
+ runHosts(specs);
58
131
  }
59
132
  // Otherwise, i.e. if there is no batch:
60
133
  else {
61
134
  // Run the script and save the result with a timestamp ID.
62
135
  await runHost(timeStamp, script);
136
+ console.log(`Report ${timeStamp}.json in ${process.env.REPORTDIR}`);
63
137
  }
64
- return timeStamp;
65
138
  }
66
139
  catch(error) {
67
140
  console.log(`ERROR: ${error.message}\n${error.stack}`);
68
- return null;
69
141
  }
70
142
  }
71
143
  else {
72
144
  console.log('ERROR: no script specified');
73
- return null;
74
145
  }
75
146
  };
package/high.js CHANGED
@@ -1,14 +1,10 @@
1
1
  /*
2
2
  high.js
3
3
  Invokes Testaro with the high-level method.
4
- Usage example: node high tp11 weborgs
4
+ Usage example: node high tp12 weborgs
5
5
  */
6
6
 
7
7
  const {runJob} = require('./create');
8
8
  const scriptID = process.argv[2];
9
9
  const batchID = process.argv[3];
10
- const run = async (scriptID, batchID) => {
11
- const timeStamp = await runJob(scriptID, batchID);
12
- console.log(`Reports in ${process.env.REPORTDIR}; ID base ${timeStamp}`);
13
- };
14
- run(scriptID, batchID);
10
+ runJob(scriptID, batchID);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "5.2.2",
3
+ "version": "5.3.0",
4
4
  "description": "Automation of accessibility testing",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/run.js CHANGED
@@ -633,6 +633,12 @@ const wait = ms => {
633
633
  };
634
634
  // Recursively performs the acts in a report.
635
635
  const doActs = async (report, actIndex, page) => {
636
+ process.on('message', message => {
637
+ if (message === 'interrupt') {
638
+ console.log('ERROR: Terminal interrupted doActs');
639
+ process.exit();
640
+ }
641
+ });
636
642
  const {acts} = report;
637
643
  // If any more commands are to be performed:
638
644
  if (actIndex > -1 && actIndex < acts.length) {
@@ -1310,8 +1316,7 @@ const doScript = async (report) => {
1310
1316
  const injectLaunches = acts => {
1311
1317
  let injectMore = true;
1312
1318
  while (injectMore) {
1313
- const injectIndex = acts.findIndex(
1314
- (act, index) =>
1319
+ const injectIndex = acts.findIndex((act, index) =>
1315
1320
  index < acts.length - 1
1316
1321
  && act.type === 'test'
1317
1322
  && acts[index + 1].type === 'test'
package/runHost.js ADDED
@@ -0,0 +1,36 @@
1
+ /*
2
+ runHost.js
3
+ Runs a host job and writes a report file.
4
+ */
5
+
6
+ // ########## IMPORTS
7
+
8
+ // Module to keep secrets.
9
+ require('dotenv').config();
10
+ // Module to read and write files.
11
+ const fs = require('fs/promises');
12
+ const {handleRequest} = require('./run');
13
+
14
+ // ########## CONSTANTS
15
+ const reportDir = process.env.REPORTDIR;
16
+
17
+ // ########## FUNCTIONS
18
+
19
+ // Runs one script and writes a report file.
20
+ const runHost = async (id, scriptJSON, hostJSON) => {
21
+ const report = {
22
+ id,
23
+ host: JSON.parse(hostJSON),
24
+ log: [],
25
+ script: JSON.parse(scriptJSON),
26
+ acts: []
27
+ };
28
+ await handleRequest(report);
29
+ const reportJSON = JSON.stringify(report, null, 2);
30
+ await fs.writeFile(`${reportDir}/${id}.json`, reportJSON);
31
+ process.disconnect();
32
+ process.exit();
33
+ };
34
+
35
+ // ########## OPERATION
36
+ runHost(... process.argv.slice(2));
package/tests/hover.js CHANGED
@@ -158,7 +158,10 @@ const find = async (withItems, page, region, sample, popRatio) => {
158
158
  position: {
159
159
  x: 0,
160
160
  y: 0
161
- }
161
+ },
162
+ timeout: 500,
163
+ force: true,
164
+ noWaitAfter: true
162
165
  });
163
166
  // Wait for any delayed and/or slowed hover reaction.
164
167
  await page.waitForTimeout(200);
package/tests/htmlcs.js CHANGED
@@ -28,7 +28,7 @@ exports.reporter = async page => {
28
28
  }
29
29
  return issues;
30
30
  }, standard);
31
- if (nextIssues) {
31
+ if (nextIssues && nextIssues.every(issue => typeof issue === 'string')) {
32
32
  messageStrings.push(... nextIssues);
33
33
  }
34
34
  else {
package/tests/ibm.js CHANGED
@@ -104,7 +104,7 @@ exports.reporter = async (page, withItems, withNewContent) => {
104
104
  result.content = await doTest(typeContent, withItems, timeLimit);
105
105
  if (result.content.prevented) {
106
106
  result.prevented = true;
107
- console.log('ERROR: Getting ibm test report from page took too long');
107
+ console.log(`ERROR: Getting ibm test report from page timed out at ${timeLimit} seconds`);
108
108
  }
109
109
  }
110
110
  // If a test with new content is to be performed:
@@ -114,7 +114,7 @@ exports.reporter = async (page, withItems, withNewContent) => {
114
114
  result.url = await doTest(typeContent, withItems, timeLimit);
115
115
  if (result.url.prevented) {
116
116
  result.prevented = true;
117
- console.log('ERROR: Getting ibm test report from URL took too long');
117
+ console.log(`ERROR: Getting ibm test report from URL timed out at ${timeLimit} seconds`);
118
118
  }
119
119
  }
120
120
  await close();
package/tests/tabNav.js CHANGED
@@ -103,7 +103,9 @@ exports.reporter = async (page, withItems) => {
103
103
  });
104
104
  })
105
105
  .catch(error => {
106
- console.log(`ERROR: could not click tab element ${itemData.text} (${error.message})`);
106
+ console.log(
107
+ `ERROR clicking tab element ${itemData.text} (${error.message.replace(/\n.+/s, '')})`
108
+ );
107
109
  pressed = false;
108
110
  });
109
111
  // Increment the counts of navigations and key navigations.