Compare commits

..

11 Commits

Author SHA1 Message Date
Liyas Thomas
502da61b8b fix: ui styles 2023-12-07 14:43:22 +05:30
nivedin
f2777a9a75 chore: cleanup 2023-12-07 14:43:22 +05:30
nivedin
4bd3e89f89 refactor: update embed customize flow 2023-12-07 14:43:22 +05:30
nivedin
09e9601940 chore: add save option to modify sahred request 2023-12-07 14:43:22 +05:30
nivedin
fd4a5c626f chore: disable method and input 2023-12-07 14:43:22 +05:30
nivedin
67cfef82af chore: cleanup 2023-12-07 14:43:22 +05:30
nivedin
aa18249791 chore: use shared request properties for embed 2023-12-07 14:43:22 +05:30
nivedin
9d8fdb4d04 chore: add doc button if response is null 2023-12-07 14:43:22 +05:30
nivedin
fbca9b06c3 chore: add open app link 2023-12-07 14:43:22 +05:30
nivedin
bb0bf35164 feat: added embeds components wip 2023-12-07 14:43:22 +05:30
nivedin
5a35c098ec feat: mutations and small refactor in share req 2023-12-07 14:43:22 +05:30
66 changed files with 3569 additions and 3054 deletions

View File

@@ -32,9 +32,6 @@
"lint-staged": "12.4.0"
},
"pnpm": {
"overrides": {
"vue": "3.3.9"
},
"packageExtensions": {
"httpsnippet@^3.0.1": {
"peerDependencies": {

View File

@@ -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

View File

@@ -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",

View File

@@ -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");
});

View File

@@ -1,6 +1,7 @@
{
"URL": "https://echo.hoppscotch.io",
"HOST": "echo.hoppscotch.io",
"X-COUNTRY": "IN",
"BODY_VALUE": "body_value",
"BODY_KEY": "body_key"
}

View File

@@ -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}"

View File

@@ -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

View File

@@ -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)
)
)
),

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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']

View File

@@ -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)
}
}
})

View File

@@ -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>()

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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") }}

View File

@@ -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 `[![Run in Hoppscotch](${baseURL}/${badge})](${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 = ({

View File

@@ -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

View File

@@ -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")
}
}
}

View File

@@ -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="{

View File

@@ -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

View File

@@ -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(

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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
}
>
}

View File

@@ -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)
}
}
/**

View File

@@ -57,7 +57,6 @@ export type SpotlightSearcherResult = {
* The keyboard shortcut to trigger the result
*/
keyboardShortcut?: string[]
additionalInfo?: unknown
}
}

View File

@@ -1,2 +0,0 @@
export { default } from "./dist/types/index.d.ts"
export * from "./dist/types/index.d.ts"

View File

@@ -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",
},
}

View File

@@ -1,2 +0,0 @@
export { default } from "./dist/node.d.ts"
export * from "./dist/node.d.ts"

View File

@@ -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"
}
}

View File

@@ -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")
`,

View File

@@ -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)
)

View File

@@ -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)
)

View File

@@ -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)
)

View File

@@ -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)
)

View File

@@ -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)
)

View File

@@ -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)
)

View File

@@ -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)
)

View File

@@ -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)
)

View File

@@ -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)
)

View File

@@ -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)
)

View File

@@ -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()
})
})

View 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

View File

@@ -1,2 +0,0 @@
export * from "./pre-request/node-vm"
export * from "./test-runner/node-vm"

View File

@@ -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}`
)
)
)

View File

@@ -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,
})
})

View File

@@ -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 })
})

View 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)
})
)

View 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,
})
}
)
)

View File

@@ -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,
})
})
}

View File

@@ -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,
})
})
}

View File

@@ -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 })
})

View File

@@ -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 }

View File

@@ -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 }
}

View File

@@ -1,2 +0,0 @@
export * from "./pre-request/web-worker"
export * from "./test-runner/web-worker"

View File

@@ -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

View 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,
})

View File

@@ -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"),
},
},
})

View File

@@ -1,2 +0,0 @@
export { default } from "./dist/web.d.ts"
export * from "./dist/web.d.ts"

View File

@@ -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>

View File

@@ -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

File diff suppressed because it is too large Load Diff