feat: collection CLI runner with iterations and data feed (#4475)

Co-authored-by: Shoban <mshobanr@ford.com>
Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
shobanrajm
2024-11-22 22:38:51 +05:30
committed by GitHub
parent e040f44245
commit b78cd57884
16 changed files with 705 additions and 320 deletions

View File

@@ -28,31 +28,50 @@ hopp [options or commands] arguments
- Displays the help text - Displays the help text
3. #### **`hopp test [options] <file_path>`** 3. #### **`hopp test [options] <file_path>`**
- Interactive CLI to accept Hoppscotch collection JSON path - Interactive CLI to accept Hoppscotch collection JSON path
- Parses the collection JSON and executes each requests - Parses the collection JSON and executes each requests
- Executes pre-request script. - Executes pre-request script.
- Outputs the response of each request. - Outputs the response of each request.
- Executes and outputs test-script response. - Executes and outputs test-script response.
#### Options: #### Options:
##### `-e <file_path>` / `--env <file_path>` ##### `-e <file_path>` / `--env <file_path>`
- Accepts path to env.json with contents in below format: - Accepts path to env.json with contents in below format:
```json ```json
{ {
"ENV1":"value1", "ENV1": "value1",
"ENV2":"value2" "ENV2": "value2"
} }
``` ```
- You can now access those variables using `pw.env.get('<var_name>')` - You can now access those variables using `pw.env.get('<var_name>')`
Taking the above example, `pw.env.get("ENV1")` will return `"value1"` Taking the above example, `pw.env.get("ENV1")` will return `"value1"`
##### `--iteration-count <no_of_iterations>`
- Accepts the number of iterations to run the collection
##### `--iteration-data <file_path>`
- Accepts the path to a CSV file with contents in the below format:
```text
key1,key2,key3
value1,value2,value3
value4,value5,value6
```
For every iteration the values will be replaced with the respective keys in the environment. For iteration 1 the value1,value2,value3 will be replaced and for iteration 2 value4,value5,value6 will be replaced and so on.
## Install ## Install
- Before you install Hoppscotch CLI you need to make sure you have the dependencies it requires to run. - Before you install Hoppscotch CLI you need to make sure you have the dependencies it requires to run.
- **Windows & macOS**: You will need `node-gyp` installed. Find instructions here: https://github.com/nodejs/node-gyp - **Windows & macOS**: You will need `node-gyp` installed. Find instructions here: https://github.com/nodejs/node-gyp
- **Debian/Ubuntu derivatives**: - **Debian/Ubuntu derivatives**:
```sh ```sh
@@ -75,7 +94,6 @@ hopp [options or commands] arguments
sudo dnf install python3 make gcc gcc-c++ zlib-devel brotli-devel openssl-devel libuv-devel sudo dnf install python3 make gcc gcc-c++ zlib-devel brotli-devel openssl-devel libuv-devel
``` ```
- Once the dependencies are installed, install [@hoppscotch/cli](https://www.npmjs.com/package/@hoppscotch/cli) from npm by running: - Once the dependencies are installed, install [@hoppscotch/cli](https://www.npmjs.com/package/@hoppscotch/cli) from npm by running:
``` ```
npm i -g @hoppscotch/cli npm i -g @hoppscotch/cli
@@ -112,39 +130,39 @@ Please note we have a code of conduct, please follow it in all your interactions
1. After cloning the repository, execute the following commands: 1. After cloning the repository, execute the following commands:
```bash ```bash
pnpm install pnpm install
pnpm run build pnpm run build
``` ```
2. In order to test locally, you can use two types of package linking: 2. In order to test locally, you can use two types of package linking:
1. The 'pnpm exec' way (preferred since it does not hamper your original installation of the CLI): 1. The 'pnpm exec' way (preferred since it does not hamper your original installation of the CLI):
```bash ```bash
pnpm link @hoppscotch/cli pnpm link @hoppscotch/cli
// Then to use or test the CLI: // Then to use or test the CLI:
pnpm exec hopp pnpm exec hopp
// After testing, to remove the package linking: // After testing, to remove the package linking:
pnpm rm @hoppscotch/cli pnpm rm @hoppscotch/cli
``` ```
2. The 'global' way (warning: this might override the globally installed CLI, if exists): 2. The 'global' way (warning: this might override the globally installed CLI, if exists):
```bash ```bash
sudo pnpm link --global sudo pnpm link --global
// Then to use or test the CLI: // Then to use or test the CLI:
hopp hopp
// After testing, to remove the package linking: // After testing, to remove the package linking:
sudo pnpm rm --global @hoppscotch/cli sudo pnpm rm --global @hoppscotch/cli
``` ```
3. To use the Typescript watch scripts: 3. To use the Typescript watch scripts:
```bash ```bash
pnpm run dev pnpm run dev
``` ```

View File

@@ -1,6 +1,6 @@
{ {
"name": "@hoppscotch/cli", "name": "@hoppscotch/cli",
"version": "0.12.0", "version": "0.13.0",
"description": "A CLI to run Hoppscotch test scripts in CI environments.", "description": "A CLI to run Hoppscotch test scripts in CI environments.",
"homepage": "https://hoppscotch.io", "homepage": "https://hoppscotch.io",
"type": "module", "type": "module",
@@ -51,7 +51,8 @@
"qs": "6.13.0", "qs": "6.13.0",
"verzod": "0.2.3", "verzod": "0.2.3",
"xmlbuilder2": "3.1.1", "xmlbuilder2": "3.1.1",
"zod": "3.23.8" "zod": "3.23.8",
"papaparse": "5.4.1"
}, },
"devDependencies": { "devDependencies": {
"@hoppscotch/data": "workspace:^", "@hoppscotch/data": "workspace:^",
@@ -64,6 +65,7 @@
"qs": "6.11.2", "qs": "6.11.2",
"tsup": "8.3.0", "tsup": "8.3.0",
"typescript": "5.6.3", "typescript": "5.6.3",
"vitest": "2.1.2" "vitest": "2.1.2",
"@types/papaparse": "5.3.14"
} }
} }

View File

@@ -3,6 +3,92 @@
exports[`hopp test [options] <file_path_or_id> > Test\`hopp test <file_path_or_id> --env <file_path_or_id> --reporter-junit [path] > Generates a JUnit report at the default path 1`] = ` exports[`hopp test [options] <file_path_or_id> > Test\`hopp test <file_path_or_id> --env <file_path_or_id> --reporter-junit [path] > Generates a JUnit report at the default path 1`] = `
"<?xml version="1.0" encoding="UTF-8"?> "<?xml version="1.0" encoding="UTF-8"?>
<testsuites tests="76" failures="2" errors="66" time="time"> <testsuites tests="76" failures="2" errors="66" time="time">
<testsuite name="test-junit-report-export/assertions/error" time="time" timestamp="timestamp" tests="22" failures="0" errors="22">
<testcase name="\`toBeLevelxxx()\` error scenarios - Expected 200-level status but could not parse value 'foo'" classname="test-junit-report-export/assertions/error">
<error message="Expected 200-level status but could not parse value 'foo'"/>
</testcase>
<testcase name="\`toBeLevelxxx()\` error scenarios - Expected 200-level status but could not parse value 'foo'" classname="test-junit-report-export/assertions/error">
<error message="Expected 200-level status but could not parse value 'foo'"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string" classname="test-junit-report-export/assertions/error">
<error message="Expected toHaveLength to be called for an array or string"/>
</testcase>
<testcase name="\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string" classname="test-junit-report-export/assertions/error">
<error message="Expected toHaveLength to be called for an array or string"/>
</testcase>
<testcase name="\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string" classname="test-junit-report-export/assertions/error">
<error message="Expected toHaveLength to be called for an array or string"/>
</testcase>
<testcase name="\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string" classname="test-junit-report-export/assertions/error">
<error message="Expected toHaveLength to be called for an array or string"/>
</testcase>
<testcase name="\`toHaveLength()\` error scenarios - Argument for toHaveLength should be a number" classname="test-junit-report-export/assertions/error">
<error message="Argument for toHaveLength should be a number"/>
</testcase>
<testcase name="\`toHaveLength()\` error scenarios - Argument for toHaveLength should be a number" classname="test-junit-report-export/assertions/error">
<error message="Argument for toHaveLength should be a number"/>
</testcase>
<testcase name="\`toInclude() error scenarios\` - Expected toInclude to be called for an array or string" classname="test-junit-report-export/assertions/error">
<error message="Expected toInclude to be called for an array or string"/>
</testcase>
<testcase name="\`toInclude() error scenarios\` - Expected toInclude to be called for an array or string" classname="test-junit-report-export/assertions/error">
<error message="Expected toInclude to be called for an array or string"/>
</testcase>
<testcase name="\`toInclude() error scenarios\` - Argument for toInclude should not be null" classname="test-junit-report-export/assertions/error">
<error message="Argument for toInclude should not be null"/>
</testcase>
<testcase name="\`toInclude() error scenarios\` - Argument for toInclude should not be undefined" classname="test-junit-report-export/assertions/error">
<error message="Argument for toInclude should not be undefined"/>
</testcase>
</testsuite>
<testsuite name="test-junit-report-export/assertions/success" time="time" timestamp="timestamp" tests="5" failures="0" errors="0">
<testcase name="Status code is 200 - Expected '200' to be '200'" classname="test-junit-report-export/assertions/success"/>
<testcase name="Check headers - Expected 'application/json, text/plain, */*,image/webp' to be 'application/json, text/plain, */*,image/webp'" classname="test-junit-report-export/assertions/success"/>
<testcase name="Check headers - Expected 'echo.hoppscotch.io' to be 'echo.hoppscotch.io'" classname="test-junit-report-export/assertions/success"/>
<testcase name="Check headers - Expected 'undefined' to be 'undefined'" classname="test-junit-report-export/assertions/success"/>
<testcase name="Status code is 2xx - Expected '200' to be 200-level status" classname="test-junit-report-export/assertions/success"/>
</testsuite>
<testsuite name="test-junit-report-export/assertions/failure" time="time" timestamp="timestamp" tests="5" failures="2" errors="0">
<testcase name="Simulating failure - Status code is 200 - Expected '200' to not be '200'" classname="test-junit-report-export/assertions/failure">
<failure type="AssertionFailure" message="Expected '200' to not be '200'"/>
</testcase>
<testcase name="Simulating failure - Check headers - Expected 'application/json, text/plain, */*,image/webp' to not be 'application/json, text/plain, */*'" classname="test-junit-report-export/assertions/failure"/>
<testcase name="Simulating failure - Check headers - Expected 'echo.hoppscotch.io' to not be 'httpbin.org'" classname="test-junit-report-export/assertions/failure"/>
<testcase name="Simulating failure - Check headers - Expected 'undefined' to not be 'value'" classname="test-junit-report-export/assertions/failure"/>
<testcase name="Simulating failure - Status code is 2xx - Expected '200' to not be 200-level status" classname="test-junit-report-export/assertions/failure">
<failure type="AssertionFailure" message="Expected '200' to not be 200-level status"/>
</testcase>
</testsuite>
<testsuite name="test-junit-report-export/request-level-errors/invalid-url" time="time" timestamp="timestamp" tests="22" failures="0" errors="22"> <testsuite name="test-junit-report-export/request-level-errors/invalid-url" time="time" timestamp="timestamp" tests="22" failures="0" errors="22">
<system-err><![CDATA[ <system-err><![CDATA[
REQUEST_ERROR - TypeError: Invalid URL]]></system-err> REQUEST_ERROR - TypeError: Invalid URL]]></system-err>
@@ -150,98 +236,98 @@ exports[`hopp test [options] <file_path_or_id> > Test\`hopp test <file_path_or_i
<error message="Argument for toInclude should not be undefined"/> <error message="Argument for toInclude should not be undefined"/>
</testcase> </testcase>
</testsuite> </testsuite>
<testsuite name="test-junit-report-export/assertions/error" time="time" timestamp="timestamp" tests="22" failures="0" errors="22">
<testcase name="\`toBeLevelxxx()\` error scenarios - Expected 200-level status but could not parse value 'foo'" classname="test-junit-report-export/assertions/error">
<error message="Expected 200-level status but could not parse value 'foo'"/>
</testcase>
<testcase name="\`toBeLevelxxx()\` error scenarios - Expected 200-level status but could not parse value 'foo'" classname="test-junit-report-export/assertions/error">
<error message="Expected 200-level status but could not parse value 'foo'"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string" classname="test-junit-report-export/assertions/error">
<error message="Expected toHaveLength to be called for an array or string"/>
</testcase>
<testcase name="\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string" classname="test-junit-report-export/assertions/error">
<error message="Expected toHaveLength to be called for an array or string"/>
</testcase>
<testcase name="\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string" classname="test-junit-report-export/assertions/error">
<error message="Expected toHaveLength to be called for an array or string"/>
</testcase>
<testcase name="\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string" classname="test-junit-report-export/assertions/error">
<error message="Expected toHaveLength to be called for an array or string"/>
</testcase>
<testcase name="\`toHaveLength()\` error scenarios - Argument for toHaveLength should be a number" classname="test-junit-report-export/assertions/error">
<error message="Argument for toHaveLength should be a number"/>
</testcase>
<testcase name="\`toHaveLength()\` error scenarios - Argument for toHaveLength should be a number" classname="test-junit-report-export/assertions/error">
<error message="Argument for toHaveLength should be a number"/>
</testcase>
<testcase name="\`toInclude() error scenarios\` - Expected toInclude to be called for an array or string" classname="test-junit-report-export/assertions/error">
<error message="Expected toInclude to be called for an array or string"/>
</testcase>
<testcase name="\`toInclude() error scenarios\` - Expected toInclude to be called for an array or string" classname="test-junit-report-export/assertions/error">
<error message="Expected toInclude to be called for an array or string"/>
</testcase>
<testcase name="\`toInclude() error scenarios\` - Argument for toInclude should not be null" classname="test-junit-report-export/assertions/error">
<error message="Argument for toInclude should not be null"/>
</testcase>
<testcase name="\`toInclude() error scenarios\` - Argument for toInclude should not be undefined" classname="test-junit-report-export/assertions/error">
<error message="Argument for toInclude should not be undefined"/>
</testcase>
</testsuite>
<testsuite name="test-junit-report-export/assertions/success" time="time" timestamp="timestamp" tests="5" failures="0" errors="0">
<testcase name="Status code is 200 - Expected '200' to be '200'" classname="test-junit-report-export/assertions/success"/>
<testcase name="Check headers - Expected 'application/json, text/plain, */*,image/webp' to be 'application/json, text/plain, */*,image/webp'" classname="test-junit-report-export/assertions/success"/>
<testcase name="Check headers - Expected 'echo.hoppscotch.io' to be 'echo.hoppscotch.io'" classname="test-junit-report-export/assertions/success"/>
<testcase name="Check headers - Expected 'undefined' to be 'undefined'" classname="test-junit-report-export/assertions/success"/>
<testcase name="Status code is 2xx - Expected '200' to be 200-level status" classname="test-junit-report-export/assertions/success"/>
</testsuite>
<testsuite name="test-junit-report-export/assertions/failure" time="time" timestamp="timestamp" tests="5" failures="2" errors="0">
<testcase name="Simulating failure - Status code is 200 - Expected '200' to not be '200'" classname="test-junit-report-export/assertions/failure">
<failure type="AssertionFailure" message="Expected '200' to not be '200'"/>
</testcase>
<testcase name="Simulating failure - Check headers - Expected 'application/json, text/plain, */*,image/webp' to not be 'application/json, text/plain, */*'" classname="test-junit-report-export/assertions/failure"/>
<testcase name="Simulating failure - Check headers - Expected 'echo.hoppscotch.io' to not be 'httpbin.org'" classname="test-junit-report-export/assertions/failure"/>
<testcase name="Simulating failure - Check headers - Expected 'undefined' to not be 'value'" classname="test-junit-report-export/assertions/failure"/>
<testcase name="Simulating failure - Status code is 2xx - Expected '200' to not be 200-level status" classname="test-junit-report-export/assertions/failure">
<failure type="AssertionFailure" message="Expected '200' to not be 200-level status"/>
</testcase>
</testsuite>
</testsuites>" </testsuites>"
`; `;
exports[`hopp test [options] <file_path_or_id> > Test\`hopp test <file_path_or_id> --env <file_path_or_id> --reporter-junit [path] > Generates a JUnit report at the specified path 1`] = ` exports[`hopp test [options] <file_path_or_id> > Test\`hopp test <file_path_or_id> --env <file_path_or_id> --reporter-junit [path] > Generates a JUnit report at the specified path 1`] = `
"<?xml version="1.0" encoding="UTF-8"?> "<?xml version="1.0" encoding="UTF-8"?>
<testsuites tests="76" failures="2" errors="66" time="time"> <testsuites tests="76" failures="2" errors="66" time="time">
<testsuite name="test-junit-report-export/assertions/error" time="time" timestamp="timestamp" tests="22" failures="0" errors="22">
<testcase name="\`toBeLevelxxx()\` error scenarios - Expected 200-level status but could not parse value 'foo'" classname="test-junit-report-export/assertions/error">
<error message="Expected 200-level status but could not parse value 'foo'"/>
</testcase>
<testcase name="\`toBeLevelxxx()\` error scenarios - Expected 200-level status but could not parse value 'foo'" classname="test-junit-report-export/assertions/error">
<error message="Expected 200-level status but could not parse value 'foo'"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string" classname="test-junit-report-export/assertions/error">
<error message="Expected toHaveLength to be called for an array or string"/>
</testcase>
<testcase name="\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string" classname="test-junit-report-export/assertions/error">
<error message="Expected toHaveLength to be called for an array or string"/>
</testcase>
<testcase name="\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string" classname="test-junit-report-export/assertions/error">
<error message="Expected toHaveLength to be called for an array or string"/>
</testcase>
<testcase name="\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string" classname="test-junit-report-export/assertions/error">
<error message="Expected toHaveLength to be called for an array or string"/>
</testcase>
<testcase name="\`toHaveLength()\` error scenarios - Argument for toHaveLength should be a number" classname="test-junit-report-export/assertions/error">
<error message="Argument for toHaveLength should be a number"/>
</testcase>
<testcase name="\`toHaveLength()\` error scenarios - Argument for toHaveLength should be a number" classname="test-junit-report-export/assertions/error">
<error message="Argument for toHaveLength should be a number"/>
</testcase>
<testcase name="\`toInclude() error scenarios\` - Expected toInclude to be called for an array or string" classname="test-junit-report-export/assertions/error">
<error message="Expected toInclude to be called for an array or string"/>
</testcase>
<testcase name="\`toInclude() error scenarios\` - Expected toInclude to be called for an array or string" classname="test-junit-report-export/assertions/error">
<error message="Expected toInclude to be called for an array or string"/>
</testcase>
<testcase name="\`toInclude() error scenarios\` - Argument for toInclude should not be null" classname="test-junit-report-export/assertions/error">
<error message="Argument for toInclude should not be null"/>
</testcase>
<testcase name="\`toInclude() error scenarios\` - Argument for toInclude should not be undefined" classname="test-junit-report-export/assertions/error">
<error message="Argument for toInclude should not be undefined"/>
</testcase>
</testsuite>
<testsuite name="test-junit-report-export/assertions/success" time="time" timestamp="timestamp" tests="5" failures="0" errors="0">
<testcase name="Status code is 200 - Expected '200' to be '200'" classname="test-junit-report-export/assertions/success"/>
<testcase name="Check headers - Expected 'application/json, text/plain, */*,image/webp' to be 'application/json, text/plain, */*,image/webp'" classname="test-junit-report-export/assertions/success"/>
<testcase name="Check headers - Expected 'echo.hoppscotch.io' to be 'echo.hoppscotch.io'" classname="test-junit-report-export/assertions/success"/>
<testcase name="Check headers - Expected 'undefined' to be 'undefined'" classname="test-junit-report-export/assertions/success"/>
<testcase name="Status code is 2xx - Expected '200' to be 200-level status" classname="test-junit-report-export/assertions/success"/>
</testsuite>
<testsuite name="test-junit-report-export/assertions/failure" time="time" timestamp="timestamp" tests="5" failures="2" errors="0">
<testcase name="Simulating failure - Status code is 200 - Expected '200' to not be '200'" classname="test-junit-report-export/assertions/failure">
<failure type="AssertionFailure" message="Expected '200' to not be '200'"/>
</testcase>
<testcase name="Simulating failure - Check headers - Expected 'application/json, text/plain, */*,image/webp' to not be 'application/json, text/plain, */*'" classname="test-junit-report-export/assertions/failure"/>
<testcase name="Simulating failure - Check headers - Expected 'echo.hoppscotch.io' to not be 'httpbin.org'" classname="test-junit-report-export/assertions/failure"/>
<testcase name="Simulating failure - Check headers - Expected 'undefined' to not be 'value'" classname="test-junit-report-export/assertions/failure"/>
<testcase name="Simulating failure - Status code is 2xx - Expected '200' to not be 200-level status" classname="test-junit-report-export/assertions/failure">
<failure type="AssertionFailure" message="Expected '200' to not be 200-level status"/>
</testcase>
</testsuite>
<testsuite name="test-junit-report-export/request-level-errors/invalid-url" time="time" timestamp="timestamp" tests="22" failures="0" errors="22"> <testsuite name="test-junit-report-export/request-level-errors/invalid-url" time="time" timestamp="timestamp" tests="22" failures="0" errors="22">
<system-err><![CDATA[ <system-err><![CDATA[
REQUEST_ERROR - TypeError: Invalid URL]]></system-err> REQUEST_ERROR - TypeError: Invalid URL]]></system-err>
@@ -389,92 +475,6 @@ exports[`hopp test [options] <file_path_or_id> > Test\`hopp test <file_path_or_i
<error message="Argument for toInclude should not be undefined"/> <error message="Argument for toInclude should not be undefined"/>
</testcase> </testcase>
</testsuite> </testsuite>
<testsuite name="test-junit-report-export/assertions/error" time="time" timestamp="timestamp" tests="22" failures="0" errors="22">
<testcase name="\`toBeLevelxxx()\` error scenarios - Expected 200-level status but could not parse value 'foo'" classname="test-junit-report-export/assertions/error">
<error message="Expected 200-level status but could not parse value 'foo'"/>
</testcase>
<testcase name="\`toBeLevelxxx()\` error scenarios - Expected 200-level status but could not parse value 'foo'" classname="test-junit-report-export/assertions/error">
<error message="Expected 200-level status but could not parse value 'foo'"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;" classname="test-junit-report-export/assertions/error">
<error message="Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;"/>
</testcase>
<testcase name="\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string" classname="test-junit-report-export/assertions/error">
<error message="Expected toHaveLength to be called for an array or string"/>
</testcase>
<testcase name="\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string" classname="test-junit-report-export/assertions/error">
<error message="Expected toHaveLength to be called for an array or string"/>
</testcase>
<testcase name="\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string" classname="test-junit-report-export/assertions/error">
<error message="Expected toHaveLength to be called for an array or string"/>
</testcase>
<testcase name="\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string" classname="test-junit-report-export/assertions/error">
<error message="Expected toHaveLength to be called for an array or string"/>
</testcase>
<testcase name="\`toHaveLength()\` error scenarios - Argument for toHaveLength should be a number" classname="test-junit-report-export/assertions/error">
<error message="Argument for toHaveLength should be a number"/>
</testcase>
<testcase name="\`toHaveLength()\` error scenarios - Argument for toHaveLength should be a number" classname="test-junit-report-export/assertions/error">
<error message="Argument for toHaveLength should be a number"/>
</testcase>
<testcase name="\`toInclude() error scenarios\` - Expected toInclude to be called for an array or string" classname="test-junit-report-export/assertions/error">
<error message="Expected toInclude to be called for an array or string"/>
</testcase>
<testcase name="\`toInclude() error scenarios\` - Expected toInclude to be called for an array or string" classname="test-junit-report-export/assertions/error">
<error message="Expected toInclude to be called for an array or string"/>
</testcase>
<testcase name="\`toInclude() error scenarios\` - Argument for toInclude should not be null" classname="test-junit-report-export/assertions/error">
<error message="Argument for toInclude should not be null"/>
</testcase>
<testcase name="\`toInclude() error scenarios\` - Argument for toInclude should not be undefined" classname="test-junit-report-export/assertions/error">
<error message="Argument for toInclude should not be undefined"/>
</testcase>
</testsuite>
<testsuite name="test-junit-report-export/assertions/success" time="time" timestamp="timestamp" tests="5" failures="0" errors="0">
<testcase name="Status code is 200 - Expected '200' to be '200'" classname="test-junit-report-export/assertions/success"/>
<testcase name="Check headers - Expected 'application/json, text/plain, */*,image/webp' to be 'application/json, text/plain, */*,image/webp'" classname="test-junit-report-export/assertions/success"/>
<testcase name="Check headers - Expected 'echo.hoppscotch.io' to be 'echo.hoppscotch.io'" classname="test-junit-report-export/assertions/success"/>
<testcase name="Check headers - Expected 'undefined' to be 'undefined'" classname="test-junit-report-export/assertions/success"/>
<testcase name="Status code is 2xx - Expected '200' to be 200-level status" classname="test-junit-report-export/assertions/success"/>
</testsuite>
<testsuite name="test-junit-report-export/assertions/failure" time="time" timestamp="timestamp" tests="5" failures="2" errors="0">
<testcase name="Simulating failure - Status code is 200 - Expected '200' to not be '200'" classname="test-junit-report-export/assertions/failure">
<failure type="AssertionFailure" message="Expected '200' to not be '200'"/>
</testcase>
<testcase name="Simulating failure - Check headers - Expected 'application/json, text/plain, */*,image/webp' to not be 'application/json, text/plain, */*'" classname="test-junit-report-export/assertions/failure"/>
<testcase name="Simulating failure - Check headers - Expected 'echo.hoppscotch.io' to not be 'httpbin.org'" classname="test-junit-report-export/assertions/failure"/>
<testcase name="Simulating failure - Check headers - Expected 'undefined' to not be 'value'" classname="test-junit-report-export/assertions/failure"/>
<testcase name="Simulating failure - Status code is 2xx - Expected '200' to not be 200-level status" classname="test-junit-report-export/assertions/failure">
<failure type="AssertionFailure" message="Expected '200' to not be 200-level status"/>
</testcase>
</testsuite>
</testsuites>" </testsuites>"
`; `;
@@ -501,14 +501,6 @@ exports[`hopp test [options] <file_path_or_id> > Test\`hopp test <file_path_or_i
exports[`hopp test [options] <file_path_or_id> > Test\`hopp test <file_path_or_id> --env <file_path_or_id> --reporter-junit [path] > Generates a JUnit report for a collection with authorization/headers set at the collection level 1`] = ` exports[`hopp test [options] <file_path_or_id> > Test\`hopp test <file_path_or_id> --env <file_path_or_id> --reporter-junit [path] > Generates a JUnit report for a collection with authorization/headers set at the collection level 1`] = `
"<?xml version="1.0" encoding="UTF-8"?> "<?xml version="1.0" encoding="UTF-8"?>
<testsuites tests="12" failures="0" errors="0" time="time"> <testsuites tests="12" failures="0" errors="0" time="time">
<testsuite name="CollectionB/RequestA" time="time" timestamp="timestamp" tests="2" failures="0" errors="0">
<testcase name="Correctly inherits auth and headers from the root collection - Expected 'Set at root collection' to be 'Set at root collection'" classname="CollectionB/RequestA"/>
<testcase name="Correctly inherits auth and headers from the root collection - Expected 'Bearer BearerToken' to be 'Bearer BearerToken'" classname="CollectionB/RequestA"/>
</testsuite>
<testsuite name="CollectionB/FolderA/RequestB" time="time" timestamp="timestamp" tests="2" failures="0" errors="0">
<testcase name="Correctly inherits auth and headers from the parent folder - Expected 'Set at root collection' to be 'Set at root collection'" classname="CollectionB/FolderA/RequestB"/>
<testcase name="Correctly inherits auth and headers from the parent folder - Expected 'Bearer BearerToken' to be 'Bearer BearerToken'" classname="CollectionB/FolderA/RequestB"/>
</testsuite>
<testsuite name="CollectionA/RequestA" time="time" timestamp="timestamp" tests="2" failures="0" errors="0"> <testsuite name="CollectionA/RequestA" time="time" timestamp="timestamp" tests="2" failures="0" errors="0">
<testcase name="Correctly inherits auth and headers from the root collection - Expected 'Set at root collection' to be 'Set at root collection'" classname="CollectionA/RequestA"/> <testcase name="Correctly inherits auth and headers from the root collection - Expected 'Set at root collection' to be 'Set at root collection'" classname="CollectionA/RequestA"/>
<testcase name="Correctly inherits auth and headers from the root collection - Expected 'Bearer BearerToken' to be 'Bearer BearerToken'" classname="CollectionA/RequestA"/> <testcase name="Correctly inherits auth and headers from the root collection - Expected 'Bearer BearerToken' to be 'Bearer BearerToken'" classname="CollectionA/RequestA"/>
@@ -525,5 +517,13 @@ exports[`hopp test [options] <file_path_or_id> > Test\`hopp test <file_path_or_i
<testcase name="Overrides auth and headers set at the parent folder - Expected 'Overriden at RequestD' to be 'Overriden at RequestD'" classname="CollectionA/FolderA/FolderB/FolderC/RequestD"/> <testcase name="Overrides auth and headers set at the parent folder - Expected 'Overriden at RequestD' to be 'Overriden at RequestD'" classname="CollectionA/FolderA/FolderB/FolderC/RequestD"/>
<testcase name="Overrides auth and headers set at the parent folder - Expected 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=' to be 'Basic dXNlcm5hbWU6cGFzc3dvcmQ='" classname="CollectionA/FolderA/FolderB/FolderC/RequestD"/> <testcase name="Overrides auth and headers set at the parent folder - Expected 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=' to be 'Basic dXNlcm5hbWU6cGFzc3dvcmQ='" classname="CollectionA/FolderA/FolderB/FolderC/RequestD"/>
</testsuite> </testsuite>
<testsuite name="CollectionB/RequestA" time="time" timestamp="timestamp" tests="2" failures="0" errors="0">
<testcase name="Correctly inherits auth and headers from the root collection - Expected 'Set at root collection' to be 'Set at root collection'" classname="CollectionB/RequestA"/>
<testcase name="Correctly inherits auth and headers from the root collection - Expected 'Bearer BearerToken' to be 'Bearer BearerToken'" classname="CollectionB/RequestA"/>
</testsuite>
<testsuite name="CollectionB/FolderA/RequestB" time="time" timestamp="timestamp" tests="2" failures="0" errors="0">
<testcase name="Correctly inherits auth and headers from the parent folder - Expected 'Set at root collection' to be 'Set at root collection'" classname="CollectionB/FolderA/RequestB"/>
<testcase name="Correctly inherits auth and headers from the parent folder - Expected 'Bearer BearerToken' to be 'Bearer BearerToken'" classname="CollectionB/FolderA/RequestB"/>
</testsuite>
</testsuites>" </testsuites>"
`; `;

View File

@@ -1,7 +1,7 @@
import { ExecException } from "child_process"; import { ExecException } from "child_process";
import { afterAll, beforeAll, describe, expect, test } from "vitest";
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import { afterAll, beforeAll, describe, expect, test } from "vitest";
import { HoppErrorCode } from "../../../types/errors"; import { HoppErrorCode } from "../../../types/errors";
import { getErrorCode, getTestJsonFilePath, runCLI } from "../../utils"; import { getErrorCode, getTestJsonFilePath, runCLI } from "../../utils";
@@ -181,6 +181,45 @@ describe("hopp test [options] <file_path_or_id>", { timeout: 100000 }, () => {
}); });
}); });
test("Ensures tests run in sequence order based on request path", async () => {
// Expected order of collection runs
const expectedOrder = [
"root-collection-request",
"folder-1/folder-1-request",
"folder-1/folder-11/folder-11-request",
"folder-1/folder-12/folder-12-request",
"folder-1/folder-13/folder-13-request",
"folder-2/folder-2-request",
"folder-2/folder-21/folder-21-request",
"folder-2/folder-22/folder-22-request",
"folder-2/folder-23/folder-23-request",
"folder-3/folder-3-request",
"folder-3/folder-31/folder-31-request",
"folder-3/folder-32/folder-32-request",
"folder-3/folder-33/folder-33-request",
];
const normalizePath = (path: string) => path.replace(/\\/g, "/");
const extractRunningOrder = (stdout: string): string[] =>
[...stdout.matchAll(/Running:.*?\/(.*?)\r?\n/g)].map(
([, path]) => normalizePath(path.replace(/\x1b\[\d+m/g, "")) // Remove ANSI codes and normalize paths
);
const args = `test ${getTestJsonFilePath(
"multiple-child-collections-auth-headers-coll.json",
"collection"
)}`;
const { stdout, error } = await runCLI(args);
// Verify the actual order matches the expected order
expect(extractRunningOrder(stdout)).toStrictEqual(expectedOrder);
// Ensure no errors occurred
expect(error).toBeNull();
});
describe("Test `hopp test <file_path_or_id> --env <file_path_or_id>` command:", () => { describe("Test `hopp test <file_path_or_id> --env <file_path_or_id>` command:", () => {
describe("Supplied environment export file validations", () => { describe("Supplied environment export file validations", () => {
describe("Argument parsing", () => { describe("Argument parsing", () => {
@@ -235,12 +274,12 @@ describe("hopp test [options] <file_path_or_id>", { timeout: 100000 }, () => {
}); });
test("Successfully resolves values from the supplied environment export file", async () => { test("Successfully resolves values from the supplied environment export file", async () => {
const TESTS_PATH = getTestJsonFilePath( const COLL_PATH = getTestJsonFilePath(
"env-flag-tests-coll.json", "env-flag-tests-coll.json",
"collection" "collection"
); );
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json", "environment"); const ENV_PATH = getTestJsonFilePath("env-flag-envs.json", "environment");
const args = `test ${TESTS_PATH} --env ${ENV_PATH}`; const args = `test ${COLL_PATH} --env ${ENV_PATH}`;
const { error } = await runCLI(args); const { error } = await runCLI(args);
expect(error).toBeNull(); expect(error).toBeNull();
@@ -251,23 +290,23 @@ describe("hopp test [options] <file_path_or_id>", { timeout: 100000 }, () => {
"req-body-env-vars-coll.json", "req-body-env-vars-coll.json",
"collection" "collection"
); );
const ENVS_PATH = getTestJsonFilePath( const ENV_PATH = getTestJsonFilePath(
"req-body-env-vars-envs.json", "req-body-env-vars-envs.json",
"environment" "environment"
); );
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`; const args = `test ${COLL_PATH} --env ${ENV_PATH}`;
const { error } = await runCLI(args); const { error } = await runCLI(args);
expect(error).toBeNull(); expect(error).toBeNull();
}); });
test("Works with short `-e` flag", async () => { test("Works with short `-e` flag", async () => {
const TESTS_PATH = getTestJsonFilePath( const COLL_PATH = getTestJsonFilePath(
"env-flag-tests-coll.json", "env-flag-tests-coll.json",
"collection" "collection"
); );
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json", "environment"); const ENV_PATH = getTestJsonFilePath("env-flag-envs.json", "environment");
const args = `test ${TESTS_PATH} -e ${ENV_PATH}`; const args = `test ${COLL_PATH} -e ${ENV_PATH}`;
const { error } = await runCLI(args); const { error } = await runCLI(args);
expect(error).toBeNull(); expect(error).toBeNull();
@@ -290,11 +329,8 @@ describe("hopp test [options] <file_path_or_id>", { timeout: 100000 }, () => {
"secret-envs-coll.json", "secret-envs-coll.json",
"collection" "collection"
); );
const ENVS_PATH = getTestJsonFilePath( const ENV_PATH = getTestJsonFilePath("secret-envs.json", "environment");
"secret-envs.json", const args = `test ${COLL_PATH} --env ${ENV_PATH}`;
"environment"
);
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
const { error, stdout } = await runCLI(args, { env }); const { error, stdout } = await runCLI(args, { env });
@@ -310,11 +346,11 @@ describe("hopp test [options] <file_path_or_id>", { timeout: 100000 }, () => {
"secret-envs-coll.json", "secret-envs-coll.json",
"collection" "collection"
); );
const ENVS_PATH = getTestJsonFilePath( const ENV_PATH = getTestJsonFilePath(
"secret-supplied-values-envs.json", "secret-supplied-values-envs.json",
"environment" "environment"
); );
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`; const args = `test ${COLL_PATH} --env ${ENV_PATH}`;
const { error, stdout } = await runCLI(args); const { error, stdout } = await runCLI(args);
@@ -330,11 +366,11 @@ describe("hopp test [options] <file_path_or_id>", { timeout: 100000 }, () => {
"secret-envs-persistence-coll.json", "secret-envs-persistence-coll.json",
"collection" "collection"
); );
const ENVS_PATH = getTestJsonFilePath( const ENV_PATH = getTestJsonFilePath(
"secret-supplied-values-envs.json", "secret-supplied-values-envs.json",
"environment" "environment"
); );
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`; const args = `test ${COLL_PATH} --env ${ENV_PATH}`;
const { error, stdout } = await runCLI(args); const { error, stdout } = await runCLI(args);
@@ -349,12 +385,12 @@ describe("hopp test [options] <file_path_or_id>", { timeout: 100000 }, () => {
"secret-envs-persistence-scripting-coll.json", "secret-envs-persistence-scripting-coll.json",
"collection" "collection"
); );
const ENVS_PATH = getTestJsonFilePath( const ENV_PATH = getTestJsonFilePath(
"secret-envs-persistence-scripting-envs.json", "secret-envs-persistence-scripting-envs.json",
"environment" "environment"
); );
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`; const args = `test ${COLL_PATH} --env ${ENV_PATH}`;
const { error } = await runCLI(args); const { error } = await runCLI(args);
expect(error).toBeNull(); expect(error).toBeNull();
@@ -372,12 +408,12 @@ describe("hopp test [options] <file_path_or_id>", { timeout: 100000 }, () => {
"request-vars-coll.json", "request-vars-coll.json",
"collection" "collection"
); );
const ENVS_PATH = getTestJsonFilePath( const ENV_PATH = getTestJsonFilePath(
"request-vars-envs.json", "request-vars-envs.json",
"environment" "environment"
); );
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`; const args = `test ${COLL_PATH} --env ${ENV_PATH}`;
const { error, stdout } = await runCLI(args, { env }); const { error, stdout } = await runCLI(args, { env });
expect(stdout).toContain( expect(stdout).toContain(
@@ -399,12 +435,12 @@ describe("hopp test [options] <file_path_or_id>", { timeout: 100000 }, () => {
"aws-signature-auth-coll.json", "aws-signature-auth-coll.json",
"collection" "collection"
); );
const ENVS_PATH = getTestJsonFilePath( const ENV_PATH = getTestJsonFilePath(
"aws-signature-auth-envs.json", "aws-signature-auth-envs.json",
"environment" "environment"
); );
const args = `test ${COLL_PATH} -e ${ENVS_PATH}`; const args = `test ${COLL_PATH} -e ${ENV_PATH}`;
const { error } = await runCLI(args, { env }); const { error } = await runCLI(args, { env });
expect(error).toBeNull(); expect(error).toBeNull();
@@ -417,14 +453,13 @@ describe("hopp test [options] <file_path_or_id>", { timeout: 100000 }, () => {
"digest-auth-success-coll.json", "digest-auth-success-coll.json",
"collection" "collection"
); );
const ENVS_PATH = getTestJsonFilePath( const ENV_PATH = getTestJsonFilePath(
"digest-auth-envs.json", "digest-auth-envs.json",
"environment" "environment"
); );
const args = `test ${COLL_PATH} -e ${ENVS_PATH}`; const args = `test ${COLL_PATH} -e ${ENV_PATH}`;
const { error } = await runCLI(args); const { error } = await runCLI(args);
expect(error).toBeNull(); expect(error).toBeNull();
}); });
}); });
@@ -434,12 +469,12 @@ describe("hopp test [options] <file_path_or_id>", { timeout: 100000 }, () => {
"digest-auth-failure-coll.json", "digest-auth-failure-coll.json",
"collection" "collection"
); );
const ENVS_PATH = getTestJsonFilePath( const ENV_PATH = getTestJsonFilePath(
"digest-auth-envs.json", "digest-auth-envs.json",
"environment" "environment"
); );
const args = `test ${COLL_PATH} -e ${ENVS_PATH}`; const args = `test ${COLL_PATH} -e ${ENV_PATH}`;
const { error } = await runCLI(args); const { error } = await runCLI(args);
expect(error).toBeTruthy(); expect(error).toBeTruthy();
@@ -583,11 +618,11 @@ describe("hopp test [options] <file_path_or_id>", { timeout: 100000 }, () => {
}); });
test("Supports specifying collection file path along with environment ID", async () => { test("Supports specifying collection file path along with environment ID", async () => {
const TESTS_PATH = getTestJsonFilePath( const COLL_PATH = getTestJsonFilePath(
"req-body-env-vars-coll.json", "req-body-env-vars-coll.json",
"collection" "collection"
); );
const args = `test ${TESTS_PATH} --env ${REQ_BODY_ENV_VARS_ENVS_ID} --token ${PERSONAL_ACCESS_TOKEN} --server ${SERVER_URL}`; const args = `test ${COLL_PATH} --env ${REQ_BODY_ENV_VARS_ENVS_ID} --token ${PERSONAL_ACCESS_TOKEN} --server ${SERVER_URL}`;
const { error } = await runCLI(args); const { error } = await runCLI(args);
expect(error).toBeNull(); expect(error).toBeNull();
@@ -605,7 +640,7 @@ describe("hopp test [options] <file_path_or_id>", { timeout: 100000 }, () => {
}); });
test("Supports specifying both collection and environment file paths", async () => { test("Supports specifying both collection and environment file paths", async () => {
const TESTS_PATH = getTestJsonFilePath( const COLL_PATH = getTestJsonFilePath(
"req-body-env-vars-coll.json", "req-body-env-vars-coll.json",
"collection" "collection"
); );
@@ -613,7 +648,7 @@ describe("hopp test [options] <file_path_or_id>", { timeout: 100000 }, () => {
"req-body-env-vars-envs.json", "req-body-env-vars-envs.json",
"environment" "environment"
); );
const args = `test ${TESTS_PATH} --env ${ENV_PATH} --token ${PERSONAL_ACCESS_TOKEN}`; const args = `test ${COLL_PATH} --env ${ENV_PATH} --token ${PERSONAL_ACCESS_TOKEN}`;
const { error } = await runCLI(args); const { error } = await runCLI(args);
expect(error).toBeNull(); expect(error).toBeNull();
@@ -644,9 +679,10 @@ describe("hopp test [options] <file_path_or_id>", { timeout: 100000 }, () => {
const COLL_PATH = getTestJsonFilePath("passes-coll.json", "collection"); const COLL_PATH = getTestJsonFilePath("passes-coll.json", "collection");
const invalidPath = process.platform === 'win32' const invalidPath =
? 'Z:/non-existent-path/report.xml' process.platform === "win32"
: '/non-existent/report.xml'; ? "Z:/non-existent-path/report.xml"
: "/non-existent/report.xml";
const args = `test ${COLL_PATH} --reporter-junit ${invalidPath}`; const args = `test ${COLL_PATH} --reporter-junit ${invalidPath}`;
@@ -782,4 +818,139 @@ describe("hopp test [options] <file_path_or_id>", { timeout: 100000 }, () => {
expect(replaceDynamicValuesInStr(fileContents)).toMatchSnapshot(); expect(replaceDynamicValuesInStr(fileContents)).toMatchSnapshot();
}); });
}); });
describe("Test `hopp test <file> --iteration-count <no_of_iterations>` command:", () => {
const VALID_TEST_ARGS = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
test("Errors with the code `INVALID_ARGUMENT` on not supplying an iteration count", async () => {
const args = `${VALID_TEST_ARGS} --iteration-count`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
test("Errors with the code `INVALID_ARGUMENT` on supplying an invalid iteration count", async () => {
const args = `${VALID_TEST_ARGS} --iteration-count NaN`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
test("Errors with the code `INVALID_ARGUMENT` on supplying an iteration count below `1`", async () => {
const args = `${VALID_TEST_ARGS} --iteration-count -5`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
test("Successfully executes all requests in the collection iteratively based on the specified iteration count", async () => {
const iterationCount = 3;
const args = `${VALID_TEST_ARGS} --iteration-count ${iterationCount}`;
const { error, stdout } = await runCLI(args);
// Logs iteration count in each pass
Array.from({ length: 3 }).forEach((_, idx) =>
expect(stdout).include(`Iteration: ${idx + 1}/${iterationCount}`)
);
expect(error).toBeNull();
});
test("Doesn't log iteration count if the value supplied is `1`", async () => {
const args = `${VALID_TEST_ARGS} --iteration-count 1`;
const { error, stdout } = await runCLI(args);
expect(stdout).not.include(`Iteration: 1/1`);
expect(error).toBeNull();
});
});
describe("Test `hopp test <file> --iteration-data <file_path>` command:", () => {
describe("Supplied data export file validations", () => {
const VALID_TEST_ARGS = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
test("Errors with the code `INVALID_ARGUMENT` if no file is supplied", async () => {
const args = `${VALID_TEST_ARGS} --iteration-data`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
test("Errors with the code `INVALID_DATA_FILE_TYPE` if the supplied data file doesn't end with the `.csv` extension", async () => {
const args = `${VALID_TEST_ARGS} --iteration-data ${getTestJsonFilePath(
"notjson-coll.txt",
"collection"
)}`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_DATA_FILE_TYPE");
});
test("Errors with the code `FILE_NOT_FOUND` if the supplied data export file doesn't exist", async () => {
const args = `${VALID_TEST_ARGS} --iteration-data notfound.csv`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
});
});
test("Prioritizes values from the supplied data export file over environment variables with relevant fallbacks for missing entries", async () => {
const COLL_PATH = getTestJsonFilePath(
"iteration-data-tests-coll.json",
"collection"
);
const ITERATION_DATA_PATH = getTestJsonFilePath(
"iteration-data-export.csv",
"environment"
);
const ENV_PATH = getTestJsonFilePath(
"iteration-data-envs.json",
"environment"
);
const args = `test ${COLL_PATH} --iteration-data ${ITERATION_DATA_PATH} -e ${ENV_PATH}`;
const { error, stdout } = await runCLI(args);
const iterationCount = 3;
// Even though iteration count is not supplied, it will be inferred from the iteration data size
Array.from({ length: iterationCount }).forEach((_, idx) =>
expect(stdout).include(`Iteration: ${idx + 1}/${iterationCount}`)
);
expect(error).toBeNull();
});
test("Iteration count takes priority if supplied instead of inferring from the iteration data size", async () => {
const COLL_PATH = getTestJsonFilePath(
"iteration-data-tests-coll.json",
"collection"
);
const ITERATION_DATA_PATH = getTestJsonFilePath(
"iteration-data-export.csv",
"environment"
);
const ENV_PATH = getTestJsonFilePath(
"iteration-data-envs.json",
"environment"
);
const iterationCount = 5;
const args = `test ${COLL_PATH} --iteration-data ${ITERATION_DATA_PATH} -e ${ENV_PATH} --iteration-count ${iterationCount}`;
const { error, stdout } = await runCLI(args);
Array.from({ length: iterationCount }).forEach((_, idx) =>
expect(stdout).include(`Iteration: ${idx + 1}/${iterationCount}`)
);
expect(error).toBeNull();
});
});
}); });

View File

@@ -0,0 +1,23 @@
{
"v": 1,
"name": "iteration-data-tests-coll",
"folders": [],
"requests": [
{
"v": "3",
"endpoint": "<<URL>>",
"name": "test1",
"params": [],
"headers": [],
"method": "POST",
"auth": { "authType": "none", "authActive": true },
"preRequestScript": "",
"testScript": "// Iteration data is prioritised over environment variables \n const { data, headers } = pw.response.body;\n pw.expect(headers['host']).toBe('echo.hoppscotch.io')\n // Falls back to environment variables for missing entries in data export\n pw.expect(data).toInclude('overriden-body-key-at-environment')\n pw.expect(data).toInclude('body_value')",
"body": {
"contentType": "application/json",
"body": "{\n \"<<BODY_KEY>>\":\"<<BODY_VALUE>>\"\n}"
},
"requestVariables": []
}
]
}

View File

@@ -0,0 +1,18 @@
{
"v": 0,
"name": "Iteration data environments",
"variables": [
{
"key": "URL",
"value": "https://httpbin.org/get"
},
{
"key": "BODY_KEY",
"value": "overriden-body-key-at-environment"
},
{
"key": "BODY_VALUE",
"value": "overriden-body-value-at-environment"
}
]
}

View File

@@ -0,0 +1,4 @@
URL,BODY_KEY,BODY_VALUE
https://echo.hoppscotch.io/1,,body_value1
https://echo.hoppscotch.io/2,,body_value2
https://echo.hoppscotch.io/3,,body_value3
1 URL BODY_KEY BODY_VALUE
2 https://echo.hoppscotch.io/1 body_value1
3 https://echo.hoppscotch.io/2 body_value2
4 https://echo.hoppscotch.io/3 body_value3

View File

@@ -1,7 +1,14 @@
import fs from "fs";
import { isSafeInteger } from "lodash-es";
import Papa from "papaparse";
import path from "path";
import { handleError } from "../handlers/error"; import { handleError } from "../handlers/error";
import { parseDelayOption } from "../options/test/delay"; import { parseDelayOption } from "../options/test/delay";
import { parseEnvsData } from "../options/test/env"; import { parseEnvsData } from "../options/test/env";
import { IterationDataItem } from "../types/collections";
import { TestCmdEnvironmentOptions, TestCmdOptions } from "../types/commands"; import { TestCmdEnvironmentOptions, TestCmdOptions } from "../types/commands";
import { error } from "../types/errors";
import { HoppEnvs } from "../types/request"; import { HoppEnvs } from "../types/request";
import { isHoppCLIError } from "../utils/checks"; import { isHoppCLIError } from "../utils/checks";
import { import {
@@ -13,16 +20,79 @@ import { parseCollectionData } from "../utils/mutators";
export const test = (pathOrId: string, options: TestCmdOptions) => async () => { export const test = (pathOrId: string, options: TestCmdOptions) => async () => {
try { try {
const delay = options.delay ? parseDelayOption(options.delay) : 0; const { delay, env, iterationCount, iterationData, reporterJunit } =
options;
const envs = options.env if (
iterationCount !== undefined &&
(iterationCount < 1 || !isSafeInteger(iterationCount))
) {
throw error({
code: "INVALID_ARGUMENT",
data: "The value must be a positive integer",
});
}
const resolvedDelay = delay ? parseDelayOption(delay) : 0;
const envs = env
? await parseEnvsData(options as TestCmdEnvironmentOptions) ? await parseEnvsData(options as TestCmdEnvironmentOptions)
: <HoppEnvs>{ global: [], selected: [] }; : <HoppEnvs>{ global: [], selected: [] };
let parsedIterationData: unknown[] | null = null;
let transformedIterationData: IterationDataItem[][] | undefined;
const collections = await parseCollectionData(pathOrId, options); const collections = await parseCollectionData(pathOrId, options);
const report = await collectionsRunner({ collections, envs, delay }); if (iterationData) {
const hasSucceeded = collectionsRunnerResult(report, options.reporterJunit); // Check file existence
if (!fs.existsSync(iterationData)) {
throw error({ code: "FILE_NOT_FOUND", path: iterationData });
}
// Check the file extension
if (path.extname(iterationData) !== ".csv") {
throw error({
code: "INVALID_DATA_FILE_TYPE",
data: iterationData,
});
}
const csvData = fs.readFileSync(iterationData, "utf8");
parsedIterationData = Papa.parse(csvData, { header: true }).data;
// Transform data into the desired format
transformedIterationData = parsedIterationData
.map((item) => {
const iterationDataItem = item as Record<string, unknown>;
const keys = Object.keys(iterationDataItem);
return (
keys
// Ignore keys with empty string values
.filter((key) => iterationDataItem[key] !== "")
.map(
(key) =>
<IterationDataItem>{
key: key,
value: iterationDataItem[key],
secret: false,
}
)
);
})
// Ignore items that result in an empty array
.filter((item) => item.length > 0);
}
const report = await collectionsRunner({
collections,
envs,
delay: resolvedDelay,
iterationData: transformedIterationData,
iterationCount,
});
const hasSucceeded = collectionsRunnerResult(report, reporterJunit);
collectionsRunnerExit(hasSucceeded); collectionsRunnerExit(hasSucceeded);
} catch (e) { } catch (e) {

View File

@@ -65,6 +65,9 @@ export const handleError = <T extends HoppErrorCode>(error: HoppError<T>) => {
case "INVALID_FILE_TYPE": case "INVALID_FILE_TYPE":
ERROR_MSG = `Please provide file of extension type .json: ${error.data}`; ERROR_MSG = `Please provide file of extension type .json: ${error.data}`;
break; break;
case "INVALID_DATA_FILE_TYPE":
ERROR_MSG = `Please provide file of extension type .csv: ${error.data}`;
break;
case "REQUEST_ERROR": case "REQUEST_ERROR":
case "TEST_SCRIPT_ERROR": case "TEST_SCRIPT_ERROR":
case "PRE_REQUEST_SCRIPT_ERROR": case "PRE_REQUEST_SCRIPT_ERROR":

View File

@@ -69,6 +69,15 @@ program
"--reporter-junit [path]", "--reporter-junit [path]",
"generate JUnit report optionally specifying the path" "generate JUnit report optionally specifying the path"
) )
.option(
"--iteration-count <no_of_iterations>",
"number of iterations to run the test",
parseInt
)
.option(
"--iteration-data <file_path>",
"path to a CSV file for data-driven testing"
)
.allowExcessArguments(false) .allowExcessArguments(false)
.allowUnknownOption(false) .allowUnknownOption(false)
.description("running hoppscotch collection.json file") .description("running hoppscotch collection.json file")

View File

@@ -1,10 +1,15 @@
import { HoppCollection } from "@hoppscotch/data"; import { HoppCollection } from "@hoppscotch/data";
import { HoppEnvs } from "./request"; import { HoppEnvPair, HoppEnvs } from "./request";
export type CollectionRunnerParam = { export type CollectionRunnerParam = {
collections: HoppCollection[]; collections: HoppCollection[];
envs: HoppEnvs; envs: HoppEnvs;
delay?: number; delay?: number;
iterationData?: IterationDataItem[][];
iterationCount?: number;
}; };
export type HoppCollectionFileExt = "json"; export type HoppCollectionFileExt = "json";
// Indicates the shape each iteration data entry gets transformed into
export type IterationDataItem = Extract<HoppEnvPair, { value: string }>;

View File

@@ -4,6 +4,8 @@ export type TestCmdOptions = {
token?: string; token?: string;
server?: string; server?: string;
reporterJunit?: string; reporterJunit?: string;
iterationCount?: number;
iterationData?: string;
}; };
// Consumed in the collection `file_path_or_id` argument action handler // Consumed in the collection `file_path_or_id` argument action handler

View File

@@ -26,6 +26,7 @@ type HoppErrors = {
MALFORMED_ENV_FILE: HoppErrorPath & HoppErrorData; MALFORMED_ENV_FILE: HoppErrorPath & HoppErrorData;
BULK_ENV_FILE: HoppErrorPath & HoppErrorData; BULK_ENV_FILE: HoppErrorPath & HoppErrorData;
INVALID_FILE_TYPE: HoppErrorData; INVALID_FILE_TYPE: HoppErrorData;
INVALID_DATA_FILE_TYPE: HoppErrorData;
TOKEN_EXPIRED: HoppErrorData; TOKEN_EXPIRED: HoppErrorData;
TOKEN_INVALID: HoppErrorData; TOKEN_INVALID: HoppErrorData;
INVALID_ID: HoppErrorData; INVALID_ID: HoppErrorData;

View File

@@ -18,7 +18,7 @@ export type HoppEnvs = {
selected: HoppEnvPair[]; selected: HoppEnvPair[];
}; };
export type CollectionStack = { export type CollectionQueue = {
path: string; path: string;
collection: HoppCollection; collection: HoppCollection;
}; };

View File

@@ -7,7 +7,7 @@ import { round } from "lodash-es";
import { CollectionRunnerParam } from "../types/collections"; import { CollectionRunnerParam } from "../types/collections";
import { import {
CollectionStack, CollectionQueue,
HoppEnvs, HoppEnvs,
ProcessRequestParams, ProcessRequestParams,
RequestReport, RequestReport,
@@ -35,7 +35,7 @@ import {
} from "./request"; } from "./request";
import { getTestMetrics } from "./test"; import { getTestMetrics } from "./test";
const { WARN, FAIL } = exceptionColors; const { WARN, FAIL, INFO } = exceptionColors;
/** /**
* Processes each requests within collections to prints details of subsequent requests, * Processes each requests within collections to prints details of subsequent requests,
@@ -43,93 +43,134 @@ const { WARN, FAIL } = exceptionColors;
* @param param Data of hopp-collection with hopp-requests, envs to be processed. * @param param Data of hopp-collection with hopp-requests, envs to be processed.
* @returns List of report for each processed request. * @returns List of report for each processed request.
*/ */
export const collectionsRunner = async ( export const collectionsRunner = async (
param: CollectionRunnerParam param: CollectionRunnerParam
): Promise<RequestReport[]> => { ): Promise<RequestReport[]> => {
const envs: HoppEnvs = param.envs; const { collections, envs, delay, iterationCount, iterationData } = param;
const delay = param.delay ?? 0;
const resolvedDelay = delay ?? 0;
const requestsReport: RequestReport[] = []; const requestsReport: RequestReport[] = [];
const collectionStack: CollectionStack[] = getCollectionStack( const collectionQueue = getCollectionQueue(collections);
param.collections
);
while (collectionStack.length) { // If iteration count is not supplied, it should be based on the size of iteration data if in scope
// Pop out top-most collection from stack to be processed. const resolvedIterationCount = iterationCount ?? iterationData?.length ?? 1;
const { collection, path } = <CollectionStack>collectionStack.pop();
// Processing each request in collection const originalSelectedEnvs = [...envs.selected];
for (const request of collection.requests) {
const _request = preProcessRequest(
request as HoppRESTRequest,
collection
);
const requestPath = `${path}/${_request.name}`;
const processRequestParams: ProcessRequestParams = {
path: requestPath,
request: _request,
envs,
delay,
};
// Request processing initiated message. for (let count = 0; count < resolvedIterationCount; count++) {
log(WARN(`\nRunning: ${chalk.bold(requestPath)}`)); if (resolvedIterationCount > 1) {
log(INFO(`\nIteration: ${count + 1}/${resolvedIterationCount}`));
// Processing current request.
const result = await processRequest(processRequestParams)();
// Updating global & selected envs with new envs from processed-request output.
const { global, selected } = result.envs;
envs.global = global;
envs.selected = selected;
// Storing current request's report.
const requestReport = result.report;
requestsReport.push(requestReport);
} }
// Pushing remaining folders realted collection to stack. // Reset `envs` to the original value at the start of each iteration
for (const folder of collection.folders) { envs.selected = [...originalSelectedEnvs];
const updatedFolder: HoppCollection = { ...folder };
if (updatedFolder.auth?.authType === "inherit") { if (iterationData) {
updatedFolder.auth = collection.auth; // Ensure last item is picked if the iteration count exceeds size of the iteration data
} const iterationDataItem =
iterationData[Math.min(count, iterationData.length - 1)];
if (collection.headers?.length) { // Ensure iteration data takes priority over supplied environment variables
// Filter out header entries present in the parent collection under the same name envs.selected = envs.selected
// This ensures the folder headers take precedence over the collection headers .filter(
const filteredHeaders = collection.headers.filter( (envPair) =>
(collectionHeaderEntries) => { !iterationDataItem.some((dataPair) => dataPair.key === envPair.key)
return !updatedFolder.headers.some( )
(folderHeaderEntries) => .concat(iterationDataItem);
folderHeaderEntries.key === collectionHeaderEntries.key }
);
}
);
updatedFolder.headers.push(...filteredHeaders);
}
collectionStack.push({ for (const { collection, path } of collectionQueue) {
path: `${path}/${updatedFolder.name}`, await processCollection(
collection: updatedFolder, collection,
}); path,
envs,
resolvedDelay,
requestsReport
);
} }
} }
return requestsReport; return requestsReport;
}; };
const processCollection = async (
collection: HoppCollection,
path: string,
envs: HoppEnvs,
delay: number,
requestsReport: RequestReport[]
) => {
// Process each request in the collection
for (const request of collection.requests) {
const _request = preProcessRequest(request as HoppRESTRequest, collection);
const requestPath = `${path}/${_request.name}`;
const processRequestParams: ProcessRequestParams = {
path: requestPath,
request: _request,
envs,
delay,
};
// Request processing initiated message.
log(WARN(`\nRunning: ${chalk.bold(requestPath)}`));
// Processing current request.
const result = await processRequest(processRequestParams)();
// Updating global & selected envs with new envs from processed-request output.
const { global, selected } = result.envs;
envs.global = global;
envs.selected = selected;
// Storing current request's report.
const requestReport = result.report;
requestsReport.push(requestReport);
}
// Process each folder in the collection
for (const folder of collection.folders) {
const updatedFolder: HoppCollection = { ...folder };
if (updatedFolder.auth?.authType === "inherit") {
updatedFolder.auth = collection.auth;
}
if (collection.headers?.length) {
// Filter out header entries present in the parent collection under the same name
// This ensures the folder headers take precedence over the collection headers
const filteredHeaders = collection.headers.filter(
(collectionHeaderEntries) => {
return !updatedFolder.headers.some(
(folderHeaderEntries) =>
folderHeaderEntries.key === collectionHeaderEntries.key
);
}
);
updatedFolder.headers.push(...filteredHeaders);
}
await processCollection(
updatedFolder,
`${path}/${updatedFolder.name}`,
envs,
delay,
requestsReport
);
}
};
/** /**
* Transforms collections to generate collection-stack which describes each collection's * Transforms collections to generate collection-stack which describes each collection's
* path within collection & the collection itself. * path within collection & the collection itself.
* @param collections Hopp-collection objects to be mapped to collection-stack type. * @param collections Hopp-collection objects to be mapped to collection-stack type.
* @returns Mapped collections to collection-stack. * @returns Mapped collections to collection-stack.
*/ */
const getCollectionStack = (collections: HoppCollection[]): CollectionStack[] => const getCollectionQueue = (collections: HoppCollection[]): CollectionQueue[] =>
pipe( pipe(
collections, collections,
A.map( A.map(
(collection) => <CollectionStack>{ collection, path: collection.name } (collection) => <CollectionQueue>{ collection, path: collection.name }
) )
); );

18
pnpm-lock.yaml generated
View File

@@ -395,6 +395,9 @@ importers:
lodash-es: lodash-es:
specifier: 4.17.21 specifier: 4.17.21
version: 4.17.21 version: 4.17.21
papaparse:
specifier: 5.4.1
version: 5.4.1
qs: qs:
specifier: 6.13.0 specifier: 6.13.0
version: 6.13.0 version: 6.13.0
@@ -420,6 +423,9 @@ importers:
'@types/lodash-es': '@types/lodash-es':
specifier: 4.17.12 specifier: 4.17.12
version: 4.17.12 version: 4.17.12
'@types/papaparse':
specifier: 5.3.14
version: 5.3.14
'@types/qs': '@types/qs':
specifier: 6.9.16 specifier: 6.9.16
version: 6.9.16 version: 6.9.16
@@ -5148,6 +5154,9 @@ packages:
'@types/paho-mqtt@1.0.10': '@types/paho-mqtt@1.0.10':
resolution: {integrity: sha512-xOEii1m7jw7mIKjufDkolpz7VlyqptUmm/YFPtLJCybrPCuLhN+WYgNpulQ/CXo7wtEW7x4uGon2v89+6g/pcA==} resolution: {integrity: sha512-xOEii1m7jw7mIKjufDkolpz7VlyqptUmm/YFPtLJCybrPCuLhN+WYgNpulQ/CXo7wtEW7x4uGon2v89+6g/pcA==}
'@types/papaparse@5.3.14':
resolution: {integrity: sha512-LxJ4iEFcpqc6METwp9f6BV6VVc43m6MfH0VqFosHvrUgfXiFe6ww7R3itkOQ+TCK6Y+Iv/+RnnvtRZnkc5Kc9g==}
'@types/passport-github2@1.2.9': '@types/passport-github2@1.2.9':
resolution: {integrity: sha512-/nMfiPK2E6GKttwBzwj0Wjaot8eHrM57hnWxu52o6becr5/kXlH/4yE2v2rh234WGvSgEEzIII02Nc5oC5xEHA==} resolution: {integrity: sha512-/nMfiPK2E6GKttwBzwj0Wjaot8eHrM57hnWxu52o6becr5/kXlH/4yE2v2rh234WGvSgEEzIII02Nc5oC5xEHA==}
@@ -9691,6 +9700,9 @@ packages:
pako@1.0.11: pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
papaparse@5.4.1:
resolution: {integrity: sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==}
param-case@3.0.4: param-case@3.0.4:
resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==}
@@ -16956,6 +16968,10 @@ snapshots:
'@types/paho-mqtt@1.0.10': {} '@types/paho-mqtt@1.0.10': {}
'@types/papaparse@5.3.14':
dependencies:
'@types/node': 22.7.6
'@types/passport-github2@1.2.9': '@types/passport-github2@1.2.9':
dependencies: dependencies:
'@types/express': 5.0.0 '@types/express': 5.0.0
@@ -23374,6 +23390,8 @@ snapshots:
pako@1.0.11: {} pako@1.0.11: {}
papaparse@5.4.1: {}
param-case@3.0.4: param-case@3.0.4:
dependencies: dependencies:
dot-case: 3.0.4 dot-case: 3.0.4