Compare commits

..

13 Commits

Author SHA1 Message Date
jamesgeorge007
019c2cec46 refactor: remove unnecessary check for slot contents 2023-12-12 14:26:13 +05:30
Liyas Thomas
40b9508361 chore: use template wrapper for slot 2023-12-12 14:26:13 +05:30
Liyas Thomas
85285a5204 fix: search input height 2023-12-12 14:26:13 +05:30
Liyas Thomas
c0c0c37a67 chore: improve placeholder component styles 2023-12-12 14:26:13 +05:30
Liyas Thomas
4ac8a117ef chore: add protocol logos to realtime page (#3637) 2023-12-12 14:25:56 +05:30
Muhammed Ajmal M
c1bc430ee6 fix: overflowing modal fix on small screens (#3643)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-12-12 14:21:19 +05:30
James George
9201aa7d7d fix(common): ensure banner colors are displayed correctly (#3630) 2023-12-12 13:55:18 +05:30
Liyas Thomas
87395a4553 fix: minor ui issues 2023-12-07 17:12:33 +05:30
Andrew Bastin
6063c633ee chore: pin vue workspace-wide to 3.3.9 2023-12-07 17:04:17 +05:30
Akash K
7481feb366 feat: introduce platform defs for adding additional spotlight searchers (#3631) 2023-12-07 16:28:02 +05:30
James George
bdfa14fa54 refactor(scripting-revamp): migrate js-sandbox to web worker/Node vm based implementation (#3619) 2023-12-07 16:10:42 +05:30
Andrew Bastin
0a61ec2bfe fix: useI18n type breaking 2023-12-07 15:17:41 +05:30
Nivedin
2bf0106aa2 feat: embeds (#3627)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-12-07 15:03:49 +05:30
105 changed files with 3519 additions and 3968 deletions

View File

@@ -32,6 +32,9 @@
"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__/**/*.*.ts",
"**/src/__tests__/commands/**/*.*.ts",
],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped

View File

@@ -19,8 +19,9 @@
"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",
"test": "pnpm run build && jest && rm -rf dist"
"do-test": "pnpm test"
},
"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 } = await execAsync(cmd);
const { error, stdout } = await execAsync(cmd);
expect(error).toBeNull();
});
@@ -129,7 +129,6 @@ 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,7 +1,6 @@
{
"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 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})",
"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})",
"body": {
"contentType": "application/json",
"body": "{\n \"<<BODY_KEY>>\":\"<<BODY_VALUE>>\"\n}"

View File

@@ -6,23 +6,24 @@ import {
parseTemplateString,
parseTemplateStringE,
} from "@hoppscotch/data";
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 { 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 { flow, pipe } from "fp-ts/function";
import * as S from "fp-ts/string";
import qs from "qs";
import { EffectiveHoppRESTRequest } from "../interfaces/request";
import { error, HoppCLIError } from "../types/errors";
import { HoppCLIError, error } from "../types/errors";
import { HoppEnvs } from "../types/request";
import { isHoppCLIError } from "./checks";
import { tupleToRecord, arraySort, arrayFlatMap } from "./functions/array";
import { toFormData } from "./mutators";
import { getEffectiveFinalMetaData } from "./getters";
import { PreRequestMetrics } from "../types/response";
import { isHoppCLIError } from "./checks";
import { arrayFlatMap, arraySort, tupleToRecord } from "./functions/array";
import { getEffectiveFinalMetaData } from "./getters";
import { toFormData } from "./mutators";
/**
* Runs pre-request-script runner over given request which extracts set ENVs and

View File

@@ -1,17 +1,19 @@
import { HoppRESTRequest } from "@hoppscotch/data";
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 { TestDescriptor } from "@hoppscotch/js-sandbox";
import { runTestScript } from "@hoppscotch/js-sandbox/node";
import * as A from "fp-ts/Array";
import * as TE from "fp-ts/TaskEither";
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 { hrtime } from "process";
import {
RequestRunnerResponse,
TestReport,
TestScriptParams,
} from "../interfaces/response";
import { error, HoppCLIError } from "../types/errors";
import { HoppCLIError, error } from "../types/errors";
import { HoppEnvs } from "../types/request";
import { ExpectResult, TestMetrics, TestRunnerRes } from "../types/response";
import { getDurationInSeconds } from "./getters";
@@ -36,7 +38,7 @@ export const testRunner = (
pipe(
TE.of(testScriptData),
TE.chain(({ testScript, response, envs }) =>
execTestScript(testScript, envs, response)
runTestScript(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" 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>
<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>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1017 B

View File

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 684 B

View File

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 633 B

View File

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 610 B

View File

@@ -164,6 +164,12 @@ 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,10 +305,9 @@ watch(isOnline, () => {
if (!isOnline.value) {
bannerID = banner.showBanner(offlineBanner)
return
} else {
if (banner.content && bannerID) {
banner.removeBanner(bannerID)
}
}
if (banner.content && bannerID) {
banner.removeBanner(bannerID)
}
})

View File

@@ -17,9 +17,10 @@
v-if="isEmpty(shortcutsResults)"
:text="`${t('state.nothing_found')} ‟${filterText}”`"
>
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
<template #icon>
<icon-lucide-search class="svg-icons opacity-75" />
</template>
</HoppSmartPlaceholder>
<details
v-for="(sectionResults, sectionTitle) in shortcutsResults"
v-else

View File

@@ -49,13 +49,15 @@
:text="`${t('state.nothing_found')} ‟${search}”`"
>
<template #icon>
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
<icon-lucide-search class="svg-icons opacity-75" />
</template>
<template #body>
<HoppButtonSecondary
:label="t('action.clear')"
outline
@click="search = ''"
/>
</template>
<HoppButtonSecondary
:label="t('action.clear')"
outline
@click="search = ''"
/>
</HoppSmartPlaceholder>
</div>
<div
@@ -112,6 +114,7 @@ import {
WorkspaceSpotlightSearcherService,
} from "~/services/spotlight/searchers/workspace.searcher"
import { InterceptorSpotlightSearcherService } from "~/services/spotlight/searchers/interceptor.searcher"
import { platform } from "~/platform"
const t = useI18n()
@@ -141,6 +144,10 @@ useService(WorkspaceSpotlightSearcherService)
useService(SwitchWorkspaceSpotlightSearcherService)
useService(InterceptorSpotlightSearcherService)
platform.spotlight?.additionalSearchers?.forEach((searcher) =>
useService(searcher)
)
const search = ref("")
const searchSession = ref<SpotlightSearchState>()

View File

@@ -254,7 +254,7 @@
:text="`${t('state.nothing_found')}${filterText}`"
>
<template #icon>
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
<icon-lucide-search class="svg-icons opacity-75" />
</template>
</HoppSmartPlaceholder>
<HoppSmartPlaceholder
@@ -263,27 +263,29 @@
:alt="`${t('empty.collections')}`"
:text="t('empty.collections')"
>
<div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight">
{{ t("collection.import_or_create") }}
</span>
<div class="flex flex-col items-stretch gap-4">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled
outline
@click="emit('display-modal-import-export')"
/>
<HoppButtonSecondary
:icon="IconPlus"
:label="t('add.new')"
filled
outline
@click="emit('display-modal-add')"
/>
<template #body>
<div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight">
{{ t("collection.import_or_create") }}
</span>
<div class="flex flex-col items-stretch gap-4">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled
outline
@click="emit('display-modal-import-export')"
/>
<HoppButtonSecondary
:icon="IconPlus"
:label="t('add.new')"
filled
outline
@click="emit('display-modal-add')"
/>
</div>
</div>
</div>
</template>
</HoppSmartPlaceholder>
<HoppSmartPlaceholder
v-else-if="node.data.type === 'collections'"
@@ -291,18 +293,20 @@
:alt="`${t('empty.collections')}`"
:text="t('empty.collections')"
>
<HoppButtonSecondary
:label="t('add.new')"
filled
outline
@click="
node.data.type === 'collections' &&
emit('add-folder', {
path: node.id,
folder: node.data.data.data,
})
"
/>
<template #body>
<HoppButtonSecondary
:label="t('add.new')"
filled
outline
@click="
node.data.type === 'collections' &&
emit('add-folder', {
path: node.id,
folder: node.data.data.data,
})
"
/>
</template>
</HoppSmartPlaceholder>
<HoppSmartPlaceholder
v-else-if="node.data.type === 'folders'"

View File

@@ -274,33 +274,37 @@
:text="t('empty.collections')"
@drop.stop
>
<div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight">
{{ t("collection.import_or_create") }}
</span>
<div class="flex flex-col items-stretch gap-4">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled
outline
:disabled="hasNoTeamAccess"
:title="hasNoTeamAccess ? t('team.no_access') : ''"
@click="
hasNoTeamAccess ? null : emit('display-modal-import-export')
"
/>
<HoppButtonSecondary
:icon="IconPlus"
:label="t('add.new')"
filled
outline
:disabled="hasNoTeamAccess"
:title="hasNoTeamAccess ? t('team.no_access') : ''"
@click="hasNoTeamAccess ? null : emit('display-modal-add')"
/>
<template #body>
<div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight">
{{ t("collection.import_or_create") }}
</span>
<div class="flex flex-col items-stretch gap-4">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled
outline
:disabled="hasNoTeamAccess"
:title="hasNoTeamAccess ? t('team.no_access') : ''"
@click="
hasNoTeamAccess
? null
: emit('display-modal-import-export')
"
/>
<HoppButtonSecondary
:icon="IconPlus"
:label="t('add.new')"
filled
outline
:disabled="hasNoTeamAccess"
:title="hasNoTeamAccess ? t('team.no_access') : ''"
@click="hasNoTeamAccess ? null : emit('display-modal-add')"
/>
</div>
</div>
</div>
</template>
</HoppSmartPlaceholder>
<HoppSmartPlaceholder
v-else-if="node.data.type === 'collections'"
@@ -309,18 +313,20 @@
:text="t('empty.collections')"
@drop.stop
>
<HoppButtonSecondary
:label="t('add.new')"
filled
outline
@click="
node.data.type === 'collections' &&
emit('add-folder', {
path: node.id,
folder: node.data.data.data,
})
"
/>
<template #body>
<HoppButtonSecondary
:label="t('add.new')"
filled
outline
@click="
node.data.type === 'collections' &&
emit('add-folder', {
path: node.id,
folder: node.data.data.data,
})
"
/>
</template>
</HoppSmartPlaceholder>
<HoppSmartPlaceholder
v-else-if="node.data.type === 'folders'"

View File

@@ -180,16 +180,18 @@
:alt="`${t('empty.collection')}`"
:text="t('empty.collection')"
>
<HoppButtonSecondary
:label="t('add.new')"
filled
outline
@click="
emit('add-folder', {
path: `${collectionIndex}`,
})
"
/>
<template #body>
<HoppButtonSecondary
:label="t('add.new')"
filled
outline
@click="
emit('add-folder', {
path: `${collectionIndex}`,
})
"
/>
</template>
</HoppSmartPlaceholder>
</div>
</div>

View File

@@ -176,8 +176,7 @@
:src="`/images/states/${colorMode.value}/pack.svg`"
:alt="`${t('empty.folder')}`"
:text="t('empty.folder')"
>
</HoppSmartPlaceholder>
/>
</div>
</div>
<HoppSmartConfirmModal

View File

@@ -11,7 +11,7 @@
type="search"
autocomplete="off"
:placeholder="t('action.search')"
class="!border-0 bg-transparent py-2 pl-4 pr-2"
class="flex w-full bg-transparent px-4 py-2 h-8"
/>
<div
class="flex flex-1 flex-shrink-0 justify-between border-y border-dividerLight bg-primary"
@@ -66,34 +66,36 @@
:alt="`${t('empty.collections')}`"
:text="t('empty.collections')"
>
<div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight">
{{ t("collection.import_or_create") }}
</span>
<div class="flex flex-col items-stretch gap-4">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled
outline
@click="displayModalImportExport(true)"
/>
<HoppButtonSecondary
:label="t('add.new')"
filled
outline
:icon="IconPlus"
@click="displayModalAdd(true)"
/>
<template #body>
<div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight">
{{ t("collection.import_or_create") }}
</span>
<div class="flex flex-col items-stretch gap-4">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled
outline
@click="displayModalImportExport(true)"
/>
<HoppButtonSecondary
:label="t('add.new')"
filled
outline
:icon="IconPlus"
@click="displayModalAdd(true)"
/>
</div>
</div>
</div>
</template>
</HoppSmartPlaceholder>
<HoppSmartPlaceholder
v-if="!(filteredCollections.length !== 0 || collections.length === 0)"
:text="`${t('state.nothing_found')}${filterText}`"
>
<template #icon>
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
<icon-lucide-search class="svg-icons opacity-75" />
</template>
</HoppSmartPlaceholder>
<CollectionsGraphqlAdd

View File

@@ -22,7 +22,7 @@
v-model="filterTexts"
type="search"
autocomplete="off"
class="flex w-full bg-transparent px-4 py-2"
class="flex w-full bg-transparent px-4 py-2 h-8"
:placeholder="t('action.search')"
:disabled="collectionsType.type === 'team-collections'"
/>

View File

@@ -11,7 +11,9 @@
v-if="!currentInterceptorSupportsCookies"
:text="t('cookies.modal.interceptor_no_support')"
>
<AppInterceptor class="rounded border border-dividerLight p-2" />
<template #body>
<AppInterceptor class="rounded border border-dividerLight p-2" />
</template>
</HoppSmartPlaceholder>
<div v-else class="flex flex-col">
<div
@@ -38,8 +40,7 @@
:alt="`${t('cookies.modal.empty_domains')}`"
:text="t('cookies.modal.empty_domains')"
class="mt-6"
>
</HoppSmartPlaceholder>
/>
<div
v-for="[domain, entries] in workingCookieJar.entries()"
v-else

View File

@@ -1,17 +1,17 @@
<template>
<div class="flex flex-1 flex-col">
<div class="flex flex-col flex-1">
<header
class="flex flex-1 flex-shrink-0 items-center justify-between space-x-2 overflow-x-auto overflow-y-hidden px-2 py-2"
class="flex items-center justify-between flex-1 flex-shrink-0 px-2 py-2 space-x-2 overflow-x-auto overflow-y-hidden"
>
<div class="flex flex-1 items-center justify-between space-x-2">
<div class="flex items-center justify-between flex-1 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">
<HoppSmartItem
<HoppButtonSecondary
:label="t('app.open_in_hoppscotch')"
:to="sharedRequestURL"
blank
@@ -19,63 +19,57 @@
</div>
</div>
</header>
<div class="flex-1">
<div class="sticky top-0 z-10 flex-1">
<div
class="flex-none flex-shrink-0 bg-primary p-4 sm:flex sm:flex-shrink-0 sm:space-x-2"
class="flex-none flex-shrink-0 p-4 bg-primary sm:flex sm:flex-shrink-0 sm:space-x-2"
>
<div
class="min-w-52 flex flex-1 whitespace-nowrap rounded border border-divider"
class="flex flex-1 overflow-hidden border divide-x rounded text-secondaryDark divide-divider min-w-[12rem] overflow-x-auto border-divider"
>
<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 flex-1 whitespace-nowrap rounded-r border-l border-divider bg-primaryLight transition"
<span
class="flex items-center justify-center px-4 py-2 font-semibold transition rounded-l"
>
<input
name="method"
:value="tab.document.request.endpoint"
class="flex-1 px-4 bg-primary"
disabled
/>
{{ tab.document.request.method }}
</span>
<div
class="flex items-center flex-1 flex-shrink-0 min-w-0 px-4 py-2 truncate rounded-r"
>
{{ tab.document.request.endpoint }}
</div>
</div>
<div class="mt-2 flex sm:mt-0">
<div class="flex mt-2 space-x-2 sm:mt-0">
<HoppButtonPrimary
id="send"
:title="`${t(
'action.send'
)} <kbd>${getSpecialKey()}</kbd><kbd>↩</kbd>`"
:label="`${!loading ? t('action.send') : t('action.cancel')}`"
class="min-w-20 flex-1"
class="flex-1 min-w-20"
outline
@click="!loading ? newSendRequest() : cancelRequest()"
/>
<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 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>
</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

@@ -93,20 +93,12 @@
}
"
/>
<div
<HoppSmartPlaceholder
v-if="myEnvironments.length === 0"
class="flex flex-col items-center justify-center text-secondaryLight"
>
<img
:src="`/images/states/${colorMode.value}/blockchain.svg`"
loading="lazy"
class="mb-2 inline-flex h-16 w-16 flex-col object-contain object-center"
:alt="`${t('empty.environments')}`"
/>
<span class="pb-2 text-center">
{{ t("empty.environments") }}
</span>
</div>
:src="`/images/states/${colorMode.value}/blockchain.svg`"
:alt="`${t('empty.environments')}`"
:text="t('empty.environments')"
/>
</HoppSmartTab>
<HoppSmartTab
:id="'team-environments'"
@@ -140,20 +132,12 @@
}
"
/>
<div
<HoppSmartPlaceholder
v-if="teamEnvironmentList.length === 0"
class="flex flex-col items-center justify-center text-secondaryLight"
>
<img
:src="`/images/states/${colorMode.value}/blockchain.svg`"
loading="lazy"
class="mb-2 inline-flex h-16 w-16 flex-col object-contain object-center"
:alt="`${t('empty.environments')}`"
/>
<span class="pb-2 text-center">
{{ t("empty.environments") }}
</span>
</div>
:src="`/images/states/${colorMode.value}/blockchain.svg`"
:alt="`${t('empty.environments')}`"
:text="t('empty.environments')"
/>
</div>
<div
v-if="!teamListLoading && teamAdapterError"

View File

@@ -78,11 +78,13 @@
:alt="`${t('empty.environments')}`"
:text="t('empty.environments')"
>
<HoppButtonSecondary
:label="`${t('add.new')}`"
filled
@click="addEnvironmentVariable"
/>
<template #body>
<HoppButtonSecondary
:label="`${t('add.new')}`"
filled
@click="addEnvironmentVariable"
/>
</template>
</HoppSmartPlaceholder>
</div>
</div>

View File

@@ -38,27 +38,29 @@
:alt="`${t('empty.environments')}`"
:text="t('empty.environments')"
>
<div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight">
{{ t("environment.import_or_create") }}
</span>
<div class="flex flex-col items-stretch gap-4">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled
outline
@click="displayModalImportExport(true)"
/>
<HoppButtonSecondary
:icon="IconPlus"
:label="`${t('add.new')}`"
filled
outline
@click="displayModalAdd(true)"
/>
<template #body>
<div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight">
{{ t("environment.import_or_create") }}
</span>
<div class="flex flex-col items-stretch gap-4">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled
outline
@click="displayModalImportExport(true)"
/>
<HoppButtonSecondary
:icon="IconPlus"
:label="`${t('add.new')}`"
filled
outline
@click="displayModalAdd(true)"
/>
</div>
</div>
</div>
</template>
</HoppSmartPlaceholder>
<EnvironmentsMyDetails
:show="showModalDetails"

View File

@@ -81,18 +81,20 @@
:alt="`${t('empty.environments')}`"
:text="t('empty.environments')"
>
<HoppButtonSecondary
v-if="isViewer"
disabled
:label="`${t('add.new')}`"
filled
/>
<HoppButtonSecondary
v-else
:label="`${t('add.new')}`"
filled
@click="addEnvironmentVariable"
/>
<template #body>
<HoppButtonSecondary
v-if="isViewer"
disabled
:label="`${t('add.new')}`"
filled
/>
<HoppButtonSecondary
v-else
:label="`${t('add.new')}`"
filled
@click="addEnvironmentVariable"
/>
</template>
</HoppSmartPlaceholder>
</div>
</div>

View File

@@ -49,31 +49,33 @@
:alt="`${t('empty.environments')}`"
:text="t('empty.environments')"
>
<div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight">
{{ t("environment.import_or_create") }}
</span>
<div class="flex flex-col items-stretch gap-4">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled
outline
:title="isTeamViewer ? t('team.no_access') : ''"
:disabled="isTeamViewer"
@click="isTeamViewer ? null : displayModalImportExport(true)"
/>
<HoppButtonSecondary
:label="`${t('add.new')}`"
filled
outline
:icon="IconPlus"
:title="isTeamViewer ? t('team.no_access') : ''"
:disabled="isTeamViewer"
@click="isTeamViewer ? null : displayModalAdd(true)"
/>
<template #body>
<div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight">
{{ t("environment.import_or_create") }}
</span>
<div class="flex flex-col items-stretch gap-4">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled
outline
:title="isTeamViewer ? t('team.no_access') : ''"
:disabled="isTeamViewer"
@click="isTeamViewer ? null : displayModalImportExport(true)"
/>
<HoppButtonSecondary
:label="`${t('add.new')}`"
filled
outline
:icon="IconPlus"
:title="isTeamViewer ? t('team.no_access') : ''"
:disabled="isTeamViewer"
@click="isTeamViewer ? null : displayModalAdd(true)"
/>
</div>
</div>
</div>
</template>
</HoppSmartPlaceholder>
<div v-else-if="!loading">
<EnvironmentsTeamsEnvironment

View File

@@ -120,14 +120,16 @@
:alt="`${t('empty.authorization')}`"
:text="t('empty.authorization')"
>
<HoppButtonSecondary
outline
:label="t('app.documentation')"
to="https://docs.hoppscotch.io/documentation/features/authorization"
blank
:icon="IconExternalLink"
reverse
/>
<template #body>
<HoppButtonSecondary
outline
:label="t('app.documentation')"
to="https://docs.hoppscotch.io/documentation/features/authorization"
blank
:icon="IconExternalLink"
reverse
/>
</template>
</HoppSmartPlaceholder>
<div v-else class="flex flex-1 border-b border-dividerLight">
<div class="w-2/3 border-r border-dividerLight">

View File

@@ -162,12 +162,14 @@
:alt="`${t('empty.headers')}`"
:text="t('empty.headers')"
>
<HoppButtonSecondary
:label="`${t('add.new')}`"
filled
:icon="IconPlus"
@click="addHeader"
/>
<template #body>
<HoppButtonSecondary
:label="`${t('add.new')}`"
filled
:icon="IconPlus"
@click="addHeader"
/>
</template>
</HoppSmartPlaceholder>
</div>
</template>

View File

@@ -20,8 +20,7 @@
:src="`/images/states/${colorMode.value}/add_comment.svg`"
:alt="`${t('empty.documentation')}`"
:text="t('empty.documentation')"
>
</HoppSmartPlaceholder>
/>
<div v-else>
<div
class="sticky top-0 z-10 flex flex-shrink-0 overflow-x-auto bg-primary"
@@ -30,7 +29,7 @@
v-model="graphqlFieldsFilterText"
type="search"
autocomplete="off"
class="flex w-full bg-transparent px-4 py-2"
class="flex w-full bg-transparent px-4 py-2 h-8"
:placeholder="`${t('action.search')}`"
/>
<div class="flex">

View File

@@ -9,7 +9,7 @@
v-model="filterText"
type="search"
autocomplete="off"
class="flex w-full bg-transparent px-4 py-2"
class="flex w-full bg-transparent px-4 py-2 h-8"
:placeholder="`${t('action.search')}`"
/>
<div class="flex">
@@ -114,8 +114,7 @@
:src="`/images/states/${colorMode.value}/history.svg`"
:alt="`${t('empty.history')}`"
:text="t('empty.history')"
>
</HoppSmartPlaceholder>
/>
<HoppSmartPlaceholder
v-else-if="
Object.keys(filteredHistoryGroups).length === 0 ||
@@ -124,18 +123,20 @@
:text="`${t('state.nothing_found')} ‟${filterText || filterSelection}”`"
>
<template #icon>
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
<icon-lucide-search class="svg-icons opacity-75" />
</template>
<template #body>
<HoppButtonSecondary
:label="t('action.clear')"
outline
@click="
() => {
filterText = ''
filterSelection = 'ALL'
}
"
/>
</template>
<HoppButtonSecondary
:label="t('action.clear')"
outline
@click="
() => {
filterText = ''
filterSelection = 'ALL'
}
"
/>
</HoppSmartPlaceholder>
<HoppSmartConfirmModal
:show="confirmRemove"

View File

@@ -119,14 +119,16 @@
:alt="`${t('empty.authorization')}`"
:text="t('empty.authorization')"
>
<HoppButtonSecondary
outline
:label="t('app.documentation')"
to="https://docs.hoppscotch.io/documentation/features/authorization"
blank
:icon="IconExternalLink"
reverse
/>
<template #body>
<HoppButtonSecondary
outline
:label="t('app.documentation')"
to="https://docs.hoppscotch.io/documentation/features/authorization"
blank
:icon="IconExternalLink"
reverse
/>
</template>
</HoppSmartPlaceholder>
<div v-else class="flex flex-1 border-b border-dividerLight">
<div class="w-2/3 border-r border-dividerLight">

View File

@@ -112,14 +112,16 @@
:alt="`${t('empty.body')}`"
:text="t('empty.body')"
>
<HoppButtonSecondary
outline
:label="`${t('app.documentation')}`"
to="https://docs.hoppscotch.io/documentation/getting-started/rest/uploading-data"
blank
:icon="IconExternalLink"
reverse
/>
<template #body>
<HoppButtonSecondary
outline
:label="`${t('app.documentation')}`"
to="https://docs.hoppscotch.io/documentation/getting-started/rest/uploading-data"
blank
:icon="IconExternalLink"
reverse
/>
</template>
</HoppSmartPlaceholder>
</div>
</template>

View File

@@ -158,12 +158,14 @@
:alt="`${t('empty.body')}`"
:text="t('empty.body')"
>
<HoppButtonSecondary
:label="`${t('add.new')}`"
filled
:icon="IconPlus"
@click="addBodyParam"
/>
<template #body>
<HoppButtonSecondary
:label="`${t('add.new')}`"
filled
:icon="IconPlus"
@click="addBodyParam"
/>
</template>
</HoppSmartPlaceholder>
</div>
</template>

View File

@@ -61,7 +61,7 @@
:text="`${t('state.nothing_found')}${searchQuery}`"
>
<template #icon>
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
<icon-lucide-search class="svg-icons opacity-75" />
</template>
</HoppSmartPlaceholder>
</div>

View File

@@ -209,12 +209,14 @@
:alt="`${t('empty.headers')}`"
:text="t('empty.headers')"
>
<HoppButtonSecondary
filled
:label="`${t('add.new')}`"
:icon="IconPlus"
@click="addHeader"
/>
<template #body>
<HoppButtonSecondary
filled
:label="`${t('add.new')}`"
:icon="IconPlus"
@click="addHeader"
/>
</template>
</HoppSmartPlaceholder>
</div>
</div>

View File

@@ -157,12 +157,14 @@
:alt="`${t('empty.parameters')}`"
:text="t('empty.parameters')"
>
<HoppButtonSecondary
:label="`${t('add.new')}`"
:icon="IconPlus"
filled
@click="addParam"
/>
<template #body>
<HoppButtonSecondary
:label="`${t('add.new')}`"
:icon="IconPlus"
filled
@click="addParam"
/>
</template>
</HoppSmartPlaceholder>
</div>
</div>

View File

@@ -37,7 +37,9 @@
:text="t('helpers.network_fail')"
large
>
<AppInterceptor class="rounded border border-dividerLight p-2" />
<template #body>
<AppInterceptor class="rounded border border-dividerLight p-2" />
</template>
</HoppSmartPlaceholder>
<HoppSmartPlaceholder
v-if="response.type === 'script_fail'"
@@ -47,12 +49,14 @@
:text="t('helpers.script_fail')"
large
>
<div
class="mt-2 w-full overflow-auto whitespace-normal rounded bg-primaryLight px-4 py-2 font-mono text-red-400"
>
{{ response.error.name }}: {{ response.error.message }}<br />
{{ response.error.stack }}
</div>
<template #body>
<div
class="mt-2 w-full overflow-auto whitespace-normal rounded bg-primaryLight px-4 py-2 font-mono text-red-400"
>
{{ response.error.name }}: {{ response.error.message }}<br />
{{ response.error.stack }}
</div>
</template>
</HoppSmartPlaceholder>
<div
v-if="response.type === 'success' || response.type === 'fail'"

View File

@@ -41,7 +41,7 @@
<div class="divide-y divide-dividerLight">
<div
v-if="noEnvSelected && !globalHasAdditions"
class="flex bg-info p-4 text-secondaryDark"
class="flex bg-bannerInfo p-4 text-secondaryDark"
role="alert"
>
<icon-lucide-alert-triangle class="svg-icons mr-4" />
@@ -159,8 +159,7 @@
:alt="`${t('error.test_script_fail')}`"
:heading="t('error.test_script_fail')"
:text="t('helpers.test_script_fail')"
>
</HoppSmartPlaceholder>
/>
<HoppSmartPlaceholder
v-else
:src="`/images/states/${colorMode.value}/validation.svg`"
@@ -168,15 +167,16 @@
:heading="t('empty.tests')"
:text="t('helpers.tests')"
>
<HoppButtonSecondary
outline
:label="`${t('action.learn_more')}`"
to="https://docs.hoppscotch.io/documentation/getting-started/rest/tests"
blank
:icon="IconExternalLink"
reverse
class="my-4"
/>
<template #body>
<HoppButtonSecondary
outline
:label="`${t('action.learn_more')}`"
to="https://docs.hoppscotch.io/documentation/getting-started/rest/tests"
blank
:icon="IconExternalLink"
reverse
/>
</template>
</HoppSmartPlaceholder>
<EnvironmentsMyDetails
:show="showMyEnvironmentDetailsModal"

View File

@@ -149,12 +149,14 @@
:alt="`${t('empty.body')}`"
:text="t('empty.body')"
>
<HoppButtonSecondary
filled
:label="`${t('add.new')}`"
:icon="IconPlus"
@click="addUrlEncodedParam"
/>
<template #body>
<HoppButtonSecondary
filled
:label="`${t('add.new')}`"
:icon="IconPlus"
@click="addUrlEncodedParam"
/>
</template>
</HoppSmartPlaceholder>
</div>
</div>

View File

@@ -5,55 +5,57 @@
:heading="t('error.network_fail')"
large
>
<div class="my-1 flex flex-col items-center text-secondaryLight">
<span>
{{ t("error.please_install_extension") }}
</span>
<span>
{{ t("error.check_how_to_add_origin") }}
<HoppSmartLink
blank
to="https://docs.hoppscotch.io/documentation/features/interceptor#browser-extension"
class="text-accent hover:text-accentDark"
>
here
</HoppSmartLink>
</span>
</div>
<div class="flex flex-col space-y-2 py-4">
<span>
<HoppSmartItem
to="https://chrome.google.com/webstore/detail/hoppscotch-browser-extens/amknoiejhlmhancpahfcfcfhllgkpbld"
blank
:icon="IconChrome"
label="Chrome"
:info-icon="hasChromeExtInstalled ? IconCheckCircle : null"
:active-info-icon="hasChromeExtInstalled"
outline
/>
</span>
<span>
<HoppSmartItem
to="https://addons.mozilla.org/en-US/firefox/addon/hoppscotch"
blank
:icon="IconFirefox"
label="Firefox"
:info-icon="hasFirefoxExtInstalled ? IconCheckCircle : null"
:active-info-icon="hasFirefoxExtInstalled"
outline
/>
</span>
</div>
<div class="space-y-4 py-4">
<div class="flex items-center">
<HoppSmartToggle
:on="extensionEnabled"
@change="extensionEnabled = !extensionEnabled"
>
{{ t("settings.extensions_use_toggle") }}
</HoppSmartToggle>
<template #body>
<div class="my-1 flex flex-col items-center text-secondaryLight">
<span>
{{ t("error.please_install_extension") }}
</span>
<span>
{{ t("error.check_how_to_add_origin") }}
<HoppSmartLink
blank
to="https://docs.hoppscotch.io/documentation/features/interceptor#browser-extension"
class="text-accent hover:text-accentDark"
>
here
</HoppSmartLink>
</span>
</div>
</div>
<div class="flex flex-col space-y-2 py-4">
<span>
<HoppSmartItem
to="https://chrome.google.com/webstore/detail/hoppscotch-browser-extens/amknoiejhlmhancpahfcfcfhllgkpbld"
blank
:icon="IconChrome"
label="Chrome"
:info-icon="hasChromeExtInstalled ? IconCheckCircle : null"
:active-info-icon="hasChromeExtInstalled"
outline
/>
</span>
<span>
<HoppSmartItem
to="https://addons.mozilla.org/en-US/firefox/addon/hoppscotch"
blank
:icon="IconFirefox"
label="Firefox"
:info-icon="hasFirefoxExtInstalled ? IconCheckCircle : null"
:active-info-icon="hasFirefoxExtInstalled"
outline
/>
</span>
</div>
<div class="space-y-4 py-4">
<div class="flex items-center">
<HoppSmartToggle
:on="extensionEnabled"
@change="extensionEnabled = !extensionEnabled"
>
{{ t("settings.extensions_use_toggle") }}
</HoppSmartToggle>
</div>
</div>
</template>
</HoppSmartPlaceholder>
</template>

View File

@@ -26,7 +26,7 @@
</div>
<div
v-else-if="myTeams.length"
class="bg-info flex flex-col space-y-2 rounded-lg border border-red-500 p-4 text-secondaryDark"
class="bg-bannerInfo 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-info mb-4 flex flex-col space-y-2 rounded-lg border border-red-500 p-4 text-secondaryDark"
class="bg-bannerInfo 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,9 +306,8 @@ const embedThemeIcon = computed(() => {
return IconMonitor
} else if (embedOptions.value.theme === "light") {
return IconSun
} else {
return IconMoon
}
return IconMoon
})
const removeEmbedOption = (option: EmbedTabs) => {
@@ -392,9 +391,8 @@ 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) => {
@@ -402,9 +400,8 @@ 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,17 +29,15 @@
@copy-shared-request="copySharedRequest"
/>
</template>
<template #footer>
<template v-if="step === 1" #footer>
<div class="flex justify-start flex-1">
<HoppButtonPrimary
v-if="step === 1"
:label="t('action.create')"
:loading="loading"
@click="createSharedRequest"
/>
<HoppButtonSecondary
:label="step === 1 ? t('action.cancel') : t('action.close')"
:label="t('action.cancel')"
class="ml-2"
filled
outline

View File

@@ -32,10 +32,12 @@
:alt="`${t('empty.shared_requests_logout')}`"
:text="`${t('empty.shared_requests_logout')}`"
>
<HoppButtonPrimary
:label="t('auth.login')"
@click="invokeAction('modals.login.toggle')"
/>
<template #body>
<HoppButtonPrimary
:label="t('auth.login')"
@click="invokeAction('modals.login.toggle')"
/>
</template>
</HoppSmartPlaceholder>
<template v-else-if="sharedRequests.length">
@@ -446,13 +448,12 @@ const resolveConfirmModal = (title: string | null) => {
const getErrorMessage = (err: GQLError<string>) => {
if (err.type === "network_error") {
return t("error.network_error")
} else {
switch (err.error) {
case "shortcode/not_found":
return t("shared_request.not_found")
default:
return t("error.something_went_wrong")
}
}
switch (err.error) {
case "shortcode/not_found":
return t("shared_request.not_found")
default:
return t("error.something_went_wrong")
}
}

View File

@@ -11,7 +11,9 @@
'bg-accentContrast': isEmbedThemeLight,
}"
>
<span class="flex items-center min-w-0 border rounded border-divider">
<span
class="flex items-center flex-1 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

@@ -60,7 +60,7 @@
:text="`${t('state.nothing_found')}${searchQuery}`"
>
<template #icon>
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
<icon-lucide-search class="svg-icons opacity-75" />
</template>
</HoppSmartPlaceholder>
</div>

View File

@@ -45,15 +45,17 @@
:alt="`${t('empty.members')}`"
:text="t('empty.members')"
>
<HoppButtonSecondary
:icon="IconUserPlus"
:label="t('team.invite')"
@click="
() => {
emit('invite-team')
}
"
/>
<template #body>
<HoppButtonSecondary
:icon="IconUserPlus"
:label="t('team.invite')"
@click="
() => {
emit('invite-team')
}
"
/>
</template>
</HoppSmartPlaceholder>
<div v-else class="divide-y divide-dividerLight">
<div

View File

@@ -125,8 +125,7 @@
pendingInvites.data.right.team?.teamInvitations.length === 0
"
:text="t('empty.pending_invites')"
>
</HoppSmartPlaceholder>
/>
<div
v-if="!pendingInvites.loading && E.isLeft(pendingInvites.data)"
class="flex flex-col items-center p-4"
@@ -245,11 +244,13 @@
:alt="`${t('empty.invites')}`"
:text="`${t('empty.invites')}`"
>
<HoppButtonSecondary
:label="t('add.new')"
filled
@click="addNewInvitee"
/>
<template #body>
<HoppButtonSecondary
:label="t('add.new')"
filled
@click="addNewInvitee"
/>
</template>
</HoppSmartPlaceholder>
</div>
<div

View File

@@ -16,11 +16,13 @@
:alt="`${t('empty.teams')}`"
:text="`${t('empty.teams')}`"
>
<HoppButtonSecondary
:label="`${t('team.create_new')}`"
filled
@click="displayModalAdd(true)"
/>
<template #body>
<HoppButtonSecondary
:label="`${t('team.create_new')}`"
filled
@click="displayModalAdd(true)"
/>
</template>
</HoppSmartPlaceholder>
<div
v-else-if="!loading"

View File

@@ -21,13 +21,15 @@
:alt="`${t('empty.teams')}`"
:text="`${t('empty.teams')}`"
>
<HoppButtonSecondary
:label="t('team.create_new')"
filled
outline
:icon="IconPlus"
@click="displayModalAdd(true)"
/>
<template #body>
<HoppButtonSecondary
:label="t('team.create_new')"
filled
outline
:icon="IconPlus"
@click="displayModalAdd(true)"
/>
</template>
</HoppSmartPlaceholder>
<div v-else-if="!loading" class="flex flex-col">
<div

View File

@@ -1,6 +1,7 @@
import { flow } from "fp-ts/function"
import { useI18n as _useI18n } from "vue-i18n"
export const useI18n = flow(_useI18n, (x) => x.t)
export function useI18n() {
return _useI18n().t
}
export const useFullI18n = _useI18n

View File

@@ -1,26 +1,15 @@
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 { 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 { Ref } from "vue"
import {
environmentsStore,
getCurrentEnvironment,
@@ -29,9 +18,18 @@ 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" }
@@ -89,7 +87,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)) {
@@ -125,7 +123,7 @@ export function runRESTRequest$(
body: getTestableBody(res),
headers: res.headers,
}
)()
)
if (E.isRight(runResult)) {
tab.value.document.testResults = translateToSandboxTestResults(

View File

@@ -1,10 +1,13 @@
import { runPreRequestScript } from "@hoppscotch/js-sandbox"
import * as E from "fp-ts/Either"
import { runPreRequestScript } from "@hoppscotch/js-sandbox/web"
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()),
@@ -17,4 +20,5 @@ export const getFinalEnvsFromPreRequest = (
global: Environment["variables"]
selected: Environment["variables"]
}
) => runPreRequestScript(script, envs)
): Promise<E.Either<string, TestResult["envs"]>> =>
runPreRequestScript(script, envs)

View File

@@ -14,10 +14,12 @@
:alt="`${t('empty.profile')}`"
:text="`${t('empty.profile')}`"
>
<HoppButtonPrimary
:label="t('auth.login')"
@click="invokeAction('modals.login.toggle')"
/>
<template #body>
<HoppButtonPrimary
:label="t('auth.login')"
@click="invokeAction('modals.login.toggle')"
/>
</template>
</HoppSmartPlaceholder>
<div v-else class="space-y-8">
<div

View File

@@ -5,10 +5,11 @@
content-styles="!h-[calc(100%-var(--sidebar-primary-sticky-fold)-1px)] !flex"
>
<HoppSmartTab
v-for="{ target, title } in REALTIME_NAVIGATION"
:id="target"
:key="target"
:label="title"
v-for="(navigation, index) in REALTIME_NAVIGATION"
:id="navigation.target"
:key="index"
:label="navigation.title"
:icon="navigation.icon"
>
<RouterView />
</HoppSmartTab>
@@ -20,6 +21,10 @@ 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()
@@ -29,18 +34,22 @@ 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

@@ -142,12 +142,14 @@
:alt="`${t('empty.subscription')}`"
:text="`${t('empty.subscription')}`"
>
<HoppButtonSecondary
:label="t('mqtt.new')"
filled
outline
@click="showSubscriptionModal(true)"
/>
<template #body>
<HoppButtonSecondary
:label="t('mqtt.new')"
filled
outline
@click="showSubscriptionModal(true)"
/>
</template>
</HoppSmartPlaceholder>
<div v-else>
<div

View File

@@ -197,14 +197,16 @@
:alt="`${t('socketio.connection_not_authorized')}`"
:text="`${t('socketio.connection_not_authorized')}`"
>
<HoppButtonSecondary
outline
:label="t('app.documentation')"
to="https://docs.hoppscotch.io/documentation/features/authorization"
blank
:icon="IconExternalLink"
reverse
/>
<template #body>
<HoppButtonSecondary
outline
:label="t('app.documentation')"
to="https://docs.hoppscotch.io/documentation/features/authorization"
blank
:icon="IconExternalLink"
reverse
/>
</template>
</HoppSmartPlaceholder>
<div
v-if="authType === 'Bearer'"

View File

@@ -163,8 +163,7 @@
:src="`/images/states/${colorMode.value}/add_category.svg`"
:alt="`${t('empty.protocols')}`"
:text="`${t('empty.protocols')}`"
>
</HoppSmartPlaceholder>
/>
</HoppSmartTab>
</HoppSmartTabs>
</template>

View File

@@ -11,6 +11,7 @@ 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
@@ -28,6 +29,7 @@ export type PlatformDef = {
}
interceptors: InterceptorsPlatformDef
additionalInspectors?: InspectorsPlatformDef
spotlight?: SpotlightPlatformDef
platformFeatureFlags: {
exportAsGIST: boolean
hasTelemetry: boolean

View File

@@ -0,0 +1,10 @@
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,10 +26,9 @@ export type Banner = {
const getBannerWithHighestScore = (list: Banner[]) => {
if (list.length === 0) return null
else if (list.length === 1) return list[0]
else {
const highestScore = Math.max(...list.map((banner) => banner.content.score))
return list.find((banner) => banner.content.score === highestScore)
}
const highestScore = Math.max(...list.map((banner) => banner.content.score))
return list.find((banner) => banner.content.score === highestScore)
}
/**

View File

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

View File

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

View File

@@ -1,6 +1,10 @@
module.exports = {
export default {
preset: "ts-jest",
testEnvironment: "jsdom",
collectCoverage: true,
setupFilesAfterEnv: ["./jest.setup.ts"],
moduleNameMapper: {
"~/(.*)": "<rootDir>/src/$1",
"^lodash-es$": "lodash",
},
}

View File

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

View File

@@ -2,16 +2,27 @@
"name": "@hoppscotch/js-sandbox",
"version": "2.1.0",
"description": "JavaScript sandboxes for running external scripts used by Hoppscotch clients",
"main": "./lib/index.js",
"module": "./lib/index.mjs",
"type": "commonjs",
"type": "module",
"files": [
"dist",
"index.d.ts"
],
"exports": {
".": {
"require": "./lib/index.js",
"default": "./lib/index.mjs"
"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"
}
},
"types": "./lib/",
"types": "./index.d.ts",
"engines": {
"node": ">=14",
"pnpm": ">=3"
@@ -20,7 +31,7 @@
"lint": "eslint --ext .ts,.js --ignore-path .gitignore .",
"lintfix": "eslint --fix --ext .ts,.js --ignore-path .gitignore .",
"test": "pnpm exec jest",
"build": "pnpm exec tsup",
"build": "vite build && tsc --emitDeclarationOnly",
"clean": "pnpm tsc --build --clean",
"postinstall": "pnpm run build",
"prepublish": "pnpm run build",
@@ -41,10 +52,10 @@
"license": "MIT",
"dependencies": {
"@hoppscotch/data": "workspace:^",
"@types/lodash-es": "^4.17.12",
"fp-ts": "^2.11.10",
"lodash": "^4.17.21",
"quickjs-emscripten": "^0.15.0",
"tsup": "^5.12.5"
"lodash-es": "^4.17.21"
},
"devDependencies": {
"@digitak/esrun": "^3.1.2",
@@ -61,6 +72,7 @@
"jest": "^27.5.1",
"prettier": "^2.8.4",
"ts-jest": "^27.1.4",
"typescript": "^4.6.3"
"typescript": "^4.6.3",
"vite": "^5.0.4"
}
}

View File

@@ -1,10 +1,10 @@
import { execPreRequestScript } from "../preRequest"
import { runPreRequestScript } from "~/pre-request/node-vm"
import "@relmify/jest-fp-ts"
describe("execPreRequestScript", () => {
test("returns the updated envirionment properly", () => {
return expect(
execPreRequestScript(
runPreRequestScript(
`
pw.env.set("bob", "newbob")
`,
@@ -27,7 +27,7 @@ describe("execPreRequestScript", () => {
test("fails if the key is not a string", () => {
return expect(
execPreRequestScript(
runPreRequestScript(
`
pw.env.set(10, "newbob")
`,
@@ -44,7 +44,7 @@ describe("execPreRequestScript", () => {
test("fails if the value is not a string", () => {
return expect(
execPreRequestScript(
runPreRequestScript(
`
pw.env.set("bob", 10)
`,
@@ -61,7 +61,7 @@ describe("execPreRequestScript", () => {
test("fails for invalid syntax", () => {
return expect(
execPreRequestScript(
runPreRequestScript(
`
pw.env.set("bob",
`,
@@ -78,7 +78,7 @@ describe("execPreRequestScript", () => {
test("creates new env variable if doesn't exist", () => {
return expect(
execPreRequestScript(
runPreRequestScript(
`
pw.env.set("foo", "bar")
`,

View File

@@ -1,8 +1,9 @@
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 "@relmify/jest-fp-ts"
import { runTestScript } from "~/test-runner/node-vm"
import { TestResponse, TestResult } from "~/types"
const fakeResponse: TestResponse = {
status: 200,
@@ -12,7 +13,7 @@ const fakeResponse: TestResponse = {
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
execTestScript(script, envs, fakeResponse),
runTestScript(script, envs, fakeResponse),
TE.map((x) => x.tests)
)

View File

@@ -1,8 +1,9 @@
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 "@relmify/jest-fp-ts"
import { runTestScript } from "~/test-runner/node-vm"
import { TestResponse, TestResult } from "~/types"
const fakeResponse: TestResponse = {
status: 200,
@@ -12,7 +13,7 @@ const fakeResponse: TestResponse = {
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
execTestScript(script, envs, fakeResponse),
runTestScript(script, envs, fakeResponse),
TE.map((x) => x.tests)
)

View File

@@ -1,6 +1,8 @@
import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither"
import { execTestScript, TestResponse, TestResult } from "../../../test-runner"
import { pipe } from "fp-ts/function"
import { runTestScript } from "~/test-runner/node-vm"
import { TestResponse, TestResult } from "~/types"
const fakeResponse: TestResponse = {
status: 200,
@@ -10,7 +12,7 @@ const fakeResponse: TestResponse = {
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
execTestScript(script, envs, fakeResponse),
runTestScript(script, envs, fakeResponse),
TE.map((x) => x.tests)
)

View File

@@ -1,6 +1,8 @@
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"
const fakeResponse: TestResponse = {
status: 200,
@@ -10,13 +12,13 @@ const fakeResponse: TestResponse = {
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
execTestScript(script, envs, fakeResponse),
runTestScript(script, envs, fakeResponse),
TE.map((x) => x.envs)
)
const funcTest = (script: string, envs: TestResult["envs"]) =>
pipe(
execTestScript(script, envs, fakeResponse),
runTestScript(script, envs, fakeResponse),
TE.map((x) => x.tests)
)

View File

@@ -1,7 +1,9 @@
import "@relmify/jest-fp-ts"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { execTestScript, TestResponse } from "../../../test-runner"
import "@relmify/jest-fp-ts"
import { runTestScript } from "~/test-runner/node-vm"
import { TestResponse } from "~/types"
const fakeResponse: TestResponse = {
status: 200,
@@ -11,7 +13,7 @@ const fakeResponse: TestResponse = {
const func = (script: string, res: TestResponse) =>
pipe(
execTestScript(script, { global: [], selected: [] }, res),
runTestScript(script, { global: [], selected: [] }, res),
TE.map((x) => x.tests)
)

View File

@@ -1,7 +1,9 @@
import "@relmify/jest-fp-ts"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { execTestScript, TestResponse } from "../../../test-runner"
import "@relmify/jest-fp-ts"
import { runTestScript } from "~/test-runner/node-vm"
import { TestResponse } from "~/types"
const fakeResponse: TestResponse = {
status: 200,
@@ -11,7 +13,7 @@ const fakeResponse: TestResponse = {
const func = (script: string, res: TestResponse) =>
pipe(
execTestScript(script, { global: [], selected: [] }, res),
runTestScript(script, { global: [], selected: [] }, res),
TE.map((x) => x.tests)
)

View File

@@ -1,6 +1,8 @@
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { execTestScript, TestResponse } from "../../../test-runner"
import { runTestScript } from "~/test-runner/node-vm"
import { TestResponse } from "~/types"
const fakeResponse: TestResponse = {
status: 200,
@@ -10,7 +12,7 @@ const fakeResponse: TestResponse = {
const func = (script: string, res: TestResponse) =>
pipe(
execTestScript(script, { global: [], selected: [] }, res),
runTestScript(script, { global: [], selected: [] }, res),
TE.map((x) => x.tests)
)

View File

@@ -1,6 +1,8 @@
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { execTestScript, TestResponse } from "../../../test-runner"
import { runTestScript } from "~/test-runner/node-vm"
import { TestResponse } from "~/types"
const fakeResponse: TestResponse = {
status: 200,
@@ -10,7 +12,7 @@ const fakeResponse: TestResponse = {
const func = (script: string, res: TestResponse) =>
pipe(
execTestScript(script, { global: [], selected: [] }, res),
runTestScript(script, { global: [], selected: [] }, res),
TE.map((x) => x.tests)
)

View File

@@ -1,6 +1,8 @@
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { execTestScript, TestResponse } from "../../../test-runner"
import { runTestScript } from "~/test-runner/node-vm"
import { TestResponse } from "~/types"
const fakeResponse: TestResponse = {
status: 200,
@@ -10,7 +12,7 @@ const fakeResponse: TestResponse = {
const func = (script: string, res: TestResponse) =>
pipe(
execTestScript(script, { global: [], selected: [] }, res),
runTestScript(script, { global: [], selected: [] }, res),
TE.map((x) => x.tests)
)

View File

@@ -1,6 +1,8 @@
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { execTestScript, TestResponse } from "../../test-runner"
import { runTestScript } from "~/test-runner/node-vm"
import { TestResponse } from "~/types"
const fakeResponse: TestResponse = {
status: 200,
@@ -10,7 +12,7 @@ const fakeResponse: TestResponse = {
const func = (script: string, res: TestResponse) =>
pipe(
execTestScript(script, { global: [], selected: [] }, res),
runTestScript(script, { global: [], selected: [] }, res),
TE.map((x) => x.tests)
)

View File

@@ -1,40 +1,15 @@
import { match } from "fp-ts/lib/Either"
import { pipe } from "fp-ts/lib/function"
import * as QuickJS from "quickjs-emscripten"
import { marshalObjectToVM } from "../utils"
import { preventCyclicObjects } from "~/utils"
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", () => {
describe("preventCyclicObjects", () => {
test("succeeds with a simple object", () => {
const testObj = {
a: 1,
}
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()
expect(preventCyclicObjects(testObj)).toBeRight()
})
test("fails marshalling cyclic object into vm", () => {
test("fails with a cyclic object", () => {
const testObj = {
a: 1,
b: null as any,
@@ -42,14 +17,6 @@ describe("marshalObjectToVM", () => {
testObj.b = testObj
const objVMHandle: QuickJS.QuickJSHandle | null = pipe(
marshalObjectToVM(vm, testObj),
match(
() => null,
(result) => result
)
)
expect(objVMHandle).toBeNull()
expect(preventCyclicObjects(testObj)).toBeLeft()
})
})

View File

@@ -1,43 +0,0 @@
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

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

View File

@@ -0,0 +1,36 @@
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

@@ -0,0 +1,24 @@
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

@@ -0,0 +1,33 @@
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

@@ -1,166 +0,0 @@
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

@@ -1,610 +0,0 @@
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

@@ -0,0 +1,55 @@
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

@@ -0,0 +1,27 @@
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

@@ -0,0 +1,43 @@
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

@@ -0,0 +1,61 @@
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,89 +1,439 @@
import * as O from "fp-ts/Option"
import { parseTemplateStringE } from "@hoppscotch/data"
import * as E from "fp-ts/Either"
import * as QuickJS from "quickjs-emscripten"
import { TestResult } from "./test-runner"
import * as O from "fp-ts/Option"
import { pipe } from "fp-ts/lib/function"
import { cloneDeep } from "lodash-es"
export function marshalObjectToVM(
vm: QuickJS.QuickJSVm,
obj: object
): E.Either<string, QuickJS.QuickJSHandle> {
let jsonString
import {
GlobalEnvItem,
SelectedEnvItem,
TestDescriptor,
TestResult,
} from "./types"
try {
jsonString = JSON.stringify(obj)
} catch (e) {
return E.left("Marshaling stringification 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)
}
export function getEnv(envName: string, envs: TestResult["envs"]) {
const getEnv = (envName: string, envs: TestResult["envs"]) => {
return O.fromNullable(
envs.selected.find((x) => x.key === envName) ??
envs.global.find((x) => x.key === envName)
envs.selected.find((x: SelectedEnvItem) => x.key === envName) ??
envs.global.find((x: GlobalEnvItem) => x.key === envName)
)
}
export function setEnv(
// 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 indexInSelected = envs.selected.findIndex((x) => x.key === envName)
): TestResult["envs"] => {
const { global, selected } = envs
const indexInSelected = selected.findIndex(
(x: SelectedEnvItem) => x.key === envName
)
// Found the match in selected
if (indexInSelected >= 0) {
envs.selected[indexInSelected].value = envValue
selected[indexInSelected].value = envValue
return {
global: envs.global,
selected: envs.selected,
global,
selected,
}
}
const indexInGlobal = envs.global.findIndex((x) => x.key == envName)
const indexInGlobal = global.findIndex((x: GlobalEnvItem) => x.key == envName)
// Found a match in globals
if (indexInGlobal >= 0) {
envs.global[indexInGlobal].value = envValue
global[indexInGlobal].value = envValue
return {
global: envs.global,
selected: envs.selected,
global,
selected,
}
}
// Didn't find in both places, create a new variable in selected
envs.selected.push({
selected.push({
key: envName,
value: envValue,
})
return {
global: envs.global,
selected: envs.selected,
global,
selected,
}
}
export function preventCyclicObjects(
obj: Record<string, any>
): E.Left<string> | E.Right<Record<string, any>> {
let jsonString
try {
jsonString = JSON.stringify(obj)
} catch (e) {
return E.left("Stringification failed")
}
try {
const parsedJson = JSON.parse(jsonString)
return E.right(parsedJson)
} catch (err) {
return E.left("Parsing failed")
}
}
/**
* 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> = {}
const toBeFn = (expectedVal: any) => {
let assertion = expectVal === expectedVal
if (negated) {
assertion = !assertion
}
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)
if (!Number.isNaN(parsedExpectVal)) {
let assertion =
parsedExpectVal >= rangeStart && parsedExpectVal <= rangeEnd
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 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),
},
})
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)
}
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

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

View File

@@ -1,19 +1,17 @@
{
"compilerOptions": {
"target": "ES6",
"module": "CommonJS",
"target": "ESNext",
"module": "ESNext",
"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"],
"outDir": "./lib/",
"rootDir": "./src/",
"types": ["@types/node", "@types/jest", "@relmify/jest-fp-ts", "vite/client"],
"outDir": "./dist/",
"declaration": true,
"declarationMap": true,
"sourceMap": true

View File

@@ -1,11 +0,0 @@
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

@@ -0,0 +1,25 @@
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

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

View File

@@ -117,7 +117,13 @@
alt="No invites"
text="No invites"
>
<HoppButtonSecondary label="Add new" filled @click="addNewMember" />
<template #body>
<HoppButtonSecondary
label="Add new"
filled
@click="addNewMember"
/>
</template>
</HoppSmartPlaceholder>
</div>
<div

View File

@@ -15,15 +15,17 @@
v-if="team?.teamMembers?.length === 0"
text="No members in this team. Add members to this team to collaborate"
>
<HoppButtonSecondary
:icon="IconUserPlus"
:label="t('teams.add_members')"
@click="
() => {
showInvite = !showInvite;
}
"
/>
<template #body>
<HoppButtonSecondary
:icon="IconUserPlus"
:label="t('teams.add_members')"
@click="
() => {
showInvite = !showInvite;
}
"
/>
</template>
</HoppSmartPlaceholder>
<div v-else class="divide-y divide-dividerLight">
<div

Some files were not shown because too many files have changed in this diff Show More