Compare commits
11 Commits
add-realti
...
feat/share
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
502da61b8b | ||
|
|
f2777a9a75 | ||
|
|
4bd3e89f89 | ||
|
|
09e9601940 | ||
|
|
fd4a5c626f | ||
|
|
67cfef82af | ||
|
|
aa18249791 | ||
|
|
9d8fdb4d04 | ||
|
|
fbca9b06c3 | ||
|
|
bb0bf35164 | ||
|
|
5a35c098ec |
@@ -32,9 +32,6 @@
|
||||
"lint-staged": "12.4.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"vue": "3.3.9"
|
||||
},
|
||||
"packageExtensions": {
|
||||
"httpsnippet@^3.0.1": {
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -147,7 +147,7 @@ module.exports = {
|
||||
// The glob patterns Jest uses to detect test files
|
||||
testMatch: [
|
||||
// "**/__tests__/**/*.[jt]s?(x)",
|
||||
"**/src/__tests__/commands/**/*.*.ts",
|
||||
"**/src/__tests__/**/*.*.ts",
|
||||
],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||
|
||||
@@ -19,9 +19,8 @@
|
||||
"debugger": "node debugger.js 9999",
|
||||
"prepublish": "pnpm exec tsup",
|
||||
"prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write",
|
||||
"test": "pnpm run build && jest && rm -rf dist",
|
||||
"do-typecheck": "pnpm exec tsc --noEmit",
|
||||
"do-test": "pnpm test"
|
||||
"test": "pnpm run build && jest && rm -rf dist"
|
||||
},
|
||||
"keywords": [
|
||||
"cli",
|
||||
|
||||
@@ -28,7 +28,7 @@ describe("Test 'hopp test <file>' command:", () => {
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("UNKNOWN_ERROR");
|
||||
});
|
||||
|
||||
|
||||
test("Malformed collection file.", async () => {
|
||||
const cmd = `node ./bin/hopp test ${getTestJsonFilePath(
|
||||
"malformed-collection2.json"
|
||||
@@ -106,7 +106,7 @@ describe("Test 'hopp test <file> --env <file>' command:", () => {
|
||||
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, stdout } = await execAsync(cmd);
|
||||
const { error } = await execAsync(cmd);
|
||||
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
@@ -129,6 +129,7 @@ describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
|
||||
const cmd = `${VALID_TEST_CMD} --delay 'NaN'`;
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
console.log("invalid value thing", out)
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"URL": "https://echo.hoppscotch.io",
|
||||
"HOST": "echo.hoppscotch.io",
|
||||
"X-COUNTRY": "IN",
|
||||
"BODY_VALUE": "body_value",
|
||||
"BODY_KEY": "body_key"
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"method": "POST",
|
||||
"auth": { "authType": "none", "authActive": true },
|
||||
"preRequestScript": "",
|
||||
"testScript": "const HOST = pw.env.get(\"HOST\");\nconst UNSET_ENV = pw.env.get(\"UNSET_ENV\");\nconst EXPECTED_URL = \"https://echo.hoppscotch.io\";\nconst URL = pw.env.get(\"URL\");\nconst BODY_VALUE = pw.env.get(\"BODY_VALUE\");\n\n// Check JSON response property\npw.test(\"Check headers properties.\", ()=> {\n pw.expect(pw.response.body.headers.host).toBe(HOST);\n});\n\npw.test(\"Check data properties.\", () => {\n\tconst DATA = pw.response.body.data;\n \n pw.expect(DATA).toBeType(\"string\");\n pw.expect(JSON.parse(DATA).body_key).toBe(BODY_VALUE);\n});\n\npw.test(\"Check request URL.\", () => {\n pw.expect(URL).toBe(EXPECTED_URL);\n})\n\npw.test(\"Check unset ENV.\", () => {\n pw.expect(UNSET_ENV).toBeType(\"undefined\");\n})",
|
||||
"testScript": "const HOST = pw.env.get(\"HOST\");\nconst UNSET_ENV = pw.env.get(\"UNSET_ENV\");\nconst EXPECTED_URL = \"https://echo.hoppscotch.io\";\nconst URL = pw.env.get(\"URL\");\nconst X_COUNTRY = pw.env.get(\"X-COUNTRY\");\nconst BODY_VALUE = pw.env.get(\"BODY_VALUE\");\n\n// Check JSON response property\npw.test(\"Check headers properties.\", ()=> {\n pw.expect(pw.response.body.headers.host).toBe(HOST);\n\t pw.expect(pw.response.body.headers[\"x-country\"]).toBe(X_COUNTRY); \n});\n\npw.test(\"Check data properties.\", () => {\n\tconst DATA = pw.response.body.data;\n \n pw.expect(DATA).toBeType(\"string\");\n pw.expect(JSON.parse(DATA).body_key).toBe(BODY_VALUE);\n});\n\npw.test(\"Check request URL.\", () => {\n pw.expect(URL).toBe(EXPECTED_URL);\n})\n\npw.test(\"Check unset ENV.\", () => {\n pw.expect(UNSET_ENV).toBeType(\"undefined\");\n})",
|
||||
"body": {
|
||||
"contentType": "application/json",
|
||||
"body": "{\n \"<<BODY_KEY>>\":\"<<BODY_VALUE>>\"\n}"
|
||||
|
||||
@@ -6,24 +6,23 @@ import {
|
||||
parseTemplateString,
|
||||
parseTemplateStringE,
|
||||
} from "@hoppscotch/data";
|
||||
import { runPreRequestScript } from "@hoppscotch/js-sandbox/node";
|
||||
import * as A from "fp-ts/Array";
|
||||
import * as E from "fp-ts/Either";
|
||||
import * as O from "fp-ts/Option";
|
||||
import * as RA from "fp-ts/ReadonlyArray";
|
||||
import * as TE from "fp-ts/TaskEither";
|
||||
import { runPreRequestScript } from "@hoppscotch/js-sandbox";
|
||||
import { flow, pipe } from "fp-ts/function";
|
||||
import * as TE from "fp-ts/TaskEither";
|
||||
import * as E from "fp-ts/Either";
|
||||
import * as RA from "fp-ts/ReadonlyArray";
|
||||
import * as A from "fp-ts/Array";
|
||||
import * as O from "fp-ts/Option";
|
||||
import * as S from "fp-ts/string";
|
||||
import qs from "qs";
|
||||
|
||||
import { EffectiveHoppRESTRequest } from "../interfaces/request";
|
||||
import { HoppCLIError, error } from "../types/errors";
|
||||
import { error, HoppCLIError } from "../types/errors";
|
||||
import { HoppEnvs } from "../types/request";
|
||||
import { PreRequestMetrics } from "../types/response";
|
||||
import { isHoppCLIError } from "./checks";
|
||||
import { arrayFlatMap, arraySort, tupleToRecord } from "./functions/array";
|
||||
import { getEffectiveFinalMetaData } from "./getters";
|
||||
import { tupleToRecord, arraySort, arrayFlatMap } from "./functions/array";
|
||||
import { toFormData } from "./mutators";
|
||||
import { getEffectiveFinalMetaData } from "./getters";
|
||||
import { PreRequestMetrics } from "../types/response";
|
||||
|
||||
/**
|
||||
* Runs pre-request-script runner over given request which extracts set ENVs and
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import { HoppRESTRequest } from "@hoppscotch/data";
|
||||
import { TestDescriptor } from "@hoppscotch/js-sandbox";
|
||||
import { runTestScript } from "@hoppscotch/js-sandbox/node";
|
||||
import * as A from "fp-ts/Array";
|
||||
import * as RA from "fp-ts/ReadonlyArray";
|
||||
import * as T from "fp-ts/Task";
|
||||
import * as TE from "fp-ts/TaskEither";
|
||||
import { flow, pipe } from "fp-ts/function";
|
||||
import { execTestScript, TestDescriptor } from "@hoppscotch/js-sandbox";
|
||||
import { hrtime } from "process";
|
||||
|
||||
import { flow, pipe } from "fp-ts/function";
|
||||
import * as RA from "fp-ts/ReadonlyArray";
|
||||
import * as A from "fp-ts/Array";
|
||||
import * as TE from "fp-ts/TaskEither";
|
||||
import * as T from "fp-ts/Task";
|
||||
import {
|
||||
RequestRunnerResponse,
|
||||
TestReport,
|
||||
TestScriptParams,
|
||||
} from "../interfaces/response";
|
||||
import { HoppCLIError, error } from "../types/errors";
|
||||
import { error, HoppCLIError } from "../types/errors";
|
||||
import { HoppEnvs } from "../types/request";
|
||||
import { ExpectResult, TestMetrics, TestRunnerRes } from "../types/response";
|
||||
import { getDurationInSeconds } from "./getters";
|
||||
@@ -38,7 +36,7 @@ export const testRunner = (
|
||||
pipe(
|
||||
TE.of(testScriptData),
|
||||
TE.chain(({ testScript, response, envs }) =>
|
||||
runTestScript(testScript, envs, response)
|
||||
execTestScript(testScript, envs, response)
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width=".88em" height="1em" viewBox="0 0 21 24"><path fill="currentColor" d="M12.731 2.751 17.666 5.6a2.138 2.138 0 1 1 2.07 3.548l-.015.003v5.7a2.14 2.14 0 1 1-2.098 3.502l-.002-.002-4.905 2.832a2.14 2.14 0 1 1-4.079.054l-.004.015-4.941-2.844a2.14 2.14 0 1 1-2.067-3.556l.015-.003V9.15a2.14 2.14 0 1 1 1.58-3.926l-.01-.005c.184.106.342.231.479.376l.001.001 4.938-2.85a2.14 2.14 0 1 1 4.096.021l.004-.015zm-.515.877a.766.766 0 0 1-.057.057l-.001.001 6.461 11.19c.026-.009.056-.016.082-.023V9.146a2.14 2.14 0 0 1-1.555-2.603l-.003.015.019-.072zm-3.015.059-.06-.06-4.946 2.852A2.137 2.137 0 0 1 2.749 9.12l-.015.004-.076.021v5.708l.084.023 6.461-11.19zm2.076.507a2.164 2.164 0 0 1-1.207-.004l.015.004-6.46 11.189c.286.276.496.629.597 1.026l.003.015h12.911c.102-.413.313-.768.599-1.043l.001-.001L11.28 4.194zm.986 16.227 4.917-2.838a1.748 1.748 0 0 1-.038-.142H4.222l-.021.083 4.939 2.852c.39-.403.936-.653 1.54-.653.626 0 1.189.268 1.581.696l.001.002z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width=".88em" height="1em" viewBox="0 0 21 24" class="iconify iconify--fontisto"><path fill="currentColor" d="M12.731 2.751 17.666 5.6a2.138 2.138 0 1 1 2.07 3.548l-.015.003v5.7a2.14 2.14 0 1 1-2.098 3.502l-.002-.002-4.905 2.832a2.14 2.14 0 1 1-4.079.054l-.004.015-4.941-2.844a2.14 2.14 0 1 1-2.067-3.556l.015-.003V9.15a2.14 2.14 0 1 1 1.58-3.926l-.01-.005c.184.106.342.231.479.376l.001.001 4.938-2.85a2.14 2.14 0 1 1 4.096.021l.004-.015zm-.515.877a.766.766 0 0 1-.057.057l-.001.001 6.461 11.19c.026-.009.056-.016.082-.023V9.146a2.14 2.14 0 0 1-1.555-2.603l-.003.015.019-.072zm-3.015.059-.06-.06-4.946 2.852A2.137 2.137 0 0 1 2.749 9.12l-.015.004-.076.021v5.708l.084.023 6.461-11.19zm2.076.507a2.164 2.164 0 0 1-1.207-.004l.015.004-6.46 11.189c.286.276.496.629.597 1.026l.003.015h12.911c.102-.413.313-.768.599-1.043l.001-.001L11.28 4.194zm.986 16.227 4.917-2.838a1.748 1.748 0 0 1-.038-.142H4.222l-.021.083 4.939 2.852c.39-.403.936-.653 1.54-.653.626 0 1.189.268 1.581.696l.001.002z"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1017 B After Width: | Height: | Size: 1.0 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M10.133 1h4.409a.5.5 0 0 1 .5.5v4.422c0 .026-.035.033-.045.01l-.048-.112a9.095 9.095 0 0 0-4.825-4.776c-.023-.01-.016-.044.01-.044Zm-8.588.275h-.5v1h.5c7.027 0 12.229 5.199 12.229 12.226v.5h1v-.5c0-7.58-5.65-13.226-13.229-13.226Zm.034 4.22h-.5v1h.5c2.361 0 4.348.837 5.744 2.238 1.395 1.401 2.227 3.395 2.227 5.758v.5h1v-.5c0-2.604-.921-4.859-2.52-6.463-1.596-1.605-3.845-2.532-6.45-2.532Zm-.528 8.996v-4.423c0-.041.033-.074.074-.074a4.923 4.923 0 0 1 4.923 4.922.074.074 0 0 1-.074.074H1.551a.5.5 0 0 1-.5-.5Z" clip-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 684 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M9.277 2.084a.5.5 0 0 1 .185.607l-2.269 5.5a.5.5 0 0 1-.462.309H3.5a.5.5 0 0 1-.354-.854l5.5-5.5a.5.5 0 0 1 .631-.062ZM4.707 7.5h1.69l1.186-2.875L4.707 7.5Zm2.016 6.416a.5.5 0 0 1-.185-.607l2.269-5.5a.5.5 0 0 1 .462-.309H12.5a.5.5 0 0 1 .354.854l-5.5 5.5a.5.5 0 0 1-.631.062Zm4.57-5.416h-1.69l-1.186 2.875L11.293 8.5Z" clip-rule="evenodd"/><path fill="currentColor" fill-rule="evenodd" d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0Zm-1 0A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" clip-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 633 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 16 16"><path fill="currentColor" d="M1 2h4.257a2.5 2.5 0 0 1 1.768.732L9.293 5 5 9.293 3.732 8.025A2.5 2.5 0 0 1 3 6.257V4H2v2.257a3.5 3.5 0 0 0 1.025 2.475L5 10.707l1.25-1.25 2.396 2.397.708-.708L6.957 8.75 8.75 6.957l2.396 2.397.708-.708L9.457 6.25 10.707 5 7.732 2.025A3.5 3.5 0 0 0 5.257 1H1v1ZM10.646 2.354l2.622 2.62A2.5 2.5 0 0 1 14 6.744V12h1V6.743a3.5 3.5 0 0 0-1.025-2.475l-2.621-2.622-.707.708ZM4.268 13.975l-2.622-2.621.708-.708 2.62 2.622A2.5 2.5 0 0 0 6.744 14H15v1H6.743a3.5 3.5 0 0 1-2.475-1.025Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 610 B |
@@ -164,12 +164,6 @@ declare module 'vue' {
|
||||
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
||||
IconLucideVerified: typeof import('~icons/lucide/verified')['default']
|
||||
IconLucideX: typeof import('~icons/lucide/x')['default']
|
||||
ImportExportBase: typeof import('./components/importExport/Base.vue')['default']
|
||||
ImportExportImportExportList: typeof import('./components/importExport/ImportExportList.vue')['default']
|
||||
ImportExportImportExportSourcesList: typeof import('./components/importExport/ImportExportSourcesList.vue')['default']
|
||||
ImportExportImportExportStepsFileImport: typeof import('./components/importExport/ImportExportSteps/FileImport.vue')['default']
|
||||
ImportExportImportExportStepsMyCollectionImport: typeof import('./components/importExport/ImportExportSteps/MyCollectionImport.vue')['default']
|
||||
ImportExportImportExportStepsUrlImport: typeof import('./components/importExport/ImportExportSteps/UrlImport.vue')['default']
|
||||
InterceptorsErrorPlaceholder: typeof import('./components/interceptors/ErrorPlaceholder.vue')['default']
|
||||
InterceptorsExtensionSubtitle: typeof import('./components/interceptors/ExtensionSubtitle.vue')['default']
|
||||
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
|
||||
|
||||
@@ -305,9 +305,10 @@ watch(isOnline, () => {
|
||||
if (!isOnline.value) {
|
||||
bannerID = banner.showBanner(offlineBanner)
|
||||
return
|
||||
}
|
||||
if (banner.content && bannerID) {
|
||||
banner.removeBanner(bannerID)
|
||||
} else {
|
||||
if (banner.content && bannerID) {
|
||||
banner.removeBanner(bannerID)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -112,7 +112,6 @@ import {
|
||||
WorkspaceSpotlightSearcherService,
|
||||
} from "~/services/spotlight/searchers/workspace.searcher"
|
||||
import { InterceptorSpotlightSearcherService } from "~/services/spotlight/searchers/interceptor.searcher"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -142,10 +141,6 @@ useService(WorkspaceSpotlightSearcherService)
|
||||
useService(SwitchWorkspaceSpotlightSearcherService)
|
||||
useService(InterceptorSpotlightSearcherService)
|
||||
|
||||
platform.spotlight?.additionalSearchers?.forEach((searcher) =>
|
||||
useService(searcher)
|
||||
)
|
||||
|
||||
const search = ref("")
|
||||
|
||||
const searchSession = ref<SpotlightSearchState>()
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="flex flex-1 flex-col">
|
||||
<header
|
||||
class="flex items-center justify-between flex-1 flex-shrink-0 px-2 py-2 space-x-2 overflow-x-auto overflow-y-hidden"
|
||||
class="flex flex-1 flex-shrink-0 items-center justify-between space-x-2 overflow-x-auto overflow-y-hidden px-2 py-2"
|
||||
>
|
||||
<div class="flex items-center justify-between flex-1 space-x-2">
|
||||
<div class="flex flex-1 items-center justify-between space-x-2">
|
||||
<HoppButtonSecondary
|
||||
class="!font-bold uppercase tracking-wide !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||
:label="t('app.name')"
|
||||
to="https://hoppscotch.io"
|
||||
to="https://hoppscotch.io/"
|
||||
blank
|
||||
/>
|
||||
<div class="flex">
|
||||
<HoppButtonSecondary
|
||||
<HoppSmartItem
|
||||
:label="t('app.open_in_hoppscotch')"
|
||||
:to="sharedRequestURL"
|
||||
blank
|
||||
@@ -19,57 +19,63 @@
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="sticky top-0 z-10 flex-1">
|
||||
|
||||
<div class="flex-1">
|
||||
<div
|
||||
class="flex-none flex-shrink-0 p-4 bg-primary sm:flex sm:flex-shrink-0 sm:space-x-2"
|
||||
class="flex-none flex-shrink-0 bg-primary p-4 sm:flex sm:flex-shrink-0 sm:space-x-2"
|
||||
>
|
||||
<div
|
||||
class="flex flex-1 overflow-hidden border divide-x rounded text-secondaryDark divide-divider min-w-[12rem] overflow-x-auto border-divider"
|
||||
class="min-w-52 flex flex-1 whitespace-nowrap rounded border border-divider"
|
||||
>
|
||||
<span
|
||||
class="flex items-center justify-center px-4 py-2 font-semibold transition rounded-l"
|
||||
>
|
||||
{{ tab.document.request.method }}
|
||||
</span>
|
||||
<div class="relative flex">
|
||||
<span
|
||||
class="flex justify-center items-center w-26 cursor-pointer rounded-l bg-primaryLight px-4 py-2 font-semibold text-secondaryDark transition"
|
||||
>
|
||||
{{ tab.document.request.method }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center flex-1 flex-shrink-0 min-w-0 px-4 py-2 truncate rounded-r"
|
||||
class="flex flex-1 whitespace-nowrap rounded-r border-l border-divider bg-primaryLight transition"
|
||||
>
|
||||
{{ tab.document.request.endpoint }}
|
||||
<input
|
||||
name="method"
|
||||
:value="tab.document.request.endpoint"
|
||||
class="flex-1 px-4 bg-primary"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex mt-2 space-x-2 sm:mt-0">
|
||||
<div class="mt-2 flex sm:mt-0">
|
||||
<HoppButtonPrimary
|
||||
id="send"
|
||||
:title="`${t(
|
||||
'action.send'
|
||||
)} <kbd>${getSpecialKey()}</kbd><kbd>↩</kbd>`"
|
||||
:label="`${!loading ? t('action.send') : t('action.cancel')}`"
|
||||
class="flex-1 min-w-20"
|
||||
outline
|
||||
class="min-w-20 flex-1"
|
||||
@click="!loading ? newSendRequest() : cancelRequest()"
|
||||
/>
|
||||
<div class="flex">
|
||||
<HoppButtonSecondary
|
||||
:title="`${t(
|
||||
'request.save'
|
||||
)} <kbd>${getSpecialKey()}</kbd><kbd>S</kbd>`"
|
||||
:label="t('request.save')"
|
||||
filled
|
||||
:icon="IconSave"
|
||||
class="flex-1 rounded"
|
||||
blank
|
||||
outline
|
||||
:to="sharedRequestURL"
|
||||
/>
|
||||
</div>
|
||||
<HoppButtonSecondary
|
||||
:title="`${t(
|
||||
'request.save'
|
||||
)} <kbd>${getSpecialKey()}</kbd><kbd>S</kbd>`"
|
||||
:label="t('request.save')"
|
||||
filled
|
||||
:icon="IconSave"
|
||||
class="flex-1 rounded rounded-r-none"
|
||||
blank
|
||||
:to="sharedRequestURL"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<HttpRequestOptions
|
||||
v-model="tab.document.request"
|
||||
v-model:option-tab="selectedOptionTab"
|
||||
:properties="properties"
|
||||
/>
|
||||
|
||||
<HttpResponse :document="tab.document" :is-embed="true" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<div class="divide-y divide-dividerLight">
|
||||
<div
|
||||
v-if="noEnvSelected && !globalHasAdditions"
|
||||
class="flex bg-bannerInfo p-4 text-secondaryDark"
|
||||
class="flex bg-info p-4 text-secondaryDark"
|
||||
role="alert"
|
||||
>
|
||||
<icon-lucide-alert-triangle class="svg-icons mr-4" />
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-else-if="myTeams.length"
|
||||
class="bg-bannerInfo flex flex-col space-y-2 rounded-lg border border-red-500 p-4 text-secondaryDark"
|
||||
class="bg-info flex flex-col space-y-2 rounded-lg border border-red-500 p-4 text-secondaryDark"
|
||||
>
|
||||
<h2 class="font-bold text-red-500">
|
||||
{{ t("error.danger_zone") }}
|
||||
@@ -45,7 +45,7 @@
|
||||
</div>
|
||||
<div v-else>
|
||||
<div
|
||||
class="bg-bannerInfo mb-4 flex flex-col space-y-2 rounded-lg border border-red-500 p-4 text-secondaryDark"
|
||||
class="bg-info mb-4 flex flex-col space-y-2 rounded-lg border border-red-500 p-4 text-secondaryDark"
|
||||
>
|
||||
<h2 class="font-bold text-red-500">
|
||||
{{ t("error.danger_zone") }}
|
||||
|
||||
@@ -306,8 +306,9 @@ const embedThemeIcon = computed(() => {
|
||||
return IconMonitor
|
||||
} else if (embedOptions.value.theme === "light") {
|
||||
return IconSun
|
||||
} else {
|
||||
return IconMoon
|
||||
}
|
||||
return IconMoon
|
||||
})
|
||||
|
||||
const removeEmbedOption = (option: EmbedTabs) => {
|
||||
@@ -391,8 +392,9 @@ const copyButton = (
|
||||
|
||||
if (type === "markdown") {
|
||||
return `[](${baseURL}/r/${props.request?.id})`
|
||||
} else {
|
||||
return `<a href="${baseURL}/r/${props.request?.id}"><img src="${baseURL}/${badge}" alt="Run in Hoppscotch" /></a>`
|
||||
}
|
||||
return `<a href="${baseURL}/r/${props.request?.id}"><img src="${baseURL}/${badge}" alt="Run in Hoppscotch" /></a>`
|
||||
}
|
||||
|
||||
const copyLink = (variationID: string) => {
|
||||
@@ -400,8 +402,9 @@ const copyLink = (variationID: string) => {
|
||||
return `${baseURL}/r/${props.request?.id}`
|
||||
} else if (variationID === "link2") {
|
||||
return `<a href="${baseURL}/r/${props.request?.id}">Run in Hoppscotch</a>`
|
||||
} else {
|
||||
return `[Run in Hoppscotch](${baseURL}/r/${props.request?.id})`
|
||||
}
|
||||
return `[Run in Hoppscotch](${baseURL}/r/${props.request?.id})`
|
||||
}
|
||||
|
||||
const copyContent = ({
|
||||
|
||||
@@ -29,15 +29,17 @@
|
||||
@copy-shared-request="copySharedRequest"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="step === 1" #footer>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-start flex-1">
|
||||
<HoppButtonPrimary
|
||||
v-if="step === 1"
|
||||
:label="t('action.create')"
|
||||
:loading="loading"
|
||||
@click="createSharedRequest"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
:label="step === 1 ? t('action.cancel') : t('action.close')"
|
||||
class="ml-2"
|
||||
filled
|
||||
outline
|
||||
|
||||
@@ -446,12 +446,13 @@ const resolveConfirmModal = (title: string | null) => {
|
||||
const getErrorMessage = (err: GQLError<string>) => {
|
||||
if (err.type === "network_error") {
|
||||
return t("error.network_error")
|
||||
}
|
||||
switch (err.error) {
|
||||
case "shortcode/not_found":
|
||||
return t("shared_request.not_found")
|
||||
default:
|
||||
return t("error.something_went_wrong")
|
||||
} else {
|
||||
switch (err.error) {
|
||||
case "shortcode/not_found":
|
||||
return t("shared_request.not_found")
|
||||
default:
|
||||
return t("error.something_went_wrong")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,7 @@
|
||||
'bg-accentContrast': isEmbedThemeLight,
|
||||
}"
|
||||
>
|
||||
<span
|
||||
class="flex items-center flex-1 min-w-0 border rounded border-divider"
|
||||
>
|
||||
<span class="flex items-center min-w-0 border rounded border-divider">
|
||||
<span
|
||||
class="flex max-w-[4rem] rounded-l h-full items-center justify-center border-r border-divider text-tiny"
|
||||
:class="{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { flow } from "fp-ts/function"
|
||||
import { useI18n as _useI18n } from "vue-i18n"
|
||||
|
||||
export function useI18n() {
|
||||
return _useI18n().t
|
||||
}
|
||||
export const useI18n = flow(_useI18n, (x) => x.t)
|
||||
|
||||
export const useFullI18n = _useI18n
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
import { SandboxTestResult, TestDescriptor } from "@hoppscotch/js-sandbox"
|
||||
import { runTestScript } from "@hoppscotch/js-sandbox/web"
|
||||
import * as A from "fp-ts/Array"
|
||||
import * as E from "fp-ts/Either"
|
||||
import * as O from "fp-ts/Option"
|
||||
import { flow, pipe } from "fp-ts/function"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { Observable, Subject } from "rxjs"
|
||||
import { filter } from "rxjs/operators"
|
||||
import { Ref } from "vue"
|
||||
|
||||
import { flow, pipe } from "fp-ts/function"
|
||||
import * as O from "fp-ts/Option"
|
||||
import * as A from "fp-ts/Array"
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
import {
|
||||
SandboxTestResult,
|
||||
runTestScript,
|
||||
TestDescriptor,
|
||||
} from "@hoppscotch/js-sandbox"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import {
|
||||
getCombinedEnvVariables,
|
||||
getFinalEnvsFromPreRequest,
|
||||
} from "./preRequest"
|
||||
import { getEffectiveRESTRequest } from "./utils/EffectiveURL"
|
||||
import { HoppRESTResponse } from "./types/HoppRESTResponse"
|
||||
import { createRESTNetworkRequestStream } from "./network"
|
||||
import { HoppTestData, HoppTestResult } from "./types/HoppTestResult"
|
||||
import { isJSONContentType } from "./utils/contenttypes"
|
||||
import { updateTeamEnvironment } from "./backend/mutations/TeamEnvironment"
|
||||
import {
|
||||
environmentsStore,
|
||||
getCurrentEnvironment,
|
||||
@@ -18,18 +29,9 @@ import {
|
||||
setGlobalEnvVariables,
|
||||
updateEnvironment,
|
||||
} from "~/newstore/environments"
|
||||
import { Ref } from "vue"
|
||||
import { HoppTab } from "~/services/tab"
|
||||
import { updateTeamEnvironment } from "./backend/mutations/TeamEnvironment"
|
||||
import { createRESTNetworkRequestStream } from "./network"
|
||||
import {
|
||||
getCombinedEnvVariables,
|
||||
getFinalEnvsFromPreRequest,
|
||||
} from "./preRequest"
|
||||
import { HoppRESTDocument } from "./rest/document"
|
||||
import { HoppRESTResponse } from "./types/HoppRESTResponse"
|
||||
import { HoppTestData, HoppTestResult } from "./types/HoppTestResult"
|
||||
import { getEffectiveRESTRequest } from "./utils/EffectiveURL"
|
||||
import { isJSONContentType } from "./utils/contenttypes"
|
||||
|
||||
const getTestableBody = (
|
||||
res: HoppRESTResponse & { type: "success" | "fail" }
|
||||
@@ -87,7 +89,7 @@ export function runRESTRequest$(
|
||||
const res = getFinalEnvsFromPreRequest(
|
||||
tab.value.document.request.preRequestScript,
|
||||
getCombinedEnvVariables()
|
||||
).then((envs) => {
|
||||
)().then((envs) => {
|
||||
if (cancelCalled) return E.left("cancellation" as const)
|
||||
|
||||
if (E.isLeft(envs)) {
|
||||
@@ -123,7 +125,7 @@ export function runRESTRequest$(
|
||||
body: getTestableBody(res),
|
||||
headers: res.headers,
|
||||
}
|
||||
)
|
||||
)()
|
||||
|
||||
if (E.isRight(runResult)) {
|
||||
tab.value.document.testResults = translateToSandboxTestResults(
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import * as E from "fp-ts/Either"
|
||||
import { runPreRequestScript } from "@hoppscotch/js-sandbox/web"
|
||||
import { runPreRequestScript } from "@hoppscotch/js-sandbox"
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
|
||||
import {
|
||||
getCurrentEnvironment,
|
||||
getGlobalVariables,
|
||||
} from "~/newstore/environments"
|
||||
import { TestResult } from "@hoppscotch/js-sandbox"
|
||||
|
||||
export const getCombinedEnvVariables = () => ({
|
||||
global: cloneDeep(getGlobalVariables()),
|
||||
@@ -20,5 +17,4 @@ export const getFinalEnvsFromPreRequest = (
|
||||
global: Environment["variables"]
|
||||
selected: Environment["variables"]
|
||||
}
|
||||
): Promise<E.Either<string, TestResult["envs"]>> =>
|
||||
runPreRequestScript(script, envs)
|
||||
) => runPreRequestScript(script, envs)
|
||||
|
||||
@@ -5,11 +5,10 @@
|
||||
content-styles="!h-[calc(100%-var(--sidebar-primary-sticky-fold)-1px)] !flex"
|
||||
>
|
||||
<HoppSmartTab
|
||||
v-for="(navigation, index) in REALTIME_NAVIGATION"
|
||||
:id="navigation.target"
|
||||
:key="index"
|
||||
:label="navigation.title"
|
||||
:icon="navigation.icon"
|
||||
v-for="{ target, title } in REALTIME_NAVIGATION"
|
||||
:id="target"
|
||||
:key="target"
|
||||
:label="title"
|
||||
>
|
||||
<RouterView />
|
||||
</HoppSmartTab>
|
||||
@@ -21,10 +20,6 @@ import { watch, ref, computed } from "vue"
|
||||
import { RouterView, useRouter, useRoute } from "vue-router"
|
||||
import { usePageHead } from "~/composables/head"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
import IconWebsocket from "~icons/hopp/websocket"
|
||||
import IconSocketio from "~icons/hopp/socketio"
|
||||
import IconMqtt from "~icons/hopp/mqtt"
|
||||
import IconSse from "~icons/lucide/satellite-dish"
|
||||
|
||||
const t = useI18n()
|
||||
const router = useRouter()
|
||||
@@ -34,22 +29,18 @@ const REALTIME_NAVIGATION = [
|
||||
{
|
||||
target: "websocket",
|
||||
title: t("tab.websocket"),
|
||||
icon: IconWebsocket,
|
||||
},
|
||||
{
|
||||
target: "sse",
|
||||
title: t("tab.sse"),
|
||||
icon: IconSse,
|
||||
},
|
||||
{
|
||||
target: "socketio",
|
||||
title: t("tab.socketio"),
|
||||
icon: IconSocketio,
|
||||
},
|
||||
{
|
||||
target: "mqtt",
|
||||
title: t("tab.mqtt"),
|
||||
icon: IconMqtt,
|
||||
},
|
||||
] as const
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import { HoppModule } from "~/modules"
|
||||
import { InspectorsPlatformDef } from "./inspectors"
|
||||
import { Service } from "dioc"
|
||||
import { IOPlatformDef } from "./io"
|
||||
import { SpotlightPlatformDef } from "./spotlight"
|
||||
|
||||
export type PlatformDef = {
|
||||
ui?: UIPlatformDef
|
||||
@@ -29,7 +28,6 @@ export type PlatformDef = {
|
||||
}
|
||||
interceptors: InterceptorsPlatformDef
|
||||
additionalInspectors?: InspectorsPlatformDef
|
||||
spotlight?: SpotlightPlatformDef
|
||||
platformFeatureFlags: {
|
||||
exportAsGIST: boolean
|
||||
hasTelemetry: boolean
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Service } from "dioc"
|
||||
import { SpotlightSearcher } from "~/services/spotlight"
|
||||
|
||||
export type SpotlightPlatformDef = {
|
||||
additionalSearchers?: Array<
|
||||
typeof Service<unknown> & { ID: string } & {
|
||||
new (): Service & SpotlightSearcher
|
||||
}
|
||||
>
|
||||
}
|
||||
@@ -26,9 +26,10 @@ export type Banner = {
|
||||
const getBannerWithHighestScore = (list: Banner[]) => {
|
||||
if (list.length === 0) return null
|
||||
else if (list.length === 1) return list[0]
|
||||
|
||||
const highestScore = Math.max(...list.map((banner) => banner.content.score))
|
||||
return list.find((banner) => banner.content.score === highestScore)
|
||||
else {
|
||||
const highestScore = Math.max(...list.map((banner) => banner.content.score))
|
||||
return list.find((banner) => banner.content.score === highestScore)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -57,7 +57,6 @@ export type SpotlightSearcherResult = {
|
||||
* The keyboard shortcut to trigger the result
|
||||
*/
|
||||
keyboardShortcut?: string[]
|
||||
additionalInfo?: unknown
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
2
packages/hoppscotch-js-sandbox/index.d.ts
vendored
2
packages/hoppscotch-js-sandbox/index.d.ts
vendored
@@ -1,2 +0,0 @@
|
||||
export { default } from "./dist/types/index.d.ts"
|
||||
export * from "./dist/types/index.d.ts"
|
||||
@@ -1,10 +1,6 @@
|
||||
export default {
|
||||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "jsdom",
|
||||
collectCoverage: true,
|
||||
setupFilesAfterEnv: ["./jest.setup.ts"],
|
||||
moduleNameMapper: {
|
||||
"~/(.*)": "<rootDir>/src/$1",
|
||||
"^lodash-es$": "lodash",
|
||||
},
|
||||
}
|
||||
|
||||
2
packages/hoppscotch-js-sandbox/node.d.ts
vendored
2
packages/hoppscotch-js-sandbox/node.d.ts
vendored
@@ -1,2 +0,0 @@
|
||||
export { default } from "./dist/node.d.ts"
|
||||
export * from "./dist/node.d.ts"
|
||||
@@ -2,27 +2,16 @@
|
||||
"name": "@hoppscotch/js-sandbox",
|
||||
"version": "2.1.0",
|
||||
"description": "JavaScript sandboxes for running external scripts used by Hoppscotch clients",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"dist",
|
||||
"index.d.ts"
|
||||
],
|
||||
"main": "./lib/index.js",
|
||||
"module": "./lib/index.mjs",
|
||||
"type": "commonjs",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/types/index.d.ts"
|
||||
},
|
||||
"./web": {
|
||||
"import": "./dist/web.js",
|
||||
"require": "./dist/web.cjs",
|
||||
"types": "./dist/web.d.ts"
|
||||
},
|
||||
"./node": {
|
||||
"import": "./dist/node.js",
|
||||
"require": "./dist/node.cjs",
|
||||
"types": "./dist/node.d.ts"
|
||||
"require": "./lib/index.js",
|
||||
"default": "./lib/index.mjs"
|
||||
}
|
||||
},
|
||||
"types": "./index.d.ts",
|
||||
"types": "./lib/",
|
||||
"engines": {
|
||||
"node": ">=14",
|
||||
"pnpm": ">=3"
|
||||
@@ -31,7 +20,7 @@
|
||||
"lint": "eslint --ext .ts,.js --ignore-path .gitignore .",
|
||||
"lintfix": "eslint --fix --ext .ts,.js --ignore-path .gitignore .",
|
||||
"test": "pnpm exec jest",
|
||||
"build": "vite build && tsc --emitDeclarationOnly",
|
||||
"build": "pnpm exec tsup",
|
||||
"clean": "pnpm tsc --build --clean",
|
||||
"postinstall": "pnpm run build",
|
||||
"prepublish": "pnpm run build",
|
||||
@@ -52,10 +41,10 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@hoppscotch/data": "workspace:^",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"fp-ts": "^2.11.10",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21"
|
||||
"quickjs-emscripten": "^0.15.0",
|
||||
"tsup": "^5.12.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@digitak/esrun": "^3.1.2",
|
||||
@@ -72,7 +61,6 @@
|
||||
"jest": "^27.5.1",
|
||||
"prettier": "^2.8.4",
|
||||
"ts-jest": "^27.1.4",
|
||||
"typescript": "^4.6.3",
|
||||
"vite": "^5.0.4"
|
||||
"typescript": "^4.6.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { runPreRequestScript } from "~/pre-request/node-vm"
|
||||
import { execPreRequestScript } from "../preRequest"
|
||||
import "@relmify/jest-fp-ts"
|
||||
|
||||
describe("execPreRequestScript", () => {
|
||||
test("returns the updated envirionment properly", () => {
|
||||
return expect(
|
||||
runPreRequestScript(
|
||||
execPreRequestScript(
|
||||
`
|
||||
pw.env.set("bob", "newbob")
|
||||
`,
|
||||
@@ -27,7 +27,7 @@ describe("execPreRequestScript", () => {
|
||||
|
||||
test("fails if the key is not a string", () => {
|
||||
return expect(
|
||||
runPreRequestScript(
|
||||
execPreRequestScript(
|
||||
`
|
||||
pw.env.set(10, "newbob")
|
||||
`,
|
||||
@@ -44,7 +44,7 @@ describe("execPreRequestScript", () => {
|
||||
|
||||
test("fails if the value is not a string", () => {
|
||||
return expect(
|
||||
runPreRequestScript(
|
||||
execPreRequestScript(
|
||||
`
|
||||
pw.env.set("bob", 10)
|
||||
`,
|
||||
@@ -61,7 +61,7 @@ describe("execPreRequestScript", () => {
|
||||
|
||||
test("fails for invalid syntax", () => {
|
||||
return expect(
|
||||
runPreRequestScript(
|
||||
execPreRequestScript(
|
||||
`
|
||||
pw.env.set("bob",
|
||||
`,
|
||||
@@ -78,7 +78,7 @@ describe("execPreRequestScript", () => {
|
||||
|
||||
test("creates new env variable if doesn't exist", () => {
|
||||
return expect(
|
||||
runPreRequestScript(
|
||||
execPreRequestScript(
|
||||
`
|
||||
pw.env.set("foo", "bar")
|
||||
`,
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import "@relmify/jest-fp-ts"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import { execTestScript, TestResponse, TestResult } from "../../../test-runner"
|
||||
|
||||
import { runTestScript } from "~/test-runner/node-vm"
|
||||
import { TestResponse, TestResult } from "~/types"
|
||||
import "@relmify/jest-fp-ts"
|
||||
|
||||
const fakeResponse: TestResponse = {
|
||||
status: 200,
|
||||
@@ -13,7 +12,7 @@ const fakeResponse: TestResponse = {
|
||||
|
||||
const func = (script: string, envs: TestResult["envs"]) =>
|
||||
pipe(
|
||||
runTestScript(script, envs, fakeResponse),
|
||||
execTestScript(script, envs, fakeResponse),
|
||||
TE.map((x) => x.tests)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import "@relmify/jest-fp-ts"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import { execTestScript, TestResponse, TestResult } from "../../../test-runner"
|
||||
|
||||
import { runTestScript } from "~/test-runner/node-vm"
|
||||
import { TestResponse, TestResult } from "~/types"
|
||||
import "@relmify/jest-fp-ts"
|
||||
|
||||
const fakeResponse: TestResponse = {
|
||||
status: 200,
|
||||
@@ -13,7 +12,7 @@ const fakeResponse: TestResponse = {
|
||||
|
||||
const func = (script: string, envs: TestResult["envs"]) =>
|
||||
pipe(
|
||||
runTestScript(script, envs, fakeResponse),
|
||||
execTestScript(script, envs, fakeResponse),
|
||||
TE.map((x) => x.tests)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
|
||||
import { runTestScript } from "~/test-runner/node-vm"
|
||||
import { TestResponse, TestResult } from "~/types"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { execTestScript, TestResponse, TestResult } from "../../../test-runner"
|
||||
|
||||
const fakeResponse: TestResponse = {
|
||||
status: 200,
|
||||
@@ -12,7 +10,7 @@ const fakeResponse: TestResponse = {
|
||||
|
||||
const func = (script: string, envs: TestResult["envs"]) =>
|
||||
pipe(
|
||||
runTestScript(script, envs, fakeResponse),
|
||||
execTestScript(script, envs, fakeResponse),
|
||||
TE.map((x) => x.tests)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
|
||||
import { runTestScript } from "~/test-runner/node-vm"
|
||||
import { TestResponse, TestResult } from "~/types"
|
||||
import { execTestScript, TestResponse, TestResult } from "../../../test-runner"
|
||||
|
||||
const fakeResponse: TestResponse = {
|
||||
status: 200,
|
||||
@@ -12,13 +10,13 @@ const fakeResponse: TestResponse = {
|
||||
|
||||
const func = (script: string, envs: TestResult["envs"]) =>
|
||||
pipe(
|
||||
runTestScript(script, envs, fakeResponse),
|
||||
execTestScript(script, envs, fakeResponse),
|
||||
TE.map((x) => x.envs)
|
||||
)
|
||||
|
||||
const funcTest = (script: string, envs: TestResult["envs"]) =>
|
||||
pipe(
|
||||
runTestScript(script, envs, fakeResponse),
|
||||
execTestScript(script, envs, fakeResponse),
|
||||
TE.map((x) => x.tests)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import "@relmify/jest-fp-ts"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
|
||||
import { runTestScript } from "~/test-runner/node-vm"
|
||||
import { TestResponse } from "~/types"
|
||||
import { execTestScript, TestResponse } from "../../../test-runner"
|
||||
import "@relmify/jest-fp-ts"
|
||||
|
||||
const fakeResponse: TestResponse = {
|
||||
status: 200,
|
||||
@@ -13,7 +11,7 @@ const fakeResponse: TestResponse = {
|
||||
|
||||
const func = (script: string, res: TestResponse) =>
|
||||
pipe(
|
||||
runTestScript(script, { global: [], selected: [] }, res),
|
||||
execTestScript(script, { global: [], selected: [] }, res),
|
||||
TE.map((x) => x.tests)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import "@relmify/jest-fp-ts"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
|
||||
import { runTestScript } from "~/test-runner/node-vm"
|
||||
import { TestResponse } from "~/types"
|
||||
import { execTestScript, TestResponse } from "../../../test-runner"
|
||||
import "@relmify/jest-fp-ts"
|
||||
|
||||
const fakeResponse: TestResponse = {
|
||||
status: 200,
|
||||
@@ -13,7 +11,7 @@ const fakeResponse: TestResponse = {
|
||||
|
||||
const func = (script: string, res: TestResponse) =>
|
||||
pipe(
|
||||
runTestScript(script, { global: [], selected: [] }, res),
|
||||
execTestScript(script, { global: [], selected: [] }, res),
|
||||
TE.map((x) => x.tests)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
|
||||
import { runTestScript } from "~/test-runner/node-vm"
|
||||
import { TestResponse } from "~/types"
|
||||
import { execTestScript, TestResponse } from "../../../test-runner"
|
||||
|
||||
const fakeResponse: TestResponse = {
|
||||
status: 200,
|
||||
@@ -12,7 +10,7 @@ const fakeResponse: TestResponse = {
|
||||
|
||||
const func = (script: string, res: TestResponse) =>
|
||||
pipe(
|
||||
runTestScript(script, { global: [], selected: [] }, res),
|
||||
execTestScript(script, { global: [], selected: [] }, res),
|
||||
TE.map((x) => x.tests)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
|
||||
import { runTestScript } from "~/test-runner/node-vm"
|
||||
import { TestResponse } from "~/types"
|
||||
import { execTestScript, TestResponse } from "../../../test-runner"
|
||||
|
||||
const fakeResponse: TestResponse = {
|
||||
status: 200,
|
||||
@@ -12,7 +10,7 @@ const fakeResponse: TestResponse = {
|
||||
|
||||
const func = (script: string, res: TestResponse) =>
|
||||
pipe(
|
||||
runTestScript(script, { global: [], selected: [] }, res),
|
||||
execTestScript(script, { global: [], selected: [] }, res),
|
||||
TE.map((x) => x.tests)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
|
||||
import { runTestScript } from "~/test-runner/node-vm"
|
||||
import { TestResponse } from "~/types"
|
||||
import { execTestScript, TestResponse } from "../../../test-runner"
|
||||
|
||||
const fakeResponse: TestResponse = {
|
||||
status: 200,
|
||||
@@ -12,7 +10,7 @@ const fakeResponse: TestResponse = {
|
||||
|
||||
const func = (script: string, res: TestResponse) =>
|
||||
pipe(
|
||||
runTestScript(script, { global: [], selected: [] }, res),
|
||||
execTestScript(script, { global: [], selected: [] }, res),
|
||||
TE.map((x) => x.tests)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
|
||||
import { runTestScript } from "~/test-runner/node-vm"
|
||||
import { TestResponse } from "~/types"
|
||||
import { execTestScript, TestResponse } from "../../test-runner"
|
||||
|
||||
const fakeResponse: TestResponse = {
|
||||
status: 200,
|
||||
@@ -12,7 +10,7 @@ const fakeResponse: TestResponse = {
|
||||
|
||||
const func = (script: string, res: TestResponse) =>
|
||||
pipe(
|
||||
runTestScript(script, { global: [], selected: [] }, res),
|
||||
execTestScript(script, { global: [], selected: [] }, res),
|
||||
TE.map((x) => x.tests)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,15 +1,40 @@
|
||||
import { preventCyclicObjects } from "~/utils"
|
||||
import { match } from "fp-ts/lib/Either"
|
||||
import { pipe } from "fp-ts/lib/function"
|
||||
import * as QuickJS from "quickjs-emscripten"
|
||||
import { marshalObjectToVM } from "../utils"
|
||||
|
||||
describe("preventCyclicObjects", () => {
|
||||
test("succeeds with a simple object", () => {
|
||||
let vm: QuickJS.QuickJSVm
|
||||
|
||||
beforeAll(async () => {
|
||||
const qjs = await QuickJS.getQuickJS()
|
||||
vm = qjs.createVm()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
vm.dispose()
|
||||
})
|
||||
|
||||
describe("marshalObjectToVM", () => {
|
||||
test("successfully marshals simple object into the vm", () => {
|
||||
const testObj = {
|
||||
a: 1,
|
||||
}
|
||||
|
||||
expect(preventCyclicObjects(testObj)).toBeRight()
|
||||
const objVMHandle: QuickJS.QuickJSHandle | null = pipe(
|
||||
marshalObjectToVM(vm, testObj),
|
||||
match(
|
||||
() => null,
|
||||
(result) => result
|
||||
)
|
||||
)
|
||||
|
||||
expect(objVMHandle).not.toBeNull()
|
||||
expect(vm.dump(objVMHandle!)).toEqual(testObj)
|
||||
|
||||
objVMHandle!.dispose()
|
||||
})
|
||||
|
||||
test("fails with a cyclic object", () => {
|
||||
test("fails marshalling cyclic object into vm", () => {
|
||||
const testObj = {
|
||||
a: 1,
|
||||
b: null as any,
|
||||
@@ -17,6 +42,14 @@ describe("preventCyclicObjects", () => {
|
||||
|
||||
testObj.b = testObj
|
||||
|
||||
expect(preventCyclicObjects(testObj)).toBeLeft()
|
||||
const objVMHandle: QuickJS.QuickJSHandle | null = pipe(
|
||||
marshalObjectToVM(vm, testObj),
|
||||
match(
|
||||
() => null,
|
||||
(result) => result
|
||||
)
|
||||
)
|
||||
|
||||
expect(objVMHandle).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
43
packages/hoppscotch-js-sandbox/src/index.ts
Normal file
43
packages/hoppscotch-js-sandbox/src/index.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { execPreRequestScript } from "./preRequest"
|
||||
import {
|
||||
execTestScript,
|
||||
TestResponse as _TestResponse,
|
||||
TestDescriptor as _TestDescriptor,
|
||||
TestResult,
|
||||
} from "./test-runner"
|
||||
|
||||
export * from "./test-runner"
|
||||
|
||||
export type TestResponse = _TestResponse
|
||||
export type TestDescriptor = _TestDescriptor
|
||||
export type SandboxTestResult = TestResult & { tests: TestDescriptor }
|
||||
|
||||
/**
|
||||
* Executes a given test script on the test-runner sandbox
|
||||
* @param testScript The string of the script to run
|
||||
* @returns A TaskEither with an error message or a TestDescriptor with the final status
|
||||
*/
|
||||
export const runTestScript = (
|
||||
testScript: string,
|
||||
envs: TestResult["envs"],
|
||||
response: TestResponse
|
||||
) =>
|
||||
pipe(
|
||||
execTestScript(testScript, envs, response),
|
||||
TE.chain((results) =>
|
||||
TE.right(<SandboxTestResult>{
|
||||
envs: results.envs,
|
||||
tests: results.tests[0],
|
||||
})
|
||||
) // execTestScript returns an array of descriptors with a single element (extract that)
|
||||
)
|
||||
|
||||
/**
|
||||
* Executes a given pre-request script on the sandbox
|
||||
* @param preRequestScript The script to run
|
||||
* @param env The environment variables active
|
||||
* @returns A TaskEither with an error message or an array of the final environments with the all the script values applied
|
||||
*/
|
||||
export const runPreRequestScript = execPreRequestScript
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./pre-request/node-vm"
|
||||
export * from "./test-runner/node-vm"
|
||||
@@ -1,36 +0,0 @@
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/lib/TaskEither"
|
||||
import { createContext, runInContext } from "vm"
|
||||
|
||||
import { TestResult } from "~/types"
|
||||
import { getPreRequestScriptMethods } from "~/utils"
|
||||
|
||||
export const runPreRequestScript = (
|
||||
preRequestScript: string,
|
||||
envs: TestResult["envs"]
|
||||
): TE.TaskEither<string, TestResult["envs"]> =>
|
||||
pipe(
|
||||
TE.tryCatch(
|
||||
async () => {
|
||||
return createContext()
|
||||
},
|
||||
(reason) => `Context initialization failed: ${reason}`
|
||||
),
|
||||
TE.chain((context) =>
|
||||
TE.tryCatch(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
const { pw, updatedEnvs } = getPreRequestScriptMethods(envs)
|
||||
|
||||
// Expose pw to the context
|
||||
context.pw = pw
|
||||
|
||||
// Run the pre-request script in the provided context
|
||||
runInContext(preRequestScript, context)
|
||||
|
||||
resolve(updatedEnvs)
|
||||
}),
|
||||
(reason) => `Script execution failed: ${reason}`
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -1,24 +0,0 @@
|
||||
import * as E from "fp-ts/Either"
|
||||
|
||||
import { TestResult } from "~/types"
|
||||
|
||||
import Worker from "./worker?worker&inline"
|
||||
|
||||
export const runPreRequestScript = (
|
||||
preRequestScript: string,
|
||||
envs: TestResult["envs"]
|
||||
): Promise<E.Either<string, TestResult["envs"]>> =>
|
||||
new Promise((resolve) => {
|
||||
const worker = new Worker()
|
||||
|
||||
// Listen for the results from the web worker
|
||||
worker.addEventListener("message", (event: MessageEvent) =>
|
||||
resolve(event.data.results)
|
||||
)
|
||||
|
||||
// Send the script to the web worker
|
||||
worker.postMessage({
|
||||
preRequestScript,
|
||||
envs,
|
||||
})
|
||||
})
|
||||
@@ -1,33 +0,0 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
|
||||
import { TestResult } from "~/types"
|
||||
import { getPreRequestScriptMethods } from "~/utils"
|
||||
|
||||
const executeScriptInContext = (
|
||||
preRequestScript: string,
|
||||
envs: TestResult["envs"]
|
||||
): TE.TaskEither<string, TestResult["envs"]> => {
|
||||
try {
|
||||
const { pw, updatedEnvs } = getPreRequestScriptMethods(envs)
|
||||
|
||||
// Create a function from the pre request script using the `Function` constructor
|
||||
const executeScript = new Function("pw", preRequestScript)
|
||||
|
||||
// Execute the script
|
||||
executeScript(pw)
|
||||
|
||||
return TE.right(updatedEnvs)
|
||||
} catch (error) {
|
||||
return TE.left(`Script execution failed: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for messages from the main thread
|
||||
self.addEventListener("message", async (event) => {
|
||||
const { preRequestScript, envs } = event.data
|
||||
|
||||
const results = await executeScriptInContext(preRequestScript, envs)()
|
||||
|
||||
// Post the result back to the main thread
|
||||
self.postMessage({ results })
|
||||
})
|
||||
166
packages/hoppscotch-js-sandbox/src/preRequest.ts
Normal file
166
packages/hoppscotch-js-sandbox/src/preRequest.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as O from "fp-ts/Option"
|
||||
import * as E from "fp-ts/Either"
|
||||
import * as TE from "fp-ts/lib/TaskEither"
|
||||
import * as qjs from "quickjs-emscripten"
|
||||
import cloneDeep from "lodash/clone"
|
||||
import { Environment, parseTemplateStringE } from "@hoppscotch/data"
|
||||
import { getEnv, setEnv } from "./utils"
|
||||
|
||||
type Envs = {
|
||||
global: Environment["variables"]
|
||||
selected: Environment["variables"]
|
||||
}
|
||||
|
||||
export const execPreRequestScript = (
|
||||
preRequestScript: string,
|
||||
envs: Envs
|
||||
): TE.TaskEither<string, Envs> =>
|
||||
pipe(
|
||||
TE.tryCatch(
|
||||
async () => await qjs.getQuickJS(),
|
||||
(reason) => `QuickJS initialization failed: ${reason}`
|
||||
),
|
||||
TE.chain((QuickJS) => {
|
||||
let currentEnvs = cloneDeep(envs)
|
||||
|
||||
const vm = QuickJS.createVm()
|
||||
|
||||
const pwHandle = vm.newObject()
|
||||
|
||||
// Environment management APIs
|
||||
// TODO: Unified Implementation
|
||||
const envHandle = vm.newObject()
|
||||
|
||||
const envGetHandle = vm.newFunction("get", (keyHandle) => {
|
||||
const key: unknown = vm.dump(keyHandle)
|
||||
|
||||
if (typeof key !== "string") {
|
||||
return {
|
||||
error: vm.newString("Expected key to be a string"),
|
||||
}
|
||||
}
|
||||
|
||||
const result = pipe(
|
||||
getEnv(key, currentEnvs),
|
||||
O.match(
|
||||
() => vm.undefined,
|
||||
({ value }) => vm.newString(value)
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
value: result,
|
||||
}
|
||||
})
|
||||
|
||||
const envGetResolveHandle = vm.newFunction("getResolve", (keyHandle) => {
|
||||
const key: unknown = vm.dump(keyHandle)
|
||||
|
||||
if (typeof key !== "string") {
|
||||
return {
|
||||
error: vm.newString("Expected key to be a string"),
|
||||
}
|
||||
}
|
||||
|
||||
const result = pipe(
|
||||
getEnv(key, currentEnvs),
|
||||
E.fromOption(() => "INVALID_KEY" as const),
|
||||
|
||||
E.map(({ value }) =>
|
||||
pipe(
|
||||
parseTemplateStringE(value, [...envs.selected, ...envs.global]),
|
||||
// If the recursive resolution failed, return the unresolved value
|
||||
E.getOrElse(() => value)
|
||||
)
|
||||
),
|
||||
|
||||
// Create a new VM String
|
||||
// NOTE: Do not shorten this to map(vm.newString) apparently it breaks it
|
||||
E.map((x) => vm.newString(x)),
|
||||
|
||||
E.getOrElse(() => vm.undefined)
|
||||
)
|
||||
|
||||
return {
|
||||
value: result,
|
||||
}
|
||||
})
|
||||
|
||||
const envSetHandle = vm.newFunction("set", (keyHandle, valueHandle) => {
|
||||
const key: unknown = vm.dump(keyHandle)
|
||||
const value: unknown = vm.dump(valueHandle)
|
||||
|
||||
if (typeof key !== "string") {
|
||||
return {
|
||||
error: vm.newString("Expected key to be a string"),
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof value !== "string") {
|
||||
return {
|
||||
error: vm.newString("Expected value to be a string"),
|
||||
}
|
||||
}
|
||||
|
||||
currentEnvs = setEnv(key, value, currentEnvs)
|
||||
|
||||
return {
|
||||
value: vm.undefined,
|
||||
}
|
||||
})
|
||||
|
||||
const envResolveHandle = vm.newFunction("resolve", (valueHandle) => {
|
||||
const value: unknown = vm.dump(valueHandle)
|
||||
|
||||
if (typeof value !== "string") {
|
||||
return {
|
||||
error: vm.newString("Expected value to be a string"),
|
||||
}
|
||||
}
|
||||
|
||||
const result = pipe(
|
||||
parseTemplateStringE(value, [
|
||||
...currentEnvs.selected,
|
||||
...currentEnvs.global,
|
||||
]),
|
||||
E.getOrElse(() => value)
|
||||
)
|
||||
|
||||
return {
|
||||
value: vm.newString(result),
|
||||
}
|
||||
})
|
||||
|
||||
vm.setProp(envHandle, "resolve", envResolveHandle)
|
||||
envResolveHandle.dispose()
|
||||
|
||||
vm.setProp(envHandle, "set", envSetHandle)
|
||||
envSetHandle.dispose()
|
||||
|
||||
vm.setProp(envHandle, "getResolve", envGetResolveHandle)
|
||||
envGetResolveHandle.dispose()
|
||||
|
||||
vm.setProp(envHandle, "get", envGetHandle)
|
||||
envGetHandle.dispose()
|
||||
|
||||
vm.setProp(pwHandle, "env", envHandle)
|
||||
envHandle.dispose()
|
||||
|
||||
vm.setProp(vm.global, "pw", pwHandle)
|
||||
pwHandle.dispose()
|
||||
|
||||
const evalRes = vm.evalCode(preRequestScript)
|
||||
|
||||
if (evalRes.error) {
|
||||
const errorData = vm.dump(evalRes.error)
|
||||
evalRes.error.dispose()
|
||||
|
||||
return TE.left(errorData)
|
||||
}
|
||||
|
||||
vm.dispose()
|
||||
|
||||
return TE.right(currentEnvs)
|
||||
})
|
||||
)
|
||||
610
packages/hoppscotch-js-sandbox/src/test-runner.ts
Normal file
610
packages/hoppscotch-js-sandbox/src/test-runner.ts
Normal file
@@ -0,0 +1,610 @@
|
||||
import * as O from "fp-ts/Option"
|
||||
import * as E from "fp-ts/Either"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as qjs from "quickjs-emscripten"
|
||||
import { Environment, parseTemplateStringE } from "@hoppscotch/data"
|
||||
import cloneDeep from "lodash/cloneDeep"
|
||||
import { getEnv, marshalObjectToVM, setEnv } from "./utils"
|
||||
|
||||
/**
|
||||
* The response object structure exposed to the test script
|
||||
*/
|
||||
export type TestResponse = {
|
||||
/** Status Code of the response */
|
||||
status: number
|
||||
/** List of headers returned */
|
||||
headers: { key: string; value: string }[]
|
||||
/**
|
||||
* Body of the response, this will be the JSON object if it is a JSON content type, else body string
|
||||
*/
|
||||
body: string | object
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of an expectation statement
|
||||
*/
|
||||
type ExpectResult = { status: "pass" | "fail" | "error"; message: string } // The expectation failed (fail) or errored (error)
|
||||
|
||||
/**
|
||||
* An object defining the result of the execution of a
|
||||
* test block
|
||||
*/
|
||||
export type TestDescriptor = {
|
||||
/**
|
||||
* The name of the test block
|
||||
*/
|
||||
descriptor: string
|
||||
|
||||
/**
|
||||
* Expectation results of the test block
|
||||
*/
|
||||
expectResults: ExpectResult[]
|
||||
|
||||
/**
|
||||
* Children test blocks (test blocks inside the test block)
|
||||
*/
|
||||
children: TestDescriptor[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the result of a test script execution
|
||||
*/
|
||||
export type TestResult = {
|
||||
tests: TestDescriptor[]
|
||||
envs: {
|
||||
global: Environment["variables"]
|
||||
selected: Environment["variables"]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an Expectation object for use inside the sandbox
|
||||
* @param vm The QuickJS sandbox VM instance
|
||||
* @param expectVal The expecting value of the expectation
|
||||
* @param negated Whether the expectation is negated (negative)
|
||||
* @param currTestStack The current state of the test execution stack
|
||||
* @returns Handle to the expectation object in VM
|
||||
*/
|
||||
function createExpectation(
|
||||
vm: qjs.QuickJSVm,
|
||||
expectVal: any,
|
||||
negated: boolean,
|
||||
currTestStack: TestDescriptor[]
|
||||
): qjs.QuickJSHandle {
|
||||
const resultHandle = vm.newObject()
|
||||
|
||||
const toBeFnHandle = vm.newFunction("toBe", (expectedValHandle) => {
|
||||
const expectedVal = vm.dump(expectedValHandle)
|
||||
|
||||
let assertion = expectVal === expectedVal
|
||||
if (negated) assertion = !assertion
|
||||
|
||||
if (assertion) {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "pass",
|
||||
message: `Expected '${expectVal}' to${
|
||||
negated ? " not" : ""
|
||||
} be '${expectedVal}'`,
|
||||
})
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "fail",
|
||||
message: `Expected '${expectVal}' to${
|
||||
negated ? " not" : ""
|
||||
} be '${expectedVal}'`,
|
||||
})
|
||||
}
|
||||
|
||||
return { value: vm.undefined }
|
||||
})
|
||||
|
||||
const toBeLevel2xxHandle = vm.newFunction("toBeLevel2xx", () => {
|
||||
// Check if the expected value is a number, else it is an error
|
||||
if (typeof expectVal === "number" && !Number.isNaN(expectVal)) {
|
||||
let assertion = expectVal >= 200 && expectVal <= 299
|
||||
if (negated) assertion = !assertion
|
||||
|
||||
if (assertion) {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "pass",
|
||||
message: `Expected '${expectVal}' to${
|
||||
negated ? " not" : ""
|
||||
} be 200-level status`,
|
||||
})
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "fail",
|
||||
message: `Expected '${expectVal}' to${
|
||||
negated ? " not" : ""
|
||||
} be 200-level status`,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message: `Expected 200-level status but could not parse value '${expectVal}'`,
|
||||
})
|
||||
}
|
||||
|
||||
return { value: vm.undefined }
|
||||
})
|
||||
|
||||
const toBeLevel3xxHandle = vm.newFunction("toBeLevel3xx", () => {
|
||||
// Check if the expected value is a number, else it is an error
|
||||
if (typeof expectVal === "number" && !Number.isNaN(expectVal)) {
|
||||
let assertion = expectVal >= 300 && expectVal <= 399
|
||||
if (negated) assertion = !assertion
|
||||
|
||||
if (assertion) {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "pass",
|
||||
message: `Expected '${expectVal}' to${
|
||||
negated ? " not" : ""
|
||||
} be 300-level status`,
|
||||
})
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "fail",
|
||||
message: `Expected '${expectVal}' to${
|
||||
negated ? " not" : ""
|
||||
} be 300-level status`,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message: `Expected 300-level status but could not parse value '${expectVal}'`,
|
||||
})
|
||||
}
|
||||
|
||||
return { value: vm.undefined }
|
||||
})
|
||||
|
||||
const toBeLevel4xxHandle = vm.newFunction("toBeLevel4xx", () => {
|
||||
// Check if the expected value is a number, else it is an error
|
||||
if (typeof expectVal === "number" && !Number.isNaN(expectVal)) {
|
||||
let assertion = expectVal >= 400 && expectVal <= 499
|
||||
if (negated) assertion = !assertion
|
||||
|
||||
if (assertion) {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "pass",
|
||||
message: `Expected '${expectVal}' to${
|
||||
negated ? " not" : ""
|
||||
} be 400-level status`,
|
||||
})
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "fail",
|
||||
message: `Expected '${expectVal}' to${
|
||||
negated ? " not" : ""
|
||||
} be 400-level status`,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message: `Expected 400-level status but could not parse value '${expectVal}'`,
|
||||
})
|
||||
}
|
||||
|
||||
return { value: vm.undefined }
|
||||
})
|
||||
|
||||
const toBeLevel5xxHandle = vm.newFunction("toBeLevel5xx", () => {
|
||||
// Check if the expected value is a number, else it is an error
|
||||
if (typeof expectVal === "number" && !Number.isNaN(expectVal)) {
|
||||
let assertion = expectVal >= 500 && expectVal <= 599
|
||||
if (negated) assertion = !assertion
|
||||
|
||||
if (assertion) {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "pass",
|
||||
message: `Expected '${expectVal}' to${
|
||||
negated ? " not" : ""
|
||||
} be 500-level status`,
|
||||
})
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "fail",
|
||||
message: `Expected '${expectVal}' to${
|
||||
negated ? " not" : ""
|
||||
} be 500-level status`,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message: `Expected 500-level status but could not parse value '${expectVal}'`,
|
||||
})
|
||||
}
|
||||
|
||||
return { value: vm.undefined }
|
||||
})
|
||||
|
||||
const toBeTypeHandle = vm.newFunction("toBeType", (expectedValHandle) => {
|
||||
const expectedType = vm.dump(expectedValHandle)
|
||||
|
||||
// Check if the expectation param is a valid type name string, else error
|
||||
if (
|
||||
[
|
||||
"string",
|
||||
"boolean",
|
||||
"number",
|
||||
"object",
|
||||
"undefined",
|
||||
"bigint",
|
||||
"symbol",
|
||||
"function",
|
||||
].includes(expectedType)
|
||||
) {
|
||||
let assertion = typeof expectVal === expectedType
|
||||
if (negated) assertion = !assertion
|
||||
|
||||
if (assertion) {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "pass",
|
||||
message: `Expected '${expectVal}' to${
|
||||
negated ? " not" : ""
|
||||
} be type '${expectedType}'`,
|
||||
})
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "fail",
|
||||
message: `Expected '${expectVal}' to${
|
||||
negated ? " not" : ""
|
||||
} be type '${expectedType}'`,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message: `Argument for toBeType should be "string", "boolean", "number", "object", "undefined", "bigint", "symbol" or "function"`,
|
||||
})
|
||||
}
|
||||
|
||||
return { value: vm.undefined }
|
||||
})
|
||||
|
||||
const toHaveLengthHandle = vm.newFunction(
|
||||
"toHaveLength",
|
||||
(expectedValHandle) => {
|
||||
const expectedLength = vm.dump(expectedValHandle)
|
||||
|
||||
if (!(Array.isArray(expectVal) || typeof expectVal === "string")) {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message: `Expected toHaveLength to be called for an array or string`,
|
||||
})
|
||||
|
||||
return { value: vm.undefined }
|
||||
}
|
||||
|
||||
// Check if the parameter is a number, else error
|
||||
if (typeof expectedLength === "number" && !Number.isNaN(expectedLength)) {
|
||||
let assertion = (expectVal as any[]).length === expectedLength
|
||||
if (negated) assertion = !assertion
|
||||
|
||||
if (assertion) {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "pass",
|
||||
message: `Expected the array to${
|
||||
negated ? " not" : ""
|
||||
} be of length '${expectedLength}'`,
|
||||
})
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "fail",
|
||||
message: `Expected the array to${
|
||||
negated ? " not" : ""
|
||||
} be of length '${expectedLength}'`,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message: `Argument for toHaveLength should be a number`,
|
||||
})
|
||||
}
|
||||
|
||||
return { value: vm.undefined }
|
||||
}
|
||||
)
|
||||
|
||||
const toIncludeHandle = vm.newFunction("toInclude", (needleHandle) => {
|
||||
const expectedVal = vm.dump(needleHandle)
|
||||
|
||||
if (!(Array.isArray(expectVal) || typeof expectVal === "string")) {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message: `Expected toInclude to be called for an array or string`,
|
||||
})
|
||||
|
||||
return { value: vm.undefined }
|
||||
}
|
||||
|
||||
if (expectedVal === null) {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message: `Argument for toInclude should not be null`,
|
||||
})
|
||||
|
||||
return { value: vm.undefined }
|
||||
}
|
||||
|
||||
if (expectedVal === undefined) {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message: `Argument for toInclude should not be undefined`,
|
||||
})
|
||||
|
||||
return { value: vm.undefined }
|
||||
}
|
||||
|
||||
let assertion = expectVal.includes(expectedVal)
|
||||
if (negated) assertion = !assertion
|
||||
|
||||
const expectValPretty = JSON.stringify(expectVal)
|
||||
const expectedValPretty = JSON.stringify(expectedVal)
|
||||
|
||||
if (assertion) {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "pass",
|
||||
message: `Expected ${expectValPretty} to${
|
||||
negated ? " not" : ""
|
||||
} include ${expectedValPretty}`,
|
||||
})
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "fail",
|
||||
message: `Expected ${expectValPretty} to${
|
||||
negated ? " not" : ""
|
||||
} include ${expectedValPretty}`,
|
||||
})
|
||||
}
|
||||
|
||||
return { value: vm.undefined }
|
||||
})
|
||||
|
||||
vm.setProp(resultHandle, "toBe", toBeFnHandle)
|
||||
vm.setProp(resultHandle, "toBeLevel2xx", toBeLevel2xxHandle)
|
||||
vm.setProp(resultHandle, "toBeLevel3xx", toBeLevel3xxHandle)
|
||||
vm.setProp(resultHandle, "toBeLevel4xx", toBeLevel4xxHandle)
|
||||
vm.setProp(resultHandle, "toBeLevel5xx", toBeLevel5xxHandle)
|
||||
vm.setProp(resultHandle, "toBeType", toBeTypeHandle)
|
||||
vm.setProp(resultHandle, "toHaveLength", toHaveLengthHandle)
|
||||
vm.setProp(resultHandle, "toInclude", toIncludeHandle)
|
||||
|
||||
vm.defineProp(resultHandle, "not", {
|
||||
get: () => {
|
||||
return createExpectation(vm, expectVal, !negated, currTestStack)
|
||||
},
|
||||
})
|
||||
|
||||
toBeFnHandle.dispose()
|
||||
toBeLevel2xxHandle.dispose()
|
||||
toBeLevel3xxHandle.dispose()
|
||||
toBeLevel4xxHandle.dispose()
|
||||
toBeLevel5xxHandle.dispose()
|
||||
toBeTypeHandle.dispose()
|
||||
toHaveLengthHandle.dispose()
|
||||
toIncludeHandle.dispose()
|
||||
|
||||
return resultHandle
|
||||
}
|
||||
|
||||
export const execTestScript = (
|
||||
testScript: string,
|
||||
envs: TestResult["envs"],
|
||||
response: TestResponse
|
||||
): TE.TaskEither<string, TestResult> =>
|
||||
pipe(
|
||||
TE.tryCatch(
|
||||
async () => await qjs.getQuickJS(),
|
||||
(reason) => `QuickJS initialization failed: ${reason}`
|
||||
),
|
||||
TE.chain(
|
||||
// TODO: Make this more functional ?
|
||||
(QuickJS) => {
|
||||
let currentEnvs = cloneDeep(envs)
|
||||
|
||||
const vm = QuickJS.createVm()
|
||||
|
||||
const pwHandle = vm.newObject()
|
||||
|
||||
const testRunStack: TestDescriptor[] = [
|
||||
{ descriptor: "root", expectResults: [], children: [] },
|
||||
]
|
||||
|
||||
const testFuncHandle = vm.newFunction(
|
||||
"test",
|
||||
(descriptorHandle, testFuncHandle) => {
|
||||
const descriptor = vm.getString(descriptorHandle)
|
||||
|
||||
testRunStack.push({
|
||||
descriptor,
|
||||
expectResults: [],
|
||||
children: [],
|
||||
})
|
||||
|
||||
const result = vm.unwrapResult(
|
||||
vm.callFunction(testFuncHandle, vm.null)
|
||||
)
|
||||
result.dispose()
|
||||
|
||||
const child = testRunStack.pop() as TestDescriptor
|
||||
testRunStack[testRunStack.length - 1].children.push(child)
|
||||
}
|
||||
)
|
||||
|
||||
const expectFnHandle = vm.newFunction("expect", (expectValueHandle) => {
|
||||
const expectVal = vm.dump(expectValueHandle)
|
||||
|
||||
return {
|
||||
value: createExpectation(vm, expectVal, false, testRunStack),
|
||||
}
|
||||
})
|
||||
|
||||
// Marshal response object
|
||||
const responseObjHandle = marshalObjectToVM(vm, response)
|
||||
if (E.isLeft(responseObjHandle))
|
||||
return TE.left(
|
||||
`Response marshalling failed: ${responseObjHandle.left}`
|
||||
)
|
||||
|
||||
vm.setProp(pwHandle, "response", responseObjHandle.right)
|
||||
responseObjHandle.right.dispose()
|
||||
|
||||
vm.setProp(pwHandle, "expect", expectFnHandle)
|
||||
expectFnHandle.dispose()
|
||||
|
||||
vm.setProp(pwHandle, "test", testFuncHandle)
|
||||
testFuncHandle.dispose()
|
||||
|
||||
// Environment management APIs
|
||||
// TODO: Unified Implementation
|
||||
const envHandle = vm.newObject()
|
||||
|
||||
const envGetHandle = vm.newFunction("get", (keyHandle) => {
|
||||
const key: unknown = vm.dump(keyHandle)
|
||||
|
||||
if (typeof key !== "string") {
|
||||
return {
|
||||
error: vm.newString("Expected key to be a string"),
|
||||
}
|
||||
}
|
||||
|
||||
const result = pipe(
|
||||
getEnv(key, currentEnvs),
|
||||
O.match(
|
||||
() => vm.undefined,
|
||||
({ value }) => vm.newString(value)
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
value: result,
|
||||
}
|
||||
})
|
||||
|
||||
const envGetResolveHandle = vm.newFunction(
|
||||
"getResolve",
|
||||
(keyHandle) => {
|
||||
const key: unknown = vm.dump(keyHandle)
|
||||
|
||||
if (typeof key !== "string") {
|
||||
return {
|
||||
error: vm.newString("Expected key to be a string"),
|
||||
}
|
||||
}
|
||||
|
||||
const result = pipe(
|
||||
getEnv(key, currentEnvs),
|
||||
E.fromOption(() => "INVALID_KEY" as const),
|
||||
|
||||
E.map(({ value }) =>
|
||||
pipe(
|
||||
parseTemplateStringE(value, [
|
||||
...envs.selected,
|
||||
...envs.global,
|
||||
]),
|
||||
// If the recursive resolution failed, return the unresolved value
|
||||
E.getOrElse(() => value)
|
||||
)
|
||||
),
|
||||
|
||||
// Create a new VM String
|
||||
// NOTE: Do not shorten this to map(vm.newString) apparently it breaks it
|
||||
E.map((x) => vm.newString(x)),
|
||||
|
||||
E.getOrElse(() => vm.undefined)
|
||||
)
|
||||
|
||||
return {
|
||||
value: result,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const envSetHandle = vm.newFunction("set", (keyHandle, valueHandle) => {
|
||||
const key: unknown = vm.dump(keyHandle)
|
||||
const value: unknown = vm.dump(valueHandle)
|
||||
|
||||
if (typeof key !== "string") {
|
||||
return {
|
||||
error: vm.newString("Expected key to be a string"),
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof value !== "string") {
|
||||
return {
|
||||
error: vm.newString("Expected value to be a string"),
|
||||
}
|
||||
}
|
||||
|
||||
currentEnvs = setEnv(key, value, currentEnvs)
|
||||
|
||||
return {
|
||||
value: vm.undefined,
|
||||
}
|
||||
})
|
||||
|
||||
const envResolveHandle = vm.newFunction("resolve", (valueHandle) => {
|
||||
const value: unknown = vm.dump(valueHandle)
|
||||
|
||||
if (typeof value !== "string") {
|
||||
return {
|
||||
error: vm.newString("Expected value to be a string"),
|
||||
}
|
||||
}
|
||||
|
||||
const result = pipe(
|
||||
parseTemplateStringE(value, [
|
||||
...currentEnvs.selected,
|
||||
...currentEnvs.global,
|
||||
]),
|
||||
E.getOrElse(() => value)
|
||||
)
|
||||
|
||||
return {
|
||||
value: vm.newString(result),
|
||||
}
|
||||
})
|
||||
|
||||
vm.setProp(envHandle, "resolve", envResolveHandle)
|
||||
envResolveHandle.dispose()
|
||||
|
||||
vm.setProp(envHandle, "set", envSetHandle)
|
||||
envSetHandle.dispose()
|
||||
|
||||
vm.setProp(envHandle, "getResolve", envGetResolveHandle)
|
||||
envGetResolveHandle.dispose()
|
||||
|
||||
vm.setProp(envHandle, "get", envGetHandle)
|
||||
envGetHandle.dispose()
|
||||
|
||||
vm.setProp(pwHandle, "env", envHandle)
|
||||
envHandle.dispose()
|
||||
|
||||
vm.setProp(vm.global, "pw", pwHandle)
|
||||
pwHandle.dispose()
|
||||
|
||||
const evalRes = vm.evalCode(testScript)
|
||||
|
||||
if (evalRes.error) {
|
||||
const errorData = vm.dump(evalRes.error)
|
||||
evalRes.error.dispose()
|
||||
|
||||
return TE.left(`Script evaluation failed: ${errorData}`)
|
||||
}
|
||||
|
||||
vm.dispose()
|
||||
|
||||
return TE.right({
|
||||
tests: testRunStack,
|
||||
envs: currentEnvs,
|
||||
})
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -1,55 +0,0 @@
|
||||
import * as E from "fp-ts/Either"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import { createContext, runInContext } from "vm"
|
||||
|
||||
import { TestResponse, TestResult } from "~/types"
|
||||
import { getTestRunnerScriptMethods, preventCyclicObjects } from "~/utils"
|
||||
|
||||
export const runTestScript = (
|
||||
testScript: string,
|
||||
envs: TestResult["envs"],
|
||||
response: TestResponse
|
||||
): TE.TaskEither<string, TestResult> =>
|
||||
pipe(
|
||||
TE.tryCatch(
|
||||
async () => {
|
||||
return createContext()
|
||||
},
|
||||
(reason) => `Context initialization failed: ${reason}`
|
||||
),
|
||||
TE.chain((context) =>
|
||||
TE.tryCatch(
|
||||
() => executeScriptInContext(testScript, envs, response, context),
|
||||
(reason) => `Script execution failed: ${reason}`
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
const executeScriptInContext = (
|
||||
testScript: string,
|
||||
envs: TestResult["envs"],
|
||||
response: TestResponse,
|
||||
context: any
|
||||
): Promise<TestResult> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Parse response object
|
||||
const responseObjHandle = preventCyclicObjects(response)
|
||||
if (E.isLeft(responseObjHandle)) {
|
||||
return reject(`Response parsing failed: ${responseObjHandle.left}`)
|
||||
}
|
||||
|
||||
const { pw, testRunStack, updatedEnvs } = getTestRunnerScriptMethods(envs)
|
||||
|
||||
// Expose pw to the context
|
||||
context.pw = { ...pw, response: responseObjHandle.right }
|
||||
|
||||
// Run the test script in the provided context
|
||||
runInContext(testScript, context)
|
||||
|
||||
resolve({
|
||||
tests: testRunStack,
|
||||
envs: updatedEnvs,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import * as E from "fp-ts/Either"
|
||||
|
||||
import { SandboxTestResult, TestResponse, TestResult } from "~/types"
|
||||
|
||||
import Worker from "./worker?worker&inline"
|
||||
|
||||
export const runTestScript = (
|
||||
testScript: string,
|
||||
envs: TestResult["envs"],
|
||||
response: TestResponse
|
||||
): Promise<E.Either<string, SandboxTestResult>> => {
|
||||
return new Promise((resolve) => {
|
||||
const worker = new Worker()
|
||||
|
||||
// Listen for the results from the web worker
|
||||
worker.addEventListener("message", (event: MessageEvent) =>
|
||||
resolve(event.data.results)
|
||||
)
|
||||
|
||||
// Send the script to the web worker
|
||||
worker.postMessage({
|
||||
testScript,
|
||||
envs,
|
||||
response,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import * as E from "fp-ts/Either"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
|
||||
import { SandboxTestResult, TestResponse, TestResult } from "~/types"
|
||||
import { getTestRunnerScriptMethods, preventCyclicObjects } from "~/utils"
|
||||
|
||||
const executeScriptInContext = (
|
||||
testScript: string,
|
||||
envs: TestResult["envs"],
|
||||
response: TestResponse
|
||||
): TE.TaskEither<string, SandboxTestResult> => {
|
||||
try {
|
||||
const responseObjHandle = preventCyclicObjects(response)
|
||||
if (E.isLeft(responseObjHandle)) {
|
||||
return TE.left(`Response marshalling failed: ${responseObjHandle.left}`)
|
||||
}
|
||||
|
||||
const { pw, testRunStack, updatedEnvs } = getTestRunnerScriptMethods(envs)
|
||||
|
||||
// Create a function from the test script using the `Function` constructor
|
||||
const executeScript = new Function("pw", testScript)
|
||||
|
||||
// Execute the script
|
||||
executeScript({ ...pw, response: responseObjHandle.right })
|
||||
|
||||
return TE.right(<SandboxTestResult>{
|
||||
tests: testRunStack[0],
|
||||
envs: updatedEnvs,
|
||||
})
|
||||
} catch (error) {
|
||||
return TE.left(`Script execution failed: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for messages from the main thread
|
||||
self.addEventListener("message", async (event) => {
|
||||
const { testScript, envs, response } = event.data
|
||||
|
||||
const results = await executeScriptInContext(testScript, envs, response)()
|
||||
|
||||
// Post the result back to the main thread
|
||||
self.postMessage({ results })
|
||||
})
|
||||
@@ -1,61 +0,0 @@
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
|
||||
/**
|
||||
* The response object structure exposed to the test script
|
||||
*/
|
||||
export type TestResponse = {
|
||||
/** Status Code of the response */
|
||||
status: number
|
||||
/** List of headers returned */
|
||||
headers: { key: string; value: string }[]
|
||||
/**
|
||||
* Body of the response, this will be the JSON object if it is a JSON content type, else body string
|
||||
*/
|
||||
body: string | object
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of an expectation statement
|
||||
*/
|
||||
export type ExpectResult = {
|
||||
status: "pass" | "fail" | "error"
|
||||
message: string
|
||||
} // The expectation failed (fail) or errored (error)
|
||||
|
||||
/**
|
||||
* An object defining the result of the execution of a
|
||||
* test block
|
||||
*/
|
||||
export type TestDescriptor = {
|
||||
/**
|
||||
* The name of the test block
|
||||
*/
|
||||
descriptor: string
|
||||
|
||||
/**
|
||||
* Expectation results of the test block
|
||||
*/
|
||||
expectResults: ExpectResult[]
|
||||
|
||||
/**
|
||||
* Children test blocks (test blocks inside the test block)
|
||||
*/
|
||||
children: TestDescriptor[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the result of a test script execution
|
||||
*/
|
||||
|
||||
export type TestResult = {
|
||||
tests: TestDescriptor[]
|
||||
envs: {
|
||||
global: Environment["variables"]
|
||||
selected: Environment["variables"]
|
||||
}
|
||||
}
|
||||
|
||||
export type GlobalEnvItem = TestResult["envs"]["global"][number]
|
||||
export type SelectedEnvItem = TestResult["envs"]["selected"][number]
|
||||
|
||||
export type SandboxTestResult = TestResult & { tests: TestDescriptor }
|
||||
@@ -1,439 +1,89 @@
|
||||
import { parseTemplateStringE } from "@hoppscotch/data"
|
||||
import * as E from "fp-ts/Either"
|
||||
import * as O from "fp-ts/Option"
|
||||
import { pipe } from "fp-ts/lib/function"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import * as E from "fp-ts/Either"
|
||||
import * as QuickJS from "quickjs-emscripten"
|
||||
import { TestResult } from "./test-runner"
|
||||
|
||||
import {
|
||||
GlobalEnvItem,
|
||||
SelectedEnvItem,
|
||||
TestDescriptor,
|
||||
TestResult,
|
||||
} from "./types"
|
||||
|
||||
const getEnv = (envName: string, envs: TestResult["envs"]) => {
|
||||
return O.fromNullable(
|
||||
envs.selected.find((x: SelectedEnvItem) => x.key === envName) ??
|
||||
envs.global.find((x: GlobalEnvItem) => x.key === envName)
|
||||
)
|
||||
}
|
||||
|
||||
// Compiles shared scripting API methods for use in both pre and post request scripts
|
||||
const getSharedMethods = (envs: TestResult["envs"]) => {
|
||||
let updatedEnvs = envs
|
||||
|
||||
const envGetFn = (key: any) => {
|
||||
if (typeof key !== "string") {
|
||||
throw new Error("Expected key to be a string")
|
||||
}
|
||||
|
||||
const result = pipe(
|
||||
getEnv(key, updatedEnvs),
|
||||
O.match(
|
||||
() => undefined,
|
||||
({ value }) => String(value)
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const envGetResolveFn = (key: any) => {
|
||||
if (typeof key !== "string") {
|
||||
throw new Error("Expected key to be a string")
|
||||
}
|
||||
|
||||
const result = pipe(
|
||||
getEnv(key, updatedEnvs),
|
||||
E.fromOption(() => "INVALID_KEY" as const),
|
||||
|
||||
E.map(({ value }) =>
|
||||
pipe(
|
||||
parseTemplateStringE(value, [
|
||||
...updatedEnvs.selected,
|
||||
...updatedEnvs.global,
|
||||
]),
|
||||
// If the recursive resolution failed, return the unresolved value
|
||||
E.getOrElse(() => value)
|
||||
)
|
||||
),
|
||||
E.map((x) => String(x)),
|
||||
|
||||
E.getOrElseW(() => undefined)
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const envSetFn = (key: any, value: any) => {
|
||||
if (typeof key !== "string") {
|
||||
throw new Error("Expected key to be a string")
|
||||
}
|
||||
|
||||
if (typeof value !== "string") {
|
||||
throw new Error("Expected value to be a string")
|
||||
}
|
||||
|
||||
updatedEnvs = setEnv(key, value, updatedEnvs)
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const envResolveFn = (value: any) => {
|
||||
if (typeof value !== "string") {
|
||||
throw new Error("Expected value to be a string")
|
||||
}
|
||||
|
||||
const result = pipe(
|
||||
parseTemplateStringE(value, [
|
||||
...updatedEnvs.selected,
|
||||
...updatedEnvs.global,
|
||||
]),
|
||||
E.getOrElse(() => value)
|
||||
)
|
||||
|
||||
return String(result)
|
||||
}
|
||||
|
||||
return {
|
||||
methods: {
|
||||
env: {
|
||||
get: envGetFn,
|
||||
getResolve: envGetResolveFn,
|
||||
set: envSetFn,
|
||||
resolve: envResolveFn,
|
||||
},
|
||||
},
|
||||
updatedEnvs,
|
||||
}
|
||||
}
|
||||
|
||||
const setEnv = (
|
||||
envName: string,
|
||||
envValue: string,
|
||||
envs: TestResult["envs"]
|
||||
): TestResult["envs"] => {
|
||||
const { global, selected } = envs
|
||||
|
||||
const indexInSelected = selected.findIndex(
|
||||
(x: SelectedEnvItem) => x.key === envName
|
||||
)
|
||||
|
||||
// Found the match in selected
|
||||
if (indexInSelected >= 0) {
|
||||
selected[indexInSelected].value = envValue
|
||||
|
||||
return {
|
||||
global,
|
||||
selected,
|
||||
}
|
||||
}
|
||||
|
||||
const indexInGlobal = global.findIndex((x: GlobalEnvItem) => x.key == envName)
|
||||
|
||||
// Found a match in globals
|
||||
if (indexInGlobal >= 0) {
|
||||
global[indexInGlobal].value = envValue
|
||||
|
||||
return {
|
||||
global,
|
||||
selected,
|
||||
}
|
||||
}
|
||||
|
||||
// Didn't find in both places, create a new variable in selected
|
||||
selected.push({
|
||||
key: envName,
|
||||
value: envValue,
|
||||
})
|
||||
|
||||
return {
|
||||
global,
|
||||
selected,
|
||||
}
|
||||
}
|
||||
|
||||
export function preventCyclicObjects(
|
||||
obj: Record<string, any>
|
||||
): E.Left<string> | E.Right<Record<string, any>> {
|
||||
export function marshalObjectToVM(
|
||||
vm: QuickJS.QuickJSVm,
|
||||
obj: object
|
||||
): E.Either<string, QuickJS.QuickJSHandle> {
|
||||
let jsonString
|
||||
|
||||
try {
|
||||
jsonString = JSON.stringify(obj)
|
||||
} catch (e) {
|
||||
return E.left("Stringification failed")
|
||||
return E.left("Marshaling stringification failed")
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedJson = JSON.parse(jsonString)
|
||||
return E.right(parsedJson)
|
||||
} catch (err) {
|
||||
return E.left("Parsing failed")
|
||||
const vmStringHandle = vm.newString(jsonString)
|
||||
|
||||
const jsonHandle = vm.getProp(vm.global, "JSON")
|
||||
const parseFuncHandle = vm.getProp(jsonHandle, "parse")
|
||||
|
||||
const parseResultHandle = vm.callFunction(
|
||||
parseFuncHandle,
|
||||
vm.undefined,
|
||||
vmStringHandle
|
||||
)
|
||||
|
||||
if (parseResultHandle.error) {
|
||||
parseResultHandle.error.dispose()
|
||||
return E.left("Marshaling failed")
|
||||
}
|
||||
|
||||
const resultHandle = vm.unwrapResult(parseResultHandle)
|
||||
|
||||
vmStringHandle.dispose()
|
||||
parseFuncHandle.dispose()
|
||||
jsonHandle.dispose()
|
||||
|
||||
return E.right(resultHandle)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an Expectation object for use inside the sandbox
|
||||
* @param expectVal The expecting value of the expectation
|
||||
* @param negated Whether the expectation is negated (negative)
|
||||
* @param currTestStack The current state of the test execution stack
|
||||
* @returns Object with the expectation methods
|
||||
*/
|
||||
export const createExpectation = (
|
||||
expectVal: any,
|
||||
negated: boolean,
|
||||
currTestStack: TestDescriptor[]
|
||||
) => {
|
||||
const result: Record<string, unknown> = {}
|
||||
export function getEnv(envName: string, envs: TestResult["envs"]) {
|
||||
return O.fromNullable(
|
||||
envs.selected.find((x) => x.key === envName) ??
|
||||
envs.global.find((x) => x.key === envName)
|
||||
)
|
||||
}
|
||||
|
||||
const toBeFn = (expectedVal: any) => {
|
||||
let assertion = expectVal === expectedVal
|
||||
export function setEnv(
|
||||
envName: string,
|
||||
envValue: string,
|
||||
envs: TestResult["envs"]
|
||||
): TestResult["envs"] {
|
||||
const indexInSelected = envs.selected.findIndex((x) => x.key === envName)
|
||||
|
||||
if (negated) {
|
||||
assertion = !assertion
|
||||
// Found the match in selected
|
||||
if (indexInSelected >= 0) {
|
||||
envs.selected[indexInSelected].value = envValue
|
||||
|
||||
return {
|
||||
global: envs.global,
|
||||
selected: envs.selected,
|
||||
}
|
||||
|
||||
const status = assertion ? "pass" : "fail"
|
||||
const message = `Expected '${expectVal}' to${
|
||||
negated ? " not" : ""
|
||||
} be '${expectedVal}'`
|
||||
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status,
|
||||
message,
|
||||
})
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const toBeLevelXxx = (
|
||||
level: string,
|
||||
rangeStart: number,
|
||||
rangeEnd: number
|
||||
) => {
|
||||
const parsedExpectVal = parseInt(expectVal)
|
||||
const indexInGlobal = envs.global.findIndex((x) => x.key == envName)
|
||||
|
||||
if (!Number.isNaN(parsedExpectVal)) {
|
||||
let assertion =
|
||||
parsedExpectVal >= rangeStart && parsedExpectVal <= rangeEnd
|
||||
// Found a match in globals
|
||||
if (indexInGlobal >= 0) {
|
||||
envs.global[indexInGlobal].value = envValue
|
||||
|
||||
if (negated) {
|
||||
assertion = !assertion
|
||||
}
|
||||
|
||||
const status = assertion ? "pass" : "fail"
|
||||
const message = `Expected '${parsedExpectVal}' to${
|
||||
negated ? " not" : ""
|
||||
} be ${level}-level status`
|
||||
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status,
|
||||
message,
|
||||
})
|
||||
} else {
|
||||
const message = `Expected ${level}-level status but could not parse value '${expectVal}'`
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message,
|
||||
})
|
||||
return {
|
||||
global: envs.global,
|
||||
selected: envs.selected,
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const toBeLevel2xxFn = () => toBeLevelXxx("200", 200, 299)
|
||||
const toBeLevel3xxFn = () => toBeLevelXxx("300", 300, 399)
|
||||
const toBeLevel4xxFn = () => toBeLevelXxx("400", 400, 499)
|
||||
const toBeLevel5xxFn = () => toBeLevelXxx("500", 500, 599)
|
||||
|
||||
const toBeTypeFn = (expectedType: any) => {
|
||||
if (
|
||||
[
|
||||
"string",
|
||||
"boolean",
|
||||
"number",
|
||||
"object",
|
||||
"undefined",
|
||||
"bigint",
|
||||
"symbol",
|
||||
"function",
|
||||
].includes(expectedType)
|
||||
) {
|
||||
let assertion = typeof expectVal === expectedType
|
||||
|
||||
if (negated) {
|
||||
assertion = !assertion
|
||||
}
|
||||
|
||||
const status = assertion ? "pass" : "fail"
|
||||
const message = `Expected '${expectVal}' to${
|
||||
negated ? " not" : ""
|
||||
} be type '${expectedType}'`
|
||||
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status,
|
||||
message,
|
||||
})
|
||||
} else {
|
||||
const message =
|
||||
'Argument for toBeType should be "string", "boolean", "number", "object", "undefined", "bigint", "symbol" or "function"'
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message,
|
||||
})
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const toHaveLengthFn = (expectedLength: any) => {
|
||||
if (!(Array.isArray(expectVal) || typeof expectVal === "string")) {
|
||||
const message =
|
||||
"Expected toHaveLength to be called for an array or string"
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message,
|
||||
})
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (typeof expectedLength === "number" && !Number.isNaN(expectedLength)) {
|
||||
let assertion = expectVal.length === expectedLength
|
||||
|
||||
if (negated) {
|
||||
assertion = !assertion
|
||||
}
|
||||
|
||||
const status = assertion ? "pass" : "fail"
|
||||
const message = `Expected the array to${
|
||||
negated ? " not" : ""
|
||||
} be of length '${expectedLength}'`
|
||||
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status,
|
||||
message,
|
||||
})
|
||||
} else {
|
||||
const message = "Argument for toHaveLength should be a number"
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message,
|
||||
})
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const toIncludeFn = (needle: any) => {
|
||||
if (!(Array.isArray(expectVal) || typeof expectVal === "string")) {
|
||||
const message = "Expected toInclude to be called for an array or string"
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message,
|
||||
})
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (needle === null) {
|
||||
const message = "Argument for toInclude should not be null"
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message,
|
||||
})
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (needle === undefined) {
|
||||
const message = "Argument for toInclude should not be undefined"
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message,
|
||||
})
|
||||
return undefined
|
||||
}
|
||||
|
||||
let assertion = expectVal.includes(needle)
|
||||
|
||||
if (negated) {
|
||||
assertion = !assertion
|
||||
}
|
||||
|
||||
const expectValPretty = JSON.stringify(expectVal)
|
||||
const needlePretty = JSON.stringify(needle)
|
||||
const status = assertion ? "pass" : "fail"
|
||||
const message = `Expected ${expectValPretty} to${
|
||||
negated ? " not" : ""
|
||||
} include ${needlePretty}`
|
||||
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status,
|
||||
message,
|
||||
})
|
||||
return undefined
|
||||
}
|
||||
|
||||
result.toBe = toBeFn
|
||||
result.toBeLevel2xx = toBeLevel2xxFn
|
||||
result.toBeLevel3xx = toBeLevel3xxFn
|
||||
result.toBeLevel4xx = toBeLevel4xxFn
|
||||
result.toBeLevel5xx = toBeLevel5xxFn
|
||||
result.toBeType = toBeTypeFn
|
||||
result.toHaveLength = toHaveLengthFn
|
||||
result.toInclude = toIncludeFn
|
||||
|
||||
Object.defineProperties(result, {
|
||||
not: {
|
||||
get: () => createExpectation(expectVal, !negated, currTestStack),
|
||||
},
|
||||
// Didn't find in both places, create a new variable in selected
|
||||
envs.selected.push({
|
||||
key: envName,
|
||||
value: envValue,
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles methods for use under the `pw` namespace for pre request scripts
|
||||
* @param envs The current state of the environment variables
|
||||
* @returns Object with methods in the `pw` namespace
|
||||
*/
|
||||
export const getPreRequestScriptMethods = (envs: TestResult["envs"]) => {
|
||||
const { methods, updatedEnvs } = getSharedMethods(cloneDeep(envs))
|
||||
return { pw: methods, updatedEnvs }
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles methods for use under the `pw` namespace for post request scripts
|
||||
* @param envs The current state of the environment variables
|
||||
* @returns Object with methods in the `pw` namespace and test run stack
|
||||
*/
|
||||
export const getTestRunnerScriptMethods = (envs: TestResult["envs"]) => {
|
||||
const testRunStack: TestDescriptor[] = [
|
||||
{ descriptor: "root", expectResults: [], children: [] },
|
||||
]
|
||||
|
||||
const testFn = (descriptor: string, testFunc: () => void) => {
|
||||
testRunStack.push({
|
||||
descriptor,
|
||||
expectResults: [],
|
||||
children: [],
|
||||
})
|
||||
|
||||
testFunc()
|
||||
|
||||
const child = testRunStack.pop() as TestDescriptor
|
||||
testRunStack[testRunStack.length - 1].children.push(child)
|
||||
return {
|
||||
global: envs.global,
|
||||
selected: envs.selected,
|
||||
}
|
||||
|
||||
const expectFn = (expectVal: any) =>
|
||||
createExpectation(expectVal, false, testRunStack)
|
||||
|
||||
const { methods, updatedEnvs } = getSharedMethods(cloneDeep(envs))
|
||||
|
||||
const pw = {
|
||||
...methods,
|
||||
expect: expectFn,
|
||||
test: testFn,
|
||||
}
|
||||
|
||||
return { pw, testRunStack, updatedEnvs }
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./pre-request/web-worker"
|
||||
export * from "./test-runner/web-worker"
|
||||
@@ -1,17 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"target": "ES6",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "Node",
|
||||
"skipLibCheck": true,
|
||||
"lib": ["ESNext", "ESNext.AsyncIterable", "DOM"],
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"paths": {
|
||||
"~/*": ["./src/*"]
|
||||
"~/*": ["./src/*"],
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"types": ["@types/node", "@types/jest", "@relmify/jest-fp-ts", "vite/client"],
|
||||
"outDir": "./dist/",
|
||||
"types": ["@types/node", "@types/jest", "@relmify/jest-fp-ts"],
|
||||
"outDir": "./lib/",
|
||||
"rootDir": "./src/",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
|
||||
11
packages/hoppscotch-js-sandbox/tsup.config.ts
Normal file
11
packages/hoppscotch-js-sandbox/tsup.config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from "tsup"
|
||||
|
||||
export default defineConfig({
|
||||
entry: ["src/index.ts"],
|
||||
outDir: "./lib/",
|
||||
format: ["esm", "cjs"],
|
||||
dts: true,
|
||||
splitting: true,
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
})
|
||||
@@ -1,25 +0,0 @@
|
||||
import { resolve } from "path"
|
||||
import { defineConfig } from "vite"
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
outDir: "./dist",
|
||||
emptyOutDir: true,
|
||||
lib: {
|
||||
entry: {
|
||||
web: "./src/web.ts",
|
||||
node: "./src/node.ts",
|
||||
},
|
||||
name: "js-sandbox",
|
||||
formats: ["es", "cjs"],
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ["vm"],
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"~": resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
})
|
||||
2
packages/hoppscotch-js-sandbox/web.d.ts
vendored
2
packages/hoppscotch-js-sandbox/web.d.ts
vendored
@@ -1,2 +0,0 @@
|
||||
export { default } from "./dist/web.d.ts"
|
||||
export * from "./dist/web.d.ts"
|
||||
@@ -54,7 +54,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col overflow-y-auto max-h-[50vh]"
|
||||
class="max-h-lg flex flex-col overflow-y-auto"
|
||||
:class="{ 'p-4': !fullWidth }"
|
||||
>
|
||||
<slot name="body"></slot>
|
||||
|
||||
@@ -37,9 +37,8 @@
|
||||
:is="tabMeta.icon"
|
||||
v-if="tabMeta.icon"
|
||||
class="svg-icons"
|
||||
:class="{ 'mr-2': tabMeta.label && !vertical }"
|
||||
/>
|
||||
<span v-if="tabMeta.label && !vertical">{{ tabMeta.label }}</span>
|
||||
<span v-else-if="tabMeta.label">{{ tabMeta.label }}</span>
|
||||
<span
|
||||
v-if="tabMeta.info && tabMeta.info !== 'null'"
|
||||
class="tab-info"
|
||||
|
||||
4539
pnpm-lock.yaml
generated
4539
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user