feat: migrate to a unified scripting system based on faraday-cage (#5090)
Co-authored-by: curiouscorrelation <curiouscorrelation@gmail.com>
This commit is contained in:
parent
01d96fa577
commit
656a15a983
51 changed files with 2173 additions and 710 deletions
|
|
@ -19,54 +19,74 @@ hopp [options or commands] arguments
|
|||
|
||||
## **Command Descriptions:**
|
||||
|
||||
1. #### **`hopp -v` / `hopp --ver`**
|
||||
1. #### **`hopp -v` / `hopp --ver`**
|
||||
|
||||
- Prints out the current version of the Hoppscotch CLI
|
||||
- Prints out the current version of the Hoppscotch CLI
|
||||
|
||||
2. #### **`hopp -h` / `hopp --help`**
|
||||
2. #### **`hopp -h` / `hopp --help`**
|
||||
|
||||
- Displays the help text
|
||||
- Displays the help text
|
||||
|
||||
3. #### **`hopp test [options] <file_path>`**
|
||||
3. #### **`hopp test [options] <file_path_or_id>`**
|
||||
|
||||
- Interactive CLI to accept Hoppscotch collection JSON path
|
||||
- Parses the collection JSON and executes each requests
|
||||
- Executes pre-request script.
|
||||
- Outputs the response of each request.
|
||||
- Executes and outputs test-script response.
|
||||
- Interactive CLI to accept Hoppscotch collection JSON path
|
||||
- Parses the collection JSON and executes each requests
|
||||
- Executes pre-request script.
|
||||
- Outputs the response of each request.
|
||||
- Executes and outputs test-script response.
|
||||
|
||||
#### Options:
|
||||
#### Options:
|
||||
|
||||
##### `-e <file_path>` / `--env <file_path>`
|
||||
##### `-e, --env <file_path_or_id> `
|
||||
|
||||
- Accepts path to env.json with contents in below format:
|
||||
- Accepts path to env.json with contents in below format:
|
||||
|
||||
```json
|
||||
{
|
||||
"ENV1": "value1",
|
||||
"ENV2": "value2"
|
||||
}
|
||||
```
|
||||
```json
|
||||
{
|
||||
"ENV1": "value1",
|
||||
"ENV2": "value2"
|
||||
}
|
||||
```
|
||||
|
||||
- You can now access those variables using `pw.env.get('<var_name>')`
|
||||
- You can now access those variables using `pw.env.get('<var_name>')`
|
||||
|
||||
Taking the above example, `pw.env.get("ENV1")` will return `"value1"`
|
||||
Taking the above example, `pw.env.get("ENV1")` will return `"value1"`
|
||||
|
||||
##### `--iteration-count <no_of_iterations>`
|
||||
#### `-d, --delay <delay_in_ms>`
|
||||
|
||||
- Accepts the number of iterations to run the collection
|
||||
- Used to defer the execution of requests in a collection.
|
||||
|
||||
##### `--iteration-data <file_path>`
|
||||
#### `--token <access_token>`
|
||||
|
||||
- Accepts the path to a CSV file with contents in the below format:
|
||||
- Expects a personal access token to be passed for establishing connection with your Hoppscotch account.
|
||||
|
||||
```text
|
||||
key1,key2,key3
|
||||
value1,value2,value3
|
||||
value4,value5,value6
|
||||
```
|
||||
#### `--server <server_url>`
|
||||
|
||||
For every iteration the values will be replaced with the respective keys in the environment. For iteration 1 the value1,value2,value3 will be replaced and for iteration 2 value4,value5,value6 will be replaced and so on.
|
||||
- URL of your self-hosted instance, if your collections are on a self-hosted instance.
|
||||
|
||||
#### `--reporter-junit [path]`
|
||||
|
||||
- Expects a file path to store the JUnit Report.
|
||||
|
||||
##### `--iteration-count <no_of_iterations>`
|
||||
|
||||
- Accepts the number of iterations to run the collection
|
||||
|
||||
##### `--iteration-data <file_path>`
|
||||
|
||||
- Accepts the path to a CSV file with contents in the below format:
|
||||
|
||||
```text
|
||||
key1,key2,key3
|
||||
value1,value2,value3
|
||||
value4,value5,value6
|
||||
```
|
||||
|
||||
For every iteration the values will be replaced with the respective keys in the environment. For iteration 1 the value1,value2,value3 will be replaced and for iteration 2 value4,value5,value6 will be replaced and so on.
|
||||
|
||||
#### `--legacy-sandbox`
|
||||
|
||||
- Opt out from the experimental scripting sandbox.
|
||||
|
||||
## Install
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@hoppscotch/cli",
|
||||
"version": "0.21.0",
|
||||
"version": "0.22.0",
|
||||
"description": "A CLI to run Hoppscotch test scripts in CI environments.",
|
||||
"homepage": "https://hoppscotch.io",
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ exports[`hopp test [options] <file_path_or_id> > Test\`hopp test <file_path_or_i
|
|||
<testsuite name="test-junit-report-export/request-level-errors/test-script-reference-error" time="time" timestamp="timestamp" tests="0" failures="0" errors="0">
|
||||
<system-err><![CDATA[
|
||||
REQUEST_ERROR - TypeError: Invalid URL
|
||||
TEST_SCRIPT_ERROR - Script execution failed: ReferenceError: status is not defined]]></system-err>
|
||||
TEST_SCRIPT_ERROR - Script execution failed: ReferenceError: 'status' is not defined]]></system-err>
|
||||
</testsuite>
|
||||
<testsuite name="test-junit-report-export/request-level-errors/non-existent-env-var" time="time" timestamp="timestamp" tests="22" failures="0" errors="22">
|
||||
<system-err><![CDATA[
|
||||
|
|
@ -401,7 +401,7 @@ exports[`hopp test [options] <file_path_or_id> > Test\`hopp test <file_path_or_i
|
|||
<testsuite name="test-junit-report-export/request-level-errors/test-script-reference-error" time="time" timestamp="timestamp" tests="0" failures="0" errors="0">
|
||||
<system-err><![CDATA[
|
||||
REQUEST_ERROR - TypeError: Invalid URL
|
||||
TEST_SCRIPT_ERROR - Script execution failed: ReferenceError: status is not defined]]></system-err>
|
||||
TEST_SCRIPT_ERROR - Script execution failed: ReferenceError: 'status' is not defined]]></system-err>
|
||||
</testsuite>
|
||||
<testsuite name="test-junit-report-export/request-level-errors/non-existent-env-var" time="time" timestamp="timestamp" tests="22" failures="0" errors="22">
|
||||
<system-err><![CDATA[
|
||||
|
|
|
|||
|
|
@ -20,9 +20,7 @@ describe("parseCollectionData", () => {
|
|||
|
||||
test("Invalid HoppCollection.", () => {
|
||||
return expect(
|
||||
parseCollectionData(
|
||||
"./src/__tests__/samples/malformed-collection2.json"
|
||||
)
|
||||
parseCollectionData("./src/__tests__/samples/malformed-collection2.json")
|
||||
).rejects.toMatchObject(<HoppCLIError>{
|
||||
code: "MALFORMED_COLLECTION",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,8 +20,14 @@ import { parseCollectionData } from "../utils/mutators";
|
|||
|
||||
export const test = (pathOrId: string, options: TestCmdOptions) => async () => {
|
||||
try {
|
||||
const { delay, env, iterationCount, iterationData, reporterJunit } =
|
||||
options;
|
||||
const {
|
||||
delay,
|
||||
env,
|
||||
iterationCount,
|
||||
iterationData,
|
||||
reporterJunit,
|
||||
legacySandbox,
|
||||
} = options;
|
||||
|
||||
if (
|
||||
iterationCount !== undefined &&
|
||||
|
|
@ -86,12 +92,15 @@ export const test = (pathOrId: string, options: TestCmdOptions) => async () => {
|
|||
.filter((item) => item.length > 0);
|
||||
}
|
||||
|
||||
const resolvedLegacySandbox = Boolean(legacySandbox);
|
||||
|
||||
const report = await collectionsRunner({
|
||||
collections,
|
||||
envs,
|
||||
delay: resolvedDelay,
|
||||
iterationData: transformedIterationData,
|
||||
iterationCount,
|
||||
legacySandbox: resolvedLegacySandbox,
|
||||
});
|
||||
const hasSucceeded = collectionsRunnerResult(report, reporterJunit);
|
||||
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ program
|
|||
"--iteration-data <file_path>",
|
||||
"path to a CSV file for data-driven testing"
|
||||
)
|
||||
.option("--legacy-sandbox", "Opt out from the experimental scripting sandbox")
|
||||
.allowExcessArguments(false)
|
||||
.allowUnknownOption(false)
|
||||
.description("running hoppscotch collection.json file")
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ export interface TestScriptParams {
|
|||
testScript: string;
|
||||
response: TestResponse;
|
||||
envs: HoppEnvs;
|
||||
legacySandbox: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { error } from "../../types/errors";
|
||||
|
||||
export function parseDelayOption(delay: string): number {
|
||||
const maybeInt = Number.parseInt(delay)
|
||||
const maybeInt = Number.parseInt(delay);
|
||||
|
||||
if(!Number.isNaN(maybeInt)) {
|
||||
return maybeInt
|
||||
if (!Number.isNaN(maybeInt)) {
|
||||
return maybeInt;
|
||||
} else {
|
||||
throw error({
|
||||
code: "INVALID_ARGUMENT",
|
||||
data: "Expected '-d, --delay' value to be number",
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export type CollectionRunnerParam = {
|
|||
delay?: number;
|
||||
iterationData?: IterationDataItem[][];
|
||||
iterationCount?: number;
|
||||
legacySandbox: boolean;
|
||||
};
|
||||
|
||||
export type HoppCollectionFileExt = "json";
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export type TestCmdOptions = {
|
|||
reporterJunit?: string;
|
||||
iterationCount?: number;
|
||||
iterationData?: string;
|
||||
legacySandbox?: boolean;
|
||||
};
|
||||
|
||||
// Consumed in the collection `file_path_or_id` argument action handler
|
||||
|
|
|
|||
|
|
@ -37,4 +37,5 @@ export type ProcessRequestParams = {
|
|||
envs: HoppEnvs;
|
||||
path: string;
|
||||
delay: number;
|
||||
legacySandbox: boolean;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -47,7 +47,14 @@ const { WARN, FAIL, INFO } = exceptionColors;
|
|||
export const collectionsRunner = async (
|
||||
param: CollectionRunnerParam
|
||||
): Promise<RequestReport[]> => {
|
||||
const { collections, envs, delay, iterationCount, iterationData } = param;
|
||||
const {
|
||||
collections,
|
||||
envs,
|
||||
delay,
|
||||
iterationCount,
|
||||
iterationData,
|
||||
legacySandbox,
|
||||
} = param;
|
||||
|
||||
const resolvedDelay = delay ?? 0;
|
||||
|
||||
|
|
@ -87,7 +94,8 @@ export const collectionsRunner = async (
|
|||
path,
|
||||
envs,
|
||||
resolvedDelay,
|
||||
requestsReport
|
||||
requestsReport,
|
||||
legacySandbox
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -100,7 +108,8 @@ const processCollection = async (
|
|||
path: string,
|
||||
envs: HoppEnvs,
|
||||
delay: number,
|
||||
requestsReport: RequestReport[]
|
||||
requestsReport: RequestReport[],
|
||||
legacySandbox?: boolean
|
||||
) => {
|
||||
// Process each request in the collection
|
||||
for (const request of collection.requests) {
|
||||
|
|
@ -111,6 +120,7 @@ const processCollection = async (
|
|||
request: _request,
|
||||
envs,
|
||||
delay,
|
||||
legacySandbox,
|
||||
};
|
||||
|
||||
// Request processing initiated message.
|
||||
|
|
@ -156,7 +166,8 @@ const processCollection = async (
|
|||
`${path}/${updatedFolder.name}`,
|
||||
envs,
|
||||
delay,
|
||||
requestsReport
|
||||
requestsReport,
|
||||
legacySandbox
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -120,7 +120,9 @@ export const printErrorsReport = (
|
|||
errorsReport: HoppCLIError[]
|
||||
) => {
|
||||
if (errorsReport.length > 0) {
|
||||
const REPORTED_ERRORS_TITLE = FAIL(`\n${chalk.bold(path)} reported errors:`);
|
||||
const REPORTED_ERRORS_TITLE = FAIL(
|
||||
`\n${chalk.bold(path)} reported errors:`
|
||||
);
|
||||
|
||||
group(REPORTED_ERRORS_TITLE);
|
||||
for (const errorReport of errorsReport) {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export const arrayFlatMap =
|
|||
|
||||
export const tupleToRecord = <
|
||||
KeyType extends string | number | symbol,
|
||||
ValueType
|
||||
ValueType,
|
||||
>(
|
||||
tuples: [KeyType, ValueType][]
|
||||
): Record<KeyType, ValueType> =>
|
||||
|
|
|
|||
|
|
@ -44,15 +44,18 @@ import { calculateHawkHeader } from "@hoppscotch/data";
|
|||
*/
|
||||
export const preRequestScriptRunner = (
|
||||
request: HoppRESTRequest,
|
||||
envs: HoppEnvs
|
||||
envs: HoppEnvs,
|
||||
legacySandbox: boolean
|
||||
): TE.TaskEither<
|
||||
HoppCLIError,
|
||||
{ effectiveRequest: EffectiveHoppRESTRequest } & { updatedEnvs: HoppEnvs }
|
||||
> =>
|
||||
pipe(
|
||||
> => {
|
||||
const experimentalScriptingSandbox = !legacySandbox;
|
||||
|
||||
return pipe(
|
||||
TE.of(request),
|
||||
TE.chain(({ preRequestScript }) =>
|
||||
runPreRequestScript(preRequestScript, envs)
|
||||
runPreRequestScript(preRequestScript, envs, experimentalScriptingSandbox)
|
||||
),
|
||||
TE.map(
|
||||
({ selected, global }) =>
|
||||
|
|
@ -77,6 +80,7 @@ export const preRequestScriptRunner = (
|
|||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Outputs an executable request format with environment variables applied
|
||||
|
|
|
|||
|
|
@ -202,7 +202,7 @@ export const processRequest =
|
|||
params: ProcessRequestParams
|
||||
): T.Task<{ envs: HoppEnvs; report: RequestReport }> =>
|
||||
async () => {
|
||||
const { envs, path, request, delay } = params;
|
||||
const { envs, path, request, delay, legacySandbox } = params;
|
||||
|
||||
// Initialising updatedEnvs with given parameter envs, will eventually get updated.
|
||||
const result = {
|
||||
|
|
@ -235,7 +235,8 @@ export const processRequest =
|
|||
// Executing pre-request-script
|
||||
const preRequestRes = await preRequestScriptRunner(
|
||||
request,
|
||||
processedEnvs
|
||||
processedEnvs,
|
||||
legacySandbox
|
||||
)();
|
||||
if (E.isLeft(preRequestRes)) {
|
||||
printPreRequestRunner.fail();
|
||||
|
|
@ -287,7 +288,8 @@ export const processRequest =
|
|||
const testScriptParams = getTestScriptParams(
|
||||
_requestRunnerRes,
|
||||
request,
|
||||
updatedEnvs
|
||||
updatedEnvs,
|
||||
legacySandbox
|
||||
);
|
||||
|
||||
// Executing test-runner.
|
||||
|
|
|
|||
|
|
@ -37,9 +37,15 @@ export const testRunner = (
|
|||
TE.bind("test_response", () =>
|
||||
pipe(
|
||||
TE.of(testScriptData),
|
||||
TE.chain(({ testScript, response, envs }) =>
|
||||
runTestScript(testScript, envs, response)
|
||||
)
|
||||
TE.chain(({ testScript, response, envs, legacySandbox }) => {
|
||||
const experimentalScriptingSandbox = !legacySandbox;
|
||||
return runTestScript(
|
||||
testScript,
|
||||
envs,
|
||||
response,
|
||||
experimentalScriptingSandbox
|
||||
);
|
||||
})
|
||||
)
|
||||
),
|
||||
|
||||
|
|
@ -137,7 +143,8 @@ export const testDescriptorParser = (
|
|||
export const getTestScriptParams = (
|
||||
reqRunnerRes: RequestRunnerResponse,
|
||||
request: HoppRESTRequest,
|
||||
envs: HoppEnvs
|
||||
envs: HoppEnvs,
|
||||
legacySandbox: boolean
|
||||
) => {
|
||||
const testScriptParams: TestScriptParams = {
|
||||
testScript: request.testScript,
|
||||
|
|
@ -146,7 +153,8 @@ export const getTestScriptParams = (
|
|||
status: reqRunnerRes.status,
|
||||
headers: reqRunnerRes.headers,
|
||||
},
|
||||
envs: envs,
|
||||
envs,
|
||||
legacySandbox,
|
||||
};
|
||||
return testScriptParams;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -956,6 +956,7 @@
|
|||
"ai_request_naming_style_pascal_case": "Pascal Case ( PascalCase )",
|
||||
"ai_request_naming_style_custom": "Custom",
|
||||
"ai_request_naming_style_custom_placeholder": "Enter your custom naming style template...",
|
||||
"experimental_scripting_sandbox": "Experimental scripting sandbox",
|
||||
"sync": "Synchronise",
|
||||
"sync_collections": "Collections",
|
||||
"sync_description": "These settings are synced to cloud.",
|
||||
|
|
@ -1851,5 +1852,9 @@
|
|||
"update_billing_cycle": "Are you sure you want to update the billing cycle to {newBillingCycle}?"
|
||||
},
|
||||
"cancel_subscription": "Cancel subscription"
|
||||
},
|
||||
"app_console": {
|
||||
"entries": "Console entries",
|
||||
"no_entries": "No entries"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,12 +34,12 @@
|
|||
"@codemirror/search": "6.5.6",
|
||||
"@codemirror/state": "6.4.1",
|
||||
"@codemirror/view": "6.25.1",
|
||||
"@hoppscotch/plugin-appload": "github:CuriousCorrelation/tauri-plugin-appload",
|
||||
"@hoppscotch/codemirror-lang-graphql": "workspace:^",
|
||||
"@hoppscotch/data": "workspace:^",
|
||||
"@hoppscotch/httpsnippet": "3.0.7",
|
||||
"@hoppscotch/js-sandbox": "workspace:^",
|
||||
"@hoppscotch/kernel": "workspace:^",
|
||||
"@hoppscotch/plugin-appload": "github:CuriousCorrelation/tauri-plugin-appload",
|
||||
"@hoppscotch/ui": "0.2.2",
|
||||
"@hoppscotch/vue-toasted": "0.1.0",
|
||||
"@lezer/highlight": "1.2.0",
|
||||
|
|
@ -47,8 +47,8 @@
|
|||
"@scure/base": "1.1.9",
|
||||
"@shopify/lang-jsonc": "1.0.0",
|
||||
"@tauri-apps/plugin-store": "2.2.0",
|
||||
"@types/markdown-it": "14.1.2",
|
||||
"@types/hawk": "9.0.6",
|
||||
"@types/markdown-it": "14.1.2",
|
||||
"@unhead/vue": "1.11.10",
|
||||
"@urql/core": "5.0.6",
|
||||
"@urql/devtools": "2.0.3",
|
||||
|
|
@ -105,6 +105,7 @@
|
|||
"verzod": "0.2.4",
|
||||
"vue": "3.5.12",
|
||||
"vue-i18n": "10.0.4",
|
||||
"vue-json-pretty": "2.4.0",
|
||||
"vue-pdf-embed": "2.1.0",
|
||||
"vue-router": "4.4.5",
|
||||
"vue-tippy": "6.5.0",
|
||||
|
|
|
|||
|
|
@ -71,6 +71,9 @@ declare module 'vue' {
|
|||
CollectionsRequest: typeof import('./components/collections/Request.vue')['default']
|
||||
CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default']
|
||||
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
|
||||
ConsoleItem: typeof import('./components/console/Item.vue')['default']
|
||||
ConsolePanel: typeof import('./components/console/Panel.vue')['default']
|
||||
ConsoleValue: typeof import('./components/console/Value.vue')['default']
|
||||
CookiesAllModal: typeof import('./components/cookies/AllModal.vue')['default']
|
||||
CookiesEditCookie: typeof import('./components/cookies/EditCookie.vue')['default']
|
||||
Embeds: typeof import('./components/embeds/index.vue')['default']
|
||||
|
|
|
|||
61
packages/hoppscotch-common/src/components/console/Item.vue
Normal file
61
packages/hoppscotch-common/src/components/console/Item.vue
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<template>
|
||||
<div
|
||||
class="flex items-start px-4 py-2 text-tiny text-secondaryDark rounded-md"
|
||||
:class="color"
|
||||
>
|
||||
<component :is="icon" class="mr-2 shrink-0" />
|
||||
|
||||
<div class="flex flex-col space-y-1 overflow-x-auto text-xs">
|
||||
<div class="text-secondaryLight">{{ formattedTimestamp }}</div>
|
||||
|
||||
<div class="flex flex-col space-y-1">
|
||||
<ConsoleValue
|
||||
v-for="(arg, idx) in entry.args"
|
||||
:key="idx"
|
||||
:value="arg"
|
||||
class="overflow-auto"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue"
|
||||
|
||||
import IconAlertCircle from "~icons/lucide/alert-circle"
|
||||
import IconAlertTriangle from "~icons/lucide/alert-triangle"
|
||||
import IconBug from "~icons/lucide/bug"
|
||||
import IconInfo from "~icons/lucide/info"
|
||||
import IconTerminal from "~icons/lucide/terminal"
|
||||
|
||||
import { ConsoleEntry, ConsoleLogLevel } from "./Panel.vue"
|
||||
|
||||
type LogLevelsWithBgColor = "info" | "warn" | "error"
|
||||
|
||||
const props = defineProps<{
|
||||
entry: ConsoleEntry
|
||||
}>()
|
||||
|
||||
const bgColors: Pick<Record<ConsoleLogLevel, string>, LogLevelsWithBgColor> = {
|
||||
info: "bg-bannerInfo",
|
||||
warn: "bg-bannerWarning",
|
||||
error: "bg-bannerError",
|
||||
}
|
||||
|
||||
const icons: Record<ConsoleLogLevel, unknown> = {
|
||||
info: IconInfo,
|
||||
warn: IconAlertCircle,
|
||||
error: IconAlertTriangle,
|
||||
log: IconTerminal,
|
||||
debug: IconBug,
|
||||
}
|
||||
|
||||
const color = computed(() => bgColors[props.entry.type as LogLevelsWithBgColor])
|
||||
const icon = computed(() => icons[props.entry.type])
|
||||
|
||||
const formattedTimestamp = computed(() => {
|
||||
const dateEntry = new Date(props.entry.timestamp)
|
||||
return dateEntry.toLocaleTimeString()
|
||||
})
|
||||
</script>
|
||||
55
packages/hoppscotch-common/src/components/console/Panel.vue
Normal file
55
packages/hoppscotch-common/src/components/console/Panel.vue
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<template>
|
||||
<div class="overflow-y-auto">
|
||||
<div
|
||||
class="truncate font-semibold text-secondaryLight border-b border-dividerLight px-4 py-2"
|
||||
>
|
||||
{{ t("app_console.entries") }}
|
||||
</div>
|
||||
|
||||
<HoppSmartPlaceholder
|
||||
v-if="renderedMessages.length === 0"
|
||||
:src="`/images/states/${colorMode.value}/validation.svg`"
|
||||
:alt="t('app.console.no_entries')"
|
||||
:heading="t('app_console.no_entries')"
|
||||
>
|
||||
</HoppSmartPlaceholder>
|
||||
|
||||
<div v-else class="px-4 py-3 space-y-2">
|
||||
<ConsoleItem
|
||||
v-for="(entry, index) in renderedMessages"
|
||||
:key="index"
|
||||
:entry="entry"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
import { useColorMode } from "~/composables/theming"
|
||||
|
||||
export type ConsoleLogLevel = "log" | "warn" | "info" | "error" | "debug"
|
||||
|
||||
export type ConsoleEntry = {
|
||||
type: ConsoleLogLevel
|
||||
args: unknown[]
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
messages: ConsoleEntry[]
|
||||
}>()
|
||||
|
||||
const colorMode = useColorMode()
|
||||
const t = useI18n()
|
||||
|
||||
// Filter out "clear" and compute final list to show (simulate console.clear)
|
||||
const renderedMessages = computed(() => {
|
||||
const output: ConsoleEntry[] = []
|
||||
for (const entry of props.messages) {
|
||||
output.push(entry)
|
||||
}
|
||||
return output
|
||||
})
|
||||
</script>
|
||||
101
packages/hoppscotch-common/src/components/console/Value.vue
Normal file
101
packages/hoppscotch-common/src/components/console/Value.vue
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
<template>
|
||||
<div class="whitespace-pre-wrap font-mono text-sm">
|
||||
<VueJsonPretty
|
||||
v-if="isObjectOrArray"
|
||||
:data="parsedValue"
|
||||
:deep="2"
|
||||
:class="snippetColors"
|
||||
/>
|
||||
|
||||
<pre
|
||||
v-else-if="isStringifiedObject"
|
||||
class="overflow-auto max-h-96 p-4"
|
||||
:class="snippetColors"
|
||||
>{{ prettyStringified }}
|
||||
</pre>
|
||||
|
||||
<pre v-else
|
||||
>{{ formattedPrimitive }}
|
||||
</pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue"
|
||||
import VueJsonPretty from "vue-json-pretty"
|
||||
|
||||
import "vue-json-pretty/lib/styles.css"
|
||||
|
||||
const props = defineProps<{ value: unknown }>()
|
||||
|
||||
const snippetColors = `
|
||||
border rounded-md bg-gray-50 text-black !border-gray-200
|
||||
dark:bg-gray-900 dark:text-gray-100 dark:!border-gray-700
|
||||
`
|
||||
|
||||
const isObjectOrArray = computed(() => {
|
||||
return typeof props.value === "object" && props.value !== null
|
||||
})
|
||||
|
||||
const isStringifiedObject = computed(() => {
|
||||
if (typeof props.value !== "string") return false
|
||||
try {
|
||||
const parsed = JSON.parse(props.value)
|
||||
return typeof parsed === "object" && parsed !== null
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
const parsedValue = computed(() => {
|
||||
if (isObjectOrArray.value) return props.value
|
||||
|
||||
if (isStringifiedObject.value && typeof props.value === "string") {
|
||||
try {
|
||||
return JSON.parse(props.value)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
const prettyStringified = computed(() => {
|
||||
if (typeof props.value === "string") {
|
||||
try {
|
||||
const parsed = JSON.parse(props.value)
|
||||
return JSON.stringify(parsed, null, 2)
|
||||
} catch {
|
||||
return props.value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
})
|
||||
|
||||
const formattedPrimitive = computed(() => {
|
||||
const val = props.value
|
||||
|
||||
if (typeof val === "string") {
|
||||
return val
|
||||
}
|
||||
|
||||
if (typeof val === "number" || typeof val === "boolean") {
|
||||
return String(val)
|
||||
}
|
||||
|
||||
if (val === null) {
|
||||
return "null"
|
||||
}
|
||||
|
||||
if (val === undefined) {
|
||||
return "undefined"
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.stringify(val, null, 2)
|
||||
} catch {
|
||||
return "[Unserializable]"
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
@ -49,19 +49,29 @@
|
|||
:is-editable="false"
|
||||
/>
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
v-if="showConsoleTab"
|
||||
id="console"
|
||||
label="Console"
|
||||
class="flex flex-1 flex-col"
|
||||
>
|
||||
<ConsolePanel :messages="consoleEntries" />
|
||||
</HoppSmartTab>
|
||||
</HoppSmartTabs>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue"
|
||||
import {
|
||||
getSuitableLenses,
|
||||
getLensRenderers,
|
||||
Lens,
|
||||
} from "~/helpers/lenses/lenses"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { computed, ref, watch } from "vue"
|
||||
import { useSetting } from "~/composables/settings"
|
||||
import {
|
||||
getLensRenderers,
|
||||
getSuitableLenses,
|
||||
Lens,
|
||||
} from "~/helpers/lenses/lenses"
|
||||
import { HoppRequestDocument } from "~/helpers/rest/document"
|
||||
import { ConsoleEntry } from "../console/Panel.vue"
|
||||
|
||||
const props = defineProps<{
|
||||
document: HoppRequestDocument
|
||||
|
|
@ -75,6 +85,10 @@ const emit = defineEmits<{
|
|||
}>()
|
||||
|
||||
const doc = useVModel(props, "document", emit)
|
||||
const t = useI18n()
|
||||
const EXPERIMENTAL_SCRIPTING_SANDBOX = useSetting(
|
||||
"EXPERIMENTAL_SCRIPTING_SANDBOX"
|
||||
)
|
||||
|
||||
const isSavable = computed(() => {
|
||||
return doc.value.response?.type === "success" && doc.value.saveContext
|
||||
|
|
@ -99,8 +113,6 @@ function lensRendererFor(name: string) {
|
|||
return allLensRenderers[name]
|
||||
}
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const selectedLensTab = ref("")
|
||||
|
||||
const maybeHeaders = computed(() => {
|
||||
|
|
@ -134,6 +146,27 @@ const validLenses = computed(() => {
|
|||
return getSuitableLenses(doc.value.response)
|
||||
})
|
||||
|
||||
const showConsoleTab = computed(() => {
|
||||
if (!doc.value.testResults?.consoleEntries) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
doc.value.testResults?.consoleEntries.length > 0 &&
|
||||
EXPERIMENTAL_SCRIPTING_SANDBOX.value
|
||||
)
|
||||
})
|
||||
|
||||
const consoleEntries = computed(() => {
|
||||
if (!doc.value.testResults?.consoleEntries) {
|
||||
return []
|
||||
}
|
||||
|
||||
return doc.value.testResults?.consoleEntries.filter(({ type }) =>
|
||||
["log", "warn", "debug", "error", "info"].includes(type)
|
||||
) as ConsoleEntry[]
|
||||
})
|
||||
|
||||
watch(
|
||||
validLenses,
|
||||
(newLenses: Lens[]) => {
|
||||
|
|
|
|||
|
|
@ -4,8 +4,12 @@ import {
|
|||
HoppRESTRequest,
|
||||
HoppRESTRequestVariable,
|
||||
} from "@hoppscotch/data"
|
||||
import { SandboxTestResult, TestDescriptor } from "@hoppscotch/js-sandbox"
|
||||
import { runTestScript } from "@hoppscotch/js-sandbox/web"
|
||||
import {
|
||||
SandboxPreRequestResult,
|
||||
SandboxTestResult,
|
||||
TestDescriptor,
|
||||
TestResult,
|
||||
} from "@hoppscotch/js-sandbox"
|
||||
import * as A from "fp-ts/Array"
|
||||
import * as E from "fp-ts/Either"
|
||||
import * as O from "fp-ts/Option"
|
||||
|
|
@ -15,6 +19,9 @@ import { Observable, Subject } from "rxjs"
|
|||
import { filter } from "rxjs/operators"
|
||||
import { Ref } from "vue"
|
||||
|
||||
import { map } from "fp-ts/Either"
|
||||
|
||||
import { runTestScript } from "@hoppscotch/js-sandbox/web"
|
||||
import { getService } from "~/modules/dioc"
|
||||
import {
|
||||
environmentsStore,
|
||||
|
|
@ -31,15 +38,8 @@ import {
|
|||
import { HoppTab } from "~/services/tab"
|
||||
import { updateTeamEnvironment } from "./backend/mutations/TeamEnvironment"
|
||||
import { createRESTNetworkRequestStream } from "./network"
|
||||
import {
|
||||
getCombinedEnvVariables,
|
||||
getFinalEnvsFromPreRequest,
|
||||
} from "./preRequest"
|
||||
import { getFinalEnvsFromPreRequest } from "./preRequest"
|
||||
import { HoppRequestDocument } from "./rest/document"
|
||||
import { HoppRESTResponse } from "./types/HoppRESTResponse"
|
||||
import { HoppTestData, HoppTestResult } from "./types/HoppTestResult"
|
||||
import { getEffectiveRESTRequest } from "./utils/EffectiveURL"
|
||||
import { isJSONContentType } from "./utils/contenttypes"
|
||||
import {
|
||||
getTemporaryVariables,
|
||||
setTemporaryVariables,
|
||||
|
|
@ -48,10 +48,31 @@ import {
|
|||
CurrentValueService,
|
||||
Variable,
|
||||
} from "~/services/current-environment-value.service"
|
||||
import { HoppRESTResponse } from "./types/HoppRESTResponse"
|
||||
import { HoppTestData, HoppTestResult } from "./types/HoppTestResult"
|
||||
import { getEffectiveRESTRequest } from "./utils/EffectiveURL"
|
||||
import { isJSONContentType } from "./utils/contenttypes"
|
||||
import { getCombinedEnvVariables } from "./utils/environments"
|
||||
import { useSetting } from "~/composables/settings"
|
||||
import {
|
||||
OutgoingSandboxPostRequestWorkerMessage,
|
||||
OutgoingSandboxPreRequestWorkerMessage,
|
||||
} from "./workers/sandbox.worker"
|
||||
|
||||
const sandboxWorker = new Worker(
|
||||
new URL("./workers/sandbox.worker.ts", import.meta.url),
|
||||
{
|
||||
type: "module",
|
||||
}
|
||||
)
|
||||
|
||||
const secretEnvironmentService = getService(SecretEnvironmentService)
|
||||
const currentEnvironmentValueService = getService(CurrentValueService)
|
||||
|
||||
const EXPERIMENTAL_SCRIPTING_SANDBOX = useSetting(
|
||||
"EXPERIMENTAL_SCRIPTING_SANDBOX"
|
||||
)
|
||||
|
||||
export const getTestableBody = (
|
||||
res: HoppRESTResponse & { type: "success" | "fail" }
|
||||
) => {
|
||||
|
|
@ -226,6 +247,88 @@ const filterNonEmptyEnvironmentVariables = (
|
|||
return Array.from(envsMap.values())
|
||||
}
|
||||
|
||||
const runPreRequestScript = (
|
||||
script: string,
|
||||
envs: {
|
||||
global: Environment["variables"]
|
||||
selected: Environment["variables"]
|
||||
temp: Environment["variables"]
|
||||
}
|
||||
): Promise<E.Either<string, SandboxPreRequestResult>> => {
|
||||
if (!EXPERIMENTAL_SCRIPTING_SANDBOX.value) {
|
||||
return getFinalEnvsFromPreRequest(script, envs, false)
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const handleMessage = (
|
||||
event: MessageEvent<OutgoingSandboxPreRequestWorkerMessage>
|
||||
) => {
|
||||
if (event.data.type === "PRE_REQUEST_SCRIPT_ERROR") {
|
||||
const error =
|
||||
event.data.data instanceof Error
|
||||
? event.data.data.message
|
||||
: String(event.data.data)
|
||||
|
||||
sandboxWorker.removeEventListener("message", handleMessage)
|
||||
resolve(E.left(error))
|
||||
}
|
||||
|
||||
if (event.data.type === "PRE_REQUEST_SCRIPT_RESULT") {
|
||||
sandboxWorker.removeEventListener("message", handleMessage)
|
||||
resolve(event.data.data)
|
||||
}
|
||||
}
|
||||
|
||||
sandboxWorker.addEventListener("message", handleMessage)
|
||||
|
||||
sandboxWorker.postMessage({
|
||||
type: "pre",
|
||||
script,
|
||||
envs,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const runPostRequestScript = (
|
||||
script: string,
|
||||
envs: TestResult["envs"],
|
||||
response: HoppRESTResponse
|
||||
): Promise<E.Either<string, SandboxTestResult>> => {
|
||||
if (!EXPERIMENTAL_SCRIPTING_SANDBOX.value) {
|
||||
return runTestScript(script, envs, response, false)
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const handleMessage = (
|
||||
event: MessageEvent<OutgoingSandboxPostRequestWorkerMessage>
|
||||
) => {
|
||||
if (event.data.type === "POST_REQUEST_SCRIPT_ERROR") {
|
||||
const error =
|
||||
event.data.data instanceof Error
|
||||
? event.data.data.message
|
||||
: String(event.data.data)
|
||||
|
||||
sandboxWorker.removeEventListener("message", handleMessage)
|
||||
resolve(E.left(error))
|
||||
}
|
||||
|
||||
if (event.data.type === "POST_REQUEST_SCRIPT_RESULT") {
|
||||
sandboxWorker.removeEventListener("message", handleMessage)
|
||||
resolve(event.data.data)
|
||||
}
|
||||
}
|
||||
|
||||
sandboxWorker.addEventListener("message", handleMessage)
|
||||
|
||||
sandboxWorker.postMessage({
|
||||
type: "post",
|
||||
script,
|
||||
envs,
|
||||
response,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function runRESTRequest$(
|
||||
tab: Ref<HoppTab<HoppRequestDocument>>
|
||||
): [
|
||||
|
|
@ -243,14 +346,14 @@ export function runRESTRequest$(
|
|||
cancelFunc?.()
|
||||
}
|
||||
|
||||
const res = getFinalEnvsFromPreRequest(
|
||||
const res = runPreRequestScript(
|
||||
tab.value.document.request.preRequestScript,
|
||||
getCombinedEnvVariables()
|
||||
).then(async (envs) => {
|
||||
).then(async (preRequestScriptResult) => {
|
||||
if (cancelCalled) return E.left("cancellation" as const)
|
||||
|
||||
if (E.isLeft(envs)) {
|
||||
console.error(envs.left)
|
||||
if (E.isLeft(preRequestScriptResult)) {
|
||||
console.error(preRequestScriptResult.left)
|
||||
return E.left("script_fail" as const)
|
||||
}
|
||||
|
||||
|
|
@ -302,7 +405,7 @@ export function runRESTRequest$(
|
|||
|
||||
const finalEnvs = {
|
||||
requestVariables: finalRequestVariables as Environment["variables"],
|
||||
environments: envs.right,
|
||||
environments: preRequestScriptResult.right.envs,
|
||||
}
|
||||
|
||||
const finalEnvsWithNonEmptyValues = filterNonEmptyEnvironmentVariables(
|
||||
|
|
@ -326,9 +429,9 @@ export function runRESTRequest$(
|
|||
if (res.type === "success" || res.type === "fail") {
|
||||
executedResponses$.next(res)
|
||||
|
||||
const runResult = await runTestScript(
|
||||
const postRequestScriptResult = await runPostRequestScript(
|
||||
res.req.testScript,
|
||||
envs.right,
|
||||
preRequestScriptResult.right.envs,
|
||||
{
|
||||
status: res.statusCode,
|
||||
body: getTestableBody(res),
|
||||
|
|
@ -336,13 +439,26 @@ export function runRESTRequest$(
|
|||
}
|
||||
)
|
||||
|
||||
if (E.isRight(runResult)) {
|
||||
if (E.isRight(postRequestScriptResult)) {
|
||||
// set the response in the tab so that multiple tabs can run request simultaneously
|
||||
tab.value.document.response = res
|
||||
tab.value.document.testResults = translateToSandboxTestResults(
|
||||
runResult.right
|
||||
)
|
||||
updateEnvsAfterTestScript(runResult)
|
||||
|
||||
// Combine console entries from pre and post request scripts
|
||||
const combinedResult = pipe(
|
||||
postRequestScriptResult,
|
||||
map((result) => ({
|
||||
...result,
|
||||
consoleEntries: [
|
||||
...(preRequestScriptResult.right.consoleEntries ?? []),
|
||||
...(result.consoleEntries ?? []),
|
||||
],
|
||||
}))
|
||||
) as E.Right<SandboxTestResult>
|
||||
|
||||
const updatedRunResult = updateEnvsAfterTestScript(combinedResult)
|
||||
|
||||
tab.value.document.testResults =
|
||||
translateToSandboxTestResults(updatedRunResult)
|
||||
} else {
|
||||
tab.value.document.testResults = {
|
||||
description: "",
|
||||
|
|
@ -361,6 +477,7 @@ export function runRESTRequest$(
|
|||
},
|
||||
},
|
||||
scriptError: true,
|
||||
consoleEntries: [],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -452,12 +569,12 @@ export function runTestRunnerRequest(
|
|||
}>
|
||||
| undefined
|
||||
> {
|
||||
return getFinalEnvsFromPreRequest(
|
||||
return runPreRequestScript(
|
||||
request.preRequestScript,
|
||||
getCombinedEnvVariables()
|
||||
).then(async (envs) => {
|
||||
if (E.isLeft(envs)) {
|
||||
console.error(envs.left)
|
||||
).then(async (preRequestScriptResult) => {
|
||||
if (E.isLeft(preRequestScriptResult)) {
|
||||
console.error(preRequestScriptResult.left)
|
||||
return E.left("script_fail" as const)
|
||||
}
|
||||
|
||||
|
|
@ -467,7 +584,7 @@ export function runTestRunnerRequest(
|
|||
name: "Env",
|
||||
variables: combineEnvVariables({
|
||||
environments: {
|
||||
...envs.right,
|
||||
...preRequestScriptResult.right.envs,
|
||||
temp: !persistEnv ? getTemporaryVariables() : [],
|
||||
},
|
||||
requestVariables: [],
|
||||
|
|
@ -483,9 +600,9 @@ export function runTestRunnerRequest(
|
|||
if (res?.type === "success" || res?.type === "fail") {
|
||||
executedResponses$.next(res)
|
||||
|
||||
const runResult = await runTestScript(
|
||||
const postRequestScriptResult = await runPostRequestScript(
|
||||
res.req.testScript,
|
||||
envs.right,
|
||||
preRequestScriptResult.right.envs,
|
||||
{
|
||||
status: res.statusCode,
|
||||
body: getTestableBody(res),
|
||||
|
|
@ -493,19 +610,27 @@ export function runTestRunnerRequest(
|
|||
}
|
||||
)
|
||||
|
||||
if (E.isRight(runResult)) {
|
||||
const sandboxTestResult = translateToSandboxTestResults(
|
||||
runResult.right
|
||||
)
|
||||
if (E.isRight(postRequestScriptResult)) {
|
||||
// Combine console entries from pre and post request scripts
|
||||
const combinedResult = {
|
||||
...postRequestScriptResult.right,
|
||||
consoleEntries: [
|
||||
...(preRequestScriptResult.right.consoleEntries ?? []),
|
||||
...(postRequestScriptResult.right.consoleEntries ?? []),
|
||||
],
|
||||
}
|
||||
|
||||
const sandboxTestResult =
|
||||
translateToSandboxTestResults(combinedResult)
|
||||
|
||||
// Update the environment variables after running the test script when persistEnv is true. else store the updated environment variables in the store as a temporary variable.
|
||||
if (persistEnv) {
|
||||
updateEnvsAfterTestScript(runResult)
|
||||
updateEnvsAfterTestScript(postRequestScriptResult)
|
||||
} else {
|
||||
// Combine global and selected environment changes
|
||||
const allChanges = [
|
||||
...runResult.right.envs.global,
|
||||
...runResult.right.envs.selected,
|
||||
...postRequestScriptResult.right.envs.global,
|
||||
...postRequestScriptResult.right.envs.selected,
|
||||
]
|
||||
|
||||
setTemporaryVariables(allChanges)
|
||||
|
|
@ -533,6 +658,7 @@ export function runTestRunnerRequest(
|
|||
},
|
||||
},
|
||||
scriptError: true,
|
||||
consoleEntries: [],
|
||||
}
|
||||
return E.right({
|
||||
response: res,
|
||||
|
|
@ -631,5 +757,6 @@ function translateToSandboxTestResults(
|
|||
updations: getUpdatedEnvVariables(envVars, testDesc.envs.selected),
|
||||
},
|
||||
},
|
||||
consoleEntries: testDesc.consoleEntries,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { getService } from "~/modules/dioc"
|
||||
import { getCombinedEnvVariables } from "../preRequest"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import { parseTemplateStringE } from "@hoppscotch/data"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { getCombinedEnvVariables } from "../utils/environments"
|
||||
|
||||
export const replaceTemplateStringsInObjectValues = <
|
||||
T extends Record<string, unknown>,
|
||||
|
|
|
|||
|
|
@ -1,86 +1,8 @@
|
|||
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 { runPreRequestScript } from "@hoppscotch/js-sandbox/web"
|
||||
import * as E from "fp-ts/Either"
|
||||
|
||||
import {
|
||||
getCurrentEnvironment,
|
||||
getGlobalVariables,
|
||||
} from "~/newstore/environments"
|
||||
import { TestResult } from "@hoppscotch/js-sandbox"
|
||||
import { getService } from "~/modules/dioc"
|
||||
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
||||
import { CurrentValueService } from "~/services/current-environment-value.service"
|
||||
|
||||
const secretEnvironmentService = getService(SecretEnvironmentService)
|
||||
const currentEnvironmentValueService = getService(CurrentValueService)
|
||||
|
||||
/**
|
||||
* Populate the currentValue of the environment variables and set the secret values
|
||||
* @param selected
|
||||
* @param global
|
||||
* @returns
|
||||
*/
|
||||
const unWrapEnvironments = (
|
||||
selected: Environment,
|
||||
global: Environment["variables"]
|
||||
) => {
|
||||
const resolvedGlobalWithSecrets = global.map((globalVar, index) => {
|
||||
const secretVar = secretEnvironmentService.getSecretEnvironmentVariable(
|
||||
"Global",
|
||||
index
|
||||
)
|
||||
const currentVar = currentEnvironmentValueService.getEnvironmentVariable(
|
||||
"Global",
|
||||
index
|
||||
)
|
||||
|
||||
if (secretVar) {
|
||||
return {
|
||||
...globalVar,
|
||||
currentValue: secretVar.value,
|
||||
}
|
||||
}
|
||||
return { ...globalVar, currentValue: currentVar?.currentValue ?? "" }
|
||||
})
|
||||
|
||||
const resolvedSelectedWithSecrets = selected.variables.map(
|
||||
(selectedVar, index) => {
|
||||
const secretVar = secretEnvironmentService.getSecretEnvironmentVariable(
|
||||
selected.id,
|
||||
index
|
||||
)
|
||||
const currentVar = currentEnvironmentValueService.getEnvironmentVariable(
|
||||
selected.id,
|
||||
index
|
||||
)
|
||||
if (secretVar) {
|
||||
return {
|
||||
...selectedVar,
|
||||
currentValue: secretVar.value,
|
||||
}
|
||||
}
|
||||
return { ...selectedVar, currentValue: currentVar?.currentValue ?? "" }
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
global: resolvedGlobalWithSecrets,
|
||||
selected: resolvedSelectedWithSecrets,
|
||||
}
|
||||
}
|
||||
|
||||
export const getCombinedEnvVariables = (temp?: Environment["variables"]) => {
|
||||
const reformedVars = unWrapEnvironments(
|
||||
getCurrentEnvironment(),
|
||||
getGlobalVariables()
|
||||
)
|
||||
return {
|
||||
global: cloneDeep(reformedVars.global),
|
||||
selected: cloneDeep(reformedVars.selected),
|
||||
temp: temp ? cloneDeep(temp) : [],
|
||||
}
|
||||
}
|
||||
import { SandboxPreRequestResult } from "@hoppscotch/js-sandbox"
|
||||
|
||||
export const getFinalEnvsFromPreRequest = (
|
||||
script: string,
|
||||
|
|
@ -88,6 +10,7 @@ export const getFinalEnvsFromPreRequest = (
|
|||
global: Environment["variables"]
|
||||
selected: Environment["variables"]
|
||||
temp: Environment["variables"]
|
||||
}
|
||||
): Promise<E.Either<string, TestResult["envs"]>> =>
|
||||
runPreRequestScript(script, envs)
|
||||
},
|
||||
experimentalScriptingSandbox = true
|
||||
): Promise<E.Either<string, SandboxPreRequestResult>> =>
|
||||
runPreRequestScript(script, envs, experimentalScriptingSandbox)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Environment } from "@hoppscotch/data"
|
||||
import { SandboxTestResult } from "@hoppscotch/js-sandbox"
|
||||
|
||||
export type HoppTestExpectResult = {
|
||||
status: "fail" | "pass" | "error"
|
||||
|
|
@ -33,4 +34,6 @@ export type HoppTestResult = {
|
|||
deletions: Environment["variables"]
|
||||
}
|
||||
}
|
||||
|
||||
consoleEntries: SandboxTestResult["consoleEntries"]
|
||||
}
|
||||
|
|
|
|||
80
packages/hoppscotch-common/src/helpers/utils/environments.ts
Normal file
80
packages/hoppscotch-common/src/helpers/utils/environments.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { Environment } from "@hoppscotch/data"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
|
||||
import { getService } from "~/modules/dioc"
|
||||
import {
|
||||
getCurrentEnvironment,
|
||||
getGlobalVariables,
|
||||
} from "~/newstore/environments"
|
||||
import { CurrentValueService } from "~/services/current-environment-value.service"
|
||||
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
||||
|
||||
const secretEnvironmentService = getService(SecretEnvironmentService)
|
||||
const currentEnvironmentValueService = getService(CurrentValueService)
|
||||
|
||||
/**
|
||||
* Populate the currentValue of the environment variables and set the secret values
|
||||
* @param selected
|
||||
* @param global
|
||||
* @returns
|
||||
*/
|
||||
const unWrapEnvironments = (
|
||||
selected: Environment,
|
||||
global: Environment["variables"]
|
||||
) => {
|
||||
const resolvedGlobalWithSecrets = global.map((globalVar, index) => {
|
||||
const secretVar = secretEnvironmentService.getSecretEnvironmentVariable(
|
||||
"Global",
|
||||
index
|
||||
)
|
||||
const currentVar = currentEnvironmentValueService.getEnvironmentVariable(
|
||||
"Global",
|
||||
index
|
||||
)
|
||||
|
||||
if (secretVar) {
|
||||
return {
|
||||
...globalVar,
|
||||
currentValue: secretVar.value,
|
||||
}
|
||||
}
|
||||
return { ...globalVar, currentValue: currentVar?.currentValue ?? "" }
|
||||
})
|
||||
|
||||
const resolvedSelectedWithSecrets = selected.variables.map(
|
||||
(selectedVar, index) => {
|
||||
const secretVar = secretEnvironmentService.getSecretEnvironmentVariable(
|
||||
selected.id,
|
||||
index
|
||||
)
|
||||
const currentVar = currentEnvironmentValueService.getEnvironmentVariable(
|
||||
selected.id,
|
||||
index
|
||||
)
|
||||
if (secretVar) {
|
||||
return {
|
||||
...selectedVar,
|
||||
currentValue: secretVar.value,
|
||||
}
|
||||
}
|
||||
return { ...selectedVar, currentValue: currentVar?.currentValue ?? "" }
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
global: resolvedGlobalWithSecrets,
|
||||
selected: resolvedSelectedWithSecrets,
|
||||
}
|
||||
}
|
||||
|
||||
export const getCombinedEnvVariables = (temp?: Environment["variables"]) => {
|
||||
const reformedVars = unWrapEnvironments(
|
||||
getCurrentEnvironment(),
|
||||
getGlobalVariables()
|
||||
)
|
||||
return {
|
||||
global: cloneDeep(reformedVars.global),
|
||||
selected: cloneDeep(reformedVars.selected),
|
||||
temp: temp ? cloneDeep(temp) : [],
|
||||
}
|
||||
}
|
||||
108
packages/hoppscotch-common/src/helpers/workers/sandbox.worker.ts
Normal file
108
packages/hoppscotch-common/src/helpers/workers/sandbox.worker.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import { Environment } from "@hoppscotch/data"
|
||||
import {
|
||||
SandboxPreRequestResult,
|
||||
SandboxTestResult,
|
||||
TestResult,
|
||||
} from "@hoppscotch/js-sandbox"
|
||||
import { runTestScript } from "@hoppscotch/js-sandbox/web"
|
||||
import * as E from "fp-ts/Either"
|
||||
|
||||
import { getFinalEnvsFromPreRequest } from "../preRequest"
|
||||
import { HoppRESTResponse } from "../types/HoppRESTResponse"
|
||||
|
||||
interface PreRequestMessage {
|
||||
type: "pre"
|
||||
script: string
|
||||
envs: {
|
||||
global: Environment["variables"]
|
||||
selected: Environment["variables"]
|
||||
temp: Environment["variables"]
|
||||
}
|
||||
}
|
||||
|
||||
interface PostRequestMessage {
|
||||
type: "post"
|
||||
script: string
|
||||
envs: TestResult["envs"]
|
||||
response: HoppRESTResponse
|
||||
}
|
||||
|
||||
type IncomingSandboxWorkerMessage = PreRequestMessage | PostRequestMessage
|
||||
|
||||
interface PreRequestScriptResultMessage {
|
||||
type: "PRE_REQUEST_SCRIPT_RESULT"
|
||||
data: E.Either<string, SandboxPreRequestResult>
|
||||
}
|
||||
|
||||
interface PreRequestScriptErrorMessage {
|
||||
type: "PRE_REQUEST_SCRIPT_ERROR"
|
||||
data: unknown
|
||||
}
|
||||
|
||||
interface PostRequestScriptResultMessage {
|
||||
type: "POST_REQUEST_SCRIPT_RESULT"
|
||||
data: E.Either<string, SandboxTestResult>
|
||||
}
|
||||
|
||||
interface PostRequestScriptErrorMessage {
|
||||
type: "POST_REQUEST_SCRIPT_ERROR"
|
||||
data: unknown
|
||||
}
|
||||
|
||||
export type OutgoingSandboxPreRequestWorkerMessage =
|
||||
| PreRequestScriptResultMessage
|
||||
| PreRequestScriptErrorMessage
|
||||
|
||||
export type OutgoingSandboxPostRequestWorkerMessage =
|
||||
| PostRequestScriptResultMessage
|
||||
| PostRequestScriptErrorMessage
|
||||
|
||||
self.addEventListener(
|
||||
"message",
|
||||
async (event: MessageEvent<IncomingSandboxWorkerMessage>) => {
|
||||
const { type, script, envs } = event.data
|
||||
|
||||
if (type === "pre") {
|
||||
try {
|
||||
const preRequestScriptResult = await getFinalEnvsFromPreRequest(
|
||||
script,
|
||||
envs
|
||||
)
|
||||
const result: PreRequestScriptResultMessage = {
|
||||
type: "PRE_REQUEST_SCRIPT_RESULT",
|
||||
data: preRequestScriptResult,
|
||||
}
|
||||
self.postMessage(result)
|
||||
} catch (error) {
|
||||
const err: PreRequestScriptErrorMessage = {
|
||||
type: "PRE_REQUEST_SCRIPT_ERROR",
|
||||
data: error,
|
||||
}
|
||||
self.postMessage(err)
|
||||
}
|
||||
}
|
||||
|
||||
if (type === "post") {
|
||||
const { response } = event.data
|
||||
|
||||
try {
|
||||
const postRequestScriptResult = await runTestScript(
|
||||
script,
|
||||
envs,
|
||||
response
|
||||
)
|
||||
const result: PostRequestScriptResultMessage = {
|
||||
type: "POST_REQUEST_SCRIPT_RESULT",
|
||||
data: postRequestScriptResult,
|
||||
}
|
||||
self.postMessage(result)
|
||||
} catch (error) {
|
||||
const err: PostRequestScriptErrorMessage = {
|
||||
type: "POST_REQUEST_SCRIPT_ERROR",
|
||||
data: error,
|
||||
}
|
||||
self.postMessage(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -82,6 +82,8 @@ export type SettingsDef = {
|
|||
| "PascalCase"
|
||||
| "CUSTOM"
|
||||
CUSTOM_NAMING_STYLE: string
|
||||
|
||||
EXPERIMENTAL_SCRIPTING_SANDBOX: boolean
|
||||
}
|
||||
|
||||
let defaultProxyURL = DEFAULT_HOPP_PROXY_URL
|
||||
|
|
@ -142,6 +144,8 @@ export const getDefaultSettings = (): SettingsDef => {
|
|||
ENABLE_AI_EXPERIMENTS: true,
|
||||
AI_REQUEST_NAMING_STYLE: "DESCRIPTIVE_WITH_SPACES",
|
||||
CUSTOM_NAMING_STYLE: "",
|
||||
|
||||
EXPERIMENTAL_SCRIPTING_SANDBOX: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -149,12 +149,21 @@
|
|||
)
|
||||
"
|
||||
rows="4"
|
||||
/>
|
||||
>
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<HoppSmartToggle
|
||||
:on="EXPERIMENTAL_SCRIPTING_SANDBOX"
|
||||
@change="toggleSetting('EXPERIMENTAL_SCRIPTING_SANDBOX')"
|
||||
>
|
||||
{{ t("settings.experimental_scripting_sandbox") }}
|
||||
</HoppSmartToggle>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -342,6 +351,10 @@ const ENABLE_AI_EXPERIMENTS = useSetting("ENABLE_AI_EXPERIMENTS")
|
|||
const AI_REQUEST_NAMING_STYLE = useSetting("AI_REQUEST_NAMING_STYLE")
|
||||
const CUSTOM_NAMING_STYLE = useSetting("CUSTOM_NAMING_STYLE")
|
||||
|
||||
const EXPERIMENTAL_SCRIPTING_SANDBOX = useSetting(
|
||||
"EXPERIMENTAL_SCRIPTING_SANDBOX"
|
||||
)
|
||||
|
||||
const supportedNamingStyles = [
|
||||
{
|
||||
id: "DESCRIPTIVE_WITH_SPACES" as const,
|
||||
|
|
|
|||
|
|
@ -82,6 +82,8 @@ const SettingsDefSchema = z.object({
|
|||
.optional()
|
||||
.catch("DESCRIPTIVE_WITH_SPACES"),
|
||||
CUSTOM_NAMING_STYLE: z.string().optional().catch(""),
|
||||
|
||||
EXPERIMENTAL_SCRIPTING_SANDBOX: z.optional(z.boolean()),
|
||||
})
|
||||
|
||||
const HoppRESTRequestSchema = entityReference(HoppRESTRequest)
|
||||
|
|
@ -431,6 +433,7 @@ const HoppTestResultSchema = z
|
|||
.strict(),
|
||||
})
|
||||
.strict(),
|
||||
consoleEntries: z.array(z.record(z.string(), z.unknown()).optional()),
|
||||
})
|
||||
.strict()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
import * as E from "fp-ts/Either"
|
||||
import { expect } from "vitest"
|
||||
|
||||
globalThis.Worker = class {
|
||||
constructor() {}
|
||||
postMessage = () => {}
|
||||
terminate = () => {}
|
||||
onmessage = null
|
||||
onerror = null
|
||||
} as any
|
||||
|
||||
expect.extend({
|
||||
toBeLeft(received, expected) {
|
||||
const { isNot } = this
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
"csp": {
|
||||
"default-src": "blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self' customprotocol: asset:",
|
||||
"script-src": "* 'self' 'unsafe-eval' 'wasm-unsafe-eval' 'unsafe-inline'",
|
||||
"connect-src": "ipc: http://ipc.localhost https://api.hoppscotch.io *",
|
||||
"connect-src": "ipc: http://ipc.localhost https://api.hoppscotch.io data: *",
|
||||
"font-src": "https://fonts.gstatic.com data: 'self' *",
|
||||
"img-src": "'self' asset: http://asset.localhost blob: data: customprotocol: *",
|
||||
"style-src": "'unsafe-inline' 'self' https://fonts.googleapis.com data: asset: *",
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@
|
|||
"dependencies": {
|
||||
"@hoppscotch/data": "workspace:^",
|
||||
"@types/lodash-es": "4.17.12",
|
||||
"faraday-cage": "0.0.15",
|
||||
"fp-ts": "2.16.9",
|
||||
"lodash": "4.17.21",
|
||||
"lodash-es": "4.17.21"
|
||||
|
|
|
|||
|
|
@ -18,410 +18,412 @@ const func = (script: string, res: TestResponse) =>
|
|||
TE.map((x) => x.tests)
|
||||
)
|
||||
|
||||
describe("toBeLevel2xx", () => {
|
||||
test("assertion passes for 200 series with no negation", async () => {
|
||||
for (let i = 200; i < 300; i++) {
|
||||
describe("toBeLevelxxx", { timeout: 100000 }, () => {
|
||||
describe("toBeLevel2xx", () => {
|
||||
test("assertion passes for 200 series with no negation", async () => {
|
||||
for (let i = 200; i < 300; i++) {
|
||||
await expect(
|
||||
func(`pw.expect(${i}).toBeLevel2xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "pass",
|
||||
message: `Expected '${i}' to be 200-level status`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
test("assertion fails for non 200 series with no negation", async () => {
|
||||
for (let i = 300; i < 500; i++) {
|
||||
await expect(
|
||||
func(`pw.expect(${i}).toBeLevel2xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "fail",
|
||||
message: `Expected '${i}' to be 200-level status`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
test("give error if the expect value was not a number with no negation", async () => {
|
||||
await expect(
|
||||
func(`pw.expect(${i}).toBeLevel2xx()`, fakeResponse)()
|
||||
func(`pw.expect("foo").toBeLevel2xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "pass",
|
||||
message: `Expected '${i}' to be 200-level status`,
|
||||
status: "error",
|
||||
message:
|
||||
"Expected 200-level status but could not parse value 'foo'",
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test("assertion fails for non 200 series with no negation", async () => {
|
||||
for (let i = 300; i < 500; i++) {
|
||||
test("assertion fails for 200 series with negation", async () => {
|
||||
for (let i = 200; i < 300; i++) {
|
||||
await expect(
|
||||
func(`pw.expect(${i}).not.toBeLevel2xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "fail",
|
||||
message: `Expected '${i}' to not be 200-level status`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
test("assertion passes for non 200 series with negation", async () => {
|
||||
for (let i = 300; i < 500; i++) {
|
||||
await expect(
|
||||
func(`pw.expect(${i}).not.toBeLevel2xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "pass",
|
||||
message: `Expected '${i}' to not be 200-level status`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
test("give error if the expect value was not a number with negation", async () => {
|
||||
await expect(
|
||||
func(`pw.expect(${i}).toBeLevel2xx()`, fakeResponse)()
|
||||
func(`pw.expect("foo").not.toBeLevel2xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "fail",
|
||||
message: `Expected '${i}' to be 200-level status`,
|
||||
status: "error",
|
||||
message:
|
||||
"Expected 200-level status but could not parse value 'foo'",
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test("give error if the expect value was not a number with no negation", async () => {
|
||||
await expect(
|
||||
func(`pw.expect("foo").toBeLevel2xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "error",
|
||||
message:
|
||||
"Expected 200-level status but could not parse value 'foo'",
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
})
|
||||
describe("toBeLevel3xx", () => {
|
||||
test("assertion passes for 300 series with no negation", async () => {
|
||||
for (let i = 300; i < 400; i++) {
|
||||
await expect(
|
||||
func(`pw.expect(${i}).toBeLevel3xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "pass",
|
||||
message: `Expected '${i}' to be 300-level status`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
test("assertion fails for 200 series with negation", async () => {
|
||||
for (let i = 200; i < 300; i++) {
|
||||
await expect(
|
||||
func(`pw.expect(${i}).not.toBeLevel2xx()`, fakeResponse)()
|
||||
test("assertion fails for non 300 series with no negation", async () => {
|
||||
for (let i = 400; i < 500; i++) {
|
||||
await expect(
|
||||
func(`pw.expect(${i}).toBeLevel3xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "fail",
|
||||
message: `Expected '${i}' to be 300-level status`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
test("give error if the expect value is not a number without negation", () => {
|
||||
return expect(
|
||||
func(`pw.expect("foo").toBeLevel3xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "fail",
|
||||
message: `Expected '${i}' to not be 200-level status`,
|
||||
status: "error",
|
||||
message:
|
||||
"Expected 300-level status but could not parse value 'foo'",
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test("assertion passes for non 200 series with negation", async () => {
|
||||
for (let i = 300; i < 500; i++) {
|
||||
await expect(
|
||||
func(`pw.expect(${i}).not.toBeLevel2xx()`, fakeResponse)()
|
||||
test("assertion fails for 400 series with negation", async () => {
|
||||
for (let i = 300; i < 400; i++) {
|
||||
await expect(
|
||||
func(`pw.expect(${i}).not.toBeLevel3xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "fail",
|
||||
message: `Expected '${i}' to not be 300-level status`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
test("assertion passes for non 200 series with negation", async () => {
|
||||
for (let i = 400; i < 500; i++) {
|
||||
await expect(
|
||||
func(`pw.expect(${i}).not.toBeLevel3xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "pass",
|
||||
message: `Expected '${i}' to not be 300-level status`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
test("give error if the expect value is not a number with negation", () => {
|
||||
return expect(
|
||||
func(`pw.expect("foo").not.toBeLevel3xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "pass",
|
||||
message: `Expected '${i}' to not be 200-level status`,
|
||||
status: "error",
|
||||
message:
|
||||
"Expected 300-level status but could not parse value 'foo'",
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test("give error if the expect value was not a number with negation", async () => {
|
||||
await expect(
|
||||
func(`pw.expect("foo").not.toBeLevel2xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "error",
|
||||
message:
|
||||
"Expected 200-level status but could not parse value 'foo'",
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("toBeLevel3xx", () => {
|
||||
test("assertion passes for 300 series with no negation", async () => {
|
||||
for (let i = 300; i < 400; i++) {
|
||||
await expect(
|
||||
func(`pw.expect(${i}).toBeLevel3xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "pass",
|
||||
message: `Expected '${i}' to be 300-level status`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
test("assertion fails for non 300 series with no negation", async () => {
|
||||
for (let i = 400; i < 500; i++) {
|
||||
await expect(
|
||||
func(`pw.expect(${i}).toBeLevel3xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "fail",
|
||||
message: `Expected '${i}' to be 300-level status`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
test("give error if the expect value is not a number without negation", () => {
|
||||
return expect(
|
||||
func(`pw.expect("foo").toBeLevel3xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "error",
|
||||
message:
|
||||
"Expected 300-level status but could not parse value 'foo'",
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
test("assertion fails for 400 series with negation", async () => {
|
||||
for (let i = 300; i < 400; i++) {
|
||||
await expect(
|
||||
func(`pw.expect(${i}).not.toBeLevel3xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "fail",
|
||||
message: `Expected '${i}' to not be 300-level status`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
test("assertion passes for non 200 series with negation", async () => {
|
||||
for (let i = 400; i < 500; i++) {
|
||||
await expect(
|
||||
func(`pw.expect(${i}).not.toBeLevel3xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "pass",
|
||||
message: `Expected '${i}' to not be 300-level status`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
test("give error if the expect value is not a number with negation", () => {
|
||||
return expect(
|
||||
func(`pw.expect("foo").not.toBeLevel3xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "error",
|
||||
message:
|
||||
"Expected 300-level status but could not parse value 'foo'",
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("toBeLevel4xx", () => {
|
||||
test("assertion passes for 400 series with no negation", async () => {
|
||||
for (let i = 400; i < 500; i++) {
|
||||
await expect(
|
||||
func(`pw.expect(${i}).toBeLevel4xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "pass",
|
||||
message: `Expected '${i}' to be 400-level status`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
test("assertion fails for non 400 series with no negation", async () => {
|
||||
for (let i = 500; i < 600; i++) {
|
||||
await expect(
|
||||
func(`pw.expect(${i}).toBeLevel4xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "fail",
|
||||
message: `Expected '${i}' to be 400-level status`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
test("give error if the expected value is not a number without negation", () => {
|
||||
return expect(
|
||||
func(`pw.expect("foo").toBeLevel4xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "error",
|
||||
message:
|
||||
"Expected 400-level status but could not parse value 'foo'",
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
test("assertion fails for 400 series with negation", async () => {
|
||||
for (let i = 400; i < 500; i++) {
|
||||
await expect(
|
||||
func(`pw.expect(${i}).not.toBeLevel4xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "fail",
|
||||
message: `Expected '${i}' to not be 400-level status`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
test("assertion passes for non 400 series with negation", async () => {
|
||||
for (let i = 500; i < 600; i++) {
|
||||
await expect(
|
||||
func(`pw.expect(${i}).not.toBeLevel4xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "pass",
|
||||
message: `Expected '${i}' to not be 400-level status`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
test("give error if the expected value is not a number with negation", () => {
|
||||
return expect(
|
||||
func(`pw.expect("foo").not.toBeLevel4xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "error",
|
||||
message:
|
||||
"Expected 400-level status but could not parse value 'foo'",
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("toBeLevel5xx", () => {
|
||||
test("assertion passes for 500 series with no negation", async () => {
|
||||
for (let i = 500; i < 600; i++) {
|
||||
await expect(
|
||||
func(`pw.expect(${i}).toBeLevel5xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "pass",
|
||||
message: `Expected '${i}' to be 500-level status`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
test("assertion fails for non 500 series with no negation", async () => {
|
||||
for (let i = 200; i < 500; i++) {
|
||||
await expect(
|
||||
func(`pw.expect(${i}).toBeLevel5xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "fail",
|
||||
message: `Expected '${i}' to be 500-level status`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
test("give error if the expect value is not a number with no negation", () => {
|
||||
return expect(
|
||||
func(`pw.expect("foo").toBeLevel5xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "error",
|
||||
message:
|
||||
"Expected 500-level status but could not parse value 'foo'",
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
test("assertion fails for 500 series with negation", async () => {
|
||||
for (let i = 500; i < 600; i++) {
|
||||
await expect(
|
||||
func(`pw.expect(${i}).not.toBeLevel5xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "fail",
|
||||
message: `Expected '${i}' to not be 500-level status`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
test("assertion passes for non 500 series with negation", async () => {
|
||||
for (let i = 200; i < 500; i++) {
|
||||
await expect(
|
||||
func(`pw.expect(${i}).not.toBeLevel5xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "pass",
|
||||
message: `Expected '${i}' to not be 500-level status`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
test("give error if the expect value is not a number with negation", () => {
|
||||
return expect(
|
||||
func(`pw.expect("foo").not.toBeLevel5xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "error",
|
||||
message:
|
||||
"Expected 500-level status but could not parse value 'foo'",
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
describe("toBeLevel4xx", () => {
|
||||
test("assertion passes for 400 series with no negation", async () => {
|
||||
for (let i = 400; i < 500; i++) {
|
||||
await expect(
|
||||
func(`pw.expect(${i}).toBeLevel4xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "pass",
|
||||
message: `Expected '${i}' to be 400-level status`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
test("assertion fails for non 400 series with no negation", async () => {
|
||||
for (let i = 500; i < 600; i++) {
|
||||
await expect(
|
||||
func(`pw.expect(${i}).toBeLevel4xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "fail",
|
||||
message: `Expected '${i}' to be 400-level status`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
test("give error if the expected value is not a number without negation", () => {
|
||||
return expect(
|
||||
func(`pw.expect("foo").toBeLevel4xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "error",
|
||||
message:
|
||||
"Expected 400-level status but could not parse value 'foo'",
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
test("assertion fails for 400 series with negation", async () => {
|
||||
for (let i = 400; i < 500; i++) {
|
||||
await expect(
|
||||
func(`pw.expect(${i}).not.toBeLevel4xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "fail",
|
||||
message: `Expected '${i}' to not be 400-level status`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
test("assertion passes for non 400 series with negation", async () => {
|
||||
for (let i = 500; i < 600; i++) {
|
||||
await expect(
|
||||
func(`pw.expect(${i}).not.toBeLevel4xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "pass",
|
||||
message: `Expected '${i}' to not be 400-level status`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
test("give error if the expected value is not a number with negation", () => {
|
||||
return expect(
|
||||
func(`pw.expect("foo").not.toBeLevel4xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "error",
|
||||
message:
|
||||
"Expected 400-level status but could not parse value 'foo'",
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("toBeLevel5xx", () => {
|
||||
test("assertion passes for 500 series with no negation", async () => {
|
||||
for (let i = 500; i < 600; i++) {
|
||||
await expect(
|
||||
func(`pw.expect(${i}).toBeLevel5xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "pass",
|
||||
message: `Expected '${i}' to be 500-level status`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
test("assertion fails for non 500 series with no negation", async () => {
|
||||
for (let i = 200; i < 500; i++) {
|
||||
await expect(
|
||||
func(`pw.expect(${i}).toBeLevel5xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "fail",
|
||||
message: `Expected '${i}' to be 500-level status`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
test("give error if the expect value is not a number with no negation", () => {
|
||||
return expect(
|
||||
func(`pw.expect("foo").toBeLevel5xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "error",
|
||||
message:
|
||||
"Expected 500-level status but could not parse value 'foo'",
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
test("assertion fails for 500 series with negation", async () => {
|
||||
for (let i = 500; i < 600; i++) {
|
||||
await expect(
|
||||
func(`pw.expect(${i}).not.toBeLevel5xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "fail",
|
||||
message: `Expected '${i}' to not be 500-level status`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
test("assertion passes for non 500 series with negation", async () => {
|
||||
for (let i = 200; i < 500; i++) {
|
||||
await expect(
|
||||
func(`pw.expect(${i}).not.toBeLevel5xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "pass",
|
||||
message: `Expected '${i}' to not be 500-level status`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
test("give error if the expect value is not a number with negation", () => {
|
||||
return expect(
|
||||
func(`pw.expect("foo").not.toBeLevel5xx()`, fakeResponse)()
|
||||
).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
expectResults: [
|
||||
{
|
||||
status: "error",
|
||||
message:
|
||||
"Expected 500-level status but could not parse value 'foo'",
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -28,6 +28,14 @@ describe("toBeType", () => {
|
|||
pw.expect(true).toBeType("boolean")
|
||||
pw.expect({}).toBeType("object")
|
||||
pw.expect(undefined).toBeType("undefined")
|
||||
|
||||
const utcDate = new Date("2025-05-22T17:42:26Z")
|
||||
const istOffset = 5.5 * 60 * 60 * 1000 // 5.5 hours in ms
|
||||
const istDate = new Date(utcDate.getTime() + istOffset)
|
||||
|
||||
pw.expect(istDate).toBeType("object")
|
||||
pw.expect(istDate.toISOString()).toBeType("string")
|
||||
pw.expect(JSON.stringify(istDate)).toBeType("string")
|
||||
`,
|
||||
fakeResponse
|
||||
)()
|
||||
|
|
@ -45,6 +53,18 @@ describe("toBeType", () => {
|
|||
status: "pass",
|
||||
message: `Expected 'undefined' to be type 'undefined'`,
|
||||
},
|
||||
{
|
||||
message: expect.stringMatching(/to be type 'object'$/),
|
||||
status: "pass",
|
||||
},
|
||||
{
|
||||
message: "Expected '2025-05-22T23:12:26.000Z' to be type 'string'",
|
||||
status: "pass",
|
||||
},
|
||||
{
|
||||
message: `Expected '"2025-05-22T23:12:26.000Z"' to be type 'string'`,
|
||||
status: "pass",
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
|
|
@ -124,6 +144,14 @@ describe("toBeType", () => {
|
|||
pw.expect(true).not.toBeType("string")
|
||||
pw.expect({}).not.toBeType("number")
|
||||
pw.expect(undefined).not.toBeType("number")
|
||||
|
||||
const utcDate = new Date("2025-05-22T17:42:26Z")
|
||||
const istOffset = 5.5 * 60 * 60 * 1000 // 5.5 hours in ms
|
||||
const istDate = new Date(utcDate.getTime() + istOffset)
|
||||
|
||||
pw.expect(istDate).not.toBeType("string")
|
||||
pw.expect(istDate.toISOString()).not.toBeType("object")
|
||||
pw.expect(JSON.stringify(istDate)).not.toBeType("object")
|
||||
`,
|
||||
fakeResponse
|
||||
)()
|
||||
|
|
@ -144,6 +172,19 @@ describe("toBeType", () => {
|
|||
status: "pass",
|
||||
message: `Expected 'undefined' to not be type 'number'`,
|
||||
},
|
||||
{
|
||||
message: expect.stringMatching(/to not be type 'string'$/),
|
||||
status: "pass",
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Expected '2025-05-22T23:12:26.000Z' to not be type 'object'",
|
||||
status: "pass",
|
||||
},
|
||||
{
|
||||
message: `Expected '"2025-05-22T23:12:26.000Z"' to not be type 'object'`,
|
||||
status: "pass",
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-expressions */
|
||||
;(inputs) => {
|
||||
globalThis.pw = {
|
||||
env: {
|
||||
get: (key) => inputs.envGet(key),
|
||||
getResolve: (key) => inputs.envGetResolve(key),
|
||||
set: (key, value) => inputs.envSet(key, value),
|
||||
unset: (key) => inputs.envUnset(key),
|
||||
resolve: (key) => inputs.envResolve(key),
|
||||
},
|
||||
expect: (expectVal) => {
|
||||
return {
|
||||
toBe: (expectedVal) => inputs.expectToBe(expectVal, expectedVal),
|
||||
toBeLevel2xx: (expectedVal) =>
|
||||
inputs.expectToBeLevel2xx(expectVal, expectedVal),
|
||||
toBeLevel3xx: (expectedVal) =>
|
||||
inputs.expectToBeLevel3xx(expectVal, expectedVal),
|
||||
toBeLevel4xx: (expectedVal) =>
|
||||
inputs.expectToBeLevel4xx(expectVal, expectedVal),
|
||||
toBeLevel5xx: (expectedVal) =>
|
||||
inputs.expectToBeLevel5xx(expectVal, expectedVal),
|
||||
toBeType: (expectedVal) => {
|
||||
const isExpectValDateInstance = expectVal instanceof Date
|
||||
return inputs.expectToBeType(
|
||||
expectVal,
|
||||
expectedVal,
|
||||
isExpectValDateInstance
|
||||
)
|
||||
},
|
||||
toHaveLength: (expectedVal) =>
|
||||
inputs.expectToHaveLength(expectVal, expectedVal),
|
||||
toInclude: (expectedVal) =>
|
||||
inputs.expectToInclude(expectVal, expectedVal),
|
||||
not: {
|
||||
toBe: (expectedVal) => inputs.expectNotToBe(expectVal, expectedVal),
|
||||
toBeLevel2xx: (expectedVal) =>
|
||||
inputs.expectNotToBeLevel2xx(expectVal, expectedVal),
|
||||
toBeLevel3xx: (expectedVal) =>
|
||||
inputs.expectNotToBeLevel3xx(expectVal, expectedVal),
|
||||
toBeLevel4xx: (expectedVal) =>
|
||||
inputs.expectNotToBeLevel4xx(expectVal, expectedVal),
|
||||
toBeLevel5xx: (expectedVal) =>
|
||||
inputs.expectNotToBeLevel5xx(expectVal, expectedVal),
|
||||
toBeType: (expectedVal) => {
|
||||
const isExpectValDateInstance = expectVal instanceof Date
|
||||
return inputs.expectNotToBeType(
|
||||
expectVal,
|
||||
expectedVal,
|
||||
isExpectValDateInstance
|
||||
)
|
||||
},
|
||||
toHaveLength: (expectedVal) =>
|
||||
inputs.expectNotToHaveLength(expectVal, expectedVal),
|
||||
toInclude: (expectedVal) =>
|
||||
inputs.expectNotToInclude(expectVal, expectedVal),
|
||||
},
|
||||
}
|
||||
},
|
||||
test: (descriptor, testFn) => {
|
||||
inputs.preTest(descriptor)
|
||||
testFn()
|
||||
inputs.postTest()
|
||||
},
|
||||
response: inputs.getResponse(),
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-expressions */
|
||||
;(inputs) => {
|
||||
globalThis.pw = {
|
||||
env: {
|
||||
get: (key) => inputs.envGet(key),
|
||||
getResolve: (key) => inputs.envGetResolve(key),
|
||||
set: (key, value) => inputs.envSet(key, value),
|
||||
unset: (key) => inputs.envUnset(key),
|
||||
resolve: (key) => inputs.envResolve(key),
|
||||
},
|
||||
}
|
||||
}
|
||||
268
packages/hoppscotch-js-sandbox/src/cage-modules/pw.ts
Normal file
268
packages/hoppscotch-js-sandbox/src/cage-modules/pw.ts
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
import {
|
||||
defineCageModule,
|
||||
defineSandboxFn,
|
||||
defineSandboxObject,
|
||||
} from "faraday-cage/modules"
|
||||
import { createExpectation, getSharedMethods } from "~/shared-utils"
|
||||
import { TestDescriptor, TestResponse, TestResult } from "~/types"
|
||||
|
||||
import postRequestBootstrapCode from "../bootstrap-code/post-request?raw"
|
||||
import preRequestBootstrapCode from "../bootstrap-code/pre-request?raw"
|
||||
|
||||
type PwPostRequestModuleConfig = {
|
||||
envs: TestResult["envs"]
|
||||
testRunStack: TestDescriptor[]
|
||||
response: TestResponse
|
||||
handleSandboxResults: ({
|
||||
envs,
|
||||
testRunStack,
|
||||
}: {
|
||||
envs: TestResult["envs"]
|
||||
testRunStack: TestDescriptor[]
|
||||
}) => void
|
||||
}
|
||||
|
||||
type PwPreRequestModuleConfig = {
|
||||
envs: TestResult["envs"]
|
||||
handleSandboxResults: ({ envs }: { envs: TestResult["envs"] }) => void
|
||||
}
|
||||
|
||||
type PwModuleType = "pre" | "post"
|
||||
type PwModuleConfig = PwPreRequestModuleConfig | PwPostRequestModuleConfig
|
||||
|
||||
const createPwInputsObj = (
|
||||
ctx: any,
|
||||
methods: any,
|
||||
type: PwModuleType,
|
||||
config: PwModuleConfig
|
||||
) => {
|
||||
const baseInputs = {
|
||||
envGet: defineSandboxFn(ctx, "get", (key) => methods.env.get(key)),
|
||||
envGetResolve: defineSandboxFn(ctx, "getResolve", (key) =>
|
||||
methods.env.getResolve(key)
|
||||
),
|
||||
envSet: defineSandboxFn(ctx, "set", (key, value) => {
|
||||
return methods.env.set(key, value)
|
||||
}),
|
||||
envUnset: defineSandboxFn(ctx, "unset", (key) => methods.env.unset(key)),
|
||||
envResolve: defineSandboxFn(ctx, "resolve", (key) =>
|
||||
methods.env.resolve(key)
|
||||
),
|
||||
}
|
||||
|
||||
if (type === "post") {
|
||||
const postConfig = config as PwPostRequestModuleConfig
|
||||
return {
|
||||
...baseInputs,
|
||||
expectToBe: defineSandboxFn(ctx, "toBe", (expectVal, expectedVal) =>
|
||||
createExpectation(expectVal, false, postConfig.testRunStack).toBe(
|
||||
expectedVal
|
||||
)
|
||||
),
|
||||
expectToBeLevel2xx: defineSandboxFn(ctx, "toBeLevel2xx", (expectVal) =>
|
||||
createExpectation(
|
||||
expectVal,
|
||||
false,
|
||||
postConfig.testRunStack
|
||||
).toBeLevel2xx()
|
||||
),
|
||||
expectToBeLevel3xx: defineSandboxFn(ctx, "toBeLevel3xx", (expectVal) =>
|
||||
createExpectation(
|
||||
expectVal,
|
||||
false,
|
||||
postConfig.testRunStack
|
||||
).toBeLevel3xx()
|
||||
),
|
||||
expectToBeLevel4xx: defineSandboxFn(ctx, "toBeLevel4xx", (expectVal) =>
|
||||
createExpectation(
|
||||
expectVal,
|
||||
false,
|
||||
postConfig.testRunStack
|
||||
).toBeLevel4xx()
|
||||
),
|
||||
expectToBeLevel5xx: defineSandboxFn(ctx, "toBeLevel5xx", (expectVal) =>
|
||||
createExpectation(
|
||||
expectVal,
|
||||
false,
|
||||
postConfig.testRunStack
|
||||
).toBeLevel5xx()
|
||||
),
|
||||
expectToBeType: defineSandboxFn(
|
||||
ctx,
|
||||
"toBeType",
|
||||
(expectVal, expectedType, isExpectValDateInstance) => {
|
||||
// Supplying `new Date()` in the script gets serialized in the sandbox context
|
||||
// Parse the string back to a date instance
|
||||
const resolvedExpectVal =
|
||||
isExpectValDateInstance && typeof expectVal === "string"
|
||||
? new Date(expectVal)
|
||||
: expectVal
|
||||
|
||||
return createExpectation(
|
||||
resolvedExpectVal,
|
||||
false,
|
||||
postConfig.testRunStack
|
||||
).toBeType(expectedType)
|
||||
}
|
||||
),
|
||||
expectToHaveLength: defineSandboxFn(
|
||||
ctx,
|
||||
"toHaveLength",
|
||||
(expectVal, expectedLength) =>
|
||||
createExpectation(
|
||||
expectVal,
|
||||
false,
|
||||
postConfig.testRunStack
|
||||
).toHaveLength(expectedLength)
|
||||
),
|
||||
expectToInclude: defineSandboxFn(ctx, "toInclude", (expectVal, needle) =>
|
||||
createExpectation(expectVal, false, postConfig.testRunStack).toInclude(
|
||||
needle
|
||||
)
|
||||
),
|
||||
expectNotToBe: defineSandboxFn(ctx, "notToBe", (expectVal, expectedVal) =>
|
||||
createExpectation(expectVal, false, postConfig.testRunStack).not.toBe(
|
||||
expectedVal
|
||||
)
|
||||
),
|
||||
expectNotToBeLevel2xx: defineSandboxFn(
|
||||
ctx,
|
||||
"notToBeLevel2xx",
|
||||
(expectVal) =>
|
||||
createExpectation(
|
||||
expectVal,
|
||||
false,
|
||||
postConfig.testRunStack
|
||||
).not.toBeLevel2xx()
|
||||
),
|
||||
expectNotToBeLevel3xx: defineSandboxFn(
|
||||
ctx,
|
||||
"notToBeLevel3xx",
|
||||
(expectVal) =>
|
||||
createExpectation(
|
||||
expectVal,
|
||||
false,
|
||||
postConfig.testRunStack
|
||||
).not.toBeLevel3xx()
|
||||
),
|
||||
expectNotToBeLevel4xx: defineSandboxFn(
|
||||
ctx,
|
||||
"notToBeLevel4xx",
|
||||
(expectVal) =>
|
||||
createExpectation(
|
||||
expectVal,
|
||||
false,
|
||||
postConfig.testRunStack
|
||||
).not.toBeLevel4xx()
|
||||
),
|
||||
expectNotToBeLevel5xx: defineSandboxFn(
|
||||
ctx,
|
||||
"notToBeLevel5xx",
|
||||
(expectVal) =>
|
||||
createExpectation(
|
||||
expectVal,
|
||||
false,
|
||||
postConfig.testRunStack
|
||||
).not.toBeLevel5xx()
|
||||
),
|
||||
expectNotToBeType: defineSandboxFn(
|
||||
ctx,
|
||||
"notToBeType",
|
||||
(expectVal, expectedType, isExpectValDateInstance) => {
|
||||
// Supplying `new Date()` in the script gets serialized in the sandbox context
|
||||
// Parse the string back to a date instance
|
||||
const resolvedExpectVal =
|
||||
isExpectValDateInstance && typeof expectVal === "string"
|
||||
? new Date(expectVal)
|
||||
: expectVal
|
||||
|
||||
return createExpectation(
|
||||
resolvedExpectVal,
|
||||
false,
|
||||
postConfig.testRunStack
|
||||
).not.toBeType(expectedType)
|
||||
}
|
||||
),
|
||||
expectNotToHaveLength: defineSandboxFn(
|
||||
ctx,
|
||||
"notToHaveLength",
|
||||
(expectVal, expectedLength) =>
|
||||
createExpectation(
|
||||
expectVal,
|
||||
false,
|
||||
postConfig.testRunStack
|
||||
).not.toHaveLength(expectedLength)
|
||||
),
|
||||
expectNotToInclude: defineSandboxFn(
|
||||
ctx,
|
||||
"notToInclude",
|
||||
(expectVal, needle) =>
|
||||
createExpectation(
|
||||
expectVal,
|
||||
false,
|
||||
postConfig.testRunStack
|
||||
).not.toInclude(needle)
|
||||
),
|
||||
preTest: defineSandboxFn(ctx, "preTest", (descriptor: any) => {
|
||||
postConfig.testRunStack.push({
|
||||
descriptor,
|
||||
expectResults: [],
|
||||
children: [],
|
||||
})
|
||||
}),
|
||||
postTest: defineSandboxFn(ctx, "postTest", () => {
|
||||
const child = postConfig.testRunStack.pop() as TestDescriptor
|
||||
postConfig.testRunStack[
|
||||
postConfig.testRunStack.length - 1
|
||||
].children.push(child)
|
||||
}),
|
||||
getResponse: defineSandboxFn(
|
||||
ctx,
|
||||
"getResponse",
|
||||
() => postConfig.response
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
return baseInputs
|
||||
}
|
||||
|
||||
const createPwModule = (
|
||||
type: PwModuleType,
|
||||
bootstrapCode: string,
|
||||
config: PwModuleConfig
|
||||
) => {
|
||||
return defineCageModule((ctx) => {
|
||||
const funcHandle = ctx.scope.manage(ctx.vm.evalCode(bootstrapCode)).unwrap()
|
||||
|
||||
const { methods, updatedEnvs } = getSharedMethods(config.envs)
|
||||
|
||||
const inputsObj = defineSandboxObject(
|
||||
ctx,
|
||||
createPwInputsObj(ctx, methods, type, config)
|
||||
)
|
||||
|
||||
ctx.vm.callFunction(funcHandle, ctx.vm.undefined, inputsObj)
|
||||
|
||||
ctx.afterScriptExecutionHooks.push(() => {
|
||||
if (type === "post") {
|
||||
const postConfig = config as PwPostRequestModuleConfig
|
||||
postConfig.handleSandboxResults({
|
||||
envs: updatedEnvs,
|
||||
testRunStack: postConfig.testRunStack,
|
||||
})
|
||||
} else {
|
||||
const preConfig = config as PwPreRequestModuleConfig
|
||||
preConfig.handleSandboxResults({
|
||||
envs: updatedEnvs,
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const pwPostRequestModule = (config: PwPostRequestModuleConfig) =>
|
||||
createPwModule("post", postRequestBootstrapCode, config)
|
||||
|
||||
export const pwPreRequestModule = (config: PwPreRequestModuleConfig) =>
|
||||
createPwModule("pre", preRequestBootstrapCode, config)
|
||||
|
|
@ -1,10 +1,20 @@
|
|||
import { FaradayCage } from "faraday-cage"
|
||||
import {
|
||||
blobPolyfill,
|
||||
console as ConsoleModule,
|
||||
crypto,
|
||||
esmModuleLoader,
|
||||
fetch,
|
||||
} from "faraday-cage/modules"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/lib/TaskEither"
|
||||
import { createRequire } from "module"
|
||||
|
||||
import type ivmT from "isolated-vm"
|
||||
import { cloneDeep } from "lodash"
|
||||
import { pwPreRequestModule } from "~/cage-modules/pw"
|
||||
|
||||
import { createRequire } from "module"
|
||||
import { TestResult } from "~/types"
|
||||
|
||||
import { getPreRequestScriptMethods } from "~/shared-utils"
|
||||
import { getSerializedAPIMethods } from "./utils"
|
||||
|
||||
|
|
@ -13,79 +23,143 @@ const ivm = nodeRequire("isolated-vm")
|
|||
|
||||
export const runPreRequestScript = (
|
||||
preRequestScript: string,
|
||||
envs: TestResult["envs"]
|
||||
): TE.TaskEither<string, TestResult["envs"]> =>
|
||||
pipe(
|
||||
TE.tryCatch(
|
||||
async () => {
|
||||
const isolate: ivmT.Isolate = new ivm.Isolate()
|
||||
const context = await isolate.createContext()
|
||||
return { isolate, context }
|
||||
},
|
||||
(reason) => `Context initialization failed: ${reason}`
|
||||
),
|
||||
TE.chain(({ isolate, context }) =>
|
||||
pipe(
|
||||
TE.tryCatch(
|
||||
async () => {
|
||||
const jail = context.global
|
||||
|
||||
const { pw, updatedEnvs } = getPreRequestScriptMethods(envs)
|
||||
|
||||
const serializedAPIMethods = getSerializedAPIMethods(pw)
|
||||
jail.setSync("serializedAPIMethods", serializedAPIMethods, {
|
||||
copy: true,
|
||||
})
|
||||
|
||||
jail.setSync("atob", atob)
|
||||
jail.setSync("btoa", btoa)
|
||||
|
||||
// Methods in the isolate context can't be invoked straightaway
|
||||
const finalScript = `
|
||||
const pw = new Proxy(serializedAPIMethods, {
|
||||
get: (pwObjTarget, pwObjProp) => {
|
||||
const topLevelEntry = pwObjTarget[pwObjProp]
|
||||
|
||||
// "pw.env" set of API methods
|
||||
if (topLevelEntry && typeof topLevelEntry === "object") {
|
||||
return new Proxy(topLevelEntry, {
|
||||
get: (subTarget, subProp) => {
|
||||
const subLevelProperty = subTarget[subProp]
|
||||
if (subLevelProperty && subLevelProperty.typeof === "function") {
|
||||
return (...args) => subLevelProperty.applySync(null, args)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
envs: TestResult["envs"],
|
||||
experimentalScriptingSandbox = true
|
||||
): TE.TaskEither<string, TestResult["envs"]> => {
|
||||
if (!experimentalScriptingSandbox) {
|
||||
return pipe(
|
||||
TE.tryCatch(
|
||||
async () => {
|
||||
const isolate: ivmT.Isolate = new ivm.Isolate()
|
||||
const context = await isolate.createContext()
|
||||
return { isolate, context }
|
||||
},
|
||||
(reason) => `Context initialization failed: ${reason}`
|
||||
),
|
||||
TE.chain(({ isolate, context }) =>
|
||||
pipe(
|
||||
TE.tryCatch(
|
||||
async () => {
|
||||
const jail = context.global
|
||||
const { pw, updatedEnvs } = getPreRequestScriptMethods(envs)
|
||||
const serializedAPIMethods = getSerializedAPIMethods(pw)
|
||||
jail.setSync("serializedAPIMethods", serializedAPIMethods, {
|
||||
copy: true,
|
||||
})
|
||||
|
||||
${preRequestScript}
|
||||
`
|
||||
|
||||
// Create a script and compile it
|
||||
const script = await isolate.compileScript(finalScript)
|
||||
|
||||
// Run the pre-request script in the provided context
|
||||
await script.run(context)
|
||||
|
||||
return updatedEnvs
|
||||
},
|
||||
(reason) => reason
|
||||
),
|
||||
TE.fold(
|
||||
(error) => TE.left(`Script execution failed: ${error}`),
|
||||
(result) =>
|
||||
pipe(
|
||||
TE.tryCatch(
|
||||
async () => {
|
||||
await isolate.dispose()
|
||||
return result
|
||||
},
|
||||
(disposeError) => `Isolate disposal failed: ${disposeError}`
|
||||
jail.setSync("atob", atob)
|
||||
jail.setSync("btoa", btoa)
|
||||
// Methods in the isolate context can't be invoked straightaway
|
||||
const finalScript = `
|
||||
const pw = new Proxy(serializedAPIMethods, {
|
||||
get: (pwObjTarget, pwObjProp) => {
|
||||
const topLevelEntry = pwObjTarget[pwObjProp]
|
||||
// "pw.env" set of API methods
|
||||
if (topLevelEntry && typeof topLevelEntry === "object") {
|
||||
return new Proxy(topLevelEntry, {
|
||||
get: (subTarget, subProp) => {
|
||||
const subLevelProperty = subTarget[subProp]
|
||||
if (subLevelProperty && subLevelProperty.typeof === "function") {
|
||||
return (...args) => subLevelProperty.applySync(null, args)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
${preRequestScript}
|
||||
`
|
||||
// Create a script and compile it
|
||||
const script = await isolate.compileScript(finalScript)
|
||||
// Run the pre-request script in the provided context
|
||||
await script.run(context)
|
||||
return updatedEnvs
|
||||
},
|
||||
(reason) => reason
|
||||
),
|
||||
TE.fold(
|
||||
(error) => TE.left(`Script execution failed: ${error}`),
|
||||
(result) =>
|
||||
pipe(
|
||||
TE.tryCatch(
|
||||
async () => {
|
||||
await isolate.dispose()
|
||||
return result
|
||||
},
|
||||
(disposeError) => `Isolate disposal failed: ${disposeError}`
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return pipe(
|
||||
TE.tryCatch(
|
||||
async (): Promise<TestResult["envs"]> => {
|
||||
let finalEnvs = envs
|
||||
|
||||
const cage = await FaradayCage.create()
|
||||
|
||||
const result = await cage.runCode(preRequestScript, [
|
||||
pwPreRequestModule({
|
||||
envs: cloneDeep(envs),
|
||||
handleSandboxResults: ({ envs }) => {
|
||||
finalEnvs = envs
|
||||
},
|
||||
}),
|
||||
blobPolyfill,
|
||||
ConsoleModule({
|
||||
onLog(...args) {
|
||||
console[args[0]](...args)
|
||||
},
|
||||
onCount(...args) {
|
||||
console.count(args[0])
|
||||
},
|
||||
onTime(...args) {
|
||||
console.timeEnd(args[0])
|
||||
},
|
||||
onTimeLog(...args) {
|
||||
console.timeLog(...args)
|
||||
},
|
||||
onGroup(...args) {
|
||||
console.group(...args)
|
||||
},
|
||||
onGroupEnd(...args) {
|
||||
console.groupEnd(...args)
|
||||
},
|
||||
onClear(...args) {
|
||||
console.clear(...args)
|
||||
},
|
||||
onAssert(...args) {
|
||||
console.assert(...args)
|
||||
},
|
||||
onDir(...args) {
|
||||
console.dir(...args)
|
||||
},
|
||||
onTable(...args) {
|
||||
console.table(...args)
|
||||
},
|
||||
}),
|
||||
crypto(),
|
||||
esmModuleLoader,
|
||||
fetch(),
|
||||
])
|
||||
|
||||
if (result.type === "error") {
|
||||
throw result.err
|
||||
}
|
||||
|
||||
return finalEnvs
|
||||
},
|
||||
(error) => {
|
||||
if (error !== null && typeof error === "object" && "message" in error) {
|
||||
const reason = `${"name" in error ? error.name : ""}: ${error.message}`
|
||||
return `Script execution failed: ${reason}`
|
||||
}
|
||||
|
||||
return `Script execution failed: ${String(error)}`
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,24 @@
|
|||
import { FaradayCage } from "faraday-cage"
|
||||
import {
|
||||
blobPolyfill,
|
||||
console as ConsoleModule,
|
||||
crypto,
|
||||
esmModuleLoader,
|
||||
fetch,
|
||||
} from "faraday-cage/modules"
|
||||
import * as E from "fp-ts/Either"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import { createRequire } from "module"
|
||||
|
||||
import type ivmT from "isolated-vm"
|
||||
import { cloneDeep } from "lodash"
|
||||
import { createRequire } from "module"
|
||||
import { pwPostRequestModule } from "~/cage-modules/pw"
|
||||
|
||||
import { TestResponse, TestResult } from "~/types"
|
||||
import {
|
||||
getTestRunnerScriptMethods,
|
||||
preventCyclicObjects,
|
||||
} from "~/shared-utils"
|
||||
import { TestDescriptor, TestResponse, TestResult } from "~/types"
|
||||
import { getSerializedAPIMethods } from "./utils"
|
||||
|
||||
const nodeRequire = createRequire(import.meta.url)
|
||||
|
|
@ -18,42 +27,133 @@ const ivm = nodeRequire("isolated-vm")
|
|||
export const runTestScript = (
|
||||
testScript: string,
|
||||
envs: TestResult["envs"],
|
||||
response: TestResponse
|
||||
): TE.TaskEither<string, TestResult> =>
|
||||
pipe(
|
||||
TE.tryCatch(
|
||||
async () => {
|
||||
const isolate: ivmT.Isolate = new ivm.Isolate()
|
||||
const context = await isolate.createContext()
|
||||
return { isolate, context }
|
||||
},
|
||||
(reason) => `Context initialization failed: ${reason}`
|
||||
),
|
||||
TE.chain(({ isolate, context }) =>
|
||||
pipe(
|
||||
TE.tryCatch(
|
||||
async () =>
|
||||
executeScriptInContext(
|
||||
testScript,
|
||||
envs,
|
||||
response,
|
||||
isolate,
|
||||
context
|
||||
),
|
||||
(reason) => `Script execution failed: ${reason}`
|
||||
),
|
||||
TE.chain((result) =>
|
||||
response: TestResponse,
|
||||
experimentalScriptingSandbox = true
|
||||
): TE.TaskEither<string, TestResult> => {
|
||||
const responseObjHandle = preventCyclicObjects(response)
|
||||
|
||||
if (E.isLeft(responseObjHandle)) {
|
||||
return TE.left(`Response marshalling failed: ${responseObjHandle.left}`)
|
||||
}
|
||||
|
||||
if (!experimentalScriptingSandbox) {
|
||||
return pipe(
|
||||
TE.tryCatch(
|
||||
async () => {
|
||||
const isolate: ivmT.Isolate = new ivm.Isolate()
|
||||
const context = await isolate.createContext()
|
||||
return { isolate, context }
|
||||
},
|
||||
(reason) => `Context initialization failed: ${reason}`
|
||||
),
|
||||
TE.chain(({ isolate, context }) =>
|
||||
pipe(
|
||||
TE.tryCatch(
|
||||
async () => {
|
||||
await isolate.dispose()
|
||||
return result
|
||||
},
|
||||
(disposeReason) => `Isolate disposal failed: ${disposeReason}`
|
||||
async () =>
|
||||
executeScriptInContext(
|
||||
testScript,
|
||||
envs,
|
||||
response,
|
||||
isolate,
|
||||
context
|
||||
),
|
||||
(reason) => `Script execution failed: ${reason}`
|
||||
),
|
||||
TE.chain((result) =>
|
||||
TE.tryCatch(
|
||||
async () => {
|
||||
await isolate.dispose()
|
||||
return result
|
||||
},
|
||||
(disposeReason) => `Isolate disposal failed: ${disposeReason}`
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return pipe(
|
||||
TE.tryCatch(
|
||||
async (): Promise<TestResult> => {
|
||||
const testRunStack: TestDescriptor[] = [
|
||||
{ descriptor: "root", expectResults: [], children: [] },
|
||||
]
|
||||
|
||||
let finalEnvs = envs
|
||||
let finalTestResults = testRunStack
|
||||
|
||||
const cage = await FaradayCage.create()
|
||||
|
||||
const result = await cage.runCode(testScript, [
|
||||
pwPostRequestModule({
|
||||
envs: cloneDeep(envs),
|
||||
testRunStack: cloneDeep(testRunStack),
|
||||
response,
|
||||
handleSandboxResults: ({ envs, testRunStack }) => {
|
||||
finalEnvs = envs
|
||||
finalTestResults = testRunStack
|
||||
},
|
||||
}),
|
||||
blobPolyfill,
|
||||
ConsoleModule({
|
||||
onLog(...args) {
|
||||
console[args[0]](...args)
|
||||
},
|
||||
onCount(...args) {
|
||||
console.count(args[0])
|
||||
},
|
||||
onTime(...args) {
|
||||
console.timeEnd(args[0])
|
||||
},
|
||||
onTimeLog(...args) {
|
||||
console.timeLog(...args)
|
||||
},
|
||||
onGroup(...args) {
|
||||
console.group(...args)
|
||||
},
|
||||
onGroupEnd(...args) {
|
||||
console.groupEnd(...args)
|
||||
},
|
||||
onClear(...args) {
|
||||
console.clear(...args)
|
||||
},
|
||||
onAssert(...args) {
|
||||
console.assert(...args)
|
||||
},
|
||||
onDir(...args) {
|
||||
console.dir(...args)
|
||||
},
|
||||
onTable(...args) {
|
||||
console.table(...args)
|
||||
},
|
||||
}),
|
||||
crypto(),
|
||||
esmModuleLoader,
|
||||
fetch(),
|
||||
])
|
||||
|
||||
if (result.type === "error") {
|
||||
throw result.err
|
||||
}
|
||||
|
||||
return {
|
||||
tests: finalTestResults,
|
||||
envs: finalEnvs,
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
if (error !== null && typeof error === "object" && "message" in error) {
|
||||
const reason = `${"name" in error ? error.name : ""}: ${error.message}`
|
||||
return `Script execution failed: ${reason}`
|
||||
}
|
||||
|
||||
return `Script execution failed: ${String(error)}`
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const executeScriptInContext = (
|
||||
testScript: string,
|
||||
envs: TestResult["envs"],
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { pipe } from "fp-ts/lib/function"
|
|||
import { cloneDeep } from "lodash-es"
|
||||
|
||||
import {
|
||||
Expectation,
|
||||
GlobalEnvItem,
|
||||
SelectedEnvItem,
|
||||
TestDescriptor,
|
||||
|
|
@ -83,7 +84,7 @@ const unsetEnv = (
|
|||
}
|
||||
|
||||
// Compiles shared scripting API methods for use in both pre and post request scripts
|
||||
const getSharedMethods = (envs: TestResult["envs"]) => {
|
||||
export const getSharedMethods = (envs: TestResult["envs"]) => {
|
||||
let updatedEnvs = envs
|
||||
|
||||
const envGetFn = (key: any) => {
|
||||
|
|
@ -242,9 +243,7 @@ export const createExpectation = (
|
|||
expectVal: any,
|
||||
negated: boolean,
|
||||
currTestStack: TestDescriptor[]
|
||||
) => {
|
||||
const result: Record<string, unknown> = {}
|
||||
|
||||
): Expectation => {
|
||||
// Non-primitive values supplied are stringified in the isolate context
|
||||
const resolvedExpectVal = getResolvedExpectValue(expectVal)
|
||||
|
||||
|
|
@ -445,14 +444,16 @@ export const createExpectation = (
|
|||
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
|
||||
const result = {
|
||||
toBe: toBeFn,
|
||||
toBeLevel2xx: toBeLevel2xxFn,
|
||||
toBeLevel3xx: toBeLevel3xxFn,
|
||||
toBeLevel4xx: toBeLevel4xxFn,
|
||||
toBeLevel5xx: toBeLevel5xxFn,
|
||||
toBeType: toBeTypeFn,
|
||||
toHaveLength: toHaveLengthFn,
|
||||
toInclude: toIncludeFn,
|
||||
} as Expectation
|
||||
|
||||
Object.defineProperties(result, {
|
||||
not: {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { ConsoleEntry } from "faraday-cage/modules"
|
||||
|
||||
/**
|
||||
* The response object structure exposed to the test script
|
||||
*/
|
||||
|
|
@ -64,4 +66,23 @@ export type TestResult = {
|
|||
export type GlobalEnvItem = TestResult["envs"]["global"][number]
|
||||
export type SelectedEnvItem = TestResult["envs"]["selected"][number]
|
||||
|
||||
export type SandboxTestResult = TestResult & { tests: TestDescriptor }
|
||||
export type SandboxTestResult = TestResult & { tests: TestDescriptor } & {
|
||||
consoleEntries?: ConsoleEntry[]
|
||||
}
|
||||
|
||||
export type SandboxPreRequestResult = {
|
||||
envs: TestResult["envs"]
|
||||
consoleEntries?: ConsoleEntry[]
|
||||
}
|
||||
|
||||
export interface Expectation {
|
||||
toBe(expectedVal: any): void
|
||||
toBeLevel2xx(): void
|
||||
toBeLevel3xx(): void
|
||||
toBeLevel4xx(): void
|
||||
toBeLevel5xx(): void
|
||||
toBeType(expectedType: any): void
|
||||
toHaveLength(expectedLength: any): void
|
||||
toInclude(needle: any): void
|
||||
readonly not: Expectation
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,113 @@
|
|||
import * as E from "fp-ts/Either"
|
||||
|
||||
import { TestResult } from "~/types"
|
||||
import { SandboxPreRequestResult, TestResult } from "~/types"
|
||||
|
||||
import { FaradayCage } from "faraday-cage"
|
||||
import {
|
||||
blobPolyfill,
|
||||
ConsoleEntry,
|
||||
console as ConsoleModule,
|
||||
crypto,
|
||||
esmModuleLoader,
|
||||
fetch,
|
||||
} from "faraday-cage/modules"
|
||||
import { cloneDeep } from "lodash"
|
||||
|
||||
import * as TE from "fp-ts/lib/TaskEither"
|
||||
import { pwPreRequestModule } from "~/cage-modules/pw"
|
||||
|
||||
import Worker from "./worker?worker&inline"
|
||||
|
||||
export const runPreRequestScript = (
|
||||
export const runPreRequestScript = async (
|
||||
preRequestScript: string,
|
||||
envs: TestResult["envs"]
|
||||
): Promise<E.Either<string, TestResult["envs"]>> =>
|
||||
new Promise((resolve) => {
|
||||
const worker = new Worker()
|
||||
envs: TestResult["envs"],
|
||||
experimentalScriptingSandbox = true
|
||||
): Promise<E.Either<string, SandboxPreRequestResult>> => {
|
||||
const consoleEntries: ConsoleEntry[] = []
|
||||
let finalEnvs = envs
|
||||
|
||||
// Listen for the results from the web worker
|
||||
worker.addEventListener("message", (event: MessageEvent) =>
|
||||
resolve(event.data.results)
|
||||
)
|
||||
if (!experimentalScriptingSandbox) {
|
||||
return new Promise((resolve) => {
|
||||
const worker = new Worker()
|
||||
|
||||
// Send the script to the web worker
|
||||
worker.postMessage({
|
||||
preRequestScript,
|
||||
envs,
|
||||
// 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,
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const cage = await FaradayCage.create()
|
||||
|
||||
const result = await cage.runCode(preRequestScript, [
|
||||
pwPreRequestModule({
|
||||
envs: cloneDeep(envs),
|
||||
handleSandboxResults: ({ envs }) => {
|
||||
finalEnvs = envs
|
||||
},
|
||||
}),
|
||||
blobPolyfill,
|
||||
ConsoleModule({
|
||||
onLog(...args) {
|
||||
console[args[0]](...args)
|
||||
},
|
||||
onCount(...args) {
|
||||
console.count(args[0])
|
||||
},
|
||||
onTime(...args) {
|
||||
console.timeEnd(args[0])
|
||||
},
|
||||
onTimeLog(...args) {
|
||||
console.timeLog(...args)
|
||||
},
|
||||
onGroup(...args) {
|
||||
console.group(...args)
|
||||
},
|
||||
onGroupEnd(...args) {
|
||||
console.groupEnd(...args)
|
||||
},
|
||||
onClear(...args) {
|
||||
console.clear(...args)
|
||||
},
|
||||
onAssert(...args) {
|
||||
console.assert(...args)
|
||||
},
|
||||
onDir(...args) {
|
||||
console.dir(...args)
|
||||
},
|
||||
onTable(...args) {
|
||||
console.table(...args)
|
||||
},
|
||||
onFinish(entries) {
|
||||
consoleEntries.push(...entries)
|
||||
},
|
||||
}),
|
||||
crypto(),
|
||||
esmModuleLoader,
|
||||
fetch(),
|
||||
esmModuleLoader,
|
||||
])
|
||||
|
||||
if (result.type === "error") {
|
||||
if (
|
||||
result.err !== null &&
|
||||
typeof result.err === "object" &&
|
||||
"message" in result.err
|
||||
) {
|
||||
return TE.left(`Script execution failed: ${result.err.message}`)()
|
||||
}
|
||||
|
||||
return TE.left(`Script execution failed: ${String(result.err)}`)()
|
||||
}
|
||||
|
||||
return TE.right({
|
||||
envs: finalEnvs,
|
||||
consoleEntries,
|
||||
})()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import * as TE from "fp-ts/TaskEither"
|
||||
|
||||
import { TestResult } from "~/types"
|
||||
import { getPreRequestScriptMethods } from "~/shared-utils"
|
||||
import { SandboxPreRequestResult, TestResult } from "~/types"
|
||||
|
||||
const executeScriptInContext = (
|
||||
preRequestScript: string,
|
||||
envs: TestResult["envs"]
|
||||
): TE.TaskEither<string, TestResult["envs"]> => {
|
||||
): TE.TaskEither<string, SandboxPreRequestResult> => {
|
||||
try {
|
||||
const { pw, updatedEnvs } = getPreRequestScriptMethods(envs)
|
||||
|
||||
|
|
@ -16,7 +16,9 @@ const executeScriptInContext = (
|
|||
// Execute the script
|
||||
executeScript(pw)
|
||||
|
||||
return TE.right(updatedEnvs)
|
||||
return TE.right({
|
||||
envs: updatedEnvs,
|
||||
})
|
||||
} catch (error) {
|
||||
return TE.left(`Script execution failed: ${(error as Error).message}`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,139 @@
|
|||
import * as E from "fp-ts/Either"
|
||||
|
||||
import { SandboxTestResult, TestResponse, TestResult } from "~/types"
|
||||
import {
|
||||
SandboxTestResult,
|
||||
TestDescriptor,
|
||||
TestResponse,
|
||||
TestResult,
|
||||
} from "~/types"
|
||||
|
||||
import {
|
||||
blobPolyfill,
|
||||
ConsoleEntry,
|
||||
console as ConsoleModule,
|
||||
crypto,
|
||||
esmModuleLoader,
|
||||
fetch,
|
||||
} from "faraday-cage/modules"
|
||||
|
||||
import { FaradayCage } from "faraday-cage"
|
||||
|
||||
import * as TE from "fp-ts/lib/TaskEither"
|
||||
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { pwPostRequestModule } from "~/cage-modules/pw"
|
||||
import { preventCyclicObjects } from "~/shared-utils"
|
||||
|
||||
import Worker from "./worker?worker&inline"
|
||||
|
||||
export const runTestScript = (
|
||||
export const runTestScript = async (
|
||||
testScript: string,
|
||||
envs: TestResult["envs"],
|
||||
response: TestResponse
|
||||
response: TestResponse,
|
||||
experimentalScriptingSandbox = true
|
||||
): Promise<E.Either<string, SandboxTestResult>> => {
|
||||
return new Promise((resolve) => {
|
||||
const worker = new Worker()
|
||||
const testRunStack: TestDescriptor[] = [
|
||||
{ descriptor: "root", expectResults: [], children: [] },
|
||||
]
|
||||
|
||||
// Listen for the results from the web worker
|
||||
worker.addEventListener("message", (event: MessageEvent) =>
|
||||
resolve(event.data.results)
|
||||
)
|
||||
let finalEnvs = envs
|
||||
let finalTestResults = testRunStack
|
||||
const consoleEntries: ConsoleEntry[] = []
|
||||
|
||||
// Send the script to the web worker
|
||||
worker.postMessage({
|
||||
testScript,
|
||||
envs,
|
||||
response,
|
||||
const responseObjHandle = preventCyclicObjects(response)
|
||||
|
||||
if (E.isLeft(responseObjHandle)) {
|
||||
return TE.left(`Response marshalling failed: ${responseObjHandle.left}`)()
|
||||
}
|
||||
|
||||
if (!experimentalScriptingSandbox) {
|
||||
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,
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const cage = await FaradayCage.create()
|
||||
|
||||
const result = await cage.runCode(testScript, [
|
||||
pwPostRequestModule({
|
||||
envs: cloneDeep(envs),
|
||||
testRunStack: cloneDeep(testRunStack),
|
||||
response: responseObjHandle.right as TestResponse,
|
||||
handleSandboxResults: ({ envs, testRunStack }) => {
|
||||
finalEnvs = envs
|
||||
finalTestResults = testRunStack
|
||||
},
|
||||
}),
|
||||
blobPolyfill,
|
||||
ConsoleModule({
|
||||
onLog(...args) {
|
||||
console[args[0]](...args.slice(1))
|
||||
},
|
||||
onCount(...args) {
|
||||
console.count(args[0])
|
||||
},
|
||||
onTime(...args) {
|
||||
console.timeEnd(args[0])
|
||||
},
|
||||
onTimeLog(...args) {
|
||||
console.timeLog(...args)
|
||||
},
|
||||
onGroup(...args) {
|
||||
console.group(...args)
|
||||
},
|
||||
onGroupEnd(...args) {
|
||||
console.groupEnd(...args)
|
||||
},
|
||||
onClear(...args) {
|
||||
console.clear(...args)
|
||||
},
|
||||
onAssert(...args) {
|
||||
console.assert(...args)
|
||||
},
|
||||
onDir(...args) {
|
||||
console.dir(...args)
|
||||
},
|
||||
onTable(...args) {
|
||||
console.table(...args)
|
||||
},
|
||||
onFinish(entries) {
|
||||
consoleEntries.push(...entries)
|
||||
},
|
||||
}),
|
||||
crypto(),
|
||||
esmModuleLoader,
|
||||
fetch(),
|
||||
])
|
||||
|
||||
if (result.type === "error") {
|
||||
if (result.type === "error") {
|
||||
if (
|
||||
result.err !== null &&
|
||||
typeof result.err === "object" &&
|
||||
"message" in result.err
|
||||
) {
|
||||
return TE.left(`Script execution failed: ${result.err.message}`)()
|
||||
}
|
||||
|
||||
return TE.left(`Script execution failed: ${String(result.err)}`)()
|
||||
}
|
||||
}
|
||||
|
||||
return TE.right(<SandboxTestResult>{
|
||||
tests: finalTestResults[0],
|
||||
envs: finalEnvs,
|
||||
consoleEntries,
|
||||
})()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import * as E from "fp-ts/Either"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
|
||||
import { SandboxTestResult, TestResponse, TestResult } from "~/types"
|
||||
import {
|
||||
getTestRunnerScriptMethods,
|
||||
preventCyclicObjects,
|
||||
} from "~/shared-utils"
|
||||
import { SandboxTestResult, TestResponse, TestResult } from "~/types"
|
||||
|
||||
const executeScriptInContext = (
|
||||
testScript: string,
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@ export default defineConfig({
|
|||
maxParallelFileOps: 2,
|
||||
},
|
||||
},
|
||||
worker: {
|
||||
format: "es",
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"tailwind.config.cjs": path.resolve(
|
||||
|
|
|
|||
|
|
@ -721,6 +721,9 @@ importers:
|
|||
vue-i18n:
|
||||
specifier: 10.0.4
|
||||
version: 10.0.4(vue@3.5.12(typescript@5.3.3))
|
||||
vue-json-pretty:
|
||||
specifier: 2.4.0
|
||||
version: 2.4.0(vue@3.5.12(typescript@5.3.3))
|
||||
vue-pdf-embed:
|
||||
specifier: 2.1.0
|
||||
version: 2.1.0(vue@3.5.12(typescript@5.3.3))
|
||||
|
|
@ -1129,6 +1132,9 @@ importers:
|
|||
'@types/lodash-es':
|
||||
specifier: 4.17.12
|
||||
version: 4.17.12
|
||||
faraday-cage:
|
||||
specifier: 0.0.15
|
||||
version: 0.0.15
|
||||
fp-ts:
|
||||
specifier: 2.16.9
|
||||
version: 2.16.9
|
||||
|
|
@ -4919,6 +4925,24 @@ packages:
|
|||
resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
||||
'@jitl/quickjs-ffi-types@0.31.0':
|
||||
resolution: {integrity: sha512-1yrgvXlmXH2oNj3eFTrkwacGJbmM0crwipA3ohCrjv52gBeDaD7PsTvFYinlAnqU8iPME3LGP437yk05a2oejw==}
|
||||
|
||||
'@jitl/quickjs-singlefile-mjs-release-asyncify@0.31.0':
|
||||
resolution: {integrity: sha512-usrttM3cCk6lrykUp1jdRI3I3j5zDZlJsq7QjHeJbcujqhqJriPH1GKjEWZuQr+LU53gUw6ZkX4z00TC04yXSw==}
|
||||
|
||||
'@jitl/quickjs-wasmfile-debug-asyncify@0.31.0':
|
||||
resolution: {integrity: sha512-YkdzQdr1uaftFhgEnTRjTTZHk2SFZdpWO7XhOmRVbi6CEVsH9g5oNF8Ta1q3OuSJHRwwT8YsuR1YzEiEIJEk6w==}
|
||||
|
||||
'@jitl/quickjs-wasmfile-debug-sync@0.31.0':
|
||||
resolution: {integrity: sha512-8XvloaaWBONqcHXYs5tWOjdhQVxzULilIfB2hvZfS6S+fI4m2+lFiwQy7xeP8ExHmiZ7D8gZGChNkdLgjGfknw==}
|
||||
|
||||
'@jitl/quickjs-wasmfile-release-asyncify@0.31.0':
|
||||
resolution: {integrity: sha512-uz0BbQYTxNsFkvkurd7vk2dOg57ElTBLCuvNtRl4rgrtbC++NIndD5qv2+AXb6yXDD3Uy1O2PCwmoaH0eXgEOg==}
|
||||
|
||||
'@jitl/quickjs-wasmfile-release-sync@0.31.0':
|
||||
resolution: {integrity: sha512-hYduecOByj9AsAfsJhZh5nA6exokmuFC8cls39+lYmTCGY51bgjJJJwReEu7Ff7vBWaQCL6TeDdVlnp2WYz0jw==}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.5':
|
||||
resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
|
@ -8835,6 +8859,9 @@ packages:
|
|||
resolution: {integrity: sha512-FuoE1qtbJ4bBVvv94CC7s0oTnKUGvQs+Rjf1L2SJFfS+HTVVjhPFtehPdQ0JiGPqVNfSSZvL5yzHHQq2Z4WNhQ==}
|
||||
engines: {node: ^12.20 || >= 14.13}
|
||||
|
||||
faraday-cage@0.0.15:
|
||||
resolution: {integrity: sha512-ZERKHFsea2fZ0gfDVFOkXmtryhp+7CyUBFaa9nvRq7Gbf6XVnZ0f0dKsZjwoBzYqSD3Lhl2DFfK8bW4PEo1y0g==}
|
||||
|
||||
fast-decode-uri-component@1.0.1:
|
||||
resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==}
|
||||
|
||||
|
|
@ -11959,6 +11986,13 @@ packages:
|
|||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
quickjs-emscripten-core@0.31.0:
|
||||
resolution: {integrity: sha512-oQz8p0SiKDBc1TC7ZBK2fr0GoSHZKA0jZIeXxsnCyCs4y32FStzCW4d1h6E1sE0uHDMbGITbk2zhNaytaoJwXQ==}
|
||||
|
||||
quickjs-emscripten@0.31.0:
|
||||
resolution: {integrity: sha512-K7Yt78aRPLjPcqv3fIuLW1jW3pvwO21B9pmFOolsjM/57ZhdVXBr51GqJpalgBlkPu9foAvhEAuuQPnvIGvLvQ==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
|
||||
quicktype-core@23.0.170:
|
||||
resolution: {integrity: sha512-ZsjveG0yJUIijUx4yQshzyQ5EAXKbFSBTQJHnJ+KoSZVxcS+m3GcmDpzrdUIRYMhgLaF11ZGvLSYi5U0xcwemw==}
|
||||
|
||||
|
|
@ -13980,6 +14014,12 @@ packages:
|
|||
peerDependencies:
|
||||
vue: 3.5.12
|
||||
|
||||
vue-json-pretty@2.4.0:
|
||||
resolution: {integrity: sha512-e9bP41DYYIc2tWaB6KuwqFJq5odZ8/GkE6vHQuGcbPn37kGk4a3n1RNw3ZYeDrl66NWXgTlOfS+M6NKkowmkWw==}
|
||||
engines: {node: '>= 10.0.0', npm: '>= 5.0.0'}
|
||||
peerDependencies:
|
||||
vue: 3.5.12
|
||||
|
||||
vue-pdf-embed@2.1.0:
|
||||
resolution: {integrity: sha512-yEtitm2q1jY2pmgnbwDILhm8cEby01WhfZJCMKvhKwdorwc92FDefIDZ5yf0ixu4t+WHMfc1+Zz4OGsoW/PRag==}
|
||||
peerDependencies:
|
||||
|
|
@ -19275,6 +19315,28 @@ snapshots:
|
|||
'@types/yargs': 17.0.33
|
||||
chalk: 4.1.2
|
||||
|
||||
'@jitl/quickjs-ffi-types@0.31.0': {}
|
||||
|
||||
'@jitl/quickjs-singlefile-mjs-release-asyncify@0.31.0':
|
||||
dependencies:
|
||||
'@jitl/quickjs-ffi-types': 0.31.0
|
||||
|
||||
'@jitl/quickjs-wasmfile-debug-asyncify@0.31.0':
|
||||
dependencies:
|
||||
'@jitl/quickjs-ffi-types': 0.31.0
|
||||
|
||||
'@jitl/quickjs-wasmfile-debug-sync@0.31.0':
|
||||
dependencies:
|
||||
'@jitl/quickjs-ffi-types': 0.31.0
|
||||
|
||||
'@jitl/quickjs-wasmfile-release-asyncify@0.31.0':
|
||||
dependencies:
|
||||
'@jitl/quickjs-ffi-types': 0.31.0
|
||||
|
||||
'@jitl/quickjs-wasmfile-release-sync@0.31.0':
|
||||
dependencies:
|
||||
'@jitl/quickjs-ffi-types': 0.31.0
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.5':
|
||||
dependencies:
|
||||
'@jridgewell/set-array': 1.2.1
|
||||
|
|
@ -24377,6 +24439,12 @@ snapshots:
|
|||
|
||||
extract-files@11.0.0: {}
|
||||
|
||||
faraday-cage@0.0.15:
|
||||
dependencies:
|
||||
'@jitl/quickjs-ffi-types': 0.31.0
|
||||
'@jitl/quickjs-singlefile-mjs-release-asyncify': 0.31.0
|
||||
quickjs-emscripten: 0.31.0
|
||||
|
||||
fast-decode-uri-component@1.0.1: {}
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
|
|
@ -28421,6 +28489,18 @@ snapshots:
|
|||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
quickjs-emscripten-core@0.31.0:
|
||||
dependencies:
|
||||
'@jitl/quickjs-ffi-types': 0.31.0
|
||||
|
||||
quickjs-emscripten@0.31.0:
|
||||
dependencies:
|
||||
'@jitl/quickjs-wasmfile-debug-asyncify': 0.31.0
|
||||
'@jitl/quickjs-wasmfile-debug-sync': 0.31.0
|
||||
'@jitl/quickjs-wasmfile-release-asyncify': 0.31.0
|
||||
'@jitl/quickjs-wasmfile-release-sync': 0.31.0
|
||||
quickjs-emscripten-core: 0.31.0
|
||||
|
||||
quicktype-core@23.0.170:
|
||||
dependencies:
|
||||
'@glideapps/ts-necessities': 2.2.3
|
||||
|
|
@ -31283,6 +31363,10 @@ snapshots:
|
|||
'@vue/devtools-api': 6.6.4
|
||||
vue: 3.5.12(typescript@5.6.3)
|
||||
|
||||
vue-json-pretty@2.4.0(vue@3.5.12(typescript@5.3.3)):
|
||||
dependencies:
|
||||
vue: 3.5.12(typescript@5.3.3)
|
||||
|
||||
vue-pdf-embed@2.1.0(vue@3.5.12(typescript@5.3.3)):
|
||||
dependencies:
|
||||
pdfjs-dist: 4.7.76
|
||||
|
|
|
|||
Loading…
Reference in a new issue