doc-detective 1.0.5 → 1.0.7

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
@@ -530,9 +530,7 @@ If you opt into sending analytics, you can add additional servers that Doc Detec
530
530
 
531
531
  ```json
532
532
  {
533
- ...
534
533
  "analytics": {
535
- ...
536
534
  "customServers": [
537
535
  {
538
536
  "name": "My Analytics Server",
@@ -547,7 +545,6 @@ If you opt into sending analytics, you can add additional servers that Doc Detec
547
545
  }
548
546
  ]
549
547
  }
550
- ...
551
548
  }
552
549
  ```
553
550
 
@@ -569,15 +566,12 @@ Analytics reporting is off by default. If you want to make extra sure that Doc D
569
566
  - New/upgraded test actions:
570
567
  - New: Test if a referenced image (such as an icon) is present in the captured screenshot.
571
568
  - Upgrade: Additional `httpRequest` input sanitization.
572
- - Upgrade: `screenshot` and `startRecording` boolean for whether to perform the action or not if the expected output file already exists.
573
569
  - Upgrade: `startRecording` and `stopRecording` to support start, stop, and intermediate test action state image matching to track differences between video captures from different runs.
574
- - Upgrade: `startRecording` to store the output file in a different location if a recorded action fails. This could help with debugging.
575
- - In-content test framing to identify when content is covered by a test defined in another file. This could enable content coverage analysis.
570
+ - Content coverage analysis based on in-content test statements and markup declarations.
576
571
  - Suggest tests by parsing document text.
577
572
  - Automatically insert suggested tests based on document text.
578
573
  - Detailed field descriptions per action.
579
574
  - Refactor tests into individual files.
580
- - Rewrite cross-action recording status tracking.
581
575
 
582
576
  ## License
583
577
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doc-detective",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Unit test documentation (and record videos of those tests).",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -14,14 +14,19 @@
14
14
  ".json"
15
15
  ],
16
16
  "mediaDirectory": "sample",
17
+ "saveFailedTestRecordings": true,
18
+ "failedTestDirectory": "sample/failedTests",
17
19
  "fileTypes": [
18
20
  {
19
21
  "extensions": [
20
22
  ".md",
21
23
  ".mdx"
22
24
  ],
23
- "openTestStatement": "[comment]: # (test",
24
- "closeTestStatement": ")"
25
+ "testStartStatementOpen": "[comment]: # (test start",
26
+ "testStartStatementClose": ")",
27
+ "testEndStatement": "[comment]: # (test end)",
28
+ "actionStatementOpen": "[comment]: # (action",
29
+ "actionStatementClose": ")"
25
30
  },
26
31
  {
27
32
  "extensions": [
@@ -29,8 +34,11 @@
29
34
  ".htm",
30
35
  ".xml"
31
36
  ],
32
- "openTestStatement": "<!-- test",
33
- "closeTestStatement": "-->"
37
+ "testStartStatementOpen": "<!-- test start",
38
+ "testStartStatementClose": "-->",
39
+ "testEndStatement": "<!-- test end -->",
40
+ "actionStatementOpen": "<!-- action",
41
+ "actionStatementClose": "-->"
34
42
  }
35
43
  ],
36
44
  "browserOptions": {
@@ -2,21 +2,25 @@
2
2
 
3
3
  To use Google Search to find information on kittens,
4
4
 
5
+ [comment]: # (test start {"id":"process-search-kittens" })
6
+
5
7
  1. Open [Google Search](https://www.google.com).
6
8
 
7
- [comment]: # (test {"testId":"process-search-kittens", "action":"startRecording", "overwrite":false, "filename":"results.gif", "fps":15})
8
- [comment]: # (test {"testId":"process-search-kittens", "action":"goTo", "uri":"www.google.com"})
9
+ [comment]: # (action {"action":"startRecording", "overwrite":false, "filename":"results.gif", "fps":15})
10
+ [comment]: # (action {"action":"goTo", "uri":"www.google.com"})
9
11
 
10
12
  2. In the search bar, enter "kittens", then press Enter.
11
13
 
12
- [comment]: # (test {"testId":"process-search-kittens", "action":"moveMouse", "css":"#gbqfbb", "alignH": "center", "alignV": "center"})
13
- [comment]: # (test {"testId":"process-search-kittens", "action":"wait", "duration":"5000"})
14
- [comment]: # (test {"testId":"process-search-kittens", "action":"moveMouse", "css":"[title=Search]", "alignV": "center"})
15
- [comment]: # (test {"testId":"process-search-kittens", "action":"type", "css":"[title=Search]", "keys":"kittens", "trailingSpecialKey":"Enter"})
16
- [comment]: # (test {"testId":"process-search-kittens", "action":"wait", "duration":"5000"})
17
- [comment]: # (test {"testId":"process-search-kittens", "action":"scroll", "y": 300})
18
- [comment]: # (test {"testId":"process-search-kittens", "action":"stopRecording"})
19
- [comment]: # (test {"testId":"process-search-kittens", "action":"screenshot", "filename":"results.png", "matchPrevious": true, "matchThreshold": 0.1})
14
+ [comment]: # (action {"action":"moveMouse", "css":"#gbqfbb", "alignH": "center", "alignV": "center"})
15
+ [comment]: # (action {"action":"wait", "duration":"5000"})
16
+ [comment]: # (action {"action":"moveMouse", "css":"[title=Search]", "alignV": "center"})
17
+ [comment]: # (action {"action":"type", "css":"[title=Search]", "keys":"kittens", "trailingSpecialKey":"Enter"})
18
+ [comment]: # (action {"action":"wait", "duration":"5000"})
19
+ [comment]: # (action {"action":"scroll", "y": 300})
20
+ [comment]: # (action {"action":"stopRecording"})
21
+ [comment]: # (action {"action":"screenshot", "filename":"results.png", "matchPrevious": true, "matchThreshold": 0.1})
22
+
23
+ [comment]: # (test end {"id":"process-search-kittens" })
20
24
 
21
25
  Search results appear on the page.
22
26
 
@@ -26,17 +30,17 @@ Search results appear on the page.
26
30
 
27
31
  To go directly to a recommended result for your search, use the **I'm Feeling Lucky** button. If you're searching for american shorthair information,
28
32
 
29
- [comment]: # (test {"testId":"text-match-lucky", "action":"goTo", "uri":"www.google.com"})
30
- [comment]: # (test {"testId":"text-match-lucky", "action":"matchText", "css":"#gbqfbb", "text":"I'm Feeling Lucky"})
33
+ [comment]: # (action {"testId":"text-match-lucky", "action":"goTo", "uri":"www.google.com"})
34
+ [comment]: # (action {"testId":"text-match-lucky", "action":"matchText", "css":"#gbqfbb", "text":"I'm Feeling Lucky"})
31
35
 
32
36
  1. Open [Google Search](https://www.google.com).
33
37
 
34
- [comment]: # (test {"testId":"process-lucky-shorthair", "action":"goTo", "uri":"www.google.com"})
38
+ [comment]: # (action {"testId":"process-lucky-shorthair", "action":"goTo", "uri":"www.google.com"})
35
39
 
36
40
  2. In the search bar, enter "american shorthair cats".
37
41
 
38
- [comment]: # (test {"testId":"process-lucky-shorthair", "action":"type", "css":"[title=Search]", "keys":"american shorthair cats"})
42
+ [comment]: # (action {"testId":"process-lucky-shorthair", "action":"type", "css":"[title=Search]", "keys":"american shorthair cats"})
39
43
 
40
44
  3. Click **I'm Feeling Lucky**.
41
45
 
42
- [comment]: # (test {"testId":"process-lucky-shorthair", "action":"click", "css":"#gbqfbb"})
46
+ [comment]: # (action {"testId":"process-lucky-shorthair", "action":"click", "css":"#gbqfbb"})
Binary file
@@ -2,20 +2,65 @@
2
2
  "tests": [
3
3
  {
4
4
  "id": "process-search-kittens",
5
+ "file": "/config/workspace/doc-detective/sample/doc-content.md",
5
6
  "actions": [
7
+ {
8
+ "action": "startRecording",
9
+ "overwrite": false,
10
+ "filename": "results.gif",
11
+ "fps": 15,
12
+ "line": 9,
13
+ "result": {
14
+ "status": "PASS",
15
+ "description": "Started recording: /config/workspace/doc-detective/sample/temp_results.mp4",
16
+ "video": "/config/workspace/doc-detective/sample/temp_results.mp4"
17
+ }
18
+ },
6
19
  {
7
20
  "action": "goTo",
8
21
  "uri": "www.google.com",
22
+ "line": 10,
9
23
  "result": {
10
24
  "status": "PASS",
11
25
  "description": "Opened URI."
12
26
  }
13
27
  },
28
+ {
29
+ "action": "moveMouse",
30
+ "css": "#gbqfbb",
31
+ "alignH": "center",
32
+ "alignV": "center",
33
+ "line": 14,
34
+ "result": {
35
+ "status": "PASS",
36
+ "description": "Moved mouse to element."
37
+ }
38
+ },
39
+ {
40
+ "action": "wait",
41
+ "duration": "5000",
42
+ "line": 15,
43
+ "result": {
44
+ "status": "PASS",
45
+ "description": "Wait complete."
46
+ }
47
+ },
48
+ {
49
+ "action": "moveMouse",
50
+ "css": "[title=Search]",
51
+ "alignV": "center",
52
+ "line": 16,
53
+ "result": {
54
+ "status": "PASS",
55
+ "description": "Moved mouse to element."
56
+ }
57
+ },
14
58
  {
15
59
  "action": "type",
16
60
  "css": "[title=Search]",
17
61
  "keys": "kittens",
18
62
  "trailingSpecialKey": "Enter",
63
+ "line": 17,
19
64
  "result": {
20
65
  "status": "PASS",
21
66
  "description": "Typed keys."
@@ -24,16 +69,35 @@
24
69
  {
25
70
  "action": "wait",
26
71
  "duration": "5000",
72
+ "line": 18,
27
73
  "result": {
28
74
  "status": "PASS",
29
75
  "description": "Wait complete."
30
76
  }
31
77
  },
78
+ {
79
+ "action": "scroll",
80
+ "y": 300,
81
+ "line": 19,
82
+ "result": {
83
+ "status": "PASS",
84
+ "description": "Scroll complete."
85
+ }
86
+ },
87
+ {
88
+ "action": "stopRecording",
89
+ "line": 20,
90
+ "result": {
91
+ "status": "PASS",
92
+ "description": "Stopped recording: /config/workspace/doc-detective/sample/results.gif"
93
+ }
94
+ },
32
95
  {
33
96
  "action": "screenshot",
34
97
  "filename": "results.png",
35
98
  "matchPrevious": false,
36
99
  "matchThreshold": 0.1,
100
+ "line": 21,
37
101
  "result": {
38
102
  "status": "PASS",
39
103
  "description": "Captured screenshot.",
@@ -45,10 +109,12 @@
45
109
  },
46
110
  {
47
111
  "id": "text-match-lucky",
112
+ "file": "/config/workspace/doc-detective/sample/doc-content.md",
48
113
  "actions": [
49
114
  {
50
115
  "action": "goTo",
51
116
  "uri": "www.google.com",
117
+ "line": 33,
52
118
  "result": {
53
119
  "status": "PASS",
54
120
  "description": "Opened URI."
@@ -58,16 +124,7 @@
58
124
  "action": "matchText",
59
125
  "css": "#gbqfbb",
60
126
  "text": "I'm Feeling Lucky",
61
- "result": {
62
- "status": "PASS",
63
- "description": "Element text matched expected text."
64
- }
65
- },
66
- {
67
- "action": "matchText",
68
- "css": "#gbqfbb",
69
- "text": "$TEXT",
70
- "env": "./sample/variables.env",
127
+ "line": 34,
71
128
  "result": {
72
129
  "status": "PASS",
73
130
  "description": "Element text matched expected text."
@@ -78,11 +135,12 @@
78
135
  },
79
136
  {
80
137
  "id": "process-lucky-shorthair",
138
+ "file": "/config/workspace/doc-detective/sample/doc-content.md",
81
139
  "actions": [
82
140
  {
83
141
  "action": "goTo",
84
- "uri": "$URL",
85
- "env": "./sample/variables.env",
142
+ "uri": "www.google.com",
143
+ "line": 38,
86
144
  "result": {
87
145
  "status": "PASS",
88
146
  "description": "Opened URI."
@@ -91,7 +149,8 @@
91
149
  {
92
150
  "action": "type",
93
151
  "css": "[title=Search]",
94
- "keys": "$SHORTHAIR_CAT_SEARCH",
152
+ "keys": "american shorthair cats",
153
+ "line": 42,
95
154
  "result": {
96
155
  "status": "PASS",
97
156
  "description": "Typed keys."
@@ -100,6 +159,7 @@
100
159
  {
101
160
  "action": "click",
102
161
  "css": "#gbqfbb",
162
+ "line": 46,
103
163
  "result": {
104
164
  "status": "PASS",
105
165
  "description": "Clicked element."
@@ -107,57 +167,6 @@
107
167
  }
108
168
  ],
109
169
  "status": "PASS"
110
- },
111
- {
112
- "id": "non-ui-tests",
113
- "actions": [
114
- {
115
- "action": "runShell",
116
- "command": "echo $USERNAME",
117
- "env": "./sample/variables.env",
118
- "result": {
119
- "status": "PASS",
120
- "description": "Executed command.",
121
- "stdout": "foo",
122
- "stderr": "",
123
- "exitCode": 0
124
- }
125
- },
126
- {
127
- "action": "checkLink",
128
- "uri": "https://www.google.com",
129
- "statusCodes": [
130
- 200
131
- ],
132
- "result": {
133
- "status": "PASS",
134
- "description": "Returned 200"
135
- }
136
- },
137
- {
138
- "action": "checkLink",
139
- "uri": "$URL",
140
- "statusCodes": [
141
- 200
142
- ],
143
- "result": {
144
- "status": "PASS",
145
- "description": "Returned 200"
146
- }
147
- },
148
- {
149
- "action": "httpRequest",
150
- "uri": "$URL",
151
- "statusCodes": [
152
- 200
153
- ],
154
- "result": {
155
- "status": "PASS",
156
- "description": "Returned 200."
157
- }
158
- }
159
- ],
160
- "status": "PASS"
161
170
  }
162
171
  ]
163
172
  }
Binary file
package/sample/tests.json CHANGED
@@ -2,6 +2,8 @@
2
2
  "tests": [
3
3
  {
4
4
  "id": "process-search-kittens",
5
+ "saveFailedTestRecordings": true,
6
+ "failedTestDirectory": "sample",
5
7
  "actions": [
6
8
  {
7
9
  "action": "goTo",
package/src/config.json CHANGED
@@ -14,14 +14,19 @@
14
14
  ".json"
15
15
  ],
16
16
  "mediaDirectory": ".",
17
+ "saveFailedTestRecordings": true,
18
+ "failedTestDirectory": ".",
17
19
  "fileTypes": [
18
20
  {
19
21
  "extensions": [
20
22
  ".md",
21
23
  ".mdx"
22
24
  ],
23
- "openTestStatement": "[comment]: # (test",
24
- "closeTestStatement": ")"
25
+ "testStartStatementOpen": "[comment]: # (test start",
26
+ "testStartStatementClose": ")",
27
+ "testEndStatement": "[comment]: # (test end)",
28
+ "actionStatementOpen": "[comment]: # (action",
29
+ "actionStatementClose": ")"
25
30
  },
26
31
  {
27
32
  "extensions": [
@@ -29,8 +34,11 @@
29
34
  ".htm",
30
35
  ".xml"
31
36
  ],
32
- "openTestStatement": "<!-- test",
33
- "closeTestStatement": "-->"
37
+ "testStartStatementOpen": "<!-- test start",
38
+ "testStartStatementClose": "-->",
39
+ "testEndStatement": "<!-- test end -->",
40
+ "actionStatementOpen": "<!-- action",
41
+ "actionStatementClose": "-->"
34
42
  }
35
43
  ],
36
44
  "browserOptions": {
package/src/index.js CHANGED
@@ -2,7 +2,7 @@ const {
2
2
  setArgs,
3
3
  setConfig,
4
4
  setFiles,
5
- parseFiles,
5
+ parseTests,
6
6
  outputResults,
7
7
  log,
8
8
  } = require("./lib/utils");
@@ -30,7 +30,7 @@ async function main(config, argv) {
30
30
  log(config, "debug", files);
31
31
 
32
32
  // Set tests
33
- const tests = parseFiles(config, files);
33
+ const tests = parseTests(config, files);
34
34
  if (config.logLevel === "debug") {
35
35
  console.log("(DEBUG) TESTS:");
36
36
  tests.tests.forEach((test) => {
@@ -87,12 +87,17 @@ async function startRecording(action, page, config) {
87
87
  }
88
88
 
89
89
  // Set FPS
90
- fps = action.fps || action.gifFps || defaultPayload.fps;
90
+ targetFps = action.fps || action.gifFps || defaultPayload.fps;
91
91
  try {
92
- fps = Number(fps);
92
+ targetFps = Number(targetFps);
93
+ if (targetFps >= 30) {
94
+ fps = targetFps;
95
+ } else {
96
+ fps = 30;
97
+ }
93
98
  } catch {
94
- fps = defaultPayload;
95
- log(config, "warning", `Invalid FPS. Reverting to default: ${fps}`);
99
+ targetFps = defaultPayload.fps;
100
+ log(config, "warning", `Invalid FPS. Reverting to default: ${targetFps}`);
96
101
  }
97
102
 
98
103
  // Set height
@@ -126,6 +131,7 @@ async function startRecording(action, page, config) {
126
131
  filepath,
127
132
  tempFilepath,
128
133
  fps,
134
+ targetFps,
129
135
  height,
130
136
  width,
131
137
  };
@@ -143,12 +149,30 @@ async function stopRecording(videoDetails, config) {
143
149
  let status;
144
150
  let description;
145
151
  let result;
152
+
153
+ if (typeof videoDetails.recorder === "undefined") {
154
+ status = "PASS";
155
+ description = `Skipping action. No action-defined recording in progress.`;
156
+ result = { status, description };
157
+ return { result };
158
+ }
159
+
160
+ recorder = videoDetails.recorder;
161
+ targetExtension = videoDetails.targetExtension;
162
+ height = videoDetails.height;
163
+ width = videoDetails.width;
164
+ filepath = videoDetails.filepath;
165
+ tempFilepath = videoDetails.tempFilepath;
166
+ fps = videoDetails.fps;
167
+ targetFps = videoDetails.targetFps;
168
+
146
169
  try {
147
- await videoDetails.recorder.stop();
170
+ await recorder.stop();
148
171
  if (
149
- videoDetails.targetExtension === ".gif" ||
150
- videoDetails.height != config.browserOptions.height ||
151
- videoDetails.width != config.browserOptions.width
172
+ targetExtension === ".gif" ||
173
+ height != config.browserOptions.height ||
174
+ width != config.browserOptions.width ||
175
+ targetFps != fps
152
176
  ) {
153
177
  let output = await convertVideo(config, videoDetails);
154
178
  filepath = output;
@@ -87,36 +87,59 @@ async function screenshot(action, page, config) {
87
87
  if (action.matchPrevious) {
88
88
  const expected = PNG.sync.read(fs.readFileSync(previousFilePath));
89
89
  const actual = PNG.sync.read(fs.readFileSync(filePath));
90
- const numDiffPixels = pixelmatch(
91
- expected.data,
92
- actual.data,
93
- null,
94
- expected.width,
95
- expected.height,
96
- {
97
- threshold: action.matchThreshold,
98
- }
99
- );
100
- fs.unlink(filePath, function (err) {
101
- if (err) {
102
- log(config, "warning", `Couldn't delete intermediate file: ${filePath}`);
90
+ try {
91
+ const numDiffPixels = pixelmatch(
92
+ expected.data,
93
+ actual.data,
94
+ null,
95
+ expected.width,
96
+ expected.height,
97
+ {
98
+ threshold: action.matchThreshold,
99
+ }
100
+ );
101
+ fs.unlink(filePath, function (err) {
102
+ if (err) {
103
+ log(
104
+ config,
105
+ "warning",
106
+ `Couldn't delete intermediate file: ${filePath}`
107
+ );
108
+ } else {
109
+ log(config, "debug", `Deleted intermediate file: ${filePath}`);
110
+ }
111
+ });
112
+ if (numDiffPixels) {
113
+ // FAIL: Couldn't capture screenshot
114
+ const diffPercentage =
115
+ numDiffPixels / (expected.width * expected.height);
116
+ status = "FAIL";
117
+ description = `Screenshot comparison had larger diff (${diffPercentage}) than threshold (${action.matchThreshold}).`;
118
+ result = { status, description };
119
+ return { result };
103
120
  } else {
104
- log(config, "debug", `Deleted intermediate file: ${filePath}`);
121
+ // PASS
122
+ status = "PASS";
123
+ description = `Screenshot matches previously captured image.`;
124
+ result = { status, description, image: previousFilePath };
125
+ return { result };
105
126
  }
106
- });
107
- if (numDiffPixels) {
108
- // FAIL: Couldn't capture screenshot
109
- const diffPercentage = numDiffPixels / (expected.width * expected.height);
127
+ } catch {
128
+ fs.unlink(filePath, function (err) {
129
+ if (err) {
130
+ log(
131
+ config,
132
+ "warning",
133
+ `Couldn't delete intermediate file: ${filePath}`
134
+ );
135
+ } else {
136
+ log(config, "debug", `Deleted intermediate file: ${filePath}`);
137
+ }
138
+ });
110
139
  status = "FAIL";
111
- description = `Screenshot comparison had larger diff (${diffPercentage}) than threshold (${action.matchThreshold}).`;
140
+ description = `Image sizes don't match.`;
112
141
  result = { status, description };
113
142
  return { result };
114
- } else {
115
- // PASS
116
- status = "PASS";
117
- description = `Screenshot matches previously captured image.`;
118
- result = { status, description, image: previousFilePath };
119
- return { result };
120
143
  }
121
144
  }
122
145
  }
package/src/lib/tests.js CHANGED
@@ -2,7 +2,7 @@ const puppeteer = require("puppeteer");
2
2
  const fs = require("fs");
3
3
  const { exit, stdout, exitCode } = require("process");
4
4
  const { installMouseHelper } = require("./install-mouse-helper");
5
- const { setEnvs, log } = require("./utils");
5
+ const { setEnvs, log, timestamp } = require("./utils");
6
6
  const util = require("util");
7
7
  const exec = util.promisify(require("child_process").exec);
8
8
  const axios = require("axios");
@@ -88,33 +88,54 @@ async function runTests(config, tests) {
88
88
  let pass = 0;
89
89
  let warning = 0;
90
90
  let fail = 0;
91
- let videoDetails;
91
+ config.videoDetails = {};
92
+ config.debugRecording = {};
92
93
  // Instantiate page
93
94
  const page = await browser.newPage();
95
+ if (
96
+ test.saveFailedTestRecordings ||
97
+ (config.saveFailedTestRecordings &&
98
+ test.saveFailedTestRecordings != false)
99
+ ) {
100
+ failedTestDirectory =
101
+ test.failedTestDirectory || config.failedTestDirectory;
102
+ debugRecordingOptions = {
103
+ action: "startRecording",
104
+ mediaDirectory: failedTestDirectory,
105
+ filename: `${test.id}-${timestamp()}.mp4`,
106
+ overwrite: true,
107
+ };
108
+ config.debugRecording = await startRecording(
109
+ debugRecordingOptions,
110
+ page,
111
+ config
112
+ );
113
+ }
94
114
  // Instantiate mouse cursor
95
115
  await installMouseHelper(page);
96
116
  // Iterate through actions
97
117
  for (const action of test.actions) {
98
118
  log(config, "debug", `ACTION: ${JSON.stringify(action)}`);
99
- action.result = await runAction(config, action, page, videoDetails);
119
+ action.result = await runAction(
120
+ config,
121
+ action,
122
+ page,
123
+ config.videoDetails
124
+ );
100
125
  if (action.result.videoDetails) {
101
- videoDetails = action.result.videoDetails;
126
+ config.videoDetails = action.result.videoDetails;
102
127
  }
103
128
  action.result = action.result.result;
104
129
  if (action.result.status === "FAIL") fail++;
105
130
  if (action.result.status === "WARNING") warning++;
106
131
  if (action.result.status === "PASS") pass++;
107
- log(config, "debug", `RESULT: ${action.result.status}. ${action.result.description}`);
132
+ log(
133
+ config,
134
+ "debug",
135
+ `RESULT: ${action.result.status}. ${action.result.description}`
136
+ );
108
137
  }
109
138
 
110
- // Close open recorders/pages
111
- if (videoDetails) {
112
- await runAction("", { action: "stopRecording" }, "", videoDetails);
113
- }
114
- try {
115
- await page.close();
116
- } catch {}
117
-
118
139
  // Calc overall test result
119
140
  if (fail) {
120
141
  test.status = "FAIL";
@@ -126,6 +147,33 @@ async function runTests(config, tests) {
126
147
  console.log("Error: Couldn't read test action results.");
127
148
  exit(1);
128
149
  }
150
+
151
+ // Close open recorders/pages
152
+ if (config.debugRecording.videoDetails) {
153
+ await stopRecording(config.debugRecording.videoDetails, config);
154
+ if (!fail) {
155
+ fs.unlink(config.debugRecording.videoDetails.filepath, function (err) {
156
+ if (err) {
157
+ log(
158
+ config,
159
+ "warning",
160
+ `Couldn't delete debug recording: ${config.debugRecording.videoDetails.filepath}`
161
+ );
162
+ } else {
163
+ log(
164
+ config,
165
+ "debug",
166
+ `Deleted debug recording: ${config.debugRecording.videoDetails.filepath}`
167
+ );
168
+ }
169
+ });
170
+ }
171
+ }
172
+
173
+ // Close page
174
+ try {
175
+ await page.close();
176
+ } catch {}
129
177
  }
130
178
  await browser.close();
131
179
  return tests;
package/src/lib/utils.js CHANGED
@@ -11,11 +11,12 @@ const defaultConfig = require("../config.json");
11
11
  exports.setArgs = setArgs;
12
12
  exports.setConfig = setConfig;
13
13
  exports.setFiles = setFiles;
14
- exports.parseFiles = parseFiles;
14
+ exports.parseTests = parseTests;
15
15
  exports.outputResults = outputResults;
16
16
  exports.setEnvs = setEnvs;
17
17
  exports.loadEnvsForObject = loadEnvsForObject;
18
18
  exports.log = log;
19
+ exports.timestamp = timestamp;
19
20
 
20
21
  const analyticsRequest =
21
22
  "Thanks for using Doc Detective! If you want to contribute to the project, consider sending analytics to help us understand usage patterns and functional gaps. To turn on analytics, set 'analytics.send = true' in your config, or use the '-a true' argument. See https://github.com/hawkeyexl/doc-detective#analytics";
@@ -182,7 +183,8 @@ function selectConfig(config, argv) {
182
183
  }
183
184
 
184
185
  function setEnv(config, argv) {
185
- config.env = argv.env || process.env.DOC_ENV_PATH || config.env;
186
+ config.env =
187
+ argv.env || process.env.DOC_ENV_PATH || config.env || defaultConfig.env;
186
188
  if (config.env) {
187
189
  config.env = path.resolve(config.env);
188
190
  if (fs.existsSync(config.env)) {
@@ -201,7 +203,11 @@ function setEnv(config, argv) {
201
203
  }
202
204
 
203
205
  function setInput(config, argv) {
204
- config.input = argv.input || process.env.DOC_INPUT_PATH || config.input;
206
+ config.input =
207
+ argv.input ||
208
+ process.env.DOC_INPUT_PATH ||
209
+ config.input ||
210
+ defaultConfig.input;
205
211
  if (config.input) {
206
212
  config.input = path.resolve(config.input);
207
213
  if (fs.existsSync(config.input)) {
@@ -225,14 +231,19 @@ function setInput(config, argv) {
225
231
  }
226
232
 
227
233
  function setOutput(config, argv) {
228
- config.output = argv.output || process.env.DOC_OUTPUT_PATH || config.output;
234
+ config.output =
235
+ argv.output ||
236
+ process.env.DOC_OUTPUT_PATH ||
237
+ config.output ||
238
+ defaultConfig.output;
229
239
  config.output = path.resolve(config.output);
230
240
  log(config, "debug", `Output path set: ${config.output}`);
231
241
  return config;
232
242
  }
233
243
 
234
244
  function setSetup(config, argv) {
235
- config.setup = argv.setup || process.env.DOC_SETUP || config.setup;
245
+ config.setup =
246
+ argv.setup || process.env.DOC_SETUP || config.setup || defaultConfig.setup;
236
247
  if (config.setup === "") {
237
248
  log(config, "debug", `No setup tests.`);
238
249
  return config;
@@ -249,7 +260,11 @@ function setSetup(config, argv) {
249
260
  }
250
261
 
251
262
  function setCleanup(config, argv) {
252
- config.cleanup = argv.cleanup || process.env.DOC_CLEANUP || config.cleanup;
263
+ config.cleanup =
264
+ argv.cleanup ||
265
+ process.env.DOC_CLEANUP ||
266
+ config.cleanup ||
267
+ defaultConfig.cleanup;
253
268
  if (config.cleanup === "") {
254
269
  log(config, "debug", `No cleanup tests.`);
255
270
  return config;
@@ -269,7 +284,8 @@ function setMediaDirectory(config, argv) {
269
284
  config.mediaDirectory =
270
285
  argv.mediaDir ||
271
286
  process.env.DOC_MEDIA_DIRECTORY_PATH ||
272
- config.mediaDirectory;
287
+ config.mediaDirectory ||
288
+ defaultConfig.mediaDirectory;
273
289
  config.mediaDirectory = path.resolve(config.mediaDirectory);
274
290
  if (fs.existsSync(config.mediaDirectory)) {
275
291
  log(config, "debug", `Media directory set: ${config.mediaDirectory}`);
@@ -284,9 +300,75 @@ function setMediaDirectory(config, argv) {
284
300
  return config;
285
301
  }
286
302
 
303
+ function setFailedTestRecording(config, argv) {
304
+ config.saveFailedTestRecordings =
305
+ argv.saveFailedTestRecordings ||
306
+ process.env.DOC_SAVE_FAILED_RECORDINGS ||
307
+ config.saveFailedTestRecordings ||
308
+ defaultConfig.saveFailedTestRecordings;
309
+ switch (config.saveFailedTestRecordings) {
310
+ case true:
311
+ case "true":
312
+ config.saveFailedTestRecordings = true;
313
+ log(
314
+ config,
315
+ "debug",
316
+ `Save failed test recordings set: ${config.saveFailedTestRecordings}`
317
+ );
318
+ break;
319
+ case false:
320
+ case "false":
321
+ config.saveFailedTestRecordings = false;
322
+ log(
323
+ config,
324
+ "debug",
325
+ `Save failed test recordings set: ${config.saveFailedTestRecordings}`
326
+ );
327
+ log(config, "info", analyticsRequest);
328
+ break;
329
+ default:
330
+ config.saveFailedTestRecordings = defaultConfig.saveFailedTestRecordings;
331
+ log(
332
+ config,
333
+ "warning",
334
+ `Invalid save failed test recordings value. Reverted to default: ${config.saveFailedTestRecordings}`
335
+ );
336
+ }
337
+ return config;
338
+ }
339
+
340
+ function setFailedTestDirectory(config, argv) {
341
+ config.failedTestDirectory =
342
+ argv.failedTestDirectory ||
343
+ process.env.DOC_FAILED_TEST_DIRECTORY_PATH ||
344
+ config.failedTestDirectory ||
345
+ defaultConfig.failedTestDirectory;
346
+ config.failedTestDirectory = path.resolve(config.failedTestDirectory);
347
+ if (fs.existsSync(config.failedTestDirectory)) {
348
+ log(
349
+ config,
350
+ "debug",
351
+ `Failed test directory set: ${config.failedTestDirectory}`
352
+ );
353
+ } else {
354
+ config.failedTestDirectory = path.resolve(
355
+ defaultConfig.failedTestDirectory
356
+ );
357
+ log(
358
+ config,
359
+ "warning",
360
+ `Invalid failed test directory. Reverted to default: ${config.failedTestDirectory}`
361
+ );
362
+ }
363
+ return config;
364
+ }
365
+
287
366
  function setRecursion(config, argv) {
288
367
  config.recursive =
289
- argv.recursive || process.env.DOC_RECURSIVE || config.recursive;
368
+ argv.recursive ||
369
+ process.env.DOC_RECURSIVE ||
370
+ config.recursive ||
371
+ defaultConfig.recursive;
290
372
  switch (config.recursive) {
291
373
  case true:
292
374
  case "true":
@@ -309,9 +391,33 @@ function setRecursion(config, argv) {
309
391
  return config;
310
392
  }
311
393
 
394
+ function setFileTypes(config, argv) {
395
+ config.fileTypes =
396
+ argv.fileTypes ||
397
+ process.env.DOC_FILE_TYPES ||
398
+ config.fileTypes ||
399
+ defaultConfig.fileTypes;
400
+ if (config.fileTypes.length > 0) {
401
+ log(config, "debug", `File types set: ${JSON.stringify(config.fileTypes)}`);
402
+ } else {
403
+ config.fileTypes = defaultConfig.fileTypes;
404
+ log(
405
+ config,
406
+ "debug",
407
+ `Invalid file type value(s). Reverted to default: ${JSON.stringify(
408
+ config.fileTypes
409
+ )}`
410
+ );
411
+ }
412
+ return config;
413
+ }
414
+
312
415
  function setTestFileExtensions(config, argv) {
313
416
  config.testExtensions =
314
- argv.ext || process.env.DOC_TEST_EXTENSTIONS || config.testExtensions;
417
+ argv.ext ||
418
+ process.env.DOC_TEST_EXTENSTIONS ||
419
+ config.testExtensions ||
420
+ defaultConfig.testExtensions;
315
421
  if (typeof config.testExtensions === "string")
316
422
  config.testExtensions = config.testExtensions
317
423
  .replace(/\s+/g, "")
@@ -339,7 +445,8 @@ function setBrowserHeadless(config, argv) {
339
445
  config.browserOptions.headless =
340
446
  argv.browserHeadless ||
341
447
  process.env.DOC_BROWSER_HEADLESS ||
342
- config.browserOptions.headless;
448
+ config.browserOptions.headless ||
449
+ defaultConfig.browserOptions.headless;
343
450
  switch (config.browserOptions.headless) {
344
451
  case true:
345
452
  case "true":
@@ -374,7 +481,8 @@ function setBrowserPath(config, argv) {
374
481
  config.browserOptions.path =
375
482
  argv.browserPath ||
376
483
  process.env.DOC_BROWSER_PATH ||
377
- config.browserOptions.path;
484
+ config.browserOptions.path ||
485
+ defaultConfig.browserOptions.path;
378
486
  if (config.browserOptions.path === "") {
379
487
  log(config, "debug", `Browser set to default Chromium install.`);
380
488
  return config;
@@ -398,7 +506,8 @@ function setBrowserHeight(config, argv) {
398
506
  config.browserOptions.height =
399
507
  argv.browserHeight ||
400
508
  process.env.DOC_BROWSER_HEIGHT ||
401
- config.browserOptions.height;
509
+ config.browserOptions.height ||
510
+ defaultConfig.browserOptions.height;
402
511
  if (typeof config.browserOptions.height === "string") {
403
512
  try {
404
513
  config.browserOptions.height = Number(config.browserOptions.height);
@@ -428,7 +537,8 @@ function setBrowserWidth(config, argv) {
428
537
  config.browserOptions.width =
429
538
  argv.browserWidth ||
430
539
  process.env.DOC_BROWSER_WIDTH ||
431
- config.browserOptions.width;
540
+ config.browserOptions.width ||
541
+ defaultConfig.browserOptions.width;
432
542
  if (typeof config.browserOptions.width === "string") {
433
543
  try {
434
544
  config.browserOptions.width = Number(config.browserOptions.width);
@@ -456,7 +566,10 @@ function setBrowserWidth(config, argv) {
456
566
 
457
567
  function setAnalytics(config, argv) {
458
568
  config.analytics.send =
459
- argv.analytics || process.env.DOC_ANALYTICS || config.analytics.send;
569
+ argv.analytics ||
570
+ process.env.DOC_ANALYTICS ||
571
+ config.analytics.send ||
572
+ defaultConfig.analytics.send;
460
573
  switch (config.analytics.send) {
461
574
  case true:
462
575
  case "true":
@@ -484,7 +597,8 @@ function setAnalyticsUserId(config, argv) {
484
597
  config.analytics.userId =
485
598
  argv.analyticsUserId ||
486
599
  process.env.DOC_ANALYTICS_USER_ID ||
487
- config.analytics.userId;
600
+ config.analytics.userId ||
601
+ defaultConfig.analytics.userId;
488
602
  log(config, "debug", `Analytics user ID set: ${config.analytics.userId}`);
489
603
  return config;
490
604
  }
@@ -494,7 +608,8 @@ function setAnalyticsDetailLevel(config, argv) {
494
608
  detailLevel =
495
609
  argv.analyticsDetailLevel ||
496
610
  process.env.DOC_ANALYTCS_DETAIL_LEVEL ||
497
- config.analytics.detailLevel;
611
+ config.analytics.detailLevel ||
612
+ defaultConfig.analytics.detailLevel;
498
613
  detailLevel = String(detailLevel).toLowerCase();
499
614
  if (enums.indexOf(detailLevel) >= 0) {
500
615
  config.analytics.detailLevel = detailLevel;
@@ -537,8 +652,14 @@ function setConfig(config, argv) {
537
652
 
538
653
  config = setMediaDirectory(config, argv);
539
654
 
655
+ config = setFailedTestRecording(config, argv);
656
+
657
+ config = setFailedTestDirectory(config, argv);
658
+
540
659
  config = setRecursion(config, argv);
541
660
 
661
+ config = setFileTypes(config, argv);
662
+
542
663
  config = setTestFileExtensions(config, argv);
543
664
 
544
665
  config = setBrowserHeadless(config, argv);
@@ -620,13 +741,14 @@ function setFiles(config) {
620
741
  }
621
742
 
622
743
  // Parse files for tests
623
- function parseFiles(config, files) {
744
+ function parseTests(config, files) {
624
745
  let json = { tests: [] };
625
746
 
626
747
  // Loop through test files
627
748
  files.forEach((file) => {
628
749
  log(config, "debug", `file: ${file}`);
629
- let id = uuid.v4();
750
+ let fileId = `${uuid.v4()}`;
751
+ let id = fileId;
630
752
  let line;
631
753
  let lineNumber = 1;
632
754
  let inputFile = new nReadlines(file);
@@ -634,36 +756,135 @@ function parseFiles(config, files) {
634
756
  let fileType = config.fileTypes.find((fileType) =>
635
757
  fileType.extensions.includes(extension)
636
758
  );
759
+ let testStartStatementOpen;
760
+ let testStartStatementClose;
761
+ let testEndStatement;
762
+ let actionStatementOpen;
763
+ let actionStatementClose;
764
+
765
+ if (typeof fileType != "undefined") {
766
+ testStartStatementOpen = fileType.testStartStatementOpen;
767
+ if (!testStartStatementOpen) {
768
+ log(
769
+ config,
770
+ "warning",
771
+ `Skipping tests in ${file}. No 'testStartStatementOpen' value specified.`
772
+ );
773
+ return;
774
+ }
775
+ testStartStatementClose = fileType.testStartStatementClose;
776
+ if (!testStartStatementClose) {
777
+ log(
778
+ config,
779
+ "warning",
780
+ `Skipping tests in ${file}. No 'testStartStatementClose' value specified.`
781
+ );
782
+ return;
783
+ }
784
+ testEndStatement = fileType.testEndStatement;
785
+ if (!testEndStatement) {
786
+ log(
787
+ config,
788
+ "warning",
789
+ `Skipping tests in ${file}. No 'testEndStatement' value specified.`
790
+ );
791
+ return;
792
+ }
793
+ actionStatementOpen =
794
+ fileType.actionStatementOpen ||
795
+ fileType.openActionStatement ||
796
+ fileType.openTestStatement;
797
+ if (!actionStatementOpen) {
798
+ log(
799
+ config,
800
+ "warning",
801
+ `Skipping tests in ${file}. No 'actionStatementOpen' value specified.`
802
+ );
803
+ return;
804
+ }
805
+ actionStatementClose =
806
+ fileType.actionStatementClose ||
807
+ fileType.closeActionStatement ||
808
+ fileType.closeTestStatement;
809
+ if (!actionStatementClose) {
810
+ log(
811
+ config,
812
+ "warning",
813
+ `Skipping tests in ${file}. No 'actionStatementClose' value specified.`
814
+ );
815
+ return;
816
+ }
817
+ }
818
+
637
819
  if (!fileType && extension !== ".json") {
638
820
  // Missing filetype options
639
- console.log(
640
- `Error: Specify options for the ${extension} extension in your config file.`
821
+ log(
822
+ config,
823
+ "warning",
824
+ `Skipping file with ${extension} extension. Specify options for the ${extension} extension in your config file.`
641
825
  );
642
- exit(1);
826
+ return;
643
827
  }
644
828
 
645
829
  // If file is JSON, add tests straight to array
646
830
  if (path.extname(file) === ".json") {
647
831
  content = require(file);
648
- content.tests.forEach((test) => {
649
- json.tests.push(test);
650
- });
832
+ if (typeof content.tests === "object" && content.tests.length > 0) {
833
+ content.tests.forEach((test) => {
834
+ json.tests.push(test);
835
+ });
836
+ } else {
837
+ log(
838
+ config,
839
+ "warning",
840
+ `Skipping ${file} because of unexpected object structure.`
841
+ );
842
+ return;
843
+ }
651
844
  } else {
652
845
  // Loop through lines
653
846
  while ((line = inputFile.next())) {
654
- let lineJson = "";
655
- let subStart = "";
656
- let subEnd = "";
657
- if (line.includes(fileType.openTestStatement)) {
658
- const lineAscii = line.toString("ascii");
659
- if (fileType.closeTestStatement) {
660
- subEnd = lineAscii.lastIndexOf(fileType.closeTestStatement);
847
+ let lineJson;
848
+ let subStart;
849
+ let subEnd;
850
+ const lineAscii = line.toString("ascii");
851
+
852
+ if (line.includes(testStartStatementOpen)) {
853
+ // Test start
854
+ if (testStartStatementClose) {
855
+ subEnd = lineAscii.lastIndexOf(testStartStatementClose);
661
856
  } else {
662
857
  subEnd = lineAscii.length;
663
858
  }
664
859
  subStart =
665
- lineAscii.indexOf(fileType.openTestStatement) +
666
- fileType.openTestStatement.length;
860
+ lineAscii.indexOf(testStartStatementOpen) +
861
+ testStartStatementOpen.length;
862
+ lineJson = JSON.parse(lineAscii.substring(subStart, subEnd));
863
+ // If test is defined in this file instead of referencing a test defined in another file
864
+ if (!lineJson.file) {
865
+ test = { id, file, actions: [] };
866
+ if (lineJson.id) {
867
+ test.id = lineJson.id;
868
+ // Set ID for following actions
869
+ id = lineJson.id;
870
+ }
871
+ if (lineJson.saveFailedTestRecordings)
872
+ test.saveFailedTestRecordings = lineJson.saveFailedTestRecordings;
873
+ if (lineJson.failedTestDirectory)
874
+ test.failedTestDirectory = lineJson.failedTestDirectory;
875
+ json.tests.push(test);
876
+ }
877
+ } else if (line.includes(testEndStatement)) {
878
+ // Revert back to file-based ID
879
+ id = fileId;
880
+ } else if (line.includes(actionStatementOpen)) {
881
+ if (actionStatementClose) {
882
+ subEnd = lineAscii.lastIndexOf(actionStatementClose);
883
+ } else {
884
+ subEnd = lineAscii.length;
885
+ }
886
+ subStart =
887
+ lineAscii.indexOf(actionStatementOpen) + actionStatementOpen.length;
667
888
  lineJson = JSON.parse(lineAscii.substring(subStart, subEnd));
668
889
  if (!lineJson.testId) {
669
890
  lineJson.testId = id;
@@ -692,6 +913,7 @@ async function outputResults(config, results) {
692
913
  log(config, "info", "RESULTS:");
693
914
  log(config, "info", results);
694
915
  log(config, "info", `See detailed results at ${config.output}`);
916
+ log(config, "info", "Cleaning up and finishing post-processing.");
695
917
  }
696
918
 
697
919
  async function setEnvs(envsFile) {
@@ -759,3 +981,14 @@ function loadEnvsForObject(object) {
759
981
  });
760
982
  return object;
761
983
  }
984
+
985
+ function timestamp() {
986
+ let timestamp = new Date();
987
+ return `${timestamp.getFullYear()}${("0" + (timestamp.getMonth() + 1)).slice(
988
+ -2
989
+ )}${("0" + timestamp.getDate()).slice(-2)}-${(
990
+ "0" + timestamp.getHours()
991
+ ).slice(-2)}${("0" + timestamp.getMinutes()).slice(-2)}${(
992
+ "0" + timestamp.getSeconds()
993
+ ).slice(-2)}`;
994
+ }