doc-detective 1.0.4 → 1.0.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doc-detective",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Unit test documentation (and record videos of those tests).",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -14,14 +14,16 @@
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
+ "openActionStatement": "[comment]: # (action",
26
+ "closeActionStatement": ")"
25
27
  },
26
28
  {
27
29
  "extensions": [
@@ -29,8 +31,8 @@
29
31
  ".htm",
30
32
  ".xml"
31
33
  ],
32
- "openTestStatement": "<!-- test",
33
- "closeTestStatement": "-->"
34
+ "openActionStatement": "<!-- action",
35
+ "closeActionStatement": "-->"
34
36
  }
35
37
  ],
36
38
  "browserOptions": {
@@ -4,19 +4,19 @@ To use Google Search to find information on kittens,
4
4
 
5
5
  1. Open [Google Search](https://www.google.com).
6
6
 
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"})
7
+ [comment]: # (action {"testId":"process-search-kittens", "action":"startRecording", "overwrite":false, "filename":"results.gif", "fps":15})
8
+ [comment]: # (action {"testId":"process-search-kittens", "action":"goTo", "uri":"www.google.com"})
9
9
 
10
10
  2. In the search bar, enter "kittens", then press Enter.
11
11
 
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})
12
+ [comment]: # (action {"testId":"process-search-kittens", "action":"moveMouse", "css":"#gbqfbb", "alignH": "center", "alignV": "center"})
13
+ [comment]: # (action {"testId":"process-search-kittens", "action":"wait", "duration":"5000"})
14
+ [comment]: # (action {"testId":"process-search-kittens", "action":"moveMouse", "css":"[title=Search]", "alignV": "center"})
15
+ [comment]: # (action {"testId":"process-search-kittens", "action":"type", "css":"[title=Search]", "keys":"kittens", "trailingSpecialKey":"Enter"})
16
+ [comment]: # (action {"testId":"process-search-kittens", "action":"wait", "duration":"5000"})
17
+ [comment]: # (action {"testId":"process-search-kittens", "action":"scroll", "y": 300})
18
+ [comment]: # (action {"testId":"process-search-kittens", "action":"stopRecording"})
19
+ [comment]: # (action {"testId":"process-search-kittens", "action":"screenshot", "filename":"results.png", "matchPrevious": true, "matchThreshold": 0.1})
20
20
 
21
21
  Search results appear on the page.
22
22
 
@@ -26,17 +26,17 @@ Search results appear on the page.
26
26
 
27
27
  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
28
 
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"})
29
+ [comment]: # (action {"testId":"text-match-lucky", "action":"goTo", "uri":"www.google.com"})
30
+ [comment]: # (action {"testId":"text-match-lucky", "action":"matchText", "css":"#gbqfbb", "text":"I'm Feeling Lucky"})
31
31
 
32
32
  1. Open [Google Search](https://www.google.com).
33
33
 
34
- [comment]: # (test {"testId":"process-lucky-shorthair", "action":"goTo", "uri":"www.google.com"})
34
+ [comment]: # (action {"testId":"process-lucky-shorthair", "action":"goTo", "uri":"www.google.com"})
35
35
 
36
36
  2. In the search bar, enter "american shorthair cats".
37
37
 
38
- [comment]: # (test {"testId":"process-lucky-shorthair", "action":"type", "css":"[title=Search]", "keys":"american shorthair cats"})
38
+ [comment]: # (action {"testId":"process-lucky-shorthair", "action":"type", "css":"[title=Search]", "keys":"american shorthair cats"})
39
39
 
40
40
  3. Click **I'm Feeling Lucky**.
41
41
 
42
- [comment]: # (test {"testId":"process-lucky-shorthair", "action":"click", "css":"#gbqfbb"})
42
+ [comment]: # (action {"testId":"process-lucky-shorthair", "action":"click", "css":"#gbqfbb"})
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,16 @@
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
+ "openActionStatement": "[comment]: # (action",
26
+ "closeActionStatement": ")"
25
27
  },
26
28
  {
27
29
  "extensions": [
@@ -29,8 +31,8 @@
29
31
  ".htm",
30
32
  ".xml"
31
33
  ],
32
- "openTestStatement": "<!-- test",
33
- "closeTestStatement": "-->"
34
+ "openActionStatement": "<!-- action",
35
+ "closeActionStatement": "-->"
34
36
  }
35
37
  ],
36
38
  "browserOptions": {
@@ -1,6 +1,4 @@
1
1
  const axios = require("axios");
2
- const { async } = require("q");
3
- const { exit } = require("yargs");
4
2
  const { setEnvs, loadEnvsForObject } = require("../utils");
5
3
 
6
4
  exports.httpRequest = httpRequest;
@@ -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
@@ -16,6 +16,7 @@ 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";
@@ -284,6 +285,67 @@ function setMediaDirectory(config, argv) {
284
285
  return config;
285
286
  }
286
287
 
288
+ function setFailedTestRecording(config, argv) {
289
+ config.saveFailedTestRecordings =
290
+ argv.saveFailedTestRecordings ||
291
+ process.env.DOC_SAVE_FAILED_RECORDINGS ||
292
+ config.saveFailedTestRecordings;
293
+ switch (config.saveFailedTestRecordings) {
294
+ case true:
295
+ case "true":
296
+ config.saveFailedTestRecordings = true;
297
+ log(
298
+ config,
299
+ "debug",
300
+ `Save failed test recordings set: ${config.saveFailedTestRecordings}`
301
+ );
302
+ break;
303
+ case false:
304
+ case "false":
305
+ config.saveFailedTestRecordings = false;
306
+ log(
307
+ config,
308
+ "debug",
309
+ `Save failed test recordings set: ${config.saveFailedTestRecordings}`
310
+ );
311
+ log(config, "info", analyticsRequest);
312
+ break;
313
+ default:
314
+ config.saveFailedTestRecordings = defaultConfig.saveFailedTestRecordings;
315
+ log(
316
+ config,
317
+ "warning",
318
+ `Invalid save failed test recordings value. Reverted to default: ${config.saveFailedTestRecordings}`
319
+ );
320
+ }
321
+ return config;
322
+ }
323
+
324
+ function setFailedTestDirectory(config, argv) {
325
+ config.failedTestDirectory =
326
+ argv.failedTestDirectory ||
327
+ process.env.DOC_FAILED_TEST_DIRECTORY_PATH ||
328
+ config.failedTestDirectory;
329
+ config.failedTestDirectory = path.resolve(config.failedTestDirectory);
330
+ if (fs.existsSync(config.failedTestDirectory)) {
331
+ log(
332
+ config,
333
+ "debug",
334
+ `Failed test directory set: ${config.failedTestDirectory}`
335
+ );
336
+ } else {
337
+ config.failedTestDirectory = path.resolve(
338
+ defaultConfig.failedTestDirectory
339
+ );
340
+ log(
341
+ config,
342
+ "warning",
343
+ `Invalid failed test directory. Reverted to default: ${config.failedTestDirectory}`
344
+ );
345
+ }
346
+ return config;
347
+ }
348
+
287
349
  function setRecursion(config, argv) {
288
350
  config.recursive =
289
351
  argv.recursive || process.env.DOC_RECURSIVE || config.recursive;
@@ -537,6 +599,10 @@ function setConfig(config, argv) {
537
599
 
538
600
  config = setMediaDirectory(config, argv);
539
601
 
602
+ config = setFailedTestRecording(config, argv);
603
+
604
+ config = setFailedTestDirectory(config, argv);
605
+
540
606
  config = setRecursion(config, argv);
541
607
 
542
608
  config = setTestFileExtensions(config, argv);
@@ -654,16 +720,16 @@ function parseFiles(config, files) {
654
720
  let lineJson = "";
655
721
  let subStart = "";
656
722
  let subEnd = "";
657
- if (line.includes(fileType.openTestStatement)) {
723
+ if (line.includes(fileType.openActionStatement)) {
658
724
  const lineAscii = line.toString("ascii");
659
- if (fileType.closeTestStatement) {
660
- subEnd = lineAscii.lastIndexOf(fileType.closeTestStatement);
725
+ if (fileType.closeActionStatement) {
726
+ subEnd = lineAscii.lastIndexOf(fileType.closeActionStatement);
661
727
  } else {
662
728
  subEnd = lineAscii.length;
663
729
  }
664
730
  subStart =
665
- lineAscii.indexOf(fileType.openTestStatement) +
666
- fileType.openTestStatement.length;
731
+ lineAscii.indexOf(fileType.openActionStatement) +
732
+ fileType.openActionStatement.length;
667
733
  lineJson = JSON.parse(lineAscii.substring(subStart, subEnd));
668
734
  if (!lineJson.testId) {
669
735
  lineJson.testId = id;
@@ -692,6 +758,7 @@ async function outputResults(config, results) {
692
758
  log(config, "info", "RESULTS:");
693
759
  log(config, "info", results);
694
760
  log(config, "info", `See detailed results at ${config.output}`);
761
+ log(config, "info", "Cleaning up and finishing post-processing.");
695
762
  }
696
763
 
697
764
  async function setEnvs(envsFile) {
@@ -759,3 +826,14 @@ function loadEnvsForObject(object) {
759
826
  });
760
827
  return object;
761
828
  }
829
+
830
+ function timestamp() {
831
+ let timestamp = new Date();
832
+ return `${timestamp.getFullYear()}${("0" + (timestamp.getMonth() + 1)).slice(
833
+ -2
834
+ )}${("0" + timestamp.getDate()).slice(-2)}-${(
835
+ "0" + timestamp.getHours()
836
+ ).slice(-2)}${("0" + timestamp.getMinutes()).slice(-2)}${(
837
+ "0" + timestamp.getSeconds()
838
+ ).slice(-2)}`;
839
+ }