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 +37 -14
- package/package.json +2 -1
- package/sample/config.json +2 -0
- package/sample/tests.json +7 -0
- package/src/config.json +2 -0
- package/src/index.js +0 -2
- package/src/lib/analytics.js +1 -1
- package/src/lib/tests/httpRequest.js +208 -0
- package/src/lib/tests.js +61 -2
- package/src/lib/utils.js +146 -66
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
|
-
"
|
|
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
|
-
"
|
|
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 `
|
|
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.
|
|
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
|
|
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.
|
|
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",
|
package/sample/config.json
CHANGED
package/sample/tests.json
CHANGED
package/src/config.json
CHANGED
package/src/index.js
CHANGED
package/src/lib/analytics.js
CHANGED
|
@@ -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
|
-
|
|
33
|
-
|
|
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 =
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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 =
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
-
//
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
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;
|