doc-detective 1.0.1 → 1.0.3

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
@@ -6,7 +6,7 @@ Doc Detective ingests text files, parses them for test actions, then executes th
6
6
 
7
7
  This project handles test parsing and web-based UI testing--it doesn't support results reporting or notifications. This framework is a part of testing infrastructures and needs to be complimented by other components.
8
8
 
9
- Doc Detective uses `puppeteer` to install, launch, and drive Chromium to perform tests. `puppeteer` removes the requirement to manually configure a local web browser and enables easy screenshotting and video recording.
9
+ Doc Detective uses `puppeteer` to install, launch, and drive Chromium to perform tests. `puppeteer` removes the requirement to manually configure a local web browser and enables easy screenshotting and video recording. In the event `puppeteer` fails to launch Chromium, Doc Detective tries to fall back to local installs of Chromium, Chrome, and Firefox.
10
10
 
11
11
  **Note:** By default, `puppeteer`'s Chromium doesn't run in Docker containers, which means that `puppeteer` doesn't work either. Don't run Doc Detective in a Docker container unless you first confirm that you have a custom implementation of headless Chrome/Chromium functional in the container. The approved answer to [this question](https://askubuntu.com/questions/79280/how-to-install-chrome-browser-properly-via-command-line) works for me, but it may not work in all environments.
12
12
 
@@ -18,8 +18,6 @@ You can use Doc Detective as an [NPM package](#npm-package) or a standalone [CLI
18
18
 
19
19
  Doc Detective integrates with Node projects as an NPM package. When using the NPM package, you must specify all options in the `run()` method's `config` argument, which is a JSON object with the same structure as [config.json](https://github.com/hawkeyexl/doc-detective/blob/master/sample/config.json).
20
20
 
21
- 0. Install prerequisites:
22
- - [FFmpeg](https://ffmpeg.org/) (Only required if you want the [Start recording](#start-recording) action to output GIFs. Not required for MP4 output.)
23
21
  1. In a terminal, navigate to your Node project, then install Doc Detective:
24
22
 
25
23
  ```bash
@@ -45,7 +43,6 @@ You can run Doc Detective as a standalone CLI tool. When running as a CLI tool,
45
43
  0. Install prerequisites:
46
44
 
47
45
  - [Node.js](https://nodejs.org/)
48
- - [FFmpeg](https://ffmpeg.org/) (Only required if you want the [Start recording](#start-recording) action to output GIFs. Not required for MP4 output.)
49
46
 
50
47
  1. In a terminal, clone the repo and install dependencies:
51
48
 
@@ -124,7 +121,7 @@ Advanced format:
124
121
  "type": {
125
122
  "keys": "$SHORTHAIR_CAT_SEARCH",
126
123
  "trailingSpecialKey": "Enter",
127
- "envs": "./sample/variables.env"
124
+ "env": "./sample/variables.env"
128
125
  }
129
126
  }
130
127
  ```
@@ -189,7 +186,7 @@ Advanced format with an environment variable:
189
186
  "css": "input#password",
190
187
  "keys": "$PASSWORD",
191
188
  "trailingSpecialKey": "Enter",
192
- "envs": "./sample/variables.env"
189
+ "env": "./sample/variables.env"
193
190
  }
194
191
  ```
195
192
 
@@ -258,9 +255,33 @@ Format:
258
255
  }
259
256
  ```
260
257
 
258
+ ### HTTP request
259
+
260
+ Perform a generic HTTP request, for example to a REST API. Checks if the server returns an acceptable status code. If `uri` doesn't include a protocol, the protocol defaults to HTTPS. If `statusCodes` isn't specified, defaults to `[200]`.
261
+
262
+ Format:
263
+
264
+ ```json
265
+ {
266
+ "action": "httpRequest",
267
+ "uri": "https://www.api-server.com",
268
+ "method": "post",
269
+ "requestHeaders": {
270
+ "header": "value"
271
+ },
272
+ "requestParams": {
273
+ "param": "value"
274
+ },
275
+ "requestData": {
276
+ "field": "value"
277
+ },
278
+ "statusCodes": [ 200 ]
279
+ }
280
+ ```
281
+
261
282
  ### Check a link
262
283
 
263
- Check if a link returns an acceptable status code from a GET request. If `uri` doesn't include a protocol, the protocol defaults to HTTPS. If `statuscodes` isn't specified, defaults to `[200]`.
284
+ Check if a link returns an acceptable status code from a GET request. If `uri` doesn't include a protocol, the protocol defaults to HTTPS. If `statusCodes` isn't specified, defaults to `[200]`.
264
285
 
265
286
  Format:
266
287
 
@@ -276,7 +297,7 @@ Format:
276
297
 
277
298
  Start recording the current browser viewport. Must be followed by a `stopRecording` action. Supported extensions: .mp4, .gif
278
299
 
279
- **Note:** `.gif` format is **not** recommended. Because of file format and encoding differences, `.gif` files tend to be ~6.5 times larger than `.mp4` files, and with lower visual fidelity. But if `.gif` is a hard requirement for you, it's here. Creating `.gif` files requires `ffmpeg` installed on the machine that runs Doc Detective and also creates `.mp4` files of the recordings.
300
+ **Note:** `.gif` format is **not** recommended. Because of file format and encoding differences, `.gif` files tend to be ~6.5 times larger than `.mp4` files, and with lower visual fidelity. But if `.gif` is a hard requirement for you, it's here.
280
301
 
281
302
  Format:
282
303
 
@@ -521,22 +542,24 @@ Analytics reporting is off by default. If you want to make extra sure that Doc D
521
542
 
522
543
  **Note:** Updating Doc Detective may revert any modified code, so be ready to make code edits repeatedly.
523
544
 
524
- ## Potential future features
545
+ ## Potential future updates
525
546
 
526
- - Browser auto-detection and fallback.
527
- - Improved default config experience.
528
- - Environment variable overrides for config options.
529
547
  - Docker image with bundled Chromium/Chrome/Firefox.
530
548
  - New/upgraded test actions:
531
- - New: Curl commands. (Support substitution/setting env vars. Only check for `200 OK`.)
532
549
  - New: Test if a referenced image (such as an icon) is present in the captured screenshot.
550
+ - Upgrade: `httpRequest` response data validation.
551
+ - Upgrade: `httpRequest` response header validation.
552
+ - Upgrade: Additional `httpRequest` input sanitization.
533
553
  - Upgrade: `screenshot` and `startRecording` boolean for whether to perform the action or not if the expected output file already exists.
534
- - Upgrade: `startRecording` to remove MP4 when the output is a GIF.
535
554
  - Upgrade: `startRecording` and `stopRecording` to support start, stop, and intermediate test action state image matching to track differences between video captures from different runs.
536
555
  - Upgrade: `startRecording` to store the output file in a different location if a recorded action fails. This could help with debugging.
537
556
  - In-content test framing to identify when content is covered by a test defined in another file. This could enable content coverage analysis.
538
557
  - Suggest tests by parsing document text.
539
558
  - Automatically insert suggested tests based on document text.
559
+ - Detailed field descriptions per action.
560
+ - Refactor tests into individual files.
561
+ - Rewrite cross-action recording status tracking.
562
+
540
563
 
541
564
  ## License
542
565
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doc-detective",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Unit test documentation (and record videos of those tests).",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -24,6 +24,7 @@
24
24
  },
25
25
  "homepage": "https://github.com/hawkeyexl/doc-detective#readme",
26
26
  "dependencies": {
27
+ "@ffmpeg-installer/ffmpeg": "^1.1.0",
27
28
  "axios": "^0.27.2",
28
29
  "dotenv": "^16.0.2",
29
30
  "n-readlines": "^1.0.1",
@@ -2,6 +2,8 @@
2
2
  "env": "sample/variables.env",
3
3
  "input": "sample/doc-content.md",
4
4
  "output": "sample/results.json",
5
+ "setup": "",
6
+ "cleanup": "",
5
7
  "recursive": true,
6
8
  "testExtensions": [
7
9
  ".md",
package/sample/tests.json CHANGED
@@ -85,6 +85,13 @@
85
85
  "statusCodes": [
86
86
  200
87
87
  ]
88
+ },
89
+ {
90
+ "action": "httpRequest",
91
+ "uri": "$URL",
92
+ "statusCodes": [
93
+ 200
94
+ ]
88
95
  }
89
96
  ]
90
97
  }
package/src/config.json CHANGED
@@ -2,6 +2,8 @@
2
2
  "env": "",
3
3
  "input": ".",
4
4
  "output": "./results.json",
5
+ "setup": "",
6
+ "cleanup": "",
5
7
  "recursive": true,
6
8
  "testExtensions": [
7
9
  ".md",
package/src/index.js CHANGED
@@ -40,8 +40,6 @@ async function main(config, argv) {
40
40
 
41
41
  // Run tests
42
42
  const results = await runTests(config, tests);
43
- log(config, "debug", `RESULTS:`);
44
- log(config, "debug", results);
45
43
 
46
44
  // Output
47
45
  outputResults(config, results);
@@ -483,7 +483,7 @@ async function sendAnalytics(config, results) {
483
483
  .then(() => {
484
484
  log(
485
485
  config,
486
- "info",
486
+ "debug",
487
487
  `Sucessfully sent analytics to ${server.displayname}.`
488
488
  );
489
489
  })
@@ -0,0 +1,208 @@
1
+ const axios = require("axios");
2
+ const { setEnvs } = require("../utils");
3
+
4
+ exports.httpRequest = httpRequest;
5
+
6
+ async function httpRequest(action) {
7
+ const methods = ["get", "post", "put", "patch", "delete"];
8
+
9
+ let status;
10
+ let description;
11
+ let result;
12
+ let uri;
13
+ let method;
14
+ let statusCodes = [];
15
+ let request = {};
16
+ let response;
17
+ let defaultPayload = {
18
+ uri: "",
19
+ method: "GET",
20
+ headers: {},
21
+ params: {},
22
+ requestData: {},
23
+ responseData: {},
24
+ statusCodes: ["200"],
25
+ };
26
+
27
+ // Load environment variables
28
+ if (action.env) {
29
+ let result = await setEnvs(action.env);
30
+ if (result.status === "FAIL") return { result };
31
+ }
32
+
33
+ // URI
34
+ //// Define
35
+ if (action.uri[0] === "$") {
36
+ uri = process.env[action.uri.substring(1)];
37
+ } else {
38
+ uri = action.uri || defaultPayload.uri;
39
+ }
40
+ //// Validate
41
+ if (!uri || typeof uri != "string") {
42
+ //Fail
43
+ } else if (uri.indexOf("://") < 0) {
44
+ // Insert HTTPS if no protocol present
45
+ uri = `https://${uri}`;
46
+ }
47
+ //// Set request
48
+ request.url = uri;
49
+
50
+ // Method
51
+ //// Define
52
+ if (action.method && action.method[0] === "$") {
53
+ method = process.env[action.method.substring(1)];
54
+ } else {
55
+ method = action.method || defaultPayload.method;
56
+ }
57
+ //// Sanitize
58
+ method = method.toLowerCase();
59
+ //// Validate
60
+ if (!method || typeof method != "string" || methods.indexOf(method) < 0) {
61
+ // No/undefined method, method isn't a string, method isn't an accepted enum
62
+ status = "FAIL";
63
+ description = `Invalid HTTP method: ${method}`;
64
+ result = { status, description };
65
+ return { result };
66
+ }
67
+ //// Set request
68
+ request.method = method;
69
+
70
+ // Headers
71
+ if (action.headers && JSON.stringify(action.headers) != "{}") {
72
+ //// Define
73
+ if (action.headers[0] === "$") {
74
+ headers = process.env[action.headers.substring(1)];
75
+ headers = JSON.parse(headers);
76
+ } else {
77
+ headers = action.headers || defaultPayload.headers;
78
+ }
79
+ //// Validate
80
+ //// Set request
81
+ if (JSON.stringify(headers) != "{}") request.headers = headers;
82
+ }
83
+
84
+ // Params
85
+ if (action.params && JSON.stringify(action.params) != "{}") {
86
+ //// Define
87
+ if (action.params[0] === "$") {
88
+ params = process.env[action.params.substring(1)];
89
+ params = JSON.parse(params);
90
+ } else {
91
+ params = action.params || defaultPayload.params;
92
+ }
93
+ //// Validate
94
+ //// Set request
95
+ if (params != {}) request.params = params;
96
+ }
97
+
98
+ // requestData
99
+ if (action.requestData) {
100
+ //// Define
101
+ if (action.requestData[0] === "$") {
102
+ requestData = process.env[action.requestData.substring(1)];
103
+ requestData = JSON.parse(requestData);
104
+ } else {
105
+ requestData = action.requestData || defaultPayload.requestData;
106
+ }
107
+ //// Validate
108
+ //// Set request
109
+ if (requestData != {}) request.data = requestData;
110
+ }
111
+
112
+ // // responseData
113
+ // //// Define
114
+ // if (action.responseData && action.responseData[0] === "$") {
115
+ // responseData = process.env[action.responseData.substring(1)];
116
+ // } else {
117
+ // responseData = action.responseData || defaultPayload.responseData;
118
+ // }
119
+ // //// Validate
120
+
121
+ // Status codes
122
+ //// Define
123
+ statusCodes = action.statusCodes || defaultPayload.statusCodes;
124
+ //// Sanitize
125
+ for (i = 0; i < statusCodes.length; i++) {
126
+ if (typeof statusCodes[i] === "string")
127
+ statusCodes[i] = Number(statusCodes[i]);
128
+ }
129
+ //// Validate
130
+ if (statusCodes === []) statusCodes = defaultPayload.statusCodes;
131
+
132
+ // Send request
133
+ response = await axios(request)
134
+ .then((response) => {
135
+ return response;
136
+ })
137
+ .catch((error) => {
138
+ return { error };
139
+ });
140
+
141
+ // If request returned an error
142
+ if (response.error) {
143
+ status = "FAIL";
144
+ description = `Error: ${JSON.stringify(response.error.response)}`;
145
+ result = { status, description };
146
+ return { result };
147
+ }
148
+
149
+ // Compare status codes
150
+ if (statusCodes.indexOf(response.status) >= 0) {
151
+ status = "PASS";
152
+ description = `Returned ${response.status}.`;
153
+ } else {
154
+ status = "FAIL";
155
+ description = `Returned ${
156
+ response.status
157
+ }. Expected one of ${JSON.stringify(statusCodes)}`;
158
+ }
159
+
160
+ // // Compare response and responseData
161
+ // if (JSON.stringify(responseData) != "{}") {
162
+ // dataComparison = containsJsonValues(responseData, response.data);
163
+ // if ((dataComparison.result.status = "PASS")) {
164
+ // status = "PASS";
165
+ // description =
166
+ // description +
167
+ // ` Expected response data was present in actual response data.`;
168
+ // } else {
169
+ // status = "FAIL";
170
+ // description = description + " " + dataComparison.result.description;
171
+ // }
172
+ // }
173
+
174
+ description = description.trim();
175
+ result = { status, description };
176
+ return { result };
177
+ }
178
+
179
+ function containsJsonValues(expected, actual) {
180
+ let status = "PASS";
181
+ let description = "";
182
+ Object.keys(expected).forEach((key) => {
183
+ if (!actual.hasOwnProperty(key)) {
184
+ // Key doesn't exist in actual
185
+ description =
186
+ description + `The '${key}' key did't exist in returned JSON. `;
187
+ status = "FAIL";
188
+ } else if (typeof expected[key] === "object") {
189
+ // Nested object recursion
190
+ result = containsJsonValues(expected[key], actual[key]);
191
+ if (result.result.status === "FAIL") status = "FAIL";
192
+ if (result.result.description != "")
193
+ description = description + " " + result.result.description;
194
+ } else if (expected[key] != actual[key]) {
195
+ // Actual value doesn't match expected
196
+ description =
197
+ description +
198
+ `The '${key}' key did't match the expected value. Expected: ${expected[key]}. Actual: ${actual[key]}. `;
199
+ status = "FAIL";
200
+ }
201
+
202
+ if (status === "FAIL") {
203
+ description = description.trim();
204
+ }
205
+ });
206
+ result = { status, description };
207
+ return { result };
208
+ }
package/src/lib/tests.js CHANGED
@@ -11,9 +11,27 @@ const PNG = require("pngjs").PNG;
11
11
  const pixelmatch = require("pixelmatch");
12
12
  const uuid = require("uuid");
13
13
  const axios = require("axios");
14
+ const { httpRequest } = require("./tests/httpRequest");
14
15
 
15
16
  exports.runTests = runTests;
16
17
 
18
+ const defaultBrowserPaths = {
19
+ linux: [
20
+ "/usr/bin/chromium-browser",
21
+ "/usr/bin/google-chrome",
22
+ "/usr/bin/firefox",
23
+ ],
24
+ darwin: [
25
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
26
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
27
+ "/Applications/Firefox.app/Contents/MacOS/firefox-bin",
28
+ ],
29
+ win32: [
30
+ "C:/Program Files/Google/Chrome/Application/chrome.exe",
31
+ "C:/Program Files/Mozilla Firefox/firefox.exe",
32
+ ],
33
+ };
34
+
17
35
  async function runTests(config, tests) {
18
36
  // Instantiate browser
19
37
  let browserConfig = {
@@ -27,14 +45,49 @@ async function runTests(config, tests) {
27
45
  },
28
46
  };
29
47
  try {
48
+ log(config, "debug", "Launching browser.");
30
49
  browser = await puppeteer.launch(browserConfig);
31
50
  } catch {
32
- log(config,"error","Couldn't open browser.");
33
- exit(1);
51
+ if (
52
+ process.platform === "linux" ||
53
+ process.platform === "darwin" ||
54
+ process.platform === "win32"
55
+ ) {
56
+ for (i = 0; i < defaultBrowserPaths[process.platform].length; i++) {
57
+ if (fs.existsSync(defaultBrowserPaths[process.platform][i])) {
58
+ log(
59
+ config,
60
+ "debug",
61
+ `Attempting browser fallback: ${
62
+ defaultBrowserPaths[process.platform][i]
63
+ }`
64
+ );
65
+ browserConfig.executablePath =
66
+ defaultBrowserPaths[process.platform][i];
67
+ try {
68
+ browser = await puppeteer.launch(browserConfig);
69
+ break;
70
+ } catch {}
71
+ }
72
+ if (i === defaultBrowserPaths[process.platform].length) {
73
+ log(
74
+ config,
75
+ "error",
76
+ "Couldn't open browser. Failed browser fallback."
77
+ );
78
+ exit(1);
79
+ }
80
+ }
81
+ } else {
82
+ log(config, "error", "Couldn't open browser.");
83
+ exit(1);
84
+ }
34
85
  }
35
86
 
36
87
  // Iterate tests
88
+ log(config, "info", "Running tests.");
37
89
  for (const test of tests.tests) {
90
+ log(config, "debug", `TEST: ${test.id}`);
38
91
  let pass = 0;
39
92
  let warning = 0;
40
93
  let fail = 0;
@@ -45,6 +98,7 @@ async function runTests(config, tests) {
45
98
  await installMouseHelper(page);
46
99
  // Iterate through actions
47
100
  for (const action of test.actions) {
101
+ log(config, "debug", `ACTION: ${JSON.stringify(action)}`);
48
102
  action.result = await runAction(config, action, page, videoDetails);
49
103
  if (action.result.videoDetails) {
50
104
  videoDetails = action.result.videoDetails;
@@ -53,6 +107,7 @@ async function runTests(config, tests) {
53
107
  if (action.result.status === "FAIL") fail++;
54
108
  if (action.result.status === "WARNING") warning++;
55
109
  if (action.result.status === "PASS") pass++;
110
+ log(config, "debug", `RESULT: ${action.result.status}. ${action.result.description}`);
56
111
  }
57
112
 
58
113
  // Close open recorders/pages
@@ -188,9 +243,13 @@ async function runAction(config, action, page, videoDetails) {
188
243
  case "checkLink":
189
244
  result = await checkLink(action);
190
245
  break;
246
+ case "httpRequest":
247
+ result = await httpRequest(action);
248
+ break;
191
249
  }
192
250
  return result;
193
251
  }
252
+
194
253
  async function checkLink(action) {
195
254
  let status;
196
255
  let description;
package/src/lib/utils.js CHANGED
@@ -50,6 +50,16 @@ function setArgs(args) {
50
50
  description: "Path for a JSON file of test result output.",
51
51
  type: "string",
52
52
  })
53
+ .option("setup", {
54
+ description:
55
+ "Path to a file or directory to parse for tests to run before 'input' tests. Useful for preparing environments to perform tests.",
56
+ type: "string",
57
+ })
58
+ .option("cleanup", {
59
+ description:
60
+ "Path to a file or directory to parse for tests to run after 'input' tests. Useful for resetting environments after tests run.",
61
+ type: "string",
62
+ })
53
63
  .option("recursive", {
54
64
  alias: "r",
55
65
  description:
@@ -122,7 +132,8 @@ function setArgs(args) {
122
132
  function setLogLevel(config, argv) {
123
133
  let logLevel = "";
124
134
  let enums = ["debug", "info", "warning", "error", "silent"];
125
- logLevel = argv.logLevel || process.env.DOC_LOG_LEVEL || config.logLevel || "info";
135
+ logLevel =
136
+ argv.logLevel || process.env.DOC_LOG_LEVEL || config.logLevel || "info";
126
137
  logLevel = String(logLevel).toLowerCase();
127
138
  if (enums.indexOf(logLevel) >= 0) {
128
139
  config.logLevel = logLevel;
@@ -159,7 +170,7 @@ function selectConfig(config, argv) {
159
170
  log(config, "debug", "Loaded config from function parameter.");
160
171
  } else {
161
172
  // Default
162
- config = defaultConfig;
173
+ config = JSON.parse(JSON.stringify(defaultConfig));
163
174
  setLogLevel(config, argv);
164
175
  log(
165
176
  config,
@@ -172,16 +183,18 @@ function selectConfig(config, argv) {
172
183
 
173
184
  function setEnv(config, argv) {
174
185
  config.env = argv.env || process.env.DOC_ENV_PATH || config.env;
175
- config.env = path.resolve(config.env);
176
- if (config.env && fs.existsSync(config.env)) {
177
- let envResult = setEnvs(config.env);
178
- if (envResult.status === "PASS")
179
- log(config, "debug", `Env file set: ${config.env}`);
180
- if (envResult.status === "FAIL")
181
- log(config, "warning", `File format issue. Can't load env file.`);
182
- } else if (config.env && !fs.existsSync(config.env)) {
183
- log(config, "warning", `Invalid file path. Can't load env file.`);
184
- } else if (!config.env) {
186
+ if (config.env) {
187
+ config.env = path.resolve(config.env);
188
+ if (fs.existsSync(config.env)) {
189
+ let envResult = setEnvs(config.env);
190
+ if (envResult.status === "PASS")
191
+ log(config, "debug", `Env file set: ${config.env}`);
192
+ if (envResult.status === "FAIL")
193
+ log(config, "warning", `File format issue. Can't load env file.`);
194
+ } else {
195
+ log(config, "warning", `Invalid file path. Can't load env file.`);
196
+ }
197
+ } else {
185
198
  log(config, "debug", "No env file specified.");
186
199
  }
187
200
  return config;
@@ -189,9 +202,17 @@ function setEnv(config, argv) {
189
202
 
190
203
  function setInput(config, argv) {
191
204
  config.input = argv.input || process.env.DOC_INPUT_PATH || config.input;
192
- config.input = path.resolve(config.input);
193
- if (fs.existsSync(config.input)) {
194
- log(config, "debug", `Input path set: ${config.input}`);
205
+ if (config.input) {
206
+ config.input = path.resolve(config.input);
207
+ if (fs.existsSync(config.input)) {
208
+ log(config, "debug", `Input path set: ${config.input}`);
209
+ } else {
210
+ log(
211
+ config,
212
+ "warning",
213
+ `Invalid input path. Reverted to default: ${config.input}`
214
+ );
215
+ }
195
216
  } else {
196
217
  config.input = path.resolve(defaultConfig.input);
197
218
  log(
@@ -210,6 +231,40 @@ function setOutput(config, argv) {
210
231
  return config;
211
232
  }
212
233
 
234
+ function setSetup(config, argv) {
235
+ config.setup = argv.setup || process.env.DOC_SETUP || config.setup;
236
+ if (config.setup === "") {
237
+ log(config, "debug", `No setup tests.`);
238
+ return config;
239
+ } else {
240
+ config.setup = path.resolve(config.setup);
241
+ if (fs.existsSync(config.setup)) {
242
+ log(config, "debug", `Setup tests path set: ${config.setup}`);
243
+ } else {
244
+ config.setup = defaultConfig.setup;
245
+ log(config, "warning", `Invalid setup tests path.`);
246
+ }
247
+ return config;
248
+ }
249
+ }
250
+
251
+ function setCleanup(config, argv) {
252
+ config.cleanup = argv.cleanup || process.env.DOC_CLEANUP || config.cleanup;
253
+ if (config.cleanup === "") {
254
+ log(config, "debug", `No cleanup tests.`);
255
+ return config;
256
+ } else {
257
+ config.cleanup = path.resolve(config.cleanup);
258
+ if (fs.existsSync(config.cleanup)) {
259
+ log(config, "debug", `Cleanup tests path set: ${config.cleanup}`);
260
+ } else {
261
+ config.cleanup = defaultConfig.cleanup;
262
+ log(config, "warning", `Invalid cleanup tests path.`);
263
+ }
264
+ return config;
265
+ }
266
+ }
267
+
213
268
  function setMediaDirectory(config, argv) {
214
269
  config.mediaDirectory =
215
270
  argv.mediaDir ||
@@ -320,24 +375,24 @@ function setBrowserPath(config, argv) {
320
375
  argv.browserPath ||
321
376
  process.env.DOC_BROWSER_PATH ||
322
377
  config.browserOptions.path;
323
- if (config.browserOptions.path === "") {
324
- log(config, "debug", `Browser set to default Chromium install.`);
325
- return config;
378
+ if (config.browserOptions.path === "") {
379
+ log(config, "debug", `Browser set to default Chromium install.`);
380
+ return config;
381
+ } else {
382
+ config.browserOptions.path = path.resolve(config.browserOptions.path);
383
+ if (fs.existsSync(config.browserOptions.path)) {
384
+ log(config, "debug", `Browser path set: ${config.browserOptions.path}`);
326
385
  } else {
327
- config.browserOptions.path = path.resolve(config.browserOptions.path);
328
- if (fs.existsSync(config.browserOptions.path)) {
329
- log(config, "debug", `Browser path set: ${config.browserOptions.path}`);
330
- } else {
331
- config.browserOptions.path = defaultConfig.browserOptions.path;
332
- log(
333
- config,
334
- "warning",
335
- `Invalid browser path. Reverted to default Chromium install.`
336
- );
337
- }
338
- return config;
386
+ config.browserOptions.path = defaultConfig.browserOptions.path;
387
+ log(
388
+ config,
389
+ "warning",
390
+ `Invalid browser path. Reverted to default Chromium install.`
391
+ );
339
392
  }
393
+ return config;
340
394
  }
395
+ }
341
396
 
342
397
  function setBrowserHeight(config, argv) {
343
398
  config.browserOptions.height =
@@ -467,8 +522,6 @@ function setAnalyticsServers(config, argv) {
467
522
  }
468
523
 
469
524
  function setConfig(config, argv) {
470
- config = setLogLevel(config, argv);
471
-
472
525
  config = selectConfig(config, argv);
473
526
 
474
527
  config = setEnv(config, argv);
@@ -477,6 +530,10 @@ function setConfig(config, argv) {
477
530
 
478
531
  config = setOutput(config, argv);
479
532
 
533
+ config = setSetup(config, argv);
534
+
535
+ config = setCleanup(config, argv);
536
+
480
537
  config = setMediaDirectory(config, argv);
481
538
 
482
539
  config = setRecursion(config, argv);
@@ -506,49 +563,59 @@ function setConfig(config, argv) {
506
563
  function setFiles(config) {
507
564
  let dirs = [];
508
565
  let files = [];
566
+ let sequence = [];
509
567
 
510
568
  // Validate input
511
- const input = path.resolve(config.input);
512
- let isFile = fs.statSync(input).isFile();
513
- let isDir = fs.statSync(input).isDirectory();
514
- if (!isFile && !isDir) {
515
- log(config, "error", "Input isn't a valid file or directory.");
516
- exit(1);
517
- }
518
-
519
- // Parse input
520
- if (isFile) {
521
- // if single file specified
522
- files[0] = input;
523
- return files;
524
- } else if (isDir) {
525
- // Load files from drectory
526
- dirs[0] = input;
527
- for (let i = 0; i < dirs.length; i++) {
528
- fs.readdirSync(dirs[i]).forEach((object) => {
529
- let content = path.resolve(dirs[i] + "/" + object);
530
- let isFile = fs.statSync(content).isFile();
531
- let isDir = fs.statSync(content).isDirectory();
532
- if (isFile) {
533
- // is a file
569
+ const setup = config.setup;
570
+ if (setup) sequence.push(setup);
571
+ const input = config.input;
572
+ sequence.push(input);
573
+ const cleanup = config.cleanup;
574
+ if (cleanup) sequence.push(cleanup);
575
+
576
+ for (s = 0; s < sequence.length; s++) {
577
+ let isFile = fs.statSync(sequence[s]).isFile();
578
+ let isDir = fs.statSync(sequence[s]).isDirectory();
579
+
580
+ // Parse input
581
+ if (
582
+ // Is a file
583
+ isFile &&
584
+ // Isn't present in files array already
585
+ files.indexOf(sequence[s]) < 0 &&
586
+ // No extension filter or extension included in filter
587
+ (config.testExtensions === "" ||
588
+ config.testExtensions.includes(path.extname(sequence[s])))
589
+ ) {
590
+ files.push(sequence[s]);
591
+ } else if (isDir) {
592
+ // Load files from directory
593
+ dirs = [];
594
+ dirs[0] = sequence[s];
595
+ for (let i = 0; i < dirs.length; i++) {
596
+ fs.readdirSync(dirs[i]).forEach((object) => {
597
+ let content = path.resolve(dirs[i] + "/" + object);
598
+ let isFile = fs.statSync(content).isFile();
599
+ let isDir = fs.statSync(content).isDirectory();
534
600
  if (
535
- // No specified extension filter list, or file extension is present in extension filter list.
536
- config.testExtensions === "" ||
537
- config.testExtensions.includes(path.extname(content))
601
+ // Is a file
602
+ isFile &&
603
+ // Isn't present in files array already
604
+ files.indexOf(s) < 0 &&
605
+ // No extension filter or extension included in filter
606
+ (config.testExtensions === "" ||
607
+ config.testExtensions.includes(path.extname(content)))
538
608
  ) {
539
609
  files.push(content);
540
- }
541
- } else if (isDir) {
542
- // is a directory
543
- if (config.recursive) {
610
+ } else if (isDir && config.recursive) {
544
611
  // recursive set to true
545
612
  dirs.push(content);
546
613
  }
547
- }
548
- });
614
+ });
615
+ }
549
616
  }
550
- return files;
551
617
  }
618
+ return files;
552
619
  }
553
620
 
554
621
  // Parse files for tests
@@ -621,16 +688,22 @@ async function outputResults(config, results) {
621
688
  fs.writeFile(config.output, data, (err) => {
622
689
  if (err) throw err;
623
690
  });
691
+ log(config, "info", "RESULTS:");
692
+ log(config, "info", results);
693
+ log(config, "info", `See detailed results at ${config.output}`);
624
694
  }
625
695
 
626
696
  async function convertToGif(config, input, fps, width) {
697
+ const ffmpegPath = require("@ffmpeg-installer/ffmpeg").path;
698
+
627
699
  if (!fs.existsSync(input)) return { error: "Invalid input." };
628
700
  let output = path.join(
629
701
  path.parse(input).dir,
630
702
  path.parse(input).name + ".gif"
631
703
  );
632
704
  if (!fps) fps = 15;
633
- let command = `ffmpeg -nostats -loglevel 0 -y -i ${input} -vf "fps=${fps},scale=${width}:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" -loop 0 ${output}`;
705
+
706
+ let command = `${ffmpegPath} -nostats -loglevel 0 -y -i ${input} -vf "fps=${fps},scale=${width}:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" -loop 0 ${output}`;
634
707
  exec(command, (error, stdout, stderr) => {
635
708
  if (error) {
636
709
  log(config, "debug", error.message);
@@ -641,6 +714,13 @@ async function convertToGif(config, input, fps, width) {
641
714
  return { stderr };
642
715
  }
643
716
  log(config, "debug", stdout);
717
+ fs.unlink(input, function (err) {
718
+ if (err) {
719
+ log(config, "warning", `Couldn't delete intermediate file: ${input}`);
720
+ } else {
721
+ log(config, "debug", `Deleted intermediate file: ${input}`);
722
+ }
723
+ });
644
724
  return { stdout };
645
725
  });
646
726
  return output;