doc-detective 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/suggest.js ADDED
@@ -0,0 +1,4 @@
1
+ const { suggest } = require("../src/index.js");
2
+ const { argv } = require("node:process");
3
+
4
+ suggest({}, argv);
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "doc-detective",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Unit test documentation (and record videos of those tests).",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
7
7
  "test": "node ./cli/index.js",
8
- "coverage": "node ./cli/coverage.js"
8
+ "coverage": "node ./cli/coverage.js",
9
+ "suggest": "node ./cli/suggest.js"
9
10
  },
10
11
  "repository": {
11
12
  "type": "git",
@@ -33,6 +34,7 @@
33
34
  "n-readlines": "^1.0.1",
34
35
  "pixelmatch": "^5.3.0",
35
36
  "pngjs": "^6.0.0",
37
+ "prompt-sync": "^4.2.0",
36
38
  "puppeteer": "^13.7.0",
37
39
  "puppeteer-screen-recorder": "^2.0.2",
38
40
  "uuid": "^8.3.2",
@@ -41,4 +43,4 @@
41
43
  "devDependencies": {
42
44
  "make": "^0.8.1"
43
45
  }
44
- }
46
+ }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "Doc Detective Content Coverage Report",
3
- "timestamp": "20221017-083214",
3
+ "timestamp": "20221021-132211",
4
4
  "summary": {
5
- "covered": 12,
5
+ "covered": 11,
6
6
  "uncovered": 0,
7
7
  "onscreenText": {
8
8
  "covered": 2,
@@ -37,14 +37,14 @@
37
37
  "uncovered": 0
38
38
  },
39
39
  "interaction": {
40
- "covered": 2,
40
+ "covered": 1,
41
41
  "uncovered": 0
42
42
  }
43
43
  },
44
44
  "files": [
45
45
  {
46
46
  "file": "/config/workspace/doc-detective/sample/doc-content.md",
47
- "covered": 12,
47
+ "covered": 11,
48
48
  "uncovered": 0,
49
49
  "onscreenText": {
50
50
  "covered": 2,
@@ -87,7 +87,7 @@
87
87
  "uncoveredMatches": []
88
88
  },
89
89
  "interaction": {
90
- "covered": 2,
90
+ "covered": 1,
91
91
  "uncovered": 0,
92
92
  "uncoveredMatches": []
93
93
  }
package/src/config.json CHANGED
@@ -6,6 +6,9 @@
6
6
  "cleanup": "",
7
7
  "recursive": true,
8
8
  "coverageOutput": "./sample/coverage.json",
9
+ "testSuggestions": {
10
+ "reportOutput": "./sample/suggestions.json"
11
+ },
9
12
  "testExtensions": [
10
13
  ".md",
11
14
  ".mdx",
@@ -31,42 +34,62 @@
31
34
  "actionStatementOpen": "[comment]: # (action",
32
35
  "actionStatementClose": ")",
33
36
  "markup": {
34
- "onscreenText": [
35
- "\\*\\*.+?\\*\\*"
36
- ],
37
- "emphasis": [
38
- "(?<!\\*)\\*(?!\\*).+?(?<!\\*)\\*(?!\\*)"
39
- ],
40
- "image": [
41
- "!\\[.+?\\]\\(.+?\\)"
42
- ],
43
- "hyperlink": [
44
- "(?<!!)\\[.+?\\]\\(.+?\\)"
45
- ],
46
- "orderedList": [
47
- "(?<=\n) *?[0-9][0-9]?[0-9]?.\\s*.*"
48
- ],
49
- "unorderedList": [
50
- "(?<=\n) *?\\*.\\s*.*",
51
- "(?<=\n) *?-.\\s*.*"
52
- ],
53
- "codeInline": [
54
- "(?<!`)`(?!`).+?(?<!`)`(?!`)"
55
- ],
56
- "codeBlock": [
57
- "(?=(```))(\\w|\\W)*(?<=```)"
58
- ],
59
- "interaction": [
60
- "[cC]lick",
61
- "[tT]ap",
62
- "[tT]ouch",
63
- "[sS]elect",
64
- "[cC]hoose",
65
- "[tT]oggle",
66
- "[eE]nable",
67
- "[dD]isable",
68
- "[tT]urn [oO][ff|n]"
69
- ]
37
+ "onscreenText": {
38
+ "regex": [
39
+ "\\*\\*.+?\\*\\*"
40
+ ]
41
+ },
42
+ "emphasis": {
43
+ "regex": [
44
+ "(?<!\\*)\\*(?!\\*).+?(?<!\\*)\\*(?!\\*)"
45
+ ]
46
+ },
47
+ "image": {
48
+ "regex": [
49
+ "!\\[.+?\\]\\(.+?\\)"
50
+ ]
51
+ },
52
+ "hyperlink": {
53
+ "regex": [
54
+ "(?<!!)\\[.+?\\]\\(.+?\\)"
55
+ ]
56
+ },
57
+ "orderedList": {
58
+ "regex": [
59
+ "(?<=\n) *?[0-9][0-9]?[0-9]?.\\s*.*"
60
+ ]
61
+ },
62
+ "unorderedList": {
63
+ "regex": [
64
+ "(?<=\n) *?\\*.\\s*.*",
65
+ "(?<=\n) *?-.\\s*.*"
66
+ ]
67
+ },
68
+ "codeInline": {
69
+ "regex": [
70
+ "(?<!`)`(?!`).+?(?<!`)`(?!`)"
71
+ ]
72
+ },
73
+ "codeBlock": {
74
+ "regex": [
75
+ "(?=(```))(\\w|\\W)*(?<=```)"
76
+ ]
77
+ },
78
+ "interaction": {
79
+ "regex": [
80
+ "[cC]lick",
81
+ "[tT]ap",
82
+ "[tT]ouch",
83
+ "[sS]elect",
84
+ "[cC]hoose",
85
+ "[tT]oggle",
86
+ "[eE]nable",
87
+ "[dD]isable",
88
+ "[tT]urn [oO][ff|n]",
89
+ "[tT]ype",
90
+ "[eE]nter"
91
+ ]
92
+ }
70
93
  }
71
94
  },
72
95
  {
@@ -82,41 +105,61 @@
82
105
  "actionStatementOpen": "<!-- action",
83
106
  "actionStatementClose": "-->",
84
107
  "markup": {
85
- "onscreenText": [
86
- "(?=(<b))(\\w|\\W)*(?<=<\/b>)"
87
- ],
88
- "emphasis": [
89
- "(?=(<i))(\\w|\\W)*(?<=<\/i>)"
90
- ],
91
- "image": [
92
- "(?=(<img))(\\w|\\W)*(?<=<\/img>|>)"
93
- ],
94
- "hyperlink": [
95
- "(?=(<a))(\\w|\\W)*(?<=<\/a>)"
96
- ],
97
- "orderedList": [
98
- "(?=(<ol))(\\w|\\W)*(?<=<\/ol>)"
99
- ],
100
- "unorderedList": [
101
- "(?=(<ul))(\\w|\\W)*(?<=<\/ul>)"
102
- ],
103
- "codeInline": [
104
- "(?=(<code))(\\w|\\W)*(?<=<\/code>)"
105
- ],
106
- "codeBlock": [
107
- "(?=(<pre))(\\w|\\W)*(?<=<\/pre>)"
108
- ],
109
- "interaction": [
110
- "[cC]lick",
111
- "[tT]ap",
112
- "[tT]ouch",
113
- "[sS]elect",
114
- "[cC]hoose",
115
- "[tT]oggle",
116
- "[eE]nable",
117
- "[dD]isable",
118
- "[tT]urn [oO][ff|n]"
119
- ]
108
+ "onscreenText": {
109
+ "regex": [
110
+ "(?=(<b))(\\w|\\W)*(?<=<\/b>)"
111
+ ]
112
+ },
113
+ "emphasis": {
114
+ "regex": [
115
+ "(?=(<i))(\\w|\\W)*(?<=<\/i>)"
116
+ ]
117
+ },
118
+ "image": {
119
+ "regex": [
120
+ "(?=(<img))(\\w|\\W)*(?<=<\/img>|>)"
121
+ ]
122
+ },
123
+ "hyperlink": {
124
+ "regex": [
125
+ "(?=(<a))(\\w|\\W)*(?<=<\/a>)"
126
+ ]
127
+ },
128
+ "orderedList": {
129
+ "regex": [
130
+ "(?=(<ol))(\\w|\\W)*(?<=<\/ol>)"
131
+ ]
132
+ },
133
+ "unorderedList": {
134
+ "regex": [
135
+ "(?=(<ul))(\\w|\\W)*(?<=<\/ul>)"
136
+ ]
137
+ },
138
+ "codeInline": {
139
+ "regex": [
140
+ "(?=(<code))(\\w|\\W)*(?<=<\/code>)"
141
+ ]
142
+ },
143
+ "codeBlock": {
144
+ "regex": [
145
+ "(?=(<pre))(\\w|\\W)*(?<=<\/pre>)"
146
+ ]
147
+ },
148
+ "interaction": {
149
+ "regex": [
150
+ "[cC]lick",
151
+ "[tT]ap",
152
+ "[tT]ouch",
153
+ "[sS]elect",
154
+ "[cC]hoose",
155
+ "[tT]oggle",
156
+ "[eE]nable",
157
+ "[dD]isable",
158
+ "[tT]urn [oO][ff|n]",
159
+ "[tT]ype",
160
+ "[eE]nter"
161
+ ]
162
+ }
120
163
  }
121
164
  }
122
165
  ],
package/src/index.js CHANGED
@@ -8,10 +8,14 @@ const {
8
8
  } = require("./lib/utils");
9
9
  const { sendAnalytics } = require("./lib/analytics.js");
10
10
  const { runTests } = require("./lib/tests");
11
- const { checkTestCoverage, checkMarkupCoverage } = require("./lib/coverage");
11
+ const { checkTestCoverage, checkMarkupCoverage } = require("./lib/analysis");
12
+ const { reportCoverage } = require("./lib/coverage");
13
+ const { suggestTests, runSuggestions } = require("./lib/suggest");
14
+ const { exit } = require("process");
12
15
 
13
16
  exports.run = main;
14
17
  exports.coverage = coverage;
18
+ exports.suggest = suggest;
15
19
 
16
20
  async function main(config, argv) {
17
21
  // Set args
@@ -44,7 +48,7 @@ async function main(config, argv) {
44
48
  // Output
45
49
  outputResults(config.output, results, config);
46
50
  if (config.analytics.send) {
47
- sendAnalytics(config, results);
51
+ // sendAnalytics(config, results);
48
52
  }
49
53
  }
50
54
 
@@ -65,13 +69,55 @@ async function coverage(config, argv) {
65
69
  log(config, "debug", files);
66
70
 
67
71
  const testCoverage = checkTestCoverage(config, files);
68
- log(config, "debug", "(DEBUG) TEST COVERAGE:");
72
+ log(config, "debug", "TEST COVERAGE:");
69
73
  log(config, "debug", testCoverage);
70
74
 
71
75
  const markupCoverage = checkMarkupCoverage(config, testCoverage);
72
- log(config, "debug", "(DEBUG) MARKUP COVERAGE:");
76
+ log(config, "debug", "MARKUP COVERAGE:");
73
77
  log(config, "debug", markupCoverage);
74
78
 
79
+ const coverageReport = reportCoverage(config, markupCoverage);
80
+ log(config, "debug", "COVERAGE REPORT:");
81
+ log(config, "debug", coverageReport);
82
+
83
+ // Output
84
+ outputResults(config.coverageOutput, coverageReport, config);
85
+ }
86
+
87
+ async function suggest(config, argv) {
88
+ // Set args
89
+ argv = setArgs(argv);
90
+ log(config, "debug", `ARGV:`);
91
+ log(config, "debug", argv);
92
+
93
+ // Set config
94
+ config = setConfig(config, argv);
95
+ log(config, "debug", `CONFIG:`);
96
+ log(config, "debug", config);
97
+
98
+ // Set files
99
+ const files = setFiles(config);
100
+ log(config, "debug", `FILES:`);
101
+ log(config, "debug", files);
102
+
103
+ const testCoverage = checkTestCoverage(config, files);
104
+ log(config, "debug", "TEST COVERAGE:");
105
+ log(config, "debug", testCoverage);
106
+
107
+ const markupCoverage = checkMarkupCoverage(config, testCoverage);
108
+ log(config, "debug", "MARKUP COVERAGE:");
109
+ log(config, "debug", markupCoverage);
110
+
111
+ const suggestionReport = suggestTests(config, markupCoverage);
112
+ log(config, "debug", "TEST SUGGESTIONS:");
113
+ log(config, "debug", suggestionReport);
114
+
115
+ await runSuggestions(config, suggestionReport);
116
+
75
117
  // Output
76
- outputResults(config.coverageOutput, markupCoverage, config);
118
+ outputResults(
119
+ config.testSuggestions.reportOutput,
120
+ suggestionReport,
121
+ config
122
+ );
77
123
  }
@@ -0,0 +1,276 @@
1
+ const { log } = require("./utils");
2
+ const uuid = require("uuid");
3
+ const nReadlines = require("n-readlines");
4
+ const path = require("path");
5
+ const { exit } = require("process");
6
+ const fs = require("fs");
7
+
8
+ exports.checkTestCoverage = checkTestCoverage;
9
+ exports.checkMarkupCoverage = checkMarkupCoverage;
10
+
11
+ function checkTestCoverage(config, files) {
12
+ let testCoverage = {
13
+ files: [],
14
+ errors: [],
15
+ };
16
+
17
+ // Loop through test files
18
+ files.forEach((file) => {
19
+ log(config, "debug", `file: ${file}`);
20
+ fileJson = {
21
+ file,
22
+ coveredLines: [],
23
+ uncoveredLines: [],
24
+ };
25
+ let inTest = false;
26
+ let line;
27
+ let lineNumber = 1;
28
+ let inputFile = new nReadlines(file);
29
+ let extension = path.extname(file);
30
+ let fileType = config.fileTypes.find((fileType) =>
31
+ fileType.extensions.includes(extension)
32
+ );
33
+ fileJson.fileType = fileType;
34
+
35
+ if (typeof fileType === "undefined") {
36
+ // Missing filetype options
37
+ log(
38
+ config,
39
+ "debug",
40
+ `Skipping ${file}. Specify options for the ${extension} extension in your config file.`
41
+ );
42
+ return;
43
+ }
44
+
45
+ let testStartStatementOpen = fileType.testStartStatementOpen;
46
+ if (!testStartStatementOpen) {
47
+ log(
48
+ config,
49
+ "warning",
50
+ `Skipping ${file}. No 'testStartStatementOpen' value specified.`
51
+ );
52
+ return;
53
+ }
54
+ let testStartStatementClose = fileType.testStartStatementClose;
55
+ if (!testStartStatementClose) {
56
+ log(
57
+ config,
58
+ "warning",
59
+ `Skipping ${file}. No 'testStartStatementClose' value specified.`
60
+ );
61
+ return;
62
+ }
63
+ let testIgnoreStatement = fileType.testIgnoreStatement;
64
+ if (!testIgnoreStatement) {
65
+ log(
66
+ config,
67
+ "warning",
68
+ `Skipping ${file}. No 'testIgnoreStatement' value specified.`
69
+ );
70
+ return;
71
+ }
72
+ let testEndStatement = fileType.testEndStatement;
73
+ if (!testEndStatement) {
74
+ log(
75
+ config,
76
+ "warning",
77
+ `Skipping ${file}. No 'testEndStatement' value specified.`
78
+ );
79
+ return;
80
+ }
81
+ let actionStatementOpen =
82
+ fileType.actionStatementOpen ||
83
+ fileType.openActionStatement ||
84
+ fileType.openTestStatement;
85
+ if (!actionStatementOpen) {
86
+ log(
87
+ config,
88
+ "warning",
89
+ `Skipping ${file}. No 'actionStatementOpen' value specified.`
90
+ );
91
+ return;
92
+ }
93
+ let actionStatementClose =
94
+ fileType.actionStatementClose ||
95
+ fileType.closeActionStatement ||
96
+ fileType.closeTestStatement;
97
+ if (!actionStatementClose) {
98
+ log(
99
+ config,
100
+ "warning",
101
+ `Skipping ${file}. No 'actionStatementClose' value specified.`
102
+ );
103
+ return;
104
+ }
105
+
106
+ // Loop through lines
107
+ while ((line = inputFile.next())) {
108
+ let lineJson;
109
+ let subStart;
110
+ let subEnd;
111
+ let ignore = false;
112
+ const lineAscii = line.toString("ascii");
113
+
114
+ if (line.includes(testStartStatementOpen)) {
115
+ // Test start
116
+ if (testStartStatementClose) {
117
+ subEnd = lineAscii.lastIndexOf(testStartStatementClose);
118
+ } else {
119
+ subEnd = lineAscii.length;
120
+ }
121
+ subStart =
122
+ lineAscii.indexOf(testStartStatementOpen) +
123
+ testStartStatementOpen.length;
124
+ lineJson = JSON.parse(lineAscii.substring(subStart, subEnd));
125
+ // Set inTest to true
126
+ inTest = true;
127
+ ignore = true;
128
+ // Check if test is defined externally
129
+ if (lineJson.file) {
130
+ referencePath = path.resolve(path.dirname(file), lineJson.file);
131
+ // Check to make sure file exists
132
+ if (fs.existsSync(referencePath)) {
133
+ if (lineJson.id) {
134
+ remoteJson = require(referencePath);
135
+ // Make sure test of matching ID exists in file
136
+ idMatch = remoteJson.tests.find(
137
+ (test) => test.id === lineJson.id
138
+ );
139
+ if (!idMatch) {
140
+ // log error
141
+ testCoverage.errors.push({
142
+ file,
143
+ lineNumber,
144
+ description: `Test with ID ${lineJson.id} missing from ${referencePath}.`,
145
+ });
146
+ }
147
+ }
148
+ } else {
149
+ // log error
150
+ testCoverage.errors.push({
151
+ file,
152
+ lineNumber,
153
+ description: `Referenced file missing: ${referencePath}.`,
154
+ });
155
+ }
156
+ }
157
+ } else if (line.includes(testIgnoreStatement)) {
158
+ inTest = true;
159
+ ignore = true;
160
+ } else if (line.includes(testEndStatement)) {
161
+ inTest = false;
162
+ ignore = true;
163
+ } else if (line.includes(actionStatementOpen) && line.includes(actionStatementClose)) {
164
+ ignore = true;
165
+ }
166
+
167
+ if (inTest && !ignore) {
168
+ fileJson.coveredLines.push(lineNumber);
169
+ } else if (!inTest && !ignore) {
170
+ fileJson.uncoveredLines.push(lineNumber);
171
+ }
172
+
173
+ lineNumber++;
174
+ }
175
+ testCoverage.files.push(fileJson);
176
+ });
177
+ return testCoverage;
178
+ }
179
+
180
+ function checkMarkupCoverage(config, testCoverage) {
181
+ let markupCoverage = testCoverage;
182
+
183
+ markupCoverage.files.forEach((file) => {
184
+ file.markup = {};
185
+ let extension = path.extname(file.file);
186
+ let markup = file.fileType.markup;
187
+
188
+ Object.keys(markup).forEach((mark) => {
189
+ if (markup[mark].regex.length === 1 && markup[mark].regex[0] === "") {
190
+ log(
191
+ config,
192
+ "warning",
193
+ `No regex for '${mark}'. Set 'fileType.markup.${mark}' for the '${extension}' extension in your config.`
194
+ );
195
+ delete markup[mark];
196
+ }
197
+ });
198
+
199
+ const fileBody = fs.readFileSync(file.file, {
200
+ encoding: "utf8",
201
+ flag: "r",
202
+ });
203
+
204
+ // Only keep marks that have a truthy (>0) length
205
+ Object.keys(markup).forEach((mark) => {
206
+ markCoverage = {
207
+ coveredLines: [],
208
+ coveredMatches: [],
209
+ uncoveredLines: [],
210
+ uncoveredMatches: [],
211
+ }
212
+
213
+ markup[mark].regex.forEach((matcher) => {
214
+ // Run a match
215
+ regex = new RegExp(matcher, "g");
216
+ matches = fileBody.match(regex);
217
+ if (matches != null) {
218
+ matches.forEach((match) => {
219
+ // Check for duplicates and handle lines separately
220
+ matchEscaped = match.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
221
+ start = 0;
222
+ occuranceRegex = new RegExp(matchEscaped, "g");
223
+ occurances = fileBody.match(occuranceRegex).length;
224
+ for (i = 0; i < occurances; i++) {
225
+ index = fileBody.slice(start).match(matchEscaped).index;
226
+ line = fileBody
227
+ .slice(0, start + index)
228
+ .split(/\r\n|\r|\n/).length;
229
+ start = start + index + 1;
230
+ matchObject = {
231
+ line,
232
+ indexInFile: start + index,
233
+ text: match,
234
+ };
235
+ isCovered = file.coveredLines.includes(line);
236
+ isUncovered = file.uncoveredLines.includes(line);
237
+ inCoveredMatches = markCoverage.coveredMatches.some(
238
+ (object) =>
239
+ object.line === matchObject.line &&
240
+ object.text === matchObject.text &&
241
+ object.indexInFile === matchObject.indexInFile
242
+ );
243
+ inUncoveredMatches = markCoverage.uncoveredMatches.some(
244
+ (object) =>
245
+ object.line === matchObject.line &&
246
+ object.text === matchObject.text &&
247
+ object.indexInFile === matchObject.indexInFile
248
+ );
249
+ inCoveredLines = markCoverage.coveredLines.includes(line);
250
+ inUncoveredLines = markCoverage.uncoveredLines.includes(line);
251
+ // console.log({
252
+ // mark,
253
+ // matchObject,
254
+ // isCovered,
255
+ // isUncovered,
256
+ // inCoveredLines,
257
+ // inCoveredMatches,
258
+ // inUncoveredLines,
259
+ // inUncoveredMatches,
260
+ // });
261
+ if (isCovered) {
262
+ if (!inCoveredLines) markCoverage.coveredLines.push(line);
263
+ if (!inCoveredMatches) markCoverage.coveredMatches.push(matchObject);
264
+ } else if (isUncovered) {
265
+ if (!inUncoveredLines) markCoverage.uncoveredLines.push(line);
266
+ if (!inUncoveredMatches) markCoverage.uncoveredMatches.push(matchObject);
267
+ }
268
+ }
269
+ });
270
+ }
271
+ });
272
+ file.markup[mark] = markCoverage;
273
+ });
274
+ });
275
+ return markupCoverage;
276
+ }