feat(scripting-revamp): chai powered assertions and postman compatibility layer (#5417)

Co-authored-by: nivedin <nivedinp@gmail.com>
This commit is contained in:
James George 2025-10-27 17:49:58 +05:30 committed by GitHub
parent ecf7d2507a
commit 9cd6c7d6cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
90 changed files with 30793 additions and 1416 deletions

File diff suppressed because one or more lines are too long

View file

@ -68,23 +68,36 @@ export const preRequestScriptRunner = (
const { selected, global } = updatedEnvs;
return {
updatedEnvs: <Environment>{
// Keep the original updatedEnvs with separate global and selected arrays
preRequestUpdatedEnvs: updatedEnvs,
// Create Environment format for getEffectiveRESTRequest
envForEffectiveRequest: <Environment>{
name: "Env",
variables: [...(selected ?? []), ...(global ?? [])],
},
updatedRequest: updatedRequest ?? {},
};
}),
TE.chainW(({ updatedEnvs, updatedRequest }) => {
TE.chainW(({ preRequestUpdatedEnvs, envForEffectiveRequest, updatedRequest }) => {
const finalRequest = { ...request, ...updatedRequest };
return TE.tryCatch(
() =>
getEffectiveRESTRequest(
async () => {
const result = await getEffectiveRESTRequest(
finalRequest,
updatedEnvs,
envForEffectiveRequest,
collectionVariables
),
);
// Replace the updatedEnvs from getEffectiveRESTRequest with the one from pre-request script
// This preserves the global/selected separation
if (E.isRight(result)) {
return E.right({
...result.right,
updatedEnvs: preRequestUpdatedEnvs,
});
}
return result;
},
(reason) => error({ code: "PRE_REQUEST_SCRIPT_ERROR", data: reason })
);
}),

View file

@ -686,6 +686,8 @@
"from_postman": "Import from Postman",
"from_postman_description": "Import from Postman collection",
"from_postman_import_summary": "Collections, Requests and response examples will be imported.",
"import_scripts": "Import scripts",
"import_scripts_description": "Supports Postman Collection v2.0/v2.1.",
"from_url": "Import from URL",
"gist_url": "Enter Gist URL",
"from_har": "Import from HAR",
@ -712,6 +714,9 @@
"import_summary_pre_request_scripts_title": "Pre-request scripts",
"import_summary_post_request_scripts_title": "Post request scripts",
"import_summary_not_supported_by_hoppscotch_import": "We do not support importing {featureLabel} from this source right now.",
"import_summary_script_found": "script found but not imported",
"import_summary_scripts_found": "scripts found but not imported",
"import_summary_enable_experimental_sandbox": "To import Postman scripts, enable 'Experimental scripting sandbox' in settings. Note: This feature is experimental.",
"cors_error_modal": {
"title": "CORS Error Detected",
"description": "The import failed due to CORS (Cross-Origin Resource Sharing) restrictions imposed by the server.",
@ -1314,6 +1319,7 @@
"download_failed": "Download failed",
"download_started": "Download started",
"enabled": "Enabled",
"experimental": "Experimental",
"file_imported": "File imported",
"finished_in": "Finished in {duration} ms",
"hide": "Hide",

View file

@ -59,7 +59,6 @@ import { GistSource } from "~/helpers/import-export/import/import-sources/GistSo
import { TeamWorkspace } from "~/services/workspace.service"
import { invokeAction } from "~/helpers/actions"
const isPostmanImporterInProgress = ref(false)
const isInsomniaImporterInProgress = ref(false)
const isOpenAPIImporterInProgress = ref(false)
const isRESTImporterInProgress = ref(false)
@ -171,6 +170,7 @@ const emit = defineEmits<{
const isHoppMyCollectionExporterInProgress = ref(false)
const isHoppTeamCollectionExporterInProgress = ref(false)
const isHoppGistCollectionExporterInProgress = ref(false)
const isPostmanImporterInProgress = ref(false)
const isTeamWorkspace = computed(() => {
return props.collectionsType.type === "team-collections"
@ -179,19 +179,83 @@ const isTeamWorkspace = computed(() => {
const currentImportSummary: Ref<{
showImportSummary: boolean
importedCollections: HoppCollection[] | null
scriptsImported?: boolean
originalScriptCounts?: { preRequest: number; test: number }
}> = ref({
showImportSummary: false,
importedCollections: null,
scriptsImported: false,
originalScriptCounts: undefined,
})
const setCurrentImportSummary = (collections: HoppCollection[]) => {
const setCurrentImportSummary = (
collections: HoppCollection[],
scriptsImported?: boolean,
originalScriptCounts?: { preRequest: number; test: number }
) => {
currentImportSummary.value.importedCollections = collections
currentImportSummary.value.showImportSummary = true
currentImportSummary.value.scriptsImported = scriptsImported
currentImportSummary.value.originalScriptCounts = originalScriptCounts
}
const unsetCurrentImportSummary = () => {
currentImportSummary.value.importedCollections = null
currentImportSummary.value.showImportSummary = false
currentImportSummary.value.scriptsImported = false
currentImportSummary.value.originalScriptCounts = undefined
}
// Count scripts in raw Postman collection JSON (before import strips them)
const countPostmanScripts = (
content: string[]
): { preRequest: number; test: number } => {
let preRequestCount = 0
let testCount = 0
const countInItem = (item: any) => {
// Only count if this is a request (has request object), not a folder
const isRequest = item?.request !== undefined
if (isRequest && item?.event) {
const prerequest = item.event.find((e: any) => e.listen === "prerequest")
const test = item.event.find((e: any) => e.listen === "test")
if (
prerequest?.script?.exec &&
Array.isArray(prerequest.script.exec) &&
prerequest.script.exec.some((line: string) => line?.trim())
) {
preRequestCount++
}
if (
test?.script?.exec &&
Array.isArray(test.script.exec) &&
test.script.exec.some((line: string) => line?.trim())
) {
testCount++
}
}
// Recursively count in nested items (folders)
if (item?.item && Array.isArray(item.item)) {
item.item.forEach(countInItem)
}
}
content.forEach((fileContent) => {
try {
const collection = JSON.parse(fileContent)
if (collection?.item && Array.isArray(collection.item)) {
collection.item.forEach(countInItem)
}
} catch (e) {
// Invalid JSON, skip
}
})
return { preRequest: preRequestCount, test: testCount }
}
const HoppRESTImporter: ImporterOrExporter = {
@ -379,15 +443,20 @@ const HoppPostmanImporter: ImporterOrExporter = {
caption: "import.from_file",
acceptedFileTypes: ".json",
description: "import.from_postman_import_summary",
onImportFromFile: async (content) => {
showPostmanScriptOption: true,
onImportFromFile: async (content: string[], importScripts?: boolean) => {
isPostmanImporterInProgress.value = true
const res = await hoppPostmanImporter(content)()
// Count scripts from raw Postman JSON before importing
const originalCounts =
importScripts === undefined ? countPostmanScripts(content) : undefined
const res = await hoppPostmanImporter(content, importScripts ?? false)()
if (E.isRight(res)) {
await handleImportToStore(res.right)
setCurrentImportSummary(res.right)
setCurrentImportSummary(res.right, importScripts, originalCounts)
platform.analytics?.logEvent({
platform: "rest",

View file

@ -202,6 +202,8 @@ props.importerModules.forEach((importer) => {
props: () => ({
collections: importSummary.value.importedCollections,
importFormat: importer.metadata.format,
scriptsImported: importSummary.value.scriptsImported,
originalScriptCounts: importSummary.value.originalScriptCounts,
"on-close": () => {
emit("hide-modal")
},

View file

@ -52,13 +52,41 @@
}}
</template>
</p>
<!-- Postman-specific: Script import checkbox (only use case so far) -->
<div
v-if="showPostmanScriptOption && experimentalScriptingEnabled"
class="flex items-start space-x-3 px-1"
>
<HoppSmartCheckbox
:on="importScripts"
@change="importScripts = !importScripts"
/>
<label
for="importScriptsCheckbox"
class="cursor-pointer select-none text-secondary flex flex-col space-y-0.5"
>
<span class="font-semibold flex space-x-1">
<span>
{{ t("import.import_scripts") }}
</span>
<span class="text-tiny text-secondaryLight">
({{ t("state.experimental") }})
</span>
</span>
<span class="text-tiny text-secondaryLight">
{{ t("import.import_scripts_description") }}</span
>
</label>
</div>
<div>
<HoppButtonPrimary
:disabled="disableImportCTA"
:label="t('import.title')"
:loading="loading"
class="w-full"
@click="emit('importFromFile', fileContent)"
@click="handleImport"
/>
</div>
</div>
@ -69,6 +97,7 @@ import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { computed, ref } from "vue"
import { platform } from "~/platform"
import { useSetting } from "~/composables/settings"
const props = withDefaults(
defineProps<{
@ -76,16 +105,24 @@ const props = withDefaults(
acceptedFileTypes: string
loading?: boolean
description?: string
showPostmanScriptOption?: boolean
}>(),
{
loading: false,
description: undefined,
showPostmanScriptOption: false,
}
)
const t = useI18n()
const toast = useToast()
// Postman-specific: Script import state (only use case so far)
const importScripts = ref(false)
const experimentalScriptingEnabled = useSetting(
"EXPERIMENTAL_SCRIPTING_SANDBOX"
)
const ALLOWED_FILE_SIZE_LIMIT = platform.limits?.collectionImportSizeLimit ?? 10 // Default to 10 MB
const importFilesCount = ref(0)
@ -97,7 +134,7 @@ const fileContent = ref<string[]>([])
const inputChooseFileToImportFrom = ref<HTMLInputElement | any>()
const emit = defineEmits<{
(e: "importFromFile", content: string[]): void
(e: "importFromFile", content: string[], ...additionalArgs: any[]): void
}>()
// Disable the import CTA if no file is selected, the file size limit is exceeded, or during an import action indicated by the `isLoading` prop
@ -106,6 +143,16 @@ const disableImportCTA = computed(
!hasFile.value || showFileSizeLimitExceededWarning.value || props.loading
)
const handleImport = () => {
// If Postman script option is enabled AND experimental sandbox is enabled, pass the importScripts value
// Otherwise, don't pass it (undefined) to indicate the feature wasn't available
if (props.showPostmanScriptOption && experimentalScriptingEnabled.value) {
emit("importFromFile", fileContent.value, importScripts.value)
} else {
emit("importFromFile", fileContent.value)
}
}
const onFileChange = async () => {
// Reset the state on entering the handler to avoid any stale state
if (showFileSizeLimitExceededWarning.value) {

View file

@ -1,3 +1,120 @@
<template>
<div class="flex flex-col p-1">
<div class="space-y-4 p-1">
<div v-for="feature in visibleFeatures" :key="feature.id">
<p class="flex items-center">
<span
class="inline-flex items-center justify-center flex-shrink-0 mr-4 border-4 rounded-full border-primary"
:class="{
'text-green-500':
featureSupportForImportFormat[feature.id] === 'SUPPORTED' ||
featureSupportForImportFormat[feature.id] === 'SKIPPED',
'text-amber-500':
featureSupportForImportFormat[feature.id] ===
'NOT_SUPPORTED_BY_HOPPSCOTCH_IMPORT',
}"
>
<icon-lucide-check-circle
v-if="
featureSupportForImportFormat[feature.id] === 'SUPPORTED' ||
featureSupportForImportFormat[feature.id] === 'SKIPPED'
"
class="svg-icons"
/>
<IconInfo
v-else-if="
featureSupportForImportFormat[feature.id] ===
'NOT_SUPPORTED_BY_HOPPSCOTCH_IMPORT'
"
class="svg-icons"
/>
</span>
<span>{{ t(feature.label) }}</span>
</p>
<p class="ml-10 text-secondaryLight">
<template
v-if="featureSupportForImportFormat[feature.id] === 'SUPPORTED'"
>
{{ feature.count }}
{{
feature.count != 1
? t(feature.label)
: t(feature.label).slice(0, -1)
}}
Imported
</template>
<template
v-else-if="featureSupportForImportFormat[feature.id] === 'SKIPPED'"
>
0 {{ t(feature.label) }} Imported
</template>
<template
v-else-if="
featureSupportForImportFormat[feature.id] ===
'NOT_SUPPORTED_BY_HOPPSCOTCH_IMPORT'
"
>
<!-- Special message for Postman scripts when using legacy sandbox -->
<template
v-if="
importFormat === 'postman' &&
(feature.id === 'preRequestScripts' ||
feature.id === 'testScripts') &&
scriptsImported === undefined
"
>
0 {{ t(feature.label) }} Imported
</template>
<!-- Generic message for other unsupported features -->
<template v-else>
{{
t("import.import_summary_not_supported_by_hoppscotch_import", {
featureLabel: t(feature.label),
})
}}
</template>
</template>
</p>
</div>
</div>
<!-- Informational banner for script imports when experimental sandbox is disabled -->
<div
v-if="showScriptImportInfo"
class="mt-6 flex items-start space-x-3 rounded border border-dividerLight shadow-sm bg-primaryLight px-2 py-4"
>
<IconInfo class="flex-shrink-0 text-accent svg-icons" />
<div class="flex-1 space-y-2">
<p class="font-semibold text-secondary">
{{ totalScriptsCount }}
{{
totalScriptsCount === 1
? t("import.import_summary_script_found")
: t("import.import_summary_scripts_found")
}}
</p>
<p class="text-secondaryLight">
{{ t("import.import_summary_enable_experimental_sandbox") }}
</p>
</div>
</div>
<div class="mt-9">
<HoppButtonSecondary
class="w-full"
:label="t('action.close')"
outline
filled
@click="onClose"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
import { computed, Ref, ref, watch } from "vue"
@ -18,6 +135,7 @@ type FeatureStatus =
| "SUPPORTED"
| "NOT_SUPPORTED_BY_HOPPSCOTCH_IMPORT"
| "NOT_SUPPORTED_BY_SOURCE"
| "SKIPPED"
type FeatureWithCount = {
count: number
@ -28,6 +146,8 @@ type FeatureWithCount = {
const props = defineProps<{
importFormat: SupportedImportFormat
collections: HoppCollection[]
scriptsImported?: boolean
originalScriptCounts?: { preRequest: number; test: number }
onClose: () => void
}>()
@ -158,7 +278,30 @@ watch(
)
const featureSupportForImportFormat = computed(() => {
return importSourceAndSupportedFeatures[props.importFormat]
const baseSupport = importSourceAndSupportedFeatures[props.importFormat]
// Handle Postman script import status
if (props.importFormat === "postman") {
if (props.scriptsImported === true) {
// User checked the box and imported scripts
return {
...baseSupport,
preRequestScripts: "SUPPORTED" as FeatureStatus,
testScripts: "SUPPORTED" as FeatureStatus,
}
} else if (props.scriptsImported === false) {
// User explicitly didn't import scripts (checkbox unchecked)
return {
...baseSupport,
preRequestScripts: "SKIPPED" as FeatureStatus,
testScripts: "SKIPPED" as FeatureStatus,
}
}
// props.scriptsImported === undefined means legacy sandbox or old import
// Keep default NOT_SUPPORTED_BY_HOPPSCOTCH_IMPORT
}
return baseSupport
})
const visibleFeatures = computed(() => {
@ -169,74 +312,23 @@ const visibleFeatures = computed(() => {
)
})
})
const showScriptImportInfo = computed(() => {
return (
props.importFormat === "postman" &&
props.scriptsImported === undefined &&
totalScriptsCount.value > 0
)
})
const totalScriptsCount = computed(() => {
if (props.importFormat !== "postman" || props.scriptsImported !== undefined)
return 0
// Use original counts from raw Postman JSON
const preRequestScripts = props.originalScriptCounts?.preRequest || 0
const testScripts = props.originalScriptCounts?.test || 0
return preRequestScripts + testScripts
})
</script>
<template>
<div class="space-y-4">
<div v-for="feature in visibleFeatures" :key="feature.id">
<p class="flex items-center">
<span
class="inline-flex items-center justify-center flex-shrink-0 mr-4 border-4 rounded-full border-primary"
:class="{
'text-green-500':
featureSupportForImportFormat[feature.id] === 'SUPPORTED',
'text-amber-500':
featureSupportForImportFormat[feature.id] ===
'NOT_SUPPORTED_BY_HOPPSCOTCH_IMPORT',
}"
>
<icon-lucide-check-circle
v-if="featureSupportForImportFormat[feature.id] === 'SUPPORTED'"
class="svg-icons"
/>
<IconInfo
v-else-if="
featureSupportForImportFormat[feature.id] ===
'NOT_SUPPORTED_BY_HOPPSCOTCH_IMPORT'
"
class="svg-icons"
/>
</span>
<span>{{ t(feature.label) }}</span>
</p>
<p class="ml-10 text-secondaryLight">
<template
v-if="featureSupportForImportFormat[feature.id] === 'SUPPORTED'"
>
{{ feature.count }}
{{
feature.count != 1
? t(feature.label)
: t(feature.label).slice(0, -1)
}}
Imported
</template>
<template
v-else-if="
featureSupportForImportFormat[feature.id] ===
'NOT_SUPPORTED_BY_HOPPSCOTCH_IMPORT'
"
>
{{
t("import.import_summary_not_supported_by_hoppscotch_import", {
featureLabel: t(feature.label),
})
}}
</template>
</p>
</div>
</div>
<div class="mt-10">
<HoppButtonSecondary
class="w-full"
:label="t('action.close')"
outline
filled
@click="onClose"
/>
</div>
</template>

View file

@ -2,14 +2,18 @@ import FileImportVue from "~/components/importExport/ImportExportSteps/FileImpor
import { defineStep } from "~/composables/step-components"
import { v4 as uuidv4 } from "uuid"
import { Ref } from "vue"
import type { Ref } from "vue"
export function FileSource(metadata: {
acceptedFileTypes: string
caption: string
onImportFromFile: (content: string[]) => any | Promise<any>
onImportFromFile: (
content: string[],
importScripts?: boolean
) => any | Promise<any>
isLoading?: Ref<boolean>
description?: string
showPostmanScriptOption?: boolean
}) {
const stepID = uuidv4()
@ -19,5 +23,6 @@ export function FileSource(metadata: {
onImportFromFile: metadata.onImportFromFile,
loading: metadata.isLoading?.value,
description: metadata.description,
showPostmanScriptOption: metadata.showPostmanScriptOption,
}))
}

View file

@ -39,6 +39,30 @@ const safeParseJSON = (jsonStr: string) => O.tryCatch(() => JSON.parse(jsonStr))
const isPMItem = (x: unknown): x is Item => Item.isItem(x)
/**
* Checks if the Postman collection schema version supports scripts (v2.0+)
* @param schema - The schema URL from collection.info.schema
* @returns true if v2.0 or v2.1, false otherwise
*/
const isSchemaVersionSupported = (schema?: string): boolean => {
if (!schema) return false
// Support both schema.getpostman.com and schema.postman.com
return schema.includes("/v2.0.") || schema.includes("/v2.1.")
}
/**
* Extracts the collection schema from raw JSON data
* Note: PMCollection SDK doesn't expose .info.schema, so we parse raw JSON
*/
const getCollectionSchema = (jsonStr: string): string | null => {
try {
const data = JSON.parse(jsonStr)
return data?.info?.schema ?? null
} catch {
return null
}
}
const replacePMVarTemplating = flow(
S.replace(/{{\s*/g, "<<"),
S.replace(/\s*}}/g, ">>")
@ -482,7 +506,56 @@ const getHoppReqURL = (url: Item["request"]["url"] | null): string => {
)
}
const getHoppRequest = (item: Item): HoppRESTRequest => {
/**
* Extracts script content from a Postman event
* Handles both string format and exec array format
*/
const extractScriptFromEvent = (event: any): string => {
if (!event?.script) return ""
if (typeof event.script === "string") {
return event.script
}
if (event.script.exec && Array.isArray(event.script.exec)) {
return event.script.exec.join("\n")
}
return ""
}
const getHoppScripts = (
item: Item,
importScripts: boolean
): { preRequestScript: string; testScript: string } => {
if (!importScripts) {
return { preRequestScript: "", testScript: "" }
}
let preRequestScript = ""
let testScript = ""
// Postman stores scripts in the events array
if (item.events) {
const events = item.events.all()
events.forEach((event: any) => {
if (event.listen === "prerequest") {
preRequestScript = extractScriptFromEvent(event)
} else if (event.listen === "test") {
testScript = extractScriptFromEvent(event)
}
})
}
return { preRequestScript, testScript }
}
const getHoppRequest = (
item: Item,
importScripts: boolean
): HoppRESTRequest => {
const { preRequestScript, testScript } = getHoppScripts(item, importScripts)
return makeRESTRequest({
name: item.name,
endpoint: getHoppReqURL(item.request.url),
@ -496,38 +569,68 @@ const getHoppRequest = (item: Item): HoppRESTRequest => {
}),
requestVariables: getHoppReqVariables(item.request.url.variables),
responses: getHoppResponses(item.responses),
// TODO: Decide about this
preRequestScript: "",
testScript: "",
preRequestScript,
testScript,
})
}
const getHoppFolder = (ig: ItemGroup<Item>): HoppCollection =>
const getHoppFolder = (
ig: ItemGroup<Item>,
importScripts: boolean
): HoppCollection =>
makeCollection({
name: ig.name,
folders: pipe(
ig.items.all(),
A.filter(isPMItemGroup),
A.map(getHoppFolder)
A.map((folder) => getHoppFolder(folder, importScripts))
),
requests: pipe(
ig.items.all(),
A.filter(isPMItem),
A.map((item) => getHoppRequest(item, importScripts))
),
requests: pipe(ig.items.all(), A.filter(isPMItem), A.map(getHoppRequest)),
auth: getHoppReqAuth(ig.auth),
headers: [],
variables: getHoppCollVariables(ig),
})
export const getHoppCollections = (collections: PMCollection[]) => {
return collections.map(getHoppFolder)
export const getHoppCollections = (
collections: PMCollection[],
importScripts: boolean
) => {
return collections.map((collection) =>
getHoppFolder(collection, importScripts)
)
}
export const hoppPostmanImporter = (fileContents: string[]) =>
export const hoppPostmanImporter = (
fileContents: string[],
importScripts = false
) =>
pipe(
// Try reading
fileContents,
A.traverse(O.Applicative)(readPMCollection),
O.map(flow(getHoppCollections)),
O.map((collections) => {
// Validate schema version if importing scripts
if (importScripts && fileContents.length > 0) {
const schema = getCollectionSchema(fileContents[0])
const isSupported = isSchemaVersionSupported(schema ?? undefined)
if (!isSupported) {
console.warn(
`[Postman Import] Script import requested but collection schema "${schema ?? "unknown"}" does not support scripts. ` +
`Only Postman Collection Format v2.0 and v2.1 are supported. Scripts will be skipped.`
)
// Skip script import for unsupported versions
return getHoppCollections(collections, false)
}
}
return getHoppCollections(collections, importScripts)
}),
TE.fromOption(() => IMPORTER_INVALID_FILE_FORMAT)
)

View file

@ -230,13 +230,227 @@ type HoppRESTAuth =
| HoppRESTAuthAkamaiEdgeGrid
| HoppRESTAuthJWT
interface Expectation extends ExpectationMethods {
not: BaseExpectation
// Length property that can be used both as property and callable with comparison methods
// Matches actual runtime implementation in post-request.js lines 371-414
interface ChaiLengthAssertion {
// Primary comparison methods (actually implemented in runtime)
above(n: number): ChaiExpectation
below(n: number): ChaiExpectation
within(min: number, max: number): ChaiExpectation
least(n: number): ChaiExpectation
most(n: number): ChaiExpectation
gte(n: number): ChaiExpectation
lte(n: number): ChaiExpectation
// Aliases implemented in runtime (for compatibility)
greaterThan(n: number): ChaiExpectation // Alias for above()
lessThan(n: number): ChaiExpectation // Alias for below()
// Postman-style .at.least() and .at.most() support
at: Readonly<{
least(n: number): ChaiExpectation
most(n: number): ChaiExpectation
}>
}
interface BaseExpectation extends ExpectationMethods {}
// Chai-powered assertion interface
interface ChaiExpectation {
// Negation
not: ChaiExpectation
interface ExpectationMethods {
// Language chains (improve readability without affecting assertions)
to: ChaiExpectation
be: ChaiExpectation
been: ChaiExpectation
is: ChaiExpectation
that: ChaiExpectation
which: ChaiExpectation
and: ChaiExpectation
has: ChaiExpectation
have: ChaiExpectation
with: ChaiExpectation
at: ChaiExpectation
of: ChaiExpectation
same: ChaiExpectation
but: ChaiExpectation
does: ChaiExpectation
still: ChaiExpectation
also: ChaiExpectation
// Modifiers (can be used as both properties and methods)
deep: ChaiExpectation
nested: ChaiExpectation
own: ChaiExpectation
ordered: ChaiExpectation
any: ChaiExpectation
all: ChaiExpectation
// Include/contain can be used as both properties AND methods
include: ChaiExpectation & ((value: any) => ChaiExpectation)
contain: ChaiExpectation & ((value: any) => ChaiExpectation)
includes: ChaiExpectation & ((value: any) => ChaiExpectation)
contains: ChaiExpectation & ((value: any) => ChaiExpectation)
// Type assertions - can be used as both property and method
a: ChaiExpectation & ((type: string) => ChaiExpectation)
an: ChaiExpectation & ((type: string) => ChaiExpectation)
// Equality assertions
equal(value: any): ChaiExpectation
equals(value: any): ChaiExpectation
eq(value: any): ChaiExpectation
eql(value: any): ChaiExpectation
// Truthiness assertions
true: ChaiExpectation
false: ChaiExpectation
ok: ChaiExpectation
null: ChaiExpectation
undefined: ChaiExpectation
NaN: ChaiExpectation
exist: ChaiExpectation
empty: ChaiExpectation
arguments: ChaiExpectation
// Numerical comparison assertions
above(n: number): ChaiExpectation
gt(n: number): ChaiExpectation
greaterThan(n: number): ChaiExpectation
below(n: number): ChaiExpectation
lt(n: number): ChaiExpectation
lessThan(n: number): ChaiExpectation
least(n: number): ChaiExpectation
gte(n: number): ChaiExpectation
greaterThanOrEqual(n: number): ChaiExpectation
most(n: number): ChaiExpectation
lte(n: number): ChaiExpectation
lessThanOrEqual(n: number): ChaiExpectation
within(start: number, finish: number): ChaiExpectation
closeTo(expected: number, delta: number): ChaiExpectation
approximately(expected: number, delta: number): ChaiExpectation
// Property assertions
property(name: string, value?: any): ChaiExpectation
ownProperty(name: string, value?: any): ChaiExpectation
haveOwnProperty(name: string, value?: any): ChaiExpectation
ownPropertyDescriptor(
name: string,
descriptor?: PropertyDescriptor
): ChaiExpectation
// Length assertions - SPECIAL: Can be used as property or called with comparison methods
// Allow .length to be called as function or used as property with comparison methods
length: ChaiLengthAssertion & number & ((n: number) => ChaiExpectation)
lengthOf: ((n: number) => ChaiExpectation) & ChaiLengthAssertion
// String/Array inclusion assertions
string(str: string): ChaiExpectation
match(regex: RegExp): ChaiExpectation
matches(regex: RegExp): ChaiExpectation
members(set: any[]): ChaiExpectation
oneOf(list: any[]): ChaiExpectation
// Key assertions
keys(...keys: string[] | [string[]]): ChaiExpectation
key(key: string | string[]): ChaiExpectation
// Function/Error assertions
throw(
errorLike?: any,
errMsgMatcher?: string | RegExp,
message?: string
): ChaiExpectation
throws(
errorLike?: any,
errMsgMatcher?: string | RegExp,
message?: string
): ChaiExpectation
Throw(
errorLike?: any,
errMsgMatcher?: string | RegExp,
message?: string
): ChaiExpectation
respondTo(method: string): ChaiExpectation
respondsTo(method: string): ChaiExpectation
itself: ChaiExpectation
satisfy(matcher: (value: any) => boolean): ChaiExpectation
satisfies(matcher: (value: any) => boolean): ChaiExpectation
// Object state assertions
sealed: ChaiExpectation
frozen: ChaiExpectation
extensible: ChaiExpectation
finite: ChaiExpectation
// instanceof assertion
instanceof(constructor: any): ChaiExpectation
instanceOf(constructor: any): ChaiExpectation
// Side-effect assertions
// Side effect assertions - support both getter function and object+property patterns
change(
getter: () => any
): ChaiExpectation & { by(delta: number): ChaiExpectation }
change(
obj: any,
prop: string
): ChaiExpectation & { by(delta: number): ChaiExpectation }
increase(
getter: () => any
): ChaiExpectation & { by(delta: number): ChaiExpectation }
increase(
obj: any,
prop: string
): ChaiExpectation & { by(delta: number): ChaiExpectation }
decrease(
getter: () => any
): ChaiExpectation & { by(delta: number): ChaiExpectation }
decrease(
obj: any,
prop: string
): ChaiExpectation & { by(delta: number): ChaiExpectation }
// Postman custom Chai assertions (available via pm.expect())
/**
* Assert that value matches JSON Schema
* @param schema - JSON Schema object
*/
jsonSchema(schema: {
type?: string
required?: string[]
properties?: Record<string, any>
items?: any
enum?: any[]
minimum?: number
maximum?: number
minLength?: number
maxLength?: number
pattern?: string
minItems?: number
maxItems?: number
}): ChaiExpectation
/**
* Assert that string value contains specific charset/encoding
* @param expectedCharset - Expected charset (e.g., 'utf-8', 'iso-8859-1')
*/
charset(expectedCharset: string): ChaiExpectation
/**
* Assert that cookie exists and optionally has specific value
* @param cookieName - Name of the cookie
* @param cookieValue - Optional expected value
*/
cookie(cookieName: string, cookieValue?: string): ChaiExpectation
/**
* Assert that JSON path exists and optionally has specific value
* @param path - JSONPath expression (e.g., '$.users[0].name')
* @param expectedValue - Optional expected value at path
*/
jsonPath(path: string, expectedValue?: any): ChaiExpectation
// Legacy methods (kept for backward compatibility)
toBe(value: any): void
toBeLevel2xx(): void
toBeLevel3xx(): void
@ -247,9 +461,32 @@ interface ExpectationMethods {
toInclude(value: any): void
}
// Legacy expectation interface for pw namespace (backward compatibility only)
interface LegacyExpectation extends LegacyExpectationMethods {
not: LegacyBaseExpectation
}
interface LegacyBaseExpectation extends LegacyExpectationMethods {}
interface LegacyExpectationMethods {
toBe(value: any): void
toBeLevel2xx(): void
toBeLevel3xx(): void
toBeLevel4xx(): void
toBeLevel5xx(): void
toBeType(type: string): void
toHaveLength(length: number): void
toInclude(value: any): void
}
// Backward compatibility types for hopp and pm namespaces
interface Expectation extends ChaiExpectation {}
interface BaseExpectation extends ChaiExpectation {}
interface ExpectationMethods extends ChaiExpectation {}
declare namespace pw {
function test(name: string, func: () => void): void
function expect(value: any): Expectation
function expect(value: any): LegacyExpectation
const response: Readonly<{
status: number
body: any
@ -269,15 +506,27 @@ declare namespace hopp {
get(key: string): string | null
getRaw(key: string): string | null
getInitialRaw(key: string): string | null
set(key: string, value: string): void
delete(key: string): void
reset(key: string): void
setInitial(key: string, value: string): void
active: Readonly<{
get(key: string): string | null
getRaw(key: string): string | null
getInitialRaw(key: string): string | null
set(key: string, value: string): void
delete(key: string): void
reset(key: string): void
setInitial(key: string, value: string): void
}>
global: Readonly<{
get(key: string): string | null
getRaw(key: string): string | null
getInitialRaw(key: string): string | null
set(key: string, value: string): void
delete(key: string): void
reset(key: string): void
setInitial(key: string, value: string): void
}>
}>
@ -300,9 +549,35 @@ declare namespace hopp {
readonly responseTime: number
body: Readonly<{
asText(): string
asJSON(): Record<string, any>
asJSON(): any
bytes(): Uint8Array
}>
/**
* Get response body as text string
* @returns Response body as string
*/
text(): string
/**
* Get response body as parsed JSON
* @returns Parsed JSON object
*/
json(): any
/**
* Get HTTP reason phrase (status text)
* @returns HTTP reason phrase (e.g., "OK", "Not Found")
*/
reason(): string
/**
* Convert response to data URI format
* @returns Data URI string with base64 encoded content
*/
dataURI(): string
/**
* Parse JSONP response
* @param callbackName - Optional callback function name (default: "callback")
* @returns Parsed JSON object from JSONP wrapper
*/
jsonp(callbackName?: string): any
}>
const cookies: Readonly<{
@ -315,7 +590,17 @@ declare namespace hopp {
}>
function test(name: string, testFunction: () => void): void
function expect(value: any): Expectation
interface HoppExpectFunction {
(value: any): ChaiExpectation
/**
* Fail the test with a custom message
* @param message - Optional message to display on failure
*/
fail(message?: string): never
}
const expect: HoppExpectFunction
const info: Readonly<{
readonly eventName: "post-request"
@ -328,37 +613,258 @@ declare namespace hopp {
declare namespace pm {
const environment: Readonly<{
get(key: string): string | null
set(key: string, value: string): void
readonly name: string
get(key: string): any
set(key: string, value: any): void
unset(key: string): void
has(key: string): boolean
clear(): never
toObject(): never
}>
const globals: Readonly<{
get(key: string): string | null
set(key: string, value: string): void
unset(key: string): void
has(key: string): boolean
clear(): never
toObject(): never
}>
const variables: Readonly<{
get(key: string): string | null
set(key: string, value: string): void
has(key: string): boolean
clear(): void
toObject(): Record<string, string>
replaceIn(template: string): string
}>
const globals: Readonly<{
get(key: string): any
/**
* Set a global variable
* @param key - Variable key
* @param value - Variable value (undefined is preserved, other types are coerced to strings)
*/
set(key: string, value: any): void
unset(key: string): void
has(key: string): boolean
clear(): void
toObject(): Record<string, string>
replaceIn(template: string): string
}>
const variables: Readonly<{
get(key: string): any
/**
* Set a variable in the active environment scope
* @param key - Variable key
* @param value - Variable value (undefined is preserved, other types are coerced to strings)
*/
set(key: string, value: any): void
has(key: string): boolean
replaceIn(template: string): string
toObject(): Record<string, string>
}>
const request: Readonly<{
readonly url: { toString(): string }
readonly url: Readonly<{
toString(): string
readonly protocol: string
readonly host: string[]
readonly port: string
readonly path: string[]
readonly hash: string
// URL Helper Methods (Postman-compatible)
/**
* Get the hostname as a string (e.g., "api.example.com")
* @returns The hostname portion of the URL
*/
getHost(): string
/**
* Get the path with leading slash (e.g., "/v1/users")
* @param unresolved - If true, returns unresolved path with variables (currently ignored)
* @returns The path portion of the URL
*/
getPath(unresolved?: boolean): string
/**
* Get the path with query string (e.g., "/v1/users?page=1")
* @returns Path and query string combined
*/
getPathWithQuery(): string
/**
* Get the query string without leading ? (e.g., "page=1&limit=20")
* @param options - Optional configuration (currently ignored)
* @returns Query string without the leading question mark
*/
getQueryString(options?: Record<string, unknown>): string
/**
* Get the hostname with port (e.g., "api.example.com:8080")
* @param forcePort - If true, includes standard ports (80/443)
* @returns Hostname with port if non-standard or forced
*/
getRemote(forcePort?: boolean): string
readonly query: Readonly<{
/**
* Get the value of a query parameter by key
* @param key - Parameter key to retrieve
* @returns Parameter value or null if not found
*/
get(key: string): string | null
/**
* Check if a query parameter exists
* @param key - Parameter key to check
* @returns true if parameter exists, false otherwise
*/
has(key: string): boolean
/**
* Get all query parameters as a key-value object
* @returns Object with all query parameters
*/
all(): Record<string, string>
/**
* Convert query parameters to object (alias for all())
* @returns Object with all query parameters
*/
toObject(): Record<string, string>
/**
* Get the number of query parameters
* @returns Count of parameters
*/
count(): number
/**
* Get a parameter by index
* @param index - Zero-based index
* @returns Parameter object or null if out of bounds
*/
idx(index: number): { key: string; value: string } | null
/**
* Iterate over all query parameters
* @param callback - Function to call for each parameter
*/
each(callback: (param: { key: string; value: string }) => void): void
/**
* Map query parameters to a new array
* @param callback - Transform function
* @returns Array of transformed values
*/
map<T>(callback: (param: { key: string; value: string }) => T): T[]
/**
* Filter query parameters
* @param callback - Predicate function
* @returns Array of parameters matching the predicate
*/
filter(
callback: (param: { key: string; value: string }) => boolean
): Array<{ key: string; value: string }>
/**
* Find a query parameter by string key or function predicate
* @param rule - String key or predicate function
* @param context - Optional context to bind the predicate function
* @returns Matching parameter or null if not found
*/
find(
rule: string | ((param: { key: string; value: string }) => boolean),
context?: any
): { key: string; value: string } | null
/**
* Get the index of a query parameter
* @param item - String key or parameter object to find
* @returns Index of parameter, or -1 if not found
*/
indexOf(item: string | { key: string; value: string }): number
}>
}>
/**
* Client certificate used for mutual TLS authentication
* In Postman, certificates are configured at the app/collection level, not programmatically in scripts
* Returns undefined in Hoppscotch as certificate configuration is handled at the application level
* @see https://learning.postman.com/docs/sending-requests/certificates/
*/
readonly certificate:
| {
readonly name: string
readonly matches: string[]
readonly key: { readonly src: string }
readonly cert: { readonly src: string }
readonly passphrase?: string
}
| undefined
/**
* Proxy configuration for the request
* In Postman, proxy is configured at the app level, not programmatically in scripts
* Returns undefined in Hoppscotch as proxy configuration is handled at the application level
* @see https://learning.postman.com/docs/sending-requests/capturing-request-data/proxy/
*/
readonly proxy:
| {
readonly host: string
readonly port: number
readonly tunnel: boolean
readonly disabled: boolean
}
| undefined
readonly method: string
readonly headers: Readonly<{
/**
* Get the value of a header by name (case-insensitive)
* @param name - Header name to retrieve
* @returns Header value or null if not found
*/
get(name: string): string | null
/**
* Check if a header exists (case-insensitive)
* @param name - Header name to check
* @returns true if header exists, false otherwise
*/
has(name: string): boolean
/**
* Get all headers as a key-value object
* @returns Object with all headers (keys in lowercase)
*/
all(): Record<string, string>
/**
* Convert headers to object (alias for all())
* @returns Object with all headers
*/
toObject(): Record<string, string>
/**
* Get the number of headers
* @returns Count of headers
*/
count(): number
/**
* Get a header by index
* @param index - Zero-based index
* @returns Header object or null if out of bounds
*/
idx(index: number): { key: string; value: string } | null
/**
* Iterate over all headers
* @param callback - Function to call for each header
*/
each(callback: (header: { key: string; value: string }) => void): void
/**
* Map headers to a new array
* @param callback - Transform function
* @returns Array of transformed values
*/
map<T>(callback: (header: { key: string; value: string }) => T): T[]
/**
* Filter headers
* @param callback - Predicate function
* @returns Array of headers matching the predicate
*/
filter(
callback: (header: { key: string; value: string }) => boolean
): Array<{ key: string; value: string }>
/**
* Find a header by string name or function predicate (case-insensitive)
* @param rule - String name or predicate function
* @param context - Optional context to bind the predicate function
* @returns Matching header or null if not found
*/
find(
rule: string | ((header: { key: string; value: string }) => boolean),
context?: any
): { key: string; value: string } | null
/**
* Get the index of a header (case-insensitive)
* @param item - String name or header object to find
* @returns Index of header, or -1 if not found
*/
indexOf(item: string | { key: string; value: string }): number
}>
readonly body: HoppRESTReqBody
readonly auth: HoppRESTAuth
@ -368,29 +874,104 @@ declare namespace pm {
readonly code: number
readonly status: string
readonly responseTime: number
readonly responseSize: number
text(): string
json(): Record<string, any>
json(): any
stream: Uint8Array
/**
* Get HTTP reason phrase (status text)
* @returns HTTP reason phrase (e.g., "OK", "Not Found")
*/
reason(): string
/**
* Convert response to data URI format
* @returns Data URI string with base64 encoded content
*/
dataURI(): string
/**
* Parse JSONP response
* @param callbackName - Optional callback function name (default: "callback")
* @returns Parsed JSON object from JSONP wrapper
*/
jsonp(callbackName?: string): any
headers: Readonly<{
get(name: string): string | null
has(name: string): boolean
all(): HoppRESTResponseHeader[]
}>
cookies: Readonly<{
get(name: string): any
has(name: string): any
toObject(): any
get(name: string): string | null
has(name: string): boolean
toObject(): Record<string, string>
}>
to: Readonly<{
have: Readonly<{
status(expectedCode: number): void
header(headerName: string, headerValue?: string): void
body(expectedBody: string): void
jsonBody(): void
jsonBody(key: string): void
jsonBody(key: string, expectedValue: any): void
jsonBody(schema: object): void
responseTime: Readonly<{
below(ms: number): void
above(ms: number): void
}>
jsonSchema(schema: {
type?: string
required?: string[]
properties?: Record<string, any>
items?: any
enum?: any[]
minimum?: number
maximum?: number
minLength?: number
maxLength?: number
pattern?: string
minItems?: number
maxItems?: number
}): void
charset(expectedCharset: string): void
cookie(cookieName: string, cookieValue?: string): void
jsonPath(path: string, expectedValue?: any): void
}>
be: Readonly<{
ok(): void
success(): void
accepted(): void
badRequest(): void
unauthorized(): void
forbidden(): void
notFound(): void
rateLimited(): void
serverError(): void
clientError(): void
json(): void
html(): void
xml(): void
text(): void
}>
}>
}>
const cookies: Readonly<{
get(name: string): any
set(name: string, value: string, options?: any): any
jar(): any
jar(): never
}>
function test(name: string, testFunction: () => void): void
function expect(value: any): Expectation
interface ExpectFunction {
(value: any): ChaiExpectation
/**
* Fail the test with a custom message
* @param message - Optional message to display on failure
*/
fail(message?: string): never
}
const expect: ExpectFunction
const info: Readonly<{
readonly eventName: "post-request"
@ -400,28 +981,111 @@ declare namespace pm {
readonly iterationCount: never
}>
const sendRequest: () => never
/**
* Send an HTTP request (unsupported)
* @throws Error - sendRequest is not supported in Hoppscotch
*/
function sendRequest(
request: string | { url: string; method?: string; [key: string]: any },
callback?: (err: any, response: any) => void
): never
/**
* Visualizer API (unsupported)
* The Postman Visualizer allows you to present response data as HTML templates with styling.
* This feature is not supported in Hoppscotch as it requires a browser-based visualization UI.
* @see https://learning.postman.com/docs/sending-requests/response-data/visualizer/
*/
const visualizer: Readonly<{
/**
* Set a Handlebars template to visualize response data (unsupported)
* @param layout - HTML template string with Handlebars syntax
* @param data - Data object to pass to the template
* @param options - Optional configuration object
* @throws Error - Visualizer is not supported in Hoppscotch
*/
set(
layout: string,
data?: Record<string, any>,
options?: Record<string, any>
): never
/**
* Clear the current visualization (unsupported)
* @throws Error - Visualizer is not supported in Hoppscotch
*/
clear(): never
}>
/**
* Collection variables (unsupported - Workspace feature)
* Collection variables are not supported in Hoppscotch as they are a Postman Workspace feature
*/
const collectionVariables: Readonly<{
get(): never
set(): never
unset(): never
has(): never
get(key: string): never
set(key: string, value: string): never
unset(key: string): never
has(key: string): never
clear(): never
toObject(): never
replaceIn(template: string): never
}>
/**
* Postman Vault (unsupported)
* Vault is not supported in Hoppscotch as it is a Postman-specific feature
*/
const vault: Readonly<{
get(): never
set(): never
unset(): never
get(key: string): never
set(key: string, value: string): never
unset(key: string): never
}>
/**
* Iteration data (unsupported - Collection Runner feature)
* Iteration data is not supported in Hoppscotch as it requires Collection Runner
*/
const iterationData: Readonly<{
get(): never
set(): never
unset(): never
has(): never
get(key: string): never
set(key: string, value: string): never
unset(key: string): never
has(key: string): never
toObject(): never
toJSON(): never
}>
/**
* Execution control
*/
const execution: Readonly<{
setNextRequest(): never
/**
* Execution location identifier
* Always returns ["Hoppscotch"] with current = "Hoppscotch"
*/
readonly location: readonly string[] & {
readonly current: string
}
/**
* Set next request to execute (unsupported - Collection Runner feature)
* @param requestNameOrId - Name or ID of the next request
*/
setNextRequest(requestNameOrId: string | null): never
/**
* Skip current request execution (unsupported - Collection Runner feature)
*/
skipRequest(): never
/**
* Run a request (unsupported - Collection Runner feature)
* @param requestNameOrId - Name or ID of the request to run
*/
runRequest(requestNameOrId: string): never
}>
/**
* Import packages from Package Library (unsupported)
* @param packageName - Name of the package to import (e.g., '@team-domain/package-name' or 'npm:package-name@version')
* @returns The imported package module
* @throws Error - Package imports are not supported in Hoppscotch
*/
function require(packageName: string): never
}

View file

@ -288,7 +288,7 @@ declare namespace hopp {
* - Complete replacement: When all fields are provided, replaces entire body
*
* @param body - Partial or complete HoppRESTReqBody object
*
*`
* @example
* // Partial update - just change content type
* hopp.request.setBody({ contentType: "application/xml" })
@ -348,41 +348,465 @@ declare namespace hopp {
declare namespace pm {
const environment: Readonly<{
get(key: string): string | null
set(key: string, value: string): void
/**
* Get an environment variable value
* @param key - Variable key
* @returns Variable value or undefined if not found
*/
get(key: string): any
/**
* Set an environment variable
* @param key - Variable key
* @param value - Variable value
*/
set(key: string, value: any): void
/**
* Remove an environment variable
* @param key - Variable key to remove
*/
unset(key: string): void
/**
* Check if an environment variable exists
* @param key - Variable key to check
* @returns true if variable exists, false otherwise
*/
has(key: string): boolean
clear(): never
toObject(): never
/**
* Clear all environment variables in the active environment
*/
clear(): void
/**
* Get all environment variables as an object
* @returns Object with all environment variables as key-value pairs
*/
toObject(): Record<string, string>
}>
const globals: Readonly<{
get(key: string): string | null
set(key: string, value: string): void
/**
* Get a global variable value
* @param key - Variable key
* @returns Variable value or undefined if not found
*/
get(key: string): any
/**
* Set a global variable
* @param key - Variable key
* @param value - Variable value (undefined is preserved, other types are coerced to strings)
*/
set(key: string, value: any): void
/**
* Remove a global variable
* @param key - Variable key to remove
*/
unset(key: string): void
/**
* Check if a global variable exists
* @param key - Variable key to check
* @returns true if variable exists, false otherwise
*/
has(key: string): boolean
clear(): never
toObject(): never
/**
* Clear all global variables
*/
clear(): void
/**
* Get all global variables as an object
* @returns Object with all global variables as key-value pairs
*/
toObject(): Record<string, string>
}>
const variables: Readonly<{
get(key: string): string | null
set(key: string, value: string): void
/**
* Get a variable value from either environment or global scope
* Environment variables take precedence over global variables
* @param key - Variable key
* @returns Variable value or undefined if not found
*/
get(key: string): any
/**
* Set a variable in the active environment scope
* @param key - Variable key
* @param value - Variable value (undefined is preserved, other types are coerced to strings)
*/
set(key: string, value: any): void
/**
* Check if a variable exists in either environment or global scope
* @param key - Variable key to check
* @returns true if variable exists, false otherwise
*/
has(key: string): boolean
/**
* Replace variables in a template string
* @param template - Template string with {{variable}} placeholders
* @returns String with variables replaced with their values
*/
replaceIn(template: string): string
}>
const request: Readonly<{
readonly url: { toString(): string }
readonly method: string
readonly headers: Readonly<{
/**
* Request object with full Postman compatibility
* All properties are mutable in pre-request scripts to match Postman behavior
*/
let request: {
// ID and name (read-only)
readonly id: string
readonly name: string
/**
* Client certificate used for mutual TLS authentication
* In Postman, certificates are configured at the app/collection level, not programmatically in scripts
* Returns undefined in Hoppscotch as certificate configuration is handled at the application level
* @see https://learning.postman.com/docs/sending-requests/certificates/
*/
readonly certificate:
| {
readonly name: string
readonly matches: string[]
readonly key: { readonly src: string }
readonly cert: { readonly src: string }
readonly passphrase?: string
}
| undefined
/**
* Proxy configuration for the request
* In Postman, proxy is configured at the app level, not programmatically in scripts
* Returns undefined in Hoppscotch as proxy configuration is handled at the application level
* @see https://learning.postman.com/docs/sending-requests/capturing-request-data/proxy/
*/
readonly proxy:
| {
readonly host: string
readonly port: number
readonly tunnel: boolean
readonly disabled: boolean
}
| undefined
// URL - Fully mutable with Postman URL object structure
// Intersection type allows both string assignment AND object access
url: string & {
toString(): string
protocol: string
host: string[]
port: string
path: string[]
hash: string
// URL Helper Methods (Postman-compatible)
/**
* Get the hostname as a string (e.g., "api.example.com")
* @returns The hostname portion of the URL
*/
getHost(): string
/**
* Get the path with leading slash (e.g., "/v1/users")
* @param unresolved - If true, returns unresolved path with variables (currently ignored)
* @returns The path portion of the URL
*/
getPath(unresolved?: boolean): string
/**
* Get the path with query string (e.g., "/v1/users?page=1")
* @returns Path and query string combined
*/
getPathWithQuery(): string
/**
* Get the query string without leading ? (e.g., "page=1&limit=20")
* @param options - Optional configuration (currently ignored)
* @returns Query string without the leading question mark
*/
getQueryString(options?: Record<string, unknown>): string
/**
* Get the hostname with port (e.g., "api.example.com:8080")
* @param forcePort - If true, includes standard ports (80/443)
* @returns Hostname with port if non-standard or forced
*/
getRemote(forcePort?: boolean): string
/**
* Update the entire URL from a string or object with toString()
* @param url - New URL string or object with toString() method
*/
update(url: string | { toString(): string }): void
/**
* Add multiple query parameters to the URL
* @param params - Array of parameter objects with key/value pairs
*/
addQueryParams(params: Array<{ key: string; value?: string }>): void
/**
* Remove query parameters by name
* @param params - Single parameter name or array of names to remove
*/
removeQueryParams(params: string | string[]): void
query: {
// Read methods
/**
* Get the value of a query parameter by key
* @param key - Parameter key to retrieve
* @returns Parameter value or null if not found
*/
get(key: string): string | null
/**
* Check if a query parameter exists
* @param key - Parameter key to check
* @returns true if parameter exists, false otherwise
*/
has(key: string): boolean
/**
* Get all query parameters as a key-value object
* @returns Object with all query parameters
*/
all(): Record<string, string>
/**
* Convert query parameters to object (alias for all())
* @returns Object with all query parameters
*/
toObject(): Record<string, string>
/**
* Get the number of query parameters
* @returns Count of parameters
*/
count(): number
/**
* Get a parameter by index
* @param index - Zero-based index
* @returns Parameter object or null if out of bounds
*/
idx(index: number): { key: string; value: string } | null
// Iteration methods
/**
* Iterate over all query parameters
* @param callback - Function to call for each parameter
*/
each(callback: (param: { key: string; value: string }) => void): void
/**
* Map query parameters to a new array
* @param callback - Transform function
* @returns Array of transformed values
*/
map<T>(callback: (param: { key: string; value: string }) => T): T[]
/**
* Filter query parameters
* @param callback - Predicate function
* @returns Array of parameters matching the predicate
*/
filter(
callback: (param: { key: string; value: string }) => boolean
): Array<{ key: string; value: string }>
/**
* Find a query parameter by string key or function predicate
* @param rule - String key or predicate function
* @param context - Optional context to bind the predicate function
* @returns Matching parameter or null if not found
*/
find(
rule: string | ((param: { key: string; value: string }) => boolean),
context?: any
): { key: string; value: string } | null
/**
* Get the index of a query parameter
* @param item - String key or parameter object to find
* @returns Index of parameter, or -1 if not found
*/
indexOf(item: string | { key: string; value: string }): number
// Mutation methods
/**
* Add a new query parameter
* @param param - Parameter to add
*/
add(param: { key: string; value: string }): void
/**
* Remove a query parameter by key
* @param key - Parameter key to remove
*/
remove(key: string): void
/**
* Update an existing parameter or add a new one
* @param param - Parameter to upsert
*/
upsert(param: { key: string; value: string }): void
/**
* Remove all query parameters
*/
clear(): void
/**
* Insert a query parameter before another parameter
* @param item - Parameter to insert
* @param before - String key or parameter object to insert before
*/
insert(
item: { key: string; value: string },
before: string | { key: string; value: string }
): void
/**
* Move a parameter to the end or append a new one
* @param item - Parameter to append
*/
append(item: { key: string; value: string }): void
/**
* Merge parameters from an array or object
* @param source - Array of parameters or key-value object
* @param prune - If true, remove parameters not in source
*/
assimilate(
source:
| Array<{ key: string; value: string }>
| Record<string, string>,
prune?: boolean
): void
}
}
// Method - Mutable
method: string
// Headers - With Postman mutation methods
headers: {
// Read methods
/**
* Get the value of a header by name (case-insensitive)
* @param name - Header name to retrieve
* @returns Header value or null if not found
*/
get(name: string): string | null
/**
* Check if a header exists (case-insensitive)
* @param name - Header name to check
* @returns true if header exists, false otherwise
*/
has(name: string): boolean
all(): HoppRESTHeader[]
}>
readonly body: any
readonly auth: any
}>
/**
* Get all headers as a key-value object
* @returns Object with all headers (keys in lowercase)
*/
all(): Record<string, string>
/**
* Convert headers to object (alias for all())
* @returns Object with all headers (keys in lowercase)
*/
toObject(): Record<string, string>
/**
* Get the number of headers
* @returns Count of headers
*/
count(): number
/**
* Get a header by index
* @param index - Zero-based index
* @returns Header object or null if out of bounds
*/
idx(index: number): { key: string; value: string } | null
// Iteration methods
/**
* Iterate over all headers
* @param callback - Function to call for each header
*/
each(callback: (header: { key: string; value: string }) => void): void
/**
* Map headers to a new array
* @param callback - Transform function
* @returns Array of transformed values
*/
map<T>(callback: (header: { key: string; value: string }) => T): T[]
/**
* Filter headers
* @param callback - Predicate function
* @returns Array of headers matching the predicate
*/
filter(
callback: (header: { key: string; value: string }) => boolean
): Array<{ key: string; value: string }>
/**
* Find a header by string key or function predicate (case-insensitive)
* @param rule - String key or predicate function
* @param context - Optional context to bind the predicate function
* @returns Matching header or null if not found
*/
find(
rule: string | ((header: { key: string; value: string }) => boolean),
context?: any
): { key: string; value: string } | null
/**
* Get the index of a header (case-insensitive)
* @param item - String key or header object to find
* @returns Index of header, or -1 if not found
*/
indexOf(item: string | { key: string; value: string }): number
// Mutation methods
/**
* Add a new header
* @param header - Header to add
*/
add(header: { key: string; value: string }): void
/**
* Remove a header by name (case-insensitive)
* @param headerName - Header name to remove
*/
remove(headerName: string): void
/**
* Update an existing header or add a new one
* @param header - Header to upsert
*/
upsert(header: { key: string; value: string }): void
/**
* Remove all headers
*/
clear(): void
/**
* Insert a header before another header
* @param item - Header to insert
* @param before - String key or header object to insert before
*/
insert(
item: { key: string; value: string },
before: string | { key: string; value: string }
): void
/**
* Move a header to the end or append a new one
* @param item - Header to append
*/
append(item: { key: string; value: string }): void
/**
* Merge headers from an array or object (case-insensitive)
* @param source - Array of headers or key-value object
* @param prune - If true, remove headers not in source
*/
assimilate(
source: Array<{ key: string; value: string }> | Record<string, string>,
prune?: boolean
): void
}
// Body - With Postman update() method
// Uses HoppRESTReqBody for type safety with Postman's update() extension
body: HoppRESTReqBody & {
update(
body:
| string
| {
mode?: "raw" | "urlencoded" | "formdata" | "file"
raw?: string
urlencoded?: Array<{ key: string; value: string }>
formdata?: Array<{ key: string; value: string | File }>
file?: File
options?: {
raw?: {
language?: "json" | "text" | "html" | "xml"
}
}
}
): void
}
// Auth - Mutable with proper type safety
auth: HoppRESTAuth
}
const info: Readonly<{
readonly eventName: "pre-request"
@ -393,27 +817,88 @@ declare namespace pm {
}>
const sendRequest: () => never
/**
* Collection variables (unsupported - Workspace feature)
* Collection variables are not supported in Hoppscotch as they are a Postman Workspace feature
*/
const collectionVariables: Readonly<{
get(): never
set(): never
unset(): never
has(): never
get(key: string): never
set(key: string, value: string): never
unset(key: string): never
has(key: string): never
clear(): never
toObject(): never
replaceIn(template: string): never
}>
/**
* Postman Vault (unsupported)
* Vault is not supported in Hoppscotch as it is a Postman-specific feature
*/
const vault: Readonly<{
get(): never
set(): never
unset(): never
get(key: string): never
set(key: string, value: string): never
unset(key: string): never
}>
/**
* Iteration data (unsupported - Collection Runner feature)
* Iteration data is not supported in Hoppscotch as it requires Collection Runner
*/
const iterationData: Readonly<{
get(): never
set(): never
unset(): never
has(): never
get(key: string): never
set(key: string, value: string): never
unset(key: string): never
has(key: string): never
toObject(): never
toJSON(): never
}>
/**
* Visualizer API (unsupported)
* The Postman Visualizer allows you to present response data as HTML templates with styling.
* This feature is not supported in Hoppscotch as it requires a browser-based visualization UI.
* @see https://learning.postman.com/docs/sending-requests/response-data/visualizer/
*/
const visualizer: Readonly<{
/**
* Set a Handlebars template to visualize response data (unsupported)
* @param layout - HTML template string with Handlebars syntax
* @param data - Data object to pass to the template
* @param options - Optional configuration object
* @throws Error - Visualizer is not supported in Hoppscotch
*/
set(
layout: string,
data?: Record<string, any>,
options?: Record<string, any>
): never
/**
* Clear the current visualization (unsupported)
* @throws Error - Visualizer is not supported in Hoppscotch
*/
clear(): never
}>
/**
* Execution control
*/
const execution: Readonly<{
setNextRequest(): never
/**
* Set next request to execute (unsupported - Collection Runner feature)
* @param requestNameOrId - Name or ID of the next request
*/
setNextRequest(requestNameOrId: string | null): never
/**
* Skip current request execution (unsupported - Collection Runner feature)
*/
skipRequest(): never
/**
* Run a request (unsupported - Collection Runner feature)
* @param requestNameOrId - Name or ID of the request to run
*/
runRequest(requestNameOrId: string): never
}>
}

View file

@ -53,6 +53,7 @@
"dependencies": {
"@hoppscotch/data": "workspace:^",
"@types/lodash-es": "4.17.12",
"chai": "6.2.0",
"faraday-cage": "0.1.0",
"fp-ts": "2.16.11",
"lodash": "4.17.21",
@ -61,6 +62,7 @@
"devDependencies": {
"@digitak/esrun": "3.2.26",
"@relmify/jest-fp-ts": "2.1.1",
"@types/chai": "5.2.2",
"@types/jest": "30.0.0",
"@types/lodash": "4.17.20",
"@types/node": "24.9.1",

View file

@ -0,0 +1,149 @@
/**
* Async/Await Support Tests
*
* Tests that pm.test() and hopp.test() properly support async functions with await,
* which is critical for Postman script imports that use asynchronous patterns.
*/
import { describe, expect, test } from "vitest"
import { runTest } from "~/utils/test-helpers"
const NAMESPACES = ["pm", "hopp"] as const
describe.each(NAMESPACES)("%s.test() - Async/Await Support", (namespace) => {
test("should support async function with await", () => {
return expect(
runTest(`
${namespace}.test("async with await", async function() {
const promise = new Promise((resolve) => {
resolve(42)
})
const result = await promise
${namespace}.expect(result).to.equal(42)
})
`)()
).resolves.toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
])
)
})
test("should support async arrow function", () => {
return expect(
runTest(`
${namespace}.test("async arrow", async () => {
const result = await Promise.resolve("success")
${namespace}.expect(result).to.equal("success")
})
`)()
).resolves.toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
])
)
})
test("should support Promise.all with await", () => {
return expect(
runTest(`
${namespace}.test("Promise.all", async function() {
const results = await Promise.all([
Promise.resolve(1),
Promise.resolve(2),
Promise.resolve(3)
])
${namespace}.expect(results).to.eql([1, 2, 3])
})
`)()
).resolves.toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
])
)
})
test("should support async error handling", () => {
return expect(
runTest(`
${namespace}.test("async error", async function() {
try {
await Promise.reject(new Error("test error"))
${namespace}.expect.fail("Should not reach here")
} catch (error) {
${namespace}.expect(error.message).to.equal("test error")
}
})
`)()
).resolves.toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
])
)
})
test("should support multiple sequential awaits", () => {
return expect(
runTest(`
${namespace}.test("sequential awaits", async function() {
const a = await Promise.resolve(10)
const b = await Promise.resolve(20)
const c = await Promise.resolve(30)
${namespace}.expect(a + b + c).to.equal(60)
})
`)()
).resolves.toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
])
)
})
test("should support async IIFE pattern", () => {
return expect(
runTest(`
${namespace}.test("async IIFE", async function() {
const result = await (async () => {
const data = await Promise.resolve({ value: 100 })
return data.value * 2
})()
${namespace}.expect(result).to.equal(200)
})
`)()
).resolves.toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
])
)
})
})

View file

@ -0,0 +1,607 @@
/**
* @see https://github.com/hoppscotch/hoppscotch/discussions/5221
*/
import { describe, expect, test } from "vitest"
import { runTest } from "~/utils/test-helpers"
const NAMESPACES = ["pm", "hopp"] as const
describe.each(NAMESPACES)("%s.expect() - Core Chai Assertions", (namespace) => {
describe("Equality Assertions", () => {
test("should support `.equal()` for strict equality", () => {
return expect(
runTest(`
${namespace}.test("equality assertions", () => {
${namespace}.expect(42).to.equal(42)
${namespace}.expect('test').to.equal('test')
${namespace}.expect(true).to.equal(true)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "equality assertions",
expectResults: [
{ status: "pass", message: "Expected 42 to equal 42" },
{ status: "pass", message: "Expected 'test' to equal 'test'" },
{ status: "pass", message: "Expected true to equal true" },
],
}),
],
}),
])
})
test("should support `.eql()` for deep equality", () => {
return expect(
runTest(`
${namespace}.test("deep equality", () => {
${namespace}.expect({a: 1}).to.eql({a: 1})
${namespace}.expect([1, 2, 3]).to.eql([1, 2, 3])
${namespace}.expect({nested: {value: 42}}).to.eql({nested: {value: 42}})
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "deep equality",
expectResults: [
{ status: "pass", message: "Expected {a: 1} to eql {a: 1}" },
{
status: "pass",
message: "Expected [1, 2, 3] to eql [1, 2, 3]",
},
{ status: "pass", message: expect.stringContaining("to eql") },
],
}),
],
}),
])
})
})
describe("Type Assertions", () => {
test("should assert primitive types with `.a()` and `.an()`", () => {
return expect(
runTest(`
${namespace}.test("type assertions", () => {
${namespace}.expect('foo').to.be.a('string')
${namespace}.expect({a: 1}).to.be.an('object')
${namespace}.expect([1, 2, 3]).to.be.an('array')
${namespace}.expect(42).to.be.a('number')
${namespace}.expect(true).to.be.a('boolean')
${namespace}.expect(null).to.be.a('null')
${namespace}.expect(undefined).to.be.an('undefined')
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "type assertions",
expectResults: [
{ status: "pass", message: "Expected 'foo' to be a string" },
{ status: "pass", message: "Expected {a: 1} to be an object" },
{
status: "pass",
message: "Expected [1, 2, 3] to be an array",
},
{ status: "pass", message: "Expected 42 to be a number" },
{ status: "pass", message: "Expected true to be a boolean" },
{ status: "pass", message: "Expected null to be a null" },
{
status: "pass",
message: "Expected undefined to be an undefined",
},
],
}),
],
}),
])
})
})
describe("Truthiness Assertions", () => {
test("should support `.true`, `.false`, `.ok` assertions", () => {
return expect(
runTest(`
${namespace}.test("truthiness", () => {
${namespace}.expect(true).to.be.true
${namespace}.expect(false).to.be.false
${namespace}.expect(1).to.be.ok
${namespace}.expect('hello').to.be.ok
${namespace}.expect(0).to.not.be.ok
${namespace}.expect('').to.not.be.ok
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "truthiness",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
})
describe("Numerical Comparisons", () => {
test("should support `.above()` and `.below()` comparisons", () => {
return expect(
runTest(`
${namespace}.test("numerical comparisons", () => {
${namespace}.expect(10).to.be.above(5)
${namespace}.expect(5).to.be.below(10)
${namespace}.expect(5).to.not.be.above(10)
${namespace}.expect(10).to.not.be.below(5)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "numerical comparisons",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
test("should support `.within()` for range comparisons", () => {
return expect(
runTest(`
${namespace}.test("within range", () => {
${namespace}.expect(5).to.be.within(1, 10)
${namespace}.expect(1).to.be.within(1, 10)
${namespace}.expect(10).to.be.within(1, 10)
${namespace}.expect(0).to.not.be.within(1, 10)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "within range",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
test("should support `.closeTo()` for floating point comparisons", () => {
return expect(
runTest(`
${namespace}.test("close to", () => {
${namespace}.expect(1.5).to.be.closeTo(1.0, 0.6)
${namespace}.expect(10.5).to.be.closeTo(10.0, 0.5)
${namespace}.expect(5.1).to.not.be.closeTo(5.0, 0.05)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "close to",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
})
describe("Property Assertions", () => {
test("should support `.property()` for property existence and value", () => {
return expect(
runTest(`
${namespace}.test("property assertions", () => {
${namespace}.expect({a: 1}).to.have.property('a')
${namespace}.expect({a: 1}).to.have.property('a', 1)
${namespace}.expect({x: {y: 2}}).to.have.property('x')
${namespace}.expect({}).to.not.have.property('missing')
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "property assertions",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
test("should support `.ownProperty()` for own properties", () => {
return expect(
runTest(`
${namespace}.test("own property", () => {
${namespace}.expect({a: 1}).to.have.ownProperty('a')
${namespace}.expect({a: 1}).to.have.ownProperty('a', 1)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "own property",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
})
describe("Collection Assertions", () => {
test("should support `.include()` for arrays and strings", () => {
return expect(
runTest(`
${namespace}.test("include assertions", () => {
${namespace}.expect([1, 2, 3]).to.include(2)
${namespace}.expect('hoppscotch').to.include('hopp')
${namespace}.expect({a: 1, b: 2}).to.include({a: 1})
${namespace}.expect([1, 2, 3]).to.not.include(5)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "include assertions",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
})
describe("Negation with `.not`", () => {
test("should support negation for all assertion types", () => {
return expect(
runTest(`
${namespace}.test("negation works", () => {
${namespace}.expect(42).to.not.equal(43)
${namespace}.expect('foo').to.not.be.a('number')
${namespace}.expect(false).to.not.be.true
${namespace}.expect(5).to.not.be.above(10)
${namespace}.expect({a: 1}).to.not.have.property('b')
${namespace}.expect([1, 2]).to.not.include(3)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "negation works",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
})
describe("Deep Equality Modifiers", () => {
test("should support `.deep.equal()` and `.deep.property()`", () => {
return expect(
runTest(`
${namespace}.test("deep modifiers", () => {
${namespace}.expect({a: {b: 1}}).to.deep.equal({a: {b: 1}})
${namespace}.expect({a: {b: 1}}).to.have.deep.property('a', {b: 1})
${namespace}.expect([{x: 1}]).to.deep.include({x: 1})
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "deep modifiers",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
})
describe("Language Chains", () => {
test("should support basic language chain properties (`.to`, `.be`, `.that`)", () => {
return expect(
runTest(`
${namespace}.test("basic language chains", () => {
${namespace}.expect(2).to.equal(2)
${namespace}.expect(2).to.be.equal(2)
${namespace}.expect(2).to.be.a('number').that.equals(2)
${namespace}.expect([1,2,3]).to.be.an('array').that.has.lengthOf(3)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "basic language chains",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
test("should chain type assertion with include using `.and`", () => {
return expect(
runTest(`
${namespace}.test("and chain - type and include", () => {
${namespace}.expect('hello world').to.be.a('string').and.include('world')
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "and chain - type and include",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
test("should chain type assertion with lengthOf using `.and`", () => {
return expect(
runTest(`
${namespace}.test("and chain - type and length", () => {
${namespace}.expect([1, 2, 3]).to.be.an('array').and.have.lengthOf(3)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "and chain - type and length",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
test("should support complex chaining with multiple `.and` and `.that`", () => {
return expect(
runTest(`
${namespace}.test("complex and chaining", () => {
${namespace}.expect({ name: 'John', age: 30 })
.to.be.an('object')
.and.have.property('name')
.that.is.a('string')
.and.equals('John')
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "complex and chaining",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
test("should work with `.and.not` for negation", () => {
return expect(
runTest(`
${namespace}.test("and with negation", () => {
${namespace}.expect({ a: 1, b: 2 })
.to.be.an('object')
.and.not.be.empty
.and.not.have.property('c')
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "and with negation",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
test("should chain numeric comparisons with `.and`", () => {
return expect(
runTest(`
${namespace}.test("and with numbers", () => {
${namespace}.expect(200)
.to.be.a('number')
.and.be.within(200, 299)
.and.equal(200)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "and with numbers",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
})
})
describe.each(NAMESPACES)(
"%s.expect() - Basic Function Assertions",
(namespace) => {
test("should support `.throw()` for error throwing", () => {
return expect(
runTest(`
${namespace}.test("throw assertions", () => {
${namespace}.expect(() => { throw new Error('oops') }).to.throw()
${namespace}.expect(() => { throw new Error('oops') }).to.throw(Error)
${namespace}.expect(() => {}).to.not.throw()
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "throw assertions",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
test("should support `.respondTo()` for method existence", () => {
return expect(
runTest(`
${namespace}.test("respondTo assertions", () => {
const obj = { method: () => {} }
${namespace}.expect(obj).to.respondTo('method')
${namespace}.expect([]).to.respondTo('push')
${namespace}.expect('').to.respondTo('charAt')
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "respondTo assertions",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
test("should support `.satisfy()` for custom predicates", () => {
return expect(
runTest(`
${namespace}.test("satisfy assertions", () => {
${namespace}.expect(2).to.satisfy((n) => n > 0)
${namespace}.expect(10).to.satisfy((n) => n % 2 === 0)
${namespace}.expect('hello').to.satisfy((s) => s.length > 3)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "satisfy assertions",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
}
)

View file

@ -0,0 +1,364 @@
import { describe, expect, test } from "vitest"
import { runTest } from "~/utils/test-helpers"
const NAMESPACES = ["pm", "hopp"] as const
describe.each(NAMESPACES)(
"%s.expect() - deep.include() - Object Property Inclusion",
(namespace) => {
test("should pass when object deeply includes partial match", async () => {
const testScript = `
${namespace}.test("deep.include() - object property inclusion", (namespace) => {
${namespace}.expect({ a: 1, b: 2, c: 3 }).to.deep.include({ a: 1, b: 2 });
});
`
const result = await runTest(testScript)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "deep.include() - object property inclusion",
expectResults: [expect.objectContaining({ status: "pass" })],
}),
]),
}),
])
)
})
test("should pass with negation when object does not include properties", async () => {
const testScript = `
${namespace}.test("deep.include() - negated", (namespace) => {
${namespace}.expect({ a: 1, b: 2 }).to.not.deep.include({ a: 1, c: 3 });
});
`
const result = await runTest(testScript)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "deep.include() - negated",
expectResults: [expect.objectContaining({ status: "pass" })],
}),
]),
}),
])
)
})
test("should work with nested objects", async () => {
const testScript = `
${namespace}.test("deep.include() - nested objects", (namespace) => {
const obj = { a: { b: { c: 1 } }, d: 2 };
${namespace}.expect(obj).to.deep.include({ a: { b: { c: 1 } } });
});
`
const result = await runTest(testScript)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "deep.include() - nested objects",
expectResults: [expect.objectContaining({ status: "pass" })],
}),
]),
}),
])
)
})
}
)
describe.each(NAMESPACES)(
"%s.expect() - deep.include() - Array Object Inclusion",
(namespace) => {
test("should pass when array deeply includes object", async () => {
const testScript = `
${namespace}.test("deep.include() - array object inclusion", (namespace) => {
${namespace}.expect([{ id: 1 }, { id: 2 }]).to.deep.include({ id: 1 });
});
`
const result = await runTest(testScript)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "deep.include() - array object inclusion",
expectResults: [expect.objectContaining({ status: "pass" })],
}),
]),
}),
])
)
})
test("should pass with negation when array does not include object", async () => {
const testScript = `
${namespace}.test("deep.include() - array negated", (namespace) => {
${namespace}.expect([{ id: 1 }, { id: 2 }]).to.not.deep.include({ id: 3 });
});
`
const result = await runTest(testScript)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "deep.include() - array negated",
expectResults: [expect.objectContaining({ status: "pass" })],
}),
]),
}),
])
)
})
test("should work with exact nested objects in arrays", async () => {
const testScript = `
${namespace}.test("deep.include() - complex array objects", (namespace) => {
const arr = [
{ name: 'John', age: 30, address: { city: 'NYC' } },
{ name: 'Jane', age: 25, address: { city: 'LA' } }
];
// deep.include on arrays requires exact object match
${namespace}.expect(arr).to.deep.include({ name: 'John', age: 30, address: { city: 'NYC' } });
});
`
const result = await runTest(testScript)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "deep.include() - complex array objects",
expectResults: [expect.objectContaining({ status: "pass" })],
}),
]),
}),
])
)
})
}
)
describe.each(NAMESPACES)(
"%s.expect() - include.deep() - Alternative Syntax",
(namespace) => {
test("should work with include.deep syntax", async () => {
const testScript = `
${namespace}.test("include.deep() - alternative syntax", (namespace) => {
${namespace}.expect({ a: 1, b: 2, c: 3 }).to.include.deep({ a: 1, b: 2 });
});
`
const result = await runTest(testScript)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "include.deep() - alternative syntax",
expectResults: [expect.objectContaining({ status: "pass" })],
}),
]),
}),
])
)
})
}
)
describe.each(NAMESPACES)(
"%s.expect() - include.keys() - Partial Key Matching",
(namespace) => {
test("should pass when object has at least the specified keys", async () => {
const testScript = `
${namespace}.test("include.keys() - partial key matching", (namespace) => {
${namespace}.expect({ a: 1, b: 2, c: 3 }).to.include.keys('a', 'b');
});
`
const result = await runTest(testScript)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "include.keys() - partial key matching",
expectResults: [expect.objectContaining({ status: "pass" })],
}),
]),
}),
])
)
})
test("should pass even when object has extra keys", async () => {
const testScript = `
${namespace}.test("include.keys() - with extra keys", (namespace) => {
${namespace}.expect({ a: 1, b: 2, c: 3, d: 4, e: 5 }).to.include.keys('a', 'b');
});
`
const result = await runTest(testScript)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "include.keys() - with extra keys",
expectResults: [expect.objectContaining({ status: "pass" })],
}),
]),
}),
])
)
})
test("should pass with negation when object does not have key", async () => {
const testScript = `
${namespace}.test("include.keys() - negated", (namespace) => {
${namespace}.expect({ a: 1, b: 2 }).to.not.include.keys('c');
});
`
const result = await runTest(testScript)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "include.keys() - negated",
expectResults: [expect.objectContaining({ status: "pass" })],
}),
]),
}),
])
)
})
test("should work with array of keys", async () => {
const testScript = `
${namespace}.test("include.keys() - array syntax", (namespace) => {
${namespace}.expect({ a: 1, b: 2, c: 3 }).to.include.keys(['a', 'b']);
});
`
const result = await runTest(testScript)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "include.keys() - array syntax",
expectResults: [expect.objectContaining({ status: "pass" })],
}),
]),
}),
])
)
})
test("should work with single key", async () => {
const testScript = `
${namespace}.test("include.keys() - single key", (namespace) => {
${namespace}.expect({ a: 1, b: 2, c: 3 }).to.include.keys('a');
});
`
const result = await runTest(testScript)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "include.keys() - single key",
expectResults: [expect.objectContaining({ status: "pass" })],
}),
]),
}),
])
)
})
}
)
describe.each(NAMESPACES)("%s.expect() - Combination Patterns", (namespace) => {
test("should work with chained assertions", async () => {
const testScript = `
${namespace}.test("chained deep.include", (namespace) => {
const response = { status: 'success', data: { user: { id: 1, name: 'John' } } };
${namespace}.expect(response).to.be.an('object')
.and.deep.include({ status: 'success' });
});
`
const result = await runTest(testScript)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "chained deep.include",
expectResults: [
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
],
}),
]),
}),
])
)
})
test("should work with response validation patterns", async () => {
const testScript = `
${namespace}.test("response validation with deep.include", (namespace) => {
const jsonData = { status: 200, message: 'OK', data: { results: [] } };
${namespace}.expect(jsonData).to.deep.include({ status: 200, message: 'OK' });
${namespace}.expect(jsonData).to.include.keys('status', 'message', 'data');
});
`
const result = await runTest(testScript)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "response validation with deep.include",
expectResults: [
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
],
}),
]),
}),
])
)
})
})

View file

@ -0,0 +1,503 @@
import { describe, expect, test } from "vitest"
import { runTest } from "~/utils/test-helpers"
const NAMESPACES = ["pm", "hopp"] as const
describe.each(NAMESPACES)(
"%s.expect() - instanceof assertions - Built-in types",
(namespace) => {
test("should support instanceof Object for plain objects", async () => {
const testScript = `
${namespace}.test("instanceof Object", () => {
const obj = { key: "value" };
${namespace}.expect(obj).to.be.an.instanceof(Object);
${namespace}.expect(obj).to.be.instanceOf(Object);
});
`
const result = await runTest(testScript, { global: [], selected: [] })()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "instanceof Object",
expectResults: [
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
],
}),
]),
}),
])
)
})
test("should support instanceof Array", async () => {
const testScript = `
${namespace}.test("instanceof Array", () => {
const arr = [1, 2, 3];
${namespace}.expect(arr).to.be.an.instanceof(Array);
${namespace}.expect(arr).to.be.instanceOf(Array);
});
`
const result = await runTest(testScript, { global: [], selected: [] })()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "instanceof Array",
expectResults: [
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
],
}),
]),
}),
])
)
})
test("should support instanceof Date", async () => {
const testScript = `
${namespace}.test("instanceof Date", () => {
const date = new Date();
${namespace}.expect(date).to.be.an.instanceof(Date);
});
`
const result = await runTest(testScript, { global: [], selected: [] })()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "instanceof Date",
expectResults: [expect.objectContaining({ status: "pass" })],
}),
]),
}),
])
)
})
test("should support instanceof RegExp", async () => {
const testScript = `
${namespace}.test("instanceof RegExp", () => {
const regex = /test/;
${namespace}.expect(regex).to.be.an.instanceof(RegExp);
});
`
const result = await runTest(testScript, { global: [], selected: [] })()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "instanceof RegExp",
expectResults: [expect.objectContaining({ status: "pass" })],
}),
]),
}),
])
)
})
test("should support instanceof Error", async () => {
const testScript = `
${namespace}.test("instanceof Error", () => {
const err = new Error("test error");
${namespace}.expect(err).to.be.an.instanceof(Error);
});
`
const result = await runTest(testScript, { global: [], selected: [] })()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "instanceof Error",
expectResults: [expect.objectContaining({ status: "pass" })],
}),
]),
}),
])
)
})
test("should support arrays as instanceof Object", async () => {
const testScript = `
${namespace}.test("array instanceof Object", () => {
const arr = [1, 2, 3];
${namespace}.expect(arr).to.be.an.instanceof(Object);
});
`
const result = await runTest(testScript, { global: [], selected: [] })()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "array instanceof Object",
expectResults: [expect.objectContaining({ status: "pass" })],
}),
]),
}),
])
)
})
}
)
describe.each(NAMESPACES)(
"%s.expect() - instanceof assertions - Custom classes",
(namespace) => {
test("should support instanceof with custom class definitions", async () => {
const testScript = `
${namespace}.test("custom class instanceof", () => {
class Person {
constructor(name) {
this.name = name;
}
}
const john = new Person("John");
${namespace}.expect(john).to.be.an.instanceof(Person);
});
`
const result = await runTest(testScript, { global: [], selected: [] })()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "custom class instanceof",
expectResults: [expect.objectContaining({ status: "pass" })],
}),
]),
}),
])
)
})
test("should support instanceof with ES6 class syntax", async () => {
const testScript = `
${namespace}.test("ES6 class instanceof", () => {
class Animal {
constructor(type) {
this.type = type;
}
}
class Dog extends Animal {
constructor(name) {
super("dog");
this.name = name;
}
}
const rover = new Dog("Rover");
${namespace}.expect(rover).to.be.an.instanceof(Dog);
${namespace}.expect(rover).to.be.an.instanceof(Animal);
${namespace}.expect(rover).to.be.an.instanceof(Object);
});
`
const result = await runTest(testScript, { global: [], selected: [] })()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "ES6 class instanceof",
expectResults: [
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
],
}),
]),
}),
])
)
})
test("should support instanceof with constructor functions", async () => {
const testScript = `
${namespace}.test("constructor function instanceof", () => {
function Car(make, model) {
this.make = make;
this.model = model;
}
const tesla = new Car("Tesla", "Model 3");
${namespace}.expect(tesla).to.be.an.instanceof(Car);
${namespace}.expect(tesla).to.be.an.instanceof(Object);
});
`
const result = await runTest(testScript, { global: [], selected: [] })()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "constructor function instanceof",
expectResults: [
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
],
}),
]),
}),
])
)
})
test("should work with custom classes in response data context", async () => {
const testScript = `
${namespace}.test("custom class with response data", () => {
class ResponseData {
constructor(data) {
this.data = data;
}
}
const responseData = pm.response.json();
const wrapped = new ResponseData(responseData);
${namespace}.expect(wrapped).to.be.an.instanceof(ResponseData);
${namespace}.expect(wrapped).to.be.an.instanceof(Object);
});
`
const mockResponse = {
status: 200,
statusText: "OK",
responseTime: 100,
headers: [],
body: { test: "data" },
}
const result = await runTest(
testScript,
{ global: [], selected: [] },
mockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "custom class with response data",
expectResults: [
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
],
}),
]),
}),
])
)
})
}
)
describe.each(NAMESPACES)(
"%s.expect() - instanceof assertions - Custom classes with other assertions",
(namespace) => {
test("should work with custom classes in property assertions", async () => {
const testScript = `
${namespace}.test("custom class with properties", () => {
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
const user = new User("Alice", 30);
${namespace}.expect(user).to.be.an.instanceof(User);
${namespace}.expect(user).to.have.property("name", "Alice");
${namespace}.expect(user).to.have.property("age", 30);
});
`
const result = await runTest(testScript, { global: [], selected: [] })()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "custom class with properties",
expectResults: [
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
],
}),
]),
}),
])
)
})
test("should work with custom classes in array assertions", async () => {
const testScript = `
${namespace}.test("array of custom class instances", () => {
class Item {
constructor(id) {
this.id = id;
}
}
const items = [new Item(1), new Item(2), new Item(3)];
${namespace}.expect(items).to.be.an.instanceof(Array);
${namespace}.expect(items).to.have.lengthOf(3);
${namespace}.expect(items[0]).to.be.an.instanceof(Item);
${namespace}.expect(items[1]).to.be.an.instanceof(Item);
});
`
const result = await runTest(testScript, { global: [], selected: [] })()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "array of custom class instances",
expectResults: [
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
],
}),
]),
}),
])
)
})
test("should work with custom classes and deep equality", async () => {
const testScript = `
${namespace}.test("custom class deep equality", () => {
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
const p1 = new Point(10, 20);
const p2 = new Point(10, 20);
${namespace}.expect(p1).to.be.an.instanceof(Point);
${namespace}.expect(p2).to.be.an.instanceof(Point);
${namespace}.expect(p1).to.deep.equal(p2);
});
`
const result = await runTest(testScript, { global: [], selected: [] })()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "custom class deep equality",
expectResults: [
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
],
}),
]),
}),
])
)
})
}
)
describe.each(NAMESPACES)(
"%s.expect() - instanceof assertions - Negation and failure cases",
(namespace) => {
test("should support negation with .not.instanceof", async () => {
const testScript = `
${namespace}.test("negated instanceof", () => {
const str = "hello";
const num = 42;
const arr = [1, 2, 3];
${namespace}.expect(str).to.not.be.an.instanceof(Number);
${namespace}.expect(num).to.not.be.an.instanceof(String);
${namespace}.expect(arr).to.not.be.an.instanceof(String);
});
`
const result = await runTest(testScript, { global: [], selected: [] })()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "negated instanceof",
expectResults: [
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
],
}),
]),
}),
])
)
})
test("should fail when instanceof check fails", async () => {
const testScript = `
${namespace}.test("instanceof failure", () => {
const str = "hello";
${namespace}.expect(str).to.be.an.instanceof(Array);
});
`
const result = await runTest(testScript, { global: [], selected: [] })()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "instanceof failure",
expectResults: [expect.objectContaining({ status: "fail" })],
}),
]),
}),
])
)
})
}
)

View file

@ -0,0 +1,443 @@
/**
* @see https://github.com/hoppscotch/hoppscotch/issues/5489
*/
import { describe, expect, test } from "vitest"
import { runTest } from "~/utils/test-helpers"
const NAMESPACES = ["pm", "hopp"] as const
describe.each(NAMESPACES)("%s.expect() - Keys Assertions", (namespace) => {
describe("keys() method", () => {
test("should accept array syntax: keys(['a', 'b'])", () => {
return expect(
runTest(`
${namespace}.test('Keys with array syntax', function () {
${namespace}.expect({a: 1, b: 2}).to.have.keys(['a','b']);
})
`)()
).resolves.toEqualRight([
{
descriptor: "root",
expectResults: [],
children: [
{
descriptor: "Keys with array syntax",
expectResults: [
{
status: "pass",
message: "Expected {a: 1, b: 2} to have keys 'a', 'b'",
},
],
children: [],
},
],
},
])
})
test("should accept spread syntax: keys('a', 'b')", () => {
return expect(
runTest(`
${namespace}.test('Keys with spread syntax', function () {
${namespace}.expect({a: 1, b: 2}).to.have.keys('a','b');
})
`)()
).resolves.toEqualRight([
{
descriptor: "root",
expectResults: [],
children: [
{
descriptor: "Keys with spread syntax",
expectResults: [
{
status: "pass",
message: "Expected {a: 1, b: 2} to have keys 'a', 'b'",
},
],
children: [],
},
],
},
])
})
test("should support negation with array syntax", () => {
return expect(
runTest(`
${namespace}.test('Negated keys with array', function () {
${namespace}.expect({a: 1}).to.not.have.keys(['b','c']);
})
`)()
).resolves.toEqualRight([
{
descriptor: "root",
expectResults: [],
children: [
{
descriptor: "Negated keys with array",
expectResults: [
{
status: "pass",
message: "Expected {a: 1} to not have keys 'b', 'c'",
},
],
children: [],
},
],
},
])
})
test("should support negation with spread syntax", () => {
return expect(
runTest(`
${namespace}.test('Negated keys with spread', function () {
${namespace}.expect({x:5}).to.not.have.keys('y','z');
})
`)()
).resolves.toEqualRight([
{
descriptor: "root",
expectResults: [],
children: [
{
descriptor: "Negated keys with spread",
expectResults: [
{
status: "pass",
message: "Expected {x: 5} to not have keys 'y', 'z'",
},
],
children: [],
},
],
},
])
})
})
describe("key() singular method", () => {
test("should accept single string argument", () => {
return expect(
runTest(`
${namespace}.test('Single key check', function () {
${namespace}.expect({name: 'test'}).to.have.key('name');
})
`)()
).resolves.toEqualRight([
{
descriptor: "root",
expectResults: [],
children: [
{
descriptor: "Single key check",
expectResults: [
{
status: "pass",
message: "Expected {name: 'test'} to have keys 'name'",
},
],
children: [],
},
],
},
])
})
test("should support negation: not.have.key('z')", () => {
return expect(
runTest(`
${namespace}.test('Negated key assertion', function () {
${namespace}.expect({x:5}).to.not.have.key('z');
})
`)()
).resolves.toEqualRight([
{
descriptor: "root",
expectResults: [],
children: [
{
descriptor: "Negated key assertion",
expectResults: [
{
status: "pass",
message: "Expected {x: 5} to not have keys 'z'",
},
],
children: [],
},
],
},
])
})
})
})
describe.each(NAMESPACES)("%s.expect() - Members Assertions", (namespace) => {
describe("members() method", () => {
test("should match members in any order", () => {
return expect(
runTest(`
${namespace}.test('Members matching', function () {
${namespace}.expect([1,2,3]).to.have.members([3,2,1]);
})
`)()
).resolves.toEqualRight([
{
descriptor: "root",
expectResults: [],
children: [
{
descriptor: "Members matching",
expectResults: [
{
status: "pass",
message: "Expected [1, 2, 3] to have members [3, 2, 1]",
},
],
children: [],
},
],
},
])
})
test("should support negation", () => {
return expect(
runTest(`
${namespace}.test('Negated members', function () {
${namespace}.expect([1,2,3]).to.not.have.members([4,5,6]);
})
`)()
).resolves.toEqualRight([
{
descriptor: "root",
expectResults: [],
children: [
{
descriptor: "Negated members",
expectResults: [
{
status: "pass",
message: "Expected [1, 2, 3] to not have members [4, 5, 6]",
},
],
children: [],
},
],
},
])
})
test("should fail when members don't match", () => {
return expect(
runTest(`
${namespace}.test('Members mismatch', function () {
${namespace}.expect([1,2]).to.have.members([1,2,3]);
})
`)()
).resolves.toEqualRight([
{
descriptor: "root",
expectResults: [],
children: [
{
descriptor: "Members mismatch",
expectResults: [
{
status: "fail",
message: expect.stringContaining("to have members"),
},
],
children: [],
},
],
},
])
})
})
describe("include.members() method", () => {
test("should match subset of members", () => {
return expect(
runTest(`
${namespace}.test('Include members', function () {
${namespace}.expect([1,2,3]).to.include.members([2]);
})
`)()
).resolves.toEqualRight([
{
descriptor: "root",
expectResults: [],
children: [
{
descriptor: "Include members",
expectResults: [
{
status: "pass",
message: "Expected [1, 2, 3] to include members [2]",
},
],
children: [],
},
],
},
])
})
test("should match multiple subset members", () => {
return expect(
runTest(`
${namespace}.test('Multiple include members', function () {
${namespace}.expect([1,2,3,4,5]).to.include.members([2,4]);
})
`)()
).resolves.toEqualRight([
{
descriptor: "root",
expectResults: [],
children: [
{
descriptor: "Multiple include members",
expectResults: [
{
status: "pass",
message: "Expected [1, 2, 3, 4, 5] to include members [2, 4]",
},
],
children: [],
},
],
},
])
})
test("should support negation: not.include.members([5])", () => {
return expect(
runTest(`
${namespace}.test('Negated include.members', function () {
${namespace}.expect([1,2,3]).to.not.include.members([5]);
})
`)()
).resolves.toEqualRight([
{
descriptor: "root",
expectResults: [],
children: [
{
descriptor: "Negated include.members",
expectResults: [
{
status: "pass",
message: "Expected [1, 2, 3] to not include members [5]",
},
],
children: [],
},
],
},
])
})
test("should fail when subset members not present", () => {
return expect(
runTest(`
${namespace}.test('Missing subset members', function () {
${namespace}.expect([1,2,3]).to.include.members([4,5]);
})
`)()
).resolves.toEqualRight([
{
descriptor: "root",
expectResults: [],
children: [
{
descriptor: "Missing subset members",
expectResults: [
{
status: "fail",
message: expect.stringContaining("to include members"),
},
],
children: [],
},
],
},
])
})
})
})
describe.each(NAMESPACES)(
"%s.expect() - Combined Keys and Members",
(namespace) => {
test("should handle all assertions from the reported issue", () => {
return expect(
runTest(`
${namespace}.test('Contains and includes', function () {
var arr = [1,2,3];
${namespace}.expect(arr).to.include(2);
${namespace}.expect('hoppscotch').to.include('hopp');
${namespace}.expect({a: 1, b: 2}).to.have.keys(['a','b']);
${namespace}.expect({x:5}).to.not.have.key('z');
});
${namespace}.test('Members matching', function () {
${namespace}.expect([1,2,3]).to.have.members([3,2,1]);
${namespace}.expect([1,2,3]).to.include.members([2]);
${namespace}.expect([1,2,3]).to.not.include.members([5]);
});
`)()
).resolves.toEqualRight([
{
descriptor: "root",
expectResults: [],
children: [
{
descriptor: "Contains and includes",
expectResults: [
{
status: "pass",
message: "Expected [1, 2, 3] to include 2",
},
{
status: "pass",
message: "Expected 'hoppscotch' to include 'hopp'",
},
{
status: "pass",
message: "Expected {a: 1, b: 2} to have keys 'a', 'b'",
},
{
status: "pass",
message: "Expected {x: 5} to not have keys 'z'",
},
],
children: [],
},
{
descriptor: "Members matching",
expectResults: [
{
status: "pass",
message: "Expected [1, 2, 3] to have members [3, 2, 1]",
},
{
status: "pass",
message: "Expected [1, 2, 3] to include members [2]",
},
{
status: "pass",
message: "Expected [1, 2, 3] to not include members [5]",
},
],
children: [],
},
],
},
])
})
}
)

View file

@ -0,0 +1,604 @@
import { describe, expect, test } from "vitest"
import { TestResponse } from "~/types"
import { runTest } from "~/utils/test-helpers"
const NAMESPACES = ["pm", "hopp"] as const
describe.each(NAMESPACES)("%s.expect() - Length Assertions", (namespace) => {
const mockResponse: TestResponse = {
status: 200,
statusText: "OK",
responseTime: 100,
headers: [{ key: "content-type", value: "application/json" }],
body: {
items: ["apple", "banana", "cherry"],
emptyArray: [],
singleItem: ["solo"],
data: {
nested: {
values: [1, 2, 3, 4, 5],
},
},
},
}
describe(".length getter - Basic comparison methods", () => {
test("should support .length.above() for arrays", async () => {
const testScript = `
${namespace}.test("length.above()", () => {
${namespace}.expect([1, 2, 3, 4]).to.have.length.above(3);
${namespace}.expect([1, 2]).to.not.have.length.above(5);
});
`
const result = await runTest(testScript, { global: [], selected: [] })()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "length.above()",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
]),
}),
])
)
})
test("should support .length.above() for strings", async () => {
const testScript = `
${namespace}.test("string length.above()", () => {
${namespace}.expect('hello world').to.have.length.above(5);
});
`
const result = await runTest(testScript, { global: [], selected: [] })()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "string length.above()",
expectResults: [expect.objectContaining({ status: "pass" })],
}),
]),
}),
])
)
})
test("should support .length.below() for arrays", async () => {
const testScript = `
${namespace}.test("length.below()", () => {
${namespace}.expect([1, 2]).to.have.length.below(5);
${namespace}.expect([1, 2, 3, 4]).to.not.have.length.below(3);
});
`
const result = await runTest(testScript, { global: [], selected: [] })()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "length.below()",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
]),
}),
])
)
})
test("should support .length.within() for range checks", async () => {
const testScript = `
${namespace}.test("length.within()", () => {
${namespace}.expect([1, 2, 3]).to.have.length.within(2, 5);
${namespace}.expect('test').to.have.length.within(1, 10);
${namespace}.expect([1]).to.not.have.length.within(5, 10);
});
`
const result = await runTest(testScript, { global: [], selected: [] })()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "length.within()",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
]),
}),
])
)
})
})
describe(".length.at.least() and .length.at.most() - Postman chain syntax", () => {
test("should pass when array length meets minimum (.at.least)", async () => {
const script = `
${namespace}.test("Array length at least", () => {
const items = ${namespace === "pm" ? "pm.response.json()" : "hopp.response.body.asJSON()"}.items
${namespace}.expect(items).to.have.length.at.least(1)
${namespace}.expect(items).to.have.length.at.least(3)
})
`
const result = await runTest(
script,
{ global: [], selected: [] },
mockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "Array length at least",
expectResults: [
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
],
}),
]),
}),
])
)
})
test("should fail when array length below minimum (.at.least)", async () => {
const script = `
${namespace}.test("Array too short", () => {
const items = ${namespace === "pm" ? "pm.response.json()" : "hopp.response.body.asJSON()"}.items
${namespace}.expect(items).to.have.length.at.least(10)
})
`
const result = await runTest(
script,
{ global: [], selected: [] },
mockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "Array too short",
expectResults: [expect.objectContaining({ status: "fail" })],
}),
]),
}),
])
)
})
test("should pass when array length within maximum (.at.most)", async () => {
const script = `
${namespace}.test("Array length at most", () => {
const items = ${namespace === "pm" ? "pm.response.json()" : "hopp.response.body.asJSON()"}.items
${namespace}.expect(items).to.have.length.at.most(10)
${namespace}.expect(items).to.have.length.at.most(3)
})
`
const result = await runTest(
script,
{ global: [], selected: [] },
mockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "Array length at most",
expectResults: [
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
],
}),
]),
}),
])
)
})
test("should fail when array length exceeds maximum (.at.most)", async () => {
const script = `
${namespace}.test("Array too long", () => {
const items = ${namespace === "pm" ? "pm.response.json()" : "hopp.response.body.asJSON()"}.items
${namespace}.expect(items).to.have.length.at.most(2)
})
`
const result = await runTest(
script,
{ global: [], selected: [] },
mockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "Array too long",
expectResults: [expect.objectContaining({ status: "fail" })],
}),
]),
}),
])
)
})
})
describe(".length.least() and .length.most() - Direct methods without .at", () => {
test("should support .length.least() without .at chain", async () => {
const script = `
${namespace}.test("Direct least", () => {
${namespace}.expect([1, 2, 3]).to.have.length.least(1)
${namespace}.expect([1, 2, 3]).to.have.length.least(3)
})
`
const result = await runTest(script, { global: [], selected: [] })()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "Direct least",
expectResults: [
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
],
}),
]),
}),
])
)
})
test("should support mixed syntax with and without .at", async () => {
const script = `
${namespace}.test("Mixed syntax", () => {
const items = ${namespace === "pm" ? "pm.response.json()" : "hopp.response.body.asJSON()"}.items
${namespace}.expect(items).to.have.length.least(1)
${namespace}.expect(items).to.have.length.at.least(1)
})
`
const result = await runTest(
script,
{ global: [], selected: [] },
mockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "Mixed syntax",
expectResults: [
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
],
}),
]),
}),
])
)
})
})
describe(".length.gte() and .length.lte() - Aliases", () => {
test("should support .length.gte() as alias for .least()", async () => {
const script = `
${namespace}.test("GTE alias", () => {
${namespace}.expect([1, 2, 3]).to.have.length.gte(3)
${namespace}.expect([1, 2, 3]).to.have.length.gte(1)
})
`
const result = await runTest(script, { global: [], selected: [] })()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "GTE alias",
expectResults: [
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
],
}),
]),
}),
])
)
})
test("should support .length.lte() as alias for .most()", async () => {
const script = `
${namespace}.test("LTE alias", () => {
${namespace}.expect([1, 2, 3]).to.have.length.lte(3)
${namespace}.expect([1, 2, 3]).to.have.length.lte(10)
})
`
const result = await runTest(script, { global: [], selected: [] })()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "LTE alias",
expectResults: [
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
],
}),
]),
}),
])
)
})
})
describe(".length(n) - Callable method for exact length", () => {
test("should support .length(n) as method for exact length", async () => {
const testScript = `
${namespace}.test("length as method", () => {
${namespace}.expect([1, 2, 3]).to.have.length(3);
${namespace}.expect('abc').to.have.length(3);
});
`
const result = await runTest(testScript, { global: [], selected: [] })()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "length as method",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
]),
}),
])
)
})
})
describe(".lengthOf(n) - Method for exact length", () => {
test("should support .lengthOf(n) for exact length", async () => {
const testScript = `
${namespace}.test("lengthOf()", () => {
${namespace}.expect('hello').to.have.lengthOf(5);
${namespace}.expect([1, 2, 3, 4, 5]).to.have.lengthOf(5);
${namespace}.expect('').to.have.lengthOf(0);
});
`
const result = await runTest(testScript, { global: [], selected: [] })()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "lengthOf()",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
]),
}),
])
)
})
})
describe(".lengthOf.at.least() and .lengthOf.at.most() - Alternative syntax", () => {
const lengthOfMockResponse: TestResponse = {
status: 200,
statusText: "OK",
responseTime: 100,
headers: [],
body: {
items: ["a", "b", "c", "d"],
},
}
test("should support .lengthOf.at.least()", async () => {
const script = `
${namespace}.test("lengthOf.at.least", () => {
const items = ${namespace === "pm" ? "pm.response.json()" : "hopp.response.body.asJSON()"}.items
${namespace}.expect(items).to.have.lengthOf.at.least(1)
${namespace}.expect(items).to.have.lengthOf.at.least(4)
})
`
const result = await runTest(
script,
{ global: [], selected: [] },
lengthOfMockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "lengthOf.at.least",
expectResults: [
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
],
}),
]),
}),
])
)
})
test("should support .lengthOf.at.most()", async () => {
const script = `
${namespace}.test("lengthOf.at.most", () => {
const items = ${namespace === "pm" ? "pm.response.json()" : "hopp.response.body.asJSON()"}.items
${namespace}.expect(items).to.have.lengthOf.at.most(10)
${namespace}.expect(items).to.have.lengthOf.at.most(4)
})
`
const result = await runTest(
script,
{ global: [], selected: [] },
lengthOfMockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "lengthOf.at.most",
expectResults: [
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
],
}),
]),
}),
])
)
})
})
describe("Edge cases and special scenarios", () => {
test("should work with empty arrays", async () => {
const script = `
${namespace}.test("Empty array", () => {
const empty = ${namespace === "pm" ? "pm.response.json()" : "hopp.response.body.asJSON()"}.emptyArray
${namespace}.expect(empty).to.have.length.at.least(0)
${namespace}.expect(empty).to.have.length.at.most(0)
${namespace}.expect(empty).to.have.length(0)
${namespace}.expect(empty).to.have.lengthOf(0)
})
`
const result = await runTest(
script,
{ global: [], selected: [] },
mockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "Empty array",
expectResults: [
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
],
}),
]),
}),
])
)
})
test("should work with strings", async () => {
const script = `
${namespace}.test("String length", () => {
const str = "hello world"
${namespace}.expect(str).to.have.length.at.least(5)
${namespace}.expect(str).to.have.length.at.most(20)
${namespace}.expect(str).to.have.length(11)
${namespace}.expect(str).to.have.lengthOf(11)
})
`
const result = await runTest(script, { global: [], selected: [] })()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "String length",
expectResults: [
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
],
}),
]),
}),
])
)
})
test("should work with nested arrays", async () => {
const script = `
${namespace}.test("Nested array length", () => {
const nested = ${namespace === "pm" ? "pm.response.json()" : "hopp.response.body.asJSON()"}.data.nested.values
${namespace}.expect(nested).to.have.length.at.least(5)
${namespace}.expect(nested).to.have.lengthOf(5)
})
`
const result = await runTest(
script,
{ global: [], selected: [] },
mockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "Nested array length",
expectResults: [
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
],
}),
]),
}),
])
)
})
})
})

View file

@ -0,0 +1,426 @@
/**
* @see https://github.com/hoppscotch/hoppscotch/discussions/5221
*/
import { describe, expect, test } from "vitest"
import { runTest } from "~/utils/test-helpers"
const NAMESPACES = ["pm", "hopp"] as const
describe.each(NAMESPACES)(
"%s.expect() - Side Effect Assertions (Standard Pattern)",
(namespace) => {
describe("`.change()` with object and property", () => {
test("should detect property changes", () => {
return expect(
runTest(`
${namespace}.test("change assertions work", () => {
const obj = { val: 0 }
${namespace}.expect(() => { obj.val = 5 }).to.change(obj, 'val')
${namespace}.expect(() => { obj.val = 5 }).to.not.change(obj, 'val')
${namespace}.expect(() => {}).to.not.change(obj, 'val')
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "change assertions work",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
test("should detect changes by specific delta using `.by()`", () => {
return expect(
runTest(`
${namespace}.test("change by delta works", () => {
const obj = { val: 0 }
${namespace}.expect(() => { obj.val += 5 }).to.change(obj, 'val').by(5)
${namespace}.expect(() => { obj.val -= 3 }).to.change(obj, 'val').by(3)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "change by delta works",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
test("should support negative delta", () => {
return expect(
runTest(`
${namespace}.test("change with negative delta", () => {
const obj = { value: 50 }
const decreaseValue = () => { obj.value = 30 }
${namespace}.expect(decreaseValue).to.change(obj, "value").by(-20)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "change with negative delta",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
})
describe("`.increase()` with object and property", () => {
test("should detect property increases", () => {
return expect(
runTest(`
${namespace}.test("increase assertions work", () => {
const obj = { count: 0 }
${namespace}.expect(() => { obj.count++ }).to.increase(obj, 'count')
${namespace}.expect(() => { obj.count += 5 }).to.increase(obj, 'count')
${namespace}.expect(() => { obj.count-- }).to.not.increase(obj, 'count')
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "increase assertions work",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
test("should detect increases by specific amount using `.by()`", () => {
return expect(
runTest(`
${namespace}.test("increase by amount works", () => {
const obj = { count: 0 }
${namespace}.expect(() => { obj.count += 3 }).to.increase(obj, 'count').by(3)
${namespace}.expect(() => { obj.count += 7 }).to.increase(obj, 'count').by(7)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "increase by amount works",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
})
describe("`.decrease()` with object and property", () => {
test("should detect property decreases", () => {
return expect(
runTest(`
${namespace}.test("decrease assertions work", () => {
const obj = { count: 10 }
${namespace}.expect(() => { obj.count-- }).to.decrease(obj, 'count')
${namespace}.expect(() => { obj.count -= 3 }).to.decrease(obj, 'count')
${namespace}.expect(() => { obj.count++ }).to.not.decrease(obj, 'count')
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "decrease assertions work",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
test("should detect decreases by specific amount using `.by()`", () => {
return expect(
runTest(`
${namespace}.test("decrease by amount works", () => {
const obj = { count: 10 }
${namespace}.expect(() => { obj.count -= 2 }).to.decrease(obj, 'count').by(2)
${namespace}.expect(() => { obj.count -= 4 }).to.decrease(obj, 'count').by(4)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "decrease by amount works",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
})
}
)
describe.each(NAMESPACES)(
"%s.expect() - Side Effect Assertions (Getter Function Pattern)",
(namespace) => {
describe("`.change()` with getter function", () => {
test("should detect when getter value changes", () => {
return expect(
runTest(`
${namespace}.test("change with getter function", () => {
let value = 0
const changeFn = () => { value = 1 }
${namespace}.expect(changeFn).to.change(() => value)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "change with getter function",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
test("should pass with negation when value does not change", () => {
return expect(
runTest(`
${namespace}.test("change negated when no change", () => {
let value = 0
const noChangeFn = () => { value = 0 }
${namespace}.expect(noChangeFn).to.not.change(() => value)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "change negated when no change",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
test("should support `.by()` chaining with getter", () => {
return expect(
runTest(`
${namespace}.test("change by with getter function", () => {
let value = 5
const addFive = () => { value += 5 }
${namespace}.expect(addFive).to.change(() => value).by(5)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "change by with getter function",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
})
describe("`.increase()` with getter function", () => {
test("should detect when getter value increases", () => {
return expect(
runTest(`
${namespace}.test("increase with getter function", () => {
let counter = 0
const incrementFn = () => { counter++ }
${namespace}.expect(incrementFn).to.increase(() => counter)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "increase with getter function",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
test("should pass with negation when value does not increase", () => {
return expect(
runTest(`
${namespace}.test("increase negated when no increase", () => {
let counter = 5
const noIncreaseFn = () => { counter-- }
${namespace}.expect(noIncreaseFn).to.not.increase(() => counter)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "increase negated when no increase",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
test("should support `.by()` chaining with getter", () => {
return expect(
runTest(`
${namespace}.test("increase by with getter function", () => {
let value = 5
const addFive = () => { value += 5 }
${namespace}.expect(addFive).to.increase(() => value).by(5)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "increase by with getter function",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
})
describe("`.decrease()` with getter function", () => {
test("should detect when getter value decreases", () => {
return expect(
runTest(`
${namespace}.test("decrease with getter function", () => {
let counter = 10
const decrementFn = () => { counter-- }
${namespace}.expect(decrementFn).to.decrease(() => counter)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "decrease with getter function",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
test("should pass with negation when value does not decrease", () => {
return expect(
runTest(`
${namespace}.test("decrease negated when no decrease", () => {
let counter = 5
const noDecreaseFn = () => { counter++ }
${namespace}.expect(noDecreaseFn).to.not.decrease(() => counter)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "decrease negated when no decrease",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
test("should support `.by()` chaining with getter", () => {
return expect(
runTest(`
${namespace}.test("decrease by with getter function", () => {
let value = 10
const subtractThree = () => { value -= 3 }
${namespace}.expect(subtractThree).to.decrease(() => value).by(3)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "decrease by with getter function",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
})
}
)

View file

@ -0,0 +1,616 @@
/**
* @see https://github.com/hoppscotch/hoppscotch/discussions/5221
*/
import { describe, expect, test } from "vitest"
import { runTest } from "~/utils/test-helpers"
describe("`hopp.expect` - Core Chai Assertions", () => {
describe("Language Chains", () => {
test("should support all language chain properties (`to`, `be`, `that`, `and`, `has`, etc.)", () => {
return expect(
runTest(`
hopp.test("language chains work", () => {
hopp.expect(2).to.equal(2)
hopp.expect(2).to.be.equal(2)
hopp.expect(2).to.be.a('number').that.equals(2)
hopp.expect([1,2,3]).to.be.an('array').that.has.lengthOf(3)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "language chains work",
expectResults: [
{ status: "pass", message: "Expected 2 to equal 2" },
{ status: "pass", message: "Expected 2 to be equal 2" },
{ status: "pass", message: "Expected 2 to be a number" },
{
status: "pass",
message: "Expected 2 to be a number that equals 2",
},
{
status: "pass",
message: "Expected [1, 2, 3] to be an array",
},
{
status: "pass",
message:
"Expected [1, 2, 3] to be an array that has lengthOf 3",
},
],
}),
],
}),
])
})
test("should support multiple modifier combinations with language chains", () => {
return expect(
runTest(`
hopp.test("complex chains work", () => {
hopp.expect([1,2,3]).to.be.an('array')
hopp.expect([1,2,3]).to.have.lengthOf(3)
hopp.expect([1,2,3]).to.include(2)
hopp.expect({a: 1, b: 2}).to.be.an('object')
hopp.expect({a: 1, b: 2}).to.have.property('a')
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "complex chains work",
expectResults: expect.arrayContaining([
{ status: "pass", message: expect.stringMatching(/array/) },
{
status: "pass",
message: expect.stringMatching(/lengthOf 3/),
},
{ status: "pass", message: expect.stringMatching(/include 2/) },
{ status: "pass", message: expect.stringMatching(/object/) },
{
status: "pass",
message: expect.stringMatching(/property 'a'/),
},
]),
}),
],
}),
])
})
})
describe("Type Assertions", () => {
test("should assert primitive types correctly (`.a()`, `.an()`)", () => {
return expect(
runTest(`
hopp.test("type assertions work", () => {
hopp.expect('foo').to.be.a('string')
hopp.expect({a: 1}).to.be.an('object')
hopp.expect([1, 2, 3]).to.be.an('array')
hopp.expect(null).to.be.a('null')
hopp.expect(undefined).to.be.an('undefined')
hopp.expect(42).to.be.a('number')
hopp.expect(true).to.be.a('boolean')
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "type assertions work",
expectResults: [
{ status: "pass", message: "Expected 'foo' to be a string" },
{ status: "pass", message: "Expected {a: 1} to be an object" },
{
status: "pass",
message: "Expected [1, 2, 3] to be an array",
},
{ status: "pass", message: "Expected null to be a null" },
{
status: "pass",
message: "Expected undefined to be an undefined",
},
{ status: "pass", message: "Expected 42 to be a number" },
{ status: "pass", message: "Expected true to be a boolean" },
],
}),
],
}),
])
})
test("should assert Symbol and BigInt types", () => {
return expect(
runTest(`
hopp.test("modern type assertions work", () => {
hopp.expect(Symbol('test')).to.be.a('symbol')
hopp.expect(Symbol.for('shared')).to.be.a('symbol')
hopp.expect(BigInt(123)).to.be.a('bigint')
hopp.expect(BigInt('999999999999999999')).to.be.a('bigint')
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "modern type assertions work",
expectResults: [
{
status: "pass",
message: expect.stringMatching(
/Expected Symbol\(test\) to be a symbol/
),
},
{
status: "pass",
message: expect.stringMatching(
/Expected Symbol\(shared\) to be a symbol/
),
},
{
status: "pass",
message: "Expected 123n to be a bigint",
},
{
status: "pass",
message: "Expected 999999999999999999n to be a bigint",
},
],
}),
],
}),
])
})
})
describe("Equality Assertions", () => {
test("should support `.equal()`, `.equals()`, `.eq()` for strict equality", () => {
return expect(
runTest(`
hopp.test("equality works", () => {
hopp.expect(42).to.equal(42)
hopp.expect('test').to.equals('test')
hopp.expect(true).to.eq(true)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "equality works",
expectResults: [
{ status: "pass", message: "Expected 42 to equal 42" },
{ status: "pass", message: "Expected 'test' to equals 'test'" },
{ status: "pass", message: "Expected true to eq true" },
],
}),
],
}),
])
})
test("should support `.eql()` for deep equality", () => {
return expect(
runTest(`
hopp.test("deep equality works", () => {
hopp.expect({a: 1}).to.eql({a: 1})
hopp.expect([1, 2, 3]).to.eql([1, 2, 3])
hopp.expect({a: {b: 2}}).to.eql({a: {b: 2}})
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "deep equality works",
expectResults: [
{
status: "pass",
message: "Expected {a: 1} to eql {a: 1}",
},
{
status: "pass",
message: "Expected [1, 2, 3] to eql [1, 2, 3]",
},
{
status: "pass",
message: "Expected {a: {b: 2}} to eql {a: {b: 2}}",
},
],
}),
],
}),
])
})
})
describe("Truthiness Assertions", () => {
test("should support `.true`, `.false`, `.ok` assertions", () => {
return expect(
runTest(`
hopp.test("truthiness assertions work", () => {
hopp.expect(true).to.be.true
hopp.expect(false).to.be.false
hopp.expect(1).to.be.ok
hopp.expect('hello').to.be.ok
hopp.expect({}).to.be.ok
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "truthiness assertions work",
expectResults: [
{ status: "pass", message: "Expected true to be true" },
{ status: "pass", message: "Expected false to be false" },
{ status: "pass", message: "Expected 1 to be ok" },
{ status: "pass", message: "Expected 'hello' to be ok" },
{ status: "pass", message: "Expected {} to be ok" },
],
}),
],
}),
])
})
})
describe("Nullish Assertions", () => {
test("should support `.null`, `.undefined`, `.NaN` assertions", () => {
return expect(
runTest(`
hopp.test("nullish assertions work", () => {
hopp.expect(null).to.be.null
hopp.expect(undefined).to.be.undefined
hopp.expect(NaN).to.be.NaN
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "nullish assertions work",
expectResults: [
{ status: "pass", message: "Expected null to be null" },
{
status: "pass",
message: "Expected undefined to be undefined",
},
{ status: "pass", message: "Expected NaN to be NaN" },
],
}),
],
}),
])
})
test("should support `.exist` assertion", () => {
return expect(
runTest(`
hopp.test("exist assertion works", () => {
hopp.expect(0).to.exist
hopp.expect('').to.exist
hopp.expect(false).to.exist
hopp.expect({}).to.exist
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "exist assertion works",
expectResults: [
{ status: "pass", message: "Expected 0 to exist" },
{ status: "pass", message: "Expected '' to exist" },
{ status: "pass", message: "Expected false to exist" },
{ status: "pass", message: "Expected {} to exist" },
],
}),
],
}),
])
})
})
describe("Numerical Comparisons", () => {
test("should support `.above()` and `.below()` comparisons", () => {
return expect(
runTest(`
hopp.test("numerical comparisons work", () => {
hopp.expect(10).to.be.above(5)
hopp.expect(5).to.be.below(10)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "numerical comparisons work",
expectResults: [
{ status: "pass", message: "Expected 10 to be above 5" },
{ status: "pass", message: "Expected 5 to be below 10" },
],
}),
],
}),
])
})
test("should support comparison aliases (`.gt()`, `.gte()`, `.lt()`, `.lte()`)", () => {
return expect(
runTest(`
hopp.test("comparison aliases work", () => {
hopp.expect(10).to.be.gt(5)
hopp.expect(10).to.be.gte(10)
hopp.expect(5).to.be.lt(10)
hopp.expect(5).to.be.lte(5)
hopp.expect(10).to.be.greaterThan(5)
hopp.expect(10).to.be.greaterThanOrEqual(10)
hopp.expect(5).to.be.lessThan(10)
hopp.expect(5).to.be.lessThanOrEqual(5)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "comparison aliases work",
expectResults: expect.arrayContaining([
{ status: "pass", message: expect.stringMatching(/above|gt/) },
{
status: "pass",
message: expect.stringMatching(/above|gte|at least/),
},
{ status: "pass", message: expect.stringMatching(/below|lt/) },
{
status: "pass",
message: expect.stringMatching(/below|lte|at most/),
},
]),
}),
],
}),
])
})
test("should support `.within()` for range comparisons", () => {
return expect(
runTest(`
hopp.test("within range works", () => {
hopp.expect(5).to.be.within(1, 10)
hopp.expect(7).to.be.within(7, 7)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "within range works",
expectResults: [
{ status: "pass", message: "Expected 5 to be within 1, 10" },
{ status: "pass", message: "Expected 7 to be within 7, 7" },
],
}),
],
}),
])
})
test("should support `.closeTo()` and `.approximately()` for floating point comparisons", () => {
return expect(
runTest(`
hopp.test("close to works", () => {
hopp.expect(1.5).to.be.closeTo(1.0, 0.6)
hopp.expect(1.5).to.be.approximately(1.0, 0.6)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "close to works",
expectResults: [
{
status: "pass",
message: "Expected 1.5 to be closeTo 1, 0.6",
},
{
status: "pass",
message: "Expected 1.5 to be approximately 1, 0.6",
},
],
}),
],
}),
])
})
})
describe("Special Value Assertions", () => {
test("should support `.empty` assertion for various types", () => {
return expect(
runTest(`
hopp.test("empty assertion works", () => {
hopp.expect('').to.be.empty
hopp.expect([]).to.be.empty
hopp.expect({}).to.be.empty
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "empty assertion works",
expectResults: [
{ status: "pass", message: "Expected '' to be empty" },
{ status: "pass", message: "Expected [] to be empty" },
{ status: "pass", message: "Expected {} to be empty" },
],
}),
],
}),
])
})
test("should support `.finite` assertion for numbers", () => {
return expect(
runTest(`
hopp.test("finite assertion works", () => {
hopp.expect(42).to.be.finite
hopp.expect(0).to.be.finite
hopp.expect(-100.5).to.be.finite
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "finite assertion works",
expectResults: [
{ status: "pass", message: "Expected 42 to be finite" },
{ status: "pass", message: "Expected 0 to be finite" },
{ status: "pass", message: "Expected -100.5 to be finite" },
],
}),
],
}),
])
})
test("should detect Infinity and reject `.finite`", () => {
return expect(
runTest(`
hopp.test("infinity is not finite", () => {
hopp.expect(Infinity).to.not.be.finite
hopp.expect(-Infinity).to.not.be.finite
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "infinity is not finite",
expectResults: [
{
status: "pass",
message: "Expected Infinity to not be finite",
},
{
status: "pass",
message: "Expected -Infinity to not be finite",
},
],
}),
],
}),
])
})
})
describe("Negation with `.not`", () => {
test("should support negation for all assertion types", () => {
return expect(
runTest(`
hopp.test("negation works", () => {
hopp.expect(1).to.not.equal(2)
hopp.expect('foo').to.not.be.a('number')
hopp.expect(false).to.not.be.true
hopp.expect('foo').to.not.be.empty
})
`)()
).resolves.toEqualRight([
{
descriptor: "root",
expectResults: [],
children: [
{
descriptor: "negation works",
expectResults: [
{ status: "pass", message: "Expected 1 to not equal 2" },
{
status: "pass",
message: "Expected 'foo' to not be a number",
},
{ status: "pass", message: "Expected false to not be true" },
{ status: "pass", message: "Expected 'foo' to not be empty" },
],
children: [],
},
],
},
])
})
})
describe("Boundary Value Testing", () => {
test("should handle boundary values correctly in comparisons", () => {
return expect(
runTest(`
hopp.test("boundary values work", () => {
hopp.expect(Number.MAX_SAFE_INTEGER).to.be.a('number')
hopp.expect(Number.MIN_SAFE_INTEGER).to.be.a('number')
hopp.expect(Number.EPSILON).to.be.above(0)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "boundary values work",
expectResults: expect.arrayContaining([
{ status: "pass", message: expect.stringMatching(/number/) },
]),
}),
],
}),
])
})
})
describe("Failure Cases", () => {
test("should produce meaningful error messages on assertion failures", () => {
return expect(
runTest(`
hopp.test("failures have good messages", () => {
hopp.expect(1).to.equal(2)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "failures have good messages",
expectResults: [
{
status: "fail",
message: expect.stringContaining("Expected 1 to equal 2"),
},
],
}),
],
}),
])
})
})
})

View file

@ -0,0 +1,333 @@
import { describe, expect, test } from "vitest"
import { runTest } from "~/utils/test-helpers"
describe("hopp.expect - String and Regex Methods", () => {
describe("String Inclusion (.string())", () => {
test("should support .string() for substring inclusion", () => {
return expect(
runTest(`
hopp.test("string inclusion works", () => {
hopp.expect('hello world').to.have.string('world')
hopp.expect('foobar').to.have.string('foo')
hopp.expect('foobar').to.have.string('bar')
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "string inclusion works",
expectResults: [
{
status: "pass",
message: expect.stringContaining("to have string"),
},
{
status: "pass",
message: expect.stringContaining("to have string"),
},
{
status: "pass",
message: expect.stringContaining("to have string"),
},
],
}),
],
}),
])
})
test("should support .string() negation", () => {
return expect(
runTest(`
hopp.test("string negation works", () => {
hopp.expect('hello').to.not.have.string('goodbye')
hopp.expect('foo').to.not.have.string('bar')
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
children: [
expect.objectContaining({
descriptor: "string negation works",
expectResults: [
{
status: "pass",
message: expect.stringContaining("to not have string"),
},
{
status: "pass",
message: expect.stringContaining("to not have string"),
},
],
}),
],
}),
])
})
test("should fail on missing substring", () => {
return expect(
runTest(`
hopp.test("string assertion fails correctly", () => {
hopp.expect('hello').to.have.string('goodbye')
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
children: [
expect.objectContaining({
descriptor: "string assertion fails correctly",
expectResults: [
{
status: "fail",
message: expect.stringContaining("to have string"),
},
],
}),
],
}),
])
})
test("should work with empty strings", () => {
return expect(
runTest(`
hopp.test("empty string edge case", () => {
hopp.expect('hello').to.have.string('')
hopp.expect('').to.have.string('')
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
children: [
expect.objectContaining({
descriptor: "empty string edge case",
expectResults: [
{ status: "pass", message: expect.any(String) },
{ status: "pass", message: expect.any(String) },
],
}),
],
}),
])
})
})
describe("Regex Matching (.match())", () => {
test("should support .match() with regex patterns", () => {
return expect(
runTest(`
hopp.test("regex matching works", () => {
hopp.expect('hello123').to.match(/\\d+/)
hopp.expect('test@example.com').to.match(/^[^@]+@[^@]+\\.[^@]+$/)
hopp.expect('ABC').to.match(/[A-Z]+/)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
children: [
expect.objectContaining({
descriptor: "regex matching works",
expectResults: [
{
status: "pass",
message: expect.stringMatching(/to match/),
},
{
status: "pass",
message: expect.stringMatching(/to match/),
},
{
status: "pass",
message: expect.stringMatching(/to match/),
},
],
}),
],
}),
])
})
test("should support .match() negation", () => {
return expect(
runTest(`
hopp.test("regex negation works", () => {
hopp.expect('hello').to.not.match(/\\d+/)
hopp.expect('abc').to.not.match(/[A-Z]+/)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
children: [
expect.objectContaining({
descriptor: "regex negation works",
expectResults: [
{
status: "pass",
message: expect.stringMatching(/to not match/),
},
{
status: "pass",
message: expect.stringMatching(/to not match/),
},
],
}),
],
}),
])
})
test("should support .matches() alias", () => {
return expect(
runTest(`
hopp.test("matches alias works", () => {
hopp.expect('abc123').to.matches(/[a-z]+\\d+/)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
children: [
expect.objectContaining({
descriptor: "matches alias works",
expectResults: [
{
status: "pass",
message: expect.stringMatching(/to match/),
},
],
}),
],
}),
])
})
test("should fail on non-matching regex", () => {
return expect(
runTest(`
hopp.test("regex assertion fails correctly", () => {
hopp.expect('hello').to.match(/\\d+/)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
children: [
expect.objectContaining({
descriptor: "regex assertion fails correctly",
expectResults: [
{
status: "fail",
message: expect.stringMatching(/to match/),
},
],
}),
],
}),
])
})
test("should handle regex with flags", () => {
return expect(
runTest(`
hopp.test("regex flags work", () => {
hopp.expect('HELLO').to.match(/hello/i)
hopp.expect('hello\\nworld').to.match(/hello.world/s)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
children: [
expect.objectContaining({
descriptor: "regex flags work",
expectResults: [
{ status: "pass", message: expect.any(String) },
{ status: "pass", message: expect.any(String) },
],
}),
],
}),
])
})
test("should handle complex regex patterns", () => {
return expect(
runTest(`
hopp.test("complex regex patterns", () => {
hopp.expect('user@example.com').to.match(/^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,}$/i)
hopp.expect('192.168.1.1').to.match(/^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$/)
hopp.expect('+1-555-123-4567').to.match(/^\\+?\\d{1,3}-?\\d{3}-\\d{3}-\\d{4}$/)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
children: [
expect.objectContaining({
descriptor: "complex regex patterns",
expectResults: [
{ status: "pass", message: expect.any(String) },
{ status: "pass", message: expect.any(String) },
{ status: "pass", message: expect.any(String) },
],
}),
],
}),
])
})
})
describe("Combined String and Regex Tests", () => {
test("should work with both string and regex in same test", () => {
return expect(
runTest(`
hopp.test("combined assertions", () => {
const email = 'test@example.com'
hopp.expect(email).to.have.string('@')
hopp.expect(email).to.match(/^[^@]+@[^@]+$/)
hopp.expect(email).to.have.string('example')
hopp.expect(email).to.match(/\\.com$/)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
children: [
expect.objectContaining({
descriptor: "combined assertions",
expectResults: [
{ status: "pass", message: expect.any(String) },
{ status: "pass", message: expect.any(String) },
{ status: "pass", message: expect.any(String) },
{ status: "pass", message: expect.any(String) },
],
}),
],
}),
])
})
test("should chain with other assertions", () => {
return expect(
runTest(`
hopp.test("chained assertions", () => {
hopp.expect('hello world').to.be.a('string').and.have.string('world')
hopp.expect('test123').to.be.a('string').and.match(/\\d+/)
})
`)()
).resolves.toEqualRight([
expect.objectContaining({
children: [
expect.objectContaining({
descriptor: "chained assertions",
expectResults: [
{ status: "pass", message: expect.any(String) },
{ status: "pass", message: expect.any(String) },
{ status: "pass", message: expect.any(String) },
{ status: "pass", message: expect.any(String) },
],
}),
],
}),
])
})
})
})

View file

@ -1,41 +1,10 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.envs)
)
const funcTest = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.tests)
)
import { runTestAndGetEnvs, runTest } from "~/utils/test-helpers"
describe("hopp.env.delete", () => {
test("removes variable from selected environment", () =>
expect(
func(`hopp.env.delete("baseUrl")`, {
runTestAndGetEnvs(`hopp.env.delete("baseUrl")`, {
global: [],
selected: [
{
@ -50,7 +19,7 @@ describe("hopp.env.delete", () => {
test("removes variable from global environment", () =>
expect(
func(`hopp.env.delete("baseUrl")`, {
runTestAndGetEnvs(`hopp.env.delete("baseUrl")`, {
global: [
{
key: "baseUrl",
@ -65,7 +34,7 @@ describe("hopp.env.delete", () => {
test("removes only from selected if present in both", () =>
expect(
func(`hopp.env.delete("baseUrl")`, {
runTestAndGetEnvs(`hopp.env.delete("baseUrl")`, {
global: [
{
key: "baseUrl",
@ -99,7 +68,7 @@ describe("hopp.env.delete", () => {
test("removes only first matching entry if duplicates exist in selected", () =>
expect(
func(`hopp.env.delete("baseUrl")`, {
runTestAndGetEnvs(`hopp.env.delete("baseUrl")`, {
global: [
{
key: "baseUrl",
@ -146,7 +115,7 @@ describe("hopp.env.delete", () => {
test("removes only first matching entry if duplicates exist in global", () =>
expect(
func(`hopp.env.delete("baseUrl")`, {
runTestAndGetEnvs(`hopp.env.delete("baseUrl")`, {
global: [
{
key: "baseUrl",
@ -179,19 +148,22 @@ describe("hopp.env.delete", () => {
test("no change if attempting to delete non-existent key", () =>
expect(
func(`hopp.env.delete("baseUrl")`, { global: [], selected: [] })()
runTestAndGetEnvs(`hopp.env.delete("baseUrl")`, {
global: [],
selected: [],
})()
).resolves.toEqualRight(
expect.objectContaining({ global: [], selected: [] })
))
test("key must be a string", () =>
expect(
func(`hopp.env.delete(5)`, { global: [], selected: [] })()
runTestAndGetEnvs(`hopp.env.delete(5)`, { global: [], selected: [] })()
).resolves.toBeLeft())
test("reflected in script execution", () =>
expect(
funcTest(
runTest(
`
hopp.env.delete("baseUrl")
hopp.expect(hopp.env.get("baseUrl")).toBe(null)
@ -220,7 +192,7 @@ describe("hopp.env.delete", () => {
describe("hopp.env.active.delete", () => {
test("removes variable from selected environment", () =>
expect(
func(`hopp.env.active.delete("foo")`, {
runTestAndGetEnvs(`hopp.env.active.delete("foo")`, {
selected: [
{
key: "foo",
@ -254,7 +226,7 @@ describe("hopp.env.active.delete", () => {
test("no effect if not present in selected", () =>
expect(
func(`hopp.env.active.delete("nope")`, {
runTestAndGetEnvs(`hopp.env.active.delete("nope")`, {
selected: [],
global: [
{
@ -281,14 +253,17 @@ describe("hopp.env.active.delete", () => {
test("key must be a string", () =>
expect(
func(`hopp.env.active.delete({})`, { selected: [], global: [] })()
runTestAndGetEnvs(`hopp.env.active.delete({})`, {
selected: [],
global: [],
})()
).resolves.toBeLeft())
})
describe("hopp.env.global.delete", () => {
test("removes variable from global environment", () =>
expect(
func(`hopp.env.global.delete("foo")`, {
runTestAndGetEnvs(`hopp.env.global.delete("foo")`, {
selected: [
{
key: "foo",
@ -322,7 +297,7 @@ describe("hopp.env.global.delete", () => {
test("no effect if not present in global", () =>
expect(
func(`hopp.env.global.delete("missing")`, {
runTestAndGetEnvs(`hopp.env.global.delete("missing")`, {
selected: [
{
key: "missing",
@ -349,6 +324,9 @@ describe("hopp.env.global.delete", () => {
test("key must be a string", () =>
expect(
func(`hopp.env.global.delete([])`, { selected: [], global: [] })()
runTestAndGetEnvs(`hopp.env.global.delete([])`, {
selected: [],
global: [],
})()
).resolves.toBeLeft())
})

View file

@ -1,31 +1,10 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.tests)
)
import { runTest } from "~/utils/test-helpers"
describe("hopp.env.get", () => {
test("returns the correct value for an existing selected environment value", () => {
return expect(
func(
runTest(
`
const data = hopp.env.get("a")
hopp.expect(data).toBe("b")
@ -51,7 +30,7 @@ describe("hopp.env.get", () => {
test("returns the correct value for an existing global environment value", () => {
return expect(
func(
runTest(
`
const data = hopp.env.get("a")
hopp.expect(data).toBe("b")
@ -77,7 +56,7 @@ describe("hopp.env.get", () => {
test("returns null for a key that is not present in both selected or global", () => {
return expect(
func(
runTest(
`
const data = hopp.env.get("a")
hopp.expect(data).toBe(null)
@ -98,7 +77,7 @@ describe("hopp.env.get", () => {
test("returns the value defined in selected environment if also present in global", () => {
return expect(
func(
runTest(
`
const data = hopp.env.get("a")
hopp.expect(data).toBe("selected val")
@ -136,7 +115,7 @@ describe("hopp.env.get", () => {
test("resolves environment values recursively by default", () => {
return expect(
func(
runTest(
`
const data = hopp.env.get("a")
hopp.expect(data).toBe("hello")
@ -170,7 +149,7 @@ describe("hopp.env.get", () => {
test("errors if the key is not a string", () => {
return expect(
func(
runTest(
`
const data = hopp.env.get(5)
`,
@ -186,7 +165,7 @@ describe("hopp.env.get", () => {
describe("hopp.env.active.get", () => {
test("returns the value from selected environment if present", () => {
return expect(
func(
runTest(
`
const data = hopp.env.active.get("a")
hopp.expect(data).toBe("selectedVal")
@ -224,7 +203,7 @@ describe("hopp.env.active.get", () => {
test("returns null if key does not exist in selected", () => {
return expect(
func(
runTest(
`
const data = hopp.env.active.get("absent")
hopp.expect(data).toBe(null)
@ -252,7 +231,7 @@ describe("hopp.env.active.get", () => {
test("errors if the key is not a string", () => {
return expect(
func(
runTest(
`
hopp.env.active.get({})
`,
@ -265,7 +244,7 @@ describe("hopp.env.active.get", () => {
describe("hopp.env.global.get", () => {
test("returns the value from global environment if present", () => {
return expect(
func(
runTest(
`
const data = hopp.env.global.get("foo")
hopp.expect(data).toBe("globalVal")
@ -300,7 +279,7 @@ describe("hopp.env.global.get", () => {
test("returns null if key does not exist in global", () => {
return expect(
func(
runTest(
`
const data = hopp.env.global.get("not_here")
hopp.expect(data).toBe(null)
@ -328,7 +307,7 @@ describe("hopp.env.global.get", () => {
test("errors if the key is not a string", () => {
return expect(
func(
runTest(
`
hopp.env.global.get([])
`,

View file

@ -1,32 +1,10 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.tests)
)
import { runTest } from "~/utils/test-helpers"
describe("hopp.env.getInitialRaw", () => {
test("returns initial value for existing selected env variable", () => {
return expect(
func(
runTest(
`
const val = hopp.env.getInitialRaw("foo")
hopp.expect(val).toBe("bar")
@ -54,7 +32,7 @@ describe("hopp.env.getInitialRaw", () => {
test("returns initial value from global if not in selected", () => {
return expect(
func(
runTest(
`
const val = hopp.env.getInitialRaw("foo")
hopp.expect(val).toBe("bar")
@ -82,7 +60,7 @@ describe("hopp.env.getInitialRaw", () => {
test("selected shadows global when both present", () => {
return expect(
func(
runTest(
`
const val = hopp.env.getInitialRaw("foo")
hopp.expect(val).toBe("selVal")
@ -117,7 +95,7 @@ describe("hopp.env.getInitialRaw", () => {
test("returns null for missing key", () => {
return expect(
func(
runTest(
`
const val = hopp.env.getInitialRaw("notFound")
hopp.expect(val).toBe(null)
@ -135,7 +113,7 @@ describe("hopp.env.getInitialRaw", () => {
test("returns empty string if initial value was empty", () => {
return expect(
func(
runTest(
`
const val = hopp.env.getInitialRaw("empty")
hopp.expect(val).toBe("")
@ -156,7 +134,7 @@ describe("hopp.env.getInitialRaw", () => {
test("returns literal template syntax, no resolution", () => {
return expect(
func(
runTest(
`
const val = hopp.env.getInitialRaw("templ")
hopp.expect(val).toBe("<<FOO>>")
@ -190,7 +168,7 @@ describe("hopp.env.getInitialRaw", () => {
test("errors for non-string key", () => {
return expect(
func(
runTest(
`
hopp.env.getInitialRaw(5)
`,
@ -203,7 +181,7 @@ describe("hopp.env.getInitialRaw", () => {
describe("hopp.env.active.getInitialRaw", () => {
test("returns initial value if present in selected env", () => {
return expect(
func(
runTest(
`
const val = hopp.env.active.getInitialRaw("alpha")
hopp.expect(val).toBe("a_value")
@ -238,7 +216,7 @@ describe("hopp.env.active.getInitialRaw", () => {
test("returns null if not present in selected env", () => {
return expect(
func(
runTest(
`
const val = hopp.env.active.getInitialRaw("missing")
hopp.expect(val).toBe(null)
@ -266,7 +244,7 @@ describe("hopp.env.active.getInitialRaw", () => {
test("returns '' if initial value was empty string", () => {
return expect(
func(
runTest(
`
const val = hopp.env.active.getInitialRaw("blank")
hopp.expect(val).toBe("")
@ -287,7 +265,7 @@ describe("hopp.env.active.getInitialRaw", () => {
test("returns literal template if present", () => {
return expect(
func(
runTest(
`
const val = hopp.env.active.getInitialRaw("tmpl")
hopp.expect(val).toBe("<<BAR>>")
@ -321,7 +299,7 @@ describe("hopp.env.active.getInitialRaw", () => {
test("errors for non-string key", () => {
return expect(
func(
runTest(
`
hopp.env.active.getInitialRaw({})
`,
@ -334,7 +312,7 @@ describe("hopp.env.active.getInitialRaw", () => {
describe("hopp.env.global.getInitialRaw", () => {
test("returns initial value if present in global env", () => {
return expect(
func(
runTest(
`
const val = hopp.env.global.getInitialRaw("gamma")
hopp.expect(val).toBe("g_val")
@ -369,7 +347,7 @@ describe("hopp.env.global.getInitialRaw", () => {
test("returns null if not present in global env", () => {
return expect(
func(
runTest(
`
const val = hopp.env.global.getInitialRaw("none")
hopp.expect(val).toBe(null)
@ -397,7 +375,7 @@ describe("hopp.env.global.getInitialRaw", () => {
test("returns '' if initial value was empty string", () => {
return expect(
func(
runTest(
`
const val = hopp.env.global.getInitialRaw("empty")
hopp.expect(val).toBe("")
@ -418,7 +396,7 @@ describe("hopp.env.global.getInitialRaw", () => {
test("returns literal template value if present", () => {
return expect(
func(
runTest(
`
const val = hopp.env.global.getInitialRaw("tmpl")
hopp.expect(val).toBe("<<ZED>>")
@ -452,7 +430,7 @@ describe("hopp.env.global.getInitialRaw", () => {
test("errors for non-string key", () => {
return expect(
func(
runTest(
`
hopp.env.global.getInitialRaw([])
`,

View file

@ -1,32 +1,10 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.tests)
)
import { runTest } from "~/utils/test-helpers"
describe("hopp.env.getRaw", () => {
test("returns the correct value for an existing selected environment value", () => {
return expect(
func(
runTest(
`
const data = hopp.env.getRaw("a")
hopp.expect(data).toBe("b")
@ -47,7 +25,7 @@ describe("hopp.env.getRaw", () => {
test("returns the correct value for an existing global environment value", () => {
return expect(
func(
runTest(
`
const data = hopp.env.getRaw("a")
hopp.expect(data).toBe("b")
@ -68,7 +46,7 @@ describe("hopp.env.getRaw", () => {
test("returns null for a key that is not present in both selected and global", () => {
return expect(
func(
runTest(
`
const data = hopp.env.getRaw("a")
hopp.expect(data).toBe(null)
@ -89,7 +67,7 @@ describe("hopp.env.getRaw", () => {
test("returns the value defined in selected if also present in global", () => {
return expect(
func(
runTest(
`
const data = hopp.env.getRaw("a")
hopp.expect(data).toBe("selected val")
@ -127,7 +105,7 @@ describe("hopp.env.getRaw", () => {
test("does not resolve values recursively", () => {
return expect(
func(
runTest(
`
const data = hopp.env.getRaw("a")
hopp.expect(data).toBe("<<hello>>")
@ -161,7 +139,7 @@ describe("hopp.env.getRaw", () => {
test("returns the value as is even if there is a potential recursion", () => {
return expect(
func(
runTest(
`
const data = hopp.env.getRaw("a")
hopp.expect(data).toBe("<<hello>>")
@ -195,7 +173,7 @@ describe("hopp.env.getRaw", () => {
test("errors if the key is not a string", () => {
return expect(
func(
runTest(
`
const data = hopp.env.getRaw(5)
`,
@ -208,7 +186,7 @@ describe("hopp.env.getRaw", () => {
describe("hopp.env.active.getRaw", () => {
test("returns only from selected", () => {
return expect(
func(
runTest(
`
hopp.expect(hopp.env.active.getRaw("a")).toBe("a-selected")
hopp.expect(hopp.env.active.getRaw("b")).toBe(null)
@ -247,7 +225,7 @@ describe("hopp.env.active.getRaw", () => {
test("returns null if key absent in selected", () => {
return expect(
func(
runTest(
`
hopp.expect(hopp.env.active.getRaw("missing")).toBe(null)
`,
@ -274,7 +252,7 @@ describe("hopp.env.active.getRaw", () => {
test("errors if key is not a string", () => {
return expect(
func(
runTest(
`
hopp.env.active.getRaw({})
`,
@ -287,7 +265,7 @@ describe("hopp.env.active.getRaw", () => {
describe("hopp.env.global.getRaw", () => {
test("returns only from global", () => {
return expect(
func(
runTest(
`
hopp.expect(hopp.env.global.getRaw("b")).toBe("b-global")
hopp.expect(hopp.env.global.getRaw("a")).toBe(null)
@ -323,7 +301,7 @@ describe("hopp.env.global.getRaw", () => {
test("returns null if key absent in global", () => {
return expect(
func(
runTest(
`
hopp.expect(hopp.env.global.getRaw("missing")).toBe(null)
`,
@ -350,7 +328,7 @@ describe("hopp.env.global.getRaw", () => {
test("errors if key is not a string", () => {
return expect(
func(
runTest(
`
hopp.env.global.getRaw([])
`,

View file

@ -1,42 +1,10 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.envs)
)
const funcTest = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.tests)
)
import { runTestAndGetEnvs, runTest } from "~/utils/test-helpers"
describe("hopp.env.reset", () => {
test("resets selected variable to its initial value", () =>
expect(
func(
runTestAndGetEnvs(
`
hopp.env.set("foo", "changed")
hopp.env.reset("foo")
@ -68,7 +36,7 @@ describe("hopp.env.reset", () => {
test("resets global variable to its initial value if not in selected", () =>
expect(
func(
runTestAndGetEnvs(
`
hopp.env.set("bar", "override")
hopp.env.reset("bar")
@ -100,7 +68,7 @@ describe("hopp.env.reset", () => {
test("if variable exists in both, only selected variable is reset", () =>
expect(
func(
runTestAndGetEnvs(
`
hopp.env.set("a", "S")
hopp.env.global.set("a", "G")
@ -148,7 +116,7 @@ describe("hopp.env.reset", () => {
test("resets only the first occurrence if duplicates exist in selected", () =>
expect(
func(
runTestAndGetEnvs(
`
hopp.env.set("dup", "X")
hopp.env.reset("dup")
@ -172,7 +140,7 @@ describe("hopp.env.reset", () => {
test("resets only the first occurrence if duplicates exist in global", () =>
expect(
func(
runTestAndGetEnvs(
`
hopp.env.global.set("gdup", "Y")
hopp.env.reset("gdup")
@ -216,19 +184,22 @@ describe("hopp.env.reset", () => {
test("no change if attempting to reset a non-existent key", () =>
expect(
func(`hopp.env.reset("none")`, { selected: [], global: [] })()
runTestAndGetEnvs(`hopp.env.reset("none")`, {
selected: [],
global: [],
})()
).resolves.toEqualRight(
expect.objectContaining({ selected: [], global: [] })
))
test("keys should be a string", () =>
expect(
func(`hopp.env.reset(123)`, { selected: [], global: [] })()
runTestAndGetEnvs(`hopp.env.reset(123)`, { selected: [], global: [] })()
).resolves.toBeLeft())
test("reset reflected in subsequent get in the same script (selected)", () =>
expect(
funcTest(
runTest(
`
hopp.env.set("foo", "override")
hopp.env.reset("foo")
@ -256,7 +227,7 @@ describe("hopp.env.reset", () => {
test("reset works for secret variables", () =>
expect(
func(
runTestAndGetEnvs(
`
hopp.env.set("secret", "newVal")
hopp.env.reset("secret")
@ -290,7 +261,7 @@ describe("hopp.env.reset", () => {
describe("hopp.env.active.reset", () => {
test("resets variable only in selected environment", () =>
expect(
func(
runTestAndGetEnvs(
`
hopp.env.active.set("xxx", "MUT")
hopp.env.active.reset("xxx")
@ -317,7 +288,7 @@ describe("hopp.env.active.reset", () => {
test("no effect if key not in selected", () =>
expect(
func(
runTestAndGetEnvs(
`
hopp.env.active.reset("nonexistent")
`,
@ -349,14 +320,17 @@ describe("hopp.env.active.reset", () => {
test("key must be a string", () =>
expect(
func(`hopp.env.active.reset(123)`, { selected: [], global: [] })()
runTestAndGetEnvs(`hopp.env.active.reset(123)`, {
selected: [],
global: [],
})()
).resolves.toBeLeft())
})
describe("hopp.env.global.reset", () => {
test("resets variable only in global environment", () =>
expect(
func(
runTestAndGetEnvs(
`
hopp.env.global.set("yyy", "GGG")
hopp.env.global.reset("yyy")
@ -388,7 +362,7 @@ describe("hopp.env.global.reset", () => {
test("no effect if key not in global", () =>
expect(
func(
runTestAndGetEnvs(
`
hopp.env.global.reset("nonexistent")
`,
@ -422,7 +396,7 @@ describe("hopp.env.global.reset", () => {
describe("hopp.env.reset - regression cases", () => {
test("create via setInitial then set, and reset restores to initial (selected)", () =>
expect(
func(
runTestAndGetEnvs(
`
// Variable does not exist initially
hopp.env.setInitial("AUTH_TOKEN", "seeded-v1")
@ -450,7 +424,7 @@ describe("hopp.env.global.reset", () => {
test("scope flip: remove from global, create in active, reset only affects active and not deleted global", () =>
expect(
func(
runTestAndGetEnvs(
`
// Start by ensuring global is cleared
hopp.env.global.delete("API_KEY")
@ -490,7 +464,7 @@ describe("hopp.env.global.reset", () => {
test("delete then reset within same script should be a no-op (selected)", () =>
expect(
func(
runTestAndGetEnvs(
`
hopp.env.active.delete("SESSION_ID")
// Reset after unset should not reintroduce or change anything
@ -517,6 +491,9 @@ describe("hopp.env.global.reset", () => {
})
test("key must be a string", () =>
expect(
func(`hopp.env.global.reset([])`, { selected: [], global: [] })()
runTestAndGetEnvs(`hopp.env.global.reset([])`, {
selected: [],
global: [],
})()
).resolves.toBeLeft())
})

View file

@ -1,32 +1,10 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.tests)
)
import { runTest } from "~/utils/test-helpers"
describe("hopp.env.setInitial", () => {
test("sets initial value in selected env when key doesn't exist", () => {
return expect(
func(
runTest(
`
hopp.env.setInitial("newKey", "newValue")
const val = hopp.env.getInitialRaw("newKey")
@ -48,7 +26,7 @@ describe("hopp.env.setInitial", () => {
test("updates initial value in selected env when key exists", () => {
return expect(
func(
runTest(
`
hopp.env.setInitial("existing", "updated")
const val = hopp.env.getInitialRaw("existing")
@ -77,7 +55,7 @@ describe("hopp.env.setInitial", () => {
test("updates selected env when key exists in both selected and global", () => {
return expect(
func(
runTest(
`
hopp.env.setInitial("shared", "selectedUpdate")
const val = hopp.env.getInitialRaw("shared")
@ -116,7 +94,7 @@ describe("hopp.env.setInitial", () => {
test("sets initial value in global env when only exists in global", () => {
return expect(
func(
runTest(
`
hopp.env.setInitial("globalOnly", "globalUpdate")
const val = hopp.env.getInitialRaw("globalOnly")
@ -148,7 +126,7 @@ describe("hopp.env.setInitial", () => {
test("allows setting empty string as initial value", () => {
return expect(
func(
runTest(
`
hopp.env.setInitial("empty", "")
const val = hopp.env.getInitialRaw("empty")
@ -168,7 +146,7 @@ describe("hopp.env.setInitial", () => {
test("allows setting template syntax as initial value", () => {
return expect(
func(
runTest(
`
hopp.env.setInitial("template", "<<FOO>>")
const val = hopp.env.getInitialRaw("template")
@ -190,7 +168,7 @@ describe("hopp.env.setInitial", () => {
test("errors for non-string key", () => {
return expect(
func(
runTest(
`
hopp.env.setInitial(123, "value")
`,
@ -201,7 +179,7 @@ describe("hopp.env.setInitial", () => {
test("errors for non-string value", () => {
return expect(
func(
runTest(
`
hopp.env.setInitial("key", 456)
`,
@ -214,7 +192,7 @@ describe("hopp.env.setInitial", () => {
describe("hopp.env.active.setInitial", () => {
test("sets initial value in selected env only", () => {
return expect(
func(
runTest(
`
hopp.env.active.setInitial("activeKey", "activeValue")
const activeVal = hopp.env.active.getInitialRaw("activeKey")
@ -242,7 +220,7 @@ describe("hopp.env.active.setInitial", () => {
test("updates existing selected env variable", () => {
return expect(
func(
runTest(
`
hopp.env.active.setInitial("existing", "updated")
const val = hopp.env.active.getInitialRaw("existing")
@ -271,7 +249,7 @@ describe("hopp.env.active.setInitial", () => {
test("does not affect global env even if key exists there", () => {
return expect(
func(
runTest(
`
hopp.env.active.setInitial("shared", "activeUpdate")
const activeVal = hopp.env.active.getInitialRaw("shared")
@ -316,7 +294,7 @@ describe("hopp.env.active.setInitial", () => {
test("allows setting empty string", () => {
return expect(
func(
runTest(
`
hopp.env.active.setInitial("blank", "")
const val = hopp.env.active.getInitialRaw("blank")
@ -336,7 +314,7 @@ describe("hopp.env.active.setInitial", () => {
test("errors for non-string key", () => {
return expect(
func(
runTest(
`
hopp.env.active.setInitial(null, "value")
`,
@ -347,7 +325,7 @@ describe("hopp.env.active.setInitial", () => {
test("errors for non-string value", () => {
return expect(
func(
runTest(
`
hopp.env.active.setInitial("key", {})
`,
@ -360,7 +338,7 @@ describe("hopp.env.active.setInitial", () => {
describe("hopp.env.global.setInitial", () => {
test("sets initial value in global env only", () => {
return expect(
func(
runTest(
`
hopp.env.global.setInitial("globalKey", "globalValue")
const globalVal = hopp.env.global.getInitialRaw("globalKey")
@ -388,7 +366,7 @@ describe("hopp.env.global.setInitial", () => {
test("updates existing global env variable", () => {
return expect(
func(
runTest(
`
hopp.env.global.setInitial("existing", "updated")
const val = hopp.env.global.getInitialRaw("existing")
@ -417,7 +395,7 @@ describe("hopp.env.global.setInitial", () => {
test("does not affect selected env even if key exists there", () => {
return expect(
func(
runTest(
`
hopp.env.global.setInitial("shared", "globalUpdate")
const globalVal = hopp.env.global.getInitialRaw("shared")
@ -462,7 +440,7 @@ describe("hopp.env.global.setInitial", () => {
test("allows setting empty string", () => {
return expect(
func(
runTest(
`
hopp.env.global.setInitial("empty", "")
const val = hopp.env.global.getInitialRaw("empty")
@ -482,7 +460,7 @@ describe("hopp.env.global.setInitial", () => {
test("allows setting template syntax", () => {
return expect(
func(
runTest(
`
hopp.env.global.setInitial("template", "<<BAR>>")
const val = hopp.env.global.getInitialRaw("template")
@ -504,7 +482,7 @@ describe("hopp.env.global.setInitial", () => {
test("errors for non-string key", () => {
return expect(
func(
runTest(
`
hopp.env.global.setInitial([], "value")
`,
@ -515,7 +493,7 @@ describe("hopp.env.global.setInitial", () => {
test("errors for non-string value", () => {
return expect(
func(
runTest(
`
hopp.env.global.setInitial("key", true)
`,

View file

@ -0,0 +1,120 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
// Hopp namespace enforces strict string typing (unlike PM namespace)
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "test",
headers: [],
}
const execEnv = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.envs)
)
describe("hopp.env.set undefined rejection", () => {
test("hopp.env.set rejects undefined value", () => {
return expect(
execEnv(`hopp.env.set("key", undefined)`, { selected: [], global: [] })()
).resolves.toBeLeft()
})
test("hopp.env.active.set rejects undefined value", () => {
return expect(
execEnv(`hopp.env.active.set("key", undefined)`, {
selected: [],
global: [],
})()
).resolves.toBeLeft()
})
test("hopp.env.global.set rejects undefined value", () => {
return expect(
execEnv(`hopp.env.global.set("key", undefined)`, {
selected: [],
global: [],
})()
).resolves.toBeLeft()
})
test("hopp.env.setInitial rejects undefined value", () => {
return expect(
execEnv(`hopp.env.setInitial("key", undefined)`, {
selected: [],
global: [],
})()
).resolves.toBeLeft()
})
test("hopp.env.active.setInitial rejects undefined value", () => {
return expect(
execEnv(`hopp.env.active.setInitial("key", undefined)`, {
selected: [],
global: [],
})()
).resolves.toBeLeft()
})
test("hopp.env.global.setInitial rejects undefined value", () => {
return expect(
execEnv(`hopp.env.global.setInitial("key", undefined)`, {
selected: [],
global: [],
})()
).resolves.toBeLeft()
})
test("hopp.env.set rejects null value", () => {
return expect(
execEnv(`hopp.env.set("key", null)`, { selected: [], global: [] })()
).resolves.toBeLeft()
})
test("hopp.env.set rejects boolean value", () => {
return expect(
execEnv(`hopp.env.set("key", true)`, { selected: [], global: [] })()
).resolves.toBeLeft()
})
test("hopp.env.set rejects object value", () => {
return expect(
execEnv(`hopp.env.set("key", {})`, { selected: [], global: [] })()
).resolves.toBeLeft()
})
test("hopp.env.set rejects array value", () => {
return expect(
execEnv(`hopp.env.set("key", [])`, { selected: [], global: [] })()
).resolves.toBeLeft()
})
test("hopp.env.set accepts string value (baseline)", () => {
return expect(
execEnv(`hopp.env.set("key", "value")`, { selected: [], global: [] })()
).resolves.toEqualRight(
expect.objectContaining({
selected: [
{
key: "key",
currentValue: "value",
initialValue: "value",
secret: false,
},
],
})
)
})
})

View file

@ -9,7 +9,7 @@ import { runPreRequestScript, runTestScript } from "~/web"
import { TestResponse } from "~/types"
const baseRequest: HoppRESTRequest = {
v: "15",
v: "16",
name: "Test Request",
endpoint: "https://example.com/api",
method: "GET",
@ -143,7 +143,7 @@ describe("hopp.request", () => {
)
})
test("hopp.request.setMethod should update and uppercase the method", () => {
test("hopp.request.setMethod should update the method (case preserved)", () => {
return expect(
runPreRequestScript(`hopp.request.setMethod("post")`, {
envs: { global: [], selected: [] },
@ -152,7 +152,7 @@ describe("hopp.request", () => {
).resolves.toEqualRight(
expect.objectContaining({
updatedRequest: expect.objectContaining({
method: "POST",
method: "post",
}),
})
)
@ -722,4 +722,295 @@ describe("hopp.request", () => {
).resolves.toEqualLeft(expect.stringContaining("read-only"))
})
})
describe("setter methods immediately reflect in console.log", () => {
test("setUrl should reflect immediately in hopp.request.url", () => {
return expect(
runPreRequestScript(
`
console.log("Before:", hopp.request.url)
hopp.request.setUrl("https://updated.com/api")
console.log("After:", hopp.request.url)
`,
{
envs: { global: [], selected: [] },
request: baseRequest,
}
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: [
expect.objectContaining({
args: ["Before:", "https://example.com/api"],
}),
expect.objectContaining({
args: ["After:", "https://updated.com/api"],
}),
],
updatedRequest: expect.objectContaining({
endpoint: "https://updated.com/api",
}),
})
)
})
test("setMethod should reflect immediately in hopp.request.method", () => {
return expect(
runPreRequestScript(
`
console.log("Before:", hopp.request.method)
hopp.request.setMethod("POST")
console.log("After:", hopp.request.method)
`,
{
envs: { global: [], selected: [] },
request: baseRequest,
}
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: [
expect.objectContaining({
args: ["Before:", "GET"],
}),
expect.objectContaining({
args: ["After:", "POST"],
}),
],
updatedRequest: expect.objectContaining({
method: "POST",
}),
})
)
})
test("setHeader should reflect immediately in hopp.request.headers", () => {
return expect(
runPreRequestScript(
`
const before = hopp.request.headers.find(h => h.key === "X-Test")
console.log("Before value:", before.value)
hopp.request.setHeader("X-Test", "modified")
const after = hopp.request.headers.find(h => h.key === "X-Test")
console.log("After value:", after.value)
`,
{
envs: { global: [], selected: [] },
request: baseRequest,
}
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: [
expect.objectContaining({
args: ["Before value:", "val1"],
}),
expect.objectContaining({
args: ["After value:", "modified"],
}),
],
})
)
})
test("setHeaders should reflect immediately in hopp.request.headers", () => {
return expect(
runPreRequestScript(
`
console.log("Before length:", hopp.request.headers.length)
hopp.request.setHeaders([
{ key: "X-New-1", value: "val1", active: true, description: "" },
{ key: "X-New-2", value: "val2", active: true, description: "" }
])
console.log("After length:", hopp.request.headers.length)
`,
{
envs: { global: [], selected: [] },
request: baseRequest,
}
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: [
expect.objectContaining({
args: ["Before length:", 1],
}),
expect.objectContaining({
args: ["After length:", 2],
}),
],
})
)
})
test("removeHeader should reflect immediately in hopp.request.headers", () => {
return expect(
runPreRequestScript(
`
console.log("Before:", hopp.request.headers.map(h => h.key))
hopp.request.removeHeader("X-Test")
console.log("After:", hopp.request.headers.map(h => h.key))
`,
{
envs: { global: [], selected: [] },
request: baseRequest,
}
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: [
expect.objectContaining({
args: ["Before:", ["X-Test"]],
}),
expect.objectContaining({
args: ["After:", []],
}),
],
})
)
})
test("setParam should reflect immediately in hopp.request.params", () => {
return expect(
runPreRequestScript(
`
const before = hopp.request.params.find(p => p.key === "q")
console.log("Before value:", before.value)
hopp.request.setParam("q", "updated")
const after = hopp.request.params.find(p => p.key === "q")
console.log("After value:", after.value)
`,
{
envs: { global: [], selected: [] },
request: baseRequest,
}
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: [
expect.objectContaining({
args: ["Before value:", "search"],
}),
expect.objectContaining({
args: ["After value:", "updated"],
}),
],
})
)
})
test("setParams should reflect immediately in hopp.request.params", () => {
return expect(
runPreRequestScript(
`
console.log("Before length:", hopp.request.params.length)
hopp.request.setParams([
{ key: "page", value: "1", active: true, description: "" },
{ key: "limit", value: "10", active: true, description: "" }
])
console.log("After length:", hopp.request.params.length)
`,
{
envs: { global: [], selected: [] },
request: baseRequest,
}
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: [
expect.objectContaining({
args: ["Before length:", 1],
}),
expect.objectContaining({
args: ["After length:", 2],
}),
],
})
)
})
test("removeParam should reflect immediately in hopp.request.params", () => {
return expect(
runPreRequestScript(
`
console.log("Before:", hopp.request.params.map(p => p.key))
hopp.request.removeParam("q")
console.log("After:", hopp.request.params.map(p => p.key))
`,
{
envs: { global: [], selected: [] },
request: baseRequest,
}
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: [
expect.objectContaining({
args: ["Before:", ["q"]],
}),
expect.objectContaining({
args: ["After:", []],
}),
],
})
)
})
test("setBody should reflect immediately in hopp.request.body", () => {
return expect(
runPreRequestScript(
`
console.log("Before:", hopp.request.body.contentType)
hopp.request.setBody({
contentType: "application/json",
body: '{"test": true}'
})
console.log("After:", hopp.request.body.contentType)
`,
{
envs: { global: [], selected: [] },
request: baseRequest,
}
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: [
expect.objectContaining({
args: ["Before:", null],
}),
expect.objectContaining({
args: ["After:", "application/json"],
}),
],
})
)
})
test("setAuth should reflect immediately in hopp.request.auth", () => {
return expect(
runPreRequestScript(
`
console.log("Before:", hopp.request.auth.authType)
hopp.request.setAuth({ authType: "bearer", token: "test-token" })
console.log("After:", hopp.request.auth.authType)
`,
{
envs: { global: [], selected: [] },
request: baseRequest,
}
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: [
expect.objectContaining({
args: ["Before:", "none"],
}),
expect.objectContaining({
args: ["After:", "bearer"],
}),
],
})
)
})
})
})

View file

@ -462,4 +462,148 @@ describe("hopp.response", () => {
).resolves.toEqualLeft(expect.stringContaining("read-only"))
})
})
describe("hopp.response utility methods", () => {
test("hopp.response.text() should return response as text", async () => {
await expect(
runTestScript(
`hopp.expect(hopp.response.text()).toBe("Plaintext response")`,
{
envs: { global: [], selected: [] },
request: defaultRequest,
response: sampleTextResponse,
}
)
).resolves.toEqualRight(
expect.objectContaining({
tests: expect.objectContaining({
expectResults: [
{
status: "pass",
message:
"Expected 'Plaintext response' to be 'Plaintext response'",
},
],
}),
})
)
})
test("hopp.response.json() should parse JSON response", async () => {
await expect(
runTestScript(`hopp.expect(hopp.response.json().ok).toBe(true)`, {
envs: { global: [], selected: [] },
request: defaultRequest,
response: sampleJSONResponse,
})
).resolves.toEqualRight(
expect.objectContaining({
tests: expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'true' to be 'true'" },
],
}),
})
)
})
test("hopp.response.reason() should return HTTP reason phrase", async () => {
await expect(
runTestScript(`hopp.expect(hopp.response.reason()).toBe("OK")`, {
envs: { global: [], selected: [] },
request: defaultRequest,
response: sampleTextResponse,
})
).resolves.toEqualRight(
expect.objectContaining({
tests: expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'OK' to be 'OK'" },
],
}),
})
)
})
test("hopp.response.dataURI() should convert response to data URI", async () => {
await expect(
runTestScript(
`
const dataURI = hopp.response.dataURI()
hopp.expect(dataURI).toBeType("string")
hopp.expect(dataURI.startsWith("data:")).toBe(true)
`,
{
envs: { global: [], selected: [] },
request: defaultRequest,
response: sampleJSONResponse,
}
)
).resolves.toEqualRight(
expect.objectContaining({
tests: expect.objectContaining({
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
]),
}),
})
)
})
test("hopp.response.jsonp() should parse JSONP response", async () => {
const jsonpResponse: TestResponse = {
status: 200,
body: 'callback({"data": "test"})',
headers: [{ key: "Content-Type", value: "application/javascript" }],
statusText: "OK",
responseTime: 100,
}
await expect(
runTestScript(
`hopp.expect(hopp.response.jsonp("callback").data).toBe("test")`,
{
envs: { global: [], selected: [] },
request: defaultRequest,
response: jsonpResponse,
}
)
).resolves.toEqualRight(
expect.objectContaining({
tests: expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'test' to be 'test'" },
],
}),
})
)
})
test("hopp.response.jsonp() should handle plain JSON without callback", async () => {
const plainJSONResponse: TestResponse = {
status: 200,
body: '{"plain": "json"}',
headers: [{ key: "Content-Type", value: "application/json" }],
statusText: "OK",
responseTime: 100,
}
await expect(
runTestScript(`hopp.expect(hopp.response.jsonp().plain).toBe("json")`, {
envs: { global: [], selected: [] },
request: defaultRequest,
response: plainJSONResponse,
})
).resolves.toEqualRight(
expect.objectContaining({
tests: expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'json' to be 'json'" },
],
}),
})
)
})
})
})

View file

@ -0,0 +1,806 @@
import { describe, expect, test } from "vitest"
import { runTest } from "~/utils/test-helpers"
describe("`pm.response.to.have.jsonSchema` - JSON Schema Validation", () => {
test("should validate simple type schema", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: JSON.stringify({ name: "John", age: 30 }),
headers: [{ key: "Content-Type", value: "application/json" }],
}
return expect(
runTest(
`
pm.test("Response matches schema", function() {
const schema = {
type: "object",
required: ["name", "age"],
properties: {
name: { type: "string" },
age: { type: "number" }
}
}
pm.response.to.have.jsonSchema(schema)
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "Response matches schema",
// Note: jsonSchema assertion currently doesn't populate expectResults
// TODO: Enhance implementation to track individual schema validation results
expectResults: [],
}),
],
}),
])
})
test("should validate nested object schema", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: JSON.stringify({
user: {
id: 123,
profile: {
name: "John",
email: "john@example.com",
},
},
}),
headers: [{ key: "Content-Type", value: "application/json" }],
}
return expect(
runTest(
`
pm.test("Nested schema validation", function() {
const schema = {
type: "object",
required: ["user"],
properties: {
user: {
type: "object",
required: ["id", "profile"],
properties: {
id: { type: "number" },
profile: {
type: "object",
properties: {
name: { type: "string" },
email: { type: "string" }
}
}
}
}
}
}
pm.response.to.have.jsonSchema(schema)
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "Nested schema validation",
// Note: jsonSchema assertion currently doesn't populate expectResults
expectResults: [],
}),
],
}),
])
})
test("should validate array schema with items", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: JSON.stringify([
{ id: 1, name: "Item 1" },
{ id: 2, name: "Item 2" },
]),
headers: [{ key: "Content-Type", value: "application/json" }],
}
return expect(
runTest(
`
pm.test("Array schema validation", function() {
const schema = {
type: "array",
items: {
type: "object",
required: ["id", "name"],
properties: {
id: { type: "number" },
name: { type: "string" }
}
}
}
pm.response.to.have.jsonSchema(schema)
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "Array schema validation",
// Note: jsonSchema assertion currently doesn't populate expectResults
expectResults: [],
}),
],
}),
])
})
test("should validate enum constraints", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: JSON.stringify({ status: "active", role: "admin" }),
headers: [{ key: "Content-Type", value: "application/json" }],
}
return expect(
runTest(
`
pm.test("Enum validation", function() {
const schema = {
type: "object",
properties: {
status: { enum: ["active", "inactive", "pending"] },
role: { enum: ["admin", "user", "guest"] }
}
}
pm.response.to.have.jsonSchema(schema)
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "Enum validation",
expectResults: [],
}),
],
}),
])
})
test("should validate number constraints (min/max)", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: JSON.stringify({ age: 25, score: 85 }),
headers: [{ key: "Content-Type", value: "application/json" }],
}
return expect(
runTest(
`
pm.test("Number constraints", function() {
const schema = {
type: "object",
properties: {
age: { type: "number", minimum: 0, maximum: 120 },
score: { type: "number", minimum: 0, maximum: 100 }
}
}
pm.response.to.have.jsonSchema(schema)
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "Number constraints",
expectResults: [],
}),
],
}),
])
})
test("should validate string constraints (length, pattern)", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: JSON.stringify({
username: "john123",
email: "john@example.com",
}),
headers: [{ key: "Content-Type", value: "application/json" }],
}
return expect(
runTest(
`
pm.test("String constraints", function() {
const schema = {
type: "object",
properties: {
username: { type: "string", minLength: 3, maxLength: 20 },
email: { type: "string", pattern: "^[^@]+@[^@]+\\\\.[^@]+$" }
}
}
pm.response.to.have.jsonSchema(schema)
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "String constraints",
expectResults: [],
}),
],
}),
])
})
test("should validate array length constraints", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: JSON.stringify({
items: [1, 2, 3],
tags: ["tag1", "tag2"],
}),
headers: [{ key: "Content-Type", value: "application/json" }],
}
return expect(
runTest(
`
pm.test("Array length constraints", function() {
const schema = {
type: "object",
properties: {
items: { type: "array", minItems: 1, maxItems: 10 },
tags: { type: "array", minItems: 1 }
}
}
pm.response.to.have.jsonSchema(schema)
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "Array length constraints",
expectResults: [],
}),
],
}),
])
})
test("should fail when required property is missing", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: JSON.stringify({ name: "John" }),
headers: [{ key: "Content-Type", value: "application/json" }],
}
return expect(
runTest(
`
pm.test("Missing required property", function() {
const schema = {
type: "object",
required: ["name", "age"],
properties: {
name: { type: "string" },
age: { type: "number" }
}
}
pm.response.to.have.jsonSchema(schema)
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualLeft(
expect.stringContaining("Required property 'age' is missing")
)
})
test("should fail when type doesn't match", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: JSON.stringify({ age: "thirty" }),
headers: [{ key: "Content-Type", value: "application/json" }],
}
return expect(
runTest(
`
pm.test("Wrong type", function() {
const schema = {
type: "object",
properties: {
age: { type: "number" }
}
}
pm.response.to.have.jsonSchema(schema)
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualLeft(
expect.stringContaining("Expected type number, got string")
)
})
})
describe("`pm.response.to.have.charset` - Charset Assertions", () => {
test("should assert UTF-8 charset", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: "Hello World",
headers: [{ key: "Content-Type", value: "text/html; charset=utf-8" }],
}
return expect(
runTest(
`
pm.test("Response has UTF-8 charset", function() {
pm.response.to.have.charset("utf-8")
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "Response has UTF-8 charset",
expectResults: [
{
status: "pass",
message: expect.stringContaining(
"Expected 'utf-8' to equal 'utf-8'"
),
},
],
}),
],
}),
])
})
test("should assert ISO-8859-1 charset", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: "Hello World",
headers: [
{ key: "Content-Type", value: "text/plain; charset=ISO-8859-1" },
],
}
return expect(
runTest(
`
pm.test("Response has ISO-8859-1 charset", function() {
pm.response.to.have.charset("iso-8859-1")
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "Response has ISO-8859-1 charset",
expectResults: [
{
status: "pass",
message: expect.stringContaining(
"Expected 'iso-8859-1' to equal 'iso-8859-1'"
),
},
],
}),
],
}),
])
})
test("should handle charset case-insensitively", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: "{}",
headers: [
{ key: "Content-Type", value: "application/json; charset=UTF-8" },
],
}
return expect(
runTest(
`
pm.test("Charset is case-insensitive", function() {
pm.response.to.have.charset("utf-8")
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "Charset is case-insensitive",
expectResults: [
{
status: "pass",
message: expect.stringContaining(
"Expected 'utf-8' to equal 'utf-8'"
),
},
],
}),
],
}),
])
})
test("should fail when charset doesn't match", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: "Hello",
headers: [{ key: "Content-Type", value: "text/html; charset=utf-8" }],
}
return expect(
runTest(
`
pm.test("Wrong charset fails", function() {
pm.response.to.have.charset("iso-8859-1")
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "Wrong charset fails",
expectResults: [
{
status: "fail",
message: expect.stringContaining(
"Expected 'utf-8' to equal 'iso-8859-1'"
),
},
],
}),
],
}),
])
})
})
describe("`pm.response.to.have.jsonPath` - JSONPath Queries", () => {
test("should query simple property", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: JSON.stringify({ name: "John", age: 30 }),
headers: [{ key: "Content-Type", value: "application/json" }],
}
return expect(
runTest(
`
pm.test("Query simple property", function() {
pm.response.to.have.jsonPath("$.name", "John")
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "Query simple property",
expectResults: [
{
status: "pass",
message: expect.stringContaining(
"Expected 'John' to deep equal 'John'"
),
},
],
}),
],
}),
])
})
test("should query nested property", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: JSON.stringify({
user: {
profile: {
name: "John Doe",
age: 30,
},
},
}),
headers: [{ key: "Content-Type", value: "application/json" }],
}
return expect(
runTest(
`
pm.test("Query nested property", function() {
pm.response.to.have.jsonPath("$.user.profile.name", "John Doe")
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "Query nested property",
expectResults: [
{
status: "pass",
message: expect.stringContaining(
"Expected 'John Doe' to deep equal 'John Doe'"
),
},
],
}),
],
}),
])
})
test("should query array element by index", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: JSON.stringify({
items: [
{ id: 1, name: "Item 1" },
{ id: 2, name: "Item 2" },
],
}),
headers: [{ key: "Content-Type", value: "application/json" }],
}
return expect(
runTest(
`
pm.test("Query array element", function() {
pm.response.to.have.jsonPath("$.items[0].name", "Item 1")
pm.response.to.have.jsonPath("$.items[1].id", 2)
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "Query array element",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
test("should query without expected value (existence check)", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: JSON.stringify({ user: { id: 123, name: "John" } }),
headers: [{ key: "Content-Type", value: "application/json" }],
}
return expect(
runTest(
`
pm.test("Check property exists", function() {
pm.response.to.have.jsonPath("$.user.id")
pm.response.to.have.jsonPath("$.user.name")
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "Check property exists",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
test("should handle root path", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: JSON.stringify({ name: "John", age: 30 }),
headers: [{ key: "Content-Type", value: "application/json" }],
}
return expect(
runTest(
`
pm.test("Query root", function() {
const root = pm.response.json()
pm.response.to.have.jsonPath("$", root)
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "Query root",
expectResults: [
{
status: "pass",
message: expect.stringContaining("deep equal"),
},
],
}),
],
}),
])
})
test("should fail when path doesn't exist", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: JSON.stringify({ name: "John" }),
headers: [{ key: "Content-Type", value: "application/json" }],
}
return expect(
runTest(
`
pm.test("Non-existent path fails", function() {
pm.response.to.have.jsonPath("$.nonexistent")
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualLeft(
expect.stringContaining("Property 'nonexistent' not found")
)
})
test("should fail when array index is out of bounds", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: JSON.stringify({ items: [1, 2, 3] }),
headers: [{ key: "Content-Type", value: "application/json" }],
}
return expect(
runTest(
`
pm.test("Out of bounds index fails", function() {
pm.response.to.have.jsonPath("$.items[10]")
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualLeft(
expect.stringContaining("Array index '10' out of bounds")
)
})
test("should fail when value doesn't match", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: JSON.stringify({ name: "John" }),
headers: [{ key: "Content-Type", value: "application/json" }],
}
return expect(
runTest(
`
pm.test("Wrong value fails", function() {
pm.response.to.have.jsonPath("$.name", "Jane")
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "Wrong value fails",
expectResults: [
{
status: "fail",
message: expect.stringContaining(
"Expected 'John' to deep equal 'Jane'"
),
},
],
}),
],
}),
])
})
})

View file

@ -0,0 +1,114 @@
import { describe, expect, test } from "vitest"
import { runTest } from "~/utils/test-helpers"
describe("pm.expect - Advanced Chai Features", () => {
describe(".nested property assertions", () => {
test("should access nested properties using dot notation", () => {
return expect(
runTest(
`
pm.test("Nested property access", function() {
const obj = { a: { b: { c: "value" } } }
pm.expect(obj).to.have.nested.property("a.b.c", "value")
})
`,
{ global: [], selected: [] }
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "Nested property access",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
test("should access nested properties without value check", () => {
return expect(
runTest(
`
pm.test("Nested property existence", function() {
const obj = { x: { y: { z: 123 } } }
pm.expect(obj).to.have.nested.property("x.y.z")
})
`,
{ global: [], selected: [] }
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "Nested property existence",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
test("should handle nested array indices", () => {
return expect(
runTest(
`
pm.test("Nested array access", function() {
const obj = { items: [{ name: "first" }, { name: "second" }] }
pm.expect(obj).to.have.nested.property("items[1].name", "second")
})
`,
{ global: [], selected: [] }
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "Nested array access",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
test("should work with .not negation", () => {
return expect(
runTest(
`
pm.test("Negated nested property", function() {
const obj = { a: { b: "value" } }
pm.expect(obj).to.not.have.nested.property("a.c")
})
`,
{ global: [], selected: [] }
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "Negated nested property",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
})
// Side-effect assertions with .by() chaining are comprehensively tested in
// change-increase-decrease-getter.spec.ts which includes both getter and object+property patterns,
// positive/negative deltas, and all assertion combinations
})

View file

@ -0,0 +1,689 @@
import { describe, expect, test } from "vitest"
import { TestResponse } from "~/types"
import { runTest } from "~/utils/test-helpers"
const mockResponse: TestResponse = {
status: 200,
statusText: "OK",
responseTime: 0,
body: "OK",
headers: [],
}
describe("Chai Edge Cases - .include.members() / .contain.members() Pattern", () => {
test("should support .include.members() for subset matching", async () => {
const testScript = `
pm.test("include.members subset", () => {
pm.expect([1, 2, 3, 4]).to.include.members([1, 2]);
pm.expect([1, 2, 3, 4]).to.contain.members([3, 4]);
});
`
const result = await runTest(
testScript,
{ global: [], selected: [] },
mockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "include.members subset",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
]),
}),
]),
}),
])
)
})
test("should support .have.members() for exact matching", async () => {
const testScript = `
pm.test("exact members", () => {
pm.expect([1, 2, 3]).to.have.members([1, 2, 3]);
pm.expect([1, 2, 3]).to.have.members([3, 2, 1]); // Order doesn't matter
});
`
const result = await runTest(
testScript,
{ global: [], selected: [] },
mockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "exact members",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
]),
}),
]),
}),
])
)
})
test("hopp namespace should support include.members", async () => {
const testScript = `
hopp.test("hopp include.members", () => {
hopp.expect([1, 2, 3, 4]).to.include.members([2, 3]);
});
`
const result = await runTest(
testScript,
{ global: [], selected: [] },
mockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "hopp include.members",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
]),
}),
])
)
})
})
describe("Chai Edge Cases - .any.keys() / .all.keys() Patterns", () => {
test("should support .any.keys() - at least one key matches", async () => {
const testScript = `
pm.test("any.keys pattern", () => {
const obj = { a: 1, b: 2, c: 3 };
pm.expect(obj).to.have.any.keys('a', 'b'); // Has both
pm.expect(obj).to.have.any.keys('a', 'z'); // Has at least one (a)
});
`
const result = await runTest(
testScript,
{ global: [], selected: [] },
mockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "any.keys pattern",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
]),
}),
]),
}),
])
)
})
test("should support .all.keys() - must have exactly these keys", async () => {
const testScript = `
pm.test("all.keys pattern", () => {
const obj = { a: 1, b: 2 };
pm.expect(obj).to.have.all.keys('a', 'b'); // Exact match
pm.expect(obj).to.have.keys('a', 'b'); // Default is .all
});
`
const result = await runTest(
testScript,
{ global: [], selected: [] },
mockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "all.keys pattern",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
]),
}),
]),
}),
])
)
})
test("should fail when .any.keys() has no matching keys", async () => {
const testScript = `
pm.test("any.keys failure", () => {
const obj = { a: 1, b: 2 };
pm.expect(obj).to.have.any.keys('x', 'y', 'z'); // None match
});
`
const result = await runTest(
testScript,
{ global: [], selected: [] },
mockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "any.keys failure",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "fail" }),
]),
}),
]),
}),
])
)
})
})
describe("Chai Edge Cases - .ordered.members() Pattern", () => {
test("should support .ordered.members() - order matters", async () => {
const testScript = `
pm.test("ordered.members", () => {
pm.expect([1, 2, 3]).to.have.ordered.members([1, 2, 3]);
});
`
const result = await runTest(
testScript,
{ global: [], selected: [] },
mockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "ordered.members",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
]),
}),
])
)
})
test("should fail .ordered.members() when order is wrong", async () => {
const testScript = `
pm.test("ordered.members wrong order", () => {
pm.expect([1, 2, 3]).to.have.ordered.members([3, 2, 1]);
});
`
const result = await runTest(
testScript,
{ global: [], selected: [] },
mockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "ordered.members wrong order",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "fail" }),
]),
}),
]),
}),
])
)
})
})
describe("Chai Edge Cases - .own.include() Pattern", () => {
test("should support .own.include() - own properties only", async () => {
const testScript = `
pm.test("own.include", () => {
const obj = Object.create({ inherited: 'value' });
obj.own = 'ownValue';
pm.expect(obj).to.have.own.include({ own: 'ownValue' });
});
`
const result = await runTest(
testScript,
{ global: [], selected: [] },
mockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "own.include",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
]),
}),
])
)
})
test("should fail .own.include() for inherited properties", async () => {
const testScript = `
pm.test("own.include excludes inherited", () => {
const obj = Object.create({ inherited: 'value' });
obj.own = 'ownValue';
// This should fail because 'inherited' is not an own property
pm.expect(obj).to.have.own.include({ inherited: 'value' });
});
`
const result = await runTest(
testScript,
{ global: [], selected: [] },
mockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "own.include excludes inherited",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "fail" }),
]),
}),
]),
}),
])
)
})
})
describe("Chai Edge Cases - .nested.include() Pattern", () => {
test("should support .nested.include() - dot notation for nested properties", async () => {
const testScript = `
pm.test("nested.include", () => {
const obj = {
user: {
name: 'John',
address: {
city: 'NYC'
}
}
};
pm.expect(obj).to.nested.include({ 'user.name': 'John' });
pm.expect(obj).to.nested.include({ 'user.address.city': 'NYC' });
});
`
const result = await runTest(
testScript,
{ global: [], selected: [] },
mockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "nested.include",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
]),
}),
]),
}),
])
)
})
test("should support .nested.include() with bracket notation", async () => {
const testScript = `
pm.test("nested.include bracket notation", () => {
const obj = {
'user.name': 'Alice', // Literal key with dot
user: { name: 'Bob' }
};
// Bracket notation for literal key with dot
pm.expect(obj).to.nested.include({ '["user.name"]': 'Alice' });
// Dot notation for nested property
pm.expect(obj).to.nested.include({ 'user.name': 'Bob' });
});
`
const result = await runTest(
testScript,
{ global: [], selected: [] },
mockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "nested.include bracket notation",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
]),
}),
]),
}),
])
)
})
})
describe("Chai Edge Cases - Modifier Combinations", () => {
test("should support .deep.own.include() - stacked modifiers", async () => {
const testScript = `
pm.test("deep.own.include", () => {
const obj = Object.create({ inherited: { a: 1 } });
obj.own = { b: 2 };
pm.expect(obj).to.have.deep.own.include({ own: { b: 2 } });
});
`
const result = await runTest(
testScript,
{ global: [], selected: [] },
mockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "deep.own.include",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
]),
}),
])
)
})
test("should support .deep.equal() - deep equality check", async () => {
const testScript = `
pm.test("deep.equal", () => {
const obj1 = { a: { b: { c: 1 } } };
const obj2 = { a: { b: { c: 1 } } };
pm.expect(obj1).to.deep.equal(obj2);
pm.expect(obj1).to.eql(obj2); // eql is alias for deep.equal
});
`
const result = await runTest(
testScript,
{ global: [], selected: [] },
mockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "deep.equal",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
]),
}),
]),
}),
])
)
})
test("should support .deep.nested.include() - multiple modifiers", async () => {
const testScript = `
pm.test("deep.nested.include", () => {
const obj = {
user: {
profile: {
settings: { theme: 'dark', notifications: true }
}
}
};
pm.expect(obj).to.deep.nested.include({
'user.profile.settings': { theme: 'dark', notifications: true }
});
});
`
const result = await runTest(
testScript,
{ global: [], selected: [] },
mockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "deep.nested.include",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
]),
}),
])
)
})
})
describe("Chai Edge Cases - .ownPropertyDescriptor() with Chaining", () => {
test("should support chaining after .ownPropertyDescriptor()", async () => {
const testScript = `
pm.test("ownPropertyDescriptor chaining", () => {
const obj = {};
Object.defineProperty(obj, 'foo', {
value: 42,
writable: false,
enumerable: true,
configurable: false
});
pm.expect(obj).to.have.ownPropertyDescriptor('foo')
.that.has.property('enumerable', true);
pm.expect(obj).to.have.ownPropertyDescriptor('foo')
.that.has.property('writable', false);
});
`
const result = await runTest(
testScript,
{ global: [], selected: [] },
mockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "ownPropertyDescriptor chaining",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
]),
}),
]),
}),
])
)
})
})
describe("Chai Edge Cases - .respondTo() with .itself", () => {
test("should support .itself.respondTo() for function methods", async () => {
const testScript = `
pm.test("itself.respondTo", () => {
function MyFunc() {}
MyFunc.staticMethod = function() {};
// Check that the function itself has the method
pm.expect(MyFunc).itself.to.respondTo('staticMethod');
});
`
const result = await runTest(
testScript,
{ global: [], selected: [] },
mockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "itself.respondTo",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
]),
}),
])
)
})
})
describe("Chai Edge Cases - Real-World Patterns", () => {
test("should handle complex nested assertions from API responses", async () => {
const jsonResponse: TestResponse = {
status: 200,
statusText: "OK",
responseTime: 0,
body: JSON.stringify({
data: {
users: [
{ id: 1, name: "Alice", roles: ["admin", "user"] },
{ id: 2, name: "Bob", roles: ["user"] },
],
meta: {
total: 2,
page: 1,
},
},
}),
headers: [{ key: "Content-Type", value: "application/json" }],
}
const testScript = `
pm.test("complex API response validation", () => {
const response = pm.response.json();
// Deep nested property checks
pm.expect(response).to.nested.include({ 'data.meta.total': 2 });
pm.expect(response).to.nested.include({ 'data.meta.page': 1 });
// Array member checks
const userIds = response.data.users.map(u => u.id);
pm.expect(userIds).to.include.members([1, 2]);
// Deep property on array element
pm.expect(response.data.users[0]).to.deep.include({
roles: ["admin", "user"]
});
});
`
const result = await runTest(
testScript,
{ global: [], selected: [] },
jsonResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "complex API response validation",
expectResults: [
expect.objectContaining({
status: "pass",
message: expect.stringContaining("nested include"),
}),
expect.objectContaining({
status: "pass",
message: expect.stringContaining("nested include"),
}),
expect.objectContaining({
status: "pass",
message: expect.stringContaining("include members"),
}),
expect.objectContaining({
status: "pass",
message: expect.stringContaining("deep include"),
}),
],
}),
]),
}),
])
)
})
})

View file

@ -0,0 +1,284 @@
import { describe, expect, test } from "vitest"
import { runTest, runTestAndGetEnvs } from "~/utils/test-helpers"
import { runPreRequestScript } from "~/node"
import { getDefaultRESTRequest } from "@hoppscotch/data"
const DEFAULT_REQUEST = getDefaultRESTRequest()
// Undefined marker pattern ensures values survive serialization
describe("Cross-namespace undefined preservation", () => {
test("hopp.env.get can read undefined set by pm.environment.set", () => {
return expect(
runTest(
`
pm.environment.set("undef_var", undefined)
const value = hopp.env.get("undef_var")
pm.expect(value).toBe(undefined)
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'undefined' to be 'undefined'",
},
],
}),
])
})
test("pw.env.get can read undefined set by pm.environment.set in pre-request", () => {
return expect(
runPreRequestScript(
`
pm.environment.set("env_undef_pre", undefined)
// Verify pw.env.get can read the undefined value
const value = pw.env.get("env_undef_pre")
// Store the result to verify it was read correctly
pw.env.set("read_result", value === undefined ? "success" : "failed")
`,
{
envs: {
global: [],
selected: [],
},
request: DEFAULT_REQUEST,
cookies: null,
experimentalScriptingSandbox: true,
}
)()
).resolves.toEqualRight({
updatedEnvs: {
global: [],
selected: [
{
key: "env_undef_pre",
currentValue: "undefined", // Converted from UNDEFINED_MARKER
initialValue: "undefined",
secret: false,
},
{
key: "read_result",
currentValue: "success", // Confirms pw.env.get returned undefined
initialValue: "success",
secret: false,
},
],
},
updatedRequest: DEFAULT_REQUEST,
updatedCookies: null,
})
})
test("pm.variables.get can read undefined from environment", () => {
return expect(
runTest(
`
pm.environment.set("env_undef", undefined)
const value = pm.variables.get("env_undef")
pm.expect(value).toBe(undefined)
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'undefined' to be 'undefined'",
},
],
}),
])
})
test("hopp.env.active.get can read undefined set by pm.variables.set", () => {
return expect(
runTest(
`
pm.variables.set("var_undef", undefined)
const value = hopp.env.active.get("var_undef")
pm.expect(value).toBe(undefined)
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'undefined' to be 'undefined'",
},
],
}),
])
})
test("hopp.env.global.get can read undefined set by pm.globals.set", () => {
return expect(
runTest(
`
pm.globals.set("global_test", undefined)
const value = hopp.env.global.get("global_test")
pm.expect(value).toBe(undefined)
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'undefined' to be 'undefined'",
},
],
}),
])
})
test("undefined value appears correctly in environment array", () => {
return expect(
runTestAndGetEnvs(
`
pm.environment.set("stored_undef", undefined)
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight(
expect.objectContaining({
selected: [
{
key: "stored_undef",
// The value should be stored as the marker internally but exposed as "undefined" string for UI
currentValue: "undefined",
initialValue: "undefined",
secret: false,
},
],
})
)
})
test("undefined is preserved across multiple namespace reads", () => {
return expect(
runTest(
`
pm.environment.set("multi_read", undefined)
// Read from PM namespace
const pmValue = pm.environment.get("multi_read")
pm.expect(pmValue).toBe(undefined)
// Read from hopp namespace
const hoppValue = hopp.env.get("multi_read")
pm.expect(hoppValue).toBe(undefined)
// Read from pm.variables (which resolves from environment)
const varValue = pm.variables.get("multi_read")
pm.expect(varValue).toBe(undefined)
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'undefined' to be 'undefined'",
},
{
status: "pass",
message: "Expected 'undefined' to be 'undefined'",
},
{
status: "pass",
message: "Expected 'undefined' to be 'undefined'",
},
],
}),
])
})
test("overwriting undefined with string works across namespaces", () => {
return expect(
runTest(
`
// Set undefined via PM
pm.environment.set("changeable", undefined)
pm.expect(hopp.env.get("changeable")).toBe(undefined)
// Overwrite with string via PM
pm.environment.set("changeable", "new_value")
pm.expect(hopp.env.get("changeable")).toBe("new_value")
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'undefined' to be 'undefined'",
},
{
status: "pass",
message: "Expected 'new_value' to be 'new_value'",
},
],
}),
])
})
test("undefined precedence in pm.variables.get (environment over global)", () => {
return expect(
runTest(
`
// Set undefined in both global and environment
pm.globals.set("precedence_test", undefined)
pm.environment.set("precedence_test", undefined)
// pm.variables should return environment's undefined
const value = pm.variables.get("precedence_test")
pm.expect(value).toBe(undefined)
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'undefined' to be 'undefined'",
},
],
}),
])
})
})

View file

@ -1,35 +1,14 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.tests)
)
import { runTest } from "~/utils/test-helpers"
describe("pm.environment additional coverage", () => {
test("pm.environment.set creates and retrieves environment variable", () => {
return expect(
func(
runTest(
`
pm.environment.set("test_set", "set_value")
const retrieved = pm.environment.get("test_set")
pw.expect(retrieved).toBe("set_value")
pm.expect(retrieved).toBe("set_value")
`,
{
global: [],
@ -50,12 +29,12 @@ describe("pm.environment additional coverage", () => {
test("pm.environment.has correctly identifies existing and non-existing variables", () => {
return expect(
func(
runTest(
`
const hasExisting = pm.environment.has("existing_var")
const hasNonExisting = pm.environment.has("non_existing_var")
pw.expect(hasExisting.toString()).toBe("true")
pw.expect(hasNonExisting.toString()).toBe("false")
pm.expect(hasExisting.toString()).toBe("true")
pm.expect(hasNonExisting.toString()).toBe("false")
`,
{
global: [],
@ -84,16 +63,180 @@ describe("pm.environment additional coverage", () => {
}),
])
})
test("pm.environment.toObject returns all environment variables set via pm.environment.set", () => {
return expect(
runTest(
`
pm.environment.set("key1", "value1")
pm.environment.set("key2", "value2")
pm.environment.set("key3", "value3")
const envObj = pm.environment.toObject()
pm.expect(envObj.key1).toBe("value1")
pm.expect(envObj.key2).toBe("value2")
pm.expect(envObj.key3).toBe("value3")
pm.expect(Object.keys(envObj).length.toString()).toBe("3")
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'value1' to be 'value1'",
},
{
status: "pass",
message: "Expected 'value2' to be 'value2'",
},
{
status: "pass",
message: "Expected 'value3' to be 'value3'",
},
{
status: "pass",
message: "Expected '3' to be '3'",
},
],
}),
])
})
test("pm.environment.toObject returns empty object when no variables are set", () => {
return expect(
runTest(
`
const envObj = pm.environment.toObject()
pm.expect(Object.keys(envObj).length.toString()).toBe("0")
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected '0' to be '0'",
},
],
}),
])
})
test("pm.environment.clear removes all environment variables set via pm.environment.set", () => {
return expect(
runTest(
`
pm.environment.set("key1", "value1")
pm.environment.set("key2", "value2")
// Verify variables are set
pm.expect(pm.environment.get("key1")).toBe("value1")
pm.expect(pm.environment.get("key2")).toBe("value2")
// Clear all
pm.environment.clear()
// Verify variables are cleared
pm.expect(pm.environment.get("key1")).toBe(undefined)
pm.expect(pm.environment.get("key2")).toBe(undefined)
// Verify toObject returns empty
const envObj = pm.environment.toObject()
pm.expect(Object.keys(envObj).length.toString()).toBe("0")
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'value1' to be 'value1'",
},
{
status: "pass",
message: "Expected 'value2' to be 'value2'",
},
{
status: "pass",
message: "Expected 'undefined' to be 'undefined'",
},
{
status: "pass",
message: "Expected 'undefined' to be 'undefined'",
},
{
status: "pass",
message: "Expected '0' to be '0'",
},
],
}),
])
})
test("pm.environment.unset removes key from tracking", () => {
return expect(
runTest(
`
pm.environment.set("key1", "value1")
pm.environment.set("key2", "value2")
// Unset one key
pm.environment.unset("key1")
// Verify key1 is removed but key2 remains
const envObj = pm.environment.toObject()
pm.expect(envObj.key1).toBe(undefined)
pm.expect(envObj.key2).toBe("value2")
pm.expect(Object.keys(envObj).length.toString()).toBe("1")
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'undefined' to be 'undefined'",
},
{
status: "pass",
message: "Expected 'value2' to be 'value2'",
},
{
status: "pass",
message: "Expected '1' to be '1'",
},
],
}),
])
})
})
describe("pm.globals additional coverage", () => {
test("pm.globals.set creates and retrieves global variable", () => {
return expect(
func(
runTest(
`
pm.globals.set("test_global", "global_value")
const retrieved = pm.globals.get("test_global")
pw.expect(retrieved).toBe("global_value")
pm.expect(retrieved).toBe("global_value")
`,
{
global: [],
@ -111,16 +254,259 @@ describe("pm.globals additional coverage", () => {
}),
])
})
test("pm.globals.toObject returns all global variables set via pm.globals.set", () => {
return expect(
runTest(
`
pm.globals.set("globalKey1", "globalValue1")
pm.globals.set("globalKey2", "globalValue2")
pm.globals.set("globalKey3", "globalValue3")
const globalObj = pm.globals.toObject()
pm.expect(globalObj.globalKey1).toBe("globalValue1")
pm.expect(globalObj.globalKey2).toBe("globalValue2")
pm.expect(globalObj.globalKey3).toBe("globalValue3")
pm.expect(Object.keys(globalObj).length.toString()).toBe("3")
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'globalValue1' to be 'globalValue1'",
},
{
status: "pass",
message: "Expected 'globalValue2' to be 'globalValue2'",
},
{
status: "pass",
message: "Expected 'globalValue3' to be 'globalValue3'",
},
{
status: "pass",
message: "Expected '3' to be '3'",
},
],
}),
])
})
test("pm.globals.toObject returns empty object when no globals are set", () => {
return expect(
runTest(
`
const globalObj = pm.globals.toObject()
pm.expect(Object.keys(globalObj).length.toString()).toBe("0")
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected '0' to be '0'",
},
],
}),
])
})
test("pm.globals.clear removes all global variables set via pm.globals.set", () => {
return expect(
runTest(
`
pm.globals.set("globalKey1", "globalValue1")
pm.globals.set("globalKey2", "globalValue2")
// Verify variables are set
pm.expect(pm.globals.get("globalKey1")).toBe("globalValue1")
pm.expect(pm.globals.get("globalKey2")).toBe("globalValue2")
// Clear all
pm.globals.clear()
// Verify variables are cleared
pm.expect(pm.globals.get("globalKey1")).toBe(undefined)
pm.expect(pm.globals.get("globalKey2")).toBe(undefined)
// Verify toObject returns empty
const globalObj = pm.globals.toObject()
pm.expect(Object.keys(globalObj).length.toString()).toBe("0")
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'globalValue1' to be 'globalValue1'",
},
{
status: "pass",
message: "Expected 'globalValue2' to be 'globalValue2'",
},
{
status: "pass",
message: "Expected 'undefined' to be 'undefined'",
},
{
status: "pass",
message: "Expected 'undefined' to be 'undefined'",
},
{
status: "pass",
message: "Expected '0' to be '0'",
},
],
}),
])
})
test("pm.globals.clear also removes initial global variables from environment", () => {
return expect(
runTest(
`
// Verify initial globals exist
pm.expect(pm.globals.get("initial_global1")).toBe("initial_value1")
pm.expect(pm.globals.get("initial_global2")).toBe("initial_value2")
// Add tracked globals
pm.globals.set("tracked_global", "tracked_value")
pm.expect(pm.globals.get("tracked_global")).toBe("tracked_value")
// Verify toObject includes both initial and tracked
const before = pm.globals.toObject()
pm.expect(before.initial_global1).toBe("initial_value1")
pm.expect(before.tracked_global).toBe("tracked_value")
// Clear all (both initial and tracked)
pm.globals.clear()
// Verify ALL globals are cleared
pm.expect(pm.globals.get("initial_global1")).toBe(undefined)
pm.expect(pm.globals.get("initial_global2")).toBe(undefined)
pm.expect(pm.globals.get("tracked_global")).toBe(undefined)
// Verify toObject returns empty
const after = pm.globals.toObject()
pm.expect(Object.keys(after).length.toString()).toBe("0")
`,
{
global: [
{
key: "initial_global1",
currentValue: "initial_value1",
initialValue: "initial_value1",
secret: false,
},
{
key: "initial_global2",
currentValue: "initial_value2",
initialValue: "initial_value2",
secret: false,
},
],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'initial_value1' to be 'initial_value1'",
},
{
status: "pass",
message: "Expected 'initial_value2' to be 'initial_value2'",
},
{
status: "pass",
message: "Expected 'tracked_value' to be 'tracked_value'",
},
{
status: "pass",
message: "Expected 'initial_value1' to be 'initial_value1'",
},
{
status: "pass",
message: "Expected 'tracked_value' to be 'tracked_value'",
},
{ status: "pass", message: "Expected 'undefined' to be 'undefined'" },
{ status: "pass", message: "Expected 'undefined' to be 'undefined'" },
{ status: "pass", message: "Expected 'undefined' to be 'undefined'" },
{ status: "pass", message: "Expected '0' to be '0'" },
],
}),
])
})
test("pm.globals.unset removes key from tracking", () => {
return expect(
runTest(
`
pm.globals.set("globalKey1", "globalValue1")
pm.globals.set("globalKey2", "globalValue2")
// Unset one key
pm.globals.unset("globalKey1")
// Verify key1 is removed but key2 remains
const globalObj = pm.globals.toObject()
pm.expect(globalObj.globalKey1).toBe(undefined)
pm.expect(globalObj.globalKey2).toBe("globalValue2")
pm.expect(Object.keys(globalObj).length.toString()).toBe("1")
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'undefined' to be 'undefined'",
},
{
status: "pass",
message: "Expected 'globalValue2' to be 'globalValue2'",
},
{
status: "pass",
message: "Expected '1' to be '1'",
},
],
}),
])
})
})
describe("pm.variables additional coverage", () => {
test("pm.variables.set creates and retrieves variable in active environment", () => {
return expect(
func(
runTest(
`
pm.variables.set("test_var", "test_value")
const retrieved = pm.variables.get("test_var")
pw.expect(retrieved).toBe("test_value")
pm.expect(retrieved).toBe("test_value")
`,
{
global: [],
@ -141,12 +527,12 @@ describe("pm.variables additional coverage", () => {
test("pm.variables.has correctly identifies existing and non-existing variables", () => {
return expect(
func(
runTest(
`
const hasExisting = pm.variables.has("existing_var")
const hasNonExisting = pm.variables.has("non_existing_var")
pw.expect(hasExisting.toString()).toBe("true")
pw.expect(hasNonExisting.toString()).toBe("false")
pm.expect(hasExisting.toString()).toBe("true")
pm.expect(hasNonExisting.toString()).toBe("false")
`,
{
global: [],
@ -178,10 +564,10 @@ describe("pm.variables additional coverage", () => {
test("pm.variables.get returns the correct value from any scope", () => {
return expect(
func(
runTest(
`
const data = pm.variables.get("scopedVar")
pw.expect(data).toBe("scopedValue")
pm.expect(data).toBe("scopedValue")
`,
{
global: [
@ -209,11 +595,11 @@ describe("pm.variables additional coverage", () => {
test("pm.variables.replaceIn handles multiple variables", () => {
return expect(
func(
runTest(
`
const template = "User {{name}} has {{count}} items in {{location}}"
const result = pm.variables.replaceIn(template)
pw.expect(result).toBe("User Alice has 10 items in Cart")
pm.expect(result).toBe("User Alice has 10 items in Cart")
`,
{
global: [

View file

@ -0,0 +1,310 @@
import { describe, expect, test } from "vitest"
import { TestResponse } from "~/types"
import { runTest } from "~/utils/test-helpers"
const mockResponse: TestResponse = {
status: 200,
statusText: "OK",
responseTime: 0,
body: "OK",
headers: [],
}
describe("pm.expect.fail() - Explicit test failures", () => {
test("pm.expect.fail() with no arguments fails the test", async () => {
const testScript = `
pm.test("explicit failure", () => {
pm.expect.fail();
});
`
const result = await runTest(
testScript,
{ global: [], selected: [] },
mockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "explicit failure",
expectResults: [
expect.objectContaining({
status: "fail",
message: expect.stringContaining("expect.fail()"),
}),
],
}),
]),
}),
])
)
})
test("pm.expect.fail() with custom message", async () => {
const testScript = `
pm.test("custom failure message", () => {
pm.expect.fail("This test intentionally fails");
});
`
const result = await runTest(
testScript,
{ global: [], selected: [] },
mockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "custom failure message",
expectResults: [
expect.objectContaining({
status: "fail",
message: "This test intentionally fails",
}),
],
}),
]),
}),
])
)
})
test("pm.expect.fail() with actual and expected values", async () => {
const testScript = `
pm.test("failure with values", () => {
pm.expect.fail(5, 10);
});
`
const result = await runTest(
testScript,
{ global: [], selected: [] },
mockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "failure with values",
expectResults: [
expect.objectContaining({
status: "fail",
message: expect.stringMatching(/expected.*5.*equal.*10/i),
}),
],
}),
]),
}),
])
)
})
test("pm.expect.fail() practical use case - conditional validation", async () => {
const jsonResponse: TestResponse = {
status: 200,
statusText: "OK",
responseTime: 0,
body: JSON.stringify({ id: 1, name: "Test" }),
headers: [{ key: "Content-Type", value: "application/json" }],
}
const testScript = `
pm.test("validate response", () => {
const data = pm.response.json();
if (!data.email) {
pm.expect.fail("Missing required email field");
}
pm.expect(data).to.be.an("object");
});
`
const result = await runTest(
testScript,
{ global: [], selected: [] },
jsonResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "validate response",
expectResults: expect.arrayContaining([
expect.objectContaining({
status: "fail",
message: "Missing required email field",
}),
]),
}),
]),
}),
])
)
})
})
describe("hopp.expect.fail() - Explicit test failures", () => {
test("hopp.expect.fail() with no arguments fails the test", async () => {
const testScript = `
hopp.test("explicit failure", () => {
hopp.expect.fail();
});
`
const result = await runTest(
testScript,
{ global: [], selected: [] },
mockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "explicit failure",
expectResults: [
expect.objectContaining({
status: "fail",
message: expect.stringContaining("expect.fail()"),
}),
],
}),
]),
}),
])
)
})
test("hopp.expect.fail() with custom message", async () => {
const testScript = `
hopp.test("custom failure message", () => {
hopp.expect.fail("This test intentionally fails");
});
`
const result = await runTest(
testScript,
{ global: [], selected: [] },
mockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "custom failure message",
expectResults: [
expect.objectContaining({
status: "fail",
message: "This test intentionally fails",
}),
],
}),
]),
}),
])
)
})
test("hopp.expect.fail() with actual and expected values", async () => {
const testScript = `
hopp.test("failure with values", () => {
hopp.expect.fail("hello", "world");
});
`
const result = await runTest(
testScript,
{ global: [], selected: [] },
mockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "failure with values",
expectResults: [
expect.objectContaining({
status: "fail",
message: expect.stringMatching(
/expected.*hello.*equal.*world/i
),
}),
],
}),
]),
}),
])
)
})
})
describe("expect.fail() - Cross-namespace compatibility", () => {
test("both pm and hopp namespaces support fail() with same behavior", async () => {
const testScript = `
pm.test("pm namespace fail", () => {
pm.expect.fail("PM failure");
});
hopp.test("hopp namespace fail", () => {
hopp.expect.fail("Hopp failure");
});
`
const result = await runTest(
testScript,
{ global: [], selected: [] },
mockResponse
)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "pm namespace fail",
expectResults: [
expect.objectContaining({
status: "fail",
message: "PM failure",
}),
],
}),
expect.objectContaining({
descriptor: "hopp namespace fail",
expectResults: [
expect.objectContaining({
status: "fail",
message: "Hopp failure",
}),
],
}),
]),
}),
])
)
})
})

View file

@ -2,28 +2,10 @@ import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
import { runTest, defaultRequest, fakeResponse } from "~/utils/test-helpers"
import { runPreRequestScript } from "~/web"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "test response",
headers: [],
}
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: { ...defaultRequest, name: "default-request", id: "test-id" },
response: fakeResponse,
}),
TE.map((x) => x.tests)
)
describe("pm.info context", () => {
test("pm.info.eventName returns 'pre-request' in pre-request context", () => {
const defaultRequest = getDefaultRESTRequest()
@ -50,12 +32,12 @@ describe("pm.info context", () => {
)
})
test("pm.info.eventName returns 'post-request' in post-request context", () => {
test("pm.info.eventName returns 'test' in test context", () => {
return expect(
func(
runTest(
`
pm.test("Event name is correct", () => {
pm.expect(pm.info.eventName).toBe("post-request")
pm.expect(pm.info.eventName).toBe("test")
})
`,
{
@ -71,7 +53,7 @@ describe("pm.info context", () => {
expectResults: [
{
status: "pass",
message: "Expected 'post-request' to be 'post-request'",
message: "Expected 'test' to be 'test'",
},
],
}),
@ -81,8 +63,14 @@ describe("pm.info context", () => {
})
test("pm.info provides requestName and requestId", () => {
const customRequest = {
...defaultRequest,
name: "default-request",
id: "test-id",
}
return expect(
func(
runTest(
`
pm.test("Request info is available", () => {
pm.expect(pm.info.requestName).toBe("default-request")
@ -92,7 +80,9 @@ describe("pm.info context", () => {
{
global: [],
selected: [],
}
},
fakeResponse,
customRequest
)()
).resolves.toEqualRight([
expect.objectContaining({
@ -111,4 +101,36 @@ describe("pm.info context", () => {
}),
])
})
test("pm.info.requestId falls back to requestName when id is undefined", () => {
return expect(
pipe(
runTestScript(
`
pm.test("Request ID fallback works", () => {
pm.expect(pm.info.requestId).to.exist
pm.expect(pm.info.requestId).toBe("fallback-request-name")
})
`,
{
envs: { global: [], selected: [] },
request: { ...defaultRequest, name: "fallback-request-name" },
response: fakeResponse,
}
),
TE.map((x) => x.tests)
)()
).resolves.toEqualRight([
expect.objectContaining({
children: [
expect.objectContaining({
descriptor: "Request ID fallback works",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
})

View file

@ -0,0 +1,204 @@
// Map/Set serialize as {} across sandbox boundary, so we extract .size before serialization
import { describe, expect, test } from "vitest"
import { runTest } from "~/utils/test-helpers"
describe("Map.size property assertions", () => {
test("should support .property('size') for Map", async () => {
const testScript = `
pm.test("Map - size property", () => {
const myMap = new Map([['key1', 'value1'], ['key2', 'value2']]);
pm.expect(myMap).to.have.property('size', 2);
});
`
const result = await runTest(testScript)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "Map - size property",
expectResults: [expect.objectContaining({ status: "pass" })],
}),
]),
}),
])
)
})
test("should support .property('size') with chaining for Map", async () => {
const testScript = `
pm.test("Map - size with chaining", () => {
const myMap = new Map([['a', 1], ['b', 2], ['c', 3]]);
pm.expect(myMap).to.have.property('size').that.equals(3);
});
`
const result = await runTest(testScript)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "Map - size with chaining",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
]),
}),
])
)
})
test("should support negation for Map size", async () => {
const testScript = `
pm.test("Map - size negation", () => {
const myMap = new Map([['key1', 'value1']]);
pm.expect(myMap).to.not.have.property('size', 5);
});
`
const result = await runTest(testScript)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "Map - size negation",
expectResults: [expect.objectContaining({ status: "pass" })],
}),
]),
}),
])
)
})
})
describe("Set.size property assertions", () => {
test("should support .property('size') for Set", async () => {
const testScript = `
pm.test("Set - size property", () => {
const mySet = new Set([1, 2, 3]);
pm.expect(mySet).to.have.property('size', 3);
});
`
const result = await runTest(testScript)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "Set - size property",
expectResults: [expect.objectContaining({ status: "pass" })],
}),
]),
}),
])
)
})
test("should support .property('size') with chaining for Set", async () => {
const testScript = `
pm.test("Set - size with chaining", () => {
const mySet = new Set(['a', 'b', 'c', 'd']);
pm.expect(mySet).to.have.property('size').that.is.above(2);
});
`
const result = await runTest(testScript)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "Set - size with chaining",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
]),
}),
])
)
})
test("should support negation for Set size", async () => {
const testScript = `
pm.test("Set - size negation", () => {
const mySet = new Set([1, 2]);
pm.expect(mySet).to.not.have.property('size', 10);
});
`
const result = await runTest(testScript)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "Set - size negation",
expectResults: [expect.objectContaining({ status: "pass" })],
}),
]),
}),
])
)
})
test("should handle empty Set", async () => {
const testScript = `
pm.test("Set - empty size", () => {
const mySet = new Set();
pm.expect(mySet).to.have.property('size', 0);
});
`
const result = await runTest(testScript)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "Set - empty size",
expectResults: [expect.objectContaining({ status: "pass" })],
}),
]),
}),
])
)
})
test("should handle empty Map", async () => {
const testScript = `
pm.test("Map - empty size", () => {
const myMap = new Map();
pm.expect(myMap).to.have.property('size', 0);
});
`
const result = await runTest(testScript)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "Map - empty size",
expectResults: [expect.objectContaining({ status: "pass" })],
}),
]),
}),
])
)
})
})

View file

@ -0,0 +1,513 @@
import { describe, expect, test } from "vitest"
import { getDefaultRESTRequest } from "@hoppscotch/data"
import { runPreRequestScript } from "~/node"
const DEFAULT_REQUEST = getDefaultRESTRequest()
// Pre-request scripts use markers to preserve null/undefined across serialization
describe("PM namespace type preservation in pre-request context", () => {
const emptyEnvs = {
envs: {
global: [],
selected: [],
},
request: DEFAULT_REQUEST,
}
describe("pm.environment.set() type preservation", () => {
test("preserves arrays (not String() coercion to '1,2,3')", () => {
return expect(
runPreRequestScript(
`
pm.environment.set('testArray', [1, 2, 3])
`,
emptyEnvs
)()
).resolves.toEqualRight({
updatedEnvs: {
global: [],
selected: [
{
key: "testArray",
currentValue: "[1,2,3]", // JSON stringified for UI display
initialValue: "[1,2,3]",
secret: false,
},
],
},
updatedRequest: DEFAULT_REQUEST,
updatedCookies: null,
})
})
test("preserves objects (not String() coercion to '[object Object]')", () => {
return expect(
runPreRequestScript(
`
pm.environment.set('testObj', { foo: 'bar', num: 42 })
`,
emptyEnvs
)()
).resolves.toEqualRight({
updatedEnvs: {
global: [],
selected: [
{
key: "testObj",
currentValue: '{"foo":"bar","num":42}', // JSON stringified for UI display
secret: false,
initialValue: '{"foo":"bar","num":42}',
},
],
},
updatedRequest: DEFAULT_REQUEST,
updatedCookies: null,
})
})
test("preserves null with NULL_MARKER", () => {
return expect(
runPreRequestScript(
`
pm.environment.set('nullValue', null)
`,
emptyEnvs
)()
).resolves.toEqualRight({
updatedEnvs: {
global: [],
selected: [
{
key: "nullValue",
currentValue: "null", // Converted from NULL_MARKER by getUpdatedEnvs
initialValue: "null",
secret: false,
},
],
},
updatedRequest: DEFAULT_REQUEST,
updatedCookies: null,
})
})
test("preserves undefined with UNDEFINED_MARKER", () => {
return expect(
runPreRequestScript(
`
pm.environment.set('undefinedValue', undefined)
`,
emptyEnvs
)()
).resolves.toEqualRight({
updatedEnvs: {
global: [],
selected: [
{
key: "undefinedValue",
currentValue: "undefined", // Converted from UNDEFINED_MARKER by getUpdatedEnvs
initialValue: "undefined",
secret: false,
},
],
},
updatedRequest: DEFAULT_REQUEST,
updatedCookies: null,
})
})
test("preserves nested structures", () => {
return expect(
runPreRequestScript(
`
pm.environment.set('nested', {
users: [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" }
],
meta: { count: 2, filters: ["active", "verified"] }
})
`,
emptyEnvs
)()
).resolves.toEqualRight({
updatedEnvs: {
global: [],
selected: [
{
key: "nested",
currentValue:
'{"users":[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}],"meta":{"count":2,"filters":["active","verified"]}}',
initialValue:
'{"users":[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}],"meta":{"count":2,"filters":["active","verified"]}}',
secret: false,
},
],
},
updatedRequest: DEFAULT_REQUEST,
updatedCookies: null,
})
})
test("preserves primitives correctly", () => {
return expect(
runPreRequestScript(
`
pm.environment.set('str', 'hello')
pm.environment.set('num', 42)
pm.environment.set('bool', true)
`,
emptyEnvs
)()
).resolves.toEqualRight({
updatedEnvs: {
global: [],
selected: [
{
key: "str",
currentValue: "hello",
initialValue: "hello",
secret: false,
},
{
key: "num",
currentValue: "42", // Converted to string for UI compatibility
initialValue: "42",
secret: false,
},
{
key: "bool",
currentValue: "true", // Converted to string for UI compatibility
initialValue: "true",
secret: false,
},
],
},
updatedRequest: DEFAULT_REQUEST,
updatedCookies: null,
})
})
})
describe("pm.globals.set() type preservation", () => {
test("preserves arrays in globals", () => {
return expect(
runPreRequestScript(
`
pm.globals.set('globalArray', [10, 20, 30])
`,
emptyEnvs
)()
).resolves.toEqualRight({
updatedEnvs: {
global: [
{
key: "globalArray",
currentValue: "[10,20,30]",
initialValue: "[10,20,30]",
secret: false,
},
],
selected: [],
},
updatedRequest: DEFAULT_REQUEST,
updatedCookies: null,
})
})
test("preserves objects in globals", () => {
return expect(
runPreRequestScript(
`
pm.globals.set('globalObj', { env: 'prod', port: 8080 })
`,
emptyEnvs
)()
).resolves.toEqualRight({
updatedEnvs: {
global: [
{
key: "globalObj",
currentValue: '{"env":"prod","port":8080}',
initialValue: '{"env":"prod","port":8080}',
secret: false,
},
],
selected: [],
},
updatedRequest: DEFAULT_REQUEST,
updatedCookies: null,
})
})
test("preserves null in globals", () => {
return expect(
runPreRequestScript(
`
pm.globals.set('globalNull', null)
`,
emptyEnvs
)()
).resolves.toEqualRight({
updatedEnvs: {
global: [
{
key: "globalNull",
currentValue: "null", // Converted from NULL_MARKER
initialValue: "null",
secret: false,
},
],
selected: [],
},
updatedRequest: DEFAULT_REQUEST,
updatedCookies: null,
})
})
test("preserves undefined in globals", () => {
return expect(
runPreRequestScript(
`
pm.globals.set('globalUndefined', undefined)
`,
emptyEnvs
)()
).resolves.toEqualRight({
updatedEnvs: {
global: [
{
key: "globalUndefined",
currentValue: "undefined", // Converted from UNDEFINED_MARKER
initialValue: "undefined",
secret: false,
},
],
selected: [],
},
updatedRequest: DEFAULT_REQUEST,
updatedCookies: null,
})
})
})
describe("pm.variables.set() type preservation", () => {
test("preserves arrays (uses active scope)", () => {
return expect(
runPreRequestScript(
`
pm.variables.set('varArray', [5, 10, 15])
`,
emptyEnvs
)()
).resolves.toEqualRight({
updatedEnvs: {
global: [],
selected: [
{
key: "varArray",
currentValue: "[5,10,15]",
initialValue: "[5,10,15]",
secret: false,
},
],
},
updatedRequest: DEFAULT_REQUEST,
updatedCookies: null,
})
})
test("preserves objects", () => {
return expect(
runPreRequestScript(
`
pm.variables.set('varObj', { status: 'active', count: 100 })
`,
emptyEnvs
)()
).resolves.toEqualRight({
updatedEnvs: {
global: [],
selected: [
{
key: "varObj",
currentValue: '{"status":"active","count":100}',
initialValue: '{"status":"active","count":100}',
secret: false,
},
],
},
updatedRequest: DEFAULT_REQUEST,
updatedCookies: null,
})
})
test("preserves null", () => {
return expect(
runPreRequestScript(
`
pm.variables.set('varNull', null)
`,
emptyEnvs
)()
).resolves.toEqualRight({
updatedEnvs: {
global: [],
selected: [
{
key: "varNull",
currentValue: "null",
initialValue: "null",
secret: false,
},
],
},
updatedRequest: DEFAULT_REQUEST,
updatedCookies: null,
})
})
})
describe("Regression tests for String() coercion bug", () => {
test("CRITICAL: does NOT convert [1,2,3] to '1,2,3' string", () => {
return expect(
runPreRequestScript(
`
pm.environment.set('arr', [1, 2, 3])
`,
emptyEnvs
)()
).resolves.toEqualRight({
updatedEnvs: {
global: [],
selected: [
{
key: "arr",
currentValue: "[1,2,3]", // JSON stringified for UI display
initialValue: "[1,2,3]",
secret: false,
},
],
},
updatedRequest: DEFAULT_REQUEST,
updatedCookies: null,
})
})
test("CRITICAL: does NOT convert object to '[object Object]'", () => {
return expect(
runPreRequestScript(
`
pm.environment.set('obj', { foo: 'bar' })
`,
emptyEnvs
)()
).resolves.toEqualRight({
updatedEnvs: {
global: [],
selected: [
{
key: "obj",
currentValue: '{"foo":"bar"}', // Object (JSON string for UI), not "[object Object]" string
initialValue: '{"foo":"bar"}',
secret: false,
},
],
},
updatedRequest: DEFAULT_REQUEST,
updatedCookies: null,
})
})
})
describe("Complex scenarios", () => {
test("mixed array of primitives, null, undefined, objects", () => {
return expect(
runPreRequestScript(
`
pm.environment.set('mixed', [
'string',
42,
true,
null,
undefined,
[1, 2],
{ key: 'value' }
])
`,
emptyEnvs
)()
).resolves.toEqualRight({
updatedEnvs: {
global: [],
selected: [
{
key: "mixed",
currentValue:
'["string",42,true,null,null,[1,2],{"key":"value"}]',
initialValue:
'["string",42,true,null,null,[1,2],{"key":"value"}]', // JSON stringified for UI display
secret: false,
},
],
},
updatedRequest: DEFAULT_REQUEST,
updatedCookies: null,
})
})
test("multiple PM namespace calls in same pre-request", () => {
return expect(
runPreRequestScript(
`
pm.environment.set('arr1', [1, 2])
pm.globals.set('arr2', [3, 4])
pm.variables.set('arr3', [5, 6])
pm.environment.set('obj1', { a: 1 })
pm.globals.set('obj2', { b: 2 })
`,
emptyEnvs
)()
).resolves.toEqualRight({
updatedEnvs: {
selected: [
{
key: "arr1",
currentValue: "[1,2]",
initialValue: "[1,2]",
secret: false,
},
{
key: "arr3",
currentValue: "[5,6]",
initialValue: "[5,6]",
secret: false,
},
{
key: "obj1",
currentValue: '{"a":1}',
initialValue: '{"a":1}',
secret: false,
},
],
global: [
{
key: "arr2",
currentValue: "[3,4]",
initialValue: "[3,4]",
secret: false,
},
{
key: "obj2",
currentValue: '{"b":2}',
initialValue: '{"b":2}',
secret: false,
},
],
},
updatedRequest: DEFAULT_REQUEST,
updatedCookies: null,
})
})
})
})

View file

@ -0,0 +1,216 @@
// .property('key') returns a NEW expectation wrapping the property value
import { describe, expect, test } from "vitest"
import { runTest } from "~/utils/test-helpers"
describe("property() with .that chaining", () => {
test("should chain property value with .that.equals()", async () => {
const testScript = `
pm.test("property chaining with that", () => {
pm.expect({ a: 1, b: 2 }).to.have.property('a').that.equals(1);
});
`
const result = await runTest(testScript)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "property chaining with that",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
]),
}),
])
)
})
test("should chain nested property with .that.is.an()", async () => {
const testScript = `
pm.test("nested property chaining", () => {
pm.expect({ nested: { value: 42 } })
.to.have.property('nested')
.that.is.an('object');
});
`
const result = await runTest(testScript)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "nested property chaining",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
]),
}),
])
)
})
test("should support .which as alias for .that", async () => {
const testScript = `
pm.test("property chaining with which", () => {
pm.expect({ x: 1 }).to.have.property('x').which.equals(1);
});
`
const result = await runTest(testScript)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "property chaining with which",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
]),
}),
])
)
})
test("should support complex chaining", async () => {
const testScript = `
pm.test("complex property chaining", () => {
pm.expect({ name: 'John', age: 30 })
.to.be.an('object')
.and.have.property('name')
.that.is.a('string')
.and.equals('John');
});
`
const result = await runTest(testScript)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "complex property chaining",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
]),
}),
])
)
})
test("should fail when chained value doesn't match", async () => {
const testScript = `
pm.test("property chaining fails correctly", () => {
pm.expect({ a: 1, b: 2 }).to.have.property('a').that.equals(2);
});
`
const result = await runTest(testScript)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "property chaining fails correctly",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "fail" }),
]),
}),
]),
}),
])
)
})
})
describe("property() with value parameter", () => {
test("should assert property value directly", async () => {
const testScript = `
pm.test("property with value", () => {
pm.expect({ a: 1, b: 2 }).to.have.property('a', 1);
});
`
const result = await runTest(testScript)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "property with value",
expectResults: [expect.objectContaining({ status: "pass" })],
}),
]),
}),
])
)
})
test("should fail when property value doesn't match", async () => {
const testScript = `
pm.test("property value mismatch", () => {
pm.expect({ a: 1, b: 2 }).to.not.have.property('a', 2);
});
`
const result = await runTest(testScript)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "property value mismatch",
expectResults: [expect.objectContaining({ status: "pass" })],
}),
]),
}),
])
)
})
})
describe("own.property() assertions", () => {
test("should check own properties vs inherited", async () => {
const testScript = `
pm.test("own property check", () => {
const obj = Object.create({ inherited: true });
obj.own = true;
pm.expect(obj).to.have.own.property('own');
pm.expect(obj).to.not.have.own.property('inherited');
pm.expect(obj).to.have.property('inherited');
});
`
const result = await runTest(testScript)()
expect(result).toEqualRight(
expect.arrayContaining([
expect.objectContaining({
descriptor: "root",
children: expect.arrayContaining([
expect.objectContaining({
descriptor: "own property check",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
]),
}),
])
)
})
})

View file

@ -1,31 +1,10 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.tests)
)
import { runTest } from "~/utils/test-helpers"
describe("pm.request coverage", () => {
test("pm.request object provides access to request data", () => {
return expect(
func(
runTest(
`
pw.expect(pm.request.url.toString()).toBe("https://echo.hoppscotch.io")
pw.expect(pm.request.method).toBe("GET")
@ -59,7 +38,7 @@ describe("pm.request coverage", () => {
test("pm.request.url provides correct URL value", () => {
return expect(
func(
runTest(
`
pw.expect(pm.request.url.toString()).toBe("https://echo.hoppscotch.io")
pw.expect(pm.request.url.toString().length).toBe(26)
@ -93,7 +72,7 @@ describe("pm.request coverage", () => {
test("pm.request.headers functionality", () => {
return expect(
func(
runTest(
`
pw.expect(pm.request.headers.get("Content-Type")).toBe(null)
pw.expect(pm.request.headers.has("Content-Type")).toBe(false)

View file

@ -0,0 +1,509 @@
import { HoppRESTRequest } from "@hoppscotch/data"
import { describe, expect, test } from "vitest"
import { runPreRequestScript } from "~/web"
const baseRequest: HoppRESTRequest = {
v: "16",
name: "Test Request",
endpoint: "https://api.example.com/users",
method: "POST",
headers: [
{ key: "Content-Type", value: "application/json", active: true },
{ key: "Authorization", value: "Bearer token123", active: true },
{ key: "X-Custom-Header", value: "custom-value", active: true },
],
params: [],
body: { contentType: null, body: null },
auth: { authType: "none", authActive: false },
preRequestScript: "",
testScript: "",
requestVariables: [],
responses: {},
}
const envs = { global: [], selected: [] }
describe("pm.request.headers.find()", () => {
test("finds header by predicate function", () => {
return expect(
runPreRequestScript(
`
const result = pm.request.headers.find((header) => header.key === 'Authorization')
console.log("Found header:", result)
console.log("Header key:", result.key)
console.log("Header value:", result.value)
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: [
"Found header:",
expect.objectContaining({
key: "Authorization",
value: "Bearer token123",
}),
],
}),
expect.objectContaining({ args: ["Header key:", "Authorization"] }),
expect.objectContaining({
args: ["Header value:", "Bearer token123"],
}),
]),
})
)
})
test("finds header by key string (case-insensitive)", () => {
return expect(
runPreRequestScript(
`
const result = pm.request.headers.find('content-type')
console.log("Found by string:", result)
console.log("Value:", result.value)
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: [
"Found by string:",
expect.objectContaining({
key: "Content-Type",
value: "application/json",
}),
],
}),
expect.objectContaining({ args: ["Value:", "application/json"] }),
]),
})
)
})
test("returns null when header not found", () => {
return expect(
runPreRequestScript(
`
const result = pm.request.headers.find('Nonexistent-Header')
console.log("Result:", result)
console.log("Is null:", result === null)
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Result:", null] }),
expect.objectContaining({ args: ["Is null:", true] }),
]),
})
)
})
})
describe("pm.request.headers.indexOf()", () => {
test("returns index of header by key (case-insensitive)", () => {
return expect(
runPreRequestScript(
`
const idx1 = pm.request.headers.indexOf('content-type')
const idx2 = pm.request.headers.indexOf('AUTHORIZATION')
const idx3 = pm.request.headers.indexOf('X-Custom-Header')
console.log("Index of content-type:", idx1)
console.log("Index of AUTHORIZATION:", idx2)
console.log("Index of X-Custom-Header:", idx3)
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Index of content-type:", 0] }),
expect.objectContaining({ args: ["Index of AUTHORIZATION:", 1] }),
expect.objectContaining({ args: ["Index of X-Custom-Header:", 2] }),
]),
})
)
})
test("returns index of header by object (case-insensitive)", () => {
return expect(
runPreRequestScript(
`
const idx = pm.request.headers.indexOf({ key: 'authorization' })
console.log("Index:", idx)
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Index:", 1] }),
]),
})
)
})
test("returns -1 when header not found", () => {
return expect(
runPreRequestScript(
`
const idx = pm.request.headers.indexOf('Nonexistent')
console.log("Index:", idx)
console.log("Is -1:", idx === -1)
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Index:", -1] }),
expect.objectContaining({ args: ["Is -1:", true] }),
]),
})
)
})
})
describe("pm.request.headers.insert()", () => {
test("inserts header before specified key", () => {
return expect(
runPreRequestScript(
`
pm.request.headers.insert({ key: 'X-API-Key', value: 'secret123' }, 'Authorization')
const allHeaders = pm.request.headers.map((h) => h.key)
console.log("All headers:", allHeaders)
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: [
"All headers:",
["Content-Type", "X-API-Key", "Authorization", "X-Custom-Header"],
],
}),
]),
})
)
})
test("appends header if 'before' key not found", () => {
return expect(
runPreRequestScript(
`
pm.request.headers.insert({ key: 'X-New-Header', value: 'new' }, 'NonExistent')
const allHeaders = pm.request.headers.map((h) => h.key)
console.log("All headers:", allHeaders)
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: [
"All headers:",
[
"Content-Type",
"Authorization",
"X-Custom-Header",
"X-New-Header",
],
],
}),
]),
})
)
})
test("appends header when no 'before' specified", () => {
return expect(
runPreRequestScript(
`
pm.request.headers.insert({ key: 'X-Added-Header', value: 'added' })
const allHeaders = pm.request.headers.map((h) => h.key)
console.log("All headers:", allHeaders)
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: [
"All headers:",
[
"Content-Type",
"Authorization",
"X-Custom-Header",
"X-Added-Header",
],
],
}),
]),
})
)
})
test("throws error when item has no key", () => {
return expect(
runPreRequestScript(
`
try {
pm.request.headers.insert({ value: 'test' })
console.log("Should not reach here")
} catch (error) {
console.log("Error caught:", error.message)
}
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: ["Error caught:", "Header must have a 'key' property"],
}),
]),
})
)
})
})
describe("pm.request.headers.append()", () => {
test("moves existing header to end", () => {
return expect(
runPreRequestScript(
`
pm.request.headers.append({ key: 'Content-Type', value: 'application/xml' })
const allHeaders = pm.request.headers.map((h) => h.key)
const contentType = pm.request.headers.get('Content-Type')
console.log("All headers:", allHeaders)
console.log("Content-Type value:", contentType)
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: [
"All headers:",
["Authorization", "X-Custom-Header", "Content-Type"],
],
}),
expect.objectContaining({
args: ["Content-Type value:", "application/xml"],
}),
]),
})
)
})
test("adds new header at end", () => {
return expect(
runPreRequestScript(
`
pm.request.headers.append({ key: 'X-New-Header', value: 'new-value' })
const allHeaders = pm.request.headers.map((h) => h.key)
console.log("All headers:", allHeaders)
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: [
"All headers:",
[
"Content-Type",
"Authorization",
"X-Custom-Header",
"X-New-Header",
],
],
}),
]),
})
)
})
test("throws error when item has no key", () => {
return expect(
runPreRequestScript(
`
try {
pm.request.headers.append({ value: 'test' })
console.log("Should not reach here")
} catch (error) {
console.log("Error caught:", error.message)
}
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: ["Error caught:", "Header must have a 'key' property"],
}),
]),
})
)
})
})
describe("pm.request.headers.assimilate()", () => {
test("updates existing headers and adds new ones from array", () => {
return expect(
runPreRequestScript(
`
pm.request.headers.assimilate([
{ key: 'Content-Type', value: 'text/plain' },
{ key: 'X-API-Key', value: 'key123' }
])
const allHeaders = pm.request.headers.all()
console.log("Content-Type:", allHeaders['Content-Type'])
console.log("Authorization:", allHeaders['Authorization'])
console.log("X-API-Key:", allHeaders['X-API-Key'])
console.log("Count:", pm.request.headers.count())
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Content-Type:", "text/plain"] }),
expect.objectContaining({
args: ["Authorization:", "Bearer token123"],
}),
expect.objectContaining({ args: ["X-API-Key:", "key123"] }),
expect.objectContaining({ args: ["Count:", 4] }),
]),
})
)
})
test("updates existing headers and adds new ones from object", () => {
return expect(
runPreRequestScript(
`
pm.request.headers.assimilate({
'Content-Type': 'application/xml',
'X-New-Header': 'new-value'
})
const allHeaders = pm.request.headers.all()
console.log("All headers:", allHeaders)
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: [
"All headers:",
expect.objectContaining({
"Content-Type": "application/xml",
Authorization: "Bearer token123",
"X-Custom-Header": "custom-value",
"X-New-Header": "new-value",
}),
],
}),
]),
})
)
})
test("prunes headers not in source when prune=true", () => {
return expect(
runPreRequestScript(
`
pm.request.headers.assimilate(
{
'Content-Type': 'application/json',
'X-API-Key': 'key123'
},
true
)
const allHeaders = pm.request.headers.all()
const count = pm.request.headers.count()
console.log("All headers:", allHeaders)
console.log("Count:", count)
console.log("Has Authorization:", pm.request.headers.has('Authorization'))
console.log("Has X-Custom-Header:", pm.request.headers.has('X-Custom-Header'))
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: [
"All headers:",
expect.objectContaining({
"Content-Type": "application/json",
"X-API-Key": "key123",
}),
],
}),
expect.objectContaining({ args: ["Count:", 2] }),
expect.objectContaining({ args: ["Has Authorization:", false] }),
expect.objectContaining({
args: ["Has X-Custom-Header:", false],
}),
]),
})
)
})
test("throws error for invalid source", () => {
return expect(
runPreRequestScript(
`
try {
pm.request.headers.assimilate("invalid")
console.log("Should not reach here")
} catch (error) {
console.log("Error caught:", error.message)
}
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: ["Error caught:", "Source must be an array or object"],
}),
]),
})
)
})
})

View file

@ -0,0 +1,950 @@
import { HoppRESTRequest, getDefaultRESTRequest } from "@hoppscotch/data"
import { describe, expect, test } from "vitest"
import { runPreRequestScript } from "~/web"
const baseRequest: HoppRESTRequest = {
v: "16",
name: "Test Request",
endpoint: "https://api.example.com/users",
method: "GET",
headers: [
{
key: "Content-Type",
value: "application/json",
active: true,
description: "",
},
{
key: "Authorization",
value: "Bearer token123",
active: true,
description: "",
},
{
key: "X-API-Version",
value: "v1",
active: true,
description: "",
},
],
params: [],
body: { contentType: null, body: null },
auth: { authType: "none", authActive: false },
preRequestScript: "",
testScript: "",
requestVariables: [],
responses: {},
}
const envs = { global: [], selected: [] }
describe("pm.request.headers.each()", () => {
test("iterates over all headers", () => {
return expect(
runPreRequestScript(
`
const keys = []
const values = []
pm.request.headers.each((header) => {
keys.push(header.key)
values.push(header.value)
})
console.log("Keys:", keys)
console.log("Values:", values)
console.log("Count:", keys.length)
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: ["Keys:", ["Content-Type", "Authorization", "X-API-Version"]],
}),
expect.objectContaining({
args: ["Values:", ["application/json", "Bearer token123", "v1"]],
}),
expect.objectContaining({ args: ["Count:", 3] }),
]),
})
)
})
test("callback receives complete header object", () => {
return expect(
runPreRequestScript(
`
pm.request.headers.each((header) => {
console.log("Key:", header.key)
console.log("Value:", header.value)
console.log("Has active:", 'active' in header)
return // Check only first header
})
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Key:", "Content-Type"] }),
expect.objectContaining({ args: ["Value:", "application/json"] }),
expect.objectContaining({ args: ["Has active:", true] }),
]),
})
)
})
test("updates after headers are modified", () => {
return expect(
runPreRequestScript(
`
let count1 = 0
pm.request.headers.each(() => { count1++ })
console.log("Initial count:", count1)
pm.request.headers.add({ key: 'X-Custom', value: 'custom-value' })
let count2 = 0
pm.request.headers.each(() => { count2++ })
console.log("Count after add:", count2)
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Initial count:", 3] }),
expect.objectContaining({ args: ["Count after add:", 4] }),
]),
})
)
})
})
describe("pm.request.headers.map()", () => {
test("transforms headers and returns new array", () => {
return expect(
runPreRequestScript(
`
const mapped = pm.request.headers.map((header) => {
return header.key.toLowerCase() + ': ' + header.value
})
console.log("Mapped:", mapped)
console.log("Array length:", mapped.length)
console.log("First item:", mapped[0])
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: [
"Mapped:",
[
"content-type: application/json",
"authorization: Bearer token123",
"x-api-version: v1",
],
],
}),
expect.objectContaining({ args: ["Array length:", 3] }),
expect.objectContaining({
args: ["First item:", "content-type: application/json"],
}),
]),
})
)
})
test("does not modify original headers", () => {
return expect(
runPreRequestScript(
`
const originalCount = pm.request.headers.count()
const mapped = pm.request.headers.map((header) => {
return { modified: header.key }
})
const afterCount = pm.request.headers.count()
console.log("Original count:", originalCount)
console.log("Mapped length:", mapped.length)
console.log("After count:", afterCount)
console.log("Headers unchanged:", originalCount === afterCount)
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Original count:", 3] }),
expect.objectContaining({ args: ["Mapped length:", 3] }),
expect.objectContaining({ args: ["After count:", 3] }),
expect.objectContaining({ args: ["Headers unchanged:", true] }),
]),
})
)
})
test("returns array with custom objects", () => {
return expect(
runPreRequestScript(
`
const customHeaders = pm.request.headers.map((header) => ({
name: header.key,
val: header.value,
length: header.value.length
}))
console.log("First custom:", customHeaders[0])
console.log("Has name:", 'name' in customHeaders[0])
console.log("Has val:", 'val' in customHeaders[0])
console.log("Has length:", 'length' in customHeaders[0])
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: [
"First custom:",
{ name: "Content-Type", val: "application/json", length: 16 },
],
}),
expect.objectContaining({ args: ["Has name:", true] }),
expect.objectContaining({ args: ["Has val:", true] }),
expect.objectContaining({ args: ["Has length:", true] }),
]),
})
)
})
})
describe("pm.request.headers.filter()", () => {
test("filters headers based on condition", () => {
return expect(
runPreRequestScript(
`
const filtered = pm.request.headers.filter((header) => {
return header.key.startsWith('X-')
})
console.log("Filtered:", filtered)
console.log("Count:", filtered.length)
console.log("Keys:", filtered.map(h => h.key))
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Count:", 1] }),
expect.objectContaining({ args: ["Keys:", ["X-API-Version"]] }),
]),
})
)
})
test("filters by value content", () => {
return expect(
runPreRequestScript(
`
const filtered = pm.request.headers.filter((header) => {
return header.value.includes('Bearer')
})
console.log("Filtered count:", filtered.length)
console.log("Header key:", filtered[0].key)
console.log("Header value:", filtered[0].value)
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Filtered count:", 1] }),
expect.objectContaining({ args: ["Header key:", "Authorization"] }),
expect.objectContaining({
args: ["Header value:", "Bearer token123"],
}),
]),
})
)
})
test("returns empty array when no matches", () => {
return expect(
runPreRequestScript(
`
const filtered = pm.request.headers.filter((header) => {
return header.key === 'NonExistent'
})
console.log("Filtered:", filtered)
console.log("Is array:", Array.isArray(filtered))
console.log("Length:", filtered.length)
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Filtered:", []] }),
expect.objectContaining({ args: ["Is array:", true] }),
expect.objectContaining({ args: ["Length:", 0] }),
]),
})
)
})
test("returns all headers when condition always true", () => {
return expect(
runPreRequestScript(
`
const filtered = pm.request.headers.filter((header) => {
return true
})
console.log("Count:", filtered.length)
console.log("Equals total:", filtered.length === pm.request.headers.count())
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Count:", 3] }),
expect.objectContaining({ args: ["Equals total:", true] }),
]),
})
)
})
})
describe("pm.request.headers.count()", () => {
test("returns correct count of headers", () => {
return expect(
runPreRequestScript(
`
const count = pm.request.headers.count()
console.log("Count:", count)
console.log("Type:", typeof count)
let manualCount = 0
pm.request.headers.each(() => { manualCount++ })
console.log("Manual count:", manualCount)
console.log("Counts match:", count === manualCount)
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Count:", 3] }),
expect.objectContaining({ args: ["Type:", "number"] }),
expect.objectContaining({ args: ["Manual count:", 3] }),
expect.objectContaining({ args: ["Counts match:", true] }),
]),
})
)
})
test("updates after headers are added or removed", () => {
return expect(
runPreRequestScript(
`
console.log("Initial count:", pm.request.headers.count())
pm.request.headers.add({ key: 'X-New-Header', value: 'value' })
console.log("After add:", pm.request.headers.count())
pm.request.headers.remove('Content-Type')
console.log("After remove:", pm.request.headers.count())
pm.request.headers.clear()
console.log("After clear:", pm.request.headers.count())
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Initial count:", 3] }),
expect.objectContaining({ args: ["After add:", 4] }),
expect.objectContaining({ args: ["After remove:", 3] }),
expect.objectContaining({ args: ["After clear:", 0] }),
]),
})
)
})
test("returns 0 for empty headers", () => {
const requestWithoutHeaders: HoppRESTRequest = {
...baseRequest,
headers: [],
}
return expect(
runPreRequestScript(
`
const count = pm.request.headers.count()
console.log("Count:", count)
console.log("Is zero:", count === 0)
`,
{ envs, request: requestWithoutHeaders }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Count:", 0] }),
expect.objectContaining({ args: ["Is zero:", true] }),
]),
})
)
})
})
describe("pm.request.headers.idx()", () => {
test("returns header at specific index", () => {
return expect(
runPreRequestScript(
`
const first = pm.request.headers.idx(0)
const second = pm.request.headers.idx(1)
const third = pm.request.headers.idx(2)
console.log("First:", first)
console.log("Second:", second)
console.log("Third:", third)
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: [
"First:",
{
key: "Content-Type",
value: "application/json",
active: true,
description: "",
},
],
}),
expect.objectContaining({
args: [
"Second:",
{
key: "Authorization",
value: "Bearer token123",
active: true,
description: "",
},
],
}),
expect.objectContaining({
args: [
"Third:",
{
key: "X-API-Version",
value: "v1",
active: true,
description: "",
},
],
}),
]),
})
)
})
test("returns null for out-of-bounds index", () => {
return expect(
runPreRequestScript(
`
const header = pm.request.headers.idx(999)
console.log("Out of bounds:", header)
console.log("Is null:", header === null)
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Out of bounds:", null] }),
expect.objectContaining({ args: ["Is null:", true] }),
]),
})
)
})
test("returns null for negative index", () => {
return expect(
runPreRequestScript(
`
const header = pm.request.headers.idx(-1)
console.log("Negative index:", header)
console.log("Is null:", header === null)
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Negative index:", null] }),
expect.objectContaining({ args: ["Is null:", true] }),
]),
})
)
})
test("can access header properties via idx", () => {
return expect(
runPreRequestScript(
`
const header = pm.request.headers.idx(0)
console.log("Key:", header.key)
console.log("Value:", header.value)
console.log("Active:", header.active)
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Key:", "Content-Type"] }),
expect.objectContaining({ args: ["Value:", "application/json"] }),
expect.objectContaining({ args: ["Active:", true] }),
]),
})
)
})
})
describe("pm.request.headers.clear()", () => {
test("removes all headers", () => {
return expect(
runPreRequestScript(
`
console.log("Count before:", pm.request.headers.count())
console.log("Headers before:", pm.request.headers.toObject())
pm.request.headers.clear()
console.log("Count after:", pm.request.headers.count())
console.log("Headers after:", pm.request.headers.toObject())
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
updatedRequest: expect.objectContaining({
headers: [],
}),
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Count before:", 3] }),
expect.objectContaining({ args: ["Count after:", 0] }),
expect.objectContaining({ args: ["Headers after:", {}] }),
]),
})
)
})
test("allows adding headers after clearing", () => {
return expect(
runPreRequestScript(
`
pm.request.headers.clear()
console.log("After clear:", pm.request.headers.count())
pm.request.headers.add({ key: 'X-New', value: 'value' })
console.log("After add:", pm.request.headers.count())
console.log("Headers:", pm.request.headers.toObject())
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["After clear:", 0] }),
expect.objectContaining({ args: ["After add:", 1] }),
expect.objectContaining({ args: ["Headers:", { "X-New": "value" }] }),
]),
})
)
})
test("clear followed by get returns null", () => {
return expect(
runPreRequestScript(
`
console.log("Before clear - Content-Type:", pm.request.headers.get('Content-Type'))
pm.request.headers.clear()
console.log("After clear - Content-Type:", pm.request.headers.get('Content-Type'))
console.log("After clear - has Content-Type:", pm.request.headers.has('Content-Type'))
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: ["Before clear - Content-Type:", "application/json"],
}),
expect.objectContaining({
args: ["After clear - Content-Type:", null],
}),
expect.objectContaining({
args: ["After clear - has Content-Type:", false],
}),
]),
})
)
})
})
describe("pm.request.headers.toObject()", () => {
test("returns headers as key-value object", () => {
return expect(
runPreRequestScript(
`
const obj = pm.request.headers.toObject()
console.log("Headers object:", obj)
console.log("Type:", typeof obj)
console.log("Content-Type:", obj['Content-Type'])
console.log("Authorization:", obj['Authorization'])
console.log("X-API-Version:", obj['X-API-Version'])
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: [
"Headers object:",
{
"Content-Type": "application/json",
Authorization: "Bearer token123",
"X-API-Version": "v1",
},
],
}),
expect.objectContaining({ args: ["Type:", "object"] }),
expect.objectContaining({
args: ["Content-Type:", "application/json"],
}),
expect.objectContaining({
args: ["Authorization:", "Bearer token123"],
}),
expect.objectContaining({ args: ["X-API-Version:", "v1"] }),
]),
})
)
})
test("matches all() method", () => {
return expect(
runPreRequestScript(
`
const toObjectResult = pm.request.headers.toObject()
const allResult = pm.request.headers.all()
console.log("toObject:", toObjectResult)
console.log("all:", allResult)
console.log("Are equal:", JSON.stringify(toObjectResult) === JSON.stringify(allResult))
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Are equal:", true] }),
]),
})
)
})
test("returns empty object for empty headers", () => {
const requestWithoutHeaders: HoppRESTRequest = {
...baseRequest,
headers: [],
}
return expect(
runPreRequestScript(
`
const obj = pm.request.headers.toObject()
console.log("Headers object:", obj)
console.log("Keys count:", Object.keys(obj).length)
console.log("Is empty:", Object.keys(obj).length === 0)
`,
{ envs, request: requestWithoutHeaders }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Headers object:", {}] }),
expect.objectContaining({ args: ["Keys count:", 0] }),
expect.objectContaining({ args: ["Is empty:", true] }),
]),
})
)
})
test("updates dynamically after mutations", () => {
return expect(
runPreRequestScript(
`
const before = pm.request.headers.toObject()
console.log("Before:", before)
pm.request.headers.add({ key: 'X-Custom', value: 'test' })
const after = pm.request.headers.toObject()
console.log("After:", after)
console.log("Has X-Custom:", 'X-Custom' in after)
console.log("X-Custom value:", after['X-Custom'])
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Has X-Custom:", true] }),
expect.objectContaining({ args: ["X-Custom value:", "test"] }),
]),
})
)
})
})
describe("combined headers PropertyList operations", () => {
test("chaining multiple PropertyList methods", () => {
return expect(
runPreRequestScript(
`
const filteredAndMapped = pm.request.headers
.filter(h => h.key.startsWith('X-') || h.key === 'Content-Type')
.map(h => ({ name: h.key, length: h.value.length }))
console.log("Result:", filteredAndMapped)
console.log("Count:", filteredAndMapped.length)
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: [
"Result:",
[
{ name: "Content-Type", length: 16 },
{ name: "X-API-Version", length: 2 },
],
],
}),
expect.objectContaining({ args: ["Count:", 2] }),
]),
})
)
})
test("using each to build custom structure", () => {
return expect(
runPreRequestScript(
`
const headerMap = new Map()
pm.request.headers.each((header) => {
headerMap.set(header.key.toLowerCase(), header.value.toUpperCase())
})
console.log("Map size:", headerMap.size)
console.log("content-type:", headerMap.get('content-type'))
console.log("authorization:", headerMap.get('authorization'))
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Map size:", 3] }),
expect.objectContaining({
args: ["content-type:", "APPLICATION/JSON"],
}),
expect.objectContaining({
args: ["authorization:", "BEARER TOKEN123"],
}),
]),
})
)
})
})
describe("pm.request.headers.remove() - case insensitive", () => {
const envs: TestResult["envs"] = {
global: [],
selected: [],
}
const baseRequest: HoppRESTRequest = {
...getDefaultRESTRequest(),
name: "Test",
method: "GET",
endpoint: "https://example.com/api",
headers: [
{ key: "Content-Type", value: "application/json", active: true },
{ key: "Authorization", value: "Bearer token123", active: true },
{ key: "X-Custom-Header", value: "custom-value", active: true },
],
}
test("removes header with exact case match", () => {
return expect(
runPreRequestScript(
`
const hasContentType = pm.request.headers.has("Content-Type")
pm.request.headers.remove("Content-Type")
const afterRemove = pm.request.headers.has("Content-Type")
console.log("Has before:", hasContentType)
console.log("Has after:", afterRemove)
console.log("Count after:", pm.request.headers.count())
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
updatedRequest: expect.objectContaining({
headers: expect.not.arrayContaining([
expect.objectContaining({ key: "Content-Type" }),
]),
}),
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Has before:", true] }),
expect.objectContaining({ args: ["Has after:", false] }),
expect.objectContaining({ args: ["Count after:", 2] }),
]),
})
)
})
test("removes header with different case (lowercase)", () => {
return expect(
runPreRequestScript(
`
const hasAuth = pm.request.headers.has("Authorization")
// Remove with lowercase - should be case-insensitive
pm.request.headers.remove("authorization")
const afterRemove = pm.request.headers.has("Authorization")
console.log("Has before:", hasAuth)
console.log("Has after:", afterRemove)
console.log("Count after:", pm.request.headers.count())
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
updatedRequest: expect.objectContaining({
headers: expect.not.arrayContaining([
expect.objectContaining({
key: expect.stringMatching(/^authorization$/i),
}),
]),
}),
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Has before:", true] }),
expect.objectContaining({ args: ["Has after:", false] }),
expect.objectContaining({ args: ["Count after:", 2] }),
]),
})
)
})
test("removes header with different case (UPPERCASE)", () => {
return expect(
runPreRequestScript(
`
const hasCustom = pm.request.headers.has("X-Custom-Header")
// Remove with uppercase - should be case-insensitive
pm.request.headers.remove("X-CUSTOM-HEADER")
const afterRemove = pm.request.headers.has("X-Custom-Header")
console.log("Has before:", hasCustom)
console.log("Has after:", afterRemove)
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
updatedRequest: expect.objectContaining({
headers: expect.not.arrayContaining([
expect.objectContaining({
key: expect.stringMatching(/^x-custom-header$/i),
}),
]),
}),
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Has before:", true] }),
expect.objectContaining({ args: ["Has after:", false] }),
]),
})
)
})
test("multiple remove operations with mixed case", () => {
return expect(
runPreRequestScript(
`
pm.request.headers.remove("Content-Type")
pm.request.headers.remove("authorization") // lowercase
const hasContentType = pm.request.headers.has("Content-Type")
const hasAuth = pm.request.headers.has("Authorization")
const hasCustom = pm.request.headers.has("X-Custom-Header")
console.log("Final count:", pm.request.headers.count())
console.log("Has Content-Type:", hasContentType)
console.log("Has Authorization:", hasAuth)
console.log("Has Custom:", hasCustom)
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
updatedRequest: expect.objectContaining({
headers: [
{ key: "X-Custom-Header", value: "custom-value", active: true },
],
}),
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Final count:", 1] }),
expect.objectContaining({ args: ["Has Content-Type:", false] }),
expect.objectContaining({ args: ["Has Authorization:", false] }),
expect.objectContaining({ args: ["Has Custom:", true] }),
]),
})
)
})
})

View file

@ -0,0 +1,633 @@
import { HoppRESTRequest } from "@hoppscotch/data"
import { describe, expect, test } from "vitest"
import { runPreRequestScript } from "~/web"
const baseRequest: HoppRESTRequest = {
v: "16",
name: "Test Request",
endpoint:
"https://api.example.com:8080/v1/users/profile?filter=active&sort=name",
method: "GET",
headers: [
{
key: "Content-Type",
value: "application/json",
active: true,
description: "",
},
],
params: [
{ key: "filter", value: "active", active: true, description: "" },
{ key: "sort", value: "name", active: true, description: "" },
],
body: { contentType: null, body: null },
auth: { authType: "none", authActive: false },
preRequestScript: "",
testScript: "",
requestVariables: [],
responses: {},
}
const envs = { global: [], selected: [] }
describe("pm.request.url.getHost()", () => {
test("returns hostname as a string", () => {
return expect(
runPreRequestScript(
`
const host = pm.request.url.getHost()
console.log("Host:", host)
console.log("Host type:", typeof host)
console.log("Is string:", typeof host === 'string')
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Host:", "api.example.com"] }),
expect.objectContaining({ args: ["Host type:", "string"] }),
expect.objectContaining({ args: ["Is string:", true] }),
]),
})
)
})
test("updates after host mutation", () => {
return expect(
runPreRequestScript(
`
console.log("Initial host:", pm.request.url.getHost())
pm.request.url.host = ['newapi', 'test', 'com']
console.log("Updated host:", pm.request.url.getHost())
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: ["Initial host:", "api.example.com"],
}),
expect.objectContaining({
args: ["Updated host:", "newapi.test.com"],
}),
]),
})
)
})
})
describe("pm.request.url.getPath()", () => {
test("returns path as string with leading slash", () => {
return expect(
runPreRequestScript(
`
const path = pm.request.url.getPath()
console.log("Path:", path)
console.log("Starts with slash:", path.startsWith('/'))
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Path:", "/v1/users/profile"] }),
expect.objectContaining({ args: ["Starts with slash:", true] }),
]),
})
)
})
test("returns '/' for empty path", () => {
const requestWithoutPath: HoppRESTRequest = {
...baseRequest,
endpoint: "https://api.example.com",
}
return expect(
runPreRequestScript(
`
const path = pm.request.url.getPath()
console.log("Path:", path)
console.log("Is root:", path === '/')
`,
{ envs, request: requestWithoutPath }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Path:", "/"] }),
expect.objectContaining({ args: ["Is root:", true] }),
]),
})
)
})
test("updates after path mutation", () => {
return expect(
runPreRequestScript(
`
console.log("Initial path:", pm.request.url.getPath())
pm.request.url.path = ['api', 'v2', 'posts']
console.log("Updated path:", pm.request.url.getPath())
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: ["Initial path:", "/v1/users/profile"],
}),
expect.objectContaining({ args: ["Updated path:", "/api/v2/posts"] }),
]),
})
)
})
})
describe("pm.request.url.getPathWithQuery()", () => {
test("returns path with query string", () => {
return expect(
runPreRequestScript(
`
const pathWithQuery = pm.request.url.getPathWithQuery()
console.log("Path with query:", pathWithQuery)
console.log("Includes path:", pathWithQuery.includes('/v1/users/profile'))
console.log("Includes query:", pathWithQuery.includes('filter=active'))
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: [
"Path with query:",
"/v1/users/profile?filter=active&sort=name",
],
}),
expect.objectContaining({ args: ["Includes path:", true] }),
expect.objectContaining({ args: ["Includes query:", true] }),
]),
})
)
})
test("returns only path when no query parameters", () => {
const requestWithoutQuery: HoppRESTRequest = {
...baseRequest,
endpoint: "https://api.example.com/users",
params: [],
}
return expect(
runPreRequestScript(
`
const pathWithQuery = pm.request.url.getPathWithQuery()
console.log("Path with query:", pathWithQuery)
console.log("Has question mark:", pathWithQuery.includes('?'))
`,
{ envs, request: requestWithoutQuery }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Path with query:", "/users"] }),
expect.objectContaining({ args: ["Has question mark:", false] }),
]),
})
)
})
test("updates after query mutation", () => {
return expect(
runPreRequestScript(
`
console.log("Initial:", pm.request.url.getPathWithQuery())
pm.request.url.query.add({ key: 'page', value: '5' })
console.log("Updated:", pm.request.url.getPathWithQuery())
console.log("Includes new param:", pm.request.url.getPathWithQuery().includes('page=5'))
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: ["Initial:", "/v1/users/profile?filter=active&sort=name"],
}),
expect.objectContaining({
args: [
"Updated:",
"/v1/users/profile?filter=active&sort=name&page=5",
],
}),
expect.objectContaining({ args: ["Includes new param:", true] }),
]),
})
)
})
})
describe("pm.request.url.getQueryString()", () => {
test("returns query string without leading question mark", () => {
return expect(
runPreRequestScript(
`
const queryString = pm.request.url.getQueryString()
console.log("Query string:", queryString)
console.log("Starts with question mark:", queryString.startsWith('?'))
console.log("Contains filter:", queryString.includes('filter=active'))
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: ["Query string:", "filter=active&sort=name"],
}),
expect.objectContaining({
args: ["Starts with question mark:", false],
}),
expect.objectContaining({ args: ["Contains filter:", true] }),
]),
})
)
})
test("returns empty string when no query parameters", () => {
const requestWithoutQuery: HoppRESTRequest = {
...baseRequest,
endpoint: "https://api.example.com/users",
params: [],
}
return expect(
runPreRequestScript(
`
const queryString = pm.request.url.getQueryString()
console.log("Query string:", queryString)
console.log("Is empty:", queryString === '')
console.log("Length:", queryString.length)
`,
{ envs, request: requestWithoutQuery }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Query string:", ""] }),
expect.objectContaining({ args: ["Is empty:", true] }),
expect.objectContaining({ args: ["Length:", 0] }),
]),
})
)
})
})
describe("pm.request.url.getRemote()", () => {
test("includes port when not standard (443/80)", () => {
return expect(
runPreRequestScript(
`
const remote = pm.request.url.getRemote()
console.log("Remote:", remote)
console.log("Includes port:", remote.includes(':8080'))
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: ["Remote:", "api.example.com:8080"],
}),
expect.objectContaining({ args: ["Includes port:", true] }),
]),
})
)
})
test("excludes standard HTTPS port (443) by default", () => {
const requestWithStandardPort: HoppRESTRequest = {
...baseRequest,
endpoint: "https://api.example.com/users",
}
return expect(
runPreRequestScript(
`
const remote = pm.request.url.getRemote()
console.log("Remote:", remote)
console.log("Has port in string:", remote.includes(':'))
`,
{ envs, request: requestWithStandardPort }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Remote:", "api.example.com"] }),
expect.objectContaining({ args: ["Has port in string:", false] }),
]),
})
)
})
test("forces port display when forcePort is true", () => {
const requestWithStandardPort: HoppRESTRequest = {
...baseRequest,
endpoint: "https://api.example.com/users",
}
return expect(
runPreRequestScript(
`
const remote = pm.request.url.getRemote(true)
console.log("Remote with forced port:", remote)
console.log("Has port in string:", remote.includes(':443'))
`,
{ envs, request: requestWithStandardPort }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: ["Remote with forced port:", "api.example.com:443"],
}),
expect.objectContaining({ args: ["Has port in string:", true] }),
]),
})
)
})
})
describe("pm.request.url.update()", () => {
test("updates entire URL from string", () => {
return expect(
runPreRequestScript(
`
console.log("Initial URL:", pm.request.url.toString())
pm.request.url.update('http://newapi.test.com:3000/v2/posts?id=123')
console.log("Updated URL:", pm.request.url.toString())
console.log("Protocol:", pm.request.url.protocol)
console.log("Host:", pm.request.url.getHost())
console.log("Port:", pm.request.url.port)
console.log("Path:", pm.request.url.getPath())
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
updatedRequest: expect.objectContaining({
endpoint: "http://newapi.test.com:3000/v2/posts?id=123",
}),
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Protocol:", "http"] }),
expect.objectContaining({ args: ["Host:", "newapi.test.com"] }),
expect.objectContaining({ args: ["Port:", "3000"] }),
expect.objectContaining({ args: ["Path:", "/v2/posts"] }),
]),
})
)
})
test("accepts object with toString() method", () => {
return expect(
runPreRequestScript(
`
const urlObj = {
toString: () => 'https://custom.api.com/endpoint'
}
pm.request.url.update(urlObj)
console.log("Updated URL:", pm.request.url.toString())
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
updatedRequest: expect.objectContaining({
endpoint: "https://custom.api.com/endpoint",
}),
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: ["Updated URL:", "https://custom.api.com/endpoint"],
}),
]),
})
)
})
test("throws error for invalid input", () => {
return expect(
runPreRequestScript(
`
try {
pm.request.url.update(12345)
console.log("Should not reach here")
} catch (error) {
console.log("Error caught:", error.message)
}
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: expect.arrayContaining([
"Error caught:",
expect.stringContaining("URL update requires"),
]),
}),
]),
})
)
})
})
describe("pm.request.url.addQueryParams()", () => {
test("adds multiple query parameters", () => {
return expect(
runPreRequestScript(
`
console.log("Initial params:", pm.request.url.query.all())
pm.request.url.addQueryParams([
{ key: 'page', value: '1' },
{ key: 'limit', value: '20' }
])
console.log("Updated params:", pm.request.url.query.all())
console.log("URL:", pm.request.url.toString())
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
updatedRequest: expect.objectContaining({
endpoint: expect.stringContaining("page=1&limit=20"),
}),
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: [
"Updated params:",
{ filter: "active", sort: "name", page: "1", limit: "20" },
],
}),
]),
})
)
})
test("handles empty value parameters", () => {
return expect(
runPreRequestScript(
`
pm.request.url.addQueryParams([
{ key: 'flag' },
{ key: 'emptyValue', value: '' }
])
console.log("Params:", pm.request.url.query.all())
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: [
"Params:",
{ filter: "active", sort: "name", flag: "", emptyValue: "" },
],
}),
]),
})
)
})
test("throws error for non-array input", () => {
return expect(
runPreRequestScript(
`
try {
pm.request.url.addQueryParams({ key: 'test', value: '123' })
console.log("Should not reach here")
} catch (error) {
console.log("Error caught:", error.message)
}
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: expect.arrayContaining([
"Error caught:",
expect.stringContaining("requires an array"),
]),
}),
]),
})
)
})
})
describe("pm.request.url.removeQueryParams()", () => {
test("removes single query parameter by name", () => {
return expect(
runPreRequestScript(
`
console.log("Initial params:", pm.request.url.query.all())
pm.request.url.removeQueryParams('filter')
console.log("Updated params:", pm.request.url.query.all())
console.log("URL:", pm.request.url.toString())
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
updatedRequest: expect.objectContaining({
endpoint: "https://api.example.com:8080/v1/users/profile?sort=name",
}),
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: ["Updated params:", { sort: "name" }],
}),
]),
})
)
})
test("removes multiple query parameters by array", () => {
return expect(
runPreRequestScript(
`
console.log("Initial params:", pm.request.url.query.all())
pm.request.url.removeQueryParams(['filter', 'sort'])
console.log("Updated params:", pm.request.url.query.all())
console.log("Params object is empty:", Object.keys(pm.request.url.query.all()).length === 0)
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
updatedRequest: expect.objectContaining({
endpoint: "https://api.example.com:8080/v1/users/profile",
}),
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: ["Updated params:", {}],
}),
expect.objectContaining({
args: ["Params object is empty:", true],
}),
]),
})
)
})
test("handles non-existent parameter names gracefully", () => {
return expect(
runPreRequestScript(
`
console.log("Initial params:", pm.request.url.query.all())
pm.request.url.removeQueryParams(['nonexistent', 'alsoNotThere'])
console.log("Updated params:", pm.request.url.query.all())
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: ["Initial params:", { filter: "active", sort: "name" }],
}),
expect.objectContaining({
args: ["Updated params:", { filter: "active", sort: "name" }],
}),
]),
})
)
})
})

View file

@ -0,0 +1,285 @@
import { HoppRESTRequest } from "@hoppscotch/data"
import { describe, expect, test } from "vitest"
import { runPreRequestScript } from "~/web"
const baseRequest: HoppRESTRequest = {
v: "16",
name: "Test Request",
endpoint: "https://api.example.com/users#section1",
method: "GET",
headers: [],
params: [],
body: { contentType: null, body: null },
auth: { authType: "none", authActive: false },
preRequestScript: "",
testScript: "",
requestVariables: [],
responses: {},
}
const envs = { global: [], selected: [] }
describe("pm.request.url.hash property", () => {
test("hash getter returns fragment without leading #", () => {
return expect(
runPreRequestScript(
`
const hash = pm.request.url.hash
console.log("Hash:", hash)
console.log("Hash type:", typeof hash)
console.log("Starts with #:", hash.startsWith('#'))
console.log("Full URL:", pm.request.url.toString())
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Hash:", "section1"] }),
expect.objectContaining({ args: ["Hash type:", "string"] }),
expect.objectContaining({ args: ["Starts with #:", false] }),
expect.objectContaining({
args: expect.arrayContaining([
"Full URL:",
expect.stringContaining("#section1"),
]),
}),
]),
})
)
})
test("hash getter returns empty string when no fragment", () => {
const requestWithoutHash: HoppRESTRequest = {
...baseRequest,
endpoint: "https://api.example.com/users",
}
return expect(
runPreRequestScript(
`
const hash = pm.request.url.hash
console.log("Hash:", hash)
console.log("Hash is empty:", hash === '')
console.log("Hash length:", hash.length)
`,
{ envs, request: requestWithoutHash }
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Hash:", ""] }),
expect.objectContaining({ args: ["Hash is empty:", true] }),
expect.objectContaining({ args: ["Hash length:", 0] }),
]),
})
)
})
test("hash setter adds fragment to URL", () => {
const requestWithoutHash: HoppRESTRequest = {
...baseRequest,
endpoint: "https://api.example.com/users",
}
return expect(
runPreRequestScript(
`
console.log("Initial hash:", pm.request.url.hash)
console.log("Initial URL:", pm.request.url.toString())
pm.request.url.hash = 'overview'
console.log("Updated hash:", pm.request.url.hash)
console.log("Updated URL:", pm.request.url.toString())
console.log("URL includes #:", pm.request.url.toString().includes('#overview'))
`,
{ envs, request: requestWithoutHash }
)
).resolves.toEqualRight(
expect.objectContaining({
updatedRequest: expect.objectContaining({
endpoint: "https://api.example.com/users#overview",
}),
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Initial hash:", ""] }),
expect.objectContaining({ args: ["Updated hash:", "overview"] }),
expect.objectContaining({ args: ["URL includes #:", true] }),
]),
})
)
})
test("hash setter updates existing fragment", () => {
return expect(
runPreRequestScript(
`
console.log("Initial hash:", pm.request.url.hash)
console.log("Initial URL:", pm.request.url.toString())
pm.request.url.hash = 'newsection'
console.log("Updated hash:", pm.request.url.hash)
console.log("Updated URL:", pm.request.url.toString())
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
updatedRequest: expect.objectContaining({
endpoint: "https://api.example.com/users#newsection",
}),
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Initial hash:", "section1"] }),
expect.objectContaining({ args: ["Updated hash:", "newsection"] }),
expect.objectContaining({
args: expect.arrayContaining([
"Updated URL:",
expect.stringContaining("#newsection"),
]),
}),
]),
})
)
})
test("hash setter accepts value with leading #", () => {
const requestWithoutHash: HoppRESTRequest = {
...baseRequest,
endpoint: "https://api.example.com/users",
}
return expect(
runPreRequestScript(
`
pm.request.url.hash = '#details'
console.log("Hash:", pm.request.url.hash)
console.log("URL:", pm.request.url.toString())
console.log("Hash value:", pm.request.url.hash === 'details')
`,
{ envs, request: requestWithoutHash }
)
).resolves.toEqualRight(
expect.objectContaining({
updatedRequest: expect.objectContaining({
endpoint: "https://api.example.com/users#details",
}),
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Hash:", "details"] }),
expect.objectContaining({ args: ["Hash value:", true] }),
]),
})
)
})
test("hash setter removes fragment when set to empty string", () => {
return expect(
runPreRequestScript(
`
console.log("Initial hash:", pm.request.url.hash)
console.log("Initial URL:", pm.request.url.toString())
pm.request.url.hash = ''
console.log("Updated hash:", pm.request.url.hash)
console.log("Updated URL:", pm.request.url.toString())
console.log("URL has #:", pm.request.url.toString().includes('#'))
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
updatedRequest: expect.objectContaining({
endpoint: "https://api.example.com/users",
}),
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Initial hash:", "section1"] }),
expect.objectContaining({ args: ["Updated hash:", ""] }),
expect.objectContaining({ args: ["URL has #:", false] }),
]),
})
)
})
test("hash works with query parameters", () => {
const requestWithQueryAndHash: HoppRESTRequest = {
...baseRequest,
endpoint: "https://api.example.com/users?filter=active#top",
params: [
{ key: "filter", value: "active", active: true, description: "" },
],
}
return expect(
runPreRequestScript(
`
console.log("Initial URL:", pm.request.url.toString())
console.log("Initial hash:", pm.request.url.hash)
console.log("Query params:", pm.request.url.query.all())
pm.request.url.hash = 'bottom'
console.log("Updated URL:", pm.request.url.toString())
console.log("Updated hash:", pm.request.url.hash)
console.log("Query params unchanged:", JSON.stringify(pm.request.url.query.all()))
`,
{ envs, request: requestWithQueryAndHash }
)
).resolves.toEqualRight(
expect.objectContaining({
updatedRequest: expect.objectContaining({
endpoint: "https://api.example.com/users?filter=active#bottom",
}),
consoleEntries: expect.arrayContaining([
expect.objectContaining({ args: ["Initial hash:", "top"] }),
expect.objectContaining({ args: ["Updated hash:", "bottom"] }),
expect.objectContaining({
args: ["Query params:", { filter: "active" }],
}),
]),
})
)
})
})
describe("combined host and hash mutations", () => {
test("host and hash can be changed independently", () => {
return expect(
runPreRequestScript(
`
console.log("Initial URL:", pm.request.url.toString())
console.log("Initial host:", pm.request.url.getHost())
console.log("Initial hash:", pm.request.url.hash)
pm.request.url.host = ['newdomain', 'com']
console.log("After host change:", pm.request.url.toString())
pm.request.url.hash = 'newhash'
console.log("After hash change:", pm.request.url.toString())
console.log("Final host:", pm.request.url.getHost())
console.log("Final hash:", pm.request.url.hash)
`,
{ envs, request: baseRequest }
)
).resolves.toEqualRight(
expect.objectContaining({
updatedRequest: expect.objectContaining({
endpoint: "https://newdomain.com/users#newhash",
}),
consoleEntries: expect.arrayContaining([
expect.objectContaining({
args: ["Initial host:", "api.example.com"],
}),
expect.objectContaining({ args: ["Initial hash:", "section1"] }),
expect.objectContaining({
args: ["Final host:", "newdomain.com"],
}),
expect.objectContaining({ args: ["Final hash:", "newhash"] }),
]),
})
)
})
})

View file

@ -1,199 +0,0 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
statusText: "OK",
body: JSON.stringify({ message: "Hello, World!" }),
headers: [
{ key: "Content-Type", value: "application/json" },
{ key: "Authorization", value: "Bearer token123" },
],
}
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.tests)
)
describe("pm.response", () => {
test("pm.response.code provides access to status code", () => {
return expect(
func(
`
const code = pm.response.code
pw.expect(code.toString()).toBe("200")
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected '200' to be '200'",
},
],
}),
])
})
test("pm.response.status provides access to status text", () => {
return expect(
func(
`
const status = pm.response.status
pw.expect(status).toBe("OK")
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'OK' to be 'OK'",
},
],
}),
])
})
test("pm.response.text() provides response body as text", () => {
return expect(
func(
`
const text = pm.response.text()
pw.expect(text).toBe('{"message":"Hello, World!"}')
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message:
'Expected \'{"message":"Hello, World!"}\' to be \'{"message":"Hello, World!"}\'',
},
],
}),
])
})
test("pm.response.json() provides parsed JSON response", () => {
return expect(
func(
`
const json = pm.response.json()
pw.expect(json.message).toBe("Hello, World!")
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'Hello, World!' to be 'Hello, World!'",
},
],
}),
])
})
test("pm.response.headers provides access to response headers", () => {
return expect(
func(
`
const headers = pm.response.headers
pw.expect(headers.get("Content-Type")).toBe("application/json")
pw.expect(headers.get("Authorization")).toBe("Bearer token123")
pw.expect(headers.get("nonexistent")).toBe(null)
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'application/json' to be 'application/json'",
},
{
status: "pass",
message: "Expected 'Bearer token123' to be 'Bearer token123'",
},
{
status: "pass",
message: "Expected 'null' to be 'null'",
},
],
}),
])
})
test("pm.response object has correct structure and values", () => {
return expect(
func(
`
pw.expect(pm.response.code).toBe(200)
pw.expect(pm.response.status).toBe("OK")
pw.expect(pm.response.text()).toBe('{"message":"Hello, World!"}')
pw.expect(pm.response.json().message).toBe("Hello, World!")
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected '200' to be '200'",
},
{
status: "pass",
message: "Expected 'OK' to be 'OK'",
},
{
status: "pass",
message:
'Expected \'{"message":"Hello, World!"}\' to be \'{"message":"Hello, World!"}\'',
},
{
status: "pass",
message: "Expected 'Hello, World!' to be 'Hello, World!'",
},
],
}),
])
})
})

View file

@ -0,0 +1,360 @@
import { describe, expect, test } from "vitest"
import { TestResponse, TestResult } from "~/types"
import { runTest } from "~/utils/test-helpers"
const customResponse: TestResponse = {
status: 200,
statusText: "OK",
body: JSON.stringify({ message: "Hello, World!" }),
headers: [
{ key: "Content-Type", value: "application/json" },
{ key: "Authorization", value: "Bearer token123" },
],
}
const func = (
script: string,
envs: TestResult["envs"],
response: TestResponse = customResponse
) => runTest(script, envs, response)
describe("pm.response", () => {
test("pm.response.code provides access to status code", () => {
return expect(
func(
`
const code = pm.response.code
pw.expect(code.toString()).toBe("200")
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected '200' to be '200'",
},
],
}),
])
})
test("pm.response.status provides access to status text", () => {
return expect(
func(
`
const status = pm.response.status
pw.expect(status).toBe("OK")
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'OK' to be 'OK'",
},
],
}),
])
})
test("pm.response.text() provides response body as text", () => {
return expect(
func(
`
const text = pm.response.text()
pw.expect(text).toBe('{"message":"Hello, World!"}')
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message:
'Expected \'{"message":"Hello, World!"}\' to be \'{"message":"Hello, World!"}\'',
},
],
}),
])
})
test("pm.response.json() provides parsed JSON response", () => {
return expect(
func(
`
const json = pm.response.json()
pw.expect(json.message).toBe("Hello, World!")
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'Hello, World!' to be 'Hello, World!'",
},
],
}),
])
})
test("pm.response.headers provides access to response headers", () => {
return expect(
func(
`
const headers = pm.response.headers
pw.expect(headers.get("Content-Type")).toBe("application/json")
pw.expect(headers.get("Authorization")).toBe("Bearer token123")
// Postman returns undefined for non-existent headers, not null
pw.expect(headers.get("nonexistent")).toBe(undefined)
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'application/json' to be 'application/json'",
},
{
status: "pass",
message: "Expected 'Bearer token123' to be 'Bearer token123'",
},
{
status: "pass",
message: "Expected 'undefined' to be 'undefined'",
},
],
}),
])
})
test("pm.response object has correct structure and values", () => {
return expect(
func(
`
pw.expect(pm.response.code).toBe(200)
pw.expect(pm.response.status).toBe("OK")
pw.expect(pm.response.text()).toBe('{"message":"Hello, World!"}')
pw.expect(pm.response.json().message).toBe("Hello, World!")
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected '200' to be '200'",
},
{
status: "pass",
message: "Expected 'OK' to be 'OK'",
},
{
status: "pass",
message:
'Expected \'{"message":"Hello, World!"}\' to be \'{"message":"Hello, World!"}\'',
},
{
status: "pass",
message: "Expected 'Hello, World!' to be 'Hello, World!'",
},
],
}),
])
})
test("pm.response.stream provides response body as Uint8Array", () => {
return expect(
func(
`
const stream = pm.response.stream
// Verify it's a Uint8Array by checking it can be decoded
const decoder = new TextDecoder()
const decoded = decoder.decode(stream)
pw.expect(decoded).toBe('{"message":"Hello, World!"}')
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message:
'Expected \'{"message":"Hello, World!"}\' to be \'{"message":"Hello, World!"}\'',
},
],
}),
])
})
test("pm.response.stream contains correct byte data", () => {
return expect(
func(
`
const stream = pm.response.stream
const decoder = new TextDecoder()
const text = decoder.decode(stream)
pw.expect(text).toBe('{"message":"Hello, World!"}')
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message:
'Expected \'{"message":"Hello, World!"}\' to be \'{"message":"Hello, World!"}\'',
},
],
}),
])
})
test("pm.response.reason() returns HTTP reason phrase", () => {
return expect(
func(
`
const reason = pm.response.reason()
pw.expect(reason).toBe("OK")
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'OK' to be 'OK'",
},
],
}),
])
})
test("pm.response.dataURI() converts response to data URI", () => {
return expect(
func(
`
const dataURI = pm.response.dataURI()
pw.expect(dataURI).toBeType("string")
// Check it starts with data: and contains base64
const startsWithData = dataURI.startsWith("data:")
pw.expect(startsWithData).toBe(true)
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: expect.stringContaining("to be type 'string'"),
},
{
status: "pass",
message: "Expected 'true' to be 'true'",
},
],
}),
])
})
test("pm.response.jsonp() parses JSONP response", () => {
const jsonpResponse: TestResponse = {
status: 200,
statusText: "OK",
body: 'callback({"data": "test value"})',
headers: [{ key: "Content-Type", value: "application/javascript" }],
}
return expect(
func(
`
const data = pm.response.jsonp("callback")
pw.expect(data.data).toBe("test value")
`,
{ global: [], selected: [] },
jsonpResponse
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'test value' to be 'test value'",
},
],
}),
])
})
test("pm.response.jsonp() handles response without callback wrapper", () => {
const jsonResponse: TestResponse = {
status: 200,
statusText: "OK",
body: '{"plain": "json"}',
headers: [{ key: "Content-Type", value: "application/json" }],
}
return expect(
func(
`
const data = pm.response.jsonp()
pw.expect(data.plain).toBe("json")
`,
{ global: [], selected: [] },
jsonResponse
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'json' to be 'json'",
},
],
}),
])
})
})

View file

@ -0,0 +1,897 @@
/**
* @file Tests for Postman BDD-style response assertions (pm.response.to.*)
*
* These tests verify the Postman compatibility layer's BDD-style assertion helpers
* which are commonly used in Postman collections. They provide syntactic sugar over
* the standard Chai assertions for common response validation patterns.
*/
import { describe, expect, test } from "vitest"
import { runTest } from "~/utils/test-helpers"
describe("`pm.response.to.have.*` - Status Code Assertions", () => {
test("should support `.status()` for exact status code matching", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: "{}",
headers: [],
}
return expect(
runTest(
`
pm.test("Status code is 200", function() {
pm.response.to.have.status(200)
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "Status code is 200",
expectResults: [
{
status: "pass",
message: "Expected 200 to equal 200",
},
],
}),
],
}),
])
})
test("should fail `.status()` when status code doesn't match", () => {
const response: TestResponse = {
status: 404,
statusText: "Not Found",
body: "{}",
headers: [],
}
return expect(
runTest(
`
pm.test("Status code is 200", function() {
pm.response.to.have.status(200)
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "Status code is 200",
expectResults: [
{
status: "fail",
message: expect.stringContaining("Expected 404 to equal 200"),
},
],
}),
],
}),
])
})
})
describe("`pm.response.to.be.*` - Status Code Convenience Methods", () => {
test("should support `.ok()` for 2xx status codes", () => {
const responses = [
{ status: 200, statusText: "OK" },
{ status: 201, statusText: "Created" },
{ status: 204, statusText: "No Content" },
]
return Promise.all(
responses.map((r) =>
expect(
runTest(
`
pm.test("Response is OK", function() {
pm.response.to.be.ok()
})
`,
{ global: [], selected: [] },
{ ...r, body: "{}", headers: [] }
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
expectResults: [
{ status: "pass", message: expect.any(String) },
],
}),
],
}),
])
)
)
})
test("should fail `.ok()` for non-2xx status codes", () => {
const response: TestResponse = {
status: 404,
statusText: "Not Found",
body: "{}",
headers: [],
}
return expect(
runTest(
`
pm.test("Response is OK", function() {
pm.response.to.be.ok()
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
expectResults: [
{
status: "fail",
message: expect.any(String),
},
],
}),
],
}),
])
})
test("should support `.accepted()` for 202 status code", () => {
const response: TestResponse = {
status: 202,
statusText: "Accepted",
body: "{}",
headers: [],
}
return expect(
runTest(
`
pm.test("Request accepted", function() {
pm.response.to.be.accepted()
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
expectResults: [{ status: "pass", message: expect.any(String) }],
}),
],
}),
])
})
test("should support `.badRequest()` for 400 status code", () => {
const response: TestResponse = {
status: 400,
statusText: "Bad Request",
body: "{}",
headers: [],
}
return expect(
runTest(
`
pm.test("Bad request error", function() {
pm.response.to.be.badRequest()
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
expectResults: [{ status: "pass", message: expect.any(String) }],
}),
],
}),
])
})
test("should support `.unauthorized()` for 401 status code", () => {
const response: TestResponse = {
status: 401,
statusText: "Unauthorized",
body: "{}",
headers: [],
}
return expect(
runTest(
`
pm.test("Unauthorized error", function() {
pm.response.to.be.unauthorized()
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
expectResults: [{ status: "pass", message: expect.any(String) }],
}),
],
}),
])
})
test("should support `.forbidden()` for 403 status code", () => {
const response: TestResponse = {
status: 403,
statusText: "Forbidden",
body: "{}",
headers: [],
}
return expect(
runTest(
`
pm.test("Forbidden error", function() {
pm.response.to.be.forbidden()
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
expectResults: [{ status: "pass", message: expect.any(String) }],
}),
],
}),
])
})
test("should support `.notFound()` for 404 status code", () => {
const response: TestResponse = {
status: 404,
statusText: "Not Found",
body: "{}",
headers: [],
}
return expect(
runTest(
`
pm.test("Not found error", function() {
pm.response.to.be.notFound()
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
expectResults: [{ status: "pass", message: expect.any(String) }],
}),
],
}),
])
})
test("should support `.rateLimited()` for 429 status code", () => {
const response: TestResponse = {
status: 429,
statusText: "Too Many Requests",
body: "{}",
headers: [],
}
return expect(
runTest(
`
pm.test("Rate limited", function() {
pm.response.to.be.rateLimited()
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
expectResults: [{ status: "pass", message: expect.any(String) }],
}),
],
}),
])
})
test("should support `.serverError()` for 5xx status codes", () => {
const responses = [
{ status: 500, statusText: "Internal Server Error" },
{ status: 502, statusText: "Bad Gateway" },
{ status: 503, statusText: "Service Unavailable" },
]
return Promise.all(
responses.map((r) =>
expect(
runTest(
`
pm.test("Server error", function() {
pm.response.to.be.serverError()
})
`,
{ global: [], selected: [] },
{ ...r, body: "{}", headers: [] }
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
expectResults: [
{ status: "pass", message: expect.any(String) },
],
}),
],
}),
])
)
)
})
})
describe("`pm.response.to.have.header()` - Header Assertions", () => {
test("should check header existence", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: "{}",
headers: [
{ key: "Content-Type", value: "application/json" },
{ key: "X-Custom-Header", value: "custom-value" },
],
}
return expect(
runTest(
`
pm.test("Headers exist", function() {
pm.response.to.have.header("Content-Type")
pm.response.to.have.header("X-Custom-Header")
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
expectResults: [
{ status: "pass", message: expect.any(String) },
{ status: "pass", message: expect.any(String) },
],
}),
],
}),
])
})
test("should check header value when specified", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: "{}",
headers: [{ key: "Content-Type", value: "application/json" }],
}
return expect(
runTest(
`
pm.test("Header has correct value", function() {
pm.response.to.have.header("Content-Type", "application/json")
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
expectResults: [{ status: "pass", message: expect.any(String) }],
}),
],
}),
])
})
test("should be case-insensitive for header names", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: "{}",
headers: [{ key: "Content-Type", value: "application/json" }],
}
return expect(
runTest(
`
pm.test("Case insensitive header check", function() {
pm.response.to.have.header("content-type")
pm.response.to.have.header("CONTENT-TYPE")
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
expectResults: [
{ status: "pass", message: expect.any(String) },
{ status: "pass", message: expect.any(String) },
],
}),
],
}),
])
})
})
describe("`pm.response.to.have.body()` - Body Assertions", () => {
test("should match exact body content", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: "Hello, World!",
headers: [],
}
return expect(
runTest(
`
pm.test("Body matches", function() {
pm.response.to.have.body("Hello, World!")
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
expectResults: [{ status: "pass", message: expect.any(String) }],
}),
],
}),
])
})
})
describe("`pm.response.to.have.jsonBody()` - JSON Body Assertions", () => {
test("should assert response is JSON object when called without arguments", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: JSON.stringify({ message: "Hello", count: 42 }),
headers: [{ key: "Content-Type", value: "application/json" }],
}
return expect(
runTest(
`
pm.test("Response is JSON", function() {
pm.response.to.have.jsonBody()
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
expectResults: [{ status: "pass", message: expect.any(String) }],
}),
],
}),
])
})
test("should check for property existence when key provided", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: JSON.stringify({ message: "Hello", count: 42 }),
headers: [],
}
return expect(
runTest(
`
pm.test("JSON has properties", function() {
pm.response.to.have.jsonBody("message")
pm.response.to.have.jsonBody("count")
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
expectResults: [
{ status: "pass", message: expect.any(String) },
{ status: "pass", message: expect.any(String) },
],
}),
],
}),
])
})
test("should check property value when key and value provided", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: JSON.stringify({ message: "Hello", count: 42 }),
headers: [],
}
return expect(
runTest(
`
pm.test("JSON property values match", function() {
pm.response.to.have.jsonBody("message", "Hello")
pm.response.to.have.jsonBody("count", 42)
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
expectResults: [
{ status: "pass", message: expect.any(String) },
{ status: "pass", message: expect.any(String) },
],
}),
],
}),
])
})
test("should support nested property checks", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: JSON.stringify({ user: { name: "Alice", age: 30 } }),
headers: [],
}
return expect(
runTest(
`
pm.test("Nested properties", function() {
const data = pm.response.json()
pm.expect(data.user).to.have.property("name")
pm.expect(data.user.name).to.equal("Alice")
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
expectResults: [
{ status: "pass", message: expect.any(String) },
{ status: "pass", message: expect.any(String) },
],
}),
],
}),
])
})
})
describe("`pm.response.to.be.*` - Content Type Convenience Methods", () => {
test("should support `.json()` for JSON content type", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: "{}",
headers: [
{ key: "Content-Type", value: "application/json; charset=utf-8" },
],
}
return expect(
runTest(
`
pm.test("Response is JSON", function() {
pm.response.to.be.json()
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
expectResults: [{ status: "pass", message: expect.any(String) }],
}),
],
}),
])
})
test("should support `.html()` for HTML content type", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: "<html></html>",
headers: [{ key: "Content-Type", value: "text/html; charset=utf-8" }],
}
return expect(
runTest(
`
pm.test("Response is HTML", function() {
pm.response.to.be.html()
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
expectResults: [{ status: "pass", message: expect.any(String) }],
}),
],
}),
])
})
test("should support `.xml()` for XML content types", () => {
const responses = [
{ headers: [{ key: "Content-Type", value: "application/xml" }] },
{ headers: [{ key: "Content-Type", value: "text/xml" }] },
]
return Promise.all(
responses.map((r) =>
expect(
runTest(
`
pm.test("Response is XML", function() {
pm.response.to.be.xml()
})
`,
{ global: [], selected: [] },
{ status: 200, statusText: "OK", body: "<xml></xml>", ...r }
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
expectResults: [
{ status: "pass", message: expect.any(String) },
],
}),
],
}),
])
)
)
})
test("should support `.text()` for plain text content type", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: "Plain text response",
headers: [{ key: "Content-Type", value: "text/plain" }],
}
return expect(
runTest(
`
pm.test("Response is text", function() {
pm.response.to.be.text()
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
expectResults: [{ status: "pass", message: expect.any(String) }],
}),
],
}),
])
})
})
describe("`pm.response.to.have.responseTime.*` - Response Time Assertions", () => {
test("should support `.below()` for response time upper bound", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: "{}",
headers: [],
responseTime: 250,
}
return expect(
runTest(
`
pm.test("Response time is acceptable", function() {
pm.response.to.have.responseTime.below(500)
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
expectResults: [{ status: "pass", message: expect.any(String) }],
}),
],
}),
])
})
test("should support `.above()` for response time lower bound", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: "{}",
headers: [],
responseTime: 250,
}
return expect(
runTest(
`
pm.test("Response time is above threshold", function() {
pm.response.to.have.responseTime.above(100)
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
expectResults: [{ status: "pass", message: expect.any(String) }],
}),
],
}),
])
})
})
describe("Real-world Postman script patterns", () => {
test("should handle complex assertion combinations", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: JSON.stringify({
success: true,
data: { id: 123, name: "Test" },
timestamp: Date.now(),
}),
headers: [
{ key: "Content-Type", value: "application/json" },
{ key: "X-Request-ID", value: "abc-123" },
],
responseTime: 150,
}
return expect(
runTest(
`
pm.test("API response validation", function() {
// Status code checks
pm.response.to.have.status(200)
pm.response.to.be.ok()
// Header checks
pm.response.to.have.header("Content-Type")
pm.response.to.have.header("X-Request-ID", "abc-123")
// Content type
pm.response.to.be.json()
// JSON body checks
pm.response.to.have.jsonBody("success", true)
pm.response.to.have.jsonBody("data")
// Response time
pm.response.to.have.responseTime.below(500)
// Detailed JSON assertions
const json = pm.response.json()
pm.expect(json.data.id).to.equal(123)
pm.expect(json.data.name).to.equal("Test")
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "API response validation",
expectResults: expect.arrayContaining([
{ status: "pass", message: expect.any(String) },
]),
}),
],
}),
])
})
})

View file

@ -0,0 +1,392 @@
/**
* @file Tests for Postman cookie handling (pm.response.cookies.*, pm.response.to.have.cookie)
*
* These tests verify cookie parsing from Set-Cookie headers and cookie assertions.
* Cookies in responses are extracted from Set-Cookie headers and made available
* through the pm.response.cookies API.
*/
import { describe, expect, test } from "vitest"
import { runTest } from "~/utils/test-helpers"
describe("`pm.response.cookies` - Cookie Access Methods", () => {
test("should support `.get()` to retrieve a cookie value by name", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: "{}",
headers: [
{ key: "Set-Cookie", value: "session=abc123; Path=/; HttpOnly" },
],
}
return expect(
runTest(
`
pm.test("Can retrieve cookie value by name", function() {
const cookieValue = pm.response.cookies.get("session")
pm.expect(cookieValue).to.not.be.null
pm.expect(cookieValue).to.equal("abc123")
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "Can retrieve cookie value by name",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
test("should return null for non-existent cookies", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: "{}",
headers: [{ key: "Set-Cookie", value: "session=abc123; Path=/" }],
}
return expect(
runTest(
`
pm.test("Returns null for non-existent cookie", function() {
const cookie = pm.response.cookies.get("nonexistent")
pm.expect(cookie).to.be.null
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "Returns null for non-existent cookie",
expectResults: [
{
status: "pass",
message: expect.stringContaining("Expected null to be null"),
},
],
}),
],
}),
])
})
test("should support `.has()` to check cookie existence", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: "{}",
headers: [
{ key: "Set-Cookie", value: "auth_token=xyz789; Secure" },
{ key: "Set-Cookie", value: "user_id=42; SameSite=Strict" },
],
}
return expect(
runTest(
`
pm.test("Can check cookie existence", function() {
pm.expect(pm.response.cookies.has("auth_token")).to.be.true
pm.expect(pm.response.cookies.has("user_id")).to.be.true
pm.expect(pm.response.cookies.has("nonexistent")).to.be.false
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "Can check cookie existence",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
test("should support `.toObject()` to get all cookies", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: "{}",
headers: [
{ key: "Set-Cookie", value: "cookie1=value1; Path=/" },
{ key: "Set-Cookie", value: "cookie2=value2; Domain=example.com" },
],
}
return expect(
runTest(
`
pm.test("Can get all cookies as object", function() {
const cookies = pm.response.cookies.toObject()
pm.expect(cookies).to.be.an("object")
pm.expect(cookies.cookie1).to.equal("value1")
pm.expect(cookies.cookie2).to.equal("value2")
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "Can get all cookies as object",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
test("should return just the cookie value (matching Postman behavior)", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: "{}",
headers: [
{
key: "Set-Cookie",
value:
"full_cookie=test_value; Domain=.example.com; Path=/api; Max-Age=3600; Secure; HttpOnly; SameSite=Lax",
},
],
}
return expect(
runTest(
`
pm.test("Returns only cookie value, not attributes", function() {
const cookieValue = pm.response.cookies.get("full_cookie")
pm.expect(cookieValue).to.equal("test_value")
pm.expect(cookieValue).to.be.a("string")
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "Returns only cookie value, not attributes",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
test("should handle cookies with equals signs in value", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: "{}",
headers: [
{
key: "Set-Cookie",
value:
"jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0=; Path=/",
},
],
}
return expect(
runTest(
`
pm.test("Handles equals signs in cookie value", function() {
const cookieValue = pm.response.cookies.get("jwt")
pm.expect(cookieValue).to.include("=")
pm.expect(cookieValue).to.equal("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0=")
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "Handles equals signs in cookie value",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
})
describe("`pm.response.to.have.cookie` - Cookie Assertions", () => {
test("should assert cookie exists by name", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: "{}",
headers: [{ key: "Set-Cookie", value: "session=abc123; Path=/" }],
}
return expect(
runTest(
`
pm.test("Response has session cookie", function() {
pm.response.to.have.cookie("session")
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "Response has session cookie",
expectResults: expect.arrayContaining([
expect.objectContaining({ status: "pass" }),
]),
}),
],
}),
])
})
test("should assert cookie value matches", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: "{}",
headers: [{ key: "Set-Cookie", value: "user=john_doe; Path=/" }],
}
return expect(
runTest(
`
pm.test("Cookie has correct value", function() {
pm.response.to.have.cookie("user", "john_doe")
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "Cookie has correct value",
expectResults: [
{
status: "pass",
message: expect.stringContaining(
"Expected 'john_doe' to equal 'john_doe'"
),
},
],
}),
],
}),
])
})
test("should fail when cookie doesn't exist", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: "{}",
headers: [],
}
return expect(
runTest(
`
pm.test("Missing cookie fails", function() {
pm.response.to.have.cookie("nonexistent")
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "Missing cookie fails",
expectResults: [
{
status: "fail",
message: expect.stringContaining("Expected false to be true"),
},
],
}),
],
}),
])
})
test("should fail when cookie value doesn't match", () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: "{}",
headers: [{ key: "Set-Cookie", value: "token=wrong_value; Path=/" }],
}
return expect(
runTest(
`
pm.test("Wrong cookie value fails", function() {
pm.response.to.have.cookie("token", "expected_value")
})
`,
{ global: [], selected: [] },
response
)()
).resolves.toEqualRight([
expect.objectContaining({
descriptor: "root",
children: [
expect.objectContaining({
descriptor: "Wrong cookie value fails",
expectResults: [
{
status: "fail",
message: expect.stringContaining(
"Expected 'wrong_value' to equal 'expected_value'"
),
},
],
}),
],
}),
])
})
})

View file

@ -0,0 +1,318 @@
import { describe, expect, test } from "vitest"
import { runTest } from "~/utils/test-helpers"
describe("pm.response.dataURI() comprehensive coverage", () => {
test("should handle Content-Type without charset", async () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: JSON.stringify({ test: "data" }),
headers: [{ key: "Content-Type", value: "application/json" }],
}
const testScript = `
pm.test("dataURI format without charset", () => {
const dataUri = pm.response.dataURI()
pm.expect(dataUri).to.be.a('string')
pm.expect(dataUri).to.include('data:')
pm.expect(dataUri).to.match(/^data:.+;base64,/)
pm.expect(dataUri).to.include('application/json')
pm.expect(dataUri).to.include('base64,')
})
`
return expect(
runTest(testScript, { global: [], selected: [] }, response)()
).resolves.toEqualRight([
expect.objectContaining({
children: [
expect.objectContaining({
descriptor: "dataURI format without charset",
expectResults: [
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
],
}),
],
}),
])
})
test("should handle Content-Type with charset parameter", async () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: JSON.stringify({ test: "data" }),
headers: [
{ key: "Content-Type", value: "application/json; charset=utf-8" },
],
}
const testScript = `
pm.test("dataURI format with charset", () => {
const dataUri = pm.response.dataURI()
pm.expect(dataUri).to.be.a('string')
pm.expect(dataUri).to.include('data:')
// Updated regex pattern that handles charset parameters
pm.expect(dataUri).to.match(/^data:.+;base64,/)
pm.expect(dataUri).to.include('application/json')
pm.expect(dataUri).to.include('charset=utf-8')
pm.expect(dataUri).to.include('base64,')
})
`
return expect(
runTest(testScript, { global: [], selected: [] }, response)()
).resolves.toEqualRight([
expect.objectContaining({
children: [
expect.objectContaining({
descriptor: "dataURI format with charset",
expectResults: [
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
],
}),
],
}),
])
})
test("should handle text/html with charset", async () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: "<html><body>Hello</body></html>",
headers: [{ key: "Content-Type", value: "text/html; charset=utf-8" }],
}
const testScript = `
pm.test("dataURI with text/html and charset", () => {
const dataUri = pm.response.dataURI()
pm.expect(dataUri).to.be.a('string')
pm.expect(dataUri).to.match(/^data:.+;base64,/)
pm.expect(dataUri).to.include('text/html')
pm.expect(dataUri).to.include('charset=utf-8')
})
`
return expect(
runTest(testScript, { global: [], selected: [] }, response)()
).resolves.toEqualRight([
expect.objectContaining({
children: [
expect.objectContaining({
descriptor: "dataURI with text/html and charset",
expectResults: [
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
],
}),
],
}),
])
})
test("should handle text/plain with multiple parameters", async () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: "Plain text content",
headers: [
{
key: "Content-Type",
value: "text/plain; charset=utf-8; format=flowed",
},
],
}
const testScript = `
pm.test("dataURI with multiple parameters", () => {
const dataUri = pm.response.dataURI()
pm.expect(dataUri).to.be.a('string')
// Regex should handle multiple semicolons
pm.expect(dataUri).to.match(/^data:.+;base64,/)
pm.expect(dataUri).to.include('text/plain')
pm.expect(dataUri).to.include('base64,')
})
`
return expect(
runTest(testScript, { global: [], selected: [] }, response)()
).resolves.toEqualRight([
expect.objectContaining({
children: [
expect.objectContaining({
descriptor: "dataURI with multiple parameters",
expectResults: [
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
],
}),
],
}),
])
})
test("should handle application/xml with charset", async () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: '<?xml version="1.0"?><root><data>test</data></root>',
headers: [
{ key: "Content-Type", value: "application/xml; charset=utf-8" },
],
}
const testScript = `
pm.test("dataURI with XML and charset", () => {
const dataUri = pm.response.dataURI()
pm.expect(dataUri).to.be.a('string')
pm.expect(dataUri).to.match(/^data:.+;base64,/)
pm.expect(dataUri).to.include('application/xml')
})
`
return expect(
runTest(testScript, { global: [], selected: [] }, response)()
).resolves.toEqualRight([
expect.objectContaining({
children: [
expect.objectContaining({
descriptor: "dataURI with XML and charset",
expectResults: [
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
],
}),
],
}),
])
})
test("should handle missing Content-Type header", async () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: "Some content",
headers: [],
}
const testScript = `
pm.test("dataURI without Content-Type header", () => {
const dataUri = pm.response.dataURI()
pm.expect(dataUri).to.be.a('string')
pm.expect(dataUri).to.match(/^data:.+;base64,/)
// Should default to application/octet-stream
pm.expect(dataUri).to.include('application/octet-stream')
})
`
return expect(
runTest(testScript, { global: [], selected: [] }, response)()
).resolves.toEqualRight([
expect.objectContaining({
children: [
expect.objectContaining({
descriptor: "dataURI without Content-Type header",
expectResults: [
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
],
}),
],
}),
])
})
test("should properly encode UTF-8 content", async () => {
const response: TestResponse = {
status: 200,
statusText: "OK",
body: JSON.stringify({ message: "Hello 世界 🌍" }),
headers: [
{ key: "Content-Type", value: "application/json; charset=utf-8" },
],
}
const testScript = `
pm.test("dataURI with UTF-8 characters", () => {
const dataUri = pm.response.dataURI()
pm.expect(dataUri).to.be.a('string')
pm.expect(dataUri).to.match(/^data:.+;base64,/)
pm.expect(dataUri.length).to.be.above(50)
// Should contain valid base64 characters after "base64,"
const base64Part = dataUri.split('base64,')[1]
pm.expect(base64Part).to.be.a('string')
pm.expect(base64Part.length).to.be.above(0)
})
`
return expect(
runTest(testScript, { global: [], selected: [] }, response)()
).resolves.toEqualRight([
expect.objectContaining({
children: [
expect.objectContaining({
descriptor: "dataURI with UTF-8 characters",
expectResults: [
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
],
}),
],
}),
])
})
test("should handle empty response body", async () => {
const response: TestResponse = {
status: 204,
statusText: "No Content",
body: "",
headers: [{ key: "Content-Type", value: "text/plain" }],
}
const testScript = `
pm.test("dataURI with empty body", () => {
const dataUri = pm.response.dataURI()
pm.expect(dataUri).to.be.a('string')
pm.expect(dataUri).to.match(/^data:.+;base64,/)
pm.expect(dataUri).to.include('text/plain')
})
`
return expect(
runTest(testScript, { global: [], selected: [] }, response)()
).resolves.toEqualRight([
expect.objectContaining({
children: [
expect.objectContaining({
descriptor: "dataURI with empty body",
expectResults: [
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
],
}),
],
}),
])
})
})

View file

@ -0,0 +1,211 @@
import { describe, expect, it } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse } from "~/types/test-runner"
describe("pm.response method existence checks", () => {
const mockResponse: TestResponse = {
status: 200,
headers: [{ key: "Content-Type", value: "application/json" }],
body: JSON.stringify({ message: "Hello" }),
}
it("should recognize pm.response.reason as a function", async () => {
const testScript = `
pm.test("pm.response.reason is a function", () => {
pm.expect(pm.response.reason).to.be.a('function')
})
`
await expect(
runTestScript(testScript, { response: mockResponse })()
).resolves.toEqualRight(
expect.objectContaining({
tests: [
expect.objectContaining({
expectResults: [],
children: [
expect.objectContaining({
descriptor: "pm.response.reason is a function",
expectResults: [
expect.objectContaining({
status: "pass",
}),
],
}),
],
}),
],
})
)
})
it("should recognize pm.response.dataURI as a function", async () => {
const testScript = `
pm.test("pm.response.dataURI is a function", () => {
pm.expect(pm.response.dataURI).to.be.a('function')
})
`
await expect(
runTestScript(testScript, { response: mockResponse })()
).resolves.toEqualRight(
expect.objectContaining({
tests: [
expect.objectContaining({
expectResults: [],
children: [
expect.objectContaining({
descriptor: "pm.response.dataURI is a function",
expectResults: [
expect.objectContaining({
status: "pass",
}),
],
}),
],
}),
],
})
)
})
it("should recognize pm.response.jsonp as a function", async () => {
const testScript = `
pm.test("pm.response.jsonp is a function", () => {
pm.expect(pm.response.jsonp).to.be.a('function')
})
`
await expect(
runTestScript(testScript, { response: mockResponse })()
).resolves.toEqualRight(
expect.objectContaining({
tests: [
expect.objectContaining({
expectResults: [],
children: [
expect.objectContaining({
descriptor: "pm.response.jsonp is a function",
expectResults: [
expect.objectContaining({
status: "pass",
}),
],
}),
],
}),
],
})
)
})
it("should work with typeof checks", async () => {
const testScript = `
pm.test("typeof pm.response.reason equals function", () => {
pm.expect(typeof pm.response.reason).to.equal('function')
})
`
await expect(
runTestScript(testScript, { response: mockResponse })()
).resolves.toEqualRight(
expect.objectContaining({
tests: [
expect.objectContaining({
expectResults: [],
children: [
expect.objectContaining({
descriptor: "typeof pm.response.reason equals function",
expectResults: [
expect.objectContaining({
status: "pass",
}),
],
}),
],
}),
],
})
)
})
it("should verify all three utility methods exist as functions", async () => {
const testScript = `
pm.test("all response utility methods exist", () => {
pm.expect(pm.response.reason).to.be.a('function')
pm.expect(pm.response.dataURI).to.be.a('function')
pm.expect(pm.response.jsonp).to.be.a('function')
})
`
await expect(
runTestScript(testScript, { response: mockResponse })()
).resolves.toEqualRight(
expect.objectContaining({
tests: [
expect.objectContaining({
expectResults: [],
children: [
expect.objectContaining({
descriptor: "all response utility methods exist",
expectResults: [
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
],
}),
],
}),
],
})
)
})
it("should verify methods work correctly when called", async () => {
const testScript = `
pm.test("response.reason() returns status text", () => {
pm.expect(pm.response.reason()).to.equal('OK')
})
pm.test("response.dataURI() returns data URI string", () => {
const uri = pm.response.dataURI()
pm.expect(uri).to.be.a('string')
pm.expect(uri).to.include('data:')
})
pm.test("response.jsonp() parses JSON", () => {
const result = pm.response.jsonp()
pm.expect(result).to.deep.equal({ message: "Hello" })
})
`
await expect(
runTestScript(testScript, { response: mockResponse })()
).resolves.toEqualRight(
expect.objectContaining({
tests: [
expect.objectContaining({
expectResults: [],
children: [
expect.objectContaining({
descriptor: "response.reason() returns status text",
expectResults: [expect.objectContaining({ status: "pass" })],
}),
expect.objectContaining({
descriptor: "response.dataURI() returns data URI string",
expectResults: [
expect.objectContaining({ status: "pass" }),
expect.objectContaining({ status: "pass" }),
],
}),
expect.objectContaining({
descriptor: "response.jsonp() parses JSON",
expectResults: [expect.objectContaining({ status: "pass" })],
}),
],
}),
],
})
)
})
})

View file

@ -0,0 +1,360 @@
import { describe, expect, test } from "vitest"
import { runTest } from "~/utils/test-helpers"
// Postman compatibility: all types preserved during runtime, only undefined needs special handling
describe("PM namespace type preservation (Postman compatibility)", () => {
describe("Array preservation", () => {
test("arrays are preserved as arrays with .length property", () => {
return expect(
runTest(
`
pm.environment.set("array", [1, 2, 3])
const value = pm.environment.get("array")
pm.expect(Array.isArray(value)).toBe(true)
pm.expect(value.length).toBe(3)
pm.expect(value[0]).toBe(1)
pm.expect(value[2]).toBe(3)
`,
{ global: [], selected: [] }
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'true' to be 'true'" },
{ status: "pass", message: "Expected '3' to be '3'" },
{ status: "pass", message: "Expected '1' to be '1'" },
{ status: "pass", message: "Expected '3' to be '3'" },
],
}),
])
})
test("single-element arrays remain arrays", () => {
return expect(
runTest(
`
pm.environment.set("single", [42])
const value = pm.environment.get("single")
pm.expect(Array.isArray(value)).toBe(true)
pm.expect(value.length).toBe(1)
pm.expect(value[0]).toBe(42)
`,
{ global: [], selected: [] }
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'true' to be 'true'" },
{ status: "pass", message: "Expected '1' to be '1'" },
{ status: "pass", message: "Expected '42' to be '42'" },
],
}),
])
})
test("empty arrays are preserved", () => {
return expect(
runTest(
`
pm.environment.set("empty", [])
const value = pm.environment.get("empty")
pm.expect(Array.isArray(value)).toBe(true)
pm.expect(value.length).toBe(0)
`,
{ global: [], selected: [] }
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'true' to be 'true'" },
{ status: "pass", message: "Expected '0' to be '0'" },
],
}),
])
})
test("nested arrays are preserved", () => {
return expect(
runTest(
`
pm.environment.set("nested", [[1, 2], [3, 4]])
const value = pm.environment.get("nested")
pm.expect(Array.isArray(value)).toBe(true)
pm.expect(value.length).toBe(2)
pm.expect(Array.isArray(value[0])).toBe(true)
pm.expect(value[0][1]).toBe(2)
pm.expect(value[1][0]).toBe(3)
`,
{ global: [], selected: [] }
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'true' to be 'true'" },
{ status: "pass", message: "Expected '2' to be '2'" },
{ status: "pass", message: "Expected 'true' to be 'true'" },
{ status: "pass", message: "Expected '2' to be '2'" },
{ status: "pass", message: "Expected '3' to be '3'" },
],
}),
])
})
})
describe("Object preservation", () => {
test("objects are preserved with accessible properties", () => {
return expect(
runTest(
`
pm.environment.set("obj", { key: "value", num: 42 })
const value = pm.environment.get("obj")
pm.expect(typeof value).toBe("object")
pm.expect(value.key).toBe("value")
pm.expect(value.num).toBe(42)
`,
{ global: [], selected: [] }
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'object' to be 'object'" },
{ status: "pass", message: "Expected 'value' to be 'value'" },
{ status: "pass", message: "Expected '42' to be '42'" },
],
}),
])
})
test("empty objects are preserved", () => {
return expect(
runTest(
`
pm.environment.set("empty_obj", {})
const value = pm.environment.get("empty_obj")
pm.expect(typeof value).toBe("object")
pm.expect(Array.isArray(value)).toBe(false)
`,
{ global: [], selected: [] }
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'object' to be 'object'" },
{ status: "pass", message: "Expected 'false' to be 'false'" },
],
}),
])
})
test("nested objects are preserved", () => {
return expect(
runTest(
`
const original = { key: "value", nested: { prop: 123, deep: { inner: "test" } } }
pm.environment.set("nested_obj", original)
const retrieved = pm.environment.get("nested_obj")
pm.expect(typeof retrieved).toBe("object")
pm.expect(retrieved.key).toBe("value")
pm.expect(retrieved.nested.prop).toBe(123)
pm.expect(retrieved.nested.deep.inner).toBe("test")
`,
{ global: [], selected: [] }
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'object' to be 'object'" },
{ status: "pass", message: "Expected 'value' to be 'value'" },
{ status: "pass", message: "Expected '123' to be '123'" },
{ status: "pass", message: "Expected 'test' to be 'test'" },
],
}),
])
})
})
describe("Null preservation", () => {
test("null is preserved as actual null value", () => {
return expect(
runTest(
`
pm.environment.set("nullable", null)
const value = pm.environment.get("nullable")
pm.expect(value).toBe(null)
pm.expect(value === null).toBe(true)
pm.expect(typeof value).toBe("object")
`,
{ global: [], selected: [] }
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'null' to be 'null'" },
{ status: "pass", message: "Expected 'true' to be 'true'" },
{ status: "pass", message: "Expected 'object' to be 'object'" },
],
}),
])
})
})
describe("Undefined preservation (special case)", () => {
test("undefined is preserved as actual undefined", () => {
return expect(
runTest(
`
pm.environment.set("undef", undefined)
const value = pm.environment.get("undef")
pm.expect(value).toBe(undefined)
pm.expect(typeof value).toBe("undefined")
`,
{ global: [], selected: [] }
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'undefined' to be 'undefined'",
},
{
status: "pass",
message: "Expected 'undefined' to be 'undefined'",
},
],
}),
])
})
test("undefined is distinguishable from non-existent keys", () => {
return expect(
runTest(
`
pm.environment.set("explicit_undef", undefined)
pm.expect(pm.environment.has("explicit_undef")).toBe(true)
pm.expect(pm.environment.has("never_set")).toBe(false)
`,
{ global: [], selected: [] }
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'true' to be 'true'" },
{ status: "pass", message: "Expected 'false' to be 'false'" },
],
}),
])
})
})
describe("Primitive type preservation", () => {
test("numbers are preserved as numbers", () => {
return expect(
runTest(
`
pm.environment.set("num", 123)
const value = pm.environment.get("num")
pm.expect(typeof value).toBe("number")
pm.expect(value).toBe(123)
`,
{ global: [], selected: [] }
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'number' to be 'number'" },
{ status: "pass", message: "Expected '123' to be '123'" },
],
}),
])
})
test("booleans are preserved as booleans", () => {
return expect(
runTest(
`
pm.environment.set("bool", true)
const value = pm.environment.get("bool")
pm.expect(typeof value).toBe("boolean")
pm.expect(value).toBe(true)
`,
{ global: [], selected: [] }
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'boolean' to be 'boolean'" },
{ status: "pass", message: "Expected 'true' to be 'true'" },
],
}),
])
})
test("strings remain strings", () => {
return expect(
runTest(
`
pm.environment.set("str", "hello")
const value = pm.environment.get("str")
pm.expect(typeof value).toBe("string")
pm.expect(value).toBe("hello")
`,
{ global: [], selected: [] }
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'string' to be 'string'" },
{ status: "pass", message: "Expected 'hello' to be 'hello'" },
],
}),
])
})
})
describe("Cross-scope type preservation", () => {
test("pm.globals preserves arrays", () => {
return expect(
runTest(
`
pm.globals.set("global_array", [1, 2, 3])
const value = pm.globals.get("global_array")
pm.expect(Array.isArray(value)).toBe(true)
pm.expect(value.length).toBe(3)
`,
{ global: [], selected: [] }
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'true' to be 'true'" },
{ status: "pass", message: "Expected '3' to be '3'" },
],
}),
])
})
test("pm.variables preserves objects", () => {
return expect(
runTest(
`
pm.variables.set("var_obj", { key: "value" })
const value = pm.variables.get("var_obj")
pm.expect(typeof value).toBe("object")
pm.expect(value.key).toBe("value")
`,
{ global: [], selected: [] }
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'object' to be 'object'" },
{ status: "pass", message: "Expected 'value' to be 'value'" },
],
}),
])
})
})
})

View file

@ -1,31 +1,10 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "test response",
headers: [],
}
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.tests)
)
import { runTest } from "~/utils/test-helpers"
describe("pm namespace - unsupported features", () => {
test("pm.info.iteration throws error", () => {
return expect(
func(
runTest(
`
try {
const iteration = pm.info.iteration
@ -57,7 +36,7 @@ describe("pm namespace - unsupported features", () => {
test("pm.info.iterationCount throws error", () => {
return expect(
func(
runTest(
`
try {
const iterationCount = pm.info.iterationCount
@ -89,7 +68,7 @@ describe("pm namespace - unsupported features", () => {
test("pm.collectionVariables.get() throws error", () => {
return expect(
func(
runTest(
`
try {
pm.collectionVariables.get("test")
@ -121,7 +100,7 @@ describe("pm namespace - unsupported features", () => {
test("pm.vault.get() throws error", () => {
return expect(
func(
runTest(
`
try {
pm.vault.get("test")
@ -153,7 +132,7 @@ describe("pm namespace - unsupported features", () => {
test("pm.iterationData.get() throws error", () => {
return expect(
func(
runTest(
`
try {
pm.iterationData.get("test")
@ -185,7 +164,7 @@ describe("pm namespace - unsupported features", () => {
test("pm.execution.setNextRequest() throws error", () => {
return expect(
func(
runTest(
`
try {
pm.execution.setNextRequest("next-request")
@ -217,7 +196,7 @@ describe("pm namespace - unsupported features", () => {
test("pm.sendRequest() throws error", () => {
return expect(
func(
runTest(
`
try {
pm.sendRequest("https://example.com", () => {})
@ -246,4 +225,68 @@ describe("pm namespace - unsupported features", () => {
}),
])
})
test("pm.visualizer.set() throws error", () => {
return expect(
runTest(
`
try {
pm.visualizer.set("<h1>Test</h1>")
pm.test("Should not reach here", () => {
pm.expect(true).toBe(false)
})
} catch (error) {
pm.test("Throws correct error", () => {
pm.expect(error.message).toInclude("pm.visualizer.set() is not supported")
})
}
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
children: [
expect.objectContaining({
descriptor: "Throws correct error",
expectResults: [{ status: "pass", message: expect.any(String) }],
}),
],
}),
])
})
test("pm.visualizer.clear() throws error", () => {
return expect(
runTest(
`
try {
pm.visualizer.clear()
pm.test("Should not reach here", () => {
pm.expect(true).toBe(false)
})
} catch (error) {
pm.test("Throws correct error", () => {
pm.expect(error.message).toInclude("pm.visualizer.clear() is not supported")
})
}
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
children: [
expect.objectContaining({
descriptor: "Throws correct error",
expectResults: [{ status: "pass", message: expect.any(String) }],
}),
],
}),
])
})
})

View file

@ -1,31 +1,12 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript, runPreRequestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.tests)
)
import { runPreRequestScript } from "~/node"
import { runTest } from "~/utils/test-helpers"
describe("pm.environment", () => {
test("pm.environment.get returns the correct value for an existing active environment value", () => {
return expect(
func(
runTest(
`
const data = pm.environment.get("a")
pm.expect(data).toBe("b")
@ -51,7 +32,7 @@ describe("pm.environment", () => {
test("pm.environment.set creates and retrieves environment variable", () => {
return expect(
func(
runTest(
`
pm.environment.set("test_set", "set_value")
const retrieved = pm.environment.get("test_set")
@ -76,7 +57,7 @@ describe("pm.environment", () => {
test("pm.environment.set works correctly", () => {
return expect(
func(
runTest(
`
pm.environment.set("newVar", "newValue")
const data = pm.environment.get("newVar")
@ -98,7 +79,7 @@ describe("pm.environment", () => {
test("pm.environment.has correctly identifies existing and non-existing variables", () => {
return expect(
func(
runTest(
`
const hasExisting = pm.environment.has("existing_var")
const hasNonExisting = pm.environment.has("non_existing_var")
@ -137,7 +118,7 @@ describe("pm.environment", () => {
describe("pm.globals", () => {
test("pm.globals.get returns the correct value for an existing global environment value", () => {
return expect(
func(
runTest(
`
const data = pm.globals.get("globalVar")
pm.expect(data).toBe("globalValue")
@ -168,7 +149,7 @@ describe("pm.globals", () => {
test("pm.globals.set creates and retrieves global variable", () => {
return expect(
func(
runTest(
`
pm.globals.set("test_global", "global_value")
const retrieved = pm.globals.get("test_global")
@ -195,7 +176,7 @@ describe("pm.globals", () => {
describe("pm.variables", () => {
test("pm.variables.get returns the correct value from any scope", () => {
return expect(
func(
runTest(
`
const data = pm.variables.get("scopedVar")
pm.expect(data).toBe("scopedValue")
@ -226,7 +207,7 @@ describe("pm.variables", () => {
test("pm.variables.set creates and retrieves variable in active environment", () => {
return expect(
func(
runTest(
`
pm.variables.set("test_var", "test_value")
const retrieved = pm.variables.get("test_var")
@ -251,7 +232,7 @@ describe("pm.variables", () => {
test("pm.variables.has correctly identifies existing and non-existing variables", () => {
return expect(
func(
runTest(
`
const hasExisting = pm.variables.has("existing_var")
const hasNonExisting = pm.variables.has("non_existing_var")
@ -288,7 +269,7 @@ describe("pm.variables", () => {
test("pm.variables.replaceIn works correctly", () => {
return expect(
func(
runTest(
`
const template = "Hello {{name}}, welcome to {{place}}!"
const result = pm.variables.replaceIn(template)
@ -327,7 +308,7 @@ describe("pm.variables", () => {
test("pm.variables.replaceIn handles multiple variables", () => {
return expect(
func(
runTest(
`
const template = "User {{name}} has {{count}} items in {{location}}"
const result = pm.variables.replaceIn(template)
@ -375,7 +356,7 @@ describe("pm.variables", () => {
describe("pm.test", () => {
test("pm.test works as expected", () => {
return expect(
func(
runTest(
`
pm.test("Simple test", function() {
pm.expect(1 + 1).toBe(2)
@ -401,6 +382,238 @@ describe("pm.test", () => {
})
})
describe("pm environment get() null vs undefined behavior", () => {
test("pm.environment.get() returns undefined (not null) for non-existent keys", () => {
return expect(
runTest(
`
const value = pm.environment.get("non_existent_key")
pw.expect(value).toBe(undefined)
pw.expect(value === null).toBe(false)
pw.expect(value === undefined).toBe(true)
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'undefined' to be 'undefined'" },
{ status: "pass", message: "Expected 'false' to be 'false'" },
{ status: "pass", message: "Expected 'true' to be 'true'" },
],
}),
])
})
test("pm.globals.get() returns undefined (not null) for non-existent keys", () => {
return expect(
runTest(
`
const value = pm.globals.get("non_existent_global")
pw.expect(value).toBe(undefined)
pw.expect(value === null).toBe(false)
pw.expect(value === undefined).toBe(true)
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'undefined' to be 'undefined'" },
{ status: "pass", message: "Expected 'false' to be 'false'" },
{ status: "pass", message: "Expected 'true' to be 'true'" },
],
}),
])
})
test("pm.variables.get() returns undefined (not null) for non-existent keys", () => {
return expect(
runTest(
`
const value = pm.variables.get("non_existent_var")
pw.expect(value).toBe(undefined)
pw.expect(value === null).toBe(false)
pw.expect(value === undefined).toBe(true)
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'undefined' to be 'undefined'" },
{ status: "pass", message: "Expected 'false' to be 'false'" },
{ status: "pass", message: "Expected 'true' to be 'true'" },
],
}),
])
})
})
describe("pm environment clear() and toObject() methods", () => {
test("pm.environment.clear() removes all environment variables", () => {
return expect(
runTest(
`
// Set some variables
pm.environment.set("var1", "value1")
pm.environment.set("var2", "value2")
pm.environment.set("var3", "value3")
// Verify they exist
pw.expect(pm.environment.has("var1")).toBe(true)
pw.expect(pm.environment.has("var2")).toBe(true)
pw.expect(pm.environment.has("var3")).toBe(true)
// Clear all
pm.environment.clear()
// Verify all are removed
pw.expect(pm.environment.has("var1")).toBe(false)
pw.expect(pm.environment.has("var2")).toBe(false)
pw.expect(pm.environment.has("var3")).toBe(false)
// Verify toObject returns empty
const allVars = pm.environment.toObject()
pw.expect(Object.keys(allVars).length).toBe(0)
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: expect.arrayContaining([
{ status: "pass", message: "Expected 'true' to be 'true'" },
{ status: "pass", message: "Expected 'false' to be 'false'" },
{ status: "pass", message: "Expected '0' to be '0'" },
]),
}),
])
})
test("pm.environment.toObject() returns all environment variables as object", () => {
return expect(
runTest(
`
const allVars = pm.environment.toObject()
pw.expect(typeof allVars).toBe("object")
pw.expect(allVars.existing_var).toBe("existing_value")
`,
{
global: [],
selected: [
{
key: "existing_var",
currentValue: "existing_value",
initialValue: "existing_value",
secret: false,
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'object' to be 'object'",
},
{
status: "pass",
message: "Expected 'existing_value' to be 'existing_value'",
},
],
}),
])
})
test("pm.globals.clear() removes all global variables", () => {
return expect(
runTest(
`
// Set some globals
pm.globals.set("global1", "value1")
pm.globals.set("global2", "value2")
// Verify they exist
pw.expect(pm.globals.has("global1")).toBe(true)
pw.expect(pm.globals.has("global2")).toBe(true)
// Clear all
pm.globals.clear()
// Verify all are removed
pw.expect(pm.globals.has("global1")).toBe(false)
pw.expect(pm.globals.has("global2")).toBe(false)
// Verify toObject returns empty
const allGlobals = pm.globals.toObject()
pw.expect(Object.keys(allGlobals).length).toBe(0)
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: expect.arrayContaining([
{ status: "pass", message: "Expected 'true' to be 'true'" },
{ status: "pass", message: "Expected 'false' to be 'false'" },
{ status: "pass", message: "Expected '0' to be '0'" },
]),
}),
])
})
test("pm.globals.toObject() returns all global variables as object", () => {
return expect(
runTest(
`
const allGlobals = pm.globals.toObject()
pw.expect(typeof allGlobals).toBe("object")
pw.expect(allGlobals.global_var).toBe("global_value")
`,
{
global: [
{
key: "global_var",
currentValue: "global_value",
initialValue: "global_value",
secret: false,
},
],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'object' to be 'object'",
},
{
status: "pass",
message: "Expected 'global_value' to be 'global_value'",
},
],
}),
])
})
})
describe("pm namespace - pre-request scripts", () => {
const DEFAULT_REQUEST = getDefaultRESTRequest()

View file

@ -1,32 +1,10 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.tests)
)
import { runTest } from "~/utils/test-helpers"
describe("pw.env.get", () => {
test("returns the correct value for an existing selected environment value", () => {
return expect(
func(
runTest(
`
const data = pw.env.get("a")
pw.expect(data).toBe("b")
@ -57,7 +35,7 @@ describe("pw.env.get", () => {
test("returns the correct value for an existing global environment value", () => {
return expect(
func(
runTest(
`
const data = pw.env.get("a")
pw.expect(data).toBe("b")
@ -88,7 +66,7 @@ describe("pw.env.get", () => {
test("returns undefined for a key that is not present in both selected or environment", () => {
return expect(
func(
runTest(
`
const data = pw.env.get("a")
pw.expect(data).toBe(undefined)
@ -112,7 +90,7 @@ describe("pw.env.get", () => {
test("returns the value defined in selected environment if it is also present in global", () => {
return expect(
func(
runTest(
`
const data = pw.env.get("a")
pw.expect(data).toBe("selected val")
@ -150,7 +128,7 @@ describe("pw.env.get", () => {
test("does not resolve environment values", () => {
return expect(
func(
runTest(
`
const data = pw.env.get("a")
pw.expect(data).toBe("<<hello>>")
@ -181,7 +159,7 @@ describe("pw.env.get", () => {
test("errors if the key is not a string", () => {
return expect(
func(
runTest(
`
const data = pw.env.get(5)
`,

View file

@ -1,32 +1,10 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.tests)
)
import { runTest } from "~/utils/test-helpers"
describe("pw.env.getResolve", () => {
test("returns the correct value for an existing selected environment value", () => {
return expect(
func(
runTest(
`
const data = pw.env.getResolve("a")
pw.expect(data).toBe("b")
@ -57,7 +35,7 @@ describe("pw.env.getResolve", () => {
test("returns the correct value for an existing global environment value", () => {
return expect(
func(
runTest(
`
const data = pw.env.getResolve("a")
pw.expect(data).toBe("b")
@ -88,7 +66,7 @@ describe("pw.env.getResolve", () => {
test("returns undefined for a key that is not present in both selected or environment", () => {
return expect(
func(
runTest(
`
const data = pw.env.getResolve("a")
pw.expect(data).toBe(undefined)
@ -112,7 +90,7 @@ describe("pw.env.getResolve", () => {
test("returns the value defined in selected environment if it is also present in global", () => {
return expect(
func(
runTest(
`
const data = pw.env.getResolve("a")
pw.expect(data).toBe("selected val")
@ -150,7 +128,7 @@ describe("pw.env.getResolve", () => {
test("resolve environment values", () => {
return expect(
func(
runTest(
`
const data = pw.env.getResolve("a")
pw.expect(data).toBe("there")
@ -187,7 +165,7 @@ describe("pw.env.getResolve", () => {
test("returns unresolved value on infinite loop in resolution", () => {
return expect(
func(
runTest(
`
const data = pw.env.getResolve("a")
pw.expect(data).toBe("<<hello>>")
@ -224,7 +202,7 @@ describe("pw.env.getResolve", () => {
test("errors if the key is not a string", () => {
return expect(
func(
runTest(
`
const data = pw.env.getResolve(5)
`,

View file

@ -1,32 +1,10 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.tests)
)
import { runTest } from "~/utils/test-helpers"
describe("pw.env.resolve", () => {
test("value should be a string", () => {
return expect(
func(
runTest(
`
pw.env.resolve(5)
`,
@ -40,7 +18,7 @@ describe("pw.env.resolve", () => {
test("resolves global variables correctly", () => {
return expect(
func(
runTest(
`
const data = pw.env.resolve("<<hello>>")
pw.expect(data).toBe("there")
@ -71,7 +49,7 @@ describe("pw.env.resolve", () => {
test("resolves selected env variables correctly", () => {
return expect(
func(
runTest(
`
const data = pw.env.resolve("<<hello>>")
pw.expect(data).toBe("there")
@ -102,7 +80,7 @@ describe("pw.env.resolve", () => {
test("chooses selected env variable over global variables when both have same variable", () => {
return expect(
func(
runTest(
`
const data = pw.env.resolve("<<hello>>")
pw.expect(data).toBe("there")
@ -140,7 +118,7 @@ describe("pw.env.resolve", () => {
test("if infinite loop in resolution, abandons resolutions altogether", () => {
return expect(
func(
runTest(
`
const data = pw.env.resolve("<<hello>>")
pw.expect(data).toBe("<<hello>>")

View file

@ -1,42 +1,10 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.envs)
)
const funcTest = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.tests)
)
import { runTestAndGetEnvs, runTest } from "~/utils/test-helpers"
describe("pw.env.set", () => {
test("updates the selected environment variable correctly", () => {
return expect(
func(
runTestAndGetEnvs(
`
pw.env.set("a", "c")
`,
@ -68,7 +36,7 @@ describe("pw.env.set", () => {
test("updates the global environment variable correctly", () => {
return expect(
func(
runTestAndGetEnvs(
`
pw.env.set("a", "c")
`,
@ -100,7 +68,7 @@ describe("pw.env.set", () => {
test("updates the selected environment if env present in both", () => {
return expect(
func(
runTestAndGetEnvs(
`
pw.env.set("a", "c")
`,
@ -147,7 +115,7 @@ describe("pw.env.set", () => {
test("non existent keys are created in the selected environment", () => {
return expect(
func(
runTestAndGetEnvs(
`
pw.env.set("a", "c")
`,
@ -173,7 +141,7 @@ describe("pw.env.set", () => {
test("keys should be a string", () => {
return expect(
func(
runTestAndGetEnvs(
`
pw.env.set(5, "c")
`,
@ -187,7 +155,7 @@ describe("pw.env.set", () => {
test("values should be a string", () => {
return expect(
func(
runTestAndGetEnvs(
`
pw.env.set("a", 5)
`,
@ -201,7 +169,7 @@ describe("pw.env.set", () => {
test("both keys and values should be strings", () => {
return expect(
func(
runTestAndGetEnvs(
`
pw.env.set(5, 5)
`,
@ -215,7 +183,7 @@ describe("pw.env.set", () => {
test("set environment values are reflected in the script execution", () => {
return expect(
funcTest(
runTest(
`
pw.env.set("a", "b")
pw.expect(pw.env.get("a")).toBe("b")

View file

@ -1,42 +1,10 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.envs)
)
const funcTest = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.tests)
)
import { runTestAndGetEnvs, runTest } from "~/utils/test-helpers"
describe("pw.env.unset", () => {
test("removes the variable set in selected environment correctly", () => {
return expect(
func(
runTestAndGetEnvs(
`
pw.env.unset("baseUrl")
`,
@ -61,7 +29,7 @@ describe("pw.env.unset", () => {
test("removes the variable set in global environment correctly", () => {
return expect(
func(
runTestAndGetEnvs(
`
pw.env.unset("baseUrl")
`,
@ -86,7 +54,7 @@ describe("pw.env.unset", () => {
test("removes the variable from selected environment if the entry is present in both selected and global environments", () => {
return expect(
func(
runTestAndGetEnvs(
`
pw.env.unset("baseUrl")
`,
@ -126,7 +94,7 @@ describe("pw.env.unset", () => {
test("removes the initial occurrence of an entry if duplicate entries exist in the selected environment", () => {
return expect(
func(
runTestAndGetEnvs(
`
pw.env.unset("baseUrl")
`,
@ -179,7 +147,7 @@ describe("pw.env.unset", () => {
test("removes the initial occurrence of an entry if duplicate entries exist in the global environment", () => {
return expect(
func(
runTestAndGetEnvs(
`
pw.env.unset("baseUrl")
`,
@ -218,7 +186,7 @@ describe("pw.env.unset", () => {
test("no change if attempting to delete non-existent keys", () => {
return expect(
func(
runTestAndGetEnvs(
`
pw.env.unset("baseUrl")
`,
@ -237,7 +205,7 @@ describe("pw.env.unset", () => {
test("keys should be a string", () => {
return expect(
func(
runTestAndGetEnvs(
`
pw.env.unset(5)
`,
@ -251,7 +219,7 @@ describe("pw.env.unset", () => {
test("set environment values are reflected in the script execution", () => {
return expect(
funcTest(
runTest(
`
pw.env.unset("baseUrl")
pw.expect(pw.env.get("baseUrl")).toBe(undefined)

View file

@ -1,33 +1,11 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const func = (script: string, res: TestResponse) =>
pipe(
runTestScript(script, {
envs: { global: [], selected: [] },
request: defaultRequest,
response: res,
}),
TE.map((x) => x.tests)
)
import { runTest, fakeResponse } from "~/utils/test-helpers"
describe("toBe", () => {
describe("general assertion (no negation)", () => {
test("expect equals expected passes assertion", () => {
return expect(
func(
runTest(
`
pw.expect(2).toBe(2)
`,
@ -44,7 +22,7 @@ describe("toBe", () => {
test("expect not equals expected fails assertion", () => {
return expect(
func(
runTest(
`
pw.expect(2).toBe(4)
`,
@ -63,7 +41,7 @@ describe("toBe", () => {
describe("general assertion (with negation)", () => {
test("expect equals expected fails assertion", () => {
return expect(
func(
runTest(
`
pw.expect(2).not.toBe(2)
`,
@ -83,7 +61,7 @@ describe("toBe", () => {
test("expect not equals expected passes assertion", () => {
return expect(
func(
runTest(
`
pw.expect(2).not.toBe(4)
`,
@ -105,7 +83,7 @@ describe("toBe", () => {
test("strict checks types", () => {
return expect(
func(
runTest(
`
pw.expect(2).toBe("2")
`,

View file

@ -1,34 +1,12 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const func = (script: string, res: TestResponse) =>
pipe(
runTestScript(script, {
envs: { global: [], selected: [] },
request: defaultRequest,
response: res,
}),
TE.map((x) => x.tests)
)
import { runTest, fakeResponse } from "~/utils/test-helpers"
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)()
runTest(`pw.expect(${i}).toBeLevel2xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
@ -45,7 +23,7 @@ describe("toBeLevelxxx", { timeout: 100000 }, () => {
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)()
runTest(`pw.expect(${i}).toBeLevel2xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
@ -61,7 +39,7 @@ describe("toBeLevelxxx", { timeout: 100000 }, () => {
test("give error if the expect value was not a number with no negation", async () => {
await expect(
func(`pw.expect("foo").toBeLevel2xx()`, fakeResponse)()
runTest(`pw.expect("foo").toBeLevel2xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
@ -78,7 +56,7 @@ describe("toBeLevelxxx", { timeout: 100000 }, () => {
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)()
runTest(`pw.expect(${i}).not.toBeLevel2xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
@ -95,7 +73,7 @@ describe("toBeLevelxxx", { timeout: 100000 }, () => {
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)()
runTest(`pw.expect(${i}).not.toBeLevel2xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
@ -111,7 +89,7 @@ describe("toBeLevelxxx", { timeout: 100000 }, () => {
test("give error if the expect value was not a number with negation", async () => {
await expect(
func(`pw.expect("foo").not.toBeLevel2xx()`, fakeResponse)()
runTest(`pw.expect("foo").not.toBeLevel2xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
@ -130,7 +108,7 @@ describe("toBeLevelxxx", { timeout: 100000 }, () => {
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)()
runTest(`pw.expect(${i}).toBeLevel3xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
@ -147,7 +125,7 @@ describe("toBeLevelxxx", { timeout: 100000 }, () => {
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)()
runTest(`pw.expect(${i}).toBeLevel3xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
@ -163,7 +141,7 @@ describe("toBeLevelxxx", { timeout: 100000 }, () => {
test("give error if the expect value is not a number without negation", () => {
return expect(
func(`pw.expect("foo").toBeLevel3xx()`, fakeResponse)()
runTest(`pw.expect("foo").toBeLevel3xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
@ -180,7 +158,7 @@ describe("toBeLevelxxx", { timeout: 100000 }, () => {
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)()
runTest(`pw.expect(${i}).not.toBeLevel3xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
@ -197,7 +175,7 @@ describe("toBeLevelxxx", { timeout: 100000 }, () => {
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)()
runTest(`pw.expect(${i}).not.toBeLevel3xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
@ -213,7 +191,7 @@ describe("toBeLevelxxx", { timeout: 100000 }, () => {
test("give error if the expect value is not a number with negation", () => {
return expect(
func(`pw.expect("foo").not.toBeLevel3xx()`, fakeResponse)()
runTest(`pw.expect("foo").not.toBeLevel3xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
@ -232,7 +210,7 @@ describe("toBeLevelxxx", { timeout: 100000 }, () => {
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)()
runTest(`pw.expect(${i}).toBeLevel4xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
@ -249,7 +227,7 @@ describe("toBeLevelxxx", { timeout: 100000 }, () => {
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)()
runTest(`pw.expect(${i}).toBeLevel4xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
@ -265,7 +243,7 @@ describe("toBeLevelxxx", { timeout: 100000 }, () => {
test("give error if the expected value is not a number without negation", () => {
return expect(
func(`pw.expect("foo").toBeLevel4xx()`, fakeResponse)()
runTest(`pw.expect("foo").toBeLevel4xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
@ -282,7 +260,7 @@ describe("toBeLevelxxx", { timeout: 100000 }, () => {
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)()
runTest(`pw.expect(${i}).not.toBeLevel4xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
@ -299,7 +277,7 @@ describe("toBeLevelxxx", { timeout: 100000 }, () => {
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)()
runTest(`pw.expect(${i}).not.toBeLevel4xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
@ -315,7 +293,7 @@ describe("toBeLevelxxx", { timeout: 100000 }, () => {
test("give error if the expected value is not a number with negation", () => {
return expect(
func(`pw.expect("foo").not.toBeLevel4xx()`, fakeResponse)()
runTest(`pw.expect("foo").not.toBeLevel4xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
@ -334,7 +312,7 @@ describe("toBeLevelxxx", { timeout: 100000 }, () => {
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)()
runTest(`pw.expect(${i}).toBeLevel5xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
@ -351,7 +329,7 @@ describe("toBeLevelxxx", { timeout: 100000 }, () => {
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)()
runTest(`pw.expect(${i}).toBeLevel5xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
@ -367,7 +345,7 @@ describe("toBeLevelxxx", { timeout: 100000 }, () => {
test("give error if the expect value is not a number with no negation", () => {
return expect(
func(`pw.expect("foo").toBeLevel5xx()`, fakeResponse)()
runTest(`pw.expect("foo").toBeLevel5xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
@ -384,7 +362,7 @@ describe("toBeLevelxxx", { timeout: 100000 }, () => {
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)()
runTest(`pw.expect(${i}).not.toBeLevel5xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
@ -401,7 +379,7 @@ describe("toBeLevelxxx", { timeout: 100000 }, () => {
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)()
runTest(`pw.expect(${i}).not.toBeLevel5xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
@ -417,7 +395,7 @@ describe("toBeLevelxxx", { timeout: 100000 }, () => {
test("give error if the expect value is not a number with negation", () => {
return expect(
func(`pw.expect("foo").not.toBeLevel5xx()`, fakeResponse)()
runTest(`pw.expect("foo").not.toBeLevel5xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [

View file

@ -1,32 +1,10 @@
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { getDefaultRESTRequest } from "@hoppscotch/data"
import { runTestScript } from "~/node"
import { TestResponse } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const func = (script: string, res: TestResponse) =>
pipe(
runTestScript(script, {
envs: { global: [], selected: [] },
request: defaultRequest,
response: res,
}),
TE.map((x) => x.tests)
)
import { runTest, fakeResponse } from "~/utils/test-helpers"
describe("toBeType", () => {
test("asserts true for valid type expectations with no negation", () => {
return expect(
func(
runTest(
`
pw.expect(2).toBeType("number")
pw.expect("2").toBeType("string")
@ -77,7 +55,7 @@ describe("toBeType", () => {
test("asserts false for invalid type expectations with no negation", () => {
return expect(
func(
runTest(
`
pw.expect(2).toBeType("string")
pw.expect("2").toBeType("number")
@ -108,7 +86,7 @@ describe("toBeType", () => {
test("asserts false for valid type expectations with negation", () => {
return expect(
func(
runTest(
`
pw.expect(2).not.toBeType("number")
pw.expect("2").not.toBeType("string")
@ -142,7 +120,7 @@ describe("toBeType", () => {
test("asserts true for invalid type expectations with negation", () => {
return expect(
func(
runTest(
`
pw.expect(2).not.toBeType("string")
pw.expect("2").not.toBeType("number")
@ -197,7 +175,7 @@ describe("toBeType", () => {
test("gives error for invalid type names without negation", () => {
return expect(
func(
runTest(
`
pw.expect(2).toBeType("foo")
pw.expect("2").toBeType("bar")
@ -237,7 +215,7 @@ describe("toBeType", () => {
test("gives error for invalid type names with negation", () => {
return expect(
func(
runTest(
`
pw.expect(2).not.toBeType("foo")
pw.expect("2").not.toBeType("bar")

View file

@ -1,32 +1,10 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const func = (script: string, res: TestResponse) =>
pipe(
runTestScript(script, {
envs: { global: [], selected: [] },
request: defaultRequest,
response: res,
}),
TE.map((x) => x.tests)
)
import { runTest, fakeResponse } from "~/utils/test-helpers"
describe("toHaveLength", () => {
test("asserts true for valid lengths with no negation", () => {
return expect(
func(
runTest(
`
pw.expect([1, 2, 3, 4]).toHaveLength(4)
pw.expect([]).toHaveLength(0)
@ -45,7 +23,7 @@ describe("toHaveLength", () => {
test("asserts false for invalid lengths with no negation", () => {
return expect(
func(
runTest(
`
pw.expect([]).toHaveLength(4)
pw.expect([1, 2, 3, 4]).toHaveLength(0)
@ -64,7 +42,7 @@ describe("toHaveLength", () => {
test("asserts false for valid lengths with negation", () => {
return expect(
func(
runTest(
`
pw.expect([1, 2, 3, 4]).not.toHaveLength(4)
pw.expect([]).not.toHaveLength(0)
@ -89,7 +67,7 @@ describe("toHaveLength", () => {
test("asserts true for invalid lengths with negation", () => {
return expect(
func(
runTest(
`
pw.expect([]).not.toHaveLength(4)
pw.expect([1, 2, 3, 4]).not.toHaveLength(0)
@ -114,7 +92,7 @@ describe("toHaveLength", () => {
test("gives error if not called on an array or a string with no negation", () => {
return expect(
func(
runTest(
`
pw.expect(5).toHaveLength(0)
pw.expect(true).toHaveLength(0)
@ -141,7 +119,7 @@ describe("toHaveLength", () => {
test("gives error if not called on an array or a string with negation", () => {
return expect(
func(
runTest(
`
pw.expect(5).not.toHaveLength(0)
pw.expect(true).not.toHaveLength(0)
@ -168,7 +146,7 @@ describe("toHaveLength", () => {
test("gives an error if toHaveLength parameter is not a number without negation", () => {
return expect(
func(
runTest(
`
pw.expect([1, 2, 3, 4]).toHaveLength("a")
`,
@ -188,7 +166,7 @@ describe("toHaveLength", () => {
test("gives an error if toHaveLength parameter is not a number with negation", () => {
return expect(
func(
runTest(
`
pw.expect([1, 2, 3, 4]).not.toHaveLength("a")
`,

View file

@ -1,32 +1,10 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const func = (script: string, res: TestResponse) =>
pipe(
runTestScript(script, {
envs: { global: [], selected: [] },
request: defaultRequest,
response: res,
}),
TE.map((x) => x.tests)
)
import { runTest, fakeResponse } from "~/utils/test-helpers"
describe("toInclude", () => {
test("asserts true for collections with matching values", () => {
return expect(
func(
runTest(
`
pw.expect([1, 2, 3]).toInclude(1)
pw.expect("123").toInclude(1)
@ -45,7 +23,7 @@ describe("toInclude", () => {
test("asserts false for collections without matching values", () => {
return expect(
func(
runTest(
`
pw.expect([1, 2, 3]).toInclude(4)
pw.expect("123").toInclude(4)
@ -64,7 +42,7 @@ describe("toInclude", () => {
test("asserts false for empty collections", () => {
return expect(
func(
runTest(
`
pw.expect([]).not.toInclude(0)
pw.expect("").not.toInclude(0)
@ -89,7 +67,7 @@ describe("toInclude", () => {
test("asserts false for [number array].includes(string)", () => {
return expect(
func(
runTest(
`
pw.expect([1]).not.toInclude("1")
`,
@ -112,7 +90,7 @@ describe("toInclude", () => {
// (`"123".includes(123)` returns `True` in Node.js v14.19.1)
// See https://tc39.es/ecma262/multipage/text-processing.html#sec-string.prototype.includes
return expect(
func(`pw.expect("123").toInclude(123)`, fakeResponse)()
runTest(`pw.expect("123").toInclude(123)`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
@ -127,7 +105,7 @@ describe("toInclude", () => {
test("gives error if not called on an array or string", () => {
return expect(
func(
runTest(
`
pw.expect(5).not.toInclude(0)
pw.expect(true).not.toInclude(0)
@ -152,7 +130,7 @@ describe("toInclude", () => {
test("gives an error if toInclude parameter is null", () => {
return expect(
func(
runTest(
`
pw.expect([1, 2, 3, 4]).not.toInclude(null)
`,
@ -172,7 +150,7 @@ describe("toInclude", () => {
test("gives an error if toInclude parameter is undefined", () => {
return expect(
func(
runTest(
`
pw.expect([1, 2, 3, 4]).not.toInclude(undefined)
`,

View file

@ -1,32 +1,10 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const func = (script: string, res: TestResponse) =>
pipe(
runTestScript(script, {
envs: { global: [], selected: [] },
request: defaultRequest,
response: res,
}),
TE.map((x) => x.tests)
)
import { runTest, fakeResponse } from "~/utils/test-helpers"
describe("runTestScript", () => {
test("returns a resolved promise for a valid test script with all green", () => {
return expect(
func(
runTest(
`
pw.test("Arithmetic operations", () => {
const size = 500 + 500;
@ -43,7 +21,7 @@ describe("runTestScript", () => {
test("resolves for tests with failed expectations", () => {
return expect(
func(
runTest(
`
pw.test("Arithmetic operations", () => {
const size = 500 + 500;
@ -61,7 +39,7 @@ describe("runTestScript", () => {
// TODO: We need a more concrete behavior for this
test("rejects for invalid syntax on tests", () => {
return expect(
func(
runTest(
`
pw.test("Arithmetic operations", () => {
const size = 500 + 500;

View file

@ -8,7 +8,7 @@ import { describe, expect, test } from "vitest"
import { getRequestSetterMethods } from "~/utils/pre-request"
const baseRequest: HoppRESTRequest = {
v: "15",
v: "16",
name: "Test Request",
endpoint: "https://example.com/api",
method: "GET",
@ -30,11 +30,11 @@ describe("getRequestSetterMethods", () => {
expect(updatedRequest.endpoint).toBe("https://updated.com/api")
})
test("`setMethod` should update and uppercase the method", () => {
test("`setMethod` should update the method (case preserved)", () => {
const { methods, updatedRequest } = getRequestSetterMethods(baseRequest)
methods.setMethod("post")
expect(updatedRequest.method).toBe("POST")
expect(updatedRequest.method).toBe("post")
})
test("`setHeader` setter should update existing header case-insensitively", () => {

View file

@ -1,11 +1,13 @@
import {
getSharedCookieMethods,
getSharedEnvMethods,
getSharedRequestProps,
preventCyclicObjects,
} from "~/utils/shared"
import { Cookie, HoppRESTRequest } from "@hoppscotch/data"
import { describe, expect, test } from "vitest"
import { TestResult } from "~/types"
describe("preventCyclicObjects", () => {
test("succeeds with a simple object", () => {
@ -30,7 +32,7 @@ describe("preventCyclicObjects", () => {
describe("getSharedRequestProps", () => {
const baseRequest: HoppRESTRequest = {
v: "15",
v: "16",
name: "Test Request",
endpoint: "https://example.com/api",
method: "GET",
@ -73,21 +75,6 @@ describe("getSharedRequestProps", () => {
const request = getSharedRequestProps(baseRequest)
expect(request.auth).toEqual(baseRequest.auth)
})
test("`params` getter", () => {
const request = getSharedRequestProps(baseRequest)
expect(request.params).toEqual(baseRequest.params)
})
test("`body` getter", () => {
const request = getSharedRequestProps(baseRequest)
expect(request.body).toEqual(baseRequest.body)
})
test("`auth` getter", () => {
const request = getSharedRequestProps(baseRequest)
expect(request.auth).toEqual(baseRequest.auth)
})
})
describe("getSharedCookieMethods", () => {
@ -184,3 +171,340 @@ describe("getSharedCookieMethods", () => {
expect(() => methods.clear(123 as any)).toThrow()
})
})
describe("getSharedEnvMethods - Experimental Sandbox (isHoppNamespace=true)", () => {
const baseEnvs: TestResult["envs"] = {
global: [
{
key: "globalKey",
currentValue: "globalVal",
initialValue: "globalVal",
secret: false,
},
],
selected: [
{
key: "selectedKey",
currentValue: "selectedVal",
initialValue: "selectedVal",
secret: false,
},
],
}
test("returns pw and hopp namespace structure", () => {
const { methods } = getSharedEnvMethods(baseEnvs, true)
expect(methods).toHaveProperty("pw")
expect(methods).toHaveProperty("hopp")
expect(methods.pw).toHaveProperty("get")
expect(methods.hopp).toHaveProperty("set")
})
test("pw.get retrieves from selected then global", () => {
const { methods } = getSharedEnvMethods(baseEnvs, true)
expect(methods.pw.get("selectedKey")).toBe("selectedVal")
expect(methods.pw.get("globalKey")).toBe("globalVal")
expect(methods.pw.get("nonexistent")).toBeUndefined()
})
test("pw.set updates selected environment", () => {
const { methods, updatedEnvs } = getSharedEnvMethods(baseEnvs, true)
methods.pw.set("newKey", "newVal")
expect(updatedEnvs.selected).toContainEqual({
key: "newKey",
currentValue: "newVal",
initialValue: "newVal",
secret: false,
})
})
test("pw.set validates string key and value", () => {
const { methods } = getSharedEnvMethods(baseEnvs, true)
expect(() => methods.pw.set(123 as any, "value")).toThrow(
"Expected key to be a string"
)
expect(() => methods.pw.set("key", 123 as any)).toThrow(
"Expected value to be a string"
)
})
test("pw.resolve handles template strings", () => {
const { methods } = getSharedEnvMethods(
{
global: [],
selected: [
{
key: "name",
currentValue: "Alice",
initialValue: "Alice",
secret: false,
},
{
key: "greeting",
currentValue: "Hello <<name>>",
initialValue: "Hello <<name>>",
secret: false,
},
],
},
true
)
const resolved = methods.pw.resolve("<<greeting>>")
expect(resolved).toBe("Hello Alice")
})
test("pw.getResolve combines get and resolve", () => {
const { methods } = getSharedEnvMethods(
{
global: [],
selected: [
{
key: "baseUrl",
currentValue: "https://api.example.com",
initialValue: "https://api.example.com",
secret: false,
},
{
key: "endpoint",
currentValue: "<<baseUrl>>/users",
initialValue: "<<baseUrl>>/users",
secret: false,
},
],
},
true
)
const resolved = methods.pw.getResolve("endpoint")
expect(resolved).toBe("https://api.example.com/users")
})
test("hopp.set creates new variable in selected scope (default source='all')", () => {
const { methods, updatedEnvs } = getSharedEnvMethods(baseEnvs, true)
methods.hopp.set("hoppKey", "hoppVal")
expect(updatedEnvs.selected).toContainEqual({
key: "hoppKey",
currentValue: "hoppVal",
initialValue: "hoppVal",
secret: false,
})
})
test("hopp.set validates string types", () => {
const { methods } = getSharedEnvMethods(baseEnvs, true)
expect(() => methods.hopp.set(123 as any, "value")).toThrow(
"Expected key to be a string"
)
expect(() => methods.hopp.set("key", 123 as any)).toThrow(
"Expected value to be a string"
)
})
test("hopp.delete removes variable from selected scope", () => {
const { methods, updatedEnvs } = getSharedEnvMethods(baseEnvs, true)
methods.hopp.delete("selectedKey")
expect(updatedEnvs.selected).not.toContainEqual(
expect.objectContaining({ key: "selectedKey" })
)
expect(updatedEnvs.global.length).toBe(1)
})
test("hopp.reset resets a variable to its initial value", () => {
const { methods, updatedEnvs } = getSharedEnvMethods(
{
global: [],
selected: [
{
key: "testKey",
currentValue: "modified",
initialValue: "original",
secret: false,
},
],
},
true
)
methods.hopp.reset("testKey")
const variable = updatedEnvs.selected.find((e) => e.key === "testKey")
expect(variable?.currentValue).toBe("original")
expect(variable?.initialValue).toBe("original")
})
test("hopp.getInitialRaw returns initial value", () => {
const { methods } = getSharedEnvMethods(
{
global: [],
selected: [
{
key: "testKey",
currentValue: "currentVal",
initialValue: "initialVal",
secret: false,
},
],
},
true
)
expect(methods.hopp.getInitialRaw("testKey")).toBe("initialVal")
expect(methods.hopp.getInitialRaw("nonexistent")).toBeNull()
})
test("hopp.setInitial sets initial value", () => {
const { methods, updatedEnvs } = getSharedEnvMethods(baseEnvs, true)
methods.hopp.setInitial("initKey", "initVal")
const created = updatedEnvs.selected.find((e) => e.key === "initKey")
expect(created).toBeDefined()
expect(created?.initialValue).toBe("initVal")
expect(created?.currentValue).toBe("initVal")
})
})
describe("getSharedEnvMethods - Legacy Sandbox (isHoppNamespace=false)", () => {
const baseEnvs: TestResult["envs"] = {
global: [
{
key: "globalKey",
currentValue: "globalVal",
initialValue: "globalVal",
secret: false,
},
],
selected: [
{
key: "selectedKey",
currentValue: "selectedVal",
initialValue: "selectedVal",
secret: false,
},
],
}
test("returns env object structure (not pw/hopp)", () => {
const { methods } = getSharedEnvMethods(baseEnvs, false)
expect(methods).toHaveProperty("env")
expect(methods).not.toHaveProperty("pw")
expect(methods).not.toHaveProperty("hopp")
})
test("env object has all expected methods", () => {
const { methods } = getSharedEnvMethods(baseEnvs, false)
expect(typeof methods.env.get).toBe("function")
expect(typeof methods.env.set).toBe("function")
expect(typeof methods.env.resolve).toBe("function")
expect(typeof methods.env.getResolve).toBe("function")
})
test("env.get retrieves from selected then global", () => {
const { methods } = getSharedEnvMethods(baseEnvs, false)
expect(methods.env.get("selectedKey")).toBe("selectedVal")
expect(methods.env.get("globalKey")).toBe("globalVal")
expect(methods.env.get("nonexistent")).toBeUndefined()
})
test("env.set updates environment correctly", () => {
const { methods, updatedEnvs } = getSharedEnvMethods(baseEnvs, false)
methods.env.set("newKey", "newVal")
expect(updatedEnvs.selected).toContainEqual({
key: "newKey",
currentValue: "newVal",
initialValue: "newVal",
secret: false,
})
})
test("env.set validates string types (regression test for #5433)", () => {
const { methods } = getSharedEnvMethods(baseEnvs, false)
// This is the bug that was fixed in #5433 - missing validation
expect(() => methods.env.set(123 as any, "value")).toThrow(
"Expected key to be a string"
)
expect(() => methods.env.set("key", 123 as any)).toThrow(
"Expected value to be a string"
)
})
test("env.resolve handles template strings", () => {
const { methods } = getSharedEnvMethods(
{
global: [],
selected: [
{
key: "user",
currentValue: "Bob",
initialValue: "Bob",
secret: false,
},
{
key: "message",
currentValue: "Hello <<user>>",
initialValue: "Hello <<user>>",
secret: false,
},
],
},
false
)
const resolved = methods.env.resolve("<<message>>")
expect(resolved).toBe("Hello Bob")
})
test("env.getResolve returns resolved value", () => {
const { methods } = getSharedEnvMethods(
{
global: [],
selected: [
{
key: "domain",
currentValue: "example.com",
initialValue: "example.com",
secret: false,
},
{
key: "apiUrl",
currentValue: "https://<<domain>>/api",
initialValue: "https://<<domain>>/api",
secret: false,
},
],
},
false
)
const resolved = methods.env.getResolve("apiUrl")
expect(resolved).toBe("https://example.com/api")
})
test("env object structure prevents #5433 regression (pw.env not recognized)", () => {
const { methods } = getSharedEnvMethods(baseEnvs, false)
// In legacy sandbox, this gets assigned to pw.env
// The bug was that pw.env was undefined because the structure wasn't correct
expect(methods.env).toBeDefined()
expect(typeof methods.env).toBe("object")
expect(methods.env.get).toBeDefined()
expect(methods.env.set).toBeDefined()
})
})

View file

@ -17,7 +17,9 @@ export const createPmNamespaceMethods = (
return config.request.name
}),
pmInfoRequestId: defineSandboxFn(ctx, "pmInfoRequestId", () => {
return config.request.id
// Use request.id if available, fallback to request.name
// Postman uses a unique ID, but for compatibility we use name if ID not set
return config.request.id || config.request.name
}),
}
}

View file

@ -1,6 +1,11 @@
import { CageModuleCtx, defineSandboxFn } from "faraday-cage/modules"
import type { EnvMethods, RequestProps, PwNamespaceMethods } from "~/types"
import type {
EnvMethods,
RequestProps,
PwNamespaceMethods,
SandboxValue,
} from "~/types"
/**
* Creates pw namespace methods for the sandbox environment
@ -13,41 +18,49 @@ export const createPwNamespaceMethods = (
): PwNamespaceMethods => {
return {
// `pw` namespace environment methods
envGet: defineSandboxFn(ctx, "envGet", function (key: any, options: any) {
return envMethods.pw.get(key, options)
}),
envGet: defineSandboxFn(
ctx,
"envGet",
function (key: SandboxValue, options: SandboxValue) {
return envMethods.pw.get(key, options)
}
),
envGetResolve: defineSandboxFn(
ctx,
"envGetResolve",
function (key: any, options: any) {
function (key: SandboxValue, options: SandboxValue) {
return envMethods.pw.getResolve(key, options)
}
),
envSet: defineSandboxFn(
ctx,
"envSet",
function (key: any, value: any, options: any) {
function (key: SandboxValue, value: SandboxValue, options: SandboxValue) {
return envMethods.pw.set(key, value, options)
}
),
envUnset: defineSandboxFn(
ctx,
"envUnset",
function (key: any, options: any) {
function (key: SandboxValue, options: SandboxValue) {
return envMethods.pw.unset(key, options)
}
),
envResolve: defineSandboxFn(ctx, "envResolve", function (key: any) {
return envMethods.pw.resolve(key)
}),
envResolve: defineSandboxFn(
ctx,
"envResolve",
function (key: SandboxValue) {
return envMethods.pw.resolve(key)
}
),
// Request variable operations
getRequestVariable: defineSandboxFn(
ctx,
"getRequestVariable",
function (key: any) {
function (key: SandboxValue) {
const reqVarEntry = requestProps.requestVariables.find(
(reqVar: any) => reqVar.key === key
(reqVar: SandboxValue) => reqVar.key === key
)
return reqVarEntry ? reqVarEntry.value : null
}

View file

@ -6,10 +6,12 @@ import {
defineSandboxObject,
} from "faraday-cage/modules"
import { getStatusReason } from "~/constants/http-status-codes"
import { TestDescriptor, TestResponse, TestResult } from "~/types"
import postRequestBootstrapCode from "../bootstrap-code/post-request?raw"
import preRequestBootstrapCode from "../bootstrap-code/pre-request?raw"
import { createBaseInputs } from "./utils/base-inputs"
import { createChaiMethods } from "./utils/chai-helpers"
import { createExpectationMethods } from "./utils/expectation-helpers"
import { createRequestSetterMethods } from "./utils/request-setters"
@ -125,20 +127,21 @@ const createScriptingInputsObj = (
type: ModuleType,
config: ModuleConfig
) => {
// Create base inputs shared across all namespaces
const baseInputs = createBaseInputs(ctx, {
envs: config.envs,
request: config.request,
cookies: config.cookies,
})
if (type === "pre") {
const preConfig = config as PreRequestModuleConfig
// Create request setter methods for pre-request scripts
// Create request setter methods FIRST for pre-request scripts
const { methods: requestSetterMethods, getUpdatedRequest } =
createRequestSetterMethods(ctx, preConfig.request)
// Create base inputs with access to updated request
const baseInputs = createBaseInputs(ctx, {
envs: config.envs,
request: config.request,
cookies: config.cookies,
getUpdatedRequest, // Pass the updater function for pre-request
})
// Register hook with helper function
registerAfterScriptExecutionHook(ctx, "pre", preConfig, baseInputs, {
getUpdatedRequest,
@ -150,6 +153,13 @@ const createScriptingInputsObj = (
}
}
// Create base inputs shared across all namespaces (post-request path)
const baseInputs = createBaseInputs(ctx, {
envs: config.envs,
request: config.request,
cookies: config.cookies,
})
if (type === "post") {
const postConfig = config as PostRequestModuleConfig
@ -159,20 +169,24 @@ const createScriptingInputsObj = (
postConfig.testRunStack
)
// Create Chai methods
const chaiMethods = createChaiMethods(ctx, postConfig.testRunStack)
// Register hook with helper function
registerAfterScriptExecutionHook(ctx, "post", postConfig, baseInputs)
return {
...baseInputs,
...expectationMethods,
...chaiMethods,
// Test management methods
preTest: defineSandboxFn(
ctx,
"preTest",
function preTest(descriptor: any) {
function preTest(descriptor: unknown) {
postConfig.testRunStack.push({
descriptor,
descriptor: descriptor as string,
expectResults: [],
children: [],
})
@ -187,6 +201,90 @@ const createScriptingInputsObj = (
getResponse: defineSandboxFn(ctx, "getResponse", function getResponse() {
return postConfig.response
}),
// Response utility methods as cage functions
responseReason: defineSandboxFn(
ctx,
"responseReason",
function responseReason() {
return getStatusReason(postConfig.response.status)
}
),
responseDataURI: defineSandboxFn(
ctx,
"responseDataURI",
function responseDataURI() {
try {
const body = postConfig.response.body
const contentType =
postConfig.response.headers.find(
(h) => h.key.toLowerCase() === "content-type"
)?.value || "application/octet-stream"
// Convert body to base64 (browser and Node.js compatible)
let base64Body: string
const bodyString = typeof body === "string" ? body : String(body)
// Check if we're in a browser environment (btoa available)
if (typeof btoa !== "undefined") {
// Browser environment: use btoa
// btoa requires binary string, so we need to handle UTF-8 properly
const utf8Bytes = new TextEncoder().encode(bodyString)
const binaryString = Array.from(utf8Bytes, (byte) =>
String.fromCharCode(byte)
).join("")
base64Body = btoa(binaryString)
} else if (typeof Buffer !== "undefined") {
// Node.js environment: use Buffer
base64Body = Buffer.from(bodyString).toString("base64")
} else {
throw new Error("No base64 encoding method available")
}
return `data:${contentType};base64,${base64Body}`
} catch (error) {
throw new Error(`Failed to convert response to data URI: ${error}`)
}
}
),
responseJsonp: defineSandboxFn(
ctx,
"responseJsonp",
function responseJsonp(...args: unknown[]) {
const callbackName = args[0]
const body = postConfig.response.body
const text = typeof body === "string" ? body : String(body)
if (callbackName && typeof callbackName === "string") {
// Escape special regex characters in callback name
const escapedName = callbackName.replace(
/[.*+?^${}()|[\]\\]/g,
"\\$&"
)
const regex = new RegExp(
`^\\s*${escapedName}\\s*\\(([\\s\\S]*)\\)\\s*;?\\s*$`
)
const match = text.match(regex)
if (match && match[1]) {
return JSON.parse(match[1])
}
}
// Auto-detect callback wrapper
const autoDetect = text.match(
/^\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(([\s\S]*)\)\s*;?\s*$/
)
if (autoDetect && autoDetect[2]) {
try {
return JSON.parse(autoDetect[2])
} catch {
// If parsing fails, fall through to plain JSON
}
}
// No JSONP wrapper found, parse as plain JSON
return JSON.parse(text)
}
),
}
}

View file

@ -1,12 +1,13 @@
import { Cookie, HoppRESTRequest } from "@hoppscotch/data"
import { CageModuleCtx, defineSandboxFn } from "faraday-cage/modules"
import { TestResult, BaseInputs } from "~/types"
import { TestResult, BaseInputs, SandboxValue } from "~/types"
import {
getSharedCookieMethods,
getSharedEnvMethods,
getSharedRequestProps,
} from "~/utils/shared"
import { UNDEFINED_MARKER, NULL_MARKER } from "~/constants/sandbox-markers"
import { createHoppNamespaceMethods } from "../namespaces/hopp-namespace"
import { createPmNamespaceMethods } from "../namespaces/pm-namespace"
import { createPwNamespaceMethods } from "../namespaces/pw-namespace"
@ -15,6 +16,7 @@ type BaseInputsConfig = {
envs: TestResult["envs"]
request: HoppRESTRequest
cookies: Cookie[] | null
getUpdatedRequest?: () => HoppRESTRequest
}
/**
@ -25,56 +27,146 @@ export const createBaseInputs = (
config: BaseInputsConfig
): BaseInputs => {
// Get environment methods - Applicable to both hopp and pw namespaces
const { methods: envMethods, updatedEnvs } = getSharedEnvMethods(
config.envs,
true
)
const {
methods: envMethods,
pmSetAny,
updatedEnvs,
} = getSharedEnvMethods(config.envs, true)
const { methods: cookieMethods, getUpdatedCookies } = getSharedCookieMethods(
config.cookies
)
// Get request properties - shared across pre and post request contexts
const requestProps = getSharedRequestProps(config.request)
// For pre-request, use the updater function to read from mutated request
const requestProps = getSharedRequestProps(
config.request,
config.getUpdatedRequest
)
// Cookie accessors
const cookieProps = {
cookieGet: defineSandboxFn(ctx, "cookieGet", (domain: any, name: any) => {
return cookieMethods.get(domain, name) || null
}),
cookieSet: defineSandboxFn(ctx, "cookieSet", (domain: any, cookie: any) => {
return cookieMethods.set(domain, cookie)
}),
cookieHas: defineSandboxFn(ctx, "cookieHas", (domain: any, name: any) => {
return cookieMethods.has(domain, name)
}),
cookieGetAll: defineSandboxFn(ctx, "cookieGetAll", (domain: any) => {
return cookieMethods.getAll(domain)
}),
cookieGet: defineSandboxFn(
ctx,
"cookieGet",
(domain: SandboxValue, name: SandboxValue) => {
return cookieMethods.get(domain, name) || null
}
),
cookieSet: defineSandboxFn(
ctx,
"cookieSet",
(domain: SandboxValue, cookie: SandboxValue) => {
return cookieMethods.set(domain, cookie)
}
),
cookieHas: defineSandboxFn(
ctx,
"cookieHas",
(domain: SandboxValue, name: SandboxValue) => {
return cookieMethods.has(domain, name)
}
),
cookieGetAll: defineSandboxFn(
ctx,
"cookieGetAll",
(domain: SandboxValue) => {
return cookieMethods.getAll(domain)
}
),
cookieDelete: defineSandboxFn(
ctx,
"cookieDelete",
(domain: any, name: any) => {
(domain: SandboxValue, name: SandboxValue) => {
return cookieMethods.delete(domain, name)
}
),
cookieClear: defineSandboxFn(ctx, "cookieClear", (domain: any) => {
cookieClear: defineSandboxFn(ctx, "cookieClear", (domain: SandboxValue) => {
return cookieMethods.clear(domain)
}),
}
// Environment accessors for toObject() support
const envAccessors = {
getAllSelectedEnvs: defineSandboxFn(ctx, "getAllSelectedEnvs", () => {
return updatedEnvs.selected || []
}),
getAllGlobalEnvs: defineSandboxFn(ctx, "getAllGlobalEnvs", () => {
return updatedEnvs.global || []
}),
}
// Combine all namespace methods
const pwMethods = createPwNamespaceMethods(ctx, envMethods, requestProps)
const hoppMethods = createHoppNamespaceMethods(ctx, envMethods, requestProps)
const pmMethods = createPmNamespaceMethods(ctx, config)
// PM namespace-specific setter that accepts any type (for type preservation)
const pmEnvSetAny = defineSandboxFn(
ctx,
"pmEnvSetAny",
function (key: SandboxValue, value: SandboxValue, options: SandboxValue) {
return pmSetAny(key, value, options)
}
)
return {
...pwMethods,
...hoppMethods,
...pmMethods,
...cookieProps,
...envAccessors,
// PM-specific env setter (preserves all types)
pmEnvSetAny,
// Expose the updated state accessors
getUpdatedEnvs: () => updatedEnvs,
getUpdatedEnvs: () => {
// Convert markers back to strings for UI display
// (using centralized markers from constants/sandbox-markers.ts)
// Handle case where envs is not provided
if (!updatedEnvs) {
return { global: [], selected: [] }
}
const convertMarkersToStrings = (env: SandboxValue) => {
const convertValue = (value: SandboxValue) => {
// Convert markers to string representations
if (value === UNDEFINED_MARKER) return "undefined"
if (value === NULL_MARKER) return "null"
// Convert complex types (arrays, objects) to JSON strings for UI display
// This prevents Vue UI from calling .match() on non-string values
if (typeof value === "object" && value !== null) {
try {
return JSON.stringify(value)
} catch (_) {
// If JSON.stringify fails (circular refs, etc.), return string representation
return String(value)
}
}
// Convert all non-string primitives to strings for UI compatibility
// Vue UI calls .match() on values, which only works on strings
if (typeof value !== "string") {
return String(value)
}
// Return strings as-is
return value
}
return {
...env,
currentValue: convertValue(env.currentValue),
initialValue: convertValue(env.initialValue),
}
}
return {
global: (updatedEnvs.global || []).map(convertMarkersToStrings),
selected: (updatedEnvs.selected || []).map(convertMarkersToStrings),
}
},
getUpdatedCookies,
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
import { CageModuleCtx, defineSandboxFn } from "faraday-cage/modules"
import { TestDescriptor, ExpectationMethods } from "~/types"
import { TestDescriptor, ExpectationMethods, SandboxValue } from "~/types"
import { createExpectation } from "~/utils/shared"
/**
@ -10,49 +10,53 @@ export const createExpectationMethods = (
ctx: CageModuleCtx,
testRunStack: TestDescriptor[]
): ExpectationMethods => {
const createExpect = (expectVal: any) =>
const createExpect = (expectVal: SandboxValue) =>
createExpectation(expectVal, false, testRunStack)
return {
expectToBe: defineSandboxFn(
ctx,
"expectToBe",
(expectVal: any, expectedVal: any) => {
(expectVal: SandboxValue, expectedVal: SandboxValue) => {
return createExpect(expectVal).toBe(expectedVal)
}
),
expectToBeLevel2xx: defineSandboxFn(
ctx,
"expectToBeLevel2xx",
(expectVal: any) => {
(expectVal: SandboxValue) => {
return createExpect(expectVal).toBeLevel2xx()
}
),
expectToBeLevel3xx: defineSandboxFn(
ctx,
"expectToBeLevel3xx",
(expectVal: any) => {
(expectVal: SandboxValue) => {
return createExpect(expectVal).toBeLevel3xx()
}
),
expectToBeLevel4xx: defineSandboxFn(
ctx,
"expectToBeLevel4xx",
(expectVal: any) => {
(expectVal: SandboxValue) => {
return createExpect(expectVal).toBeLevel4xx()
}
),
expectToBeLevel5xx: defineSandboxFn(
ctx,
"expectToBeLevel5xx",
(expectVal: any) => {
(expectVal: SandboxValue) => {
return createExpect(expectVal).toBeLevel5xx()
}
),
expectToBeType: defineSandboxFn(
ctx,
"expectToBeType",
(expectVal: any, expectedType: any, isDate: any) => {
(
expectVal: SandboxValue,
expectedType: SandboxValue,
isDate: SandboxValue
) => {
const resolved =
isDate && typeof expectVal === "string"
? new Date(expectVal)
@ -65,14 +69,14 @@ export const createExpectationMethods = (
expectToHaveLength: defineSandboxFn(
ctx,
"expectToHaveLength",
(expectVal: any, expectedLength: any) => {
(expectVal: SandboxValue, expectedLength: SandboxValue) => {
return createExpect(expectVal).toHaveLength(expectedLength)
}
),
expectToInclude: defineSandboxFn(
ctx,
"expectToInclude",
(expectVal: any, needle: any) => {
(expectVal: SandboxValue, needle: SandboxValue) => {
return createExpect(expectVal).toInclude(needle)
}
),
@ -81,42 +85,46 @@ export const createExpectationMethods = (
expectNotToBe: defineSandboxFn(
ctx,
"expectNotToBe",
(expectVal: any, expectedVal: any) => {
(expectVal: SandboxValue, expectedVal: SandboxValue) => {
return createExpect(expectVal).not.toBe(expectedVal)
}
),
expectNotToBeLevel2xx: defineSandboxFn(
ctx,
"expectNotToBeLevel2xx",
(expectVal: any) => {
(expectVal: SandboxValue) => {
return createExpect(expectVal).not.toBeLevel2xx()
}
),
expectNotToBeLevel3xx: defineSandboxFn(
ctx,
"expectNotToBeLevel3xx",
(expectVal: any) => {
(expectVal: SandboxValue) => {
return createExpect(expectVal).not.toBeLevel3xx()
}
),
expectNotToBeLevel4xx: defineSandboxFn(
ctx,
"expectNotToBeLevel4xx",
(expectVal: any) => {
(expectVal: SandboxValue) => {
return createExpect(expectVal).not.toBeLevel4xx()
}
),
expectNotToBeLevel5xx: defineSandboxFn(
ctx,
"expectNotToBeLevel5xx",
(expectVal: any) => {
(expectVal: SandboxValue) => {
return createExpect(expectVal).not.toBeLevel5xx()
}
),
expectNotToBeType: defineSandboxFn(
ctx,
"expectNotToBeType",
(expectVal: any, expectedType: any, isDate: any) => {
(
expectVal: SandboxValue,
expectedType: SandboxValue,
isDate: SandboxValue
) => {
const resolved =
isDate && typeof expectVal === "string"
? new Date(expectVal)
@ -129,14 +137,14 @@ export const createExpectationMethods = (
expectNotToHaveLength: defineSandboxFn(
ctx,
"expectNotToHaveLength",
(expectVal: any, expectedLength: any) => {
(expectVal: SandboxValue, expectedLength: SandboxValue) => {
return createExpect(expectVal).not.toHaveLength(expectedLength)
}
),
expectNotToInclude: defineSandboxFn(
ctx,
"expectNotToInclude",
(expectVal: any, needle: any) => {
(expectVal: SandboxValue, needle: SandboxValue) => {
return createExpect(expectVal).not.toInclude(needle)
}
),

View file

@ -23,68 +23,68 @@ export const createRequestSetterMethods = (
const setterMethods = {
// Request setter methods
setRequestUrl: defineSandboxFn(ctx, "setRequestUrl", (url: any) => {
setRequestUrl: defineSandboxFn(ctx, "setRequestUrl", (url: unknown) => {
requestMethods.setUrl(url as string)
}),
setRequestMethod: defineSandboxFn(
ctx,
"setRequestMethod",
(method: any) => {
(method: unknown) => {
requestMethods.setMethod(method as string)
}
),
setRequestHeader: defineSandboxFn(
ctx,
"setRequestHeader",
(name: any, value: any) => {
(name: unknown, value: unknown) => {
requestMethods.setHeader(name as string, value as string)
}
),
setRequestHeaders: defineSandboxFn(
ctx,
"setRequestHeaders",
(headers: any) => {
(headers: unknown) => {
requestMethods.setHeaders(headers as HoppRESTHeaders)
}
),
removeRequestHeader: defineSandboxFn(
ctx,
"removeRequestHeader",
(key: any) => {
(key: unknown) => {
requestMethods.removeHeader(key as string)
}
),
setRequestParam: defineSandboxFn(
ctx,
"setRequestParam",
(name: any, value: any) => {
(name: unknown, value: unknown) => {
requestMethods.setParam(name as string, value as string)
}
),
setRequestParams: defineSandboxFn(
ctx,
"setRequestParams",
(params: any) => {
(params: unknown) => {
requestMethods.setParams(params as HoppRESTParams)
}
),
removeRequestParam: defineSandboxFn(
ctx,
"removeRequestParam",
(key: any) => {
(key: unknown) => {
requestMethods.removeParam(key as string)
}
),
setRequestBody: defineSandboxFn(ctx, "setRequestBody", (body: any) => {
setRequestBody: defineSandboxFn(ctx, "setRequestBody", (body: unknown) => {
requestMethods.setBody(body as HoppRESTReqBody)
}),
setRequestAuth: defineSandboxFn(ctx, "setRequestAuth", (auth: any) => {
setRequestAuth: defineSandboxFn(ctx, "setRequestAuth", (auth: unknown) => {
requestMethods.setAuth(auth as HoppRESTAuth)
}),
setRequestVariable: defineSandboxFn(
ctx,
"setRequestVariable",
(key: any, value: any) => {
(key: unknown, value: unknown) => {
requestMethods.setRequestVariable(key as string, value as string)
}
),

View file

@ -0,0 +1,126 @@
/**
* HTTP Status Code Reason Phrases
*
* Standard HTTP status codes and their corresponding reason phrases
* as defined in RFC 7231 and related specifications.
*
* @see https://tools.ietf.org/html/rfc7231#section-6
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
*/
export const HTTP_STATUS_REASONS: Readonly<Record<number, string>> = {
// 1xx Informational
100: "Continue",
101: "Switching Protocols",
102: "Processing",
103: "Early Hints",
// 2xx Success
200: "OK",
201: "Created",
202: "Accepted",
203: "Non-Authoritative Information",
204: "No Content",
205: "Reset Content",
206: "Partial Content",
207: "Multi-Status",
208: "Already Reported",
226: "IM Used",
// 3xx Redirection
300: "Multiple Choices",
301: "Moved Permanently",
302: "Found",
303: "See Other",
304: "Not Modified",
305: "Use Proxy",
307: "Temporary Redirect",
308: "Permanent Redirect",
// 4xx Client Error
400: "Bad Request",
401: "Unauthorized",
402: "Payment Required",
403: "Forbidden",
404: "Not Found",
405: "Method Not Allowed",
406: "Not Acceptable",
407: "Proxy Authentication Required",
408: "Request Timeout",
409: "Conflict",
410: "Gone",
411: "Length Required",
412: "Precondition Failed",
413: "Payload Too Large",
414: "URI Too Long",
415: "Unsupported Media Type",
416: "Range Not Satisfiable",
417: "Expectation Failed",
418: "I'm a teapot",
421: "Misdirected Request",
422: "Unprocessable Entity",
423: "Locked",
424: "Failed Dependency",
425: "Too Early",
426: "Upgrade Required",
428: "Precondition Required",
429: "Too Many Requests",
431: "Request Header Fields Too Large",
451: "Unavailable For Legal Reasons",
// 5xx Server Error
500: "Internal Server Error",
501: "Not Implemented",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Gateway Timeout",
505: "HTTP Version Not Supported",
506: "Variant Also Negotiates",
507: "Insufficient Storage",
508: "Loop Detected",
510: "Not Extended",
511: "Network Authentication Required",
} as const
/**
* Get the reason phrase for an HTTP status code
* @param statusCode - The HTTP status code
* @returns The reason phrase, or "Unknown" if not found
*/
export const getStatusReason = (statusCode: number): string => {
return HTTP_STATUS_REASONS[statusCode] || "Unknown"
}
/**
* Check if a status code is informational (1xx)
*/
export const isInformational = (statusCode: number): boolean => {
return statusCode >= 100 && statusCode < 200
}
/**
* Check if a status code is successful (2xx)
*/
export const isSuccess = (statusCode: number): boolean => {
return statusCode >= 200 && statusCode < 300
}
/**
* Check if a status code is a redirection (3xx)
*/
export const isRedirection = (statusCode: number): boolean => {
return statusCode >= 300 && statusCode < 400
}
/**
* Check if a status code is a client error (4xx)
*/
export const isClientError = (statusCode: number): boolean => {
return statusCode >= 400 && statusCode < 500
}
/**
* Check if a status code is a server error (5xx)
*/
export const isServerError = (statusCode: number): boolean => {
return statusCode >= 500 && statusCode < 600
}

View file

@ -0,0 +1,31 @@
/**
* Special marker constants for preserving undefined and null values
* across the sandbox boundary during serialization.
*
* These markers are used because:
* - JSON.stringify converts undefined to null or omits the property
* - We need to distinguish between actual null and serialized undefined
* - The sandbox boundary requires serialization, losing type information
*/
export const UNDEFINED_MARKER = "__HOPPSCOTCH_UNDEFINED__" as const
export const NULL_MARKER = "__HOPPSCOTCH_NULL__" as const
export type SandboxMarker = typeof UNDEFINED_MARKER | typeof NULL_MARKER
/**
* Converts marker strings back to their original values
*/
export const convertMarkerToValue = (value: unknown): unknown => {
if (value === UNDEFINED_MARKER) return undefined
if (value === NULL_MARKER) return null
return value
}
/**
* Converts null/undefined values to marker strings for serialization
*/
export const convertValueToMarker = (value: unknown): unknown => {
if (value === undefined) return UNDEFINED_MARKER
if (value === null) return NULL_MARKER
return value
}

View file

@ -6,6 +6,34 @@ import type { EnvAPIOptions } from "~/utils/shared"
// Infer the return type of defineSandboxFn from faraday-cage
type SandboxFunction = ReturnType<typeof defineSandboxFn>
/**
* Type alias for values that cross the QuickJS sandbox boundary.
*
* Values passed between the host environment and the QuickJS sandbox lose their
* TypeScript type information during serialization. This type alias serves as
* a documented alternative to raw `any`, making it explicit that these values:
*
* - Come from or go to the sandbox (pre-request/post-request scripts)
* - Have been serialized and may not preserve complex types
* - Require runtime validation when type safety is needed
*
* Use this type for:
* - Function parameters that accept user script values
* - Return values sent back to the sandbox
* - PM namespace compatibility (preserves non-string types like arrays, objects)
*
* @example
* ```typescript
* // Function accepting values from user scripts
* const envSetAny = (key: SandboxValue, value: SandboxValue) => {
* // Runtime validation
* if (typeof key !== "string") throw new Error("Expected string key")
* // ... handle value
* }
* ```
*/
export type SandboxValue = any
/**
* The response object structure exposed to the test script
*/
@ -93,14 +121,14 @@ export type SandboxPreRequestResult = {
}
export interface Expectation {
toBe(expectedVal: any): void
toBe(expectedVal: SandboxValue): void
toBeLevel2xx(): void
toBeLevel3xx(): void
toBeLevel4xx(): void
toBeLevel5xx(): void
toBeType(expectedType: any): void
toHaveLength(expectedLength: any): void
toInclude(needle: any): void
toBeType(expectedType: SandboxValue): void
toHaveLength(expectedLength: SandboxValue): void
toInclude(needle: SandboxValue): void
readonly not: Expectation
}
@ -252,7 +280,7 @@ export interface BaseInputs
cookieGetAll: SandboxFunction
cookieDelete: SandboxFunction
cookieClear: SandboxFunction
getUpdatedEnvs: () => any
getUpdatedEnvs: () => SandboxValue
getUpdatedCookies: () => Cookie[] | null
[key: string]: any
[key: string]: SandboxValue // Index signature for dynamic namespace properties
}

View file

@ -0,0 +1,354 @@
import * as chai from "chai"
import { TestDescriptor, SandboxValue } from "../types"
/**
* Creates a Chai expectation that records results to the test stack
* This integrates actual Chai.js with Hoppscotch's test reporting system
*
* Returns a serializable proxy object that can cross the sandbox boundary
*/
export function createChaiExpectation(
value: SandboxValue,
testStack: TestDescriptor[]
) {
// Create the actual Chai assertion
const assertion = chai.expect(value)
// Create a serializable proxy that can cross the sandbox boundary
return createSerializableProxy(assertion, value, testStack, {})
}
/**
* Creates a serializable proxy object that mimics Chai's API
* This can cross the sandbox boundary unlike the actual Chai assertion object
*/
function createSerializableProxy(
assertion: any, // Chai assertion object - dynamic API, must be any
originalValue: SandboxValue,
testStack: TestDescriptor[],
flags: SandboxValue
): any {
// Returns dynamic proxy with Chai-like API
const proxy: any = {} // Dynamic proxy object with Chai-like methods
// Helper to create assertion methods
const createMethod = (methodName: string) => {
return (...args: SandboxValue[]) => {
try {
// Call the actual Chai method
const result = assertion[methodName](...args)
// Record success
recordResult(
testStack,
true,
buildMessage(assertion, methodName, args, originalValue, false)
)
// If result is a Chai assertion, return a new serializable proxy
if (result && typeof result === "object" && result.__flags) {
return createSerializableProxy(
result,
result._obj,
testStack,
result.__flags
)
}
return result
} catch (error: any) {
// Record failure but DON'T throw - allow test to continue
recordResult(testStack, false, extractErrorMessage(error))
// Return a proxy to allow chaining even after failure
return createSerializableProxy(
assertion,
originalValue,
testStack,
flags
)
}
}
}
// Helper to create assertion getter properties (these perform assertions when accessed)
const createAssertionGetter = (propName: string) => {
return () => {
try {
// Access the property which triggers the assertion
void assertion[propName]
// Record success
recordResult(
testStack,
true,
buildMessage(assertion, propName, [], originalValue, false)
)
// Return undefined (assertion getters don't return values)
return undefined
} catch (error: any) {
// Record failure but DON'T throw - allow test to continue
recordResult(testStack, false, extractErrorMessage(error))
// Return undefined to allow test to continue
return undefined
}
}
}
// Helper to create language chain getters (these just return new assertions)
const createChainGetter = (propName: string) => {
return () => {
// Access the property on the Chai assertion
const value = assertion[propName]
// Return a new serializable proxy
if (value && typeof value === "object" && value.__flags) {
return createSerializableProxy(
value,
value._obj || originalValue,
testStack,
value.__flags
)
}
return value
}
}
// Add all Chai assertion methods (functions)
const methods = [
"equal",
"equals",
"eq",
"eql",
"include",
"includes",
"contain",
"contains",
"a",
"an",
"instanceof",
"instanceOf",
"property",
"ownProperty",
"ownPropertyDescriptor",
"lengthOf",
"length",
"match",
"matches",
"string",
"keys",
"key",
"throw",
"throws",
"Throw",
"respondTo",
"respondsTo",
"satisfy",
"satisfies",
"closeTo",
"approximately",
"members",
"oneOf",
"change",
"changes",
"increase",
"increases",
"decrease",
"decreases",
"by",
"above",
"gt",
"greaterThan",
"least",
"gte",
"below",
"lt",
"lessThan",
"most",
"lte",
"within",
]
// Add all methods to the proxy
methods.forEach((method) => {
proxy[method] = createMethod(method)
})
// Add assertion getters (these perform assertions when accessed)
const assertionGetters = [
"ok",
"true",
"false",
"null",
"undefined",
"NaN",
"exist",
"empty",
"arguments",
"Arguments",
"finite",
"extensible",
"sealed",
"frozen",
]
assertionGetters.forEach((getter) => {
Object.defineProperty(proxy, getter, {
get: createAssertionGetter(getter),
enumerable: false, // Don't enumerate to avoid serialization issues
configurable: true,
})
})
// Add language chains as getters (these just return new assertions)
const chains = [
"to",
"be",
"been",
"is",
"that",
"which",
"and",
"has",
"have",
"with",
"at",
"of",
"same",
"but",
"does",
"not",
"deep",
"nested",
"own",
"ordered",
"any",
"all",
"itself",
]
chains.forEach((chain) => {
Object.defineProperty(proxy, chain, {
get: createChainGetter(chain),
enumerable: false, // Don't enumerate to avoid serialization issues
configurable: true,
})
})
return proxy
}
/**
* Records an assertion result to the test stack
*/
function recordResult(
testStack: TestDescriptor[],
passed: boolean,
message: string
) {
if (testStack.length === 0) return
const currentTest = testStack[testStack.length - 1]
currentTest.expectResults.push({
status: passed ? "pass" : "fail",
message,
})
}
/**
* Builds a message for an assertion
* Tries to match the format expected by tests
*/
function buildMessage(
assertion: any, // Chai assertion object - dynamic API, must be any
method: string,
args: SandboxValue[],
value: SandboxValue,
_failed: boolean
): string {
const flags = assertion.__flags || {}
const valueStr = formatValue(value)
let message = `Expected ${valueStr}`
// Add "to" or "to not"
if (flags.negate) {
message += " to not"
} else {
message += " to"
}
// Add modifiers
if (flags.deep) message += " deep"
if (flags.own) message += " own"
if (flags.nested) message += " nested"
// Add the method name
message += ` ${method}`
// Add arguments
if (args.length > 0) {
const argStrs = args.map(formatValue)
message += ` ${argStrs.join(", ")}`
}
return message
}
/**
* Extracts a clean error message from a Chai assertion error
*/
function extractErrorMessage(error: any): string {
if (!error) return "Assertion failed"
// Chai errors have a message property
let message = error.message || String(error)
// Remove stack traces and extra info
const lines = message.split("\n")
if (lines.length > 0) {
message = lines[0]
}
// Clean up Chai's "expected X to Y" format
// Chai uses lowercase "expected", we want "Expected"
if (message.startsWith("expected ")) {
message = "E" + message.substring(1)
}
return message
}
/**
* Formats a value for display in messages
*/
function formatValue(val: SandboxValue): string {
if (val === null) return "null"
if (val === undefined) return "undefined"
if (typeof val === "string") return `'${val}'`
if (typeof val === "number") {
if (isNaN(val)) return "NaN"
if (val === Infinity) return "Infinity"
if (val === -Infinity) return "-Infinity"
return String(val)
}
if (typeof val === "boolean") return String(val)
if (Array.isArray(val)) {
if (val.length === 0) return "[]"
const items = val.slice(0, 10).map(formatValue)
return `[${items.join(", ")}]`
}
if (typeof val === "object") {
try {
const keys = Object.keys(val)
if (keys.length === 0) return "{}"
const pairs = keys.slice(0, 5).map((k) => `${k}: ${formatValue(val[k])}`)
return `{${pairs.join(", ")}}`
} catch {
return "[object Object]"
}
}
if (typeof val === "function") {
return val.name || "[Function]"
}
return String(val)
}

View file

@ -17,7 +17,8 @@ export const getRequestSetterMethods = (request: HoppRESTRequest) => {
}
const setMethod = (method: string) => {
updatedRequest.method = method.toUpperCase()
// NOTE: Postman does NOT normalize method to uppercase, so we preserve the original case
updatedRequest.method = method
}
const setHeader = (name: string, value: string) => {
const headers = [...updatedRequest.headers]
@ -45,7 +46,9 @@ export const getRequestSetterMethods = (request: HoppRESTRequest) => {
}
const removeHeader = (key: string) => {
updatedRequest.headers = updatedRequest.headers.filter((h) => h.key !== key)
updatedRequest.headers = updatedRequest.headers.filter(
(h) => h.key.toLowerCase() !== key.toLowerCase()
)
}
const setParam = (name: string, value: string) => {

View file

@ -15,6 +15,7 @@ import {
SelectedEnvItem,
TestDescriptor,
TestResult,
SandboxValue,
} from "../types"
export type EnvSource = "active" | "global" | "all"
@ -57,7 +58,7 @@ const findEnvIndex = (
const setEnv = (
envName: string,
envValue: string,
envValue: SandboxValue,
envs: TestResult["envs"],
options: { setInitialValue?: boolean; source: EnvSource } = {
setInitialValue: false,
@ -154,6 +155,7 @@ export function getSharedEnvMethods(
setInitial: (key: string, value: string, options?: EnvAPIOptions) => void
}
}
pmSetAny: (key: string, value: SandboxValue, options?: EnvAPIOptions) => void
updatedEnvs: TestResult["envs"]
}
@ -187,7 +189,7 @@ export function getSharedEnvMethods(
let updatedEnvs = envs
const envGetFn = (
key: any,
key: unknown,
options: EnvAPIOptions = { fallbackToNull: false, source: "all" }
) => {
if (typeof key !== "string") {
@ -198,7 +200,7 @@ export function getSharedEnvMethods(
getEnv(key, updatedEnvs, options),
O.fold(
() => (options.fallbackToNull ? null : undefined),
(env) => String(env.currentValue)
(env) => env.currentValue // Return value as-is (PM namespace preserves types)
)
)
@ -206,7 +208,7 @@ export function getSharedEnvMethods(
}
const envGetResolveFn = (
key: any,
key: unknown,
options: EnvAPIOptions = { fallbackToNull: false, source: "all" }
) => {
if (typeof key !== "string") {
@ -225,13 +227,17 @@ export function getSharedEnvMethods(
getEnv(key, updatedEnvs, options),
E.fromOption(() => "INVALID_KEY" as const),
E.map((e) =>
pipe(
parseTemplateStringE(e.currentValue, envVars), // If the recursive resolution failed, return the unresolved value
E.getOrElse(() => e.currentValue)
)
),
E.map((x) => String(x)),
E.map((e) => {
// Only resolve templates if the value is a string (PM namespace may have non-strings)
if (typeof e.currentValue === "string") {
return pipe(
parseTemplateStringE(e.currentValue, envVars),
E.getOrElse(() => e.currentValue)
)
}
// Return non-string values as-is (arrays, objects, null, etc.)
return e.currentValue
}),
E.getOrElseW(() => (options.fallbackToNull ? null : undefined))
)
@ -240,8 +246,8 @@ export function getSharedEnvMethods(
}
const envSetFn = (
key: any,
value: any,
key: unknown,
value: unknown,
options: EnvAPIOptions = { source: "all" }
) => {
if (typeof key !== "string") {
@ -257,7 +263,26 @@ export function getSharedEnvMethods(
return undefined
}
const envUnsetFn = (key: any, options: EnvAPIOptions = { source: "all" }) => {
// PM namespace-specific setter that accepts any type (for Postman compatibility)
const envSetAnyFn = (
key: unknown,
value: SandboxValue, // Intentionally SandboxValue for PM namespace type preservation
options: EnvAPIOptions = { source: "all" }
) => {
if (typeof key !== "string") {
throw new Error("Expected key to be a string")
}
// PM namespace preserves ALL types (arrays, objects, primitives, null, undefined)
updatedEnvs = setEnv(key, value, updatedEnvs, options)
return undefined
}
const envUnsetFn = (
key: unknown,
options: EnvAPIOptions = { source: "all" }
) => {
if (typeof key !== "string") {
throw new Error("Expected key to be a string")
}
@ -267,7 +292,7 @@ export function getSharedEnvMethods(
return undefined
}
const envResolveFn = (value: any) => {
const envResolveFn = (value: unknown) => {
if (typeof value !== "string") {
throw new Error("Expected value to be a string")
}
@ -317,7 +342,7 @@ export function getSharedEnvMethods(
}
const envGetInitialRawFn = (
key: any,
key: unknown,
options: EnvAPIOptions = { source: "all" }
) => {
if (typeof key !== "string") {
@ -328,7 +353,7 @@ export function getSharedEnvMethods(
getEnv(key, updatedEnvs, options),
O.fold(
() => undefined,
(env) => String(env.initialValue)
(env) => env.initialValue // Return as-is (PM namespace preserves types)
)
)
@ -375,7 +400,8 @@ export function getSharedEnvMethods(
setInitial: envSetInitialFn,
},
},
// Expose PM-specific setter that accepts any type
pmSetAny: envSetAnyFn,
updatedEnvs,
}
}
@ -408,7 +434,7 @@ export const getSharedCookieMethods = (cookies: Cookie[] | null) => {
}
}
const cookieGetFn = (domain: any, name: any): Cookie | null => {
const cookieGetFn = (domain: unknown, name: unknown): Cookie | null => {
throwIfCookiesUnsupported()
if (typeof domain !== "string" || typeof name !== "string") {
@ -492,7 +518,7 @@ export const getSharedCookieMethods = (cookies: Cookie[] | null) => {
}
}
const getResolvedExpectValue = (expectVal: any) => {
const getResolvedExpectValue = (expectVal: SandboxValue) => {
if (typeof expectVal !== "string") {
return expectVal
}
@ -549,14 +575,14 @@ export function preventCyclicObjects<T extends object = Record<string, any>>(
* @returns Object with the expectation methods
*/
export const createExpectation = (
expectVal: any,
expectVal: SandboxValue,
negated: boolean,
currTestStack: TestDescriptor[]
): Expectation => {
// Non-primitive values supplied are stringified in the isolate context
const resolvedExpectVal = getResolvedExpectValue(expectVal)
const toBeFn = (expectedVal: any) => {
const toBeFn = (expectedVal: SandboxValue) => {
let assertion = resolvedExpectVal === expectedVal
if (negated) {
@ -616,7 +642,7 @@ export const createExpectation = (
const toBeLevel4xxFn = () => toBeLevelXxx("400", 400, 499)
const toBeLevel5xxFn = () => toBeLevelXxx("500", 500, 599)
const toBeTypeFn = (expectedType: any) => {
const toBeTypeFn = (expectedType: SandboxValue) => {
if (
[
"string",
@ -656,7 +682,7 @@ export const createExpectation = (
return undefined
}
const toHaveLengthFn = (expectedLength: any) => {
const toHaveLengthFn = (expectedLength: SandboxValue) => {
if (
!(
Array.isArray(resolvedExpectVal) ||
@ -700,7 +726,7 @@ export const createExpectation = (
return undefined
}
const toIncludeFn = (needle: any) => {
const toIncludeFn = (needle: SandboxValue) => {
if (
!(
Array.isArray(resolvedExpectVal) ||
@ -806,7 +832,7 @@ export const getTestRunnerScriptMethods = (envs: TestResult["envs"]) => {
testRunStack[testRunStack.length - 1].children.push(child)
}
const expectFn = (expectVal: any) =>
const expectFn = (expectVal: unknown) =>
createExpectation(expectVal, false, testRunStack)
const { methods, updatedEnvs } = getSharedEnvMethods(cloneDeep(envs))
@ -824,30 +850,42 @@ export const getTestRunnerScriptMethods = (envs: TestResult["envs"]) => {
* Compiles shared scripting API properties (scoped to requests) for use in both pre and post request scripts
* Extracts shared properties from a request object
* @param request The request object to extract shared properties from
* @param getUpdatedRequest Optional function to get the updated request (for pre-request mutations)
* @returns An object containing the shared properties of the request
*/
export const getSharedRequestProps = (request: HoppRESTRequest) => {
export const getSharedRequestProps = (
request: HoppRESTRequest,
getUpdatedRequest?: () => HoppRESTRequest
) => {
return {
get url() {
return request.endpoint
// For pre-request scripts, read from updated request to see mutations
const currentRequest = getUpdatedRequest ? getUpdatedRequest() : request
return currentRequest.endpoint
},
get method() {
return request.method
const currentRequest = getUpdatedRequest ? getUpdatedRequest() : request
return currentRequest.method
},
get params() {
return request.params
const currentRequest = getUpdatedRequest ? getUpdatedRequest() : request
return currentRequest.params
},
get headers() {
return request.headers
const currentRequest = getUpdatedRequest ? getUpdatedRequest() : request
return currentRequest.headers
},
get body() {
return request.body
const currentRequest = getUpdatedRequest ? getUpdatedRequest() : request
return currentRequest.body
},
get auth() {
return request.auth
const currentRequest = getUpdatedRequest ? getUpdatedRequest() : request
return currentRequest.auth
},
get requestVariables() {
return request.requestVariables
const currentRequest = getUpdatedRequest ? getUpdatedRequest() : request
return currentRequest.requestVariables
},
}
}

View file

@ -0,0 +1,192 @@
/**
* Consolidated test helpers for all namespace tests
*
* This file provides reusable helper functions to eliminate duplication
* across 45+ test files that previously had inline `func` definitions.
*/
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { runTestScript, runPreRequestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
// Default fixtures used across test files
export const defaultRequest = getDefaultRESTRequest()
export const fakeResponse: TestResponse = {
status: 200,
statusText: "OK",
responseTime: 0,
body: "hoi",
headers: [],
}
/**
* Run a test script and return the test results
*
* This is the most common pattern used across all test files.
* Replaces the inline `func` helper pattern.
*
* @param script - The test script to execute
* @param envs - Environment variables (defaults to empty)
* @param response - Response object (defaults to fakeResponse)
* @param request - Request object (defaults to defaultRequest)
* @returns TaskEither containing test results
*
* @example
* ```typescript
* test("pm.expect assertion", () => {
* return expect(
* runTest(`pm.test("test", () => pm.expect(1).to.equal(1))`, {
* global: [],
* selected: []
* })()
* ).resolves.toEqualRight([...])
* })
* ```
*/
export const runTest = (
script: string,
envs: TestResult["envs"],
response: TestResponse = fakeResponse,
request: ReturnType<typeof getDefaultRESTRequest> = defaultRequest
) =>
pipe(
runTestScript(script, {
envs,
request,
response,
}),
TE.map((x) => x.tests)
)
/**
* Run a pre-request script and return the environment variables
*
* Used for testing pre-request scripts that modify environment variables.
*
* @param script - The pre-request script to execute
* @param envs - Initial environment variables (defaults to empty)
* @param request - Request object (defaults to defaultRequest)
* @returns TaskEither containing environment variables
*
* @example
* ```typescript
* test("pm.environment.set in pre-request", () => {
* return expect(
* runPreRequest(
* `pm.environment.set("key", "value")`,
* { global: [], selected: [] }
* )()
* ).resolves.toEqualRight({
* global: [],
* selected: [{ key: "key", value: "value", secret: false }]
* })
* })
* ```
*/
export const runPreRequest = (
script: string,
envs: TestResult["envs"],
request: ReturnType<typeof getDefaultRESTRequest> = defaultRequest
) =>
pipe(
runPreRequestScript(script, {
envs,
request,
}),
TE.map((x) => x.updatedEnvs)
)
/**
* Run a test script with custom response
*
* Convenience wrapper when you only need to customize the response.
*
* @param script - The test script to execute
* @param response - Custom response object
* @param envs - Environment variables (defaults to empty)
* @returns TaskEither containing test results
*/
export const runTestWithResponse = (
script: string,
response: TestResponse,
envs: TestResult["envs"] = { global: [], selected: [] }
) => runTest(script, envs, response)
/**
* Run a test script with custom request
*
* Convenience wrapper when you only need to customize the request.
*
* @param script - The test script to execute
* @param request - Custom request object
* @param envs - Environment variables (defaults to empty)
* @param response - Response object (defaults to fakeResponse)
* @returns TaskEither containing test results
*/
export const runTestWithRequest = (
script: string,
request: ReturnType<typeof getDefaultRESTRequest>,
envs: TestResult["envs"] = { global: [], selected: [] },
response: TestResponse = fakeResponse
) => runTest(script, envs, response, request)
/**
* Run a test script with empty environments
*
* Convenience wrapper for the most common case (no environment variables).
*
* @param script - The test script to execute
* @param response - Response object (defaults to fakeResponse)
* @returns TaskEither containing test results
*/
export const runTestWithEmptyEnv = (
script: string,
response: TestResponse = fakeResponse
) => runTest(script, { global: [], selected: [] }, response)
/**
* Run a test script and return the environment variables (not test results)
*
* Used for testing scripts that modify environment variables but you want
* to inspect the final env state rather than test results.
*
* This is different from runPreRequest which uses runPreRequestScript.
* This uses runTestScript but extracts envs instead of tests.
*
* @param script - The test script to execute
* @param envs - Initial environment variables
* @param response - Response object (defaults to fakeResponse)
* @param request - Request object (defaults to defaultRequest)
* @returns TaskEither containing environment variables
*
* @example
* ```typescript
* test("env mutation in test script", () => {
* return expect(
* runTestAndGetEnvs(
* `pw.env.set("key", "value")`,
* { global: [], selected: [] }
* )()
* ).resolves.toEqualRight({
* global: [],
* selected: [{ key: "key", value: "value", secret: false }]
* })
* })
* ```
*/
export const runTestAndGetEnvs = (
script: string,
envs: TestResult["envs"],
response: TestResponse = fakeResponse,
request: ReturnType<typeof getDefaultRESTRequest> = defaultRequest
) =>
pipe(
runTestScript(script, {
envs,
request,
response,
}),
TE.map((x: TestResult) => x.envs)
)

View file

@ -1158,6 +1158,9 @@ importers:
'@types/lodash-es':
specifier: 4.17.12
version: 4.17.12
chai:
specifier: 6.2.0
version: 6.2.0
faraday-cage:
specifier: 0.1.0
version: 0.1.0
@ -1180,6 +1183,9 @@ importers:
'@relmify/jest-fp-ts':
specifier: 2.1.1
version: 2.1.1(fp-ts@2.16.11)(io-ts@2.2.22(fp-ts@2.16.11))
'@types/chai':
specifier: 5.2.2
version: 5.2.2
'@types/jest':
specifier: 30.0.0
version: 30.0.0
@ -8870,6 +8876,10 @@ packages:
resolution: {integrity: sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==}
engines: {node: '>=18'}
chai@6.2.0:
resolution: {integrity: sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA==}
engines: {node: '>=18'}
chalk@2.4.2:
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
engines: {node: '>=4'}
@ -24957,6 +24967,8 @@ snapshots:
loupe: 3.2.0
pathval: 2.0.1
chai@6.2.0: {}
chalk@2.4.2:
dependencies:
ansi-styles: 3.2.1