diff --git a/README.md b/README.md index d1ff7e7ae..937a64238 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ _Customized themes are synced with cloud / local session_ -🔥 **PWA:** Install as a [PWA](https://developers.google.com/web/progressive-web-apps) on your device. +🔥 **PWA:** Install as a [PWA](https://web.dev/what-are-pwas/) on your device. - Instant loading with Service Workers - Offline support diff --git a/packages/hoppscotch-cli/package.json b/packages/hoppscotch-cli/package.json index b7eb100ce..9da4cd233 100644 --- a/packages/hoppscotch-cli/package.json +++ b/packages/hoppscotch-cli/package.json @@ -1,6 +1,6 @@ { "name": "@hoppscotch/cli", - "version": "0.2.1", + "version": "0.3.0", "description": "A CLI to run Hoppscotch test scripts in CI environments.", "homepage": "https://hoppscotch.io", "main": "dist/index.js", diff --git a/packages/hoppscotch-cli/src/__tests__/commands/test.spec.ts b/packages/hoppscotch-cli/src/__tests__/commands/test.spec.ts index 0b69f443b..a1e6861a5 100644 --- a/packages/hoppscotch-cli/src/__tests__/commands/test.spec.ts +++ b/packages/hoppscotch-cli/src/__tests__/commands/test.spec.ts @@ -92,12 +92,41 @@ describe("Test 'hopp test --env ' command:", () => { expect(out).toBe("FILE_NOT_FOUND"); }); - // test("No errors occured (exit code 0).", async () => { - // const TESTS_PATH = getTestJsonFilePath("env-flag-tests.json"); - // const ENV_PATH = getTestJsonFilePath("env-flag-envs.json"); - // const cmd = `node ./bin/hopp test ${TESTS_PATH} --env ${ENV_PATH}`; - // const { error } = await execAsync(cmd); + test("No errors occured (exit code 0).", async () => { + const TESTS_PATH = getTestJsonFilePath("env-flag-tests.json"); + const ENV_PATH = getTestJsonFilePath("env-flag-envs.json"); + const cmd = `node ./bin/hopp test ${TESTS_PATH} --env ${ENV_PATH}`; + const { error } = await execAsync(cmd); - // expect(error).toBeNull(); - // }); + expect(error).toBeNull(); + }); +}); + +describe("Test 'hopp test --delay ' command:", () => { + const VALID_TEST_CMD = `node ./bin/hopp test ${getTestJsonFilePath( + "passes.json" + )}`; + + test("No value passed to delay flag.", async () => { + const cmd = `${VALID_TEST_CMD} --delay`; + const { stdout } = await execAsync(cmd); + const out = getErrorCode(stdout); + + expect(out).toBe("INVALID_ARGUMENT"); + }); + + test("Invalid value passed to delay flag.", async () => { + const cmd = `${VALID_TEST_CMD} --delay 'NaN'`; + const { stdout } = await execAsync(cmd); + const out = getErrorCode(stdout); + + expect(out).toBe("INVALID_ARGUMENT"); + }); + + test("Valid value passed to delay flag.", async () => { + const cmd = `${VALID_TEST_CMD} --delay 1`; + const { error } = await execAsync(cmd); + + expect(error).toBeNull(); + }); }); diff --git a/packages/hoppscotch-cli/src/__tests__/functions/request/delayPromiseFunction.spec.ts b/packages/hoppscotch-cli/src/__tests__/functions/request/delayPromiseFunction.spec.ts new file mode 100644 index 000000000..5f6dd8493 --- /dev/null +++ b/packages/hoppscotch-cli/src/__tests__/functions/request/delayPromiseFunction.spec.ts @@ -0,0 +1,30 @@ +import { hrtime } from "process"; +import { getDurationInSeconds } from "../../../utils/getters"; +import { delayPromiseFunction } from "../../../utils/request"; + +describe("describePromiseFunction", () => { + let promiseFunc = (): Promise => new Promise((resolve) => resolve(2)); + beforeEach(() => { + promiseFunc = (): Promise => new Promise((resolve) => resolve(2)); + }); + + it("Should resolve the promise after 2 seconds.", async () => { + const start = hrtime(); + const res = await delayPromiseFunction(promiseFunc, 2000); + const end = hrtime(start); + const duration = getDurationInSeconds(end); + + expect(Math.floor(duration)).toEqual(2); + expect(typeof res).toBe("number"); + }); + + it("Should resolve the promise after 4 seconds.", async () => { + const start = hrtime(); + const res = await delayPromiseFunction(promiseFunc, 4000); + const end = hrtime(start); + const duration = getDurationInSeconds(end); + + expect(Math.floor(duration)).toEqual(4); + expect(typeof res).toBe("number"); + }); +}); diff --git a/packages/hoppscotch-cli/src/__tests__/functions/request/processRequest.spec.ts b/packages/hoppscotch-cli/src/__tests__/functions/request/processRequest.spec.ts index 2cc73c6cd..b2c9027a0 100644 --- a/packages/hoppscotch-cli/src/__tests__/functions/request/processRequest.spec.ts +++ b/packages/hoppscotch-cli/src/__tests__/functions/request/processRequest.spec.ts @@ -58,7 +58,12 @@ describe("processRequest", () => { (axios as unknown as jest.Mock).mockResolvedValue(DEFAULT_RESPONSE); return expect( - processRequest(SAMPLE_REQUEST, DEFAULT_ENVS, "fake/collection/path")() + processRequest({ + request: SAMPLE_REQUEST, + envs: DEFAULT_ENVS, + path: "fake/collection/path", + delay: 0, + })() ).resolves.toMatchObject({ report: { result: true, @@ -79,7 +84,12 @@ describe("processRequest", () => { (axios as unknown as jest.Mock).mockResolvedValue(DEFAULT_RESPONSE); return expect( - processRequest(SAMPLE_REQUEST, DEFAULT_ENVS, "fake/collection/path")() + processRequest({ + request: SAMPLE_REQUEST, + envs: DEFAULT_ENVS, + path: "fake/collection/path", + delay: 0, + })() ).resolves.toMatchObject({ envs: { selected: [{ key: "ENDPOINT", value: "https://example.com" }], @@ -96,7 +106,12 @@ describe("processRequest", () => { (axios as unknown as jest.Mock).mockResolvedValue(DEFAULT_RESPONSE); return expect( - processRequest(SAMPLE_REQUEST, DEFAULT_ENVS, "fake/request/path")() + processRequest({ + request: SAMPLE_REQUEST, + envs: DEFAULT_ENVS, + path: "fake/request/path", + delay: 0, + })() ).resolves.toMatchObject({ report: { result: false }, }); diff --git a/packages/hoppscotch-cli/src/commands/test.ts b/packages/hoppscotch-cli/src/commands/test.ts index 7159f4632..2f8d6500c 100644 --- a/packages/hoppscotch-cli/src/commands/test.ts +++ b/packages/hoppscotch-cli/src/commands/test.ts @@ -9,12 +9,14 @@ import { handleError } from "../handlers/error"; import { parseCollectionData } from "../utils/mutators"; import { parseEnvsData } from "../options/test/env"; import { TestCmdOptions } from "../types/commands"; +import { parseDelayOption } from "../options/test/delay"; export const test = (path: string, options: TestCmdOptions) => async () => { await pipe( TE.Do, TE.bind("envs", () => parseEnvsData(options.env)), TE.bind("collections", () => parseCollectionData(path)), + TE.bind("delay", () => parseDelayOption(options.delay)), TE.chainTaskK(collectionsRunner), TE.chainW(flow(collectionsRunnerResult, collectionsRunnerExit, TE.of)), TE.mapLeft((e) => { diff --git a/packages/hoppscotch-cli/src/index.ts b/packages/hoppscotch-cli/src/index.ts index d93248f68..aa6b0269f 100644 --- a/packages/hoppscotch-cli/src/index.ts +++ b/packages/hoppscotch-cli/src/index.ts @@ -50,6 +50,10 @@ program "path to a hoppscotch collection.json file for CI testing" ) .option("-e, --env ", "path to an environment variables json file") + .option( + "-d, --delay ", + "delay in milliseconds(ms) between consecutive requests within a collection" + ) .allowExcessArguments(false) .allowUnknownOption(false) .description("running hoppscotch collection.json file") diff --git a/packages/hoppscotch-cli/src/options/test/delay.ts b/packages/hoppscotch-cli/src/options/test/delay.ts new file mode 100644 index 000000000..939d0857d --- /dev/null +++ b/packages/hoppscotch-cli/src/options/test/delay.ts @@ -0,0 +1,20 @@ +import * as TE from "fp-ts/TaskEither"; +import * as S from "fp-ts/string"; +import { pipe } from "fp-ts/function"; +import { error, HoppCLIError } from "../../types/errors"; + +export const parseDelayOption = ( + delay: unknown +): TE.TaskEither => + !S.isString(delay) + ? TE.right(0) + : pipe( + delay, + Number, + TE.fromPredicate(Number.isSafeInteger, () => + error({ + code: "INVALID_ARGUMENT", + data: "Expected '-d, --delay' value to be number", + }) + ) + ); diff --git a/packages/hoppscotch-cli/src/types/collections.ts b/packages/hoppscotch-cli/src/types/collections.ts index 57794954f..0b341fc4b 100644 --- a/packages/hoppscotch-cli/src/types/collections.ts +++ b/packages/hoppscotch-cli/src/types/collections.ts @@ -4,6 +4,7 @@ import { HoppEnvs } from "./request"; export type CollectionRunnerParam = { collections: HoppCollection[]; envs: HoppEnvs; + delay?: number; }; export type HoppCollectionFileExt = "json"; diff --git a/packages/hoppscotch-cli/src/types/commands.ts b/packages/hoppscotch-cli/src/types/commands.ts index 59de3536c..459576bf0 100644 --- a/packages/hoppscotch-cli/src/types/commands.ts +++ b/packages/hoppscotch-cli/src/types/commands.ts @@ -1,5 +1,6 @@ export type TestCmdOptions = { env: string; + delay: number; }; export type HoppEnvFileExt = "json"; diff --git a/packages/hoppscotch-cli/src/types/request.ts b/packages/hoppscotch-cli/src/types/request.ts index 10b53a146..b969de8c8 100644 --- a/packages/hoppscotch-cli/src/types/request.ts +++ b/packages/hoppscotch-cli/src/types/request.ts @@ -26,3 +26,10 @@ export type RequestReport = { result: boolean; duration: { test: number; request: number; preRequest: number }; }; + +export type ProcessRequestParams = { + request: HoppRESTRequest; + envs: HoppEnvs; + path: string; + delay: number; +}; diff --git a/packages/hoppscotch-cli/src/utils/collections.ts b/packages/hoppscotch-cli/src/utils/collections.ts index 3bd8cd1f8..84e3806c5 100644 --- a/packages/hoppscotch-cli/src/utils/collections.ts +++ b/packages/hoppscotch-cli/src/utils/collections.ts @@ -5,7 +5,12 @@ import { bold } from "chalk"; import { log } from "console"; import round from "lodash/round"; import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"; -import { HoppEnvs, CollectionStack, RequestReport } from "../types/request"; +import { + HoppEnvs, + CollectionStack, + RequestReport, + ProcessRequestParams, +} from "../types/request"; import { getRequestMetrics, preProcessRequest, @@ -41,6 +46,7 @@ export const collectionsRunner = (param: CollectionRunnerParam): T.Task => async () => { const envs: HoppEnvs = param.envs; + const delay = param.delay ?? 0; const requestsReport: RequestReport[] = []; const collectionStack: CollectionStack[] = getCollectionStack( param.collections @@ -54,12 +60,18 @@ export const collectionsRunner = for (const request of collection.requests) { const _request = preProcessRequest(request); const requestPath = `${path}/${_request.name}`; + const processRequestParams: ProcessRequestParams = { + path: requestPath, + request: _request, + envs, + delay, + }; // Request processing initiated message. log(WARN(`\nRunning: ${bold(requestPath)}`)); // Processing current request. - const result = await processRequest(_request, envs, requestPath)(); + const result = await processRequest(processRequestParams)(); // Updating global & selected envs with new envs from processed-request output. const { global, selected } = result.envs; diff --git a/packages/hoppscotch-cli/src/utils/getters.ts b/packages/hoppscotch-cli/src/utils/getters.ts index a60a93af7..fe9c0f919 100644 --- a/packages/hoppscotch-cli/src/utils/getters.ts +++ b/packages/hoppscotch-cli/src/utils/getters.ts @@ -129,3 +129,8 @@ export const getDurationInSeconds = ( const durationInSeconds = (end[0] * 1e9 + end[1]) / 1e9; return round(durationInSeconds, precision); }; + +export const roundDuration = ( + duration: number, + precision: number = DEFAULT_DURATION_PRECISION +) => round(duration, precision); diff --git a/packages/hoppscotch-cli/src/utils/request.ts b/packages/hoppscotch-cli/src/utils/request.ts index 0a332521b..82f002dff 100644 --- a/packages/hoppscotch-cli/src/utils/request.ts +++ b/packages/hoppscotch-cli/src/utils/request.ts @@ -12,7 +12,11 @@ import { testRunner, getTestScriptParams, hasFailedTestCases } from "./test"; import { RequestConfig, EffectiveHoppRESTRequest } from "../interfaces/request"; import { RequestRunnerResponse } from "../interfaces/response"; import { preRequestScriptRunner } from "./pre-request"; -import { HoppEnvs, RequestReport } from "../types/request"; +import { + HoppEnvs, + ProcessRequestParams, + RequestReport, +} from "../types/request"; import { printPreRequestRunner, printRequestRunner, @@ -189,11 +193,11 @@ const getRequest = { */ export const processRequest = ( - request: HoppRESTRequest, - envs: HoppEnvs, - path: string + params: ProcessRequestParams ): T.Task<{ envs: HoppEnvs; report: RequestReport }> => async () => { + const { envs, path, request, delay } = params; + // Initialising updatedEnvs with given parameter envs, will eventually get updated. const result = { envs: envs, @@ -247,7 +251,9 @@ export const processRequest = duration: 0, }; // Executing request-runner. - const requestRunnerRes = await requestRunner(requestConfig)(); + const requestRunnerRes = await delayPromiseFunction< + E.Either + >(requestRunner(requestConfig), delay); if (E.isLeft(requestRunnerRes)) { // Updating report for errors & current result report.errors.push(requestRunnerRes.left); @@ -358,3 +364,15 @@ export const getRequestMetrics = ( hasReqErrors ? { failed: 1, passed: 0 } : { failed: 0, passed: 1 }, (requests) => { requests, duration } ); + +/** + * A function to execute promises with specific delay in milliseconds. + * @param func Function with promise with return type T. + * @param delay TIme in milliseconds to delay function. + * @returns Promise of type same as func. + */ +export const delayPromiseFunction = ( + func: () => Promise, + delay: number +): Promise => + new Promise((resolve) => setTimeout(() => resolve(func()), delay));