doc-detective 3.0.2 → 3.0.4

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
@@ -69,21 +69,23 @@ You can find test and config samples in the [samples](https://github.com/doc-det
69
69
 
70
70
  ## Concepts
71
71
 
72
- - [**Test specification**](https://doc-detective.com/docs/references/schemas/specification.html): A group of tests to run in one or more contexts. Conceptually parallel to a document.
73
- - [**Test**](https://doc-detective.com/docs/references/schemas/test.html): A sequence of steps to perform. Conceptually parallel to a procedure.
72
+ - **Test specification**: A group of tests to run in one or more contexts. Conceptually parallel to a document.
73
+ - [**Test**](https://doc-detective.com/docs/get-started/tests): A sequence of steps to perform. Conceptually parallel to a procedure.
74
74
  - **Step**: A portion of a test that includes a single action. Conceptually parallel to a step in a procedure.
75
75
  - **Action**: The task a performed in a step. Doc Detective supports a variety of actions:
76
- - [**checkLink**](https://doc-detective.com/docs/references/schemas/checkLink.html): Check if a URL returns an acceptable status code from a GET request.
77
- - [**find**](https://doc-detective.com/docs/references/schemas/find.html): Check if an element exists with the specified selector.
78
- - [**goTo**](https://doc-detective.com/docs/references/schemas/goTo.html): Navigate to a specified URL.
79
- - [**httpRequest**](https://doc-detective.com/docs/references/schemas/httpRequest.html): Perform a generic HTTP request, for example to an API.
80
- - [**runShell**](https://doc-detective.com/docs/references/schemas/runShell.html): Perform a native shell command.
81
- - [**saveScreenshot**](https://doc-detective.com/docs/references/schemas/saveScreenshot.html): Take a screenshot in PNG format.
82
- - [**setVariables**](https://doc-detective.com/docs/references/schemas/setVariables.html): Load environment variables from a `.env` file.
83
- - [**startRecording**](https://doc-detective.com/docs/references/schemas/startRecording.html) and [**stopRecording**](https://doc-detective.com/docs/references/schemas/stopRecording.html): Capture a video of test execution.
84
- - [**typeKeys**](https://doc-detective.com/docs/references/schemas/typeKeys.html): Type keys. To type special keys, begin and end the string with `$` and use the special key’s enum. For example, to type the Escape key, enter `$ESCAPE$`.
85
- - [**wait**](https://doc-detective.com/docs/references/schemas/wait.html): Pause before performing the next action.
86
- - [**Context**](https://doc-detective.com/docs/references/schemas/context.html): An application and platforms that support the tests.
76
+ - [**checkLink**](https://doc-detective.com/docs/get-started/actions/checkLink): Check if a URL returns an acceptable status code from a GET request.
77
+ - [**click**](https://doc-detective.com/docs/get-started/actions/click): Click an element with the specified text or selector.
78
+ - [**find**](https://doc-detective.com/docs/get-started/actions/find): Check if an element exists with the specified text or selector and optionally interact with it.
79
+ - [**goTo**](https://doc-detective.com/docs/get-started/actions/goTo): Navigate to a specified URL.
80
+ - [**httpRequest**](https://doc-detective.com/docs/get-started/actions/httpRequest): Perform a generic HTTP request, for example to an API.
81
+ - [**runCode**](https://doc-detective.com/docs/get-started/actions/runCode): Execute code, such as how it appears in a code block.
82
+ - [**runShell**](https://doc-detective.com/docs/get-started/actions/runShell): Perform a native shell command.
83
+ - [**screenshot**](https://doc-detective.com/docs/get-started/actions/screenshot): Take a screenshot in PNG format.
84
+ - [**loadVariables**](https://doc-detective.com/docs/get-started/actions/loadVariables): Load environment variables from a `.env` file.
85
+ - [**record**](https://doc-detective.com/docs/get-started/actions/record) and [**stopRecord**](https://doc-detective.com/docs/get-started/actions/stopRecord): Capture a video of test execution.
86
+ - [**type**](https://doc-detective.com/docs/get-started/actions/type): Type keys. To type special keys, begin and end the string with `$` and use the special key’s enum. For example, to type the Escape key, enter `$ESCAPE$`.
87
+ - [**wait**](https://doc-detective.com/docs/get-started/actions/wait): Pause before performing the next action.
88
+ - [**Context**](https://doc-detective.com/docs/get-started/config/contexts): A combination of platform and application to run tests on.
87
89
 
88
90
  ## Develop
89
91
 
@@ -105,4 +107,4 @@ Looking to help out? See our [contributions guide](CONTRIBUTIONS.md) for more in
105
107
 
106
108
  ## License
107
109
 
108
- This project uses the [MIT license](https://github.com/doc-detective/doc-detective/blob/master/LICENSE).
110
+ This project uses the [AGPL-3.0 license](https://github.com/doc-detective/doc-detective/blob/master/LICENSE).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doc-detective",
3
- "version": "3.0.2",
3
+ "version": "3.0.4",
4
4
  "description": "Treat doc content as testable assertions to validate doc accuracy and product UX.",
5
5
  "bin": {
6
6
  "doc-detective": "src/index.js"
@@ -33,8 +33,8 @@
33
33
  "homepage": "https://github.com/doc-detective/doc-detective#readme",
34
34
  "dependencies": {
35
35
  "@ffmpeg-installer/ffmpeg": "^1.1.0",
36
- "doc-detective-common": "^3.0.2",
37
- "doc-detective-core": "^3.0.4",
36
+ "doc-detective-common": "^3.0.3",
37
+ "doc-detective-core": "^3.0.5",
38
38
  "yargs": "^17.7.2"
39
39
  },
40
40
  "devDependencies": {
@@ -1,83 +1,94 @@
1
1
  {
2
- "envVariables": "./variables.env",
3
- "defaultCommand": "runTests",
4
- "input": ".",
5
- "output": ".",
6
- "recursive": true,
7
- "logLevel": "info",
8
- "runTests": {
9
- "detectSteps": true,
10
- "input": "./doc-content-inline-tests.md",
11
- "output": ".",
12
- "setup": "",
13
- "cleanup": "",
14
- "recursive": true,
15
- "mediaDirectory": ".",
16
- "downloadDirectory": ".",
17
- "contexts": [
18
- {
19
- "app": {
20
- "name": "chrome",
21
- "options": {
22
- "headless": false
23
- }
24
- },
25
- "platforms": ["windows", "mac", "linux"]
26
- }
27
- ]
28
- },
29
- "runCoverage": {
30
- "recursive": true,
31
- "input": ".",
32
- "output": ".",
33
- "markup": []
34
- },
2
+ "runOn": [
3
+ {
4
+ "platforms": ["windows", "mac", "linux"],
5
+ "browsers": [
6
+ {
7
+ "name": "firefox",
8
+ "headless": false
9
+ }
10
+ ]
11
+ }
12
+ ],
35
13
  "fileTypes": [
36
14
  {
37
- "name": "Markdown",
38
- "extensions": [".md"],
39
- "testStartStatementOpen": "[comment]: # (test start",
40
- "testStartStatementClose": ")",
41
- "testIgnoreStatement": "[comment]: # (test ignore)",
42
- "testEndStatement": "[comment]: # (test end)",
43
- "stepStatementOpen": "[comment]: # (step",
44
- "stepStatementClose": ")",
15
+ "name": "markdown",
16
+ "extensions": ["md", "markdown", "mdx"],
17
+ "inlineStatements": {
18
+ "testStart": [
19
+ "{\\/\\*\\s*test\\s+?([\\s\\S]*?)\\s*\\*\\/}",
20
+ "<!--\\s*test\\s*([\\s\\S]*?)\\s*-->",
21
+ "\\[comment\\]:\\s+#\\s+\\(test\\s*(.*?)\\s*\\)",
22
+ "\\[comment\\]:\\s+#\\s+\\(test start\\s*(.*?)\\s*\\)"
23
+ ],
24
+ "testEnd": [
25
+ "{\\/\\*\\s*test end\\s*\\*\\/}",
26
+ "<!--\\s*test end\\s*([\\s\\S]*?)\\s*-->",
27
+ "\\[comment\\]:\\s+#\\s+\\(test end\\)"
28
+ ],
29
+ "ignoreStart": [
30
+ "{\\/\\*\\s*test ignore start\\s*\\*\\/}",
31
+ "<!--\\s*test ignore start\\s*-->"
32
+ ],
33
+ "ignoreEnd": [
34
+ "{\\/\\*\\s*test ignore end\\s*\\*\\/}",
35
+ "<!--\\s*test ignore end\\s*-->"
36
+ ],
37
+ "step": [
38
+ "{\\/\\*\\s*step\\s+?([\\s\\S]*?)\\s*\\*\\/}",
39
+ "<!--\\s*step\\s*([\\s\\S]*?)\\s*-->",
40
+ "\\[comment\\]:\\s+#\\s+\\(step\\s*(.*?)\\s*\\)"
41
+ ]
42
+ },
45
43
  "markup": [
46
44
  {
47
- "name": "Hyperlink",
48
- "regex": ["(?<!!)\\[.+?\\]\\((.+?)\\)"],
45
+ "name": "checkHyperlink",
46
+ "regex": [
47
+ "(?<!\\!)\\[[^\\]]+\\]\\(\\s*(https?:\\/\\/[^\\s)]+)(?:\\s+\"[^\"]*\")?\\s*\\)"
48
+ ],
49
49
  "actions": ["checkLink"]
50
50
  },
51
51
  {
52
- "name": "Navigation link",
52
+ "name": "pressEnter",
53
+ "regex": ["\\bpress Enter"],
54
+ "actions": [
55
+ {
56
+ "type": "$ENTER$"
57
+ }
58
+ ]
59
+ },
60
+ {
61
+ "name": "clickOnscreenText",
53
62
  "regex": [
54
- "(?:[Cc]hose|[Oo]pen|[Cc]lick|[Nn]avigate to|[Gg]o to)\\s+(?<!!)\\[.+?\\]\\((.+?)\\)"
63
+ "\\b(?:[Cc]lick|[Tt]ap|[Ll]eft-click|[Cc]hoose|[Ss]elect|[Cc]heck)\\b\\s+\\*\\*((?:(?!\\*\\*).)+)\\*\\*"
55
64
  ],
56
- "actions": ["goTo"]
65
+ "actions": ["click"]
57
66
  },
58
67
  {
59
- "name": "Onscreen text",
60
- "regex": ["\\*\\*(.+?)\\*\\*"],
68
+ "name": "findOnscreenText",
69
+ "regex": ["\\*\\*((?:(?!\\*\\*).)+)\\*\\*"],
61
70
  "actions": ["find"]
62
71
  },
63
72
  {
64
- "name": "Image",
65
- "regex": ["!\\[.+?\\]\\((.+?)\\)"],
66
- "actions": [
67
- {
68
- "id": "reference",
69
- "action": "saveScreenshot",
70
- "maxVariation": 5,
71
- "overwrite": "byVariation"
72
- }
73
- ]
73
+ "name": "goToUrl",
74
+ "regex": [
75
+ "\\b(?:[Gg]o\\s+to|[Oo]pen|[Nn]avigate\\s+to|[Vv]isit|[Aa]ccess|[Pp]roceed\\s+to|[Ll]aunch)\\b\\s+\\[[^\\]]+\\]\\(\\s*(https?:\\/\\/[^\\s)]+)(?:\\s+\"[^\"]*\")?\\s*\\)"
76
+ ],
77
+ "actions": ["goTo"]
78
+ },
79
+ {
80
+ "name": "screenshotImage",
81
+ "regex": [
82
+ "!\\[[^\\]]*\\]\\(\\s*([^\\s)]+)(?:\\s+\"[^\"]*\")?\\s*\\)\\s*\\{(?=[^}]*\\.screenshot)[^}]*\\}"
83
+ ],
84
+ "actions": ["screenshot"]
85
+ },
86
+ {
87
+ "name": "typeText",
88
+ "regex": ["\\b(?:press|enter|type)\\b\\s+\"([^\"]+)\""],
89
+ "actions": ["type"]
74
90
  }
75
91
  ]
76
92
  }
77
- ],
78
- "integrations": {},
79
- "telemetry": {
80
- "send": true,
81
- "userId": "Doc Detective Samples"
82
- }
93
+ ]
83
94
  }
@@ -3,9 +3,8 @@
3
3
  [Doc Detective documentation](https://doc-detective.com) is split into a few key sections:
4
4
 
5
5
  - The landing page discusses what Doc Detective is, what it does, and who might find it useful.
6
-
7
6
  - [Get started](https://doc-detective.com/docs/get-started/intro) covers how to quickly get up and running with Doc Detective.
8
7
 
9
- - The [references](https://doc-detective.com/docs/category/schemas) detail the various JSON objects that Doc Detective expects for [configs](https://doc-detective.com/docs/references/schemas/config.html), [test specifications](https://doc-detective.com/docs/references/schemas/specification.html), [tests](https://doc-detective.com/docs/references/schemas/test), actions, and more. Open [typeKeys](https://doc-detective.com/docs/references/schemas/typeKeys.html)--or any other schema--and you'll find two sections: **Fields** and **Examples**.
8
+ Some pages also have unique headings. If you open [type](https://doc-detective.com/docs/get-started/actions/type), it has a **Special keys** heading.
10
9
 
11
- ![Search results.](reference.png)
10
+ ![Search results.](reference.png){ .screenshot }
@@ -1,28 +1,23 @@
1
1
  # Doc Detective documentation overview
2
2
 
3
- [comment]: # (test start {"id":"doc-detective-docs", "detectSteps": false})
3
+ <!-- test
4
+ testId: doc-detective-docs
5
+ detectSteps: false
6
+ -->
4
7
 
5
- [Doc Detective documentation](http://doc-detective.com) is split into a few key sections:
8
+ [Doc Detective documentation](https://doc-detective.com) is split into a few key sections:
6
9
 
7
- [comment]: # (step {"action":"checkLink", "url":"https://doc-detective.com"})
10
+ <!-- step checkLink: "https://doc-detective.com" -->
8
11
 
9
12
  - The landing page discusses what Doc Detective is, what it does, and who might find it useful.
10
-
11
13
  - [Get started](https://doc-detective.com/docs/get-started/intro) covers how to quickly get up and running with Doc Detective.
12
14
 
13
- [comment]: # (step {"action":"checkLink", "url":"https://doc-detective.com/docs/get-started/intro"})
14
-
15
- - The [references](https://doc-detective.com/docs/category/schemas) detail the various JSON objects that Doc Detective expects for [configs](https://doc-detective.com/docs/references/schemas/config.html), [test specifications](https://doc-detective.com/docs/references/schemas/specification.html), [tests](https://doc-detective.com/docs/references/schemas/test), actions, and more. Open [typeKeys](https://doc-detective.com/docs/references/schemas/typeKeys.html)--or any other schema--and you'll find two sections: **Fields** and **Examples**.
15
+ <!-- step checkLink: "https://doc-detective.com/docs/get-started/intro" -->
16
16
 
17
- [comment]: # (step {"action":"checkLink", "url":"https://doc-detective.com/docs/category/schemas"})
18
- [comment]: # (step {"action":"checkLink", "url":"https://doc-detective.com/docs/references/schemas/config.html"})
19
- [comment]: # (step {"action":"checkLink", "url":"https://doc-detective.com/docs/references/schemas/specification.html"})
20
- [comment]: # (step {"action":"checkLink", "url":"https://doc-detective.com/docs/references/schemas/test.html"})
21
- [comment]: # (step {"action":"goTo", "url":"https://doc-detective.com/docs/references/schemas/typeKeys.html"})
22
- [comment]: # (step {"action":"find", "selector":"h2#fields", "matchText":"Fields"})
23
- [comment]: # (step {"action":"find", "selector":"h2#examples", "matchText":"Examples"})
17
+ Some pages also have unique headings. If you open [type](https://doc-detective.com/docs/get-started/actions/type) it has **Special keys**.
24
18
 
25
- ![Search results.](reference.png)
19
+ <!-- step goTo: "https://doc-detective.com/docs/get-started/actions/type" -->
20
+ <!-- step find: Special keys -->
26
21
 
27
- [comment]: # (step {"action":"saveScreenshot", "path":"reference.png", "maxVariation":5, "overwrite":"byVariation"})
28
- [comment]: # (test end)
22
+ ![Search results.](reference.png){ .screenshot }
23
+ <!-- step screenshot: reference.png -->
@@ -3,10 +3,11 @@
3
3
  {
4
4
  "steps": [
5
5
  {
6
- "action": "runShell",
7
6
  "description": "Run a Docker container and check the output.",
8
- "command": "docker run hello-world",
9
- "output": "Hello from Docker!"
7
+ "runShell": {
8
+ "command": "docker run hello-world",
9
+ "stdio": "Hello from Docker!"
10
+ }
10
11
  }
11
12
  ]
12
13
  }
package/samples/env ADDED
@@ -0,0 +1,3 @@
1
+ USER="John Doe"
2
+ JOB="Software Engineer"
3
+ SECRET="YOUR_SECRET_KEY"
@@ -0,0 +1,37 @@
1
+ tests:
2
+ - steps:
3
+ - loadVariables: env
4
+ - httpRequest:
5
+ url: http://localhost:8080/api/users
6
+ method: post
7
+ request:
8
+ body:
9
+ name: $USER
10
+ job: $JOB
11
+ response:
12
+ body:
13
+ name: John Doe
14
+ job: Software Engineer
15
+ - httpRequest:
16
+ url: http://localhost:8080/api/users
17
+ method: post
18
+ request:
19
+ body:
20
+ data:
21
+ - first_name: George
22
+ last_name: Bluth
23
+ id: 1
24
+ response:
25
+ body:
26
+ data:
27
+ - first_name: George
28
+ last_name: Bluth
29
+ variables:
30
+ ID: $$response.body.data[0].id
31
+ - httpRequest:
32
+ url: http://localhost:8080/api/$ID
33
+ method: get
34
+ timeout: 1000
35
+ savePath: response.json
36
+ maxVariation: 0
37
+ overwrite: aboveVariation
@@ -0,0 +1,8 @@
1
+ To search for American Shorthair kittens,
2
+
3
+ 1. Go to [DuckDuckGo](https://www.duckduckgo.com).
4
+ 2. In the search bar, enter "American Shorthair kittens", then press Enter.
5
+
6
+ <!-- step wait: 10000 -->
7
+
8
+ !["Search results for kittens"](search-results.png){ .screenshot }
@@ -0,0 +1,6 @@
1
+ # Sample Local GUI
2
+
3
+ 1. Open [the GUI](http://localhost:8080).
4
+ 2. Find **Selection Elements**, and click **Option 1**.
5
+
6
+ !["Believe me now?"](proof.png){ .screenshot }
package/src/index.js CHANGED
@@ -19,7 +19,8 @@ async function main(argv) {
19
19
  );
20
20
  // Set args
21
21
  argv = setArgs(argv);
22
- // Get .doc-detective JSON or YAML config, if it exists
22
+
23
+ // Get .doc-detective JSON or YAML config, if it exists, preferring a config arg if provided
23
24
  const configPathJSON = path.resolve(process.cwd(), ".doc-detective.json");
24
25
  const configPathYAML = path.resolve(process.cwd(), ".doc-detective.yaml");
25
26
  const configPathYML = path.resolve(process.cwd(), ".doc-detective.yml");
@@ -32,13 +33,9 @@ async function main(argv) {
32
33
  : fs.existsSync(configPathYML)
33
34
  ? configPathYML
34
35
  : null;
35
- // If config file exists, read it
36
- let config = {};
37
- if (configPath) {
38
- config = await readFile({ fileURLOrPath: configPath });
39
- }
36
+
40
37
  // Set config
41
- config = await setConfig(config, argv, configPath || ".");
38
+ config = await setConfig({ configPath: configPath, args: argv });
42
39
 
43
40
  // Run tests
44
41
  const output = config.output;
package/src/utils.js CHANGED
@@ -46,7 +46,22 @@ function setArgs(args) {
46
46
  }
47
47
 
48
48
  // Override config values based on args and validate the config
49
- async function setConfig(config, args, configPath) {
49
+ async function setConfig({ configPath, args }) {
50
+ if (args.config && !configPath) {
51
+ configPath = args.config;
52
+ }
53
+
54
+ // If config file exists, read it
55
+ let config = {};
56
+ if (configPath) {
57
+ try {
58
+ config = await readFile({ fileURLOrPath: configPath });
59
+ } catch (error) {
60
+ console.error(`Error reading config file at ${configPath}: ${error}`);
61
+ return null;
62
+ }
63
+ }
64
+
50
65
  // Validate config
51
66
  const validation = validate({
52
67
  schemaKey: "config_v3",
@@ -72,12 +87,22 @@ async function setConfig(config, args, configPath) {
72
87
  loadVariables: config.loadVariables || ".env",
73
88
  detectSteps: config.detectSteps || true,
74
89
  logLevel: config.logLevel || "info",
75
- fileTypes: config.fileTypes || ["markdown","asciidoc", "html"],
90
+ fileTypes: config.fileTypes || ["markdown", "asciidoc", "html"],
76
91
  telemetry: config.telemetry || { send: true },
77
- }
92
+ };
78
93
  // Override config values
79
94
  if (args.input) {
80
- config.input = path.resolve(args.input);
95
+ // If input includes commas, split it into an array
96
+ args.input = args.input.split(",").map((item) => item.trim());
97
+ // Resolve paths
98
+ args.input = args.input.map((item) => {
99
+ if (item.startsWith("https://") || item.startsWith("http://")) {
100
+ return item; // Don't resolve URLs
101
+ }
102
+ return path.resolve(item);
103
+ });
104
+ // Add to config
105
+ config.input = args.input;
81
106
  }
82
107
  if (args.output) {
83
108
  config.output = path.resolve(args.output);
@@ -89,7 +114,7 @@ async function setConfig(config, args, configPath) {
89
114
  config = await resolvePaths({
90
115
  config: config,
91
116
  object: config,
92
- filePath: configPath,
117
+ filePath: configPath || ".",
93
118
  nested: false,
94
119
  objectType: "config",
95
120
  });
@@ -161,7 +186,7 @@ const reporters = {
161
186
  yellow: "\x1b[33m",
162
187
  cyan: "\x1b[36m",
163
188
  reset: "\x1b[0m",
164
- bold: "\x1b[1m"
189
+ bold: "\x1b[1m",
165
190
  };
166
191
 
167
192
  // Check if we have the new results format with summary
@@ -174,71 +199,112 @@ const reporters = {
174
199
  if (results.summary) {
175
200
  // Extract summary data
176
201
  const { specs, tests, contexts, steps } = results.summary;
177
-
202
+
178
203
  // Calculate totals
179
- const totalSpecs = specs ? specs.pass + specs.fail + specs.warning + specs.skipped : 0;
180
- const totalTests = tests ? tests.pass + tests.fail + tests.warning + tests.skipped : 0;
181
- const totalContexts = contexts ? contexts.pass + contexts.fail + contexts.warning + contexts.skipped : 0;
182
- const totalSteps = steps ? steps.pass + steps.fail + steps.warning + steps.skipped : 0;
183
-
204
+ const totalSpecs = specs
205
+ ? specs.pass + specs.fail + specs.warning + specs.skipped
206
+ : 0;
207
+ const totalTests = tests
208
+ ? tests.pass + tests.fail + tests.warning + tests.skipped
209
+ : 0;
210
+ const totalContexts = contexts
211
+ ? contexts.pass + contexts.fail + contexts.warning + contexts.skipped
212
+ : 0;
213
+ const totalSteps = steps
214
+ ? steps.pass + steps.fail + steps.warning + steps.skipped
215
+ : 0;
216
+
184
217
  // Any failures overall?
185
- const hasFailures = (specs && specs.fail > 0) ||
186
- (tests && tests.fail > 0) ||
187
- (contexts && contexts.fail > 0) ||
188
- (steps && steps.fail > 0);
218
+ const hasFailures =
219
+ (specs && specs.fail > 0) ||
220
+ (tests && tests.fail > 0) ||
221
+ (contexts && contexts.fail > 0) ||
222
+ (steps && steps.fail > 0);
223
+
224
+ console.log(
225
+ `\n${colors.bold}===== Doc Detective Results Summary =====${colors.reset}`
226
+ );
189
227
 
190
- console.log(`\n${colors.bold}===== Doc Detective Results Summary =====${colors.reset}`);
191
-
192
228
  // Print specs summary if available
193
229
  if (specs) {
194
230
  console.log(`\n${colors.bold}Specs:${colors.reset}`);
195
231
  console.log(`Total: ${totalSpecs}`);
196
232
  console.log(`${colors.green}Passed: ${specs.pass}${colors.reset}`);
197
- console.log(`${specs.fail > 0 ? colors.red : colors.green}Failed: ${specs.fail}${colors.reset}`);
198
- if (specs.warning > 0) console.log(`${colors.yellow}Warnings: ${specs.warning}${colors.reset}`);
233
+ console.log(
234
+ `${specs.fail > 0 ? colors.red : colors.green}Failed: ${specs.fail}${
235
+ colors.reset
236
+ }`
237
+ );
238
+ if (specs.warning > 0)
239
+ console.log(
240
+ `${colors.yellow}Warnings: ${specs.warning}${colors.reset}`
241
+ );
199
242
  if (specs.skipped > 0) console.log(`Skipped: ${specs.skipped}`);
200
243
  }
201
-
244
+
202
245
  // Print tests summary if available
203
246
  if (tests) {
204
247
  console.log(`\n${colors.bold}Tests:${colors.reset}`);
205
248
  console.log(`Total: ${totalTests}`);
206
249
  console.log(`${colors.green}Passed: ${tests.pass}${colors.reset}`);
207
- console.log(`${tests.fail > 0 ? colors.red : colors.green}Failed: ${tests.fail}${colors.reset}`);
208
- if (tests.warning > 0) console.log(`${colors.yellow}Warnings: ${tests.warning}${colors.reset}`);
250
+ console.log(
251
+ `${tests.fail > 0 ? colors.red : colors.green}Failed: ${tests.fail}${
252
+ colors.reset
253
+ }`
254
+ );
255
+ if (tests.warning > 0)
256
+ console.log(
257
+ `${colors.yellow}Warnings: ${tests.warning}${colors.reset}`
258
+ );
209
259
  if (tests.skipped > 0) console.log(`Skipped: ${tests.skipped}`);
210
260
  }
211
-
261
+
212
262
  // Print contexts summary if available
213
263
  if (contexts) {
214
264
  console.log(`\n${colors.bold}Contexts:${colors.reset}`);
215
265
  console.log(`Total: ${totalContexts}`);
216
266
  console.log(`${colors.green}Passed: ${contexts.pass}${colors.reset}`);
217
- console.log(`${contexts.fail > 0 ? colors.red : colors.green}Failed: ${contexts.fail}${colors.reset}`);
218
- if (contexts.warning > 0) console.log(`${colors.yellow}Warnings: ${contexts.warning}${colors.reset}`);
267
+ console.log(
268
+ `${contexts.fail > 0 ? colors.red : colors.green}Failed: ${
269
+ contexts.fail
270
+ }${colors.reset}`
271
+ );
272
+ if (contexts.warning > 0)
273
+ console.log(
274
+ `${colors.yellow}Warnings: ${contexts.warning}${colors.reset}`
275
+ );
219
276
  if (contexts.skipped > 0) console.log(`Skipped: ${contexts.skipped}`);
220
277
  }
221
-
278
+
222
279
  // Print steps summary if available
223
280
  if (steps) {
224
281
  console.log(`\n${colors.bold}Steps:${colors.reset}`);
225
282
  console.log(`Total: ${totalSteps}`);
226
283
  console.log(`${colors.green}Passed: ${steps.pass}${colors.reset}`);
227
- console.log(`${steps.fail > 0 ? colors.red : colors.green}Failed: ${steps.fail}${colors.reset}`);
228
- if (steps.warning > 0) console.log(`${colors.yellow}Warnings: ${steps.warning}${colors.reset}`);
284
+ console.log(
285
+ `${steps.fail > 0 ? colors.red : colors.green}Failed: ${steps.fail}${
286
+ colors.reset
287
+ }`
288
+ );
289
+ if (steps.warning > 0)
290
+ console.log(
291
+ `${colors.yellow}Warnings: ${steps.warning}${colors.reset}`
292
+ );
229
293
  if (steps.skipped > 0) console.log(`Skipped: ${steps.skipped}`);
230
294
  }
231
295
 
232
296
  // If we have specs with failures, display them
233
297
  if (results.specs && hasFailures) {
234
- console.log(`\n${colors.bold}${colors.red}Failed Items:${colors.reset}`);
235
-
298
+ console.log(
299
+ `\n${colors.bold}${colors.red}Failed Items:${colors.reset}`
300
+ );
301
+
236
302
  // Collect failures
237
303
  const failedSpecs = [];
238
304
  const failedTests = [];
239
305
  const failedContexts = [];
240
306
  const failedSteps = [];
241
-
307
+
242
308
  // Process specs array to collect failures
243
309
  results.specs.forEach((spec, specIndex) => {
244
310
  // Check if spec has failed
@@ -248,7 +314,7 @@ const reporters = {
248
314
  id: spec.specId || `Spec ${specIndex + 1}`,
249
315
  });
250
316
  }
251
-
317
+
252
318
  // Process tests in this spec
253
319
  if (spec.tests && spec.tests.length > 0) {
254
320
  spec.tests.forEach((test, testIndex) => {
@@ -261,12 +327,15 @@ const reporters = {
261
327
  id: test.testId || `Test ${testIndex + 1}`,
262
328
  });
263
329
  }
264
-
330
+
265
331
  // Process contexts in this test
266
332
  if (test.contexts && test.contexts.length > 0) {
267
333
  test.contexts.forEach((context, contextIndex) => {
268
334
  // Check if context has failed
269
- if (context.result === "FAIL" || (context.result && context.result.status === "FAIL")) {
335
+ if (
336
+ context.result === "FAIL" ||
337
+ (context.result && context.result.status === "FAIL")
338
+ ) {
270
339
  failedContexts.push({
271
340
  specIndex,
272
341
  testIndex,
@@ -274,10 +343,12 @@ const reporters = {
274
343
  specId: spec.specId || `Spec ${specIndex + 1}`,
275
344
  testId: test.testId || `Test ${testIndex + 1}`,
276
345
  platform: context.platform || "unknown",
277
- browser: context.browser ? context.browser.name : "unknown",
346
+ browser: context.browser
347
+ ? context.browser.name
348
+ : "unknown",
278
349
  });
279
350
  }
280
-
351
+
281
352
  // Process steps in this context
282
353
  if (context.steps && context.steps.length > 0) {
283
354
  context.steps.forEach((step, stepIndex) => {
@@ -291,7 +362,9 @@ const reporters = {
291
362
  specId: spec.specId || `Spec ${specIndex + 1}`,
292
363
  testId: test.testId || `Test ${testIndex + 1}`,
293
364
  platform: context.platform || "unknown",
294
- browser: context.browser ? context.browser.name : "unknown",
365
+ browser: context.browser
366
+ ? context.browser.name
367
+ : "unknown",
295
368
  stepId: step.stepId || `Step ${stepIndex + 1}`,
296
369
  error: step.resultDescription || "Unknown error",
297
370
  });
@@ -303,7 +376,7 @@ const reporters = {
303
376
  });
304
377
  }
305
378
  });
306
-
379
+
307
380
  // Display failures
308
381
  if (failedSpecs.length > 0) {
309
382
  console.log(`\n${colors.red}Failed Specs:${colors.reset}`);
@@ -311,25 +384,37 @@ const reporters = {
311
384
  console.log(`${colors.red}${i + 1}. ${item.id}${colors.reset}`);
312
385
  });
313
386
  }
314
-
387
+
315
388
  if (failedTests.length > 0) {
316
389
  console.log(`\n${colors.red}Failed Tests:${colors.reset}`);
317
390
  failedTests.forEach((item, i) => {
318
- console.log(`${colors.red}${i + 1}. ${item.id} (from ${item.specId})${colors.reset}`);
391
+ console.log(
392
+ `${colors.red}${i + 1}. ${item.id} (from ${item.specId})${
393
+ colors.reset
394
+ }`
395
+ );
319
396
  });
320
397
  }
321
-
398
+
322
399
  if (failedContexts.length > 0) {
323
400
  console.log(`\n${colors.red}Failed Contexts:${colors.reset}`);
324
401
  failedContexts.forEach((item, i) => {
325
- console.log(`${colors.red}${i + 1}. ${item.platform}/${item.browser} (from ${item.testId})${colors.reset}`);
402
+ console.log(
403
+ `${colors.red}${i + 1}. ${item.platform}/${item.browser} (from ${
404
+ item.testId
405
+ })${colors.reset}`
406
+ );
326
407
  });
327
408
  }
328
-
409
+
329
410
  if (failedSteps.length > 0) {
330
411
  console.log(`\n${colors.red}Failed Steps:${colors.reset}`);
331
412
  failedSteps.forEach((item, i) => {
332
- console.log(`${colors.red}${i + 1}. ${item.platform}/${item.browser} - ${item.stepId}${colors.reset}`);
413
+ console.log(
414
+ `${colors.red}${i + 1}. ${item.platform}/${item.browser} - ${
415
+ item.stepId
416
+ }${colors.reset}`
417
+ );
333
418
  console.log(` Error: ${item.error}`);
334
419
  });
335
420
  }
@@ -337,12 +422,14 @@ const reporters = {
337
422
  // Celebration when all tests pass
338
423
  console.log(`\n${colors.green}🎉 All items passed! 🎉${colors.reset}`);
339
424
  }
340
- } else {
341
- console.log("No tests were executed or results are in an unknown format.");
425
+ } else {
426
+ console.log(
427
+ "No tests were executed or results are in an unknown format."
428
+ );
342
429
  }
343
-
430
+
344
431
  console.log("\n===============================\n");
345
- }
432
+ },
346
433
  };
347
434
 
348
435
  // Export reporters for external use
@@ -350,8 +437,8 @@ exports.reporters = reporters;
350
437
 
351
438
  // Helper function to register custom reporters
352
439
  function registerReporter(name, reporterFunction) {
353
- if (typeof reporterFunction !== 'function') {
354
- throw new Error('Reporter must be a function');
440
+ if (typeof reporterFunction !== "function") {
441
+ throw new Error("Reporter must be a function");
355
442
  }
356
443
  reporters[name] = reporterFunction;
357
444
  return true;
@@ -362,21 +449,21 @@ exports.registerReporter = registerReporter;
362
449
 
363
450
  async function outputResults(config = {}, outputPath, results, options = {}) {
364
451
  // Default to using both built-in reporters if none specified
365
- const defaultReporters = ['terminal','json'];
366
-
452
+ const defaultReporters = ["terminal", "json"];
453
+
367
454
  let activeReporters = options.reporters || defaultReporters;
368
-
455
+
369
456
  // If the reporters option is provided as strings, normalize them
370
457
  if (activeReporters.length > 0) {
371
458
  // Convert any shorthand names to full reporter names
372
- activeReporters = activeReporters.map(reporter => {
373
- if (typeof reporter === 'string') {
459
+ activeReporters = activeReporters.map((reporter) => {
460
+ if (typeof reporter === "string") {
374
461
  // Convert shorthand names to actual reporter keys
375
462
  switch (reporter.toLowerCase()) {
376
- case 'json':
377
- return 'jsonReporter';
378
- case 'terminal':
379
- return 'terminalReporter';
463
+ case "json":
464
+ return "jsonReporter";
465
+ case "terminal":
466
+ return "terminalReporter";
380
467
  default:
381
468
  return reporter;
382
469
  }
@@ -386,15 +473,19 @@ async function outputResults(config = {}, outputPath, results, options = {}) {
386
473
  }
387
474
 
388
475
  // Execute each reporter
389
- const reporterPromises = activeReporters.map(reporter => {
390
- if (typeof reporter === 'function') {
476
+ const reporterPromises = activeReporters.map((reporter) => {
477
+ if (typeof reporter === "function") {
391
478
  // Direct function reference
392
479
  return reporter(config, outputPath, results, options);
393
- } else if (typeof reporter === 'string' && reporters[reporter]) {
480
+ } else if (typeof reporter === "string" && reporters[reporter]) {
394
481
  // String reference to built-in or registered reporter
395
482
  return reporters[reporter](config, outputPath, results, options);
396
- } else if (typeof reporter === 'string' && !reporters[reporter]) {
397
- console.error(`Reporter "${reporter}" not found. Available reporters: ${Object.keys(reporters).join(', ')}`);
483
+ } else if (typeof reporter === "string" && !reporters[reporter]) {
484
+ console.error(
485
+ `Reporter "${reporter}" not found. Available reporters: ${Object.keys(
486
+ reporters
487
+ ).join(", ")}`
488
+ );
398
489
  return Promise.resolve();
399
490
  } else {
400
491
  console.error(`Invalid reporter: ${reporter}`);
@@ -67,12 +67,15 @@ describe("Util tests", function () {
67
67
  });
68
68
 
69
69
  // Test that config overrides are set correctly
70
- it("Config overrides are set correctly", function () {
70
+ it("Config overrides are set correctly", async function () {
71
+ // This test takes a bit longer
72
+ this.timeout(5000);
73
+
71
74
  configSets = [
72
75
  {
73
76
  // Input override
74
77
  args: ["node", "runTests.js", "--input", "input.spec.json"],
75
- expected: { input: "input.spec.json" },
78
+ expected: { input: [path.resolve(process.cwd(), "input.spec.json")] },
76
79
  },
77
80
  {
78
81
  // Input and logLevel overrides
@@ -84,7 +87,7 @@ describe("Util tests", function () {
84
87
  "--logLevel",
85
88
  "debug",
86
89
  ],
87
- expected: { input: "input.spec.json", logLevel: "debug" },
90
+ expected: { input: [path.resolve(process.cwd(), "input.spec.json")], logLevel: "debug" },
88
91
  },
89
92
  {
90
93
  // Input, logLevel, and setup overrides
@@ -97,7 +100,7 @@ describe("Util tests", function () {
97
100
  "debug",
98
101
  ],
99
102
  expected: {
100
- input: "input.spec.json",
103
+ input: [path.resolve(process.cwd(), "input.spec.json")],
101
104
  logLevel: "debug",
102
105
  },
103
106
  },
@@ -105,8 +108,7 @@ describe("Util tests", function () {
105
108
  // Referenced config without overrides
106
109
  args: ["node", "runTests.js", "--config", "./test/test-config.json"],
107
110
  expected: {
108
- input: ".",
109
- output: ".",
111
+ input: process.cwd(),
110
112
  logLevel: "silent",
111
113
  recursive: true,
112
114
  },
@@ -122,18 +124,42 @@ describe("Util tests", function () {
122
124
  "input.spec.json",
123
125
  ],
124
126
  expected: {
125
- input: "input.spec.json",
126
- output: ".",
127
+ input: [path.resolve(process.cwd(), "input.spec.json")],
127
128
  logLevel: "silent",
128
129
  recursive: true,
129
130
  },
130
131
  },
132
+ {
133
+ // Multiple inputs
134
+ args: [
135
+ "node",
136
+ "runTests.js",
137
+ "--config",
138
+ "./test/test-config.json",
139
+ "--input",
140
+ "input.spec.json,anotherInput.spec.json",
141
+ ],
142
+ expected: {
143
+ input: [path.resolve(process.cwd(), "input.spec.json"), path.resolve(process.cwd(), "anotherInput.spec.json")],
144
+ output: process.cwd(),
145
+ recursive: true,
146
+ },
147
+ }
131
148
  ];
132
149
 
133
- configSets.forEach(async (configSet) => {
134
- const configResult = await setConfig({}, setArgs(configSet.args));
150
+ // Use process.stdout.write directly to force console output during tests
151
+ console.log('\n===== CONFIG TEST RESULTS =====\n');
152
+
153
+ // Use Promise.all with map instead of forEach to properly handle async operations
154
+ await Promise.all(configSets.map(async (configSet, index) => {
155
+ // Set config with the args
156
+ console.log(`Config test ${index}: ${JSON.stringify(configSet, null, 2)}`);
157
+ const configResult = await setConfig({ args: setArgs(configSet.args) });
158
+ console.log(`Config result ${index}: ${JSON.stringify(configResult, null, 2)}\n`);
159
+ // Deeply compare the config result with the expected result
135
160
  deepObjectExpect(configResult, configSet.expected);
136
- });
161
+ }));
162
+ process.stdout.write('===== END CONFIG TEST RESULTS =====\n');
137
163
  });
138
164
 
139
165
  // Test that results output correctly.
@@ -157,11 +183,33 @@ describe("Util tests", function () {
157
183
  function deepObjectExpect(actual, expected) {
158
184
  // Check that actual has all the keys of expected
159
185
  Object.entries(expected).forEach(([key, value]) => {
160
- // If value is an object, recursively check it
161
- if (typeof value === "object") {
186
+ // Make sure the property exists in actual
187
+ expect(actual).to.have.property(key);
188
+
189
+ // If value is null, check directly
190
+ if (value === null) {
191
+ expect(actual[key]).to.equal(null);
192
+ }
193
+ // If value is an array, check each item
194
+ else if (Array.isArray(value)) {
195
+ expect(Array.isArray(actual[key])).to.equal(true, `Expected ${key} to be an array. Expected: ${expected[key]}. Actual: ${actual[key]}.`);
196
+ expect(actual[key].length).to.equal(value.length, `Expected ${key} array to have length ${value.length}. Actual: ${actual[key].length}`);
197
+
198
+ // Check each array item
199
+ value.forEach((item, index) => {
200
+ if (typeof item === "object" && item !== null) {
201
+ deepObjectExpect(actual[key][index], item);
202
+ } else {
203
+ expect(actual[key][index]).to.equal(item);
204
+ }
205
+ });
206
+ }
207
+ // If value is an object but not null, recursively check it
208
+ else if (typeof value === "object") {
162
209
  deepObjectExpect(actual[key], expected[key]);
163
- } else {
164
- // Otherwise, check that the value is correct
210
+ }
211
+ // Otherwise, check that the value is correct
212
+ else {
165
213
  const expectedObject = {};
166
214
  expectedObject[key] = value;
167
215
  expect(actual).to.deep.include(expectedObject);
@@ -1,26 +0,0 @@
1
- {
2
- "tests": [
3
- {
4
- "steps": [
5
- {
6
- "action": "checkLink",
7
- "url": "https://www.duckduckgo.com"
8
- },
9
- {
10
- "action": "httpRequest",
11
- "url": "https://reqres.in/api/users",
12
- "method": "post",
13
- "requestData": {
14
- "name": "morpheus",
15
- "job": "leader"
16
- },
17
- "responseData": {
18
- "name": "morpheus",
19
- "job": "leader"
20
- },
21
- "statusCodes": [200, 201]
22
- }
23
- ]
24
- }
25
- ]
26
- }