doc-detective 1.2.2 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -298,6 +298,7 @@ Format:
298
298
  | `responseHeaders` | Optional. A non-exhaustive object of headers that should be included in the response. If a specified header is missing, or if a specified header value is incorrect, the action fails. If the response headers include headers that aren't specified by this option, the test can still pass. <br> Supports setting the whole headers object and individual header values from environment variables. | <ul><li>`{ "Content-Type": "application/json" }`</li><li>`$RESPONSE_HEADERS`</li><li>`{ "Content-Type": "$CONTENT_TYPE" }`</li></ul> |
299
299
  | `responseData` | Optional. A non-exhaustive data object that should be included in the response. If a specified data field is missing, or if a specified value is incorrect, the action fails. If the response data payload includes additional fields that aren't specified by this option, the test can still pass. <br> Supports setting the whole data object and individual data values from environment variables. | <ul><li>`{ "id": 1, "last_name": "Washington" }`</li><li>`"$REQUEST_DATA"`</li><li>`{ "id": 1, "last_name": "$LAST_NAME" }`</li></ul> |
300
300
  | `statusCodes` | Optional. An array of accepted HTTP status response codes. Defaults to `[200]`. If the response's status code isn't included in this array, the action fails. | [ 200, 204 ] |
301
+ | `envsFromResponseData` | Optional. An array of environment variables to dynamically set from response data using [`jq`](https://stedolan.github.io/jq/) filters to select and transform the JSON data.<br><br>`name` values can contain uppercase letters, lowercase letters, numbers and underscores, but **don't** include a leading "$". | <code>[ { "name": "FIRST_NAME", "jqFilter": ".first_name" } ]</code>
301
302
 
302
303
  ### Check a link
303
304
 
@@ -363,6 +364,8 @@ Format:
363
364
 
364
365
  ## Analytics
365
366
 
367
+ **Note:** Analytics is currently turned off globally.
368
+
366
369
  By default, Doc Detective doesn't collect any information about tests, devices, users, or documentation and doesn't check in with any external service or server. If you want to help inform decisions about the future development of Doc Detective—such as feature development and documentation creation—you can opt into sending anonymized analytics after you run tests at one of the multiple levels of detail.
367
370
 
368
371
  There are multiple ways to turn on analytics:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doc-detective",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "Unit test documentation (and record videos of those tests).",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -32,6 +32,7 @@
32
32
  "git": "^0.1.5",
33
33
  "gnumake": "^0.3.5",
34
34
  "n-readlines": "^1.0.1",
35
+ "node-jq": "^2.3.4",
35
36
  "pixelmatch": "^5.3.0",
36
37
  "pngjs": "^6.0.0",
37
38
  "prompt-sync": "^4.2.0",
@@ -43,4 +44,4 @@
43
44
  "devDependencies": {
44
45
  "make": "^0.8.1"
45
46
  }
46
- }
47
+ }
@@ -2,8 +2,6 @@
2
2
  "tests": [
3
3
  {
4
4
  "id": "process-search-kittens",
5
- "saveFailedTestRecordings": true,
6
- "failedTestDirectory": "sample",
7
5
  "actions": [
8
6
  {
9
7
  "action": "goTo",
@@ -13,49 +11,6 @@
13
11
  "description": "Opened URI."
14
12
  }
15
13
  },
16
- {
17
- "action": "startRecording",
18
- "overwrite": false,
19
- "filename": "results.gif",
20
- "fps": 15,
21
- "result": {
22
- "status": "PASS",
23
- "description": "Started recording: /config/workspace/doc-detective/sample/temp_results.mp4",
24
- "video": "/config/workspace/doc-detective/sample/temp_results.mp4"
25
- }
26
- },
27
- {
28
- "action": "moveMouse",
29
- "css": "#gbqfbb",
30
- "alignH": "center",
31
- "alignV": "center",
32
- "offsetX": 0,
33
- "offsetY": 0,
34
- "result": {
35
- "status": "PASS",
36
- "description": "Moved mouse to element."
37
- }
38
- },
39
- {
40
- "action": "wait",
41
- "duration": "5000",
42
- "result": {
43
- "status": "PASS",
44
- "description": "Wait complete."
45
- }
46
- },
47
- {
48
- "action": "moveMouse",
49
- "css": "[title=Search]",
50
- "alignV": "center",
51
- "alignH": "center",
52
- "offsetX": 0,
53
- "offsetY": 0,
54
- "result": {
55
- "status": "PASS",
56
- "description": "Moved mouse to element."
57
- }
58
- },
59
14
  {
60
15
  "action": "type",
61
16
  "css": "[title=Search]",
@@ -74,21 +29,6 @@
74
29
  "description": "Wait complete."
75
30
  }
76
31
  },
77
- {
78
- "action": "scroll",
79
- "y": 300,
80
- "result": {
81
- "status": "PASS",
82
- "description": "Scroll complete."
83
- }
84
- },
85
- {
86
- "action": "stopRecording",
87
- "result": {
88
- "status": "PASS",
89
- "description": "Stopped recording: /config/workspace/doc-detective/sample/results.gif"
90
- }
91
- },
92
32
  {
93
33
  "action": "screenshot",
94
34
  "filename": "results.png",
package/src/config.json CHANGED
@@ -35,47 +35,65 @@
35
35
  "actionStatementClose": ")",
36
36
  "markup": {
37
37
  "onscreenText": {
38
+ "includeInCoverage": true,
39
+ "includeInSuggestions": true,
38
40
  "regex": [
39
41
  "\\*\\*.+?\\*\\*"
40
42
  ]
41
43
  },
42
44
  "emphasis": {
45
+ "includeInCoverage": true,
46
+ "includeInSuggestions": true,
43
47
  "regex": [
44
48
  "(?<!\\*)\\*(?!\\*).+?(?<!\\*)\\*(?!\\*)"
45
49
  ]
46
50
  },
47
51
  "image": {
52
+ "includeInCoverage": true,
53
+ "includeInSuggestions": true,
48
54
  "regex": [
49
55
  "!\\[.+?\\]\\(.+?\\)"
50
56
  ]
51
57
  },
52
58
  "hyperlink": {
59
+ "includeInCoverage": true,
60
+ "includeInSuggestions": true,
53
61
  "regex": [
54
62
  "(?<!!)\\[.+?\\]\\(.+?\\)"
55
63
  ]
56
64
  },
57
65
  "orderedList": {
66
+ "includeInCoverage": true,
67
+ "includeInSuggestions": true,
58
68
  "regex": [
59
69
  "(?<=\n) *?[0-9][0-9]?[0-9]?.\\s*.*"
60
70
  ]
61
71
  },
62
72
  "unorderedList": {
73
+ "includeInCoverage": true,
74
+ "includeInSuggestions": true,
63
75
  "regex": [
64
76
  "(?<=\n) *?\\*.\\s*.*",
65
77
  "(?<=\n) *?-.\\s*.*"
66
78
  ]
67
79
  },
68
80
  "codeInline": {
81
+ "includeInCoverage": true,
82
+ "includeInSuggestions": true,
69
83
  "regex": [
70
84
  "(?<!`)`(?!`).+?(?<!`)`(?!`)"
71
85
  ]
72
86
  },
73
87
  "codeBlock": {
88
+ "includeInCoverage": true,
89
+ "includeInSuggestions": true,
74
90
  "regex": [
75
91
  "(?=(```))(\\w|\\W)*(?<=```)"
76
92
  ]
77
93
  },
78
94
  "interaction": {
95
+ "includeInCoverage": true,
96
+ "includeInSuggestions": true,
79
97
  "regex": [
80
98
  "[cC]lick",
81
99
  "[tT]ap",
@@ -106,46 +124,64 @@
106
124
  "actionStatementClose": "-->",
107
125
  "markup": {
108
126
  "onscreenText": {
127
+ "includeInCoverage": true,
128
+ "includeInSuggestions": true,
109
129
  "regex": [
110
130
  "(?=(<b))(\\w|\\W)*(?<=<\/b>)"
111
131
  ]
112
132
  },
113
133
  "emphasis": {
134
+ "includeInCoverage": true,
135
+ "includeInSuggestions": true,
114
136
  "regex": [
115
137
  "(?=(<i))(\\w|\\W)*(?<=<\/i>)"
116
138
  ]
117
139
  },
118
140
  "image": {
141
+ "includeInCoverage": true,
142
+ "includeInSuggestions": true,
119
143
  "regex": [
120
144
  "(?=(<img))(\\w|\\W)*(?<=<\/img>|>)"
121
145
  ]
122
146
  },
123
147
  "hyperlink": {
148
+ "includeInCoverage": true,
149
+ "includeInSuggestions": true,
124
150
  "regex": [
125
151
  "(?=(<a))(\\w|\\W)*(?<=<\/a>)"
126
152
  ]
127
153
  },
128
154
  "orderedList": {
155
+ "includeInCoverage": true,
156
+ "includeInSuggestions": true,
129
157
  "regex": [
130
158
  "(?=(<ol))(\\w|\\W)*(?<=<\/ol>)"
131
159
  ]
132
160
  },
133
161
  "unorderedList": {
162
+ "includeInCoverage": true,
163
+ "includeInSuggestions": true,
134
164
  "regex": [
135
165
  "(?=(<ul))(\\w|\\W)*(?<=<\/ul>)"
136
166
  ]
137
167
  },
138
168
  "codeInline": {
169
+ "includeInCoverage": true,
170
+ "includeInSuggestions": true,
139
171
  "regex": [
140
172
  "(?=(<code))(\\w|\\W)*(?<=<\/code>)"
141
173
  ]
142
174
  },
143
175
  "codeBlock": {
176
+ "includeInCoverage": true,
177
+ "includeInSuggestions": true,
144
178
  "regex": [
145
179
  "(?=(<pre))(\\w|\\W)*(?<=<\/pre>)"
146
180
  ]
147
181
  },
148
182
  "interaction": {
183
+ "includeInCoverage": true,
184
+ "includeInSuggestions": true,
149
185
  "regex": [
150
186
  "[cC]lick",
151
187
  "[tT]ap",
@@ -204,6 +204,8 @@ function checkMarkupCoverage(config, testCoverage) {
204
204
  // Only keep marks that have a truthy (>0) length
205
205
  Object.keys(markup).forEach((mark) => {
206
206
  markCoverage = {
207
+ includeInCoverage: markup[mark].includeInCoverage,
208
+ includeInSuggestions: markup[mark].includeInSuggestions,
207
209
  coveredLines: [],
208
210
  coveredMatches: [],
209
211
  uncoveredLines: [],
@@ -21,30 +21,32 @@ function reportCoverage(config, markupCoverage) {
21
21
  uncovered: 0,
22
22
  };
23
23
  Object.keys(file.markup).forEach((mark) => {
24
- if (typeof report.summary[mark] === "undefined") {
25
- report.summary[mark] = {
26
- covered: 0,
27
- uncovered: 0,
24
+ if (file.markup[mark].includeInCoverage) {
25
+ if (typeof report.summary[mark] === "undefined") {
26
+ report.summary[mark] = {
27
+ covered: 0,
28
+ uncovered: 0,
29
+ };
30
+ }
31
+ report.summary.covered =
32
+ report.summary.covered + file.markup[mark].coveredLines.length;
33
+ report.summary.uncovered =
34
+ report.summary.uncovered + file.markup[mark].uncoveredLines.length;
35
+ report.summary[mark].covered =
36
+ report.summary[mark].covered + file.markup[mark].coveredLines.length;
37
+ report.summary[mark].uncovered =
38
+ report.summary[mark].uncovered +
39
+ file.markup[mark].uncoveredLines.length;
40
+ fileJson.covered =
41
+ fileJson.covered + file.markup[mark].coveredLines.length;
42
+ fileJson.uncovered =
43
+ fileJson.uncovered + file.markup[mark].uncoveredLines.length;
44
+ fileJson[mark] = {
45
+ covered: file.markup[mark].coveredLines.length,
46
+ uncovered: file.markup[mark].uncoveredLines.length,
47
+ uncoveredMatches: file.markup[mark].uncoveredMatches,
28
48
  };
29
49
  }
30
- report.summary.covered =
31
- report.summary.covered + file.markup[mark].coveredLines.length;
32
- report.summary.uncovered =
33
- report.summary.uncovered + file.markup[mark].uncoveredLines.length;
34
- report.summary[mark].covered =
35
- report.summary[mark].covered + file.markup[mark].coveredLines.length;
36
- report.summary[mark].uncovered =
37
- report.summary[mark].uncovered +
38
- file.markup[mark].uncoveredLines.length;
39
- fileJson.covered =
40
- fileJson.covered + file.markup[mark].coveredLines.length;
41
- fileJson.uncovered =
42
- fileJson.uncovered + file.markup[mark].uncoveredLines.length;
43
- fileJson[mark] = {
44
- covered: file.markup[mark].coveredLines.length,
45
- uncovered: file.markup[mark].uncoveredLines.length,
46
- uncoveredMatches: file.markup[mark].uncoveredMatches,
47
- };
48
50
  });
49
51
  report.files.push(fileJson);
50
52
  });
@@ -599,10 +599,12 @@ function transformMatches(fileMarkupObject) {
599
599
  matches = [];
600
600
  // Load array with uncovered matches
601
601
  Object.keys(fileMarkupObject).forEach((mark) => {
602
- fileMarkupObject[mark].uncoveredMatches.forEach((match) => {
603
- match.type = mark;
604
- matches.push(match);
605
- });
602
+ if (fileMarkupObject[mark].includeInSuggestions) {
603
+ fileMarkupObject[mark].uncoveredMatches.forEach((match) => {
604
+ match.type = mark;
605
+ matches.push(match);
606
+ });
607
+ }
606
608
  });
607
609
  // Sort matches by line, then index
608
610
  matches.sort((a, b) => a.line - b.line || a.indexInFile - b.indexInFile);
@@ -1,4 +1,4 @@
1
- const { setEnvs } = require("../utils");
1
+ const { setEnvs, loadEnvs } = require("../utils");
2
2
 
3
3
  exports.goTo = goTo;
4
4
 
@@ -17,14 +17,8 @@ async function goTo(action, page) {
17
17
  let result = await setEnvs(action.env);
18
18
  if (result.status === "FAIL") return { result };
19
19
  }
20
- if (
21
- action.uri[0] === "$" &&
22
- process.env[action.uri.substring(1)] != undefined
23
- ) {
24
- uri = process.env[action.uri.substring(1)];
25
- } else {
26
- uri = action.uri;
27
- }
20
+ uri = loadEnvs(action.uri);
21
+
28
22
  // Catch common formatting errors
29
23
  if (!uri.includes("://")) uri = "https://" + uri;
30
24
  // Run action
@@ -1,9 +1,11 @@
1
1
  const axios = require("axios");
2
- const { setEnvs, loadEnvsForObject } = require("../utils");
2
+ const jq = require("node-jq");
3
+ const { exit } = require("process");
4
+ const { setEnvs, loadEnvs, log } = require("../utils");
3
5
 
4
6
  exports.httpRequest = httpRequest;
5
7
 
6
- async function httpRequest(action) {
8
+ async function httpRequest(action, config) {
7
9
  const methods = ["get", "post", "put", "patch", "delete"];
8
10
 
9
11
  let status;
@@ -23,6 +25,7 @@ async function httpRequest(action) {
23
25
  responseHeaders: {},
24
26
  responseData: {},
25
27
  statusCodes: ["200"],
28
+ envsFromResponseData: [],
26
29
  };
27
30
 
28
31
  // Load environment variables
@@ -33,11 +36,8 @@ async function httpRequest(action) {
33
36
 
34
37
  // URI
35
38
  //// Define
36
- if (action.uri[0] === "$") {
37
- uri = process.env[action.uri.substring(1)];
38
- } else {
39
- uri = action.uri || defaultPayload.uri;
40
- }
39
+ uri = loadEnvs(action.uri) || defaultPayload.uri;
40
+
41
41
  //// Validate
42
42
  if (!uri || typeof uri != "string") {
43
43
  //Fail
@@ -50,11 +50,8 @@ async function httpRequest(action) {
50
50
 
51
51
  // Method
52
52
  //// Define
53
- if (action.method && action.method[0] === "$") {
54
- method = process.env[action.method.substring(1)];
55
- } else {
56
- method = action.method || defaultPayload.method;
57
- }
53
+ method = loadEnvs(action.method) || defaultPayload.method;
54
+
58
55
  //// Sanitize
59
56
  method = method.toLowerCase();
60
57
  //// Validate
@@ -71,16 +68,10 @@ async function httpRequest(action) {
71
68
  // Headers
72
69
  if (action.headers && JSON.stringify(action.headers) != "{}") {
73
70
  //// Define
74
- if (action.headers[0] === "$") {
75
- headers = process.env[action.headers.substring(1)];
76
- try {
77
- headers = JSON.parse(headers);
78
- } catch {}
79
- } else {
80
- headers = action.headers || defaultPayload.headers;
81
- }
71
+ headers = loadEnvs(action.headers) || defaultPayload.headers;
72
+
82
73
  //// Load environment variables
83
- headers = loadEnvsForObject(headers);
74
+ headers = loadEnvs(headers);
84
75
  //// Validate
85
76
  //// Set request
86
77
  if (JSON.stringify(headers) != "{}") request.headers = headers;
@@ -89,16 +80,10 @@ async function httpRequest(action) {
89
80
  // Params
90
81
  if (action.params && JSON.stringify(action.params) != "{}") {
91
82
  //// Define
92
- if (action.params[0] === "$") {
93
- params = process.env[action.params.substring(1)];
94
- try {
95
- params = JSON.parse(params);
96
- } catch {}
97
- } else {
98
- params = action.params || defaultPayload.params;
99
- }
83
+ params = loadEnvs(action.params) || defaultPayload.params;
84
+
100
85
  //// Load environment variables
101
- params = loadEnvsForObject(params);
86
+ params = loadEnvs(params);
102
87
  //// Validate
103
88
  //// Set request
104
89
  if (params != {}) request.params = params;
@@ -107,16 +92,10 @@ async function httpRequest(action) {
107
92
  // requestData
108
93
  if (action.requestData) {
109
94
  //// Define
110
- if (action.requestData[0] === "$") {
111
- requestData = process.env[action.requestData.substring(1)];
112
- try {
113
- requestData = JSON.parse(requestData);
114
- } catch {}
115
- } else {
116
- requestData = action.requestData || defaultPayload.requestData;
117
- }
95
+ requestData = loadEnvs(action.requestData) || defaultPayload.requestData;
96
+
118
97
  //// Load environment variables
119
- requestData = loadEnvsForObject(requestData);
98
+ requestData = loadEnvs(requestData);
120
99
  //// Validate
121
100
  //// Set request
122
101
  if (requestData != {}) request.data = requestData;
@@ -124,30 +103,19 @@ async function httpRequest(action) {
124
103
 
125
104
  // responseData
126
105
  //// Define
127
- if (action.responseData && action.responseData[0] === "$") {
128
- responseData = process.env[action.responseData.substring(1)];
129
- try {
130
- responseData = JSON.parse(responseData);
131
- } catch {}
132
- } else {
133
- responseData = action.responseData || defaultPayload.responseData;
134
- }
106
+ responseData = loadEnvs(action.responseData) || defaultPayload.responseData;
107
+
135
108
  //// Load environment variables
136
- responseData = loadEnvsForObject(responseData);
109
+ responseData = loadEnvs(responseData);
137
110
  //// Validate
138
111
 
139
112
  // responseHeaders
140
113
  //// Define
141
- if (action.responseHeaders && action.responseHeaders[0] === "$") {
142
- responseHeaders = process.env[action.responseHeaders.substring(1)];
143
- try {
144
- responseHeaders = JSON.parse(responseHeaders);
145
- } catch {}
146
- } else {
147
- responseHeaders = action.responseHeaders || defaultPayload.responseHeaders;
148
- }
114
+ responseHeaders =
115
+ loadEnvs(action.responseHeaders) || defaultPayload.responseHeaders;
116
+
149
117
  //// Load environment variables
150
- responseHeaders = loadEnvsForObject(responseHeaders);
118
+ responseHeaders = loadEnvs(responseHeaders);
151
119
  //// Validate
152
120
 
153
121
  // Status codes
@@ -161,6 +129,34 @@ async function httpRequest(action) {
161
129
  //// Validate
162
130
  if (statusCodes === []) statusCodes = defaultPayload.statusCodes;
163
131
 
132
+ // Envs from response data
133
+ //// Define
134
+ envsFromResponseData =
135
+ action.envsFromResponseData || defaultPayload.envsFromResponseData;
136
+ //// Sanitize
137
+ for (i = 0; i < envsFromResponseData.length; i++) {
138
+ if (typeof statusCodes[i] === "string")
139
+ statusCodes[i] = Number(statusCodes[i]);
140
+ }
141
+ //// Validate
142
+ let validEnvs = envsFromResponseData.every(
143
+ (env) =>
144
+ typeof env === "object" &&
145
+ env.name.match(/^[a-zA-Z0-9_]+$/gm) &&
146
+ typeof env.jqFilter === "string" &&
147
+ env.jqFilter.length > 1
148
+ );
149
+ if (!validEnvs) {
150
+ envsFromResponseData = [];
151
+ log(
152
+ config,
153
+ "warning",
154
+ "Not setting environment variables. One or more invalid variable definitions."
155
+ );
156
+ }
157
+ if (envsFromResponseData === [])
158
+ envsFromResponseData = defaultPayload.envsFromResponseData;
159
+
164
160
  // Send request
165
161
  response = await axios(request)
166
162
  .then((response) => {
@@ -211,12 +207,30 @@ async function httpRequest(action) {
211
207
  description =
212
208
  description +
213
209
  ` Expected response headers were present in actual response headers.`;
214
- } else {
215
210
  status = "FAIL";
211
+ } else {
216
212
  description = description + " " + dataComparison.result.description;
217
213
  }
218
214
  }
219
215
 
216
+ // Set environment variables from response data
217
+ for (const variable of envsFromResponseData) {
218
+ let value = await jq.run(variable.jqFilter, response.data, {
219
+ input: "json",
220
+ output: "compact",
221
+ });
222
+ if (value) {
223
+ process.env[variable.name] = value;
224
+ description =
225
+ description + ` Set '$${variable.name}' environment variable.`;
226
+ } else {
227
+ if (status != "FAIL") status = "WARNING";
228
+ description =
229
+ description +
230
+ ` Couldn't set '${variable.name}' environment variable. The jq filter (${variable.jqFilter}) returned a null result.`;
231
+ }
232
+ }
233
+
220
234
  description = description.trim();
221
235
  result = { status, description };
222
236
  return { result };
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, timestamp } = require("./utils");
5
+ const { setEnvs, log, timestamp, loadEnvs } = require("./utils");
6
6
  const util = require("util");
7
7
  const exec = util.promisify(require("child_process").exec);
8
8
  const axios = require("axios");
@@ -320,7 +320,7 @@ async function runAction(config, action, page, videoDetails) {
320
320
  result = await checkLink(action);
321
321
  break;
322
322
  case "httpRequest":
323
- result = await httpRequest(action);
323
+ result = await httpRequest(action, config);
324
324
  break;
325
325
  }
326
326
  return result;
@@ -337,14 +337,7 @@ async function checkLink(action) {
337
337
  let result = await setEnvs(action.env);
338
338
  if (result.status === "FAIL") return { result };
339
339
  }
340
- if (
341
- action.uri[0] === "$" &&
342
- process.env[action.uri.substring(1)] != undefined
343
- ) {
344
- uri = process.env[action.uri.substring(1)];
345
- } else {
346
- uri = action.uri;
347
- }
340
+ uri = loadEnvs(action.uri);
348
341
 
349
342
  // Validate protocol
350
343
  if (uri.indexOf("://") < 0) {
@@ -393,15 +386,20 @@ async function runShell(action) {
393
386
  let description;
394
387
  let result;
395
388
  let exitCode;
389
+ let command;
396
390
 
397
- // Load environment variables
391
+ // Set environment variables
398
392
  if (action.env) {
399
393
  let result = await setEnvs(action.env);
400
394
  if (result.status === "FAIL") return { result };
401
395
  }
402
396
 
397
+ // Command
398
+ //// Load envs
399
+ command = loadEnvs(action.command);
400
+
403
401
  // Promisify and execute command
404
- const promise = exec(action.command);
402
+ const promise = exec(command);
405
403
  const child = promise.child;
406
404
  child.on("close", function (code) {
407
405
  exitCode = code;
@@ -474,15 +472,9 @@ async function typeElement(action, elementHandle) {
474
472
  }
475
473
  // Type keys
476
474
  if (action.keys) {
477
- // Detect if using an environment variable and sub in value
478
- if (
479
- action.keys[0] === "$" &&
480
- process.env[action.keys.substring(1)] != undefined
481
- ) {
482
- keys = process.env[action.keys.substring(1)];
483
- } else {
484
- keys = action.keys;
485
- }
475
+ // Resolve environment variables in keys
476
+ keys = loadEnvs(action.keys);
477
+
486
478
  try {
487
479
  await elementHandle.type(keys);
488
480
  } catch {
@@ -547,14 +539,8 @@ async function matchText(action, page) {
547
539
  if (result.status === "FAIL") return { result };
548
540
  }
549
541
  // Set text
550
- if (
551
- action.text[0] === "$" &&
552
- process.env[action.text.substring(1)] != undefined
553
- ) {
554
- text = process.env[action.text.substring(1)];
555
- } else {
556
- text = action.text;
557
- }
542
+ text = loadEnvs(action.text);
543
+
558
544
  let elementTag = await page.$eval(action.css, (element) =>
559
545
  element.tagName.toLowerCase()
560
546
  );
package/src/lib/utils.js CHANGED
@@ -17,6 +17,7 @@ exports.setEnvs = setEnvs;
17
17
  exports.loadEnvsForObject = loadEnvsForObject;
18
18
  exports.log = log;
19
19
  exports.timestamp = timestamp;
20
+ exports.loadEnvs = loadEnvs;
20
21
 
21
22
  const analyticsRequest =
22
23
  "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";
@@ -1014,23 +1015,60 @@ async function log(config, level, message) {
1014
1015
  }
1015
1016
  }
1016
1017
 
1017
- function loadEnvsForObject(object) {
1018
- Object.keys(object).forEach((key) => {
1018
+ function loadEnvs(stringOrObject) {
1019
+ if (!stringOrObject) return stringOrObject;
1020
+ // Try to convert string to object
1021
+ try {
1019
1022
  if (
1020
- object[key][0] === "$" &&
1021
- process.env[object[key].substring(1)] != undefined
1023
+ typeof stringOrObject === "string" &&
1024
+ typeof JSON.parse(stringOrObject) === "object"
1022
1025
  ) {
1023
- object[key] = process.env[object[key].substring(1)];
1026
+ stringOrObject = JSON.parse(stringOrObject);
1024
1027
  }
1025
- try {
1026
- if (typeof JSON.parse(object[key]) === "object") {
1027
- object[key] = JSON.parse(object[key]);
1028
- }
1029
- } catch {}
1030
- if (typeof object[key] === "object") {
1031
- object[key] = loadEnvsForObject(object[key]);
1028
+ } catch {}
1029
+ if (typeof stringOrObject === "object") {
1030
+ // Load for object
1031
+ stringOrObject = loadEnvsForObject(stringOrObject);
1032
+ } else {
1033
+ // Load for string
1034
+ stringOrObject = loadEnvsForString(stringOrObject);
1035
+ }
1036
+ // Try to convert resolved string to object
1037
+ try {
1038
+ if (typeof JSON.parse(stringOrObject) === "object") {
1039
+ stringOrObject = JSON.parse(stringOrObject);
1040
+ }
1041
+ } catch {}
1042
+ return stringOrObject;
1043
+ }
1044
+
1045
+ function loadEnvsForString(string) {
1046
+ // Find all variables
1047
+ variableRegex = new RegExp(/\$[a-zA-Z0-9_]+/, "g");
1048
+ matches = string.match(variableRegex);
1049
+ // If no matches, return
1050
+ if (!matches) return string;
1051
+ // Iterate matches
1052
+ matches.forEach((match) => {
1053
+ // Check if is declared variable
1054
+ value = process.env[match.substring(1)];
1055
+ if (value) {
1056
+ // If variable value might have a nested variable, recurse to try to resolve
1057
+ if (value.includes("$")) value = loadEnvs(value);
1058
+ // Convert to string in case value was a substring of the greater string
1059
+ if (typeof value === "object") value = JSON.stringify(value);
1060
+ // Replace match with variable value
1061
+ string = string.replace(match, value);
1032
1062
  }
1033
1063
  });
1064
+ return string;
1065
+ }
1066
+
1067
+ function loadEnvsForObject(object) {
1068
+ Object.keys(object).forEach((key) => {
1069
+ // Resolve all variables in key value
1070
+ object[key] = loadEnvs(object[key]);
1071
+ });
1034
1072
  return object;
1035
1073
  }
1036
1074
 
Binary file
Binary file