api-client/packages/hoppscotch-js-sandbox/src/bootstrap-code/pre-request.js
jamesgeorge007 98f07f8a4c test(js-sandbox): expand unsupported API coverage and sync error messages
Add comprehensive test coverage for unsupported Postman APIs and ensure
consistent error messages across pre-request and post-request contexts.

Test improvements:
- Expand coverage from 13 to 25 unsupported APIs (50 tests total)
- Add missing APIs: collectionVariables.set/unset/has/clear/toObject,
  vault.set/unset, iterationData.set/unset/has/toJSON
- Fix assertions to match actual error format with prefix
- Add pre-request context test for pm.execution.location

Implementation fixes:
- Add missing pm.iterationData.toJSON() in pre-request.js
- Sync post-request.js collectionVariables error messages to match
  pre-request.js ("use environment or request variables instead")
2025-11-12 14:35:16 +05:30

1368 lines
46 KiB
JavaScript

/* eslint-disable @typescript-eslint/no-unused-expressions */
;(inputs) => {
// Keep strict mode scoped to this IIFE to avoid leaking strictness to concatenated/bootstrapped code
"use strict"
globalThis.pw = {
env: {
get: (key) => convertMarkerToValue(inputs.envGet(key)),
getResolve: (key) => convertMarkerToValue(inputs.envGetResolve(key)),
set: (key, value) => inputs.envSet(key, value),
unset: (key) => inputs.envUnset(key),
resolve: (key) => inputs.envResolve(key),
},
}
const requestProps = {
// Setter methods
setUrl: (url) => inputs.setRequestUrl(url),
setMethod: (method) => inputs.setRequestMethod(method),
setHeader: (name, value) => inputs.setRequestHeader(name, value),
setHeaders: (headers) => inputs.setRequestHeaders(headers),
removeHeader: (key) => inputs.removeRequestHeader(key),
setParam: (name, value) => inputs.setRequestParam(name, value),
setParams: (params) => inputs.setRequestParams(params),
removeParam: (key) => inputs.removeRequestParam(key),
setBody: (body) => inputs.setRequestBody(body),
setAuth: (auth) => inputs.setRequestAuth(auth),
// Request variables
variables: {
get: (key) => inputs.getRequestVariable(key),
set: (key, value) => inputs.setRequestVariable(key, value),
},
}
// Define all properties with unified read-only protection
;["url", "method", "params", "headers", "body", "auth"].forEach((prop) => {
Object.defineProperty(requestProps, prop, {
enumerable: true,
configurable: false,
get() {
const currentValues = inputs.getRequestProps()
return currentValues[prop]
},
set(_value) {
throw new TypeError(`hopp.request.${prop} is read-only`)
},
})
})
// Freeze the entire requestProps object for additional protection
Object.freeze(requestProps)
// Special markers for undefined and null values to preserve them across sandbox boundary
// NOTE: These values MUST match constants/sandbox-markers.ts
// (Cannot import directly as this runs in QuickJS sandbox)
const UNDEFINED_MARKER = "__HOPPSCOTCH_UNDEFINED__"
const NULL_MARKER = "__HOPPSCOTCH_NULL__"
// Helper function to convert markers back to their original values
const convertMarkerToValue = (value) => {
if (value === UNDEFINED_MARKER) return undefined
if (value === NULL_MARKER) return null
return value
}
globalThis.hopp = {
env: {
get: (key) => {
return convertMarkerToValue(
inputs.envGetResolve(key, {
fallbackToNull: true,
source: "all",
})
)
},
getRaw: (key) => {
return convertMarkerToValue(
inputs.envGet(key, {
fallbackToNull: true,
source: "all",
})
)
},
set: (key, value) => inputs.envSet(key, value),
delete: (key) => inputs.envUnset(key),
reset: (key) => inputs.envReset(key),
getInitialRaw: (key) => {
return convertMarkerToValue(inputs.envGetInitialRaw(key))
},
setInitial: (key, value) => inputs.envSetInitial(key, value),
active: {
get: (key) => {
return convertMarkerToValue(
inputs.envGetResolve(key, {
fallbackToNull: true,
source: "active",
})
)
},
getRaw: (key) => {
return convertMarkerToValue(
inputs.envGet(key, {
fallbackToNull: true,
source: "active",
})
)
},
set: (key, value) => inputs.envSet(key, value, { source: "active" }),
delete: (key) => inputs.envUnset(key, { source: "active" }),
reset: (key) => inputs.envReset(key, { source: "active" }),
getInitialRaw: (key) => {
return convertMarkerToValue(
inputs.envGetInitialRaw(key, { source: "active" })
)
},
setInitial: (key, value) =>
inputs.envSetInitial(key, value, { source: "active" }),
},
global: {
get: (key) => {
return convertMarkerToValue(
inputs.envGetResolve(key, {
fallbackToNull: true,
source: "global",
})
)
},
getRaw: (key) => {
return convertMarkerToValue(
inputs.envGet(key, {
fallbackToNull: true,
source: "global",
})
)
},
set: (key, value) => inputs.envSet(key, value, { source: "global" }),
delete: (key) => inputs.envUnset(key, { source: "global" }),
reset: (key) => inputs.envReset(key, { source: "global" }),
getInitialRaw: (key) => {
return convertMarkerToValue(
inputs.envGetInitialRaw(key, { source: "global" })
)
},
setInitial: (key, value) =>
inputs.envSetInitial(key, value, { source: "global" }),
},
},
request: requestProps,
cookies: {
get: (domain, name) => inputs.cookieGet(domain, name),
set: (domain, cookie) => inputs.cookieSet(domain, cookie),
has: (domain, name) => inputs.cookieHas(domain, name),
getAll: (domain) => inputs.cookieGetAll(domain),
delete: (domain, name) => inputs.cookieDelete(domain, name),
clear: (domain) => inputs.cookieClear(domain),
},
}
// PM Namespace - Postman Compatibility Layer
globalThis.pm = {
environment: {
get: (key) => {
const value = globalThis.hopp.env.active.get(key)
// Postman returns undefined for missing keys, not null
return value === null ? undefined : value
},
set: (key, value) => {
// PM namespace preserves all types - use pmEnvSetAny directly
if (typeof value === "undefined") {
return inputs.pmEnvSetAny(key, UNDEFINED_MARKER, { source: "active" })
} else if (value === null) {
return inputs.pmEnvSetAny(key, NULL_MARKER, { source: "active" })
} else {
return inputs.pmEnvSetAny(key, value, { source: "active" })
}
},
unset: (key) => globalThis.hopp.env.active.delete(key),
has: (key) => globalThis.hopp.env.active.get(key) !== null,
clear: () => {
// Get all active environment variables and delete them
const envVars = inputs.getAllSelectedEnvs()
envVars.forEach((envVar) => {
globalThis.hopp.env.active.delete(envVar.key)
})
},
toObject: () => {
// Get all active environment variables as an object
const envVars = inputs.getAllSelectedEnvs()
const result = {}
envVars.forEach((envVar) => {
const value = globalThis.hopp.env.active.get(envVar.key)
if (value !== null) {
result[envVar.key] = value
}
})
return result
},
},
globals: {
get: (key) => {
const value = globalThis.hopp.env.global.get(key)
// Postman returns undefined for missing keys, not null
return value === null ? undefined : value
},
set: (key, value) => {
// PM namespace preserves all types - use pmEnvSetAny directly
if (typeof value === "undefined") {
return inputs.pmEnvSetAny(key, UNDEFINED_MARKER, { source: "global" })
} else if (value === null) {
return inputs.pmEnvSetAny(key, NULL_MARKER, { source: "global" })
} else {
return inputs.pmEnvSetAny(key, value, { source: "global" })
}
},
unset: (key) => globalThis.hopp.env.global.delete(key),
has: (key) => globalThis.hopp.env.global.get(key) !== null,
clear: () => {
// Get all global environment variables and delete them
const envVars = inputs.getAllGlobalEnvs()
envVars.forEach((envVar) => {
globalThis.hopp.env.global.delete(envVar.key)
})
},
toObject: () => {
// Get all global environment variables as an object
const envVars = inputs.getAllGlobalEnvs()
const result = {}
envVars.forEach((envVar) => {
const value = globalThis.hopp.env.global.get(envVar.key)
if (value !== null) {
result[envVar.key] = value
}
})
return result
},
},
variables: {
get: (key) => {
const value = globalThis.hopp.env.get(key)
// Postman returns undefined for missing keys, not null
return value === null ? undefined : value
},
set: (key, value) => {
// PM namespace preserves all types - use pmEnvSetAny directly
// variables.set uses active scope
if (typeof value === "undefined") {
return inputs.pmEnvSetAny(key, UNDEFINED_MARKER, { source: "active" })
} else if (value === null) {
return inputs.pmEnvSetAny(key, NULL_MARKER, { source: "active" })
} else {
return inputs.pmEnvSetAny(key, value, { source: "active" })
}
},
has: (key) => globalThis.hopp.env.get(key) !== null,
replaceIn: (template) => {
if (typeof template !== "string") return template
return template.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
const value = globalThis.hopp.env.get(key.trim())
return value !== null ? value : match
})
},
},
request: {
// ID and name (read-only, exposed from inputs)
get id() {
return inputs.pmInfoRequestId()
},
get name() {
return inputs.pmInfoRequestName()
},
// Certificate and proxy (read-only, not accessible in scripts)
// These are configured at app/collection level in Postman, not in scripts
get certificate() {
return undefined
},
get proxy() {
return undefined
},
// URL - Mutable with Postman-compatible structure
get url() {
// Return Postman-compatible URL object
const urlObj = {
// toString reads current URL dynamically (not cached)
toString: () => globalThis.hopp.request.url,
// Helper to parse URL into components
_parseUrl: () => {
const urlString = globalThis.hopp.request.url
try {
const parsed = new URL(urlString)
// Get query params from URL
const urlParams = Array.from(parsed.searchParams.entries()).map(
([key, value]) => ({ key, value })
)
// Only merge from hopp.request.params if URL has no query params
// This supports imported collections while allowing mutations to work
let finalParams = urlParams
if (urlParams.length === 0) {
// No params in URL - check hopp.request.params (for imported collections)
const requestParams = globalThis.hopp.request.params || []
const activeRequestParams = requestParams
.filter((p) => p.active !== false)
.map((p) => ({ key: p.key, value: p.value }))
finalParams = activeRequestParams
}
return {
protocol: parsed.protocol.slice(0, -1), // Remove trailing :
host: parsed.hostname.split("."),
port:
parsed.port || (parsed.protocol === "https:" ? "443" : "80"),
path: parsed.pathname.split("/").filter(Boolean),
queryParams: finalParams,
hash: parsed.hash ? parsed.hash.slice(1) : "", // Remove leading #
}
} catch {
// Fallback: try to get params from hopp.request.params
const requestParams = globalThis.hopp.request?.params || []
const activeParams = requestParams
.filter((p) => p.active !== false)
.map((p) => ({ key: p.key, value: p.value }))
return {
protocol: "https",
host: [],
port: "443",
path: [],
queryParams: activeParams,
}
}
},
// Helper to rebuild URL from components
_rebuildUrl: (components) => {
const protocol = components.protocol || "https"
const host = Array.isArray(components.host)
? components.host.join(".")
: components.host
const port =
components.port &&
components.port !== "443" &&
components.port !== "80"
? `:${components.port}`
: ""
const path = Array.isArray(components.path)
? "/" + components.path.join("/")
: components.path
const query =
components.queryParams && components.queryParams.length > 0
? "?" +
components.queryParams
.map(
(p) =>
`${encodeURIComponent(p.key)}=${encodeURIComponent(p.value)}`
)
.join("&")
: ""
const hash = components.hash ? `#${components.hash}` : ""
return `${protocol}://${host}${port}${path}${query}${hash}`
},
// Postman-compatible URL methods
getHost: () => urlObj._parseUrl().host.join("."),
getPath: (_unresolved = false) => {
const pathArray = urlObj._parseUrl().path
return pathArray.length > 0 ? "/" + pathArray.join("/") : "/"
},
getPathWithQuery: () => {
const parsed = urlObj._parseUrl()
const path =
parsed.path.length > 0 ? "/" + parsed.path.join("/") : "/"
const query =
parsed.queryParams.length > 0
? "?" +
parsed.queryParams
.map(
(p) =>
`${encodeURIComponent(p.key)}=${encodeURIComponent(p.value)}`
)
.join("&")
: ""
return path + query
},
getQueryString: (_options = {}) => {
const params = urlObj._parseUrl().queryParams
if (params.length === 0) return ""
return params
.map(
(p) =>
`${encodeURIComponent(p.key)}=${encodeURIComponent(p.value)}`
)
.join("&")
},
getRemote: (forcePort = false) => {
const parsed = urlObj._parseUrl()
const host = parsed.host.join(".")
const showPort =
forcePort || (parsed.port !== "443" && parsed.port !== "80")
return showPort ? `${host}:${parsed.port}` : host
},
update: (urlString) => {
if (typeof urlString === "string") {
globalThis.hopp.request.setUrl(urlString)
} else if (
urlString &&
typeof urlString === "object" &&
typeof urlString.toString === "function"
) {
globalThis.hopp.request.setUrl(urlString.toString())
} else {
throw new Error(
"URL update requires a string or object with toString() method"
)
}
},
addQueryParams: (params) => {
if (!Array.isArray(params)) {
throw new Error("addQueryParams requires an array of parameters")
}
const currentParsed = urlObj._parseUrl()
params.forEach((param) => {
if (param && param.key) {
currentParsed.queryParams.push({
key: param.key,
value: param.value || "",
})
}
})
globalThis.hopp.request.setUrl(urlObj._rebuildUrl(currentParsed))
},
removeQueryParams: (params) => {
if (!Array.isArray(params) && typeof params !== "string") {
throw new Error(
"removeQueryParams requires an array of param names or a single param name"
)
}
const keysToRemove = Array.isArray(params) ? params : [params]
const currentParsed = urlObj._parseUrl()
const updatedParams = currentParsed.queryParams.filter(
(p) => !keysToRemove.includes(p.key)
)
currentParsed.queryParams = updatedParams
globalThis.hopp.request.setUrl(urlObj._rebuildUrl(currentParsed))
// Also update the params array to ensure consistency
globalThis.hopp.request.setParams(
updatedParams.map((p) => ({
key: p.key,
value: p.value,
active: true,
description: "",
}))
)
},
}
// Lazy-loaded mutable properties
Object.defineProperty(urlObj, "protocol", {
get: () => urlObj._parseUrl().protocol,
set: (value) => {
const parsed = urlObj._parseUrl()
parsed.protocol = value
globalThis.hopp.request.setUrl(urlObj._rebuildUrl(parsed))
},
enumerable: true,
})
Object.defineProperty(urlObj, "host", {
get: () => urlObj._parseUrl().host,
set: (value) => {
const parsed = urlObj._parseUrl()
parsed.host = Array.isArray(value) ? value : value.split(".")
globalThis.hopp.request.setUrl(urlObj._rebuildUrl(parsed))
},
enumerable: true,
})
// hostname is an alias for host as a string
Object.defineProperty(urlObj, "hostname", {
get: () => urlObj._parseUrl().host.join("."),
set: (value) => {
const parsed = urlObj._parseUrl()
parsed.host = String(value).split(".")
globalThis.hopp.request.setUrl(urlObj._rebuildUrl(parsed))
},
enumerable: true,
})
Object.defineProperty(urlObj, "port", {
get: () => urlObj._parseUrl().port,
set: (value) => {
const parsed = urlObj._parseUrl()
parsed.port = String(value)
globalThis.hopp.request.setUrl(urlObj._rebuildUrl(parsed))
},
enumerable: true,
})
Object.defineProperty(urlObj, "path", {
get: () => urlObj._parseUrl().path,
set: (value) => {
const parsed = urlObj._parseUrl()
parsed.path = Array.isArray(value)
? value
: value.split("/").filter(Boolean)
globalThis.hopp.request.setUrl(urlObj._rebuildUrl(parsed))
},
enumerable: true,
})
// hash property for URL fragments
Object.defineProperty(urlObj, "hash", {
get: () => {
try {
const parsed = new URL(globalThis.hopp.request.url)
return parsed.hash ? parsed.hash.slice(1) : ""
} catch {
return ""
}
},
set: (value) => {
const current = globalThis.hopp.request.url
const baseUrl = current.split("#")[0]
const hashValue = value
? value.startsWith("#")
? value
: `#${value}`
: ""
globalThis.hopp.request.setUrl(baseUrl + hashValue)
},
enumerable: true,
})
Object.defineProperty(urlObj, "query", {
get: () => {
return {
// Basic manipulation methods
add: (param) => {
if (!param || !param.key)
throw new Error("Query param must have a 'key' property")
const currentParsed = urlObj._parseUrl()
currentParsed.queryParams.push({
key: param.key,
value: param.value || "",
})
globalThis.hopp.request.setUrl(
urlObj._rebuildUrl(currentParsed)
)
},
remove: (key) => {
if (typeof key !== "string")
throw new Error("Query param key must be a string")
const currentParsed = urlObj._parseUrl()
currentParsed.queryParams = currentParsed.queryParams.filter(
(p) => p.key !== key
)
globalThis.hopp.request.setUrl(
urlObj._rebuildUrl(currentParsed)
)
},
upsert: (param) => {
if (!param || !param.key)
throw new Error("Query param must have a 'key' property")
const currentParsed = urlObj._parseUrl()
const idx = currentParsed.queryParams.findIndex(
(p) => p.key === param.key
)
if (idx >= 0) {
currentParsed.queryParams[idx].value = param.value || ""
} else {
currentParsed.queryParams.push({
key: param.key,
value: param.value || "",
})
}
globalThis.hopp.request.setUrl(
urlObj._rebuildUrl(currentParsed)
)
},
clear: () => {
const currentParsed = urlObj._parseUrl()
currentParsed.queryParams = []
globalThis.hopp.request.setUrl(
urlObj._rebuildUrl(currentParsed)
)
// Also clear the params array to ensure consistency
globalThis.hopp.request.setParams([])
},
// Read methods
get: (key) => {
const params = urlObj._parseUrl().queryParams
const param = params.find((p) => p.key === key)
return param ? param.value : null
},
has: (key) => {
const params = urlObj._parseUrl().queryParams
return params.some((p) => p.key === key)
},
all: () => {
const currentParsed = urlObj._parseUrl()
const result = {}
// Handle duplicate keys by converting to arrays
currentParsed.queryParams.forEach((p) => {
if (Object.prototype.hasOwnProperty.call(result, p.key)) {
if (!Array.isArray(result[p.key])) {
result[p.key] = [result[p.key]]
}
result[p.key].push(p.value)
} else {
result[p.key] = p.value
}
})
return result
},
toObject: () => {
// Alias for all() for Postman compatibility
return urlObj.query.all()
},
// PropertyList iteration methods
each: (callback) => {
const params = urlObj._parseUrl().queryParams
params.forEach(callback)
},
map: (callback) => {
const params = urlObj._parseUrl().queryParams
return params.map(callback)
},
filter: (callback) => {
const params = urlObj._parseUrl().queryParams
return params.filter(callback)
},
count: () => {
return urlObj._parseUrl().queryParams.length
},
idx: (index) => {
const params = urlObj._parseUrl().queryParams
return params[index] || null
},
// Advanced PropertyList methods
find: (rule, context) => {
const params = urlObj._parseUrl().queryParams
if (typeof rule === "function") {
return (
params.find(context ? rule.bind(context) : rule) || null
)
}
// String rule: find by key
if (typeof rule === "string") {
return params.find((p) => p.key === rule) || null
}
return null
},
indexOf: (item) => {
const params = urlObj._parseUrl().queryParams
if (typeof item === "string") {
// Find by key
return params.findIndex((p) => p.key === item)
}
if (item && typeof item === "object" && item.key) {
// Find by object with key
return params.findIndex((p) => p.key === item.key)
}
return -1
},
insert: (item, before) => {
if (!item || !item.key)
throw new Error("Query param must have a 'key' property")
const currentParsed = urlObj._parseUrl()
if (before) {
// Find position to insert before
const beforeIdx = currentParsed.queryParams.findIndex(
(p) => p.key === before
)
if (beforeIdx >= 0) {
currentParsed.queryParams.splice(beforeIdx, 0, {
key: item.key,
value: item.value || "",
})
} else {
// If 'before' not found, append to end
currentParsed.queryParams.push({
key: item.key,
value: item.value || "",
})
}
} else {
// No 'before' specified, add to end
currentParsed.queryParams.push({
key: item.key,
value: item.value || "",
})
}
globalThis.hopp.request.setUrl(
urlObj._rebuildUrl(currentParsed)
)
},
append: (item) => {
if (!item || !item.key)
throw new Error("Query param must have a 'key' property")
const currentParsed = urlObj._parseUrl()
// Remove existing instances of this key
currentParsed.queryParams = currentParsed.queryParams.filter(
(p) => p.key !== item.key
)
// Add at end
currentParsed.queryParams.push({
key: item.key,
value: item.value || "",
})
globalThis.hopp.request.setUrl(
urlObj._rebuildUrl(currentParsed)
)
},
assimilate: (source, prune) => {
if (!source || typeof source !== "object") {
throw new Error("Source must be an array or object")
}
const currentParsed = urlObj._parseUrl()
// Convert source to array format
let sourceArray
if (Array.isArray(source)) {
sourceArray = source
} else {
// Convert object to array of {key, value}
sourceArray = Object.entries(source).map(([key, value]) => ({
key,
value: String(value),
}))
}
// Update or add each item from source
sourceArray.forEach((item) => {
if (!item || !item.key) return
const idx = currentParsed.queryParams.findIndex(
(p) => p.key === item.key
)
if (idx >= 0) {
// Update existing
currentParsed.queryParams[idx].value = item.value || ""
} else {
// Add new
currentParsed.queryParams.push({
key: item.key,
value: item.value || "",
})
}
})
if (prune) {
// Remove params not in source
const sourceKeys = sourceArray
.filter((i) => i && i.key)
.map((i) => i.key)
currentParsed.queryParams = currentParsed.queryParams.filter(
(p) => sourceKeys.includes(p.key)
)
}
globalThis.hopp.request.setUrl(
urlObj._rebuildUrl(currentParsed)
)
},
}
},
enumerable: true,
})
return urlObj
},
// URL setter (Postman pattern: pm.request.url = "...")
set url(value) {
let urlString
if (typeof value === "string") {
urlString = value
} else if (value && typeof value.toString === "function") {
urlString = value.toString()
} else {
throw new Error("URL must be a string or have a toString() method")
}
globalThis.hopp.request.setUrl(urlString)
// Parse query params from the new URL and update params array
try {
const parsed = new URL(urlString)
const urlParams = Array.from(parsed.searchParams.entries()).map(
([key, value]) => ({ key, value, active: true, description: "" })
)
globalThis.hopp.request.setParams(urlParams)
} catch {
// If URL parsing fails, clear params
globalThis.hopp.request.setParams([])
}
},
// Method - Mutable
// NOTE: Postman does NOT normalize method to uppercase, so we preserve the original case
get method() {
return globalThis.hopp.request.method
},
set method(value) {
if (typeof value !== "string") {
throw new Error(
"Method must be a string (GET, POST, PUT, DELETE, etc.)"
)
}
globalThis.hopp.request.setMethod(value)
},
// Headers - With Postman mutation methods and PropertyList interface
get headers() {
return {
// Read methods
get: (name) => {
const headers = globalThis.hopp.request.headers
const header = headers.find(
(h) => h.key.toLowerCase() === name.toLowerCase()
)
return header ? header.value : null
},
has: (name) => {
const headers = globalThis.hopp.request.headers
return headers.some(
(h) => h.key.toLowerCase() === name.toLowerCase()
)
},
all: () => {
const result = {}
globalThis.hopp.request.headers.forEach((header) => {
result[header.key] = header.value
})
return result
},
toObject: () => {
// Alias for all() for Postman compatibility
const result = {}
globalThis.hopp.request.headers.forEach((header) => {
result[header.key] = header.value
})
return result
},
// Mutation methods (Postman compatibility)
add: (header) => {
if (!header || typeof header !== "object") {
throw new Error(
"Header must be an object with 'key' and 'value' properties"
)
}
if (!header.key) {
throw new Error("Header must have a 'key' property")
}
globalThis.hopp.request.setHeader(header.key, header.value || "")
},
remove: (headerName) => {
if (typeof headerName !== "string") {
throw new Error("Header name must be a string")
}
globalThis.hopp.request.removeHeader(headerName)
},
upsert: (header) => {
if (!header || typeof header !== "object") {
throw new Error(
"Header must be an object with 'key' and 'value' properties"
)
}
if (!header.key) {
throw new Error("Header must have a 'key' property")
}
// Remove existing (case-insensitive) then add
globalThis.hopp.request.removeHeader(header.key)
globalThis.hopp.request.setHeader(header.key, header.value || "")
},
clear: () => {
globalThis.hopp.request.setHeaders([])
},
// PropertyList iteration methods
each: (callback) => {
globalThis.hopp.request.headers.forEach(callback)
},
map: (callback) => {
return globalThis.hopp.request.headers.map(callback)
},
filter: (callback) => {
return globalThis.hopp.request.headers.filter(callback)
},
count: () => {
return globalThis.hopp.request.headers.length
},
idx: (index) => {
return globalThis.hopp.request.headers[index] || null
},
// Advanced PropertyList methods
find: (rule, context) => {
const headers = globalThis.hopp.request.headers
if (typeof rule === "function") {
return headers.find(context ? rule.bind(context) : rule) || null
}
// String rule: find by key (case-insensitive)
if (typeof rule === "string") {
return (
headers.find(
(h) => h.key.toLowerCase() === rule.toLowerCase()
) || null
)
}
return null
},
indexOf: (item) => {
const headers = globalThis.hopp.request.headers
if (typeof item === "string") {
// Find by key (case-insensitive)
return headers.findIndex(
(h) => h.key.toLowerCase() === item.toLowerCase()
)
}
if (item && typeof item === "object" && item.key) {
// Find by object with key (case-insensitive)
return headers.findIndex(
(h) => h.key.toLowerCase() === item.key.toLowerCase()
)
}
return -1
},
insert: (item, before) => {
if (!item || !item.key)
throw new Error("Header must have a 'key' property")
const headers = globalThis.hopp.request.headers
if (before) {
// Find position to insert before (case-insensitive)
const beforeIdx = headers.findIndex(
(h) => h.key.toLowerCase() === before.toLowerCase()
)
if (beforeIdx >= 0) {
const newHeaders = [...headers]
newHeaders.splice(beforeIdx, 0, {
key: item.key,
value: item.value || "",
active: true,
})
globalThis.hopp.request.setHeaders(newHeaders)
} else {
// If 'before' not found, append to end
globalThis.hopp.request.setHeader(item.key, item.value || "")
}
} else {
// No 'before' specified, add to end
globalThis.hopp.request.setHeader(item.key, item.value || "")
}
},
append: (item) => {
if (!item || !item.key)
throw new Error("Header must have a 'key' property")
// Remove existing instances of this key (case-insensitive)
globalThis.hopp.request.removeHeader(item.key)
// Add at end
globalThis.hopp.request.setHeader(item.key, item.value || "")
},
assimilate: (source, prune) => {
if (!source || typeof source !== "object") {
throw new Error("Source must be an array or object")
}
let sourceArray
if (Array.isArray(source)) {
sourceArray = source
} else {
// Convert object to array of {key, value}
sourceArray = Object.entries(source).map(([key, value]) => ({
key,
value: String(value),
active: true,
}))
}
// Update or add each item from source
sourceArray.forEach((item) => {
if (!item || !item.key) return
// Remove existing (case-insensitive)
globalThis.hopp.request.removeHeader(item.key)
// Add new/updated
globalThis.hopp.request.setHeader(item.key, item.value || "")
})
if (prune) {
// Remove headers not in source (case-insensitive)
const sourceKeys = sourceArray
.filter((i) => i && i.key)
.map((i) => i.key.toLowerCase())
const currentHeaders = globalThis.hopp.request.headers
const filteredHeaders = currentHeaders.filter((h) =>
sourceKeys.includes(h.key.toLowerCase())
)
globalThis.hopp.request.setHeaders(filteredHeaders)
}
},
}
},
// Body - With Postman update() method
get body() {
const currentBody = globalThis.hopp.request.body
// Return body with update() method
return {
// Spread current body properties
...currentBody,
// Postman-compatible update() method
update: (bodySpec) => {
if (typeof bodySpec === "string") {
// Direct string assignment
globalThis.hopp.request.setBody({
contentType: "text/plain",
body: bodySpec,
})
} else if (bodySpec && typeof bodySpec === "object") {
const mode = bodySpec.mode || "raw"
switch (mode) {
case "raw":
globalThis.hopp.request.setBody({
contentType:
bodySpec.options?.raw?.language === "json"
? "application/json"
: "text/plain",
body: bodySpec.raw || "",
})
break
case "urlencoded":
globalThis.hopp.request.setBody({
contentType: "application/x-www-form-urlencoded",
body: bodySpec.urlencoded || [],
})
break
case "formdata":
globalThis.hopp.request.setBody({
contentType: "multipart/form-data",
body: bodySpec.formdata || [],
})
break
case "file":
globalThis.hopp.request.setBody({
contentType: "binary",
body: bodySpec.file || null,
})
break
default:
throw new Error(
`Unsupported body mode: ${mode}. Supported modes: raw, urlencoded, formdata, file`
)
}
} else {
throw new Error(
"Body spec must be a string or object with mode property"
)
}
},
}
},
// Body setter (legacy pattern for direct assignment)
set body(value) {
if (typeof value === "string") {
globalThis.hopp.request.setBody({
contentType: "text/plain",
body: value,
})
} else if (typeof value === "object" && value !== null) {
globalThis.hopp.request.setBody({
contentType: "application/json",
body: JSON.stringify(value),
})
} else {
throw new Error("Body must be a string or object")
}
},
// Auth - Mutable
get auth() {
return globalThis.hopp.request.auth
},
set auth(value) {
if (value === null || value === undefined) {
globalThis.hopp.request.setAuth({
authType: "none",
authActive: false,
})
} else if (typeof value === "object") {
globalThis.hopp.request.setAuth(value)
} else {
throw new Error("Auth must be an object or null")
}
},
// Custom serialization for console.log to ensure consistent behavior
// This method is called by faraday-cage's marshalling system
toJSON() {
// Return a plain object with all properties expanded
// This ensures console.log(pm.request) shows the full structure consistently
const urlParsed = this.url._parseUrl()
return {
id: this.id,
name: this.name,
url: {
protocol: urlParsed.protocol,
host: urlParsed.host,
hostname: urlParsed.host.join("."),
port: urlParsed.port,
path: urlParsed.path,
hash: urlParsed.hash || "",
query: this.url.query.all(),
},
method: this.method,
headers: this.headers.toObject(),
body: this.body,
auth: this.auth,
}
},
toString() {
return `Request { id: ${this.id}, name: ${this.name}, method: ${this.method}, url: ${this.url.toString()} }`
},
[Symbol.toStringTag]: "Request",
},
// Script context information
info: {
eventName: "pre-request",
get requestName() {
return inputs.pmInfoRequestName()
},
get requestId() {
return inputs.pmInfoRequestId()
},
// Unsupported Collection Runner features
get iteration() {
throw new Error(
"pm.info.iteration is not supported in Hoppscotch (Collection Runner feature)"
)
},
get iterationCount() {
throw new Error(
"pm.info.iterationCount is not supported in Hoppscotch (Collection Runner feature)"
)
},
},
// Unsupported APIs that throw errors
sendRequest: (_request, _callback) => {
throw new Error("pm.sendRequest() is not yet implemented in Hoppscotch")
},
// Collection variables (unsupported)
collectionVariables: {
get: () => {
throw new Error(
"pm.collectionVariables.get() is not supported in Hoppscotch (use environment or request variables instead)"
)
},
set: () => {
throw new Error(
"pm.collectionVariables.set() is not supported in Hoppscotch (use environment or request variables instead)"
)
},
unset: () => {
throw new Error(
"pm.collectionVariables.unset() is not supported in Hoppscotch (use environment or request variables instead)"
)
},
has: () => {
throw new Error(
"pm.collectionVariables.has() is not supported in Hoppscotch (use environment or request variables instead)"
)
},
clear: () => {
throw new Error(
"pm.collectionVariables.clear() is not supported in Hoppscotch (use environment or request variables instead)"
)
},
toObject: () => {
throw new Error(
"pm.collectionVariables.toObject() is not supported in Hoppscotch (use environment or request variables instead)"
)
},
replaceIn: () => {
throw new Error(
"pm.collectionVariables.replaceIn() is not supported in Hoppscotch (use environment or request variables instead)"
)
},
},
// Postman Vault (unsupported)
vault: {
get: () => {
throw new Error(
"pm.vault.get() is not supported in Hoppscotch (Postman Vault feature)"
)
},
set: () => {
throw new Error(
"pm.vault.set() is not supported in Hoppscotch (Postman Vault feature)"
)
},
unset: () => {
throw new Error(
"pm.vault.unset() is not supported in Hoppscotch (Postman Vault feature)"
)
},
},
// Postman Visualizer (unsupported)
visualizer: {
set: () => {
throw new Error(
"pm.visualizer.set() is not supported in Hoppscotch (Postman Visualizer feature)"
)
},
clear: () => {
throw new Error(
"pm.visualizer.clear() is not supported in Hoppscotch (Postman Visualizer feature)"
)
},
},
// Iteration data (unsupported)
iterationData: {
get: () => {
throw new Error(
"pm.iterationData.get() is not supported in Hoppscotch (Collection Runner feature)"
)
},
set: () => {
throw new Error(
"pm.iterationData.set() is not supported in Hoppscotch (Collection Runner feature)"
)
},
unset: () => {
throw new Error(
"pm.iterationData.unset() is not supported in Hoppscotch (Collection Runner feature)"
)
},
has: () => {
throw new Error(
"pm.iterationData.has() is not supported in Hoppscotch (Collection Runner feature)"
)
},
toObject: () => {
throw new Error(
"pm.iterationData.toObject() is not supported in Hoppscotch (Collection Runner feature)"
)
},
toJSON: () => {
throw new Error(
"pm.iterationData.toJSON() is not supported in Hoppscotch (Collection Runner feature)"
)
},
},
// Execution control (unsupported)
execution: {
location: (() => {
const location = ["Hoppscotch"]
Object.defineProperty(location, "current", {
value: "Hoppscotch",
writable: false,
enumerable: true,
})
Object.freeze(location)
return location
})(),
setNextRequest: () => {
throw new Error(
"pm.execution.setNextRequest() is not supported in Hoppscotch (Collection Runner feature)"
)
},
skipRequest: () => {
throw new Error(
"pm.execution.skipRequest() is not supported in Hoppscotch (Collection Runner feature)"
)
},
runRequest: () => {
throw new Error(
"pm.execution.runRequest() is not supported in Hoppscotch (Collection Runner feature)"
)
},
},
// Package imports (unsupported)
require: (packageName) => {
throw new Error(
`pm.require('${packageName}') is not supported in Hoppscotch (Package Library feature)`
)
},
}
}