diff --git a/packages/hoppscotch-cli/src/__tests__/e2e/fixtures/collections/scripting-revamp-coll.json b/packages/hoppscotch-cli/src/__tests__/e2e/fixtures/collections/scripting-revamp-coll.json
index bce7446f..c513f23a 100644
--- a/packages/hoppscotch-cli/src/__tests__/e2e/fixtures/collections/scripting-revamp-coll.json
+++ b/packages/hoppscotch-cli/src/__tests__/e2e/fixtures/collections/scripting-revamp-coll.json
@@ -70,7 +70,7 @@
"params": [],
"headers": [],
"preRequestScript": "export {};\nhopp.env.set('test_key', 'test_value')\nhopp.env.set('recursive_key', '<>')\nhopp.env.global.set('global_key', 'global_value')\nhopp.env.active.set('active_key', 'active_value')\n\n// `pm` namespace equivalents\npm.variables.set('pm_test_key', 'pm_test_value')\npm.environment.set('pm_active_key', 'pm_active_value')\npm.globals.set('pm_global_key', 'pm_global_value')\n",
- "testScript": "export {};\n\nhopp.test('`hopp.env.get()` retrieves environment variables', () => {\n const value = hopp.env.get('test_key')\n hopp.expect(value).toBe('test_value')\n})\n\npm.test('`pm.variables.get()` retrieves environment variables', () => {\n const value = pm.variables.get('test_key')\n pm.expect(value).toBe('test_value')\n})\n\nhopp.test('`hopp.env.getRaw()` retrieves raw environment variables without resolution', () => {\n const rawValue = hopp.env.getRaw('recursive_key')\n hopp.expect(rawValue).toBe('<>')\n})\n\nhopp.test('`hopp.env.get()` resolves recursive environment variables', () => {\n const resolvedValue = hopp.env.get('recursive_key')\n hopp.expect(resolvedValue).toBe('test_value')\n})\n\npm.test('`pm.variables.replaceIn()` resolves template variables', () => {\n const resolved = pm.variables.replaceIn('Value is {{test_key}}')\n pm.expect(resolved).toBe('Value is test_value')\n})\n\nhopp.test('`hopp.env.global.get()` retrieves global environment variables', () => {\n const globalValue = hopp.env.global.get('global_key')\n\n // `hopp.env.global` would be empty for the CLI\n if (globalValue) {\n hopp.expect(globalValue).toBe('global_value')\n }\n})\n\npm.test('`pm.globals.get()` retrieves global environment variables', () => {\n const globalValue = pm.globals.get('global_key')\n\n // `pm.globals` would be empty for the CLI\n if (globalValue) {\n pm.expect(globalValue).toBe('global_value')\n }\n})\n\nhopp.test('`hopp.env.active.get()` retrieves active environment variables', () => {\n const activeValue = hopp.env.active.get('active_key')\n hopp.expect(activeValue).toBe('active_value')\n})\n\npm.test('`pm.environment.get()` retrieves active environment variables', () => {\n const activeValue = pm.environment.get('active_key')\n pm.expect(activeValue).toBe('active_value')\n})\n\nhopp.test('Environment methods return null for non-existent keys', () => {\n hopp.expect(hopp.env.get('non_existent')).toBe(null)\n hopp.expect(hopp.env.getRaw('non_existent')).toBe(null)\n hopp.expect(hopp.env.global.get('non_existent')).toBe(null)\n hopp.expect(hopp.env.active.get('non_existent')).toBe(null)\n})\n\npm.test('`pm` environment methods handle non-existent keys correctly', () => {\n pm.expect(pm.variables.get('non_existent')).toBe(null)\n pm.expect(pm.environment.get('non_existent')).toBe(null)\n pm.expect(pm.globals.get('non_existent')).toBe(null)\n pm.expect(pm.variables.has('non_existent')).toBe(false)\n pm.expect(pm.environment.has('non_existent')).toBe(false)\n pm.expect(pm.globals.has('non_existent')).toBe(false)\n})\n\npm.test('`pm` variables set in pre-request script are accessible', () => {\n pm.expect(pm.variables.get('pm_test_key')).toBe('pm_test_value')\n pm.expect(pm.environment.get('pm_active_key')).toBe('pm_active_value')\n\n const pmGlobalValue = hopp.env.global.get('pm_global_key')\n\n // `hopp.env.global` would be empty for the CLI\n if (pmGlobalValue) {\n hopp.expect(pmGlobalValue).toBe('pm_global_value')\n }\n})\n",
+ "testScript": "export {};\n\nhopp.test('`hopp.env.get()` retrieves environment variables', () => {\n const value = hopp.env.get('test_key')\n hopp.expect(value).toBe('test_value')\n})\n\npm.test('`pm.variables.get()` retrieves environment variables', () => {\n const value = pm.variables.get('test_key')\n pm.expect(value).toBe('test_value')\n})\n\nhopp.test('`hopp.env.getRaw()` retrieves raw environment variables without resolution', () => {\n const rawValue = hopp.env.getRaw('recursive_key')\n hopp.expect(rawValue).toBe('<>')\n})\n\nhopp.test('`hopp.env.get()` resolves recursive environment variables', () => {\n const resolvedValue = hopp.env.get('recursive_key')\n hopp.expect(resolvedValue).toBe('test_value')\n})\n\npm.test('`pm.variables.replaceIn()` resolves template variables', () => {\n const resolved = pm.variables.replaceIn('Value is {{test_key}}')\n pm.expect(resolved).toBe('Value is test_value')\n})\n\nhopp.test('`hopp.env.global.get()` retrieves global environment variables', () => {\n const globalValue = hopp.env.global.get('global_key')\n\n // `hopp.env.global` would be empty for the CLI\n if (globalValue) {\n hopp.expect(globalValue).toBe('global_value')\n }\n})\n\npm.test('`pm.globals.get()` retrieves global environment variables', () => {\n const globalValue = pm.globals.get('global_key')\n\n // `pm.globals` would be empty for the CLI\n if (globalValue) {\n pm.expect(globalValue).toBe('global_value')\n }\n})\n\nhopp.test('`hopp.env.active.get()` retrieves active environment variables', () => {\n const activeValue = hopp.env.active.get('active_key')\n hopp.expect(activeValue).toBe('active_value')\n})\n\npm.test('`pm.environment.get()` retrieves active environment variables', () => {\n const activeValue = pm.environment.get('active_key')\n pm.expect(activeValue).toBe('active_value')\n})\n\nhopp.test('Environment methods return null for non-existent keys', () => {\n hopp.expect(hopp.env.get('non_existent')).toBe(null)\n hopp.expect(hopp.env.getRaw('non_existent')).toBe(null)\n hopp.expect(hopp.env.global.get('non_existent')).toBe(null)\n hopp.expect(hopp.env.active.get('non_existent')).toBe(null)\n})\n\npm.test('`pm` environment methods handle non-existent keys correctly', () => {\n pm.expect(pm.variables.get('non_existent')).toBe(undefined)\n pm.expect(pm.environment.get('non_existent')).toBe(undefined)\n pm.expect(pm.globals.get('non_existent')).toBe(undefined)\n pm.expect(pm.variables.has('non_existent')).toBe(false)\n pm.expect(pm.environment.has('non_existent')).toBe(false)\n pm.expect(pm.globals.has('non_existent')).toBe(false)\n})\n\npm.test('`pm` variables set in pre-request script are accessible', () => {\n pm.expect(pm.variables.get('pm_test_key')).toBe('pm_test_value')\n pm.expect(pm.environment.get('pm_active_key')).toBe('pm_active_value')\n\n const pmGlobalValue = hopp.env.global.get('pm_global_key')\n\n // `hopp.env.global` would be empty for the CLI\n if (pmGlobalValue) {\n hopp.expect(pmGlobalValue).toBe('pm_global_value')\n }\n})\n",
"auth": {
"authType": "inherit",
"authActive": true
@@ -133,7 +133,7 @@
}
],
"preRequestScript": "export {};\n",
- "testScript": "export {};\n\nhopp.test('`hopp.response.statusCode` returns the response status code', () => {\n hopp.expect(hopp.response.statusCode).toBe(200)\n})\n\npm.test('`pm.response.code` returns the response status code', () => {\n pm.expect(pm.response.code).toBe(200)\n})\n\nhopp.test('`hopp.response.statusText` returns the response status text', () => {\n hopp.expect(hopp.response.statusText).toBeType('string')\n})\n\npm.test('`pm.response.status` returns the response status text', () => {\n pm.expect(pm.response.status).toBeType('string')\n})\n\nhopp.test('`hopp.response.headers` contains response headers', () => {\n const { headers } = hopp.response\n\n hopp.expect(headers).toBeType('object')\n hopp.expect(headers.length > 0).toBe(true)\n})\n\npm.test('`pm.response.headers` contains response headers', () => {\n const headersAll = pm.response.headers.all()\n pm.expect(headersAll).toBeType('object')\n pm.expect(Object.keys(headersAll).length > 0).toBe(true)\n})\n\nhopp.test('`hopp.response.responseTime` is a positive number', () => {\n hopp.expect(hopp.response.responseTime).toBeType('number')\n hopp.expect(hopp.response.responseTime > 0).toBe(true)\n})\n\npm.test('`pm.response.responseTime` is a positive number', () => {\n pm.expect(pm.response.responseTime).toBeType('number')\n pm.expect(pm.response.responseTime > 0).toBe(true)\n})\n\nhopp.test('`hopp.response.text()` returns response as text', () => {\n const responseText = hopp.response.body.asText()\n hopp.expect(responseText).toBeType('string')\n hopp.expect(responseText.length > 0).toBe(true)\n})\n\npm.test('`pm.response.text()` returns response as text', () => {\n const responseText = pm.response.text()\n pm.expect(responseText).toBeType('string')\n pm.expect(responseText.length > 0).toBe(true)\n})\n\nhopp.test('`hopp.response.json()` parses JSON response', () => {\n const responseJSON = hopp.response.body.asJSON()\n hopp.expect(responseJSON).toBeType('object')\n})\n\npm.test('`pm.response.json()` parses JSON response', () => {\n const responseJSON = pm.response.json()\n pm.expect(responseJSON).toBeType('object')\n})\n\n\nhopp.test('`hopp.response.bytes()` returns the raw response', () => {\n const responseBuffer = hopp.response.body.bytes()\n hopp.expect(responseBuffer).toBeType('object')\n hopp.expect(responseBuffer.constructor.name).toBe('Object')\n})\n\npm.test('`pm.response.stream` returns the raw response', () => {\n const responseBuffer = pm.response.stream\n pm.expect(responseBuffer).toBeType('object')\n pm.expect(responseBuffer.constructor.name).toBe('Object')\n})",
+ "testScript": "export {};\n\nhopp.test('`hopp.response.statusCode` returns the response status code', () => {\n hopp.expect(hopp.response.statusCode).toBe(200)\n})\n\npm.test('`pm.response.code` returns the response status code', () => {\n pm.expect(pm.response.code).toBe(200)\n})\n\nhopp.test('`hopp.response.statusText` returns the response status text', () => {\n hopp.expect(hopp.response.statusText).toBeType('string')\n})\n\npm.test('`pm.response.status` returns the response status text', () => {\n pm.expect(pm.response.status).toBeType('string')\n})\n\nhopp.test('`hopp.response.headers` contains response headers', () => {\n const { headers }\u00a0= hopp.response\n\n hopp.expect(headers).toBeType('object')\n hopp.expect(headers.length > 0).toBe(true)\n})\n\npm.test('`pm.response.headers` contains response headers', () => {\n const headersAll = pm.response.headers.all()\n pm.expect(headersAll).toBeType('object')\n pm.expect(Object.keys(headersAll).length > 0).toBe(true)\n})\n\nhopp.test('`hopp.response.responseTime` is a positive number', () => {\n hopp.expect(hopp.response.responseTime).toBeType('number')\n hopp.expect(hopp.response.responseTime > 0).toBe(true)\n})\n\npm.test('`pm.response.responseTime` is a positive number', () => {\n pm.expect(pm.response.responseTime).toBeType('number')\n pm.expect(pm.response.responseTime > 0).toBe(true)\n})\n\nhopp.test('`hopp.response.text()` returns response as text', () => {\n const responseText = hopp.response.body.asText()\n hopp.expect(responseText).toBeType('string')\n hopp.expect(responseText.length > 0).toBe(true)\n})\n\npm.test('`pm.response.text()` returns response as text', () => {\n const responseText = pm.response.text()\n pm.expect(responseText).toBeType('string')\n pm.expect(responseText.length > 0).toBe(true)\n})\n\nhopp.test('`hopp.response.json()` parses JSON response', () => {\n const responseJSON = hopp.response.body.asJSON()\n hopp.expect(responseJSON).toBeType('object')\n})\n\npm.test('`pm.response.json()` parses JSON response', () => {\n const responseJSON = pm.response.json()\n pm.expect(responseJSON).toBeType('object')\n})\n\n\nhopp.test('`hopp.response.bytes()` returns the raw response', () => {\n const responseBuffer = hopp.response.body.bytes()\n hopp.expect(responseBuffer).toBeType('object')\n hopp.expect(responseBuffer.constructor.name).toBe('Object')\n})\n\npm.test('`pm.response.stream` returns the raw response', () => {\n const responseBuffer = pm.response.stream\n pm.expect(responseBuffer).toBeType('object')\n pm.expect(responseBuffer.constructor.name).toBe('Object')\n})",
"auth": {
"authType": "inherit",
"authActive": true
@@ -191,7 +191,7 @@
"params": [],
"headers": [],
"preRequestScript": "export {};\n",
- "testScript": "export {};\n\npm.test('`pm.info.eventName` indicates the script context', () => {\n pm.expect(pm.info.eventName).toBe('post-request')\n})\n\npm.test('`pm.info.requestName` returns the request name', () => {\n pm.expect(pm.info.requestName).toBe('info-context-test')\n})\n\npm.test('`pm.info.requestId` returns an optional request identifier', () => {\n const requestId = pm.info.requestId\n if (requestId) {\n pm.expect(requestId).toBeType('string')\n pm.expect(requestId?.length > 0).toBe(true)\n } else {\n pm.expect(requestId).toBe(undefined)\n }\n})",
+ "testScript": "export {};\n\npm.test('`pm.info.eventName` indicates the script context', () => {\n pm.expect(pm.info.eventName).toBe('test')\n})\n\npm.test('`pm.info.requestName` returns the request name', () => {\n pm.expect(pm.info.requestName).toBe('info-context-test')\n})\n\npm.test('`pm.info.requestId` returns an optional request identifier', () => {\n const requestId = pm.info.requestId\n if (requestId) {\n pm.expect(requestId).toBeType('string')\n pm.expect(requestId?.length > 0).toBe(true)\n } else {\n pm.expect(requestId).toBe(undefined)\n }\n})",
"auth": {
"authType": "inherit",
"authActive": true
@@ -244,6 +244,550 @@
},
"requestVariables": [],
"responses": {}
+ },
+ {
+ "v": "15",
+ "id": "cmfhzf0op00chai1qt0inext01",
+ "name": "chai-assertions-hopp-extended",
+ "method": "POST",
+ "endpoint": "https://echo.hoppscotch.io",
+ "params": [],
+ "headers": [],
+ "preRequestScript": "export {};\n",
+ "testScript": "export {};\n\n// EQUALITY ASSERTIONS\nhopp.test('Chai equality - equal() method', () => {\n hopp.expect(5).to.equal(5)\n hopp.expect('hello').to.equal('hello')\n hopp.expect(true).to.equal(true)\n})\n\nhopp.test('Chai equality - eql() for deep equality', () => {\n hopp.expect({ a: 1 }).to.eql({ a: 1 })\n hopp.expect([1, 2, 3]).to.eql([1, 2, 3])\n})\n\nhopp.test('Chai equality - negation with .not', () => {\n hopp.expect(5).to.not.equal(10)\n hopp.expect('hello').to.not.equal('world')\n})\n\n// TYPE ASSERTIONS\nhopp.test('Chai type - .a() and .an() assertions', () => {\n hopp.expect('test').to.be.a('string')\n hopp.expect(42).to.be.a('number')\n hopp.expect([]).to.be.an('array')\n hopp.expect({}).to.be.an('object')\n})\n\nhopp.test('Chai type - instanceof assertions', () => {\n hopp.expect([1, 2, 3]).to.be.instanceof(Array)\n hopp.expect(new Date()).to.be.instanceof(Date)\n hopp.expect(new Error('test')).to.be.instanceof(Error)\n})\n\n// TRUTHINESS ASSERTIONS\nhopp.test('Chai truthiness - .true, .false, .null, .undefined', () => {\n hopp.expect(true).to.be.true\n hopp.expect(false).to.be.false\n hopp.expect(null).to.be.null\n hopp.expect(undefined).to.be.undefined\n})\n\nhopp.test('Chai truthiness - .ok and .exist', () => {\n hopp.expect(1).to.be.ok\n hopp.expect('string').to.exist\n hopp.expect(0).to.not.be.ok\n})\n\nhopp.test('Chai truthiness - .NaN assertion', () => {\n hopp.expect(NaN).to.be.NaN\n hopp.expect(42).to.not.be.NaN\n})\n\n// NUMERICAL COMPARISONS\nhopp.test('Chai numbers - .above() and .below()', () => {\n hopp.expect(10).to.be.above(5)\n hopp.expect(5).to.be.below(10)\n hopp.expect(5).to.not.be.above(10)\n})\n\nhopp.test('Chai numbers - aliases gt, lt, gte, lte', () => {\n hopp.expect(10).to.be.gt(5)\n hopp.expect(5).to.be.lt(10)\n hopp.expect(5).to.be.gte(5)\n hopp.expect(5).to.be.lte(5)\n})\n\nhopp.test('Chai numbers - .least() and .most()', () => {\n hopp.expect(10).to.be.at.least(10)\n hopp.expect(10).to.be.at.most(10)\n hopp.expect(15).to.be.at.least(10)\n})\n\nhopp.test('Chai numbers - .within() range', () => {\n hopp.expect(7).to.be.within(5, 10)\n hopp.expect(5).to.be.within(5, 10)\n hopp.expect(10).to.be.within(5, 10)\n})\n\nhopp.test('Chai numbers - .closeTo() with delta', () => {\n hopp.expect(10).to.be.closeTo(10.5, 0.6)\n hopp.expect(9.99).to.be.closeTo(10, 0.1)\n})\n\n// PROPERTY ASSERTIONS\nhopp.test('Chai properties - .property() checks', () => {\n const obj = { name: 'test', nested: { value: 42 } }\n hopp.expect(obj).to.have.property('name')\n hopp.expect(obj).to.have.property('name', 'test')\n hopp.expect(obj).to.have.nested.property('nested.value', 42)\n})\n\nhopp.test('Chai properties - .ownProperty() checks', () => {\n const obj = { own: 'value' }\n hopp.expect(obj).to.have.ownProperty('own')\n hopp.expect(obj).to.not.have.ownProperty('toString')\n})\n\n// LENGTH ASSERTIONS\nhopp.test('Chai length - .lengthOf() for arrays and strings', () => {\n hopp.expect([1, 2, 3]).to.have.lengthOf(3)\n hopp.expect('hello').to.have.lengthOf(5)\n hopp.expect([]).to.have.lengthOf(0)\n})\n\n// COLLECTION ASSERTIONS\nhopp.test('Chai collections - .keys() assertions', () => {\n const obj = { a: 1, b: 2, c: 3 }\n hopp.expect(obj).to.have.keys('a', 'b', 'c')\n hopp.expect(obj).to.have.all.keys('a', 'b', 'c')\n hopp.expect(obj).to.have.any.keys('a', 'd')\n})\n\nhopp.test('Chai collections - .members() for arrays', () => {\n hopp.expect([1, 2, 3]).to.have.members([3, 2, 1])\n hopp.expect([1, 2, 3]).to.include.members([1, 2])\n})\n\nhopp.test('Chai collections - .deep.members() for object arrays', () => {\n hopp.expect([{ a: 1 }, { b: 2 }]).to.have.deep.members([{ b: 2 }, { a: 1 }])\n})\n\nhopp.test('Chai collections - .oneOf() checks', () => {\n hopp.expect(2).to.be.oneOf([1, 2, 3])\n hopp.expect('a').to.be.oneOf(['a', 'b', 'c'])\n})\n\n// INCLUSION ASSERTIONS\nhopp.test('Chai inclusion - .include() for arrays and strings', () => {\n hopp.expect([1, 2, 3]).to.include(2)\n hopp.expect('hello world').to.include('world')\n})\n\nhopp.test('Chai inclusion - .deep.include() for objects', () => {\n hopp.expect([{ a: 1 }, { b: 2 }]).to.deep.include({ a: 1 })\n})\n\n// FUNCTION/ERROR ASSERTIONS\nhopp.test('Chai functions - .throw() assertions', () => {\n const throwFn = () => { throw new Error('test error') }\n const noThrow = () => { return 42 }\n \n hopp.expect(throwFn).to.throw()\n hopp.expect(throwFn).to.throw(Error)\n hopp.expect(throwFn).to.throw('test error')\n hopp.expect(noThrow).to.not.throw()\n})\n\nhopp.test('Chai functions - .respondTo() method checks', () => {\n const obj = { method: function() {} }\n hopp.expect(obj).to.respondTo('method')\n hopp.expect([]).to.respondTo('push')\n})\n\nhopp.test('Chai functions - .satisfy() custom matcher', () => {\n hopp.expect(10).to.satisfy((num) => num > 5)\n hopp.expect('hello').to.satisfy((str) => str.length === 5)\n})\n\n// OBJECT STATE ASSERTIONS\nhopp.test('Chai object state - .sealed, .frozen, .extensible', () => {\n const sealed = Object.seal({ a: 1 })\n const frozen = Object.freeze({ b: 2 })\n const extensible = { c: 3 }\n \n hopp.expect(sealed).to.be.sealed\n hopp.expect(frozen).to.be.frozen\n hopp.expect(extensible).to.be.extensible\n})\n\nhopp.test('Chai number state - .finite', () => {\n hopp.expect(42).to.be.finite\n hopp.expect(Infinity).to.not.be.finite\n})\n\n// EXOTIC OBJECTS\nhopp.test('Chai exotic - Set assertions', () => {\n const mySet = new Set([1, 2, 3])\n hopp.expect(mySet).to.be.instanceof(Set)\n hopp.expect(mySet).to.have.lengthOf(3)\n})\n\nhopp.test('Chai exotic - Map assertions', () => {\n const myMap = new Map([['key', 'value']])\n hopp.expect(myMap).to.be.instanceof(Map)\n hopp.expect(myMap).to.have.lengthOf(1)\n})\n\n// SIDE-EFFECT ASSERTIONS\nhopp.test('Chai side-effects - .change() assertions', () => {\n const obj = { count: 0 }\n const changeFn = () => { obj.count = 5 }\n hopp.expect(changeFn).to.change(obj, 'count')\n \n const noChangeFn = () => {} \n hopp.expect(noChangeFn).to.not.change(obj, 'count')\n})\n\nhopp.test('Chai side-effects - .change().by() delta', () => {\n const obj = { count: 10 }\n const addFive = () => { obj.count += 5 }\n hopp.expect(addFive).to.change(obj, 'count').by(5)\n})\n\nhopp.test('Chai side-effects - .increase() assertions', () => {\n const obj = { count: 0 }\n const incFn = () => { obj.count++ }\n hopp.expect(incFn).to.increase(obj, 'count')\n})\n\nhopp.test('Chai side-effects - .decrease() assertions', () => {\n const obj = { count: 10 }\n const decFn = () => { obj.count-- }\n hopp.expect(decFn).to.decrease(obj, 'count')\n})\n\n// LANGUAGE CHAINS AND MODIFIERS\nhopp.test('Chai chains - Complex chaining with multiple modifiers', () => {\n hopp.expect([1, 2, 3]).to.be.an('array').that.includes(2)\n hopp.expect({ a: 1, b: 2 }).to.be.an('object').that.has.property('a')\n})\n\nhopp.test('Chai modifiers - .deep with .equal()', () => {\n hopp.expect({ a: { b: 1 } }).to.deep.equal({ a: { b: 1 } })\n hopp.expect([{ a: 1 }]).to.deep.equal([{ a: 1 }])\n})\n\n// RESPONSE-BASED TESTS\nhopp.test('Chai with response - status code checks', () => {\n hopp.expect(hopp.response.statusCode).to.equal(200)\n hopp.expect(hopp.response.statusCode).to.be.within(200, 299)\n})\n\nhopp.test('Chai with response - body parsing', () => {\n const response = hopp.response.body.asJSON()\n hopp.expect(response).to.be.an('object')\n hopp.expect(response).to.have.property('data')\n \n const body = JSON.parse(response.data)\n hopp.expect(body).to.have.property('testData')\n hopp.expect(body.testData).to.have.property('number', 42)\n})\n",
+ "auth": {
+ "authType": "inherit",
+ "authActive": true
+ },
+ "body": {
+ "contentType": "application/json",
+ "body": "{\n \"testData\": {\n \"number\": 42,\n \"string\": \"hello world\",\n \"array\": [1, 2, 3, 4, 5],\n \"object\": { \"nested\": { \"value\": true } },\n \"bool\": true,\n \"nullValue\": null\n }\n}"
+ },
+ "requestVariables": [],
+ "responses": {}
+ },
+ {
+ "v": "15",
+ "id": "cmfhzf0op00chai2qt0inext02",
+ "name": "chai-assertions-pm-parity",
+ "method": "POST",
+ "endpoint": "https://echo.hoppscotch.io",
+ "params": [],
+ "headers": [],
+ "preRequestScript": "export {};\n",
+ "testScript": "export {};\n\npm.test('PM Chai - equality assertions', () => {\n pm.expect(5).to.equal(5)\n pm.expect('test').to.not.equal('other')\n pm.expect({ a: 1 }).to.eql({ a: 1 })\n})\n\npm.test('PM Chai - type assertions', () => {\n pm.expect('string').to.be.a('string')\n pm.expect(42).to.be.a('number')\n pm.expect([]).to.be.an('array')\n})\n\npm.test('PM Chai - truthiness assertions', () => {\n pm.expect(true).to.be.true\n pm.expect(false).to.be.false\n pm.expect(null).to.be.null\n})\n\npm.test('PM Chai - numerical comparisons', () => {\n pm.expect(10).to.be.above(5)\n pm.expect(5).to.be.below(10)\n pm.expect(7).to.be.within(5, 10)\n})\n\npm.test('PM Chai - property and length assertions', () => {\n const obj = { name: 'test' }\n pm.expect(obj).to.have.property('name')\n pm.expect([1, 2, 3]).to.have.lengthOf(3)\n pm.expect('hello').to.have.lengthOf(5)\n})\n\npm.test('PM Chai - string and collection assertions', () => {\n pm.expect('hello world').to.include('world')\n pm.expect([1, 2, 3]).to.include(2)\n pm.expect({ a: 1, b: 2 }).to.have.keys('a', 'b')\n})\n\npm.test('PM Chai - function assertions', () => {\n const throwFn = () => { throw new Error('test') }\n pm.expect(throwFn).to.throw()\n pm.expect([]).to.respondTo('push')\n})\n\npm.test('PM Chai - response validation', () => {\n pm.expect(pm.response.code).to.equal(200)\n pm.expect(pm.response.responseTime).to.be.a('number')\n \n const response = pm.response.json()\n pm.expect(response).to.have.property('data')\n \n const body = JSON.parse(response.data)\n pm.expect(body).to.have.property('pmTest')\n})\n",
+ "auth": {
+ "authType": "inherit",
+ "authActive": true
+ },
+ "body": {
+ "contentType": "application/json",
+ "body": "{\n \"pmTest\": {\n \"value\": 42,\n \"text\": \"postman compatible\"\n }\n}"
+ },
+ "requestVariables": [],
+ "responses": {}
+ },
+ {
+ "v": "15",
+ "id": "cmfhzf0op00cookies01",
+ "name": "cookie-assertions-test",
+ "method": "GET",
+ "endpoint": "https://echo.hoppscotch.io",
+ "params": [],
+ "headers": [
+ {
+ "key": "Cookie",
+ "value": "session_id=abc123; user_token=xyz789; preferences=theme%3Ddark",
+ "active": true,
+ "description": "Test cookies"
+ }
+ ],
+ "preRequestScript": "export {};\n",
+ "testScript": "export {};\n\n// NOTE: Full cookie behavior with Set-Cookie headers is tested in js-sandbox unit tests\n// (see packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/response/cookies.spec.ts)\n// These CLI E2E tests verify API contracts and integration behavior\n\npm.test('pm.response.cookies API contract - all methods exist', () => {\n pm.expect(pm.response.cookies).to.be.an('object')\n pm.expect(typeof pm.response.cookies.get).to.equal('function')\n pm.expect(typeof pm.response.cookies.has).to.equal('function')\n pm.expect(typeof pm.response.cookies.toObject).to.equal('function')\n})\n\npm.test('pm.response.cookies.toObject() returns proper structure', () => {\n const allCookies = pm.response.cookies.toObject()\n pm.expect(allCookies).to.be.an('object')\n pm.expect(typeof allCookies).to.equal('object')\n})\n\npm.test('pm.response.cookies.has() returns boolean for cookie checks', () => {\n const hasCookie = pm.response.cookies.has('test_cookie_name')\n pm.expect(hasCookie).to.be.a('boolean')\n})\n\npm.test('pm.response.cookies.get() returns null for non-existent cookies', () => {\n const cookieValue = pm.response.cookies.get('non_existent_cookie_xyz')\n pm.expect(cookieValue).to.be.null\n})\n\npm.test('pm.response.cookies API integrates with response object', () => {\n pm.expect(pm.response.code).to.equal(200)\n \n // Verify cookies object is accessible from response\n pm.expect(pm.response).to.have.property('cookies')\n pm.expect(pm.response.cookies).to.not.be.null\n pm.expect(pm.response.cookies).to.not.be.undefined\n})\n\npm.test('Request cookies are properly sent via Cookie header', () => {\n const hasCookieHeader = pm.request.headers.has('Cookie')\n \n if (hasCookieHeader) {\n const cookieHeader = pm.request.headers.get('Cookie')\n pm.expect(cookieHeader).to.be.a('string')\n pm.expect(cookieHeader).to.include('session_id')\n pm.expect(cookieHeader).to.include('user_token')\n }\n})\n\npm.test('pm.response.to.have.cookie() assertion method exists', () => {\n // Verify the cookie assertion is defined in the type system\n pm.expect(typeof pm.response.to.have.cookie).to.equal('function')\n})\n\nhopp.test('hopp.cookies API contract matches pm.response.cookies', () => {\n hopp.expect(typeof hopp.cookies).toBe('object')\n hopp.expect(typeof hopp.cookies.get).toBe('function')\n hopp.expect(typeof hopp.cookies.has).toBe('function')\n hopp.expect(typeof hopp.cookies.getAll).toBe('function')\n hopp.expect(typeof hopp.cookies.set).toBe('function')\n})\n",
+ "auth": {
+ "authType": "inherit",
+ "authActive": true
+ },
+ "body": {
+ "contentType": null,
+ "body": null
+ },
+ "requestVariables": [],
+ "responses": {}
+ },
+ {
+ "v": "15",
+ "id": "cmfhzf0op00schema01",
+ "name": "json-schema-validation-test",
+ "method": "POST",
+ "endpoint": "https://echo.hoppscotch.io",
+ "params": [],
+ "headers": [],
+ "preRequestScript": "export {};\n",
+ "testScript": "export {};\n\npm.test('pm.response.to.have.jsonSchema() validates response structure', () => {\n const schema = {\n type: 'object',\n required: ['data'],\n properties: {\n data: { type: 'string' },\n headers: { type: 'object' }\n }\n }\n \n pm.response.to.have.jsonSchema(schema)\n \n // Explicit assertions to ensure schema validation passed\n const json = pm.response.json()\n pm.expect(json).to.have.property('data')\n pm.expect(json.data).to.be.a('string')\n})\n\npm.test('JSON Schema validation with nested properties', () => {\n const response = pm.response.json()\n const body = JSON.parse(response.data)\n \n const userSchema = {\n type: 'object',\n required: ['name', 'age'],\n properties: {\n name: { type: 'string' },\n age: { type: 'number', minimum: 0, maximum: 150 },\n email: { type: 'string' }\n }\n }\n \n pm.expect(body).to.have.jsonSchema(userSchema)\n \n // Explicit assertions to ensure schema validation passed\n pm.expect(body).to.have.property('name')\n pm.expect(body).to.have.property('age')\n pm.expect(body.name).to.equal('Alice Smith')\n pm.expect(body.age).to.equal(28)\n})\n",
+ "auth": {
+ "authType": "inherit",
+ "authActive": true
+ },
+ "body": {
+ "contentType": "application/json",
+ "body": "{\n \"name\": \"Alice Smith\",\n \"age\": 28,\n \"email\": \"alice@example.com\"\n}"
+ },
+ "requestVariables": [],
+ "responses": {}
+ },
+ {
+ "v": "15",
+ "id": "cmfhzf0op00charset01",
+ "name": "charset-validation-test",
+ "method": "GET",
+ "endpoint": "https://echo.hoppscotch.io",
+ "params": [],
+ "headers": [],
+ "preRequestScript": "export {};\n",
+ "testScript": "export {};\n\n// NOTE: Full charset behavior with actual charset values is tested in js-sandbox unit tests\n// (see packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/advanced-assertions.spec.ts)\n// These CLI E2E tests verify API contracts and header parsing behavior\n\npm.test('pm.expect().to.have.charset() assertion API contract exists', () => {\n const testString = 'test'\n pm.expect(typeof pm.expect(testString).to.have.charset).to.equal('function')\n})\n\npm.test('pm.response.to.have.charset() assertion API contract exists', () => {\n pm.expect(typeof pm.response.to.have.charset).to.equal('function')\n})\n\npm.test('Response Content-Type header is accessible and parseable', () => {\n const contentType = pm.response.headers.get('content-type')\n pm.expect(contentType).to.be.a('string')\n pm.expect(contentType.length).to.be.above(0)\n pm.expect(contentType).to.include('application/')\n})\n\npm.test('Content-Type header parsing logic validates structure', () => {\n const contentType = pm.response.headers.get('content-type')\n \n // Test charset detection logic\n const hasCharset = contentType.includes('charset=')\n pm.expect(typeof hasCharset).to.equal('boolean')\n \n // Test charset extraction pattern\n const charsetMatch = contentType.match(/charset=([^;\\s]+)/i)\n if (hasCharset) {\n pm.expect(charsetMatch).to.be.an('array')\n pm.expect(charsetMatch[1]).to.be.a('string')\n } else {\n pm.expect(charsetMatch).to.be.null\n }\n})\n\npm.test('Charset handling works with or without explicit charset', () => {\n const contentType = pm.response.headers.get('content-type')\n const hasExplicitCharset = contentType.toLowerCase().includes('charset=')\n \n // Whether charset is present or not, response decoding should work\n const responseText = pm.response.text()\n pm.expect(responseText).to.be.a('string')\n pm.expect(responseText.length).to.be.above(0)\n})\n\npm.test('Response text decoding works with UTF-8 default', () => {\n const responseText = pm.response.text()\n pm.expect(responseText).to.be.a('string')\n \n // Verify JSON parsing works (implies correct encoding)\n const responseJson = pm.response.json()\n pm.expect(responseJson).to.be.an('object')\n pm.expect(responseJson).to.have.property('data')\n})\n\npm.test('Response headers integrate correctly with charset assertions', () => {\n const allHeaders = pm.response.headers.all()\n pm.expect(allHeaders).to.be.an('object')\n pm.expect(Object.keys(allHeaders).length).to.be.above(0)\n})\n\nhopp.test('hopp namespace handles response encoding with proper defaults', () => {\n const textResponse = hopp.response.body.asText()\n hopp.expect(textResponse).toBeType('string')\n hopp.expect(textResponse.length > 0).toBe(true)\n \n // Verify JSON parsing works with default encoding\n const jsonResponse = hopp.response.body.asJSON()\n hopp.expect(jsonResponse).toBeType('object')\n})\n",
+ "auth": {
+ "authType": "inherit",
+ "authActive": true
+ },
+ "body": {
+ "contentType": null,
+ "body": null
+ },
+ "requestVariables": [],
+ "responses": {}
+ },
+ {
+ "v": "15",
+ "id": "cmfhzf0op00jsonpath01",
+ "name": "jsonpath-query-test",
+ "method": "POST",
+ "endpoint": "https://echo.hoppscotch.io",
+ "params": [],
+ "headers": [],
+ "preRequestScript": "export {};\n",
+ "testScript": "export {};\n\npm.test('pm.response.to.have.jsonPath() queries nested JSON data', () => {\n const response = pm.response.json()\n const body = JSON.parse(response.data)\n \n pm.expect(body).to.have.jsonPath('$.users[0].name')\n pm.expect(body).to.have.jsonPath('$.users[*].active')\n pm.expect(body).to.have.jsonPath('$.metadata.version')\n})\n\npm.test('JSONPath with value validation', () => {\n const response = pm.response.json()\n const body = JSON.parse(response.data)\n \n pm.expect(body).to.have.jsonPath('$.users[0].name', 'John')\n pm.expect(body).to.have.jsonPath('$.metadata.version', '1.0')\n})\n",
+ "auth": {
+ "authType": "inherit",
+ "authActive": true
+ },
+ "body": {
+ "contentType": "application/json",
+ "body": "{\n \"users\": [\n { \"name\": \"John\", \"active\": true },\n { \"name\": \"Jane\", \"active\": false }\n ],\n \"metadata\": {\n \"version\": \"1.0\",\n \"timestamp\": \"2025-01-15\"\n }\n}"
+ },
+ "requestVariables": [],
+ "responses": {}
+ },
+ {
+ "v": "15",
+ "id": "cmfhzf0op00envext01",
+ "name": "environment-extensions-test",
+ "method": "GET",
+ "endpoint": "https://echo.hoppscotch.io",
+ "params": [],
+ "headers": [],
+ "preRequestScript": "export {};\npm.environment.set('template_var', 'world')\npm.environment.set('user_id', '12345')\npm.globals.set('api_base', 'https://api.example.com')\npm.globals.set('version', 'v2')\n",
+ "testScript": "export {};\n\npm.test('pm.environment.name returns environment identifier', () => {\n pm.expect(pm.environment.name).to.be.a('string')\n pm.expect(pm.environment.name).to.equal('active')\n})\n\npm.test('pm.environment.replaceIn() resolves template variables', () => {\n const template = 'Hello {{template_var}}, user {{user_id}}!'\n const resolved = pm.environment.replaceIn(template)\n pm.expect(resolved).to.equal('Hello world, user 12345!')\n})\n\npm.test('pm.globals.replaceIn() resolves global template variables', () => {\n const template = '{{api_base}}/{{version}}/users'\n const resolved = pm.globals.replaceIn(template)\n pm.expect(resolved).to.equal('https://api.example.com/v2/users')\n})\n\npm.test('pm.environment.toObject() returns all environment variables', () => {\n const allVars = pm.environment.toObject()\n pm.expect(allVars).to.be.an('object')\n pm.expect(allVars).to.have.property('template_var', 'world')\n pm.expect(allVars).to.have.property('user_id', '12345')\n})\n\npm.test('pm.globals.toObject() returns all global variables', () => {\n const allGlobals = pm.globals.toObject()\n pm.expect(allGlobals).to.be.an('object')\n \n // globals might be empty in CLI context\n if (Object.keys(allGlobals).length > 0) {\n pm.expect(allGlobals).to.have.property('api_base')\n }\n})\n\npm.test('pm.variables.toObject() returns combined variables with precedence', () => {\n const allVariables = pm.variables.toObject()\n pm.expect(allVariables).to.be.an('object')\n pm.expect(allVariables).to.have.property('template_var')\n})\n\npm.test('pm.environment.clear() removes all environment variables', () => {\n pm.environment.clear()\n const clearedVars = pm.environment.toObject()\n pm.expect(Object.keys(clearedVars).length).to.equal(0)\n})\n",
+ "auth": {
+ "authType": "inherit",
+ "authActive": true
+ },
+ "body": {
+ "contentType": null,
+ "body": null
+ },
+ "requestVariables": [],
+ "responses": {}
+ },
+ {
+ "v": "15",
+ "id": "cmfhzf0op00respext01",
+ "name": "response-extensions-test",
+ "method": "POST",
+ "endpoint": "https://echo.hoppscotch.io",
+ "params": [],
+ "headers": [],
+ "preRequestScript": "export {};\n",
+ "testScript": "export {};\n\npm.test('pm.response.responseSize returns response body size in bytes', () => {\n pm.expect(pm.response.responseSize).to.be.a('number')\n pm.expect(pm.response.responseSize).to.be.above(0)\n})\n\npm.test('pm.response.responseSize matches actual body length', () => {\n const bodyText = pm.response.text()\n // Use the same workaround as pm.response.responseSize for QuickJS\n const encoder = new TextEncoder()\n const encoded = encoder.encode(bodyText)\n // QuickJS represents Uint8Array as object with numeric keys\n const actualSize = encoded && typeof encoded.length === 'number' && encoded.length > 0\n ? encoded.length\n : Object.keys(encoded).filter(k => !isNaN(k)).length\n pm.expect(pm.response.responseSize).to.equal(actualSize)\n})\n\npm.test('Response size is calculated correctly for JSON payload', () => {\n const response = pm.response.json()\n pm.expect(pm.response.responseSize).to.be.a('number')\n})\n",
+ "auth": {
+ "authType": "inherit",
+ "authActive": true
+ },
+ "body": {
+ "contentType": "application/json",
+ "body": "{\n \"message\": \"Testing response size calculation\",\n \"data\": {\n \"items\": [1, 2, 3, 4, 5],\n \"metadata\": {\n \"count\": 5,\n \"type\": \"numeric\"\n }\n }\n}"
+ },
+ "requestVariables": [],
+ "responses": {}
+ },
+ {
+ "v": "15",
+ "id": "cmfhzf0op00execext01",
+ "name": "execution-context-test",
+ "method": "GET",
+ "endpoint": "https://echo.hoppscotch.io",
+ "params": [],
+ "headers": [],
+ "preRequestScript": "export {};\n",
+ "testScript": "export {};\n\npm.test('pm.execution.location provides execution path', () => {\n pm.expect(pm.execution.location).to.be.an('array')\n pm.expect(pm.execution.location.length).to.be.above(0)\n})\n\npm.test('pm.execution.location.current returns current location', () => {\n pm.expect(pm.execution.location.current).to.be.a('string')\n pm.expect(pm.execution.location.current).to.equal('Hoppscotch')\n})\n\npm.test('pm.execution.location is immutable', () => {\n const location = pm.execution.location\n const throwFn = () => { location.push('test') }\n pm.expect(throwFn).to.throw()\n})\n",
+ "auth": {
+ "authType": "inherit",
+ "authActive": true
+ },
+ "body": {
+ "contentType": null,
+ "body": null
+ },
+ "requestVariables": [],
+ "responses": {}
+ },
+ {
+ "v": "15",
+ "id": "cmfhzf0op00bddassert01",
+ "name": "bdd-response-assertions-test",
+ "method": "POST",
+ "endpoint": "https://echo.hoppscotch.io",
+ "params": [],
+ "headers": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "active": true,
+ "description": ""
+ }
+ ],
+ "preRequestScript": "export {};\n",
+ "testScript": "export {};\n\npm.test('pm.response.to.have.status() validates exact status code', () => {\n pm.response.to.have.status(200)\n pm.expect(pm.response.code).to.equal(200)\n})\n\npm.test('pm.response.to.be.ok validates 2xx status codes', () => {\n pm.response.to.be.ok()\n})\n\npm.test('pm.response.to.be.success validates 2xx status codes (alias)', () => {\n pm.response.to.be.success()\n})\n\npm.test('pm.response.to.have.header() validates response headers', () => {\n pm.response.to.have.header('content-type')\n pm.expect(pm.response.headers.has('content-type')).to.be.true\n})\n\npm.test('pm.response.to.have.jsonBody() validates JSON response', () => {\n pm.response.to.have.jsonBody()\n pm.response.to.have.jsonBody('data')\n \n const json = pm.response.json()\n pm.expect(json).to.have.property('data')\n})\n\npm.test('pm.response.to.be.json validates JSON content type', () => {\n pm.response.to.be.json()\n})\n\npm.test('pm.response.to.have.responseTime assertions', () => {\n pm.response.to.have.responseTime.below(5000)\n pm.expect(pm.response.responseTime).to.be.a('number')\n pm.expect(pm.response.responseTime).to.be.above(0)\n})\n",
+ "auth": {
+ "authType": "inherit",
+ "authActive": true
+ },
+ "body": {
+ "contentType": "application/json",
+ "body": "{\n \"test\": \"BDD assertions\",\n \"status\": \"success\"\n}"
+ },
+ "requestVariables": [],
+ "responses": {}
+ },
+ {
+ "v": "15",
+ "id": "cmfhzf0op00includecontain01",
+ "name": "include-contain-assertions-test",
+ "method": "POST",
+ "endpoint": "https://echo.hoppscotch.io",
+ "params": [],
+ "headers": [],
+ "preRequestScript": "export {};\n",
+ "testScript": "export {};\n\npm.test('pm.expect().to.include() validates string inclusion', () => {\n pm.expect('hello world').to.include('world')\n pm.expect('hello world').to.include('hello')\n pm.expect('test string').to.not.include('missing')\n})\n\npm.test('pm.expect().to.contain() validates array inclusion', () => {\n pm.expect([1, 2, 3]).to.contain(2)\n pm.expect([1, 2, 3]).to.include(1)\n pm.expect(['a', 'b', 'c']).to.contain('b')\n})\n\npm.test('pm.expect().to.includes() alias works', () => {\n pm.expect('testing').to.includes('test')\n pm.expect([10, 20, 30]).to.includes(20)\n})\n\npm.test('pm.expect().to.contains() alias works', () => {\n pm.expect('contains test').to.contains('contains')\n pm.expect([true, false]).to.contains(true)\n})\n\npm.test('include/contain with response data', () => {\n const response = pm.response.json()\n pm.expect(response).to.have.property('data')\n \n const bodyText = pm.response.text()\n pm.expect(bodyText).to.include('includeTest')\n})\n\nhopp.test('hopp.expect() also supports toInclude()', () => {\n hopp.expect('hopp test').toInclude('hopp')\n hopp.expect([1, 2]).toInclude(1)\n})\n",
+ "auth": {
+ "authType": "inherit",
+ "authActive": true
+ },
+ "body": {
+ "contentType": "application/json",
+ "body": "{\n \"includeTest\": \"This text should be found\",\n \"array\": [1, 2, 3]\n}"
+ },
+ "requestVariables": [],
+ "responses": {}
+ },
+ {
+ "v": "15",
+ "id": "cmfhzf0op00envunsetclear01",
+ "name": "environment-unset-clear-test",
+ "method": "GET",
+ "endpoint": "https://echo.hoppscotch.io",
+ "params": [],
+ "headers": [],
+ "preRequestScript": "export {};\npm.environment.set('to_unset1', 'value1')\npm.environment.set('to_unset2', 'value2')\npm.environment.set('to_clear1', 'clear_value1')\npm.environment.set('to_clear2', 'clear_value2')\npm.environment.set('to_clear3', 'clear_value3')\npm.globals.set('global_to_unset', 'global_value')\npm.globals.set('global_to_clear1', 'global_clear1')\npm.globals.set('global_to_clear2', 'global_clear2')\n",
+ "testScript": "export {};\n\npm.test('pm.environment.unset() removes specific variables', () => {\n pm.expect(pm.environment.has('to_unset1')).to.be.true\n pm.environment.unset('to_unset1')\n pm.expect(pm.environment.has('to_unset1')).to.be.false\n pm.expect(pm.environment.get('to_unset1')).to.be.undefined\n})\n\npm.test('pm.environment.unset() handles non-existent keys gracefully', () => {\n pm.environment.unset('non_existent_key')\n pm.expect(pm.environment.has('non_existent_key')).to.be.false\n})\n\npm.test('pm.globals.unset() removes specific global variables', () => {\n const hasGlobal = pm.globals.has('global_to_unset')\n if (hasGlobal) {\n pm.globals.unset('global_to_unset')\n pm.expect(pm.globals.has('global_to_unset')).to.be.false\n }\n})\n\npm.test('pm.environment.clear() removes ALL environment variables', () => {\n // Verify variables exist before clear\n pm.expect(pm.environment.has('to_clear1')).to.be.true\n pm.expect(pm.environment.has('to_clear2')).to.be.true\n pm.expect(pm.environment.has('to_clear3')).to.be.true\n \n // Clear all environment variables\n pm.environment.clear()\n \n // Verify ALL variables are removed\n const allVars = pm.environment.toObject()\n pm.expect(Object.keys(allVars).length).to.equal(0)\n pm.expect(pm.environment.has('to_clear1')).to.be.false\n pm.expect(pm.environment.has('to_clear2')).to.be.false\n pm.expect(pm.environment.has('to_clear3')).to.be.false\n})\n\npm.test('pm.globals.clear() removes ALL global variables', () => {\n // Verify globals exist before clear (might be empty in CLI)\n const globalsBeforeClear = pm.globals.toObject()\n \n pm.globals.clear()\n \n // Verify all globals are removed\n const globalsAfterClear = pm.globals.toObject()\n pm.expect(Object.keys(globalsAfterClear).length).to.equal(0)\n})\n",
+ "auth": {
+ "authType": "inherit",
+ "authActive": true
+ },
+ "body": {
+ "contentType": null,
+ "body": null
+ },
+ "requestVariables": [],
+ "responses": {}
+ },
+ {
+ "v": "15",
+ "id": "cmfhzf0op00pmmutate01",
+ "name": "pm-request-mutation-test",
+ "method": "GET",
+ "endpoint": "https://echo.hoppscotch.io/original",
+ "params": [
+ {
+ "key": "original_param",
+ "value": "original",
+ "active": true,
+ "description": ""
+ }
+ ],
+ "headers": [
+ {
+ "key": "Original-Header",
+ "value": "original",
+ "active": true,
+ "description": ""
+ }
+ ],
+ "preRequestScript": "export {};\n// Test PM namespace mutability - URL string assignment\npm.request.url = 'https://echo.hoppscotch.io/mutated-via-string'\n\n// Test method mutation\npm.request.method = 'POST'\n\n// Test header mutations\npm.request.headers.add({ key: 'Added-Header', value: 'added-value' })\npm.request.headers.upsert({ key: 'Original-Header', value: 'mutated-value' })\n\n// Test body mutation via update()\npm.request.body.update({\n mode: 'raw',\n raw: JSON.stringify({ pmMutated: true, timestamp: Date.now() }),\n options: { raw: { language: 'json' } }\n})\n\n// Test auth mutation\npm.request.auth = {\n authType: 'bearer',\n authActive: true,\n token: 'pm-bearer-token-123'\n}\n",
+ "testScript": "export {};\n\npm.test('pm.request.url string assignment was applied', () => {\n const urlString = pm.request.url.toString()\n pm.expect(urlString).to.include('/mutated-via-string')\n pm.expect(urlString).to.not.include('/original')\n})\n\npm.test('pm.request.method mutation was applied', () => {\n pm.expect(pm.request.method).to.equal('POST')\n pm.expect(pm.request.method).to.not.equal('GET')\n})\n\npm.test('pm.request.headers.add() added new header', () => {\n pm.expect(pm.request.headers.has('Added-Header')).to.be.true\n pm.expect(pm.request.headers.get('Added-Header')).to.equal('added-value')\n})\n\npm.test('pm.request.headers.upsert() updated existing header', () => {\n pm.expect(pm.request.headers.has('Original-Header')).to.be.true\n pm.expect(pm.request.headers.get('Original-Header')).to.equal('mutated-value')\n pm.expect(pm.request.headers.get('Original-Header')).to.not.equal('original')\n})\n\npm.test('pm.request.body.update() changed body content', () => {\n pm.expect(pm.request.body.contentType).to.equal('application/json')\n const bodyString = typeof pm.request.body.body === 'string' \n ? pm.request.body.body \n : JSON.stringify(pm.request.body.body)\n pm.expect(bodyString).to.include('pmMutated')\n const bodyData = JSON.parse(bodyString)\n pm.expect(bodyData.pmMutated).to.be.true\n})\n\npm.test('pm.request.auth mutation was applied', () => {\n pm.expect(pm.request.auth.authType).to.equal('bearer')\n pm.expect(pm.request.auth.token).to.equal('pm-bearer-token-123')\n})\n\npm.test('pm.request.id and pm.request.name are accessible', () => {\n pm.expect(pm.request.id).to.be.a('string')\n pm.expect(pm.request.id.length).to.be.above(0)\n pm.expect(pm.request.name).to.equal('pm-request-mutation-test')\n})\n\nhopp.test('hopp.request reflects pm namespace mutations', () => {\n hopp.expect(hopp.request.url).toInclude('/mutated-via-string')\n hopp.expect(hopp.request.method).toBe('POST')\n const hasAddedHeader = hopp.request.headers.some(h => h.key === 'Added-Header')\n hopp.expect(hasAddedHeader).toBe(true)\n})\n",
+ "auth": {
+ "authType": "none",
+ "authActive": true
+ },
+ "body": {
+ "contentType": null,
+ "body": null
+ },
+ "requestVariables": [],
+ "responses": {}
+ },
+ {
+ "v": "15",
+ "id": "cmfhzf0op00urlmutate01",
+ "name": "pm-url-property-mutations-test",
+ "method": "GET",
+ "endpoint": "https://echo.hoppscotch.io/original?old=value",
+ "params": [],
+ "headers": [],
+ "preRequestScript": "export {};\n// Test URL object property mutations\npm.request.url.protocol = 'http'\npm.request.url.host = ['echo', 'hoppscotch', 'io']\npm.request.url.path = ['v2', 'test']\npm.request.url.port = '443'\npm.request.url.query.add({ key: 'new', value: 'param' })\npm.request.url.query.remove('old')\n",
+ "testScript": "export {};\n\npm.test('URL protocol mutation works', () => {\n const url = pm.request.url\n pm.expect(url.protocol).to.equal('http')\n pm.expect(url.toString()).to.include('http://')\n})\n\npm.test('URL host mutation works', () => {\n const url = pm.request.url\n pm.expect(url.host).to.be.an('array')\n pm.expect(url.host.join('.')).to.equal('echo.hoppscotch.io')\n pm.expect(url.toString()).to.include('echo.hoppscotch.io')\n})\n\npm.test('URL path mutation works', () => {\n const url = pm.request.url\n pm.expect(url.path).to.be.an('array')\n pm.expect(url.path).to.include('v2')\n pm.expect(url.path).to.include('test')\n pm.expect(url.toString()).to.include('/v2/test')\n})\n\npm.test('URL query.add() adds parameter', () => {\n const allParams = pm.request.url.query.all()\n pm.expect(allParams).to.have.property('new', 'param')\n})\n\npm.test('URL query.remove() removes parameter', () => {\n const allParams = pm.request.url.query.all()\n pm.expect(allParams).to.not.have.property('old')\n})\n\npm.test('Full URL reflects all mutations', () => {\n const fullUrl = pm.request.url.toString()\n pm.expect(fullUrl).to.include('http://')\n pm.expect(fullUrl).to.include('echo.hoppscotch.io')\n pm.expect(fullUrl).to.include('/v2/test')\n pm.expect(fullUrl).to.include('new=param')\n pm.expect(fullUrl).to.not.include('old=value')\n})\n\nhopp.test('hopp.request reflects URL mutations', () => {\n hopp.expect(hopp.request.url).toInclude('echo.hoppscotch.io')\n hopp.expect(hopp.request.url).toInclude('/v2/test')\n})\n",
+ "auth": {
+ "authType": "inherit",
+ "authActive": true
+ },
+ "body": {
+ "contentType": null,
+ "body": null
+ },
+ "requestVariables": [],
+ "responses": {}
+ },
+ {
+ "v": "15",
+ "id": "cmfhzf0op00unsupported01",
+ "name": "unsupported-features-test",
+ "method": "GET",
+ "endpoint": "https://echo.hoppscotch.io",
+ "params": [],
+ "headers": [],
+ "preRequestScript": "export {};\n",
+ "testScript": "export {};\n\npm.test('pm.require() throws descriptive error', () => {\n const throwFn = () => pm.require('lodash')\n pm.expect(throwFn).to.throw()\n pm.expect(throwFn).to.throw(/not supported in Hoppscotch/)\n})\n\npm.test('pm.execution.runRequest() throws descriptive error', () => {\n const throwFn = () => pm.execution.runRequest()\n pm.expect(throwFn).to.throw()\n pm.expect(throwFn).to.throw(/Collection Runner feature/)\n})\n\npm.test('pm.collectionVariables.replaceIn() throws descriptive error', () => {\n const throwFn = () => pm.collectionVariables.replaceIn('{{test}}')\n pm.expect(throwFn).to.throw()\n pm.expect(throwFn).to.throw(/Workspace feature/)\n})\n\npm.test('pm.iterationData.toJSON() throws descriptive error', () => {\n const throwFn = () => pm.iterationData.toJSON()\n pm.expect(throwFn).to.throw()\n pm.expect(throwFn).to.throw(/Collection Runner feature/)\n})\n",
+ "auth": {
+ "authType": "inherit",
+ "authActive": true
+ },
+ "body": {
+ "contentType": null,
+ "body": null
+ },
+ "requestVariables": [],
+ "responses": {}
+ },
+ {
+ "v": "15",
+ "id": "cmfhzf0op00urlpropertylist01",
+ "name": "url-propertylist-helpers-test",
+ "method": "GET",
+ "endpoint": "https://api.example.com:8080/v1/users?filter=active&sort=name&tag=js&tag=ts#section1",
+ "params": [
+ {
+ "key": "filter",
+ "value": "active",
+ "active": true,
+ "description": ""
+ },
+ {
+ "key": "sort",
+ "value": "name",
+ "active": true,
+ "description": ""
+ },
+ {
+ "key": "tag",
+ "value": "js",
+ "active": true,
+ "description": ""
+ },
+ {
+ "key": "tag",
+ "value": "ts",
+ "active": true,
+ "description": ""
+ }
+ ],
+ "headers": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "active": true,
+ "description": ""
+ },
+ {
+ "key": "Authorization",
+ "value": "Bearer test-token",
+ "active": true,
+ "description": ""
+ }
+ ],
+ "preRequestScript": "export {};\n// Test URL helper methods\npm.request.url.update('https://echo.hoppscotch.io/updated?test=value')\npm.request.url.addQueryParams([{ key: 'page', value: '1' }, { key: 'limit', value: '20' }])\npm.request.url.removeQueryParams('test')\n\n// Test hostname and hash properties\npm.request.url.hostname = 'echo.hoppscotch.io'\npm.request.url.hash = 'results'\n\n// Test query PropertyList methods\npm.request.url.query.upsert({ key: 'status', value: 'published' })\npm.request.url.query.add({ key: 'include', value: 'metadata' })\n",
+ "testScript": "export {};\n\npm.test('URL helper methods - getHost() returns hostname as string', () => {\n const host = pm.request.url.getHost()\n pm.expect(host).to.be.a('string')\n pm.expect(host).to.equal('echo.hoppscotch.io')\n})\n\npm.test('URL helper methods - getPath() returns path with leading slash', () => {\n const path = pm.request.url.getPath()\n pm.expect(path).to.be.a('string')\n pm.expect(path).to.include('/')\n pm.expect(path).to.equal('/updated')\n})\n\npm.test('URL helper methods - getPathWithQuery() includes query string', () => {\n const pathWithQuery = pm.request.url.getPathWithQuery()\n pm.expect(pathWithQuery).to.include('?')\n pm.expect(pathWithQuery).to.include('page=1')\n pm.expect(pathWithQuery).to.include('limit=20')\n})\n\npm.test('URL helper methods - getQueryString() returns query without ?', () => {\n const queryString = pm.request.url.getQueryString()\n pm.expect(queryString).to.be.a('string')\n pm.expect(queryString).to.not.include('?')\n pm.expect(queryString).to.include('page=1')\n})\n\npm.test('URL helper methods - getRemote() returns host with port', () => {\n const remote = pm.request.url.getRemote()\n pm.expect(remote).to.be.a('string')\n pm.expect(remote).to.equal('echo.hoppscotch.io')\n})\n\npm.test('URL helper methods - update() changes entire URL', () => {\n const url = pm.request.url.toString()\n pm.expect(url).to.include('echo.hoppscotch.io')\n pm.expect(url).to.include('/updated')\n})\n\npm.test('URL helper methods - addQueryParams() adds multiple params', () => {\n const allParams = pm.request.url.query.all()\n pm.expect(allParams).to.have.property('page', '1')\n pm.expect(allParams).to.have.property('limit', '20')\n})\n\npm.test('URL helper methods - removeQueryParams() removes params', () => {\n const allParams = pm.request.url.query.all()\n pm.expect(allParams).to.not.have.property('test')\n})\n\npm.test('URL properties - hostname getter returns string', () => {\n const hostname = pm.request.url.hostname\n pm.expect(hostname).to.be.a('string')\n pm.expect(hostname).to.equal('echo.hoppscotch.io')\n})\n\npm.test('URL properties - hostname matches host array', () => {\n const hostname = pm.request.url.hostname\n const hostString = pm.request.url.host.join('.')\n pm.expect(hostname).to.equal(hostString)\n})\n\npm.test('URL properties - hash getter returns string', () => {\n const hash = pm.request.url.hash\n pm.expect(hash).to.be.a('string')\n // Hash might not persist through URL mutations in E2E context\n})\n\npm.test('Query PropertyList - get() retrieves parameter value', () => {\n const pageValue = pm.request.url.query.get('page')\n pm.expect(pageValue).to.equal('1')\n})\n\npm.test('Query PropertyList - has() checks parameter existence', () => {\n pm.expect(pm.request.url.query.has('page')).to.be.true\n pm.expect(pm.request.url.query.has('nonexistent')).to.be.false\n})\n\npm.test('Query PropertyList - upsert() adds/updates parameter', () => {\n pm.expect(pm.request.url.query.has('status')).to.be.true\n pm.expect(pm.request.url.query.get('status')).to.equal('published')\n})\n\npm.test('Query PropertyList - count() returns parameter count', () => {\n const count = pm.request.url.query.count()\n pm.expect(count).to.be.a('number')\n pm.expect(count).to.be.above(0)\n})\n\npm.test('Query PropertyList - each() iterates over parameters', () => {\n let iterationCount = 0\n pm.request.url.query.each((param) => {\n pm.expect(param).to.have.property('key')\n pm.expect(param).to.have.property('value')\n iterationCount++\n })\n pm.expect(iterationCount).to.be.above(0)\n})\n\npm.test('Query PropertyList - map() transforms parameters', () => {\n const keys = pm.request.url.query.map((param) => param.key)\n pm.expect(keys).to.be.an('array')\n pm.expect(keys).to.include('page')\n pm.expect(keys).to.include('limit')\n})\n\npm.test('Query PropertyList - filter() filters parameters', () => {\n const filtered = pm.request.url.query.filter((param) => param.key === 'page')\n pm.expect(filtered).to.be.an('array')\n pm.expect(filtered.length).to.be.above(0)\n pm.expect(filtered[0].key).to.equal('page')\n})\n\npm.test('Query PropertyList - idx() accesses by index', () => {\n const firstParam = pm.request.url.query.idx(0)\n pm.expect(firstParam).to.be.an('object')\n pm.expect(firstParam).to.have.property('key')\n pm.expect(firstParam).to.have.property('value')\n})\n\npm.test('Query PropertyList - idx() returns null for out of bounds', () => {\n const param = pm.request.url.query.idx(999)\n pm.expect(param).to.be.null\n})\n\npm.test('Query PropertyList - toObject() returns object', () => {\n const obj = pm.request.url.query.toObject()\n pm.expect(obj).to.be.an('object')\n pm.expect(obj).to.have.property('page')\n})\n\npm.test('Headers PropertyList - each() iterates over headers', () => {\n let count = 0\n pm.request.headers.each((header) => {\n pm.expect(header).to.have.property('key')\n pm.expect(header).to.have.property('value')\n count++\n })\n pm.expect(count).to.be.above(0)\n})\n\npm.test('Headers PropertyList - map() transforms headers', () => {\n const keys = pm.request.headers.map((h) => h.key)\n pm.expect(keys).to.be.an('array')\n pm.expect(keys).to.include('Content-Type')\n})\n\npm.test('Headers PropertyList - filter() filters headers', () => {\n const filtered = pm.request.headers.filter((h) => h.key === 'Content-Type')\n pm.expect(filtered).to.be.an('array')\n pm.expect(filtered.length).to.be.above(0)\n})\n\npm.test('Headers PropertyList - count() returns header count', () => {\n const count = pm.request.headers.count()\n pm.expect(count).to.be.a('number')\n pm.expect(count).to.be.above(0)\n})\n\npm.test('Headers PropertyList - idx() accesses by index', () => {\n const firstHeader = pm.request.headers.idx(0)\n pm.expect(firstHeader).to.be.an('object')\n pm.expect(firstHeader).to.have.property('key')\n})\n\npm.test('Headers PropertyList - toObject() returns object', () => {\n const obj = pm.request.headers.toObject()\n pm.expect(obj).to.be.an('object')\n pm.expect(obj).to.have.property('Content-Type')\n})\n\nhopp.test('hopp namespace URL methods work identically', () => {\n const url = hopp.request.url\n hopp.expect(url).toInclude('echo.hoppscotch.io')\n hopp.expect(url).toInclude('/updated')\n})\n",
+ "auth": {
+ "authType": "inherit",
+ "authActive": true
+ },
+ "body": {
+ "contentType": null,
+ "body": null
+ },
+ "requestVariables": [],
+ "responses": {}
+ },
+ {
+ "v": "15",
+ "name": "propertylist-advanced-methods-test",
+ "method": "GET",
+ "endpoint": "https://echo.hoppscotch.io/propertylist",
+ "params": [
+ {
+ "key": "filter",
+ "value": "active",
+ "active": true,
+ "description": ""
+ },
+ {
+ "key": "sort",
+ "value": "name",
+ "active": true,
+ "description": ""
+ },
+ {
+ "key": "page",
+ "value": "1",
+ "active": true,
+ "description": ""
+ }
+ ],
+ "headers": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "active": true,
+ "description": ""
+ },
+ {
+ "key": "Authorization",
+ "value": "Bearer token123",
+ "active": true,
+ "description": ""
+ }
+ ],
+ "preRequestScript": "export {};\n// Test query.insert() - insert limit before page\npm.request.url.query.insert({ key: 'limit', value: '10' }, 'page')\n\n// Test query.append() - add new param at end\npm.request.url.query.append({ key: 'offset', value: '0' })\n\n// Test query.assimilate() - merge params\npm.request.url.query.assimilate({ include: 'metadata', status: 'active' })\n\n// Test headers.insert() - insert before Authorization\npm.request.headers.insert({ key: 'X-API-Key', value: 'secret123' }, 'Authorization')\n\n// Test headers.append() - add at end\npm.request.headers.append({ key: 'X-Request-ID', value: 'req-456' })\n\n// Test headers.assimilate() - merge headers\npm.request.headers.assimilate({ 'X-Custom-Header': 'custom-value' })\n",
+ "testScript": "export {};\n\npm.test('query.find() - finds param by string key', () => {\n const limitParam = pm.request.url.query.find('limit')\n if (limitParam) {\n pm.expect(limitParam).to.be.an('object')\n pm.expect(limitParam.key).to.equal('limit')\n } else {\n pm.expect(pm.request.url.query.has('limit')).to.be.true\n }\n})\n\npm.test('query.find() - finds param by predicate function', () => {\n const limitParam = pm.request.url.query.find((p) => p && p.key === 'limit')\n if (limitParam) {\n pm.expect(limitParam).to.be.an('object')\n pm.expect(limitParam.value).to.equal('10')\n } else {\n pm.expect(pm.request.url.query.get('limit')).to.equal('10')\n }\n})\n\npm.test('query.find() - returns null when not found', () => {\n const result = pm.request.url.query.find('nonexistent')\n pm.expect(result).to.be.null\n})\n\npm.test('query.indexOf() - returns index for existing params', () => {\n // Verify indexOf works - check params that exist in actual URL\n const allParams = pm.request.url.query.all()\n const keys = Object.keys(allParams)\n if (keys.length > 0) {\n const firstKey = keys[0]\n const idx = pm.request.url.query.indexOf(firstKey)\n pm.expect(idx).to.be.a('number')\n pm.expect(idx).to.be.at.least(0)\n }\n})\n\npm.test('query.indexOf() - returns index by object', () => {\n const allParams = pm.request.url.query.all()\n const keys = Object.keys(allParams)\n if (keys.length > 0) {\n const idx = pm.request.url.query.indexOf({ key: keys[0] })\n pm.expect(idx).to.be.a('number')\n pm.expect(idx).to.be.at.least(0)\n }\n})\n\npm.test('query.indexOf() - returns -1 when not found', () => {\n const idx = pm.request.url.query.indexOf('notfound')\n pm.expect(idx).to.equal(-1)\n})\n\npm.test('query.insert/append/assimilate - methods executed successfully', () => {\n // Verify the methods executed without errors in pre-request\n // Post-request sees actual sent URL, so we just verify params exist\n const allParams = pm.request.url.query.all()\n pm.expect(allParams).to.be.an('object')\n pm.expect(pm.request.url.query.has('limit')).to.be.true\n pm.expect(pm.request.url.query.has('offset')).to.be.true\n})\n\npm.test('query.append() - adds param at end', () => {\n const offsetIdx = pm.request.url.query.indexOf('offset')\n pm.expect(offsetIdx).to.be.at.least(0)\n pm.expect(pm.request.url.query.get('offset')).to.equal('0')\n})\n\npm.test('query.assimilate() - adds/updates params', () => {\n pm.expect(pm.request.url.query.has('include')).to.be.true\n pm.expect(pm.request.url.query.get('include')).to.equal('metadata')\n pm.expect(pm.request.url.query.has('status')).to.be.true\n pm.expect(pm.request.url.query.get('status')).to.equal('active')\n})\n\npm.test('headers.find() - finds header by string (case-insensitive)', () => {\n const ct = pm.request.headers.find('content-type')\n pm.expect(ct).to.be.an('object')\n pm.expect(ct.key).to.equal('Content-Type')\n})\n\npm.test('headers.find() - finds header by predicate function', () => {\n const auth = pm.request.headers.find((h) => h.key === 'Authorization')\n pm.expect(auth).to.be.an('object')\n pm.expect(auth.value).to.include('Bearer')\n})\n\npm.test('headers.find() - returns null when not found', () => {\n const result = pm.request.headers.find('nonexistent')\n pm.expect(result).to.be.null\n})\n\npm.test('headers.indexOf() - returns correct index (case-insensitive)', () => {\n const authIdx = pm.request.headers.indexOf('authorization')\n pm.expect(authIdx).to.be.a('number')\n pm.expect(authIdx).to.be.at.least(0)\n})\n\npm.test('headers.indexOf() - returns correct index by object', () => {\n const ctIdx = pm.request.headers.indexOf({ key: 'Content-Type' })\n pm.expect(ctIdx).to.be.a('number')\n pm.expect(ctIdx).to.be.at.least(0)\n})\n\npm.test('headers.indexOf() - returns -1 when not found', () => {\n const idx = pm.request.headers.indexOf('NotFound')\n pm.expect(idx).to.equal(-1)\n})\n\npm.test('headers.insert() - inserts header before specified header', () => {\n const apiKeyIdx = pm.request.headers.indexOf('X-API-Key')\n const authIdx = pm.request.headers.indexOf('Authorization')\n pm.expect(apiKeyIdx).to.be.below(authIdx)\n})\n\npm.test('headers.append() - adds header at end', () => {\n pm.expect(pm.request.headers.has('X-Request-ID')).to.be.true\n pm.expect(pm.request.headers.get('X-Request-ID')).to.equal('req-456')\n})\n\npm.test('headers.assimilate() - adds/updates headers', () => {\n pm.expect(pm.request.headers.has('X-Custom-Header')).to.be.true\n pm.expect(pm.request.headers.get('X-Custom-Header')).to.equal('custom-value')\n})\n\npm.test('query PropertyList - all methods work together', () => {\n const allParams = pm.request.url.query.all()\n pm.expect(allParams).to.be.an('object')\n // At minimum we should have the params added in pre-request\n pm.expect(Object.keys(allParams).length).to.be.at.least(4)\n})\n\npm.test('headers PropertyList - all methods work together', () => {\n const allHeaders = pm.request.headers.all()\n pm.expect(allHeaders).to.be.an('object')\n pm.expect(Object.keys(allHeaders).length).to.be.at.least(5)\n})\n",
+ "auth": {
+ "authType": "inherit",
+ "authActive": true
+ },
+ "body": {
+ "contentType": null,
+ "body": null
+ },
+ "requestVariables": [],
+ "responses": {}
+ },
+ {
+ "v": "5",
+ "id": "advanced-response-methods-test",
+ "name": "advanced-response-methods-test",
+ "method": "POST",
+ "endpoint": "https://echo.hoppscotch.io",
+ "params": [],
+ "headers": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "active": true
+ }
+ ],
+ "preRequestScript": "export {};\n",
+ "testScript": "export {};\n\n// Test pm.response.reason()\npm.test('pm.response.reason() returns HTTP reason phrase', () => {\n const reason = pm.response.reason()\n pm.expect(reason).to.be.a('string')\n pm.expect(reason).to.equal('OK')\n})\n\n// Test hopp.response.reason() for parity\npm.test('hopp.response.reason() returns HTTP reason phrase', () => {\n const reason = hopp.response.reason()\n hopp.expect(reason).toBeType('string')\n hopp.expect(reason).toBe('OK')\n})\n\n// Test pm.response.dataURI()\npm.test('pm.response.dataURI() converts response to data URI', () => {\n const dataURI = pm.response.dataURI()\n pm.expect(dataURI).to.be.a('string')\n pm.expect(dataURI).to.include('data:')\n pm.expect(dataURI).to.include('base64')\n})\n\n// Test hopp.response.dataURI() for parity\npm.test('hopp.response.dataURI() converts response to data URI', () => {\n const dataURI = hopp.response.dataURI()\n hopp.expect(dataURI).toBeType('string')\n hopp.expect(dataURI.startsWith('data:')).toBe(true)\n})\n\n// Test .nested property assertions\npm.test('pm.expect().to.have.nested.property() accesses nested properties', () => {\n const obj = { a: { b: { c: 'deep value' } } }\n pm.expect(obj).to.have.nested.property('a.b.c', 'deep value')\n pm.expect(obj).to.have.nested.property('a.b')\n})\n\n// Test hopp namespace nested property for parity\npm.test('hopp.expect().to.have.nested.property() accesses nested properties', () => {\n const obj = { x: { y: { z: 'nested' } } }\n hopp.expect(obj).to.have.nested.property('x.y.z', 'nested')\n hopp.expect(obj).to.have.nested.property('x.y')\n})\n\npm.test('pm.expect().to.have.nested.property() handles arrays', () => {\n const obj = { items: [{ name: 'first' }, { name: 'second' }] }\n pm.expect(obj).to.have.nested.property('items[0].name', 'first')\n pm.expect(obj).to.have.nested.property('items[1].name', 'second')\n})\n\npm.test('pm.expect().to.not.have.nested.property() negation works', () => {\n const obj = { a: { b: 'value' } }\n pm.expect(obj).to.not.have.nested.property('a.c')\n pm.expect(obj).to.not.have.nested.property('x.y.z')\n})\n\n// Test .by() chaining for change assertions\npm.test('pm.expect().to.change().by() validates exact delta', () => {\n const obj = { value: 10 }\n pm.expect(() => { obj.value = 25 }).to.change(obj, 'value').by(15)\n})\n\n// Test hopp namespace .by() chaining for parity\npm.test('hopp.expect().to.change().by() validates exact delta', () => {\n const obj = { val: 100 }\n hopp.expect(() => { obj.val = 150 }).to.change(obj, 'val').by(50)\n})\n\npm.test('pm.expect().to.increase().by() validates exact increase', () => {\n const obj = { count: 5 }\n pm.expect(() => { obj.count += 7 }).to.increase(obj, 'count').by(7)\n})\n\npm.test('pm.expect().to.decrease().by() validates exact decrease', () => {\n const obj = { score: 100 }\n pm.expect(() => { obj.score -= 30 }).to.decrease(obj, 'score').by(30)\n})\n\npm.test('pm.expect().to.change().by() with negative delta', () => {\n const obj = { value: 50 }\n pm.expect(() => { obj.value = 20 }).to.change(obj, 'value').by(-30)\n})\n",
+ "auth": {
+ "authType": "inherit",
+ "authActive": true
+ },
+ "body": {
+ "contentType": "application/json",
+ "body": "{\"test\": \"data\"}"
+ },
+ "requestVariables": [],
+ "responses": {}
+ },
+ {
+ "v": "15",
+ "id": "advanced-chai-map-set-test",
+ "name": "advanced-chai-map-set-test",
+ "method": "GET",
+ "endpoint": "https://echo.hoppscotch.io",
+ "params": [],
+ "headers": [],
+ "preRequestScript": "export {};",
+ "testScript": "export {};\n\n// Map & Set Assertions\npm.test('Map assertions - size property', () => {\n const map = new Map([['key1', 'value1'], ['key2', 'value2']])\n pm.expect(map).to.have.property('size', 2)\n pm.expect(map.size).to.equal(2)\n})\n\npm.test('Set assertions - size property', () => {\n const set = new Set([1, 2, 3, 4])\n pm.expect(set).to.have.property('size', 4)\n pm.expect(set.size).to.equal(4)\n})\n\npm.test('Map instanceOf assertion', () => {\n const map = new Map()\n pm.expect(map).to.be.instanceOf(Map)\n pm.expect(map).to.be.an.instanceOf(Map)\n})\n\npm.test('Set instanceOf assertion', () => {\n const set = new Set()\n pm.expect(set).to.be.instanceOf(Set)\n pm.expect(set).to.be.an.instanceOf(Set)\n})\n\n// Advanced Chai - closeTo\npm.test('closeTo - validates numbers within delta', () => {\n pm.expect(3.14159).to.be.closeTo(3.14, 0.01)\n pm.expect(10.5).to.be.closeTo(11, 1)\n})\n\npm.test('closeTo - negation works', () => {\n pm.expect(100).to.not.be.closeTo(50, 10)\n pm.expect(3.14).to.not.be.closeTo(10, 0.1)\n})\n\npm.test('approximately - alias for closeTo', () => {\n pm.expect(2.5).to.approximately(2.4, 0.2)\n pm.expect(99.99).to.approximately(100, 0.1)\n})\n\n// Advanced Chai - finite\npm.test('finite - validates finite numbers', () => {\n pm.expect(123).to.be.finite\n pm.expect(0).to.be.finite\n pm.expect(-456).to.be.finite\n})\n\npm.test('finite - negation for Infinity', () => {\n pm.expect(Infinity).to.not.be.finite\n pm.expect(-Infinity).to.not.be.finite\n pm.expect(NaN).to.not.be.finite\n})\n\n// Advanced Chai - satisfy\npm.test('satisfy - custom predicate function', () => {\n pm.expect(10).to.satisfy((num) => num > 5)\n pm.expect('hello').to.satisfy((str) => str.length === 5)\n})\n\npm.test('satisfy - complex validation', () => {\n const obj = { name: 'test', value: 100 }\n pm.expect(obj).to.satisfy((o) => o.value > 50 && o.name.length > 0)\n})\n\npm.test('satisfy - negation works', () => {\n pm.expect(5).to.not.satisfy((num) => num > 10)\n pm.expect('abc').to.not.satisfy((str) => str.length > 5)\n})\n\n// Advanced Chai - respondTo\npm.test('respondTo - validates method existence', () => {\n class TestClass {\n testMethod() { return 'test' }\n anotherMethod() { return 'another' }\n }\n pm.expect(TestClass).to.respondTo('testMethod')\n pm.expect(TestClass).to.respondTo('anotherMethod')\n})\n\npm.test('respondTo - with itself for static methods', () => {\n class MyClass {\n static staticMethod() { return 'static' }\n instanceMethod() { return 'instance' }\n }\n pm.expect(MyClass).itself.to.respondTo('staticMethod')\n pm.expect(MyClass).to.not.itself.respondTo('instanceMethod')\n pm.expect(MyClass).to.respondTo('instanceMethod')\n})\n\n// Property Ownership - own.property\npm.test('own.property - distinguishes own vs inherited', () => {\n const parent = { inherited: true }\n const obj = Object.create(parent)\n obj.own = true\n pm.expect(obj).to.have.own.property('own')\n pm.expect(obj).to.not.have.own.property('inherited')\n pm.expect(obj).to.have.property('inherited')\n})\n\npm.test('deep.own.property - deep check with ownership', () => {\n const proto = { shared: 'inherited' }\n const obj = Object.create(proto)\n obj.data = { nested: 'value' }\n pm.expect(obj).to.have.deep.own.property('data', { nested: 'value' })\n pm.expect(obj).to.not.have.deep.own.property('shared')\n})\n\npm.test('ownProperty - alias for own.property', () => {\n const obj = { prop: 'value' }\n pm.expect(obj).to.have.ownProperty('prop')\n pm.expect(obj).to.have.ownProperty('prop', 'value')\n})\n\n// Hopp namespace parity tests\npm.test('hopp.expect Map/Set support', () => {\n const map = new Map([['x', 1]])\n const set = new Set([1, 2])\n hopp.expect(map.size).toBe(1)\n hopp.expect(set.size).toBe(2)\n})\n\npm.test('hopp.expect closeTo support', () => {\n hopp.expect(3.14).to.be.closeTo(3.1, 0.1)\n hopp.expect(10).to.be.closeTo(10.5, 1)\n})\n\npm.test('hopp.expect finite support', () => {\n hopp.expect(42).to.be.finite\n hopp.expect(Infinity).to.not.be.finite\n})\n\npm.test('hopp.expect satisfy support', () => {\n hopp.expect(100).to.satisfy((n) => n > 50)\n hopp.expect('test').to.satisfy((s) => s.length === 4)\n})\n\npm.test('hopp.expect respondTo support', () => {\n class TestClass { method() {} }\n hopp.expect(TestClass).to.respondTo('method')\n})\n\npm.test('hopp.expect own.property support', () => {\n const obj = Object.create({ inherited: 1 })\n obj.own = 2\n hopp.expect(obj).to.have.own.property('own')\n hopp.expect(obj).to.not.have.own.property('inherited')\n})\n\npm.test('hopp.expect ordered.members support', () => {\n const arr = ['a', 'b', 'c']\n hopp.expect(arr).to.have.ordered.members(['a', 'b', 'c'])\n})\n",
+ "auth": {
+ "authType": "inherit",
+ "authActive": true
+ },
+ "body": {
+ "contentType": null,
+ "body": null
+ },
+ "requestVariables": [],
+ "responses": {}
+ },
+ {
+ "v": "16",
+ "id": "cmfhzf0op00typecoer01",
+ "name": "type-preservation-test",
+ "method": "GET",
+ "endpoint": "https://echo.hoppscotch.io",
+ "params": [],
+ "headers": [],
+ "preRequestScript": "export {};\n// For CLI E2E testing: We only set simple string values in pre-request\n// Complex types will be tested within the test script itself\n\npm.environment.set('string_value', 'hello')\n",
+ "testScript": "export {};\n\n// ========================================\n// TYPE PRESERVATION TESTS (CLI Compatible)\n// ========================================\n\n// IMPORTANT NOTE: Type preservation works perfectly WITHIN script execution scope\n// Values persisted across request boundaries (pre-request \u2192 test) may be serialized\n// This is expected CLI behavior for environment persistence/display\n\n// Test values set from pre-request\npm.test('string values work across scripts', () => {\n pm.expect(pm.environment.get('string_value')).to.equal('hello')\n})\n\n// ========================================\n// TYPE PRESERVATION WITHIN SINGLE SCRIPT\n// (This is where type preservation really shines!)\n// ========================================\n\npm.test('numbers are preserved as numbers (same script)', () => {\n pm.environment.set('num', 42)\n const value = pm.environment.get('num')\n pm.expect(value).to.equal(42)\n pm.expect(typeof value).to.equal('number')\n})\n\npm.test('booleans are preserved as booleans (same script)', () => {\n pm.environment.set('bool_true', true)\n pm.environment.set('bool_false', false)\n pm.expect(pm.environment.get('bool_true')).to.equal(true)\n pm.expect(pm.environment.get('bool_false')).to.equal(false)\n pm.expect(typeof pm.environment.get('bool_true')).to.equal('boolean')\n})\n\npm.test('null is preserved as actual null (same script)', () => {\n pm.environment.set('null_val', null)\n const value = pm.environment.get('null_val')\n pm.expect(value).to.equal(null)\n pm.expect(value === null).to.be.true\n pm.expect(typeof value).to.equal('object')\n})\n\npm.test('undefined is preserved as actual undefined (same script)', () => {\n pm.environment.set('undef_val', undefined)\n const value = pm.environment.get('undef_val')\n pm.expect(value).to.equal(undefined)\n pm.expect(typeof value).to.equal('undefined')\n pm.expect(pm.environment.has('undef_val')).to.be.true\n})\n\npm.test('arrays are preserved with direct access', () => {\n pm.environment.set('arr', [1, 2, 3])\n const value = pm.environment.get('arr')\n\n pm.expect(Array.isArray(value)).to.be.true\n pm.expect(value.length).to.equal(3)\n pm.expect(value[0]).to.equal(1)\n pm.expect(value[2]).to.equal(3)\n})\n\npm.test('single-element arrays remain arrays', () => {\n pm.environment.set('single', [42])\n const value = pm.environment.get('single')\n\n pm.expect(Array.isArray(value)).to.be.true\n pm.expect(value.length).to.equal(1)\n pm.expect(value[0]).to.equal(42)\n})\n\npm.test('empty arrays are preserved', () => {\n pm.environment.set('empty_arr', [])\n const value = pm.environment.get('empty_arr')\n\n pm.expect(Array.isArray(value)).to.be.true\n pm.expect(value.length).to.equal(0)\n})\n\npm.test('string arrays preserve all elements', () => {\n pm.environment.set('str_arr', ['a', 'b', 'c'])\n const value = pm.environment.get('str_arr')\n\n pm.expect(Array.isArray(value)).to.be.true\n pm.expect(value).to.deep.equal(['a', 'b', 'c'])\n})\n\npm.test('objects are preserved with accessible properties', () => {\n pm.environment.set('obj', { key: 'value', num: 123 })\n const value = pm.environment.get('obj')\n\n pm.expect(typeof value).to.equal('object')\n pm.expect(value.key).to.equal('value')\n pm.expect(value.num).to.equal(123)\n})\n\npm.test('empty objects are preserved', () => {\n pm.environment.set('empty_obj', {})\n const value = pm.environment.get('empty_obj')\n\n pm.expect(typeof value).to.equal('object')\n pm.expect(Object.keys(value).length).to.equal(0)\n})\n\npm.test('nested objects preserve structure', () => {\n pm.environment.set('nested', { user: { name: 'John', id: 1 }, meta: { active: true } })\n const value = pm.environment.get('nested')\n\n pm.expect(value.user.name).to.equal('John')\n pm.expect(value.user.id).to.equal(1)\n pm.expect(value.meta.active).to.equal(true)\n})\n\npm.test('complex nested structures work', () => {\n const data = {\n users: [\n { id: 1, name: 'Alice', scores: [90, 85, 88] },\n { id: 2, name: 'Bob', scores: [75, 80, 82] }\n ],\n metadata: { count: 2, page: 1, filters: ['active', 'verified'] }\n }\n\n pm.environment.set('complex', data)\n const retrieved = pm.environment.get('complex')\n\n pm.expect(retrieved.users).to.be.an('array')\n pm.expect(retrieved.users.length).to.equal(2)\n pm.expect(retrieved.users[0].name).to.equal('Alice')\n pm.expect(retrieved.users[0].scores[0]).to.equal(90)\n pm.expect(retrieved.metadata.filters).to.deep.equal(['active', 'verified'])\n})\n\n// ========================================\n// NAMESPACE SEPARATION\n// ========================================\n\npm.test('hopp.env.set rejects non-string values', () => {\n let errorCount = 0\n\n try { hopp.env.set('test', undefined) } catch (e) { errorCount++ }\n try { hopp.env.set('test', null) } catch (e) { errorCount++ }\n try { hopp.env.set('test', 42) } catch (e) { errorCount++ }\n try { hopp.env.set('test', true) } catch (e) { errorCount++ }\n try { hopp.env.set('test', [1, 2]) } catch (e) { errorCount++ }\n try { hopp.env.set('test', {}) } catch (e) { errorCount++ }\n\n pm.expect(errorCount).to.equal(6)\n})\n\npm.test('hopp.env.set only accepts strings', () => {\n hopp.env.set('hopp_str', 'valid')\n pm.expect(hopp.env.get('hopp_str')).to.equal('valid')\n})\n\npm.test('pm/hopp cross-namespace reading works', () => {\n pm.environment.set('cross_test', [1, 2, 3])\n\n // hopp can read PM-set values\n const fromHopp = hopp.env.get('cross_test')\n pm.expect(Array.isArray(fromHopp)).to.be.true\n pm.expect(fromHopp.length).to.equal(3)\n})\n\n// ========================================\n// PRACTICAL USE CASES\n// ========================================\n\npm.test('no JSON.parse needed for response data storage', () => {\n // Simulate storing parsed response data\n const responseData = {\n id: 123,\n name: 'Test User',\n permissions: ['read', 'write'],\n settings: { theme: 'dark', notifications: true }\n }\n\n pm.environment.set('user_data', responseData)\n const stored = pm.environment.get('user_data')\n\n // Direct access - no JSON.parse needed!\n pm.expect(stored.id).to.equal(123)\n pm.expect(stored.permissions).to.include('write')\n pm.expect(stored.settings.theme).to.equal('dark')\n})\n\npm.test('array iteration works directly', () => {\n pm.environment.set('items', ['apple', 'banana', 'cherry'])\n const items = pm.environment.get('items')\n\n let concatenated = ''\n items.forEach(item => {\n concatenated += item\n })\n\n pm.expect(concatenated).to.equal('applebananacherry')\n pm.expect(items.map(i => i.toUpperCase())).to.deep.equal(['APPLE', 'BANANA', 'CHERRY'])\n})\n",
+ "auth": {
+ "authType": "inherit",
+ "authActive": true
+ },
+ "body": {
+ "contentType": null,
+ "body": null
+ },
+ "requestVariables": [],
+ "responses": {}
+ },
+ {
+ "v": "15",
+ "id": "type_preservation_ui_compat",
+ "name": "type-preservation-ui-compatibility-test",
+ "method": "POST",
+ "endpoint": "https://echo.hoppscotch.io",
+ "params": [],
+ "headers": [],
+ "preRequestScript": "export {};\n// Type preservation tests run in test script scope",
+ "testScript": "export {};\n\n// ====== Type Preservation & UI Compatibility Tests ======\n// NOTE: Testing in same script scope (CLI limitation: complex types\n// may not persist across pre-request \u2192 test boundary)\n\npm.test('PM namespace preserves array types (not String coercion)', () => {\n pm.environment.set('simpleArray', [1, 2, 3])\n const arr = pm.environment.get('simpleArray')\n\n // CRITICAL: Should be actual array, not string \"1,2,3\"\n pm.expect(Array.isArray(arr)).to.equal(true)\n pm.expect(arr).to.have.lengthOf(3)\n pm.expect(arr[0]).to.equal(1)\n pm.expect(arr[1]).to.equal(2)\n pm.expect(arr[2]).to.equal(3)\n})\n\npm.test('PM namespace preserves object types (not \"[object Object]\")', () => {\n pm.environment.set('simpleObject', { foo: 'bar', num: 42 })\n const obj = pm.environment.get('simpleObject')\n\n // CRITICAL: Should be actual object, not string \"[object Object]\"\n pm.expect(typeof obj).to.equal('object')\n pm.expect(obj).to.not.be.null\n pm.expect(obj.foo).to.equal('bar')\n pm.expect(obj.num).to.equal(42)\n})\n\npm.test('PM namespace preserves null correctly', () => {\n pm.environment.set('nullValue', null)\n const val = pm.environment.get('nullValue')\n\n pm.expect(val).to.be.null\n})\n\npm.test('PM namespace preserves undefined correctly', () => {\n pm.environment.set('undefinedValue', undefined)\n const val = pm.environment.get('undefinedValue')\n\n pm.expect(val).to.be.undefined\n})\n\npm.test('PM namespace preserves primitives correctly', () => {\n pm.environment.set('stringValue', 'hello')\n pm.environment.set('numberValue', 123)\n pm.environment.set('booleanValue', true)\n\n pm.expect(pm.environment.get('stringValue')).to.equal('hello')\n pm.expect(pm.environment.get('numberValue')).to.equal(123)\n pm.expect(pm.environment.get('booleanValue')).to.equal(true)\n})\n\npm.test('PM namespace preserves nested structures', () => {\n pm.environment.set('nestedStructure', {\n users: [\n { id: 1, name: 'Alice' },\n { id: 2, name: 'Bob' }\n ],\n meta: { count: 2, tags: ['active', 'verified'] }\n })\n const nested = pm.environment.get('nestedStructure')\n\n pm.expect(nested).to.be.an('object')\n pm.expect(nested.users).to.be.an('array')\n pm.expect(nested.users).to.have.lengthOf(2)\n pm.expect(nested.users[0].name).to.equal('Alice')\n pm.expect(nested.users[1].name).to.equal('Bob')\n pm.expect(nested.meta.count).to.equal(2)\n pm.expect(nested.meta.tags).to.have.members(['active', 'verified'])\n})\n\npm.test('PM namespace handles mixed arrays (regression test for UI crash)', () => {\n pm.environment.set('mixedArray', [\n 'string',\n 42,\n true,\n null,\n undefined,\n [1, 2],\n { key: 'value' }\n ])\n const mixed = pm.environment.get('mixedArray')\n\n // This is the exact case that caused the UI crash\n pm.expect(Array.isArray(mixed)).to.equal(true)\n pm.expect(mixed).to.have.lengthOf(7)\n pm.expect(mixed[0]).to.equal('string')\n pm.expect(mixed[1]).to.equal(42)\n pm.expect(mixed[2]).to.equal(true)\n pm.expect(mixed[3]).to.be.null\n // mixed[4] is undefined in array, becomes null during JSON serialization\n pm.expect(Array.isArray(mixed[5])).to.equal(true)\n pm.expect(mixed[5]).to.have.lengthOf(2)\n pm.expect(typeof mixed[6]).to.equal('object')\n pm.expect(mixed[6].key).to.equal('value')\n})\n\npm.test('PM globals preserve arrays and objects', () => {\n pm.globals.set('globalArray', [10, 20, 30])\n pm.globals.set('globalObject', { env: 'prod', port: 8080 })\n\n const globalArr = pm.globals.get('globalArray')\n const globalObj = pm.globals.get('globalObject')\n\n pm.expect(Array.isArray(globalArr)).to.equal(true)\n pm.expect(globalArr).to.deep.equal([10, 20, 30])\n\n pm.expect(typeof globalObj).to.equal('object')\n pm.expect(globalObj.env).to.equal('prod')\n pm.expect(globalObj.port).to.equal(8080)\n})\n\npm.test('PM variables preserve arrays and objects', () => {\n pm.variables.set('varArray', [5, 10, 15])\n pm.variables.set('varObject', { status: 'active', count: 100 })\n\n const varArr = pm.variables.get('varArray')\n const varObj = pm.variables.get('varObject')\n\n pm.expect(Array.isArray(varArr)).to.equal(true)\n pm.expect(varArr).to.deep.equal([5, 10, 15])\n\n pm.expect(typeof varObj).to.equal('object')\n pm.expect(varObj.status).to.equal('active')\n pm.expect(varObj.count).to.equal(100)\n})\n\npm.test('Type preservation works with Postman compatibility', () => {\n pm.environment.set('testArr', [1, 2, 3])\n pm.environment.set('testObj', { foo: 'bar', num: 42 })\n\n const arr = pm.environment.get('testArr')\n const obj = pm.environment.get('testObj')\n\n // Should work like Postman: runtime types preserved\n pm.expect(arr.length).to.equal(3)\n pm.expect(obj.foo).to.equal('bar')\n\n // Verify no String() coercion happened\n pm.expect(arr).to.not.equal('1,2,3')\n pm.expect(obj).to.not.equal('[object Object]')\n})\n\npm.test('Type preservation: UI compatibility regression test', () => {\n // This test validates the fix for the reported bug:\n // \"TypeError: a.match is not a function at details.vue:387:10\"\n\n pm.environment.set('mixedTest', [\n 'string', 42, true, null, undefined, [1, 2], { key: 'value' }\n ])\n\n const mixed = pm.environment.get('mixedTest')\n\n // Should NOT throw any errors\n let errorCount = 0\n try {\n // Access all elements\n mixed.forEach(item => {\n // Should work with all types\n const type = typeof item\n const validTypes = ['string', 'number', 'boolean', 'object']\n if (!validTypes.includes(type)) {\n errorCount++\n }\n })\n } catch (e) {\n errorCount++\n }\n\n pm.expect(errorCount).to.equal(0)\n})\n",
+ "auth": {
+ "authType": "inherit",
+ "authActive": true
+ },
+ "body": {
+ "contentType": "application/json",
+ "body": "{\n \"test\": \"type preservation validation\"\n}"
+ },
+ "requestVariables": [],
+ "responses": {}
}
],
"auth": {
@@ -252,4 +796,4 @@
},
"headers": [],
"variables": []
-}
+}
\ No newline at end of file
diff --git a/packages/hoppscotch-cli/src/utils/pre-request.ts b/packages/hoppscotch-cli/src/utils/pre-request.ts
index 9dbdd3e5..fee73f46 100644
--- a/packages/hoppscotch-cli/src/utils/pre-request.ts
+++ b/packages/hoppscotch-cli/src/utils/pre-request.ts
@@ -68,23 +68,36 @@ export const preRequestScriptRunner = (
const { selected, global } = updatedEnvs;
return {
- updatedEnvs: {
+ // Keep the original updatedEnvs with separate global and selected arrays
+ preRequestUpdatedEnvs: updatedEnvs,
+ // Create Environment format for getEffectiveRESTRequest
+ envForEffectiveRequest: {
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 })
);
}),
diff --git a/packages/hoppscotch-common/locales/en.json b/packages/hoppscotch-common/locales/en.json
index 18151789..81c128f7 100644
--- a/packages/hoppscotch-common/locales/en.json
+++ b/packages/hoppscotch-common/locales/en.json
@@ -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",
diff --git a/packages/hoppscotch-common/src/components/collections/ImportExport.vue b/packages/hoppscotch-common/src/components/collections/ImportExport.vue
index 5fda8674..b3dba8e8 100644
--- a/packages/hoppscotch-common/src/components/collections/ImportExport.vue
+++ b/packages/hoppscotch-common/src/components/collections/ImportExport.vue
@@ -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",
diff --git a/packages/hoppscotch-common/src/components/importExport/Base.vue b/packages/hoppscotch-common/src/components/importExport/Base.vue
index b13b4ba4..d7de6c12 100644
--- a/packages/hoppscotch-common/src/components/importExport/Base.vue
+++ b/packages/hoppscotch-common/src/components/importExport/Base.vue
@@ -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")
},
diff --git a/packages/hoppscotch-common/src/components/importExport/ImportExportSteps/FileImport.vue b/packages/hoppscotch-common/src/components/importExport/ImportExportSteps/FileImport.vue
index 0659d4f3..1f85aa62 100644
--- a/packages/hoppscotch-common/src/components/importExport/ImportExportSteps/FileImport.vue
+++ b/packages/hoppscotch-common/src/components/importExport/ImportExportSteps/FileImport.vue
@@ -52,13 +52,41 @@
}}
+
+
+
+
+
+
+
@@ -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([])
const inputChooseFileToImportFrom = ref()
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) {
diff --git a/packages/hoppscotch-common/src/components/importExport/ImportExportSteps/ImportSummary.vue b/packages/hoppscotch-common/src/components/importExport/ImportExportSteps/ImportSummary.vue
index 80e030f0..72966e97 100644
--- a/packages/hoppscotch-common/src/components/importExport/ImportExportSteps/ImportSummary.vue
+++ b/packages/hoppscotch-common/src/components/importExport/ImportExportSteps/ImportSummary.vue
@@ -1,3 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+ {{ t(feature.label) }}
+
+
+
+
+ {{ feature.count }}
+ {{
+ feature.count != 1
+ ? t(feature.label)
+ : t(feature.label).slice(0, -1)
+ }}
+ Imported
+
+
+
+ 0 {{ t(feature.label) }} Imported
+
+
+
+
+
+ 0 {{ t(feature.label) }} Imported
+
+
+
+ {{
+ t("import.import_summary_not_supported_by_hoppscotch_import", {
+ featureLabel: t(feature.label),
+ })
+ }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ totalScriptsCount }}
+ {{
+ totalScriptsCount === 1
+ ? t("import.import_summary_script_found")
+ : t("import.import_summary_scripts_found")
+ }}
+
+
+ {{ t("import.import_summary_enable_experimental_sandbox") }}
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
- {{ t(feature.label) }}
-
-
-
-
- {{ feature.count }}
- {{
- feature.count != 1
- ? t(feature.label)
- : t(feature.label).slice(0, -1)
- }}
- Imported
-
-
-
- {{
- t("import.import_summary_not_supported_by_hoppscotch_import", {
- featureLabel: t(feature.label),
- })
- }}
-
-
-
-
-
-
-
-
-
diff --git a/packages/hoppscotch-common/src/helpers/import-export/import/import-sources/FileSource.ts b/packages/hoppscotch-common/src/helpers/import-export/import/import-sources/FileSource.ts
index aed2ccff..2eb4ddaf 100644
--- a/packages/hoppscotch-common/src/helpers/import-export/import/import-sources/FileSource.ts
+++ b/packages/hoppscotch-common/src/helpers/import-export/import/import-sources/FileSource.ts
@@ -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
+ onImportFromFile: (
+ content: string[],
+ importScripts?: boolean
+ ) => any | Promise
isLoading?: Ref
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,
}))
}
diff --git a/packages/hoppscotch-common/src/helpers/import-export/import/postman.ts b/packages/hoppscotch-common/src/helpers/import-export/import/postman.ts
index 623bd539..61f47ae8 100644
--- a/packages/hoppscotch-common/src/helpers/import-export/import/postman.ts
+++ b/packages/hoppscotch-common/src/helpers/import-export/import/postman.ts
@@ -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- ): HoppCollection =>
+const getHoppFolder = (
+ ig: ItemGroup
- ,
+ 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)
)
diff --git a/packages/hoppscotch-common/src/types/post-request.d.ts b/packages/hoppscotch-common/src/types/post-request.d.ts
index c8fd9eea..e733c86f 100644
--- a/packages/hoppscotch-common/src/types/post-request.d.ts
+++ b/packages/hoppscotch-common/src/types/post-request.d.ts
@@ -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
+ 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
+ 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
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
+ 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
+ }>
+
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
+ /**
+ * 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
+ /**
+ * Convert query parameters to object (alias for all())
+ * @returns Object with all query parameters
+ */
+ toObject(): Record
+ /**
+ * 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(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
+ /**
+ * Convert headers to object (alias for all())
+ * @returns Object with all headers
+ */
+ toObject(): Record
+ /**
+ * 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(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
+ 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
+ }>
+ 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
+ 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,
+ options?: Record
+ ): 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
}
diff --git a/packages/hoppscotch-common/src/types/pre-request.d.ts b/packages/hoppscotch-common/src/types/pre-request.d.ts
index 3842a89b..50de8bbf 100644
--- a/packages/hoppscotch-common/src/types/pre-request.d.ts
+++ b/packages/hoppscotch-common/src/types/pre-request.d.ts
@@ -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
}>
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
}>
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
+ /**
+ * 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
+ /**
+ * Convert query parameters to object (alias for all())
+ * @returns Object with all query parameters
+ */
+ toObject(): Record
+ /**
+ * 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(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,
+ 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
+ /**
+ * Convert headers to object (alias for all())
+ * @returns Object with all headers (keys in lowercase)
+ */
+ toObject(): Record
+ /**
+ * 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(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,
+ 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,
+ options?: Record
+ ): 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
}>
}
diff --git a/packages/hoppscotch-js-sandbox/package.json b/packages/hoppscotch-js-sandbox/package.json
index 320a7f6e..cbc730c8 100644
--- a/packages/hoppscotch-js-sandbox/package.json
+++ b/packages/hoppscotch-js-sandbox/package.json
@@ -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",
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/combined/async-await-support.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/combined/async-await-support.spec.ts
new file mode 100644
index 00000000..ce426039
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/combined/async-await-support.spec.ts
@@ -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" }),
+ ]),
+ }),
+ ])
+ )
+ })
+})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/combined/core-chai-assertions.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/combined/core-chai-assertions.spec.ts
new file mode 100644
index 00000000..fa49e24a
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/combined/core-chai-assertions.spec.ts
@@ -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" }),
+ ]),
+ }),
+ ],
+ }),
+ ])
+ })
+ }
+)
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/combined/deep-include-keys-assertions.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/combined/deep-include-keys-assertions.spec.ts
new file mode 100644
index 00000000..987fe543
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/combined/deep-include-keys-assertions.spec.ts
@@ -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" }),
+ ],
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/combined/instanceof-assertions.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/combined/instanceof-assertions.spec.ts
new file mode 100644
index 00000000..c78f0f5a
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/combined/instanceof-assertions.spec.ts
@@ -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" })],
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+ }
+)
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/combined/keys-members-assertions.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/combined/keys-members-assertions.spec.ts
new file mode 100644
index 00000000..8309bdc4
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/combined/keys-members-assertions.spec.ts
@@ -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: [],
+ },
+ ],
+ },
+ ])
+ })
+ }
+)
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/combined/length-assertions.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/combined/length-assertions.spec.ts
new file mode 100644
index 00000000..8713e2b4
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/combined/length-assertions.spec.ts
@@ -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" }),
+ ],
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+ })
+})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/combined/side-effects-assertions.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/combined/side-effects-assertions.spec.ts
new file mode 100644
index 00000000..56ece7d5
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/combined/side-effects-assertions.spec.ts
@@ -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" }),
+ ]),
+ }),
+ ],
+ }),
+ ])
+ })
+ })
+ }
+)
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/chai-powered-assertions/core-assertions.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/chai-powered-assertions/core-assertions.spec.ts
new file mode 100644
index 00000000..079c9163
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/chai-powered-assertions/core-assertions.spec.ts
@@ -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"),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+ })
+})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/chai-powered-assertions/exotic-objects.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/chai-powered-assertions/exotic-objects.spec.ts
new file mode 100644
index 00000000..9add9c2b
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/chai-powered-assertions/exotic-objects.spec.ts
@@ -0,0 +1,1042 @@
+import { describe, expect, test } from "vitest"
+import { runTest } from "~/utils/test-helpers"
+
+// Behavior validation for exotic objects (Proxy, etc.) within sandbox serialization constraints
+describe("hopp.expect - Exotic Objects & Error Edge Cases", () => {
+ describe("Proxy Objects", () => {
+ test("should handle basic Proxy objects", () => {
+ return expect(
+ runTest(`
+ hopp.test("proxy object assertions work", () => {
+ const target = { value: 42 }
+ const proxy = new Proxy(target, {})
+
+ hopp.expect(proxy).to.be.an("object")
+ hopp.expect(proxy).to.have.property("value", 42)
+ hopp.expect(proxy.value).to.equal(42)
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "proxy object assertions work",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be an object/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to have property 'value'/),
+ },
+ {
+ status: "pass",
+ message: "Expected 42 to equal 42",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should handle Proxy with custom traps", () => {
+ return expect(
+ runTest(`
+ hopp.test("proxy with custom traps work", () => {
+ const target = { value: 10 }
+ const proxy = new Proxy(target, {
+ get(target, prop) {
+ if (prop === 'value') return target[prop] * 2
+ return target[prop]
+ }
+ })
+
+ hopp.expect(proxy.value).to.equal(20)
+ hopp.expect(target.value).to.equal(10)
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "proxy with custom traps work",
+ expectResults: [
+ {
+ status: "pass",
+ message: "Expected 20 to equal 20",
+ },
+ {
+ status: "pass",
+ message: "Expected 10 to equal 10",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should handle revocable Proxy", () => {
+ return expect(
+ runTest(`
+ hopp.test("revocable proxy assertions work", () => {
+ const target = { value: 42 }
+ const { proxy, revoke } = Proxy.revocable(target, {})
+
+ hopp.expect(proxy.value).to.equal(42)
+
+ revoke()
+
+ hopp.expect(() => proxy.value).to.throw(TypeError)
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "revocable proxy assertions work",
+ expectResults: [
+ {
+ status: "pass",
+ message: "Expected 42 to equal 42",
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to throw TypeError/),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+ })
+
+ describe("WeakMap & WeakSet", () => {
+ test("should handle WeakMap behavior", () => {
+ return expect(
+ runTest(`
+ hopp.test("weakmap behavior tests work", () => {
+ const wm = new WeakMap()
+ const key1 = {}
+ const key2 = { id: 1 }
+
+ wm.set(key1, 'value1')
+ wm.set(key2, 42)
+
+ hopp.expect(wm.has(key1)).to.be.true
+ hopp.expect(wm.get(key1)).to.equal('value1')
+ hopp.expect(wm.get(key2)).to.equal(42)
+ hopp.expect(wm.has({})).to.be.false
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "weakmap behavior tests work",
+ expectResults: [
+ {
+ status: "pass",
+ message: "Expected true to be true",
+ },
+ {
+ status: "pass",
+ message: "Expected 'value1' to equal 'value1'",
+ },
+ {
+ status: "pass",
+ message: "Expected 42 to equal 42",
+ },
+ {
+ status: "pass",
+ message: "Expected false to be false",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should handle WeakSet behavior", () => {
+ return expect(
+ runTest(`
+ hopp.test("weakset behavior tests work", () => {
+ const ws = new WeakSet()
+ const obj1 = { id: 1 }
+ const obj2 = { id: 2 }
+
+ ws.add(obj1)
+ ws.add(obj2)
+
+ hopp.expect(ws.has(obj1)).to.be.true
+ hopp.expect(ws.has(obj2)).to.be.true
+ hopp.expect(ws.has({})).to.be.false
+
+ // WeakSet doesn't have reference equality for new objects
+ const newObj = { id: 1 }
+ hopp.expect(ws.has(newObj)).to.be.false
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "weakset behavior tests work",
+ expectResults: [
+ {
+ status: "pass",
+ message: "Expected true to be true",
+ },
+ {
+ status: "pass",
+ message: "Expected true to be true",
+ },
+ {
+ status: "pass",
+ message: "Expected false to be false",
+ },
+ {
+ status: "pass",
+ message: "Expected false to be false",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+ })
+
+ describe("TypedArrays", () => {
+ test("should handle Uint8Array", () => {
+ return expect(
+ runTest(`
+ hopp.test("uint8array tests work", () => {
+ const arr = new Uint8Array([1, 2, 3, 4, 5])
+
+ hopp.expect(arr).to.be.an('object')
+ hopp.expect(arr.length).to.equal(5)
+ hopp.expect(arr[0]).to.equal(1)
+ hopp.expect(arr[4]).to.equal(5)
+ hopp.expect(arr[10]).to.equal(undefined)
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "uint8array tests work",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be an object/),
+ },
+ {
+ status: "pass",
+ message: "Expected 5 to equal 5",
+ },
+ {
+ status: "pass",
+ message: "Expected 1 to equal 1",
+ },
+ {
+ status: "pass",
+ message: "Expected 5 to equal 5",
+ },
+ {
+ status: "pass",
+ message: "Expected undefined to equal undefined",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should handle Int16Array", () => {
+ return expect(
+ runTest(`
+ hopp.test("int16array tests work", () => {
+ const arr = new Int16Array([-1000, 0, 1000])
+
+ hopp.expect(arr[0]).to.equal(-1000)
+ hopp.expect(arr[2]).to.equal(1000)
+ hopp.expect(arr.byteLength).to.equal(6)
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "int16array tests work",
+ expectResults: [
+ {
+ status: "pass",
+ message: "Expected -1000 to equal -1000",
+ },
+ {
+ status: "pass",
+ message: "Expected 1000 to equal 1000",
+ },
+ {
+ status: "pass",
+ message: "Expected 6 to equal 6",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should handle Float32Array", () => {
+ return expect(
+ runTest(`
+ hopp.test("float32array tests work", () => {
+ const arr = new Float32Array([3.14, 2.718, 1.414])
+
+ hopp.expect(arr[0]).to.be.closeTo(3.14, 0.01)
+ hopp.expect(arr[1]).to.be.closeTo(2.718, 0.001)
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "float32array tests work",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be closeTo 3\.14/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be closeTo 2\.718/),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should handle multiple TypedArray types", () => {
+ return expect(
+ runTest(`
+ hopp.test("various typedarray types work", () => {
+ const u32 = new Uint32Array([1, 2, 3])
+ const i8 = new Int8Array([-1, 0, 1])
+ const f64 = new Float64Array([1.1, 2.2])
+
+ hopp.expect(u32.byteLength).to.equal(12)
+ hopp.expect(i8.length).to.equal(3)
+ hopp.expect(f64.byteLength).to.equal(16)
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "various typedarray types work",
+ expectResults: [
+ {
+ status: "pass",
+ message: "Expected 12 to equal 12",
+ },
+ {
+ status: "pass",
+ message: "Expected 3 to equal 3",
+ },
+ {
+ status: "pass",
+ message: "Expected 16 to equal 16",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+ })
+
+ describe("ArrayBuffer & DataView", () => {
+ test("should handle ArrayBuffer", () => {
+ return expect(
+ runTest(`
+ hopp.test("arraybuffer tests work", () => {
+ const buffer = new ArrayBuffer(16)
+ const view = new Uint8Array(buffer)
+
+ hopp.expect(buffer.byteLength).to.equal(16)
+ hopp.expect(view.length).to.equal(16)
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "arraybuffer tests work",
+ expectResults: [
+ {
+ status: "pass",
+ message: "Expected 16 to equal 16",
+ },
+ {
+ status: "pass",
+ message: "Expected 16 to equal 16",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should handle DataView", () => {
+ return expect(
+ runTest(`
+ hopp.test("dataview tests work", () => {
+ const buffer = new ArrayBuffer(8)
+ const view = new DataView(buffer)
+
+ view.setInt32(0, 42)
+ view.setFloat32(4, 3.14)
+
+ hopp.expect(view.byteLength).to.equal(8)
+ hopp.expect(view.getInt32(0)).to.equal(42)
+ hopp.expect(view.getFloat32(4)).to.be.closeTo(3.14, 0.01)
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "dataview tests work",
+ expectResults: [
+ {
+ status: "pass",
+ message: "Expected 8 to equal 8",
+ },
+ {
+ status: "pass",
+ message: "Expected 42 to equal 42",
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be closeTo 3\.14/),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+ })
+
+ describe("Error Objects & Custom Errors", () => {
+ test("should handle standard Error types", () => {
+ return expect(
+ runTest(`
+ hopp.test("standard error types work", () => {
+ const err = new Error('Test error')
+ const typeErr = new TypeError('Type error')
+ const rangeErr = new RangeError('Range error')
+ const refErr = new ReferenceError('Reference error')
+
+ hopp.expect(err).to.be.an.instanceof(Error)
+ hopp.expect(typeErr).to.be.an.instanceof(TypeError)
+ hopp.expect(rangeErr).to.be.an.instanceof(RangeError)
+ hopp.expect(refErr).to.be.an.instanceof(ReferenceError)
+ hopp.expect(err.message).to.equal('Test error')
+ hopp.expect(typeErr.message).to.equal('Type error')
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "standard error types work",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be an instanceof Error/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /to be an instanceof TypeError/
+ ),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /to be an instanceof RangeError/
+ ),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /to be an instanceof ReferenceError/
+ ),
+ },
+ {
+ status: "pass",
+ message: "Expected 'Test error' to equal 'Test error'",
+ },
+ {
+ status: "pass",
+ message: "Expected 'Type error' to equal 'Type error'",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should handle custom Error classes", () => {
+ return expect(
+ runTest(`
+ hopp.test("custom error classes work", () => {
+ class CustomError extends Error {
+ constructor(message, code) {
+ super(message)
+ this.name = 'CustomError'
+ this.code = code
+ }
+ }
+
+ const err = new CustomError('Custom error', 500)
+
+ hopp.expect(err).to.be.an.instanceof(Error)
+ hopp.expect(err.name).to.equal('CustomError')
+ hopp.expect(err.code).to.equal(500)
+ hopp.expect(err.message).to.equal('Custom error')
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "custom error classes work",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be an instanceof Error/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /to equal 'CustomError'|to equal CustomError/
+ ),
+ },
+ {
+ status: "pass",
+ message: "Expected 500 to equal 500",
+ },
+ {
+ status: "pass",
+ message: "Expected 'Custom error' to equal 'Custom error'",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should handle Error with cause (ES2022)", () => {
+ return expect(
+ runTest(`
+ hopp.test("error with cause work", () => {
+ const original = new Error('Original error')
+ const wrapped = new Error('Wrapped error', { cause: original })
+
+ hopp.expect(wrapped).to.be.an.instanceof(Error)
+ hopp.expect(wrapped.message).to.equal('Wrapped error')
+ hopp.expect(wrapped.cause).to.exist
+ hopp.expect(wrapped.cause.message).to.equal('Original error')
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "error with cause work",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be an instanceof Error/),
+ },
+ {
+ status: "pass",
+ message: "Expected 'Wrapped error' to equal 'Wrapped error'",
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to exist/),
+ },
+ {
+ status: "pass",
+ message:
+ "Expected 'Original error' to equal 'Original error'",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should handle throw with Error objects", () => {
+ return expect(
+ runTest(`
+ hopp.test("throwing error objects work", () => {
+ const thrower = () => { throw new TypeError('Invalid type') }
+
+ hopp.expect(thrower).to.throw(TypeError)
+ hopp.expect(thrower).to.throw(TypeError, 'Invalid type')
+ hopp.expect(thrower).to.throw('Invalid type')
+ hopp.expect(thrower).to.throw(/Invalid/)
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "throwing error objects work",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(/to throw TypeError/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to throw TypeError/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to throw 'Invalid type'/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to throw \/Invalid\//),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+ })
+
+ describe("Promise Objects", () => {
+ test("should handle Promise state checking", () => {
+ return expect(
+ runTest(`
+ hopp.test("promise tests work", async () => {
+ const resolved = Promise.resolve(42)
+ const rejected = Promise.reject(new Error('Failed'))
+
+ hopp.expect(await resolved).to.equal(42)
+
+ try {
+ await rejected
+ } catch (e) {
+ hopp.expect(e).to.be.an.instanceof(Error)
+ hopp.expect(e.message).to.equal('Failed')
+ }
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ expectResults: [
+ {
+ status: "pass",
+ message: "Expected 42 to equal 42",
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be an instanceof Error/),
+ },
+ {
+ status: "pass",
+ message: "Expected 'Failed' to equal 'Failed'",
+ },
+ ],
+ children: [
+ expect.objectContaining({
+ descriptor: "promise tests work",
+ expectResults: [],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should handle Promise.all", () => {
+ return expect(
+ runTest(`
+ hopp.test("promise.all tests work", async () => {
+ const p1 = Promise.resolve(1)
+ const p2 = Promise.resolve(2)
+ const p3 = Promise.resolve(3)
+
+ const results = await Promise.all([p1, p2, p3])
+
+ hopp.expect(results).to.have.lengthOf(3)
+ hopp.expect(results[0]).to.equal(1)
+ hopp.expect(results[2]).to.equal(3)
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(/to have lengthOf 3/),
+ },
+ {
+ status: "pass",
+ message: "Expected 1 to equal 1",
+ },
+ {
+ status: "pass",
+ message: "Expected 3 to equal 3",
+ },
+ ],
+ children: [
+ expect.objectContaining({
+ descriptor: "promise.all tests work",
+ expectResults: [],
+ }),
+ ],
+ }),
+ ])
+ })
+ })
+
+ describe("Symbol.iterator & Iterables", () => {
+ test("should handle custom iterables", () => {
+ return expect(
+ runTest(`
+ hopp.test("custom iterable tests work", () => {
+ const iterable = {
+ *[Symbol.iterator]() {
+ yield 1
+ yield 2
+ yield 3
+ }
+ }
+
+ hopp.expect(iterable).to.be.an('object')
+ hopp.expect(typeof iterable[Symbol.iterator]).to.equal('function')
+
+ const values = [...iterable]
+ hopp.expect(values[0]).to.equal(1)
+ hopp.expect(values[1]).to.equal(2)
+ hopp.expect(values[2]).to.equal(3)
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ expectResults: [],
+ children: [
+ expect.objectContaining({
+ descriptor: "custom iterable tests work",
+ expectResults: [
+ {
+ status: "pass",
+ message: "Expected {} to be an object",
+ },
+ {
+ status: "pass",
+ message: "Expected function to equal function",
+ },
+ {
+ status: "pass",
+ message: "Expected 1 to equal 1",
+ },
+ {
+ status: "pass",
+ message: "Expected 2 to equal 2",
+ },
+ {
+ status: "pass",
+ message: "Expected 3 to equal 3",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should handle generator functions", () => {
+ return expect(
+ runTest(`
+ hopp.test("generator function tests work", () => {
+ function* gen() {
+ yield 'a'
+ yield 'b'
+ yield 'c'
+ }
+
+ hopp.expect(typeof gen).to.equal('function')
+
+ const iterator = gen()
+ hopp.expect(iterator).to.be.an('object')
+ hopp.expect(typeof iterator.next).to.equal('function')
+
+ const first = iterator.next()
+ hopp.expect(first.value).to.equal('a')
+ hopp.expect(first.done).to.be.false
+
+ iterator.next()
+ iterator.next()
+ const last = iterator.next()
+ hopp.expect(last.done).to.be.true
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ expectResults: [],
+ children: [
+ expect.objectContaining({
+ descriptor: "generator function tests work",
+ expectResults: [
+ {
+ status: "pass",
+ message: "Expected function to equal function",
+ },
+ {
+ status: "pass",
+ message: "Expected {} to be an object",
+ },
+ {
+ status: "pass",
+ message: "Expected function to equal function",
+ },
+ {
+ status: "pass",
+ message: "Expected 'a' to equal 'a'",
+ },
+ {
+ status: "pass",
+ message: "Expected false to be false",
+ },
+ {
+ status: "pass",
+ message: "Expected true to be true",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+ })
+
+ describe("Complex Compositions", () => {
+ test("should handle nested exotic objects", () => {
+ return expect(
+ runTest(`
+ hopp.test("nested exotic objects work", () => {
+ const buffer = new ArrayBuffer(3)
+ const view = new Uint8Array(buffer)
+ view[0] = 1
+ view[1] = 2
+ view[2] = 3
+
+ const mySet = new Set([1, 2, 3])
+ const myMap = new Map([['key', 'value']])
+
+ const composed = {
+ buffer,
+ view,
+ set: mySet,
+ map: myMap
+ }
+
+ hopp.expect(composed.buffer.byteLength).to.equal(3)
+ hopp.expect(composed.view[1]).to.equal(2)
+ hopp.expect(composed.set).to.be.an.instanceof(Set)
+ hopp.expect(composed.map).to.be.an.instanceof(Map)
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "nested exotic objects work",
+ expectResults: [
+ {
+ status: "pass",
+ message: "Expected 3 to equal 3",
+ },
+ {
+ status: "pass",
+ message: "Expected 2 to equal 2",
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be an instanceof Set/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be an instanceof Map/),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should handle Proxy wrapping exotic objects", () => {
+ return expect(
+ runTest(`
+ hopp.test("proxy wrapping exotic objects work", () => {
+ const arr = new Uint8Array([10, 20, 30])
+ const proxy = new Proxy(arr, {
+ get(target, prop) {
+ if (typeof prop === 'string' && !isNaN(prop)) {
+ return target[prop] * 2
+ }
+ return target[prop]
+ }
+ })
+
+ hopp.expect(proxy[0]).to.equal(20)
+ hopp.expect(proxy[1]).to.equal(40)
+ hopp.expect(arr[0]).to.equal(10)
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "proxy wrapping exotic objects work",
+ expectResults: [
+ {
+ status: "pass",
+ message: "Expected 20 to equal 20",
+ },
+ {
+ status: "pass",
+ message: "Expected 40 to equal 40",
+ },
+ {
+ status: "pass",
+ message: "Expected 10 to equal 10",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+ })
+
+ describe("Edge Cases", () => {
+ test("should handle Error stack traces", () => {
+ return expect(
+ runTest(`
+ hopp.test("error stack traces work", () => {
+ const err = new Error('Test error')
+
+ hopp.expect(err).to.have.property('stack')
+ hopp.expect(err.stack).to.be.a('string')
+ hopp.expect(err.stack.length).to.be.above(0)
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "error stack traces work",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(/to have property 'stack'/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be a string/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be above 0/),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should handle TypedArray negative indexing patterns", () => {
+ return expect(
+ runTest(`
+ hopp.test("typedarray negative patterns work", () => {
+ const arr = new Int8Array([-128, -1, 0, 1, 127])
+
+ hopp.expect(arr[0]).to.equal(-128)
+ hopp.expect(arr[arr.length - 1]).to.equal(127)
+ hopp.expect(arr.slice(-2)[0]).to.equal(1)
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "typedarray negative patterns work",
+ expectResults: [
+ {
+ status: "pass",
+ message: "Expected -128 to equal -128",
+ },
+ {
+ status: "pass",
+ message: "Expected 127 to equal 127",
+ },
+ {
+ status: "pass",
+ message: "Expected 1 to equal 1",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+ })
+})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/chai-powered-assertions/functions-errors.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/chai-powered-assertions/functions-errors.spec.ts
new file mode 100644
index 00000000..6fe82ea5
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/chai-powered-assertions/functions-errors.spec.ts
@@ -0,0 +1,1352 @@
+/**
+ * @see https://github.com/hoppscotch/hoppscotch/discussions/5221
+ */
+
+import { describe, expect, test } from "vitest"
+import { runTest } from "~/utils/test-helpers"
+
+describe("`hopp.expect` - Functions and Error Assertions", () => {
+ describe("Throw Assertions", () => {
+ test("should support `.throw()` without arguments for any error", () => {
+ return expect(
+ runTest(`
+ hopp.test("basic throw works", () => {
+ hopp.expect(() => { throw new Error('test') }).to.throw()
+ hopp.expect(() => {}).to.not.throw()
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "basic throw works",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(/Expected .* to throw$/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/Expected .* to not throw/),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should support `.throw()` with error type matching", () => {
+ return expect(
+ runTest(`
+ hopp.test("throw with type works", () => {
+ hopp.expect(() => { throw new TypeError('type error') }).to.throw(TypeError)
+ hopp.expect(() => { throw new RangeError('range error') }).to.throw(RangeError)
+ hopp.expect(() => { throw new ReferenceError('ref error') }).to.throw(ReferenceError)
+ hopp.expect(() => { throw new Error('generic') }).to.throw(Error)
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "throw with type works",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected .* to throw TypeError/
+ ),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected .* to throw RangeError/
+ ),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected .* to throw ReferenceError/
+ ),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/Expected .* to throw Error/),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should support `.throw()` with string message matching", () => {
+ return expect(
+ runTest(`
+ hopp.test("throw with string message works", () => {
+ hopp.expect(() => { throw new Error('salmon') }).to.throw('salmon')
+ hopp.expect(() => { throw new Error('exact message') }).to.throw('exact message')
+ hopp.expect(() => { throw new Error('contains test word') }).to.throw('test')
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "throw with string message works",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected .* to throw 'salmon'/
+ ),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected .* to throw 'exact message'/
+ ),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/Expected .* to throw 'test'/),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should support `.throw()` with regex message matching", () => {
+ return expect(
+ runTest(`
+ hopp.test("throw with regex works", () => {
+ hopp.expect(() => { throw new Error('illegal salmon!') }).to.throw(/salmon/)
+ hopp.expect(() => { throw new Error('TEST123') }).to.throw(/test\\d+/i)
+ hopp.expect(() => { throw new Error('error: something went wrong') }).to.throw(/^error:/)
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "throw with regex works",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected .* to throw \/salmon\//
+ ),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected .* to throw \/test\\d\+\/i/
+ ),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected .* to throw \/\^error:\//
+ ),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should support `.throw()` with both error type and message", () => {
+ return expect(
+ runTest(`
+ hopp.test("throw with type and message works", () => {
+ hopp.expect(() => { throw new RangeError('out of range') }).to.throw(RangeError, 'out of range')
+ hopp.expect(() => { throw new TypeError('wrong type') }).to.throw(TypeError, /wrong/)
+ hopp.expect(() => { throw new Error('specific error') }).to.throw(Error, 'specific error')
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "throw with type and message works",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected .* to throw RangeError, 'out of range'/
+ ),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected .* to throw TypeError, \/wrong\//
+ ),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected .* to throw Error, 'specific error'/
+ ),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should support custom error classes with `.throw()`", () => {
+ return expect(
+ runTest(`
+ hopp.test("custom error classes work", () => {
+ class CustomError extends Error {
+ constructor(message) {
+ super(message)
+ this.name = 'CustomError'
+ }
+ }
+
+ hopp.expect(() => { throw new CustomError('custom message') }).to.throw(CustomError)
+ hopp.expect(() => { throw new CustomError('specific text') }).to.throw(CustomError, /specific/)
+ hopp.expect(() => { throw new CustomError('exact') }).to.throw(CustomError, 'exact')
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "custom error classes work",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected .* to throw CustomError/
+ ),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected .* to throw CustomError, \/specific\//
+ ),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected .* to throw CustomError, 'exact'/
+ ),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should support `.throws()` and `.Throw()` aliases", () => {
+ return expect(
+ runTest(`
+ hopp.test("throw aliases work", () => {
+ hopp.expect(() => { throw new Error('test1') }).to.throws()
+ hopp.expect(() => { throw new Error('test2') }).to.Throw()
+ hopp.expect(() => { throw new TypeError() }).to.throws(TypeError)
+ hopp.expect(() => { throw new Error('message') }).to.Throw('message')
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "throw aliases work",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(/Expected .* to throw/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/Expected .* to throw/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/Expected .* to throw/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/Expected .* to throw/),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+ })
+
+ describe("instanceof Assertions", () => {
+ test("should support `.instanceof()` with built-in types", () => {
+ return expect(
+ runTest(`
+ hopp.test("instanceof with built-in types works", () => {
+ hopp.expect([1, 2]).to.be.an.instanceof(Array)
+ hopp.expect(new Date()).to.be.an.instanceof(Date)
+ hopp.expect(new Error()).to.be.an.instanceof(Error)
+ hopp.expect(new RegExp('test')).to.be.an.instanceof(RegExp)
+ hopp.expect(new Map()).to.be.an.instanceof(Map)
+ hopp.expect(new Set()).to.be.an.instanceof(Set)
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "instanceof with built-in types works",
+ expectResults: expect.arrayContaining([
+ {
+ status: "pass",
+ message: "Expected [1, 2] to be an instanceof Array",
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be an instanceof Date/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be an instanceof Error/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be an instanceof RegExp/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be an instanceof Map/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be an instanceof Set/),
+ },
+ ]),
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should support `.instanceof()` with error types", () => {
+ return expect(
+ runTest(`
+ hopp.test("instanceof with error types works", () => {
+ hopp.expect(new Error()).to.be.an.instanceof(Error)
+ hopp.expect(new TypeError()).to.be.an.instanceof(TypeError)
+ hopp.expect(new RangeError()).to.be.an.instanceof(RangeError)
+ hopp.expect(new TypeError()).to.be.an.instanceof(Error)
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "instanceof with error types works",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be an instanceof/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be an instanceof/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be an instanceof/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be an instanceof/),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should support `.instanceOf()` alias", () => {
+ return expect(
+ runTest(`
+ hopp.test("instanceOf alias works", () => {
+ hopp.expect([]).to.be.an.instanceof(Array)
+ hopp.expect(new Date()).to.be.an.instanceof(Date)
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "instanceOf alias works",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be an instanceof/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be an instanceof/),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should support negation with `.not.instanceof()`", () => {
+ return expect(
+ runTest(`
+ hopp.test("not instanceof works", () => {
+ hopp.expect({}).to.not.be.an.instanceof(Array)
+ hopp.expect([]).to.not.be.an.instanceof(Date)
+ hopp.expect('string').to.not.be.an.instanceof(Number)
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "not instanceof works",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected .* to not be an instanceof/
+ ),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected .* to not be an instanceof/
+ ),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected .* to not be an instanceof/
+ ),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+ })
+
+ describe("respondTo Assertions", () => {
+ test("should support `.respondTo()` for prototype methods", () => {
+ return expect(
+ runTest(`
+ hopp.test("respondTo with prototype methods works", () => {
+ function Cat() {}
+ Cat.prototype.meow = function() { return 'meow' }
+
+ const cat = new Cat()
+ hopp.expect(cat).to.respondTo('meow')
+ hopp.expect(Cat).to.respondTo('meow')
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "respondTo with prototype methods works",
+ expectResults: [
+ {
+ status: "pass",
+ message: "Expected {} to respondTo 'meow'",
+ },
+ {
+ status: "pass",
+ message: "Expected function Cat() {} to respondTo 'meow'",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should support `.itself.respondTo()` for static methods", () => {
+ return expect(
+ runTest(`
+ hopp.test("itself respondTo for static methods works", () => {
+ function Dog() {}
+ Dog.prototype.bark = function() { return 'bark' }
+ Dog.woof = function() { return 'woof' }
+
+ hopp.expect(Dog).itself.to.respondTo('woof')
+ hopp.expect(Dog).to.respondTo('bark')
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "itself respondTo for static methods works",
+ expectResults: [
+ {
+ status: "pass",
+ message: "Expected function Dog() {} to respondTo 'woof'",
+ },
+ {
+ status: "pass",
+ message: "Expected function Dog() {} to respondTo 'bark'",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should support `.respondTo()` with constructor functions", () => {
+ return expect(
+ runTest(`
+ hopp.test("respondTo with constructor functions works", () => {
+ function Animal() {}
+ Animal.prototype.speak = function() { return 'sound' }
+ Animal.create = function() { return new Animal() }
+
+ const animal = new Animal()
+ hopp.expect(animal).to.respondTo('speak')
+ hopp.expect(Animal).to.respondTo('speak')
+ hopp.expect(Animal).itself.to.respondTo('create')
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "respondTo with constructor functions works",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(/to respondTo 'speak'/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to respondTo 'speak'/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to respondTo 'create'/),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should support `.respondsTo()` alias", () => {
+ return expect(
+ runTest(`
+ hopp.test("respondsTo alias works", () => {
+ function TestObj() {}
+ TestObj.prototype.method = function() { return 'test' }
+
+ const obj = new TestObj()
+ hopp.expect(obj).to.respondsTo('method')
+ hopp.expect(TestObj).respondsTo('method')
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "respondsTo alias works",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(/to respondTo 'method'/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to respondTo 'method'/),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should support negation with `.not.respondTo()`", () => {
+ return expect(
+ runTest(`
+ hopp.test("not respondTo works", () => {
+ function Bird() {}
+ Bird.prototype.fly = function() { return 'flying' }
+
+ const bird = new Bird()
+ hopp.expect(bird).to.not.respondTo('swim')
+ hopp.expect(Bird).to.not.respondTo('nonexistent')
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "not respondTo works",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected .* to not respondTo 'swim'/
+ ),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected .* to not respondTo 'nonexistent'/
+ ),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+ })
+
+ describe("satisfy Assertions", () => {
+ test("should support `.satisfy()` with simple predicates", () => {
+ return expect(
+ runTest(`
+ hopp.test("satisfy with simple predicates works", () => {
+ hopp.expect(1).to.satisfy(function(num) { return num > 0 })
+ hopp.expect(10).to.satisfy(function(num) { return num % 2 === 0 })
+ hopp.expect('hello').to.satisfy(function(str) { return str.length === 5 })
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "satisfy with simple predicates works",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected 1 to satisfy function/
+ ),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected 10 to satisfy function/
+ ),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected 'hello' to satisfy function/
+ ),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should support `.satisfy()` with complex object validation", () => {
+ return expect(
+ runTest(`
+ hopp.test("satisfy with complex objects works", () => {
+ hopp.expect({name: 'Alice', age: 30, active: true}).to.satisfy(function(obj) {
+ return obj.name.length > 3 && obj.age >= 18 && obj.active === true
+ })
+
+ hopp.expect([1, 2, 3, 4, 5]).to.satisfy(function(arr) {
+ return arr.every(n => n > 0) && arr.length === 5
+ })
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "satisfy with complex objects works",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected \{name: 'Alice', age: 30, active: true\} to satisfy function/
+ ),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected \[1, 2, 3, 4, 5\] to satisfy function/
+ ),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should support `.satisfy()` with regex validation", () => {
+ return expect(
+ runTest(`
+ hopp.test("satisfy with regex works", () => {
+ hopp.expect('user@example.com').to.satisfy(function(email) {
+ return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email)
+ })
+
+ hopp.expect('abc123').to.satisfy(function(str) {
+ return /^[a-z]+\\d+$/.test(str)
+ })
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "satisfy with regex works",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected 'user@example\.com' to satisfy function/
+ ),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected 'abc123' to satisfy function/
+ ),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should support `.satisfy()` with multi-condition predicates", () => {
+ return expect(
+ runTest(`
+ hopp.test("satisfy with multi-conditions works", () => {
+ hopp.expect(42).to.satisfy(function(n) {
+ return n % 2 === 0 && n > 40 && n < 50
+ })
+
+ hopp.expect([1,2,3]).to.satisfy(function(arr) {
+ return arr.length === 3 && arr[0] === 1 && arr.reduce((a, b) => a + b) === 6
+ })
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "satisfy with multi-conditions works",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected 42 to satisfy function/
+ ),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected \[1, 2, 3\] to satisfy function/
+ ),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should support `.satisfies()` alias", () => {
+ return expect(
+ runTest(`
+ hopp.test("satisfies alias works", () => {
+ hopp.expect(5).to.satisfies(function(n) { return n > 0 })
+ hopp.expect('test').satisfies(function(s) { return s.length > 0 })
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "satisfies alias works",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected 5 to satisfy function/
+ ),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected 'test' to satisfy function/
+ ),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should support negation with `.not.satisfy()`", () => {
+ return expect(
+ runTest(`
+ hopp.test("not satisfy works", () => {
+ hopp.expect(1).to.not.satisfy(function(num) { return num < 0 })
+ hopp.expect('hello').to.not.satisfy(function(str) { return str.length > 10 })
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "not satisfy works",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected 1 to not satisfy function/
+ ),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected 'hello' to not satisfy function/
+ ),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+ })
+
+ describe("Arguments Object Detection", () => {
+ test("should support `.arguments` assertion for arguments objects", () => {
+ return expect(
+ runTest(`
+ hopp.test("arguments detection works", () => {
+ function testFunc() {
+ hopp.expect(arguments).to.be.arguments
+ }
+ testFunc(1, 2, 3)
+
+ hopp.expect([1, 2, 3]).to.not.be.arguments
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "arguments detection works",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(/Expected .* to be arguments/),
+ },
+ {
+ status: "pass",
+ message: "Expected [1, 2, 3] to not be arguments",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should support `.Arguments` alias", () => {
+ return expect(
+ runTest(`
+ hopp.test("Arguments alias works", () => {
+ function testFunc() {
+ hopp.expect(arguments).to.be.Arguments
+ }
+ testFunc(1, 2, 3)
+
+ hopp.expect({0: 1, 1: 2, length: 2}).to.not.be.Arguments
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "Arguments alias works",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be Arguments/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to not be Arguments/),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should distinguish arguments from array-like objects", () => {
+ return expect(
+ runTest(`
+ hopp.test("arguments vs array-like objects works", () => {
+ function testFunc() {
+ hopp.expect(arguments).to.be.arguments
+ }
+ testFunc()
+
+ // Array-like objects should not be considered arguments
+ hopp.expect([]).to.not.be.arguments
+ hopp.expect({length: 0}).to.not.be.arguments
+ hopp.expect({0: 'a', 1: 'b', length: 2}).to.not.be.arguments
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "arguments vs array-like objects works",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(/Expected .* to be arguments/),
+ },
+ {
+ status: "pass",
+ message: "Expected [] to not be arguments",
+ },
+ {
+ status: "pass",
+ message: "Expected {length: 0} to not be arguments",
+ },
+ {
+ status: "pass",
+ message:
+ "Expected {0: 'a', 1: 'b', length: 2} to not be arguments",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+ })
+
+ describe("Object State Assertions", () => {
+ test("should support `.extensible` assertion for extensible objects", () => {
+ return expect(
+ runTest(`
+ hopp.test("extensible assertion works", () => {
+ hopp.expect({a: 1}).to.be.extensible
+ hopp.expect([1, 2, 3]).to.be.extensible
+
+ const obj = {}
+ hopp.expect(obj).to.be.extensible
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "extensible assertion works",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be extensible/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be extensible/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be extensible/),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should support `.sealed` assertion for sealed objects", () => {
+ return expect(
+ runTest(`
+ hopp.test("sealed assertion works", () => {
+ hopp.expect(Object.seal({})).to.be.sealed
+ hopp.expect(Object.seal({a: 1})).to.be.sealed
+
+ const obj = {}
+ hopp.expect(obj).to.not.be.sealed
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "sealed assertion works",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be sealed/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be sealed/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to not be sealed/),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should support `.frozen` assertion for frozen objects", () => {
+ return expect(
+ runTest(`
+ hopp.test("frozen assertion works", () => {
+ hopp.expect(Object.freeze({})).to.be.frozen
+ hopp.expect(Object.freeze({a: 1})).to.be.frozen
+
+ const obj = {}
+ hopp.expect(obj).to.not.be.frozen
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "frozen assertion works",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be frozen/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be frozen/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to not be frozen/),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should understand that frozen objects are also sealed", () => {
+ return expect(
+ runTest(`
+ hopp.test("frozen implies sealed works", () => {
+ const frozen = Object.freeze({a: 1})
+ hopp.expect(frozen).to.be.frozen
+ hopp.expect(frozen).to.be.sealed
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "frozen implies sealed works",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be frozen/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be sealed/),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should handle primitives as sealed and frozen", () => {
+ return expect(
+ runTest(`
+ hopp.test("primitives are sealed and frozen", () => {
+ hopp.expect(1).to.be.sealed
+ hopp.expect(1).to.be.frozen
+ hopp.expect('string').to.be.sealed
+ hopp.expect('string').to.be.frozen
+ hopp.expect(true).to.be.sealed
+ hopp.expect(true).to.be.frozen
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "primitives are sealed and frozen",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be sealed/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be frozen/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be sealed/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be frozen/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be sealed/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be frozen/),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should understand the relationship between extensible, sealed, and frozen", () => {
+ return expect(
+ runTest(`
+ hopp.test("object state relationships work", () => {
+ const extensible = {a: 1}
+ const sealed = Object.seal({b: 2})
+ const frozen = Object.freeze({c: 3})
+
+ // Extensible is not sealed or frozen
+ hopp.expect(extensible).to.be.extensible
+ hopp.expect(extensible).to.not.be.sealed
+ hopp.expect(extensible).to.not.be.frozen
+
+ // Sealed is not extensible, not frozen (but can be)
+ hopp.expect(sealed).to.not.be.extensible
+ hopp.expect(sealed).to.be.sealed
+ hopp.expect(sealed).to.not.be.frozen
+
+ // Frozen is not extensible, but is sealed
+ hopp.expect(frozen).to.not.be.extensible
+ hopp.expect(frozen).to.be.sealed
+ hopp.expect(frozen).to.be.frozen
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "object state relationships work",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be extensible/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to not be sealed/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to not be frozen/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to not be extensible/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be sealed/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to not be frozen/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to not be extensible/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be sealed/),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(/to be frozen/),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+ })
+
+ describe("Edge Cases and Error Scenarios", () => {
+ test("should handle throw assertion failures gracefully", () => {
+ return expect(
+ runTest(`
+ hopp.test("throw failures work", () => {
+ hopp.expect(() => {}).to.throw()
+ hopp.expect(() => { throw new Error() }).to.not.throw()
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "throw failures work",
+ expectResults: [
+ {
+ status: "fail",
+ message: expect.stringMatching(/Expected .* to throw/),
+ },
+ {
+ status: "fail",
+ message: expect.stringMatching(/Expected .* to not throw/),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should handle instanceof failures gracefully", () => {
+ return expect(
+ runTest(`
+ hopp.test("instanceof failures work", () => {
+ hopp.expect([]).to.be.an.instanceof(Date)
+ hopp.expect(new Date()).to.not.be.an.instanceof(Date)
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "instanceof failures work",
+ expectResults: [
+ {
+ status: "fail",
+ message: expect.stringMatching(
+ /Expected .* to be an instanceof/
+ ),
+ },
+ {
+ status: "fail",
+ message: expect.stringMatching(
+ /Expected .* to not be an instanceof/
+ ),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should handle satisfy failures with clear messages", () => {
+ return expect(
+ runTest(`
+ hopp.test("satisfy failures work", () => {
+ hopp.expect(5).to.satisfy(function(n) { return n < 0 })
+ hopp.expect('hello').to.not.satisfy(function(s) { return s.length === 5 })
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "satisfy failures work",
+ expectResults: [
+ {
+ status: "fail",
+ message: expect.stringMatching(
+ /Expected 5 to satisfy function/
+ ),
+ },
+ {
+ status: "fail",
+ message: expect.stringMatching(
+ /Expected 'hello' to not satisfy function/
+ ),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+ })
+})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/chai-powered-assertions/properties-collections.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/chai-powered-assertions/properties-collections.spec.ts
new file mode 100644
index 00000000..87ec921b
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/chai-powered-assertions/properties-collections.spec.ts
@@ -0,0 +1,1075 @@
+/**
+ * @see https://github.com/hoppscotch/hoppscotch/discussions/5221
+ */
+
+import { describe, expect, test } from "vitest"
+import { runTest } from "~/utils/test-helpers"
+
+describe("`hopp.expect` - Properties and Collections Assertions", () => {
+ describe("Property Assertions", () => {
+ test("should assert basic property existence and values using `.property()`", () => {
+ return expect(
+ runTest(`
+ hopp.test("property assertions work", () => {
+ hopp.expect({a: 1}).to.have.property('a')
+ hopp.expect({a: 1}).to.have.property('a', 1)
+ hopp.expect({x: {a: 1}}).to.have.deep.property('x', {a: 1})
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "property assertions work",
+ expectResults: [
+ {
+ status: "pass",
+ message: "Expected {a: 1} to have property 'a'",
+ },
+ {
+ status: "pass",
+ message: "Expected {a: 1} to have property 'a', 1",
+ },
+ {
+ status: "pass",
+ message:
+ "Expected {x: {a: 1}} to have deep property 'x', {a: 1}",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should assert own properties using `.own.property()`", () => {
+ return expect(
+ runTest(`
+ hopp.test("own property assertions work", () => {
+ hopp.expect({a: 1}).to.have.own.property('a')
+ hopp.expect({a: 1}).to.have.own.property('a', 1)
+ hopp.expect({x: {a: 1}}).to.have.deep.own.property('x', {a: 1})
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "own property assertions work",
+ expectResults: [
+ {
+ status: "pass",
+ message: "Expected {a: 1} to have own property 'a'",
+ },
+ {
+ status: "pass",
+ message: "Expected {a: 1} to have own property 'a', 1",
+ },
+ {
+ status: "pass",
+ message:
+ "Expected {x: {a: 1}} to have deep own property 'x', {a: 1}",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should assert property descriptors using `.ownPropertyDescriptor()`", () => {
+ return expect(
+ runTest(`
+ hopp.test("ownPropertyDescriptor assertions work", () => {
+ const obj = {}
+ Object.defineProperty(obj, 'test', {
+ value: 42,
+ writable: false,
+ enumerable: true,
+ configurable: false
+ })
+
+ hopp.expect(obj).to.have.ownPropertyDescriptor('test')
+ hopp.expect(obj).to.have.ownPropertyDescriptor('test', {
+ value: 42,
+ writable: false,
+ enumerable: true,
+ configurable: false
+ })
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "ownPropertyDescriptor assertions work",
+ expectResults: [
+ {
+ status: "pass",
+ message: "Expected {} to have ownPropertyDescriptor 'test'",
+ },
+ {
+ status: "pass",
+ message: "Expected {} to have ownPropertyDescriptor 'test'",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+ })
+
+ describe("Nested Property Assertions", () => {
+ test("should assert nested properties using `.nested.property()`", () => {
+ return expect(
+ runTest(`
+ hopp.test("nested property assertions work", () => {
+ hopp.expect({a: {b: ['x', 'y']}}).to.have.nested.property('a.b[1]')
+ hopp.expect({a: {b: ['x', 'y']}}).to.have.nested.property('a.b[1]', 'y')
+ hopp.expect({a: {b: ['x', 'y']}}).to.have.nested.property('a.b[0]', 'x')
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "nested property assertions work",
+ expectResults: [
+ {
+ status: "pass",
+ message:
+ "Expected {a: {b: ['x', 'y']}} to have nested property 'a.b[1]'",
+ },
+ {
+ status: "pass",
+ message:
+ "Expected {a: {b: ['x', 'y']}} to have nested property 'a.b[1]', 'y'",
+ },
+ {
+ status: "pass",
+ message:
+ "Expected {a: {b: ['x', 'y']}} to have nested property 'a.b[0]', 'x'",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should assert complex nested properties using `.deep.nested.property()`", () => {
+ return expect(
+ runTest(`
+ hopp.test("deep nested property testing work", () => {
+ const complexObj = {
+ user: {
+ profile: {
+ settings: {
+ theme: 'dark',
+ notifications: {
+ email: true,
+ push: false
+ }
+ },
+ metadata: ['tag1', 'tag2', 'tag3']
+ }
+ },
+ array: [
+ {nested: {deep: 'value'}},
+ {nested: {deeper: {deepest: 42}}}
+ ]
+ }
+
+ hopp.expect(complexObj).to.have.deep.nested.property('user.profile.settings', {
+ theme: 'dark',
+ notifications: {
+ email: true,
+ push: false
+ }
+ })
+
+ hopp.expect(complexObj).to.have.nested.property('user.profile.metadata[1]', 'tag2')
+ hopp.expect(complexObj).to.have.nested.property('array[0].nested.deep', 'value')
+ hopp.expect(complexObj).to.have.nested.property('array[1].nested.deeper.deepest', 42)
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "deep nested property testing work",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected .* to have deep nested property 'user\.profile\.settings'/
+ ),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected .* to have nested property 'user\.profile\.metadata\[1\]', 'tag2'/
+ ),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected .* to have nested property 'array\[0\]\.nested\.deep', 'value'/
+ ),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected .* to have nested property 'array\[1\]\.nested\.deeper\.deepest', 42/
+ ),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should negate nested property assertions", () => {
+ return expect(
+ runTest(`
+ hopp.test("negation with nested properties work", () => {
+ hopp.expect({a: 1}).to.not.have.own.property('b')
+ hopp.expect({a: {b: 1}}).to.not.have.nested.property('a.c')
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "negation with nested properties work",
+ expectResults: [
+ {
+ status: "pass",
+ message: "Expected {a: 1} to not have own property 'b'",
+ },
+ {
+ status: "pass",
+ message:
+ "Expected {a: {b: 1}} to not have nested property 'a.c'",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+ })
+
+ describe("Keys Assertions", () => {
+ test("should assert all keys using `.all.keys()`", () => {
+ return expect(
+ runTest(`
+ hopp.test("all keys assertions work", () => {
+ hopp.expect({a: 1, b: 2}).to.have.all.keys('a', 'b')
+ hopp.expect(['x', 'y']).to.have.all.keys(0, 1)
+ hopp.expect({a: 1, b: 2}).to.have.all.keys(['a', 'b'])
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "all keys assertions work",
+ expectResults: [
+ {
+ status: "pass",
+ message: "Expected {a: 1, b: 2} to have all keys 'a', 'b'",
+ },
+ {
+ status: "pass",
+ message: "Expected ['x', 'y'] to have all keys '0', '1'",
+ },
+ {
+ status: "pass",
+ message: "Expected {a: 1, b: 2} to have all keys 'a', 'b'",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should assert any keys using `.any.keys()`", () => {
+ return expect(
+ runTest(`
+ hopp.test("any keys assertions work", () => {
+ hopp.expect({a: 1, b: 2}).to.have.any.keys('a', 'c')
+ hopp.expect({a: 1, b: 2}).to.have.any.keys(['a', 'z'])
+ hopp.expect({a: 1, b: 2, c: 3, d: 4}).to.have.any.keys('x', 'y', 'z', 'a')
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "any keys assertions work",
+ expectResults: [
+ {
+ status: "pass",
+ message: "Expected {a: 1, b: 2} to have any keys 'a', 'c'",
+ },
+ {
+ status: "pass",
+ message: "Expected {a: 1, b: 2} to have any keys 'a', 'z'",
+ },
+ {
+ status: "pass",
+ message:
+ "Expected {a: 1, b: 2, c: 3, d: 4} to have any keys 'x', 'y', 'z', 'a'",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should assert included keys using `.include.keys()`", () => {
+ return expect(
+ runTest(`
+ hopp.test("include keys assertions work", () => {
+ hopp.expect({a: 1, b: 2, c: 3}).to.include.all.keys('a', 'b')
+ hopp.expect({a: 1, b: 2, c: 3, d: 4}).to.include.any.keys('a', 'z')
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "include keys assertions work",
+ expectResults: [
+ {
+ status: "pass",
+ message:
+ "Expected {a: 1, b: 2, c: 3} to include all keys 'a', 'b'",
+ },
+ {
+ status: "pass",
+ message:
+ "Expected {a: 1, b: 2, c: 3, d: 4} to include any keys 'a', 'z'",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should negate keys assertions", () => {
+ return expect(
+ runTest(`
+ hopp.test("negation with keys work", () => {
+ hopp.expect({a: 1, b: 2}).to.not.have.any.keys('x', 'y', 'z')
+ hopp.expect({a: 1, b: 2}).to.not.have.all.keys('a', 'b', 'c')
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "negation with keys work",
+ expectResults: [
+ {
+ status: "pass",
+ message:
+ "Expected {a: 1, b: 2} to not have any keys 'x', 'y', 'z'",
+ },
+ {
+ status: "pass",
+ message:
+ "Expected {a: 1, b: 2} to not have all keys 'a', 'b', 'c'",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+ })
+
+ describe("Members Assertions", () => {
+ test("should assert array members using `.members()`", () => {
+ return expect(
+ runTest(`
+ hopp.test("member assertions work", () => {
+ hopp.expect([1, 2, 3]).to.have.members([2, 1, 3])
+ hopp.expect([1, 2, 2]).to.have.members([2, 1, 2])
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "member assertions work",
+ expectResults: [
+ {
+ status: "pass",
+ message: "Expected [1, 2, 3] to have members [2, 1, 3]",
+ },
+ {
+ status: "pass",
+ message: "Expected [1, 2, 2] to have members [2, 1, 2]",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should assert included members using `.include.members()`", () => {
+ return expect(
+ runTest(`
+ hopp.test("include members assertions work", () => {
+ hopp.expect([1, 2, 3]).to.include.members([1, 2])
+ hopp.expect([1, 2, 3, 4]).to.include.members([2, 3])
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "include members assertions work",
+ expectResults: [
+ {
+ status: "pass",
+ message: "Expected [1, 2, 3] to include members [1, 2]",
+ },
+ {
+ status: "pass",
+ message: "Expected [1, 2, 3, 4] to include members [2, 3]",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should assert deep members using `.deep.members()`", () => {
+ return expect(
+ runTest(`
+ hopp.test("deep members assertions work", () => {
+ hopp.expect([{a: 1}]).to.have.deep.members([{a: 1}])
+ hopp.expect([{a: 1}, {b: 2}]).to.have.deep.members([{b: 2}, {a: 1}])
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "deep members assertions work",
+ expectResults: [
+ {
+ status: "pass",
+ message: "Expected [{a: 1}] to have deep members [{a: 1}]",
+ },
+ {
+ status: "pass",
+ message:
+ "Expected [{a: 1}, {b: 2}] to have deep members [{b: 2}, {a: 1}]",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should assert ordered members using `.ordered.members()`", () => {
+ return expect(
+ runTest(`
+ hopp.test("ordered members assertions work", () => {
+ hopp.expect([1, 2, 3]).to.have.ordered.members([1, 2, 3])
+ hopp.expect([1, 2]).to.not.have.ordered.members([2, 1])
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "ordered members assertions work",
+ expectResults: [
+ {
+ status: "pass",
+ message:
+ "Expected [1, 2, 3] to have ordered members [1, 2, 3]",
+ },
+ {
+ status: "pass",
+ message: "Expected [1, 2] to not have ordered members [2, 1]",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should assert deep ordered members using `.deep.ordered.members()`", () => {
+ return expect(
+ runTest(`
+ hopp.test("deep ordered members assertions work", () => {
+ hopp.expect([{a: 1}, {b: 2}]).to.have.deep.ordered.members([{a: 1}, {b: 2}])
+ hopp.expect([{a: 1}, {b: 2}]).to.not.have.deep.ordered.members([{b: 2}, {a: 1}])
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "deep ordered members assertions work",
+ expectResults: [
+ {
+ status: "pass",
+ message:
+ "Expected [{a: 1}, {b: 2}] to have deep ordered members [{a: 1}, {b: 2}]",
+ },
+ {
+ status: "pass",
+ message:
+ "Expected [{a: 1}, {b: 2}] to not have deep ordered members [{b: 2}, {a: 1}]",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should assert include deep ordered members using `.include.deep.ordered.members()`", () => {
+ return expect(
+ runTest(`
+ hopp.test("include deep ordered members assertions work", () => {
+ hopp.expect([{a: 1}, {b: 2}, {c: 3}]).to.include.deep.ordered.members([{a: 1}, {b: 2}])
+ hopp.expect([{a: 1}, {b: 2}, {c: 3}]).to.not.include.deep.ordered.members([{b: 2}, {a: 1}])
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "include deep ordered members assertions work",
+ expectResults: [
+ {
+ status: "pass",
+ message:
+ "Expected [{a: 1}, {b: 2}, {c: 3}] to include deep ordered members [{a: 1}, {b: 2}]",
+ },
+ {
+ status: "pass",
+ message:
+ "Expected [{a: 1}, {b: 2}, {c: 3}] to not include deep ordered members [{b: 2}, {a: 1}]",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should negate members assertions", () => {
+ return expect(
+ runTest(`
+ hopp.test("negation with members work", () => {
+ hopp.expect([{a: 1}]).to.not.have.deep.members([{b: 2}])
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "negation with members work",
+ expectResults: [
+ {
+ status: "pass",
+ message:
+ "Expected [{a: 1}] to not have deep members [{b: 2}]",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+ })
+
+ describe("Length Assertions", () => {
+ test("should assert length for arrays using `.lengthOf()`", () => {
+ return expect(
+ runTest(`
+ hopp.test("array length assertions work", () => {
+ hopp.expect([1, 2, 3]).to.have.lengthOf(3)
+ hopp.expect([1, 2, 3]).to.have.length(3)
+ hopp.expect([]).to.have.lengthOf(0)
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "array length assertions work",
+ expectResults: [
+ {
+ status: "pass",
+ message: "Expected [1, 2, 3] to have lengthOf 3",
+ },
+ {
+ status: "pass",
+ message: "Expected [1, 2, 3] to have length 3",
+ },
+ {
+ status: "pass",
+ message: "Expected [] to have lengthOf 0",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should assert length for strings using `.lengthOf()`", () => {
+ return expect(
+ runTest(`
+ hopp.test("string length assertions work", () => {
+ hopp.expect('foo').to.have.lengthOf(3)
+ hopp.expect('hello world').to.have.lengthOf(11)
+ hopp.expect('').to.have.lengthOf(0)
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "string length assertions work",
+ expectResults: [
+ {
+ status: "pass",
+ message: "Expected 'foo' to have lengthOf 3",
+ },
+ {
+ status: "pass",
+ message: "Expected 'hello world' to have lengthOf 11",
+ },
+ {
+ status: "pass",
+ message: "Expected '' to have lengthOf 0",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should assert length for Set using `.lengthOf()`", () => {
+ return expect(
+ runTest(`
+ hopp.test("Set length assertions work", () => {
+ hopp.expect(new Set([1, 2, 3])).to.have.lengthOf(3)
+ hopp.expect(new Set()).to.have.lengthOf(0)
+ hopp.expect(new Set([1, 2, 3])).to.be.an.instanceof(Set)
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "Set length assertions work",
+ expectResults: [
+ {
+ status: "pass",
+ message: "Expected new Set([1, 2, 3]) to have lengthOf 3",
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected .* to have lengthOf 0/
+ ),
+ },
+ {
+ status: "pass",
+ message:
+ "Expected new Set([1, 2, 3]) to be an instanceof Set",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should assert length for Map using `.lengthOf()`", () => {
+ return expect(
+ runTest(`
+ hopp.test("Map length assertions work", () => {
+ hopp.expect(new Map([['a', 1], ['b', 2], ['c', 3]])).to.have.lengthOf(3)
+ hopp.expect(new Map()).to.have.lengthOf(0)
+ hopp.expect(new Map([['a', 1]])).to.be.an.instanceof(Map)
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "Map length assertions work",
+ expectResults: [
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected .* to have lengthOf 3/
+ ),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected .* to have lengthOf 0/
+ ),
+ },
+ {
+ status: "pass",
+ message: expect.stringMatching(
+ /Expected .* to be an instanceof Map/
+ ),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should work with length in language chains", () => {
+ return expect(
+ runTest(`
+ hopp.test("language chains with length work", () => {
+ hopp.expect([1,2,3]).to.be.an('array').that.has.lengthOf(3)
+ hopp.expect([1,2,3]).to.be.an('array').and.have.lengthOf(3).and.include(2)
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "language chains with length work",
+ expectResults: expect.arrayContaining([
+ { status: "pass", message: expect.stringMatching(/array/) },
+ {
+ status: "pass",
+ message: expect.stringMatching(/lengthOf 3/),
+ },
+ { status: "pass", message: expect.stringMatching(/include 2/) },
+ ]),
+ }),
+ ],
+ }),
+ ])
+ })
+ })
+
+ describe("Include/Contain Assertions", () => {
+ test("should assert inclusion in strings using `.include()`", () => {
+ return expect(
+ runTest(`
+ hopp.test("string inclusion assertions work", () => {
+ hopp.expect('foobar').to.include('foo')
+ hopp.expect('foobar').to.contain('bar')
+ hopp.expect('hello world').to.include('world')
+ hopp.expect('test123').to.contain('123')
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "string inclusion assertions work",
+ expectResults: [
+ {
+ status: "pass",
+ message: "Expected 'foobar' to include 'foo'",
+ },
+ {
+ status: "pass",
+ message: "Expected 'foobar' to include 'bar'",
+ },
+ {
+ status: "pass",
+ message: "Expected 'hello world' to include 'world'",
+ },
+ {
+ status: "pass",
+ message: "Expected 'test123' to include '123'",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should assert inclusion in arrays using `.include()`", () => {
+ return expect(
+ runTest(`
+ hopp.test("array inclusion assertions work", () => {
+ hopp.expect([1, 2, 3]).to.include(2)
+ hopp.expect([1, 2, 3]).to.contain(3)
+ hopp.expect([1, 2, 3]).to.not.include(4)
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "array inclusion assertions work",
+ expectResults: [
+ { status: "pass", message: "Expected [1, 2, 3] to include 2" },
+ { status: "pass", message: "Expected [1, 2, 3] to include 3" },
+ {
+ status: "pass",
+ message: "Expected [1, 2, 3] to not include 4",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should assert inclusion in objects using `.include()`", () => {
+ return expect(
+ runTest(`
+ hopp.test("object inclusion assertions work", () => {
+ hopp.expect({a: 1, b: 2, c: 3}).to.include({a: 1, b: 2})
+ hopp.expect({a: 1, b: 2}).to.not.have.property('c')
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "object inclusion assertions work",
+ expectResults: [
+ {
+ status: "pass",
+ message:
+ "Expected {a: 1, b: 2, c: 3} to include {a: 1, b: 2}",
+ },
+ {
+ status: "pass",
+ message: "Expected {a: 1, b: 2} to not have property 'c'",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should assert deep inclusion using `.deep.include()`", () => {
+ return expect(
+ runTest(`
+ hopp.test("deep include assertions work", () => {
+ hopp.expect([{a: 1}]).to.deep.include({a: 1})
+ hopp.expect({x: {a: 1}}).to.deep.include({x: {a: 1}})
+ hopp.expect({a: {b: 2}, c: 3}).to.deep.include({a: {b: 2}})
+ hopp.expect([{name: 'Alice'}, {name: 'Bob'}]).to.deep.include({name: 'Alice'})
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "deep include assertions work",
+ expectResults: [
+ {
+ status: "pass",
+ message: "Expected [{a: 1}] to deep include {a: 1}",
+ },
+ {
+ status: "pass",
+ message: "Expected {x: {a: 1}} to deep include {x: {a: 1}}",
+ },
+ {
+ status: "pass",
+ message:
+ "Expected {a: {b: 2}, c: 3} to deep include {a: {b: 2}}",
+ },
+ {
+ status: "pass",
+ message:
+ "Expected [{name: 'Alice'}, {name: 'Bob'}] to deep include {name: 'Alice'}",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should assert oneOf inclusion using `.contain.oneOf()`", () => {
+ return expect(
+ runTest(`
+ hopp.test("oneOf inclusion assertions work", () => {
+ hopp.expect(1).to.be.oneOf([1, 2, 3])
+ hopp.expect('foo').to.be.oneOf(['foo', 'bar', 'baz'])
+ hopp.expect('Today is sunny').to.contain.oneOf(['sunny', 'cloudy'])
+ hopp.expect([1,2,3]).to.contain.oneOf([3,4,5])
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "oneOf inclusion assertions work",
+ expectResults: [
+ { status: "pass", message: "Expected 1 to be oneOf [1, 2, 3]" },
+ {
+ status: "pass",
+ message: "Expected 'foo' to be oneOf ['foo', 'bar', 'baz']",
+ },
+ {
+ status: "pass",
+ message:
+ "Expected 'Today is sunny' to include oneOf ['sunny', 'cloudy']",
+ },
+ {
+ status: "pass",
+ message: "Expected [1, 2, 3] to include oneOf [3, 4, 5]",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should support `.includes()` and `.contains()` aliases", () => {
+ return expect(
+ runTest(`
+ hopp.test("inclusion aliases work", () => {
+ hopp.expect([1, 2, 3]).to.includes(2)
+ hopp.expect('hello').to.contains('ell')
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "inclusion aliases work",
+ expectResults: [
+ { status: "pass", message: "Expected [1, 2, 3] to include 2" },
+ {
+ status: "pass",
+ message: "Expected 'hello' to include 'ell'",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+ })
+
+ describe("Combined Modifier Scenarios", () => {
+ test("should combine deep and own modifiers with properties", () => {
+ return expect(
+ runTest(`
+ hopp.test("deep + own combinations work", () => {
+ hopp.expect({x: {a: 1}}).to.have.deep.own.property('x', {a: 1})
+ hopp.expect({x: {a: 1}}).to.not.have.own.property('y')
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "deep + own combinations work",
+ expectResults: [
+ {
+ status: "pass",
+ message:
+ "Expected {x: {a: 1}} to have deep own property 'x', {a: 1}",
+ },
+ {
+ status: "pass",
+ message: "Expected {x: {a: 1}} to not have own property 'y'",
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+
+ test("should use properties in complex language chains", () => {
+ return expect(
+ runTest(`
+ hopp.test("complex chains with properties work", () => {
+ hopp.expect([1,2,3]).to.be.an('array').and.have.lengthOf(3).and.include(2)
+ hopp.expect({a: 1, b: 2}).to.be.an('object').that.has.property('a').which.equals(1)
+ })
+ `)()
+ ).resolves.toEqualRight([
+ expect.objectContaining({
+ descriptor: "root",
+ children: [
+ expect.objectContaining({
+ descriptor: "complex chains with properties 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'/),
+ },
+ ]),
+ }),
+ ],
+ }),
+ ])
+ })
+ })
+})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/chai-powered-assertions/strings-regex.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/chai-powered-assertions/strings-regex.spec.ts
new file mode 100644
index 00000000..73675590
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/chai-powered-assertions/strings-regex.spec.ts
@@ -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) },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+ })
+})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/delete.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/delete.spec.ts
index d22f1371..ca0f4cc5 100644
--- a/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/delete.spec.ts
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/delete.spec.ts
@@ -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())
})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/get.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/get.spec.ts
index 940a1f76..26607cea 100644
--- a/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/get.spec.ts
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/get.spec.ts
@@ -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([])
`,
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/getInitialRaw.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/getInitialRaw.spec.ts
index 76d64257..0db90551 100644
--- a/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/getInitialRaw.spec.ts
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/getInitialRaw.spec.ts
@@ -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("<>")
@@ -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("<>")
@@ -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("<>")
@@ -452,7 +430,7 @@ describe("hopp.env.global.getInitialRaw", () => {
test("errors for non-string key", () => {
return expect(
- func(
+ runTest(
`
hopp.env.global.getInitialRaw([])
`,
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/getRaw.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/getRaw.spec.ts
index 5c98a5f7..687578be 100644
--- a/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/getRaw.spec.ts
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/getRaw.spec.ts
@@ -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("<>")
@@ -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("<>")
@@ -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([])
`,
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/reset.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/reset.spec.ts
index 46036ad3..22cfe6ba 100644
--- a/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/reset.spec.ts
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/reset.spec.ts
@@ -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())
})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/setInitial.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/setInitial.spec.ts
index 83e8af56..84bbbf8d 100644
--- a/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/setInitial.spec.ts
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/setInitial.spec.ts
@@ -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", "<>")
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", "<>")
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)
`,
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/undefined-rejection.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/undefined-rejection.spec.ts
new file mode 100644
index 00000000..48ba7422
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/undefined-rejection.spec.ts
@@ -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,
+ },
+ ],
+ })
+ )
+ })
+})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/request.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/request.spec.ts
index ccf73107..a3de0b47 100644
--- a/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/request.spec.ts
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/request.spec.ts
@@ -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"],
+ }),
+ ],
+ })
+ )
+ })
+ })
})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/response.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/response.spec.ts
index cb57ee77..4afca3fa 100644
--- a/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/response.spec.ts
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/response.spec.ts
@@ -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'" },
+ ],
+ }),
+ })
+ )
+ })
+ })
})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/advanced-assertions.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/advanced-assertions.spec.ts
new file mode 100644
index 00000000..c6986b25
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/advanced-assertions.spec.ts
@@ -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'"
+ ),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/chai-advanced-features.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/chai-advanced-features.spec.ts
new file mode 100644
index 00000000..37cc0369
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/chai-advanced-features.spec.ts
@@ -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
+})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/chai-modifier-combinations.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/chai-modifier-combinations.spec.ts
new file mode 100644
index 00000000..3f2c2436
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/chai-modifier-combinations.spec.ts
@@ -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"),
+ }),
+ ],
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/cross-namespace-undefined.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/cross-namespace-undefined.spec.ts
new file mode 100644
index 00000000..2f891595
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/cross-namespace-undefined.spec.ts
@@ -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'",
+ },
+ ],
+ }),
+ ])
+ })
+})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/environment.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/environment.spec.ts
index 050d6146..64af9b0c 100644
--- a/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/environment.spec.ts
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/environment.spec.ts
@@ -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: [
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/expect-fail.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/expect-fail.spec.ts
new file mode 100644
index 00000000..9286a126
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/expect-fail.spec.ts
@@ -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",
+ }),
+ ],
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/info.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/info.spec.ts
index f987ca43..768a28b3 100644
--- a/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/info.spec.ts
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/info.spec.ts
@@ -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" }),
+ ]),
+ }),
+ ],
+ }),
+ ])
+ })
})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/map-set-size.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/map-set-size.spec.ts
new file mode 100644
index 00000000..d5ba8566
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/map-set-size.spec.ts
@@ -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" })],
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/pre-request-type-preservation.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/pre-request-type-preservation.spec.ts
new file mode 100644
index 00000000..66a8ed84
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/pre-request-type-preservation.spec.ts
@@ -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,
+ })
+ })
+ })
+})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/property-chaining.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/property-chaining.spec.ts
new file mode 100644
index 00000000..e19d8215
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/property-chaining.spec.ts
@@ -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" }),
+ ]),
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/request.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/request/basic.spec.ts
similarity index 81%
rename from packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/request.spec.ts
rename to packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/request/basic.spec.ts
index 3967fb19..bf84315e 100644
--- a/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/request.spec.ts
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/request/basic.spec.ts
@@ -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)
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/request/headers/propertylist-advanced.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/request/headers/propertylist-advanced.spec.ts
new file mode 100644
index 00000000..3b41798b
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/request/headers/propertylist-advanced.spec.ts
@@ -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"],
+ }),
+ ]),
+ })
+ )
+ })
+})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/request/headers/propertylist.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/request/headers/propertylist.spec.ts
new file mode 100644
index 00000000..67506fcc
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/request/headers/propertylist.spec.ts
@@ -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] }),
+ ]),
+ })
+ )
+ })
+})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/request/mutations.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/request/mutations.spec.ts
new file mode 100644
index 00000000..e089514c
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/request/mutations.spec.ts
@@ -0,0 +1,1039 @@
+import { HoppRESTAuth, HoppRESTRequest } from "@hoppscotch/data"
+import { describe, expect, test } from "vitest"
+
+import { runPreRequestScript } from "~/web"
+
+const baseRequest: HoppRESTRequest = {
+ v: "16",
+ name: "Test Request",
+ endpoint: "https://example.com/api/users?filter=active&sort=name",
+ method: "GET",
+ headers: [
+ {
+ key: "Content-Type",
+ value: "application/json",
+ active: true,
+ description: "",
+ },
+ {
+ key: "Authorization",
+ value: "Bearer token123",
+ 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 property mutations", () => {
+ test("pm.request.url.protocol mutation changes protocol from https to http", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ console.log("Initial protocol:", pm.request.url.protocol)
+ console.log("Initial URL:", pm.request.url.toString())
+
+ pm.request.url.protocol = 'http'
+
+ console.log("Updated protocol:", pm.request.url.protocol)
+ console.log("Updated URL:", pm.request.url.toString())
+ console.log("hopp.request.url:", hopp.request.url)
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ updatedRequest: expect.objectContaining({
+ endpoint: expect.stringContaining("http://example.com"),
+ }),
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({ args: ["Initial protocol:", "https"] }),
+ expect.objectContaining({ args: ["Updated protocol:", "http"] }),
+ expect.objectContaining({
+ args: expect.arrayContaining([
+ "Updated URL:",
+ expect.stringContaining("http://"),
+ ]),
+ }),
+ ]),
+ })
+ )
+ })
+
+ test("pm.request.url.host mutation changes hostname", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ console.log("Initial host:", pm.request.url.host)
+ console.log("Initial hostname:", pm.request.url.host.join('.'))
+
+ pm.request.url.host = ['api', 'newdomain', 'com']
+
+ console.log("Updated host:", pm.request.url.host)
+ console.log("Updated hostname:", pm.request.url.host.join('.'))
+ console.log("Updated URL:", pm.request.url.toString())
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ updatedRequest: expect.objectContaining({
+ endpoint: expect.stringContaining("api.newdomain.com"),
+ }),
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({
+ args: ["Initial host:", ["example", "com"]],
+ }),
+ expect.objectContaining({
+ args: ["Updated host:", ["api", "newdomain", "com"]],
+ }),
+ expect.objectContaining({
+ args: ["Updated hostname:", "api.newdomain.com"],
+ }),
+ ]),
+ })
+ )
+ })
+
+ test("pm.request.url.path mutation changes URL path", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ console.log("Initial path:", pm.request.url.path)
+
+ pm.request.url.path = ['v2', 'posts', '123']
+
+ console.log("Updated path:", pm.request.url.path)
+ console.log("Updated URL:", pm.request.url.toString())
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ updatedRequest: expect.objectContaining({
+ endpoint: expect.stringContaining("/v2/posts/123"),
+ }),
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({
+ args: ["Initial path:", ["api", "users"]],
+ }),
+ expect.objectContaining({
+ args: ["Updated path:", ["v2", "posts", "123"]],
+ }),
+ ]),
+ })
+ )
+ })
+
+ test("pm.request.url.port mutation changes port", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ console.log("Initial port:", pm.request.url.port)
+
+ pm.request.url.port = '8080'
+
+ console.log("Updated port:", pm.request.url.port)
+ console.log("Updated URL:", pm.request.url.toString())
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ updatedRequest: expect.objectContaining({
+ endpoint: expect.stringContaining(":8080"),
+ }),
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({ args: ["Initial port:", "443"] }),
+ expect.objectContaining({ args: ["Updated port:", "8080"] }),
+ ]),
+ })
+ )
+ })
+
+ test("multiple URL property mutations are applied sequentially", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ console.log("Initial URL:", pm.request.url.toString())
+
+ pm.request.url.protocol = 'http'
+ console.log("After protocol:", pm.request.url.toString())
+
+ pm.request.url.host = ['api', 'test', 'com']
+ console.log("After host:", pm.request.url.toString())
+
+ pm.request.url.path = ['v3', 'data']
+ console.log("After path:", pm.request.url.toString())
+
+ pm.request.url.port = '3000'
+ console.log("Final URL:", pm.request.url.toString())
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ updatedRequest: expect.objectContaining({
+ endpoint: "http://api.test.com:3000/v3/data?filter=active&sort=name",
+ }),
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({
+ args: expect.arrayContaining([
+ "Final URL:",
+ "http://api.test.com:3000/v3/data?filter=active&sort=name",
+ ]),
+ }),
+ ]),
+ })
+ )
+ })
+
+ test("pm.request.url.toString() returns dynamically updated URL", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ const url1 = pm.request.url.toString()
+ console.log("URL before mutation:", url1)
+
+ pm.request.url.protocol = 'http'
+ pm.request.url.path = ['updated']
+
+ const url2 = pm.request.url.toString()
+ console.log("URL after mutation:", url2)
+
+ console.log("URLs are different:", url1 !== url2)
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({
+ args: [
+ "URL before mutation:",
+ "https://example.com/api/users?filter=active&sort=name",
+ ],
+ }),
+ expect.objectContaining({
+ args: [
+ "URL after mutation:",
+ "http://example.com/updated?filter=active&sort=name",
+ ],
+ }),
+ expect.objectContaining({
+ args: ["URLs are different:", true],
+ }),
+ ]),
+ })
+ )
+ })
+})
+
+describe("pm.request.url.query methods", () => {
+ test("pm.request.url.query.add() adds new query parameter", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ console.log("Initial query params:", pm.request.url.query.all())
+
+ pm.request.url.query.add({ key: 'limit', value: '10' })
+
+ console.log("Updated query params:", pm.request.url.query.all())
+ console.log("Updated URL:", pm.request.url.toString())
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ updatedRequest: expect.objectContaining({
+ endpoint: expect.stringContaining("limit=10"),
+ }),
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({
+ args: ["Initial query params:", { filter: "active", sort: "name" }],
+ }),
+ expect.objectContaining({
+ args: [
+ "Updated query params:",
+ { filter: "active", sort: "name", limit: "10" },
+ ],
+ }),
+ ]),
+ })
+ )
+ })
+
+ test("pm.request.url.query.add() with empty value", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ pm.request.url.query.add({ key: 'flag' })
+
+ console.log("Query 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("flag="),
+ }),
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({
+ args: [
+ "Query params:",
+ { filter: "active", sort: "name", flag: "" },
+ ],
+ }),
+ ]),
+ })
+ )
+ })
+
+ test("pm.request.url.query.remove() removes existing query parameter", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ console.log("Initial query params:", pm.request.url.query.all())
+
+ pm.request.url.query.remove('filter')
+
+ console.log("Updated query params:", pm.request.url.query.all())
+ console.log("Updated URL:", pm.request.url.toString())
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ updatedRequest: expect.objectContaining({
+ endpoint: "https://example.com/api/users?sort=name",
+ }),
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({
+ args: ["Updated query params:", { sort: "name" }],
+ }),
+ ]),
+ })
+ )
+ })
+
+ test("pm.request.url.query.remove() handles non-existent parameter gracefully", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ console.log("Initial query params:", pm.request.url.query.all())
+
+ pm.request.url.query.remove('nonexistent')
+
+ console.log("Updated query params:", pm.request.url.query.all())
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ updatedRequest: expect.objectContaining({
+ endpoint: baseRequest.endpoint,
+ }),
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({
+ args: ["Updated query params:", { filter: "active", sort: "name" }],
+ }),
+ ]),
+ })
+ )
+ })
+
+ test("pm.request.url.query.all() returns all query parameters as object", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ const allParams = pm.request.url.query.all()
+ console.log("All params:", allParams)
+ console.log("Filter param:", allParams.filter)
+ console.log("Sort param:", allParams.sort)
+ console.log("Param count:", Object.keys(allParams).length)
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({
+ args: ["All params:", { filter: "active", sort: "name" }],
+ }),
+ expect.objectContaining({
+ args: ["Filter param:", "active"],
+ }),
+ expect.objectContaining({
+ args: ["Sort param:", "name"],
+ }),
+ expect.objectContaining({
+ args: ["Param count:", 2],
+ }),
+ ]),
+ })
+ )
+ })
+
+ test("multiple query mutations work correctly", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ console.log("Initial:", pm.request.url.query.all())
+
+ pm.request.url.query.add({ key: 'page', value: '1' })
+ console.log("After add:", pm.request.url.query.all())
+
+ pm.request.url.query.add({ key: 'limit', value: '20' })
+ console.log("After second add:", pm.request.url.query.all())
+
+ pm.request.url.query.remove('sort')
+ console.log("After remove:", pm.request.url.query.all())
+
+ console.log("Final URL:", pm.request.url.toString())
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ updatedRequest: expect.objectContaining({
+ endpoint:
+ "https://example.com/api/users?filter=active&page=1&limit=20",
+ }),
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({
+ args: [
+ "After remove:",
+ { filter: "active", page: "1", limit: "20" },
+ ],
+ }),
+ ]),
+ })
+ )
+ })
+})
+
+describe("pm.request.url string assignment", () => {
+ test("pm.request.url string assignment replaces entire URL", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ console.log("Initial URL:", pm.request.url.toString())
+
+ pm.request.url = "http://newapi.example.com:8080/v2/posts?id=5"
+
+ console.log("Updated URL:", pm.request.url.toString())
+ console.log("Updated protocol:", pm.request.url.protocol)
+ console.log("Updated host:", pm.request.url.host)
+ console.log("Updated path:", pm.request.url.path)
+ console.log("Updated query:", pm.request.url.query.all())
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ updatedRequest: expect.objectContaining({
+ endpoint: "http://newapi.example.com:8080/v2/posts?id=5",
+ }),
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({ args: ["Updated protocol:", "http"] }),
+ expect.objectContaining({
+ args: ["Updated host:", ["newapi", "example", "com"]],
+ }),
+ expect.objectContaining({ args: ["Updated path:", ["v2", "posts"]] }),
+ expect.objectContaining({ args: ["Updated query:", { id: "5" }] }),
+ ]),
+ })
+ )
+ })
+
+ test("pm.request.url string assignment followed by property mutations", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ pm.request.url = "https://api.example.com/users"
+ console.log("After string assignment:", pm.request.url.toString())
+
+ pm.request.url.path = ['posts', '42']
+ console.log("After path mutation:", pm.request.url.toString())
+
+ pm.request.url.query.add({ key: 'include', value: 'comments' })
+ console.log("Final URL:", pm.request.url.toString())
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ updatedRequest: expect.objectContaining({
+ endpoint: "https://api.example.com/posts/42?include=comments",
+ }),
+ })
+ )
+ })
+})
+
+describe("pm.request method mutations", () => {
+ test("pm.request.method mutation changes HTTP method", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ console.log("Initial method:", pm.request.method)
+
+ pm.request.method = 'POST'
+
+ console.log("Updated method:", pm.request.method)
+ console.log("hopp.request.method:", hopp.request.method)
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ updatedRequest: expect.objectContaining({
+ method: "POST",
+ }),
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({ args: ["Initial method:", "GET"] }),
+ expect.objectContaining({ args: ["Updated method:", "POST"] }),
+ expect.objectContaining({ args: ["hopp.request.method:", "POST"] }),
+ ]),
+ })
+ )
+ })
+
+ test("pm.request.method preserves case (matches Postman)", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ pm.request.method = 'put'
+ console.log("Method (lowercase input):", pm.request.method)
+
+ pm.request.method = 'DeLeTe'
+ console.log("Method (mixed case input):", pm.request.method)
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ updatedRequest: expect.objectContaining({
+ method: "DeLeTe",
+ }),
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({
+ args: ["Method (lowercase input):", "put"],
+ }),
+ expect.objectContaining({
+ args: ["Method (mixed case input):", "DeLeTe"],
+ }),
+ ]),
+ })
+ )
+ })
+})
+
+describe("pm.request.headers mutations", () => {
+ test("pm.request.headers.add() adds new header", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ console.log("Initial X-Custom-Header:", pm.request.headers.get("X-Custom-Header"))
+
+ pm.request.headers.add({ key: 'X-Custom-Header', value: 'custom-value' })
+
+ console.log("Updated X-Custom-Header:", pm.request.headers.get("X-Custom-Header"))
+ console.log("Has X-Custom-Header:", pm.request.headers.has("X-Custom-Header"))
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ updatedRequest: expect.objectContaining({
+ headers: expect.arrayContaining([
+ expect.objectContaining({
+ key: "X-Custom-Header",
+ value: "custom-value",
+ }),
+ ]),
+ }),
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({ args: ["Initial X-Custom-Header:", null] }),
+ expect.objectContaining({
+ args: ["Updated X-Custom-Header:", "custom-value"],
+ }),
+ expect.objectContaining({ args: ["Has X-Custom-Header:", true] }),
+ ]),
+ })
+ )
+ })
+
+ test("pm.request.headers.upsert() updates existing header", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ console.log("Initial Content-Type:", pm.request.headers.get("Content-Type"))
+
+ pm.request.headers.upsert({ key: 'Content-Type', value: 'application/xml' })
+
+ console.log("Updated Content-Type:", pm.request.headers.get("Content-Type"))
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ updatedRequest: expect.objectContaining({
+ headers: expect.arrayContaining([
+ expect.objectContaining({
+ key: "Content-Type",
+ value: "application/xml",
+ }),
+ ]),
+ }),
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({
+ args: ["Initial Content-Type:", "application/json"],
+ }),
+ expect.objectContaining({
+ args: ["Updated Content-Type:", "application/xml"],
+ }),
+ ]),
+ })
+ )
+ })
+
+ test("pm.request.headers.upsert() adds header if not exists", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ console.log("Initial X-New-Header:", pm.request.headers.get("X-New-Header"))
+
+ pm.request.headers.upsert({ key: 'X-New-Header', value: 'new-value' })
+
+ console.log("Updated X-New-Header:", pm.request.headers.get("X-New-Header"))
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ updatedRequest: expect.objectContaining({
+ headers: expect.arrayContaining([
+ expect.objectContaining({
+ key: "X-New-Header",
+ value: "new-value",
+ }),
+ ]),
+ }),
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({ args: ["Initial X-New-Header:", null] }),
+ expect.objectContaining({
+ args: ["Updated X-New-Header:", "new-value"],
+ }),
+ ]),
+ })
+ )
+ })
+
+ test("pm.request.headers.remove() removes header", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ console.log("Initial Authorization:", pm.request.headers.get("Authorization"))
+ console.log("Initial has Authorization:", pm.request.headers.has("Authorization"))
+
+ pm.request.headers.remove("Authorization")
+
+ console.log("Updated Authorization:", pm.request.headers.get("Authorization"))
+ console.log("Updated has Authorization:", pm.request.headers.has("Authorization"))
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ updatedRequest: expect.objectContaining({
+ headers: expect.not.arrayContaining([
+ expect.objectContaining({ key: "Authorization" }),
+ ]),
+ }),
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({
+ args: ["Initial Authorization:", "Bearer token123"],
+ }),
+ expect.objectContaining({
+ args: ["Initial has Authorization:", true],
+ }),
+ expect.objectContaining({ args: ["Updated Authorization:", null] }),
+ expect.objectContaining({
+ args: ["Updated has Authorization:", false],
+ }),
+ ]),
+ })
+ )
+ })
+
+ test("multiple header mutations work correctly", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ pm.request.headers.add({ key: 'X-Header-1', value: 'value1' })
+ pm.request.headers.add({ key: 'X-Header-2', value: 'value2' })
+ console.log("After adds - X-Header-1:", pm.request.headers.get("X-Header-1"))
+ console.log("After adds - X-Header-2:", pm.request.headers.get("X-Header-2"))
+
+ pm.request.headers.upsert({ key: 'X-Header-1', value: 'updated-value1' })
+ console.log("After upsert - X-Header-1:", pm.request.headers.get("X-Header-1"))
+
+ pm.request.headers.remove('X-Header-2')
+ console.log("After remove - X-Header-2:", pm.request.headers.get("X-Header-2"))
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ updatedRequest: expect.objectContaining({
+ headers: expect.arrayContaining([
+ expect.objectContaining({
+ key: "X-Header-1",
+ value: "updated-value1",
+ }),
+ ]),
+ }),
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({
+ args: ["After upsert - X-Header-1:", "updated-value1"],
+ }),
+ expect.objectContaining({
+ args: ["After remove - X-Header-2:", null],
+ }),
+ ]),
+ })
+ )
+ })
+})
+
+describe("pm.request.body mutations", () => {
+ test("pm.request.body.update() changes request body with Postman format", () => {
+ const requestWithBody: HoppRESTRequest = {
+ ...baseRequest,
+ body: {
+ contentType: "application/json",
+ body: '{"name": "John"}',
+ },
+ }
+
+ return expect(
+ runPreRequestScript(
+ `
+ console.log("Initial body:", pm.request.body)
+
+ pm.request.body.update({
+ mode: 'raw',
+ raw: '{"name": "Jane", "age": 30}',
+ options: { raw: { language: 'json' } }
+ })
+
+ console.log("Updated body:", pm.request.body)
+ console.log("hopp.request.body:", hopp.request.body)
+ `,
+ { envs, request: requestWithBody }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ updatedRequest: expect.objectContaining({
+ body: {
+ contentType: "application/json",
+ body: '{"name": "Jane", "age": 30}',
+ },
+ }),
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({
+ args: expect.arrayContaining([
+ "Initial body:",
+ expect.objectContaining({ contentType: "application/json" }),
+ ]),
+ }),
+ expect.objectContaining({
+ args: expect.arrayContaining([
+ "Updated body:",
+ expect.objectContaining({
+ contentType: "application/json",
+ body: '{"name": "Jane", "age": 30}',
+ }),
+ ]),
+ }),
+ ]),
+ })
+ )
+ })
+
+ test("pm.request.body.update() with string directly", () => {
+ const requestWithBody: HoppRESTRequest = {
+ ...baseRequest,
+ body: {
+ contentType: "application/json",
+ body: '{"data": "original"}',
+ },
+ }
+
+ return expect(
+ runPreRequestScript(
+ `
+ console.log("Initial body:", pm.request.body)
+
+ pm.request.body.update('plain text body')
+
+ console.log("Updated body:", pm.request.body)
+ console.log("Content-Type:", pm.request.body.contentType)
+ `,
+ { envs, request: requestWithBody }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ updatedRequest: expect.objectContaining({
+ body: {
+ contentType: "text/plain",
+ body: "plain text body",
+ },
+ }),
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({ args: ["Content-Type:", "text/plain"] }),
+ ]),
+ })
+ )
+ })
+})
+
+describe("pm.request.auth mutations", () => {
+ test("pm.request.auth setter changes authentication", () => {
+ const newAuth: HoppRESTAuth = {
+ authType: "bearer",
+ token: "new-bearer-token-xyz",
+ authActive: true,
+ }
+
+ return expect(
+ runPreRequestScript(
+ `
+ console.log("Initial auth type:", pm.request.auth.authType)
+
+ pm.request.auth = ${JSON.stringify(newAuth)}
+
+ console.log("Updated auth type:", pm.request.auth.authType)
+ console.log("Updated auth token:", pm.request.auth.token)
+ console.log("hopp.request.auth:", hopp.request.auth)
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ updatedRequest: expect.objectContaining({
+ auth: newAuth,
+ }),
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({ args: ["Initial auth type:", "none"] }),
+ expect.objectContaining({ args: ["Updated auth type:", "bearer"] }),
+ expect.objectContaining({
+ args: ["Updated auth token:", "new-bearer-token-xyz"],
+ }),
+ ]),
+ })
+ )
+ })
+
+ test("pm.request.auth setter replaces entire auth object", () => {
+ const requestWithAuth: HoppRESTRequest = {
+ ...baseRequest,
+ auth: {
+ authType: "bearer",
+ token: "original-token",
+ authActive: true,
+ },
+ }
+
+ return expect(
+ runPreRequestScript(
+ `
+ console.log("Initial auth:", pm.request.auth)
+
+ pm.request.auth = {
+ authType: 'basic',
+ username: 'user',
+ password: 'pass',
+ authActive: true
+ }
+
+ console.log("Updated auth:", pm.request.auth)
+ console.log("Auth type changed:", pm.request.auth.authType)
+ `,
+ { envs, request: requestWithAuth }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ updatedRequest: expect.objectContaining({
+ auth: expect.objectContaining({
+ authType: "basic",
+ username: "user",
+ password: "pass",
+ }),
+ }),
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({ args: ["Auth type changed:", "basic"] }),
+ ]),
+ })
+ )
+ })
+
+ test("pm.request.auth can be set to null to disable auth", () => {
+ const requestWithAuth: HoppRESTRequest = {
+ ...baseRequest,
+ auth: {
+ authType: "bearer",
+ token: "some-token",
+ authActive: true,
+ },
+ }
+
+ return expect(
+ runPreRequestScript(
+ `
+ console.log("Initial auth active:", pm.request.auth.authActive)
+ console.log("Initial auth type:", pm.request.auth.authType)
+
+ pm.request.auth = null
+
+ console.log("Updated auth active:", pm.request.auth.authActive)
+ console.log("Updated auth type:", pm.request.auth.authType)
+ `,
+ { envs, request: requestWithAuth }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ updatedRequest: expect.objectContaining({
+ auth: {
+ authType: "none",
+ authActive: false,
+ },
+ }),
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({ args: ["Initial auth active:", true] }),
+ expect.objectContaining({ args: ["Updated auth active:", false] }),
+ expect.objectContaining({ args: ["Updated auth type:", "none"] }),
+ ]),
+ })
+ )
+ })
+})
+
+describe("pm.request combined mutations", () => {
+ test("multiple different mutation types work together", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ console.log("=== Initial State ===")
+ console.log("URL:", pm.request.url.toString())
+ console.log("Method:", pm.request.method)
+ console.log("Content-Type:", pm.request.headers.get("Content-Type"))
+
+ // Change URL
+ pm.request.url = "https://api.example.com/v2/data"
+ pm.request.url.protocol = 'http'
+ pm.request.url.query.add({ key: 'page', value: '1' })
+
+ // Change method
+ pm.request.method = 'POST'
+
+ // Change headers
+ pm.request.headers.upsert({ key: 'Content-Type', value: 'application/xml' })
+ pm.request.headers.add({ key: 'X-API-Key', value: 'secret123' })
+
+ console.log("=== Final State ===")
+ console.log("URL:", pm.request.url.toString())
+ console.log("Method:", pm.request.method)
+ console.log("Content-Type:", pm.request.headers.get("Content-Type"))
+ console.log("X-API-Key:", pm.request.headers.get("X-API-Key"))
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ updatedRequest: expect.objectContaining({
+ endpoint: "http://api.example.com/v2/data?page=1",
+ method: "POST",
+ headers: expect.arrayContaining([
+ expect.objectContaining({
+ key: "Content-Type",
+ value: "application/xml",
+ }),
+ expect.objectContaining({ key: "X-API-Key", value: "secret123" }),
+ ]),
+ }),
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({
+ args: ["URL:", "http://api.example.com/v2/data?page=1"],
+ }),
+ expect.objectContaining({ args: ["Method:", "POST"] }),
+ expect.objectContaining({
+ args: ["Content-Type:", "application/xml"],
+ }),
+ expect.objectContaining({ args: ["X-API-Key:", "secret123"] }),
+ ]),
+ })
+ )
+ })
+
+ test("mutations persist across multiple reads", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ pm.request.url.protocol = 'http'
+ const url1 = pm.request.url.toString()
+
+ pm.request.url.path = ['updated', 'path']
+ const url2 = pm.request.url.toString()
+
+ pm.request.url.query.add({ key: 'test', value: 'value' })
+ const url3 = pm.request.url.toString()
+
+ console.log("URL after 1st mutation:", url1)
+ console.log("URL after 2nd mutation:", url2)
+ console.log("URL after 3rd mutation:", url3)
+ console.log("Current URL:", pm.request.url.toString())
+ console.log("hopp.request.url:", hopp.request.url)
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({
+ args: expect.arrayContaining([
+ "URL after 1st mutation:",
+ expect.stringContaining("http://"),
+ ]),
+ }),
+ expect.objectContaining({
+ args: expect.arrayContaining([
+ "URL after 2nd mutation:",
+ expect.stringContaining("/updated/path"),
+ ]),
+ }),
+ expect.objectContaining({
+ args: expect.arrayContaining([
+ "URL after 3rd mutation:",
+ expect.stringContaining("test=value"),
+ ]),
+ }),
+ ]),
+ })
+ )
+ })
+})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/request/query/propertylist.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/request/query/propertylist.spec.ts
new file mode 100644
index 00000000..225e41c5
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/request/query/propertylist.spec.ts
@@ -0,0 +1,1226 @@
+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?filter=active&sort=name&page=1",
+ method: "GET",
+ headers: [],
+ params: [
+ { key: "filter", value: "active", active: true, description: "" },
+ { key: "sort", value: "name", active: true, description: "" },
+ { key: "page", value: "1", 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.query.get()", () => {
+ test("returns value for existing parameter", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ const filterValue = pm.request.url.query.get('filter')
+ const sortValue = pm.request.url.query.get('sort')
+
+ console.log("Filter value:", filterValue)
+ console.log("Sort value:", sortValue)
+ console.log("Filter type:", typeof filterValue)
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({ args: ["Filter value:", "active"] }),
+ expect.objectContaining({ args: ["Sort value:", "name"] }),
+ expect.objectContaining({ args: ["Filter type:", "string"] }),
+ ]),
+ })
+ )
+ })
+
+ test("returns null for non-existent parameter", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ const value = pm.request.url.query.get('nonexistent')
+
+ console.log("Non-existent value:", value)
+ console.log("Is null:", value === null)
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({ args: ["Non-existent value:", null] }),
+ expect.objectContaining({ args: ["Is null:", true] }),
+ ]),
+ })
+ )
+ })
+
+ test("updates after parameter is added", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ console.log("Initial limit:", pm.request.url.query.get('limit'))
+
+ pm.request.url.query.add({ key: 'limit', value: '20' })
+
+ console.log("Updated limit:", pm.request.url.query.get('limit'))
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({ args: ["Initial limit:", null] }),
+ expect.objectContaining({ args: ["Updated limit:", "20"] }),
+ ]),
+ })
+ )
+ })
+})
+
+describe("pm.request.url.query.has()", () => {
+ test("returns true for existing parameters", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ console.log("Has filter:", pm.request.url.query.has('filter'))
+ console.log("Has sort:", pm.request.url.query.has('sort'))
+ console.log("Has page:", pm.request.url.query.has('page'))
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({ args: ["Has filter:", true] }),
+ expect.objectContaining({ args: ["Has sort:", true] }),
+ expect.objectContaining({ args: ["Has page:", true] }),
+ ]),
+ })
+ )
+ })
+
+ test("returns false for non-existent parameters", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ console.log("Has limit:", pm.request.url.query.has('limit'))
+ console.log("Has offset:", pm.request.url.query.has('offset'))
+ console.log("Has nonexistent:", pm.request.url.query.has('nonexistent'))
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({ args: ["Has limit:", false] }),
+ expect.objectContaining({ args: ["Has offset:", false] }),
+ expect.objectContaining({ args: ["Has nonexistent:", false] }),
+ ]),
+ })
+ )
+ })
+
+ test("updates after parameters are modified", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ console.log("Has status (before):", pm.request.url.query.has('status'))
+
+ pm.request.url.query.add({ key: 'status', value: 'published' })
+
+ console.log("Has status (after add):", pm.request.url.query.has('status'))
+
+ pm.request.url.query.remove('status')
+
+ console.log("Has status (after remove):", pm.request.url.query.has('status'))
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({ args: ["Has status (before):", false] }),
+ expect.objectContaining({ args: ["Has status (after add):", true] }),
+ expect.objectContaining({
+ args: ["Has status (after remove):", false],
+ }),
+ ]),
+ })
+ )
+ })
+})
+
+describe("pm.request.url.query.upsert()", () => {
+ test("adds new parameter when it doesn't exist", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ console.log("Has limit (before):", pm.request.url.query.has('limit'))
+
+ pm.request.url.query.upsert({ key: 'limit', value: '50' })
+
+ console.log("Has limit (after):", pm.request.url.query.has('limit'))
+ console.log("Limit value:", pm.request.url.query.get('limit'))
+ console.log("URL:", pm.request.url.toString())
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ updatedRequest: expect.objectContaining({
+ endpoint: expect.stringContaining("limit=50"),
+ }),
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({ args: ["Has limit (before):", false] }),
+ expect.objectContaining({ args: ["Has limit (after):", true] }),
+ expect.objectContaining({ args: ["Limit value:", "50"] }),
+ ]),
+ })
+ )
+ })
+
+ test("updates existing parameter", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ console.log("Initial filter:", pm.request.url.query.get('filter'))
+
+ pm.request.url.query.upsert({ key: 'filter', value: 'inactive' })
+
+ console.log("Updated filter:", pm.request.url.query.get('filter'))
+ console.log("URL:", pm.request.url.toString())
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ updatedRequest: expect.objectContaining({
+ endpoint: expect.stringContaining("filter=inactive"),
+ }),
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({ args: ["Initial filter:", "active"] }),
+ expect.objectContaining({ args: ["Updated filter:", "inactive"] }),
+ ]),
+ })
+ )
+ })
+
+ test("handles empty value", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ pm.request.url.query.upsert({ key: 'flag' })
+
+ console.log("Flag value:", pm.request.url.query.get('flag'))
+ console.log("Flag exists:", pm.request.url.query.has('flag'))
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({ args: ["Flag value:", ""] }),
+ expect.objectContaining({ args: ["Flag exists:", true] }),
+ ]),
+ })
+ )
+ })
+
+ test("throws error for missing key", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ try {
+ pm.request.url.query.upsert({ 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: expect.arrayContaining([
+ "Error caught:",
+ expect.stringContaining("must have a 'key' property"),
+ ]),
+ }),
+ ]),
+ })
+ )
+ })
+})
+
+describe("pm.request.url.query.clear()", () => {
+ test("removes all query parameters", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ console.log("Params before:", pm.request.url.query.all())
+ console.log("Count before:", pm.request.url.query.count())
+
+ pm.request.url.query.clear()
+
+ console.log("Params after:", pm.request.url.query.all())
+ console.log("Count after:", pm.request.url.query.count())
+ console.log("URL:", pm.request.url.toString())
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ updatedRequest: expect.objectContaining({
+ endpoint: "https://api.example.com/users",
+ }),
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({
+ args: [
+ "Params before:",
+ { filter: "active", sort: "name", page: "1" },
+ ],
+ }),
+ expect.objectContaining({ args: ["Count before:", 3] }),
+ expect.objectContaining({ args: ["Params after:", {}] }),
+ expect.objectContaining({ args: ["Count after:", 0] }),
+ ]),
+ })
+ )
+ })
+
+ test("allows adding parameters after clearing", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ pm.request.url.query.clear()
+ console.log("After clear:", pm.request.url.query.all())
+
+ pm.request.url.query.add({ key: 'new', value: 'param' })
+ console.log("After add:", pm.request.url.query.all())
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({ args: ["After clear:", {}] }),
+ expect.objectContaining({ args: ["After add:", { new: "param" }] }),
+ ]),
+ })
+ )
+ })
+})
+
+describe("pm.request.url.query.each()", () => {
+ test("iterates over all parameters", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ const keys = []
+ const values = []
+
+ pm.request.url.query.each((param) => {
+ keys.push(param.key)
+ values.push(param.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:", ["filter", "sort", "page"]],
+ }),
+ expect.objectContaining({
+ args: ["Values:", ["active", "name", "1"]],
+ }),
+ expect.objectContaining({ args: ["Count:", 3] }),
+ ]),
+ })
+ )
+ })
+
+ test("callback receives correct parameter object", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ pm.request.url.query.each((param) => {
+ console.log("Key:", param.key)
+ console.log("Value:", param.value)
+ return // Check only first param
+ })
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({ args: ["Key:", "filter"] }),
+ expect.objectContaining({ args: ["Value:", "active"] }),
+ ]),
+ })
+ )
+ })
+})
+
+describe("pm.request.url.query.map()", () => {
+ test("transforms parameters and returns new array", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ const mapped = pm.request.url.query.map((param) => {
+ return param.key.toUpperCase() + '=' + param.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:", ["FILTER=active", "SORT=name", "PAGE=1"]],
+ }),
+ expect.objectContaining({ args: ["Array length:", 3] }),
+ expect.objectContaining({ args: ["First item:", "FILTER=active"] }),
+ ]),
+ })
+ )
+ })
+
+ test("does not modify original parameters", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ const originalParams = pm.request.url.query.all()
+
+ const mapped = pm.request.url.query.map((param) => {
+ return { modified: param.key }
+ })
+
+ const afterParams = pm.request.url.query.all()
+
+ console.log("Original:", originalParams)
+ console.log("Mapped:", mapped)
+ console.log("After:", afterParams)
+ console.log("Params unchanged:", JSON.stringify(originalParams) === JSON.stringify(afterParams))
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({
+ args: ["Original:", { filter: "active", sort: "name", page: "1" }],
+ }),
+ expect.objectContaining({
+ args: ["After:", { filter: "active", sort: "name", page: "1" }],
+ }),
+ expect.objectContaining({ args: ["Params unchanged:", true] }),
+ ]),
+ })
+ )
+ })
+})
+
+describe("pm.request.url.query.filter()", () => {
+ test("filters parameters based on condition", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ const filtered = pm.request.url.query.filter((param) => {
+ return param.key.includes('sort') || param.key.includes('page')
+ })
+
+ console.log("Filtered:", filtered)
+ console.log("Count:", filtered.length)
+ console.log("Keys:", filtered.map(p => p.key))
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({ args: ["Count:", 2] }),
+ expect.objectContaining({ args: ["Keys:", ["sort", "page"]] }),
+ ]),
+ })
+ )
+ })
+
+ test("returns empty array when no matches", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ const filtered = pm.request.url.query.filter((param) => {
+ return param.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] }),
+ ]),
+ })
+ )
+ })
+})
+
+describe("pm.request.url.query.count()", () => {
+ test("returns correct count of parameters", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ const count = pm.request.url.query.count()
+
+ console.log("Count:", count)
+ console.log("Type:", typeof count)
+ console.log("Matches all():", count === Object.keys(pm.request.url.query.all()).length)
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({ args: ["Count:", 3] }),
+ expect.objectContaining({ args: ["Type:", "number"] }),
+ expect.objectContaining({ args: ["Matches all():", true] }),
+ ]),
+ })
+ )
+ })
+
+ test("updates after parameters are added or removed", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ console.log("Initial count:", pm.request.url.query.count())
+
+ pm.request.url.query.add({ key: 'limit', value: '20' })
+ console.log("After add:", pm.request.url.query.count())
+
+ pm.request.url.query.remove('filter')
+ console.log("After remove:", pm.request.url.query.count())
+
+ pm.request.url.query.clear()
+ console.log("After clear:", pm.request.url.query.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] }),
+ ]),
+ })
+ )
+ })
+})
+
+describe("pm.request.url.query.idx()", () => {
+ test("returns parameter at specific index", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ const first = pm.request.url.query.idx(0)
+ const second = pm.request.url.query.idx(1)
+ const third = pm.request.url.query.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: "filter", value: "active" }],
+ }),
+ expect.objectContaining({
+ args: ["Second:", { key: "sort", value: "name" }],
+ }),
+ expect.objectContaining({
+ args: ["Third:", { key: "page", value: "1" }],
+ }),
+ ]),
+ })
+ )
+ })
+
+ test("returns null for out-of-bounds index", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ const param = pm.request.url.query.idx(999)
+
+ console.log("Out of bounds:", param)
+ console.log("Is null:", param === 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 param = pm.request.url.query.idx(-1)
+
+ console.log("Negative index:", param)
+ console.log("Is null:", param === null)
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({ args: ["Negative index:", null] }),
+ expect.objectContaining({ args: ["Is null:", true] }),
+ ]),
+ })
+ )
+ })
+})
+
+describe("pm.request.url.query.toObject()", () => {
+ test("returns parameters as object (alias for all())", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ const obj = pm.request.url.query.toObject()
+ const all = pm.request.url.query.all()
+
+ console.log("toObject:", obj)
+ console.log("all:", all)
+ console.log("Are equal:", JSON.stringify(obj) === JSON.stringify(all))
+ console.log("Type:", typeof obj)
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({
+ args: ["toObject:", { filter: "active", sort: "name", page: "1" }],
+ }),
+ expect.objectContaining({
+ args: ["all:", { filter: "active", sort: "name", page: "1" }],
+ }),
+ expect.objectContaining({ args: ["Are equal:", true] }),
+ expect.objectContaining({ args: ["Type:", "object"] }),
+ ]),
+ })
+ )
+ })
+})
+
+describe("duplicate query parameter handling", () => {
+ test("handles duplicate parameter keys by converting to array", () => {
+ const requestWithDuplicates: HoppRESTRequest = {
+ ...baseRequest,
+ endpoint: "https://api.example.com/users?tag=js&tag=ts&tag=go",
+ params: [
+ { key: "tag", value: "js", active: true, description: "" },
+ { key: "tag", value: "ts", active: true, description: "" },
+ { key: "tag", value: "go", active: true, description: "" },
+ ],
+ }
+
+ return expect(
+ runPreRequestScript(
+ `
+ const params = pm.request.url.query.all()
+
+ console.log("Params:", params)
+ console.log("Tag value:", params.tag)
+ console.log("Is array:", Array.isArray(params.tag))
+ console.log("Array length:", params.tag.length)
+ console.log("All values:", params.tag)
+ `,
+ { envs, request: requestWithDuplicates }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({ args: ["Tag value:", ["js", "ts", "go"]] }),
+ expect.objectContaining({ args: ["Is array:", true] }),
+ expect.objectContaining({ args: ["Array length:", 3] }),
+ expect.objectContaining({
+ args: ["All values:", ["js", "ts", "go"]],
+ }),
+ ]),
+ })
+ )
+ })
+
+ test("handles mixed duplicate and unique parameters", () => {
+ const requestWithMixed: HoppRESTRequest = {
+ ...baseRequest,
+ endpoint:
+ "https://api.example.com/users?filter=active&tag=js&tag=ts&sort=name",
+ params: [
+ { key: "filter", value: "active", active: true, description: "" },
+ { key: "tag", value: "js", active: true, description: "" },
+ { key: "tag", value: "ts", active: true, description: "" },
+ { key: "sort", value: "name", active: true, description: "" },
+ ],
+ }
+
+ return expect(
+ runPreRequestScript(
+ `
+ const params = pm.request.url.query.all()
+
+ console.log("All params:", params)
+ console.log("filter is string:", typeof params.filter === 'string')
+ console.log("tag is array:", Array.isArray(params.tag))
+ console.log("sort is string:", typeof params.sort === 'string')
+ `,
+ { envs, request: requestWithMixed }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({
+ args: [
+ "All params:",
+ { filter: "active", tag: ["js", "ts"], sort: "name" },
+ ],
+ }),
+ expect.objectContaining({ args: ["filter is string:", true] }),
+ expect.objectContaining({ args: ["tag is array:", true] }),
+ expect.objectContaining({ args: ["sort is string:", true] }),
+ ]),
+ })
+ )
+ })
+
+ test("get() returns first value for duplicate keys", () => {
+ const requestWithDuplicates: HoppRESTRequest = {
+ ...baseRequest,
+ endpoint: "https://api.example.com/users?tag=first&tag=second",
+ params: [
+ { key: "tag", value: "first", active: true, description: "" },
+ { key: "tag", value: "second", active: true, description: "" },
+ ],
+ }
+
+ return expect(
+ runPreRequestScript(
+ `
+ const value = pm.request.url.query.get('tag')
+
+ console.log("get() value:", value)
+ console.log("Is first value:", value === 'first')
+ console.log("all() value:", pm.request.url.query.all().tag)
+ `,
+ { envs, request: requestWithDuplicates }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({ args: ["get() value:", "first"] }),
+ expect.objectContaining({ args: ["Is first value:", true] }),
+ expect.objectContaining({
+ args: ["all() value:", ["first", "second"]],
+ }),
+ ]),
+ })
+ )
+ })
+})
+
+describe("pm.request.url.query.find()", () => {
+ test("finds parameter by predicate function", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ const result = pm.request.url.query.find((param) => param.key === 'sort')
+
+ console.log("Found param:", result)
+ console.log("Param key:", result.key)
+ console.log("Param value:", result.value)
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({
+ args: [
+ "Found param:",
+ expect.objectContaining({ key: "sort", value: "name" }),
+ ],
+ }),
+ expect.objectContaining({ args: ["Param key:", "sort"] }),
+ expect.objectContaining({ args: ["Param value:", "name"] }),
+ ]),
+ })
+ )
+ })
+
+ test("finds parameter by key string", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ const result = pm.request.url.query.find('filter')
+
+ 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: "filter", value: "active" }),
+ ],
+ }),
+ expect.objectContaining({ args: ["Value:", "active"] }),
+ ]),
+ })
+ )
+ })
+
+ test("returns null when parameter not found", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ const result = pm.request.url.query.find('nonexistent')
+
+ 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.url.query.indexOf()", () => {
+ test("returns index of parameter by key", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ const idx1 = pm.request.url.query.indexOf('filter')
+ const idx2 = pm.request.url.query.indexOf('sort')
+ const idx3 = pm.request.url.query.indexOf('page')
+
+ console.log("Index of filter:", idx1)
+ console.log("Index of sort:", idx2)
+ console.log("Index of page:", idx3)
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({ args: ["Index of filter:", 0] }),
+ expect.objectContaining({ args: ["Index of sort:", 1] }),
+ expect.objectContaining({ args: ["Index of page:", 2] }),
+ ]),
+ })
+ )
+ })
+
+ test("returns index of parameter by object", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ const idx = pm.request.url.query.indexOf({ key: 'sort' })
+
+ console.log("Index:", idx)
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({ args: ["Index:", 1] }),
+ ]),
+ })
+ )
+ })
+
+ test("returns -1 when parameter not found", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ const idx = pm.request.url.query.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.url.query.insert()", () => {
+ test("inserts parameter before specified key", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ pm.request.url.query.insert({ key: 'limit', value: '10' }, 'page')
+
+ const allParams = pm.request.url.query.map((p) => p.key)
+ console.log("All params:", allParams)
+ console.log("URL:", pm.request.url.toString())
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({
+ args: ["All params:", ["filter", "sort", "limit", "page"]],
+ }),
+ expect.objectContaining({
+ args: [
+ "URL:",
+ "https://api.example.com/users?filter=active&sort=name&limit=10&page=1",
+ ],
+ }),
+ ]),
+ })
+ )
+ })
+
+ test("appends parameter if 'before' key not found", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ pm.request.url.query.insert({ key: 'limit', value: '10' }, 'nonexistent')
+
+ const allParams = pm.request.url.query.map((p) => p.key)
+ console.log("All params:", allParams)
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({
+ args: ["All params:", ["filter", "sort", "page", "limit"]],
+ }),
+ ]),
+ })
+ )
+ })
+
+ test("appends parameter when no 'before' specified", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ pm.request.url.query.insert({ key: 'limit', value: '10' })
+
+ const allParams = pm.request.url.query.map((p) => p.key)
+ console.log("All params:", allParams)
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({
+ args: ["All params:", ["filter", "sort", "page", "limit"]],
+ }),
+ ]),
+ })
+ )
+ })
+
+ test("throws error when item has no key", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ try {
+ pm.request.url.query.insert({ value: '10' })
+ 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:", "Query param must have a 'key' property"],
+ }),
+ ]),
+ })
+ )
+ })
+})
+
+describe("pm.request.url.query.append()", () => {
+ test("moves existing parameter to end", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ pm.request.url.query.append({ key: 'filter', value: 'updated' })
+
+ const allParams = pm.request.url.query.map((p) => p.key)
+ const filterValue = pm.request.url.query.get('filter')
+
+ console.log("All params:", allParams)
+ console.log("Filter value:", filterValue)
+ console.log("URL:", pm.request.url.toString())
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({
+ args: ["All params:", ["sort", "page", "filter"]],
+ }),
+ expect.objectContaining({ args: ["Filter value:", "updated"] }),
+ expect.objectContaining({
+ args: [
+ "URL:",
+ "https://api.example.com/users?sort=name&page=1&filter=updated",
+ ],
+ }),
+ ]),
+ })
+ )
+ })
+
+ test("adds new parameter at end", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ pm.request.url.query.append({ key: 'limit', value: '10' })
+
+ const allParams = pm.request.url.query.map((p) => p.key)
+ console.log("All params:", allParams)
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({
+ args: ["All params:", ["filter", "sort", "page", "limit"]],
+ }),
+ ]),
+ })
+ )
+ })
+
+ test("throws error when item has no key", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ try {
+ pm.request.url.query.append({ value: '10' })
+ 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:", "Query param must have a 'key' property"],
+ }),
+ ]),
+ })
+ )
+ })
+})
+
+describe("pm.request.url.query.assimilate()", () => {
+ test("updates existing parameters and adds new ones from array", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ pm.request.url.query.assimilate([
+ { key: 'filter', value: 'updated' },
+ { key: 'limit', value: '20' }
+ ])
+
+ const allParams = pm.request.url.query.all()
+ console.log("Filter:", allParams.filter)
+ console.log("Sort:", allParams.sort)
+ console.log("Limit:", allParams.limit)
+ console.log("Count:", pm.request.url.query.count())
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({ args: ["Filter:", "updated"] }),
+ expect.objectContaining({ args: ["Sort:", "name"] }),
+ expect.objectContaining({ args: ["Limit:", "20"] }),
+ expect.objectContaining({ args: ["Count:", 4] }),
+ ]),
+ })
+ )
+ })
+
+ test("updates existing parameters and adds new ones from object", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ pm.request.url.query.assimilate({
+ filter: 'inactive',
+ limit: '50'
+ })
+
+ const allParams = pm.request.url.query.all()
+ console.log("All params:", allParams)
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({
+ args: [
+ "All params:",
+ expect.objectContaining({
+ filter: "inactive",
+ sort: "name",
+ page: "1",
+ limit: "50",
+ }),
+ ],
+ }),
+ ]),
+ })
+ )
+ })
+
+ test("prunes parameters not in source when prune=true", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ pm.request.url.query.assimilate(
+ { filter: 'active', limit: '10' },
+ true
+ )
+
+ const allParams = pm.request.url.query.all()
+ const count = pm.request.url.query.count()
+
+ console.log("All params:", allParams)
+ console.log("Count:", count)
+ console.log("Has sort:", pm.request.url.query.has('sort'))
+ console.log("Has page:", pm.request.url.query.has('page'))
+ `,
+ { envs, request: baseRequest }
+ )
+ ).resolves.toEqualRight(
+ expect.objectContaining({
+ consoleEntries: expect.arrayContaining([
+ expect.objectContaining({
+ args: [
+ "All params:",
+ expect.objectContaining({ filter: "active", limit: "10" }),
+ ],
+ }),
+ expect.objectContaining({ args: ["Count:", 2] }),
+ expect.objectContaining({ args: ["Has sort:", false] }),
+ expect.objectContaining({ args: ["Has page:", false] }),
+ ]),
+ })
+ )
+ })
+
+ test("throws error for invalid source", () => {
+ return expect(
+ runPreRequestScript(
+ `
+ try {
+ pm.request.url.query.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"],
+ }),
+ ]),
+ })
+ )
+ })
+})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/request/url/helper-methods.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/request/url/helper-methods.spec.ts
new file mode 100644
index 00000000..f01646af
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/request/url/helper-methods.spec.ts
@@ -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" }],
+ }),
+ ]),
+ })
+ )
+ })
+})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/request/url/properties.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/request/url/properties.spec.ts
new file mode 100644
index 00000000..3cde7217
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/request/url/properties.spec.ts
@@ -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"] }),
+ ]),
+ })
+ )
+ })
+})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/response.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/response.spec.ts
deleted file mode 100644
index bcd8f6d1..00000000
--- a/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/response.spec.ts
+++ /dev/null
@@ -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!'",
- },
- ],
- }),
- ])
- })
-})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/response/basic.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/response/basic.spec.ts
new file mode 100644
index 00000000..4750f4c4
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/response/basic.spec.ts
@@ -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'",
+ },
+ ],
+ }),
+ ])
+ })
+})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/response/bdd-assertions.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/response/bdd-assertions.spec.ts
new file mode 100644
index 00000000..fd22d48d
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/response/bdd-assertions.spec.ts
@@ -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: "",
+ 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: "", ...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) },
+ ]),
+ }),
+ ],
+ }),
+ ])
+ })
+})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/response/cookies.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/response/cookies.spec.ts
new file mode 100644
index 00000000..b06de863
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/response/cookies.spec.ts
@@ -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'"
+ ),
+ },
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/response/datauri-comprehensive.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/response/datauri-comprehensive.spec.ts
new file mode 100644
index 00000000..758b30c2
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/response/datauri-comprehensive.spec.ts
@@ -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: "Hello",
+ 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: 'test',
+ 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" }),
+ ],
+ }),
+ ],
+ }),
+ ])
+ })
+})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/response/method-existence.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/response/method-existence.spec.ts
new file mode 100644
index 00000000..4a8ae098
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/response/method-existence.spec.ts
@@ -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" })],
+ }),
+ ],
+ }),
+ ],
+ })
+ )
+ })
+})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/serialization-edge-cases.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/serialization-edge-cases.spec.ts
new file mode 100644
index 00000000..973773e5
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/serialization-edge-cases.spec.ts
@@ -0,0 +1,1224 @@
+// QuickJS sandbox boundary serialization: compute values BEFORE crossing to preserve type info
+
+import { describe, expect, test } from "vitest"
+import {
+ runTestWithResponse,
+ fakeResponse,
+ runTest,
+} from "~/utils/test-helpers"
+
+describe("Serialization Edge Cases - Object Reference Equality", () => {
+ describe("`pm.expect.equal()` - Reference Equality", () => {
+ test("should pass for same object reference", async () => {
+ const testScript = `
+ pm.test("same reference equality", () => {
+ const obj = { a: 1 };
+ pm.expect(obj).to.equal(obj);
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "same reference equality",
+ expectResults: [expect.objectContaining({ status: "pass" })],
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+
+ test("should fail for different object references with same content", async () => {
+ const testScript = `
+ pm.test("different references", () => {
+ pm.expect({ a: 1 }).to.not.equal({ a: 1 });
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "different references",
+ expectResults: [expect.objectContaining({ status: "pass" })],
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+
+ test("should work with arrays", async () => {
+ const testScript = `
+ pm.test("array reference equality", () => {
+ const arr = [1, 2, 3];
+ pm.expect(arr).to.equal(arr);
+ pm.expect([1, 2, 3]).to.not.equal([1, 2, 3]);
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "array reference equality",
+ expectResults: [
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ ],
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+
+ test("should work with primitives (always equal by value)", async () => {
+ const testScript = `
+ pm.test("primitive equality", () => {
+ pm.expect(5).to.equal(5);
+ pm.expect('hello').to.equal('hello');
+ pm.expect(true).to.equal(true);
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "primitive equality",
+ expectResults: expect.arrayContaining([
+ expect.objectContaining({ status: "pass" }),
+ ]),
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+ })
+})
+
+describe("Serialization Edge Cases - Prototype Chain Preservation", () => {
+ describe("`pm.expect.own.property()` - Own vs Inherited Properties", () => {
+ test("should pass when object HAS own property", async () => {
+ const testScript = `
+ pm.test("own property - positive case", () => {
+ const obj = Object.create({ inherited: true });
+ obj.own = true;
+ pm.expect(obj).to.have.own.property('own');
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "own property - positive case",
+ expectResults: [expect.objectContaining({ status: "pass" })],
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+
+ test("should distinguish own properties from inherited (negation)", async () => {
+ const testScript = `
+ pm.test("own property vs inherited", () => {
+ 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 runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "own property vs inherited",
+ expectResults: [
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ ],
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+
+ test("should work with plain objects", async () => {
+ const testScript = `
+ pm.test("plain object own properties", () => {
+ const obj = { a: 1, b: 2 };
+ pm.expect(obj).to.have.own.property('a');
+ pm.expect(obj).to.have.own.property('b');
+ pm.expect(obj).to.not.have.own.property('toString');
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "plain object own properties",
+ expectResults: [
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ ],
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+
+ test("should fail when assertion is incorrect", async () => {
+ const testScript = `
+ pm.test("incorrect assertion", () => {
+ const obj = { a: 1 };
+ pm.expect(obj).to.not.have.own.property('a');
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "incorrect assertion",
+ expectResults: [expect.objectContaining({ status: "fail" })],
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+
+ test("should work with arrays (inherited from Array.prototype)", async () => {
+ const testScript = `
+ pm.test("array own vs inherited", () => {
+ const arr = [1, 2, 3];
+ pm.expect(arr).to.have.own.property('0');
+ pm.expect(arr).to.have.own.property('length');
+ pm.expect(arr).to.not.have.own.property('push');
+ pm.expect(arr).to.have.property('push');
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "array own vs inherited",
+ expectResults: expect.arrayContaining([
+ expect.objectContaining({ status: "pass" }),
+ ]),
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+ })
+
+ describe("`pm.expect.itself.respondTo()` - Static vs Instance Methods", () => {
+ test("should pass when class HAS static method", async () => {
+ const testScript = `
+ pm.test("static method - positive case", () => {
+ class MyClass {
+ static staticMethod() {}
+ instanceMethod() {}
+ }
+ pm.expect(MyClass).itself.to.respondTo('staticMethod');
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "static method - positive case",
+ expectResults: [expect.objectContaining({ status: "pass" })],
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+
+ test("should distinguish static methods from instance methods", async () => {
+ const testScript = `
+ pm.test("static vs instance methods", () => {
+ class MyClass {
+ static staticMethod() {}
+ instanceMethod() {}
+ }
+ pm.expect(MyClass).itself.to.respondTo('staticMethod');
+ pm.expect(MyClass).to.not.itself.respondTo('instanceMethod');
+ pm.expect(MyClass).to.respondTo('instanceMethod');
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "static vs instance methods",
+ expectResults: [
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ ],
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+
+ test("should work with built-in constructors", async () => {
+ const testScript = `
+ pm.test("built-in static methods", () => {
+ pm.expect(Array).itself.to.respondTo('isArray');
+ pm.expect(Array).itself.to.respondTo('from');
+ pm.expect(Object).itself.to.respondTo('keys');
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "built-in static methods",
+ expectResults: [
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ ],
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+
+ test("should fail when static method doesn't exist", async () => {
+ const testScript = `
+ pm.test("non-existent static method", () => {
+ class MyClass {
+ instanceMethod() {}
+ }
+ pm.expect(MyClass).itself.to.respondTo('nonExistent');
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "non-existent static method",
+ expectResults: [expect.objectContaining({ status: "fail" })],
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+
+ test("should work with negation", async () => {
+ const testScript = `
+ pm.test("static method negation", () => {
+ class MyClass {
+ static staticMethod() {}
+ }
+ pm.expect(MyClass).itself.to.respondTo('staticMethod');
+ pm.expect(MyClass).itself.to.not.respondTo('nonExistent');
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "static method negation",
+ expectResults: [
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ ],
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+
+ test("should work with multiple static methods", async () => {
+ const testScript = `
+ pm.test("multiple static methods", () => {
+ class MyClass {
+ static method1() {}
+ static method2() {}
+ static method3() {}
+ }
+ pm.expect(MyClass).itself.to.respondTo('method1');
+ pm.expect(MyClass).itself.to.respondTo('method2');
+ pm.expect(MyClass).itself.to.respondTo('method3');
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "multiple static methods",
+ expectResults: expect.arrayContaining([
+ expect.objectContaining({ status: "pass" }),
+ ]),
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+ })
+})
+
+describe("Serialization Edge Cases - Special Object Types", () => {
+ describe("`pm.expect.eql()` - RegExp Comparison", () => {
+ test("should pass for identical RegExp patterns", async () => {
+ const testScript = `
+ pm.test("identical regex", () => {
+ pm.expect(/test/i).to.eql(/test/i);
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "identical regex",
+ expectResults: [expect.objectContaining({ status: "pass" })],
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+
+ test("should fail for different RegExp flags", async () => {
+ const testScript = `
+ pm.test("different flags", () => {
+ pm.expect(/test/i).to.not.eql(/test/g);
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "different flags",
+ expectResults: [expect.objectContaining({ status: "pass" })],
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+
+ test("should fail for different RegExp patterns", async () => {
+ const testScript = `
+ pm.test("different patterns", () => {
+ pm.expect(/test/).to.not.eql(/demo/);
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "different patterns",
+ expectResults: [expect.objectContaining({ status: "pass" })],
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+ })
+
+ describe("`pm.expect.equal()` - Date Comparison", () => {
+ test("should pass for same Date reference", async () => {
+ const testScript = `
+ pm.test("same date reference", () => {
+ const date = new Date('2024-01-01');
+ pm.expect(date).to.equal(date);
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "same date reference",
+ expectResults: [expect.objectContaining({ status: "pass" })],
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+
+ test("should pass when different Date references are not equal (reference inequality)", async () => {
+ const testScript = `
+ pm.test("different date references", () => {
+ pm.expect(new Date('2024-01-01')).to.not.equal(new Date('2024-01-01'));
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "different date references",
+ expectResults: [expect.objectContaining({ status: "pass" })],
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+
+ test("should use eql() for value equality on dates", async () => {
+ const testScript = `
+ pm.test("date value equality", () => {
+ pm.expect(new Date('2024-01-01')).to.eql(new Date('2024-01-01'));
+ pm.expect(new Date('2024-01-01')).to.not.eql(new Date('2024-01-02'));
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "date value equality",
+ expectResults: [
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ ],
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+ })
+
+ describe("`pm.expect.a()` - Array Type Checking", () => {
+ test("should pass for 'object' type check", async () => {
+ const testScript = `
+ pm.test("array is object", () => {
+ pm.expect([]).to.be.a('object');
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "array is object",
+ expectResults: [expect.objectContaining({ status: "pass" })],
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+
+ test("should pass for 'array' type check", async () => {
+ const testScript = `
+ pm.test("array is array", () => {
+ pm.expect([]).to.be.an('array');
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "array is array",
+ expectResults: [expect.objectContaining({ status: "pass" })],
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+
+ test("should pass all typeof vs instanceof tests", async () => {
+ const testScript = `
+ pm.test("typeof vs instanceof", () => {
+ pm.expect([]).to.be.a('object');
+ pm.expect([]).to.be.an('array');
+ pm.expect([]).to.be.instanceOf(Array);
+ pm.expect([]).to.be.instanceOf(Object);
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "typeof vs instanceof",
+ expectResults: expect.arrayContaining([
+ expect.objectContaining({ status: "pass" }),
+ ]),
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+ })
+})
+
+describe("Serialization Edge Cases - Assertion Chaining", () => {
+ describe("`.length().which` - Value Proxy Chaining", () => {
+ test("should allow chaining after length check", async () => {
+ const testScript = `
+ pm.test("length chaining", () => {
+ pm.expect([1, 2, 3]).to.have.length(3).which.is.above(2);
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "length chaining",
+ expectResults: [
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ ],
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+
+ test("should chain with .that instead of .which", async () => {
+ const testScript = `
+ pm.test("length with that", () => {
+ pm.expect('test').to.have.length(4).that.is.above(3);
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "length with that",
+ expectResults: [
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ ],
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+
+ test("should support complex chaining", async () => {
+ const testScript = `
+ pm.test("complex chaining", () => {
+ pm.expect([1, 2, 3, 4, 5]).to.have.length(5).which.is.above(4).and.below(6);
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "complex chaining",
+ expectResults: expect.arrayContaining([
+ expect.objectContaining({ status: "pass" }),
+ ]),
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+ })
+
+ describe("`.not.have.lengthOf()` - Negation Support", () => {
+ test("should pass when string does NOT have specified length", async () => {
+ const testScript = `
+ pm.test("string lengthOf negation", () => {
+ pm.expect('test').to.have.lengthOf(4);
+ pm.expect('test').to.not.have.lengthOf(10);
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "string lengthOf negation",
+ expectResults: [
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ ],
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+
+ test("should pass when array does NOT have specified length", async () => {
+ const testScript = `
+ pm.test("array lengthOf negation", () => {
+ pm.expect([1, 2]).to.have.lengthOf(2);
+ pm.expect([1, 2]).to.not.have.lengthOf(3);
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "array lengthOf negation",
+ expectResults: [
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ ],
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+
+ test("should fail when negation is incorrect", async () => {
+ const testScript = `
+ pm.test("incorrect negation", () => {
+ pm.expect('test').to.not.have.lengthOf(4);
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "incorrect negation",
+ expectResults: [expect.objectContaining({ status: "fail" })],
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+
+ test("should support empty string/array", async () => {
+ const testScript = `
+ pm.test("empty values", () => {
+ pm.expect('').to.have.lengthOf(0);
+ pm.expect('').to.not.have.lengthOf(1);
+ pm.expect([]).to.have.lengthOf(0);
+ pm.expect([]).to.not.have.lengthOf(5);
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "empty values",
+ expectResults: expect.arrayContaining([
+ expect.objectContaining({ status: "pass" }),
+ ]),
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+ })
+
+ describe("Circular Reference Handling", () => {
+ test("should handle circular object references in property assertions", () => {
+ return expect(
+ runTest(`
+ pm.test("Circular reference property", function() {
+ const obj = { a: 1, b: 2 }
+ obj.self = obj // Circular reference
+
+ pm.expect(obj).to.have.property('self')
+ pm.expect(obj.self).to.equal(obj)
+ pm.expect(obj).to.have.property('a', 1)
+ })
+ `)()
+ ).resolves.toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "Circular reference property",
+ expectResults: expect.arrayContaining([
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ ]),
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+
+ test("should handle nested circular references", () => {
+ return expect(
+ runTest(`
+ pm.test("Nested circular references", function() {
+ const parent = { name: "parent", child: null }
+ const child = { name: "child", parent: parent }
+ parent.child = child // Creates circular reference
+
+ pm.expect(parent.child.parent).to.equal(parent)
+ pm.expect(parent).to.have.property('name', 'parent')
+ pm.expect(child).to.have.property('name', 'child')
+ })
+ `)()
+ ).resolves.toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "Nested circular references",
+ expectResults: expect.arrayContaining([
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ ]),
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+
+ test("should not throw 'Converting circular structure to JSON' error", () => {
+ return expect(
+ runTest(`
+ pm.test("No JSON.stringify error", function() {
+ const obj = { data: 'test' }
+ obj.circular = obj
+
+ // This should not throw even though obj has circular reference
+ pm.expect(obj).to.have.property('data')
+ pm.expect(obj.circular).to.equal(obj)
+ })
+ `)()
+ ).resolves.toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "No JSON.stringify error",
+ expectResults: expect.arrayContaining([
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ ]),
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+
+ test("should fail with stack overflow for circular array references (known limitation)", () => {
+ return expect(
+ runTest(`
+ pm.test("Circular array limitation", function() {
+ const arr = [1, 2, 3]
+ arr.push(arr) // Creates circular reference
+
+ pm.expect(arr).to.have.lengthOf(4)
+ })
+ `)()
+ ).resolves.toEqualLeft(
+ expect.stringContaining("Maximum call stack size exceeded")
+ )
+ })
+ })
+})
+
+describe("Serialization Edge Cases - Symbol Properties", () => {
+ test("should not enumerate Symbol properties in assertions", async () => {
+ const testScript = `
+ pm.test("Symbol properties are not enumerable", () => {
+ const sym = Symbol('test');
+ const obj = { regular: 'value' };
+ obj[sym] = 'symbol value';
+
+ // Should only see regular properties
+ pm.expect(obj).to.have.property('regular');
+ pm.expect(Object.keys(obj)).to.have.lengthOf(1);
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "Symbol properties are not enumerable",
+ expectResults: expect.arrayContaining([
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ ]),
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+
+ test("should handle objects with Symbol.iterator", async () => {
+ const testScript = `
+ pm.test("Symbol.iterator support", () => {
+ const customIterable = {
+ data: [1, 2, 3],
+ [Symbol.iterator]: function*() {
+ yield* this.data;
+ }
+ };
+
+ // Should be able to assert on regular properties
+ pm.expect(customIterable).to.have.property('data');
+ pm.expect(customIterable.data).to.have.lengthOf(3);
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "Symbol.iterator support",
+ expectResults: expect.arrayContaining([
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ ]),
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+
+ test("should serialize objects with Symbol properties correctly", async () => {
+ const testScript = `
+ pm.test("Symbol property serialization", () => {
+ const sym = Symbol('hidden');
+ const obj = {
+ visible: 'data',
+ nested: { value: 42 }
+ };
+ obj[sym] = 'should not appear in JSON';
+
+ const jsonStr = JSON.stringify(obj);
+ const parsed = JSON.parse(jsonStr);
+
+ // After serialization, Symbol properties are lost
+ pm.expect(parsed).to.have.property('visible', 'data');
+ pm.expect(parsed).to.have.property('nested');
+ pm.expect(parsed.nested).to.have.property('value', 42);
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "Symbol property serialization",
+ expectResults: expect.arrayContaining([
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ ]),
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+})
+
+describe("Serialization Edge Cases - Special Numbers", () => {
+ test("should handle NaN assertions correctly", async () => {
+ const testScript = `
+ pm.test("NaN handling", () => {
+ const result = 0 / 0;
+
+ // NaN is not equal to itself
+ pm.expect(result).to.be.NaN;
+ pm.expect(result).to.not.equal(result);
+ pm.expect(Number.isNaN(result)).to.be.true;
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "NaN handling",
+ expectResults: expect.arrayContaining([
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ ]),
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+
+ test("should handle Infinity and -Infinity", async () => {
+ const testScript = `
+ pm.test("Infinity values", () => {
+ const posInf = 1 / 0;
+ const negInf = -1 / 0;
+
+ pm.expect(posInf).to.equal(Infinity);
+ pm.expect(negInf).to.equal(-Infinity);
+ pm.expect(posInf).to.not.equal(negInf);
+ pm.expect(Number.isFinite(posInf)).to.be.false;
+ pm.expect(Number.isFinite(negInf)).to.be.false;
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "Infinity values",
+ expectResults: expect.arrayContaining([
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ ]),
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+
+ test("should distinguish between +0 and -0", async () => {
+ const testScript = `
+ pm.test("Negative zero handling", () => {
+ const positiveZero = 0;
+ const negativeZero = -0;
+
+ // In most contexts, +0 and -0 are equal
+ pm.expect(positiveZero).to.equal(negativeZero);
+ pm.expect(positiveZero === negativeZero).to.be.true;
+
+ // But they can be distinguished with Object.is or 1/x
+ pm.expect(Object.is(positiveZero, negativeZero)).to.be.false;
+ pm.expect(1 / positiveZero).to.equal(Infinity);
+ pm.expect(1 / negativeZero).to.equal(-Infinity);
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "Negative zero handling",
+ expectResults: expect.arrayContaining([
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ ]),
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+
+ test("should handle special numbers in JSON serialization", async () => {
+ const testScript = `
+ pm.test("Special numbers in JSON", () => {
+ const obj = {
+ nan: NaN,
+ inf: Infinity,
+ negInf: -Infinity,
+ zero: 0,
+ negZero: -0
+ };
+
+ const jsonStr = JSON.stringify(obj);
+ const parsed = JSON.parse(jsonStr);
+
+ // NaN, Infinity, -Infinity become null in JSON
+ pm.expect(parsed.nan).to.be.null;
+ pm.expect(parsed.inf).to.be.null;
+ pm.expect(parsed.negInf).to.be.null;
+
+ // Both zeros become 0
+ pm.expect(parsed.zero).to.equal(0);
+ pm.expect(parsed.negZero).to.equal(0);
+ });
+ `
+
+ const result = await runTestWithResponse(testScript, fakeResponse)()
+ expect(result).toEqualRight(
+ expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "root",
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ descriptor: "Special numbers in JSON",
+ expectResults: expect.arrayContaining([
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ expect.objectContaining({ status: "pass" }),
+ ]),
+ }),
+ ]),
+ }),
+ ])
+ )
+ })
+})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/type-coercion.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/type-coercion.spec.ts
new file mode 100644
index 00000000..e68d69ef
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/type-coercion.spec.ts
@@ -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'" },
+ ],
+ }),
+ ])
+ })
+ })
+})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/unsupported.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/unsupported.spec.ts
index df509bb1..490e4716 100644
--- a/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/unsupported.spec.ts
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/unsupported.spec.ts
@@ -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("
Test
")
+ 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) }],
+ }),
+ ],
+ }),
+ ])
+ })
})
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/variables.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/variables.spec.ts
index d9c8fe7d..36a32709 100644
--- a/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/variables.spec.ts
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pm-namespace/variables.spec.ts
@@ -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()
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/env/get.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/env/get.spec.ts
index 4071efb3..8257b96e 100644
--- a/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/env/get.spec.ts
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/env/get.spec.ts
@@ -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("<>")
@@ -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)
`,
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/env/getResolve.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/env/getResolve.spec.ts
index 0a9f78a4..d6b93502 100644
--- a/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/env/getResolve.spec.ts
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/env/getResolve.spec.ts
@@ -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("<>")
@@ -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)
`,
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/env/resolve.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/env/resolve.spec.ts
index d2c04759..671b7852 100644
--- a/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/env/resolve.spec.ts
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/env/resolve.spec.ts
@@ -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("<>")
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("<>")
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("<>")
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("<>")
pw.expect(data).toBe("<>")
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/env/set.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/env/set.spec.ts
index 01762bb2..fec21bf1 100644
--- a/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/env/set.spec.ts
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/env/set.spec.ts
@@ -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")
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/env/unset.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/env/unset.spec.ts
index c9c9f4bc..10ba2f2a 100644
--- a/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/env/unset.spec.ts
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/env/unset.spec.ts
@@ -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)
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/expect/toBe.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/expect/toBe.spec.ts
index 0bd6cfea..4a02d217 100644
--- a/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/expect/toBe.spec.ts
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/expect/toBe.spec.ts
@@ -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")
`,
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/expect/toBeLevelxxx.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/expect/toBeLevelxxx.spec.ts
index e7417a1c..bedbf963 100644
--- a/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/expect/toBeLevelxxx.spec.ts
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/expect/toBeLevelxxx.spec.ts
@@ -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: [
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/expect/toBeType.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/expect/toBeType.spec.ts
index af1ded1c..a7fd0408 100644
--- a/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/expect/toBeType.spec.ts
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/expect/toBeType.spec.ts
@@ -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")
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/expect/toHaveLength.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/expect/toHaveLength.spec.ts
index 7e2f9f87..6449f5fc 100644
--- a/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/expect/toHaveLength.spec.ts
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/expect/toHaveLength.spec.ts
@@ -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")
`,
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/expect/toInclude.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/expect/toInclude.spec.ts
index 627d228e..0959d507 100644
--- a/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/expect/toInclude.spec.ts
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/expect/toInclude.spec.ts
@@ -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)
`,
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/test-runner.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/test-runner.spec.ts
index 82c1f64d..c233cdeb 100644
--- a/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/test-runner.spec.ts
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/pw-namespace/test-runner.spec.ts
@@ -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;
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/utils/pre-request.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/utils/pre-request.spec.ts
index 2ee4b30e..82229de0 100644
--- a/packages/hoppscotch-js-sandbox/src/__tests__/utils/pre-request.spec.ts
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/utils/pre-request.spec.ts
@@ -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", () => {
diff --git a/packages/hoppscotch-js-sandbox/src/__tests__/utils/shared.spec.ts b/packages/hoppscotch-js-sandbox/src/__tests__/utils/shared.spec.ts
index 631128c8..75d24282 100644
--- a/packages/hoppscotch-js-sandbox/src/__tests__/utils/shared.spec.ts
+++ b/packages/hoppscotch-js-sandbox/src/__tests__/utils/shared.spec.ts
@@ -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 <>",
+ initialValue: "Hello <>",
+ secret: false,
+ },
+ ],
+ },
+ true
+ )
+
+ const resolved = methods.pw.resolve("<>")
+ 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: "<>/users",
+ initialValue: "<>/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 <>",
+ initialValue: "Hello <>",
+ secret: false,
+ },
+ ],
+ },
+ false
+ )
+
+ const resolved = methods.env.resolve("<>")
+ 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://<>/api",
+ initialValue: "https://<>/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()
+ })
+})
diff --git a/packages/hoppscotch-js-sandbox/src/bootstrap-code/post-request.js b/packages/hoppscotch-js-sandbox/src/bootstrap-code/post-request.js
index 325f1c5a..78624ddd 100644
--- a/packages/hoppscotch-js-sandbox/src/bootstrap-code/post-request.js
+++ b/packages/hoppscotch-js-sandbox/src/bootstrap-code/post-request.js
@@ -2,6 +2,1892 @@
;(inputs) => {
// Keep strict mode scoped to this IIFE to avoid leaking strictness to concatenated/bootstrapped code
"use strict"
+
+ // Chai proxy builder - creates a Chai-like API using actual Chai SDK
+ if (!globalThis.__createChaiProxy) {
+ globalThis.__createChaiProxy = function (
+ expectVal,
+ inputs,
+ modifiers = " to"
+ ) {
+ const proxy = {}
+
+ // Helper to create a new proxy with updated modifiers
+ const withModifiers = (newModifiers) => {
+ return globalThis.__createChaiProxy(expectVal, inputs, newModifiers)
+ }
+
+ // Assertion methods
+ proxy.equal = (expected) => {
+ // PRE-CHECK PATTERN: Track object identity for reference equality
+ // Special handling for Date/RegExp which serialize to primitives
+ let isSameReference = undefined
+ let typeInfo = undefined
+
+ if (
+ expectVal !== null &&
+ expected !== null &&
+ typeof expectVal === "object" &&
+ typeof expected === "object"
+ ) {
+ isSameReference = expectVal === expected
+
+ // Track type for objects that lose identity during serialization
+ if (expectVal instanceof Date && expected instanceof Date) {
+ typeInfo = {
+ type: "Date",
+ valueTime: expectVal.getTime(),
+ expectedTime: expected.getTime(),
+ }
+ } else if (
+ expectVal instanceof RegExp &&
+ expected instanceof RegExp
+ ) {
+ typeInfo = {
+ type: "RegExp",
+ valueSource: expectVal.source,
+ valueFlags: expectVal.flags,
+ expectedSource: expected.source,
+ expectedFlags: expected.flags,
+ }
+ }
+ }
+
+ inputs.chaiEqual(
+ expectVal,
+ expected,
+ modifiers,
+ "equal",
+ isSameReference,
+ typeInfo
+ )
+ return withModifiers(modifiers)
+ }
+ proxy.equals = (expected) => {
+ // PRE-CHECK PATTERN: Track object identity for reference equality
+ // Only pass isSameReference when BOTH values are objects
+ let isSameReference = undefined
+ if (
+ expectVal !== null &&
+ expected !== null &&
+ typeof expectVal === "object" &&
+ typeof expected === "object"
+ ) {
+ isSameReference = expectVal === expected
+ }
+ inputs.chaiEqual(
+ expectVal,
+ expected,
+ modifiers,
+ "equals",
+ isSameReference
+ )
+ return withModifiers(modifiers)
+ }
+ proxy.eq = (expected) => {
+ // PRE-CHECK PATTERN: Track object identity for reference equality
+ // Only pass isSameReference when BOTH values are objects
+ let isSameReference = undefined
+ if (
+ expectVal !== null &&
+ expected !== null &&
+ typeof expectVal === "object" &&
+ typeof expected === "object"
+ ) {
+ isSameReference = expectVal === expected
+ }
+ inputs.chaiEqual(expectVal, expected, modifiers, "eq", isSameReference)
+ return withModifiers(modifiers)
+ }
+ proxy.eql = (expected) => {
+ // PRE-CHECK PATTERN: Extract metadata from special objects before serialization
+ let valueMetadata = undefined
+ let expectedMetadata = undefined
+
+ // Handle RegExp objects - extract pattern and flags
+ if (expectVal instanceof RegExp && expected instanceof RegExp) {
+ valueMetadata = {
+ type: "RegExp",
+ source: expectVal.source,
+ flags: expectVal.flags,
+ }
+ expectedMetadata = {
+ type: "RegExp",
+ source: expected.source,
+ flags: expected.flags,
+ }
+ }
+ // Handle Date objects - extract timestamp
+ else if (expectVal instanceof Date && expected instanceof Date) {
+ valueMetadata = {
+ type: "Date",
+ time: expectVal.getTime(),
+ }
+ expectedMetadata = {
+ type: "Date",
+ time: expected.getTime(),
+ }
+ }
+
+ inputs.chaiEql(
+ expectVal,
+ expected,
+ modifiers,
+ valueMetadata,
+ expectedMetadata
+ )
+ return withModifiers(modifiers)
+ }
+
+ // Custom Postman methods - delegates to chai-helpers.ts
+ proxy.jsonSchema = (schema) => {
+ if (!inputs.chaiJsonSchema) {
+ throw new Error("chaiJsonSchema method not found in inputs")
+ }
+ inputs.chaiJsonSchema(expectVal, schema, modifiers)
+ return withModifiers(modifiers)
+ }
+
+ proxy.charset = (expectedCharset) => {
+ if (!inputs.chaiCharset) {
+ throw new Error("chaiCharset method not found in inputs")
+ }
+ inputs.chaiCharset(expectVal, expectedCharset, modifiers)
+ return withModifiers(modifiers)
+ }
+
+ proxy.cookie = (cookieName, cookieValue) => {
+ if (!inputs.chaiCookie) {
+ throw new Error("chaiCookie method not found in inputs")
+ }
+ inputs.chaiCookie(expectVal, cookieName, cookieValue, modifiers)
+ return withModifiers(modifiers)
+ }
+
+ proxy.jsonPath = (path, expectedValue) => {
+ if (!inputs.chaiJsonPath) {
+ throw new Error("chaiJsonPath method not found in inputs")
+ }
+ inputs.chaiJsonPath(expectVal, path, expectedValue, modifiers)
+ return withModifiers(modifiers)
+ }
+
+ // .a() and .an() can be both type assertion methods and language chains for .instanceof
+ const aMethod = (type) => {
+ // PRE-CHECK PATTERN: Check typeof BEFORE serialization
+ // Special handling for null (typeof null === 'object' in JS)
+ let actualType = typeof expectVal
+ if (expectVal === null) {
+ actualType = "null"
+ } else if (Array.isArray(expectVal)) {
+ actualType = "array"
+ }
+ // Record the type assertion (valid terminal assertion)
+ inputs.chaiTypeOf(expectVal, type, modifiers, actualType)
+ return withModifiers(modifiers + ` a ${type}`)
+ }
+ // Add .instanceof as a property of the function
+ const aInstanceOfMethod = function (constructor) {
+ // CRITICAL: Perform instanceof check HERE in the sandbox before serialization
+ const actualInstanceCheck = expectVal instanceof constructor
+
+ const objectType = Object.prototype.toString.call(expectVal)
+ let constructorName = "Unknown"
+ try {
+ if (constructor && typeof constructor.name === "string") {
+ constructorName = constructor.name
+ } else {
+ constructorName = String(constructor)
+ }
+ } catch (_e) {
+ constructorName = String(constructor)
+ }
+
+ // PRE-FORMAT: Create display string for Set/Map before serialization
+ let displayValue = null
+ if (expectVal instanceof Set) {
+ const values = Array.from(expectVal).slice(0, 10)
+ displayValue =
+ values.length > 0 ? `new Set([${values.join(", ")}])` : "new Set()"
+ } else if (expectVal instanceof Map) {
+ const entries = Array.from(expectVal.entries()).slice(0, 3)
+ if (entries.length > 0) {
+ const formatted = entries.map(([k, v]) => `['${k}', ${v}]`)
+ displayValue = `new Map([${formatted.join(", ")}])`
+ } else {
+ displayValue = "new Map()"
+ }
+ }
+
+ inputs.chaiInstanceOf(
+ expectVal,
+ constructorName,
+ modifiers,
+ objectType,
+ displayValue,
+ actualInstanceCheck // Pass the pre-checked result!
+ )
+ return withModifiers(modifiers)
+ }
+ aMethod.instanceof = aInstanceOfMethod
+ aMethod.instanceOf = aInstanceOfMethod // Support both lowercase and camelCase
+ proxy.a = aMethod
+
+ const anMethod = (type) => {
+ // Record the type assertion (valid terminal assertion)
+ inputs.chaiTypeOf(expectVal, type, modifiers)
+ return withModifiers(modifiers + ` an ${type}`)
+ }
+ // Add .instanceof as a property of the function
+ const instanceOfMethod = function (constructor) {
+ // CRITICAL: Perform instanceof check HERE in the sandbox before serialization
+ const actualInstanceCheck = expectVal instanceof constructor
+
+ const objectType = Object.prototype.toString.call(expectVal)
+ let constructorName = "Unknown"
+ try {
+ if (constructor && typeof constructor.name === "string") {
+ constructorName = constructor.name
+ } else {
+ constructorName = String(constructor)
+ }
+ } catch (_e) {
+ constructorName = String(constructor)
+ }
+
+ // PRE-FORMAT: Create display string for Set/Map before serialization
+ let displayValue = null
+ if (expectVal instanceof Set) {
+ const values = Array.from(expectVal).slice(0, 10)
+ displayValue =
+ values.length > 0 ? `new Set([${values.join(", ")}])` : "new Set()"
+ } else if (expectVal instanceof Map) {
+ const entries = Array.from(expectVal.entries()).slice(0, 3)
+ if (entries.length > 0) {
+ const formatted = entries.map(([k, v]) => `['${k}', ${v}]`)
+ displayValue = `new Map([${formatted.join(", ")}])`
+ } else {
+ displayValue = "new Map()"
+ }
+ }
+
+ return inputs.chaiInstanceOf(
+ expectVal,
+ constructorName,
+ modifiers,
+ objectType,
+ displayValue,
+ actualInstanceCheck // Pass the pre-checked result!
+ )
+ }
+ anMethod.instanceof = instanceOfMethod
+ anMethod.instanceOf = instanceOfMethod // Support both lowercase and camelCase
+ proxy.an = anMethod
+
+ // Include can be both a method and have properties (for .include.members and .include.all.keys)
+ const includeMethod = (item) => {
+ inputs.chaiInclude(expectVal, item, modifiers)
+ return withModifiers(modifiers + ` include ${JSON.stringify(item)}`)
+ }
+ includeMethod.members = (members) => {
+ inputs.chaiIncludeMembers(expectVal, members, modifiers)
+ return withModifiers(modifiers + ` include members`)
+ }
+ // Add .all for .include.all.keys
+ Object.defineProperty(includeMethod, "all", {
+ get: () => {
+ const newModifiers = modifiers + " include all"
+ const allProxy = {}
+ allProxy.keys = (...keys) => {
+ inputs.chaiAllKeys(expectVal, keys, newModifiers)
+ return withModifiers(newModifiers)
+ }
+ return allProxy
+ },
+ })
+ // Add .any for .include.any.keys
+ Object.defineProperty(includeMethod, "any", {
+ get: () => {
+ const newModifiers = modifiers + " include any"
+ const anyProxy = {}
+ anyProxy.keys = (...keys) => {
+ inputs.chaiAnyKeys(expectVal, keys, newModifiers)
+ return withModifiers(newModifiers)
+ }
+ return anyProxy
+ },
+ })
+ // Add .keys for .include.keys
+ includeMethod.keys = (...keys) => {
+ const keysArray =
+ keys.length === 1 && Array.isArray(keys[0]) ? keys[0] : keys
+ inputs.chaiIncludeKeys(expectVal, keysArray, modifiers)
+ return withModifiers(modifiers)
+ }
+ // Add .deep for .include.deep.ordered.members and deep.include()
+ Object.defineProperty(includeMethod, "deep", {
+ get: () => {
+ const newModifiers = modifiers + " include deep"
+ // Create a function that can be called as deep.include(obj)
+ const deepIncludeFn = (item) => {
+ inputs.chaiDeepInclude(expectVal, item, newModifiers)
+ return withModifiers(newModifiers)
+ }
+ // Add .ordered for .include.deep.ordered
+ Object.defineProperty(deepIncludeFn, "ordered", {
+ get: () => {
+ const orderedModifiers = newModifiers + " ordered"
+ const orderedProxy = {}
+ orderedProxy.members = (members) => {
+ inputs.chaiIncludeDeepOrderedMembers(
+ expectVal,
+ members,
+ orderedModifiers
+ )
+ return withModifiers(orderedModifiers)
+ }
+ return orderedProxy
+ },
+ })
+ return deepIncludeFn
+ },
+ })
+ proxy.include = includeMethod
+
+ proxy.includes = (item) => {
+ inputs.chaiInclude(expectVal, item, modifiers)
+ return withModifiers(modifiers + ` include ${JSON.stringify(item)}`)
+ }
+
+ // Contain can be both a method and have properties (for .contain.oneOf and .contain.members)
+ const containMethod = (item) => {
+ inputs.chaiInclude(expectVal, item, modifiers)
+ return withModifiers(modifiers + ` include ${JSON.stringify(item)}`)
+ }
+ containMethod.oneOf = (list) => {
+ inputs.chaiOneOf(expectVal, list, modifiers + " include")
+ return withModifiers(modifiers + " include oneOf")
+ }
+ containMethod.members = (members) => {
+ inputs.chaiIncludeMembers(expectVal, members, modifiers)
+ return withModifiers(modifiers + ` include members`)
+ }
+ proxy.contain = containMethod
+
+ proxy.contains = (item) => {
+ inputs.chaiInclude(expectVal, item, modifiers)
+ return withModifiers(modifiers + ` include ${JSON.stringify(item)}`)
+ }
+
+ // .lengthOf can be used both as a method and as a language chain
+ // As method: expect(arr).to.have.lengthOf(5)
+ // As chain: expect(arr).to.have.lengthOf.at.least(5)
+ Object.defineProperty(proxy, "lengthOf", {
+ get: () => {
+ // Extract actual length for comparison operations
+ let actualLength
+ let actualSize, typeName, serializedContent
+
+ // PRE-CHECK PATTERN: Extract size from Set/Map before serialization
+ if (expectVal instanceof Set) {
+ actualSize = expectVal.size
+ actualLength = actualSize
+ typeName = "Set"
+ serializedContent = Array.from(expectVal)
+ } else if (expectVal instanceof Map) {
+ actualSize = expectVal.size
+ actualLength = actualSize
+ typeName = "Map"
+ serializedContent = Array.from(expectVal)
+ } else {
+ try {
+ actualLength =
+ expectVal && typeof expectVal.length !== "undefined"
+ ? expectVal.length
+ : expectVal && typeof expectVal.size !== "undefined"
+ ? expectVal.size
+ : undefined
+ } catch (_e) {
+ actualLength = undefined
+ }
+ }
+
+ // Create a callable function that also has chainable properties
+ const lengthOfFn = (length) => {
+ inputs.chaiLengthOf(
+ serializedContent || expectVal,
+ length,
+ modifiers,
+ "lengthOf",
+ actualSize,
+ typeName
+ )
+ return withModifiers(modifiers + ` lengthOf ${length}`)
+ }
+
+ // Add comparison methods for chaining (like .lengthOf.at.least())
+ if (actualLength !== undefined) {
+ lengthOfFn.above = (n) => {
+ inputs.chaiAbove(actualLength, n, modifiers)
+ return withModifiers(modifiers)
+ }
+
+ lengthOfFn.below = (n) => {
+ inputs.chaiBelow(actualLength, n, modifiers)
+ return withModifiers(modifiers)
+ }
+
+ lengthOfFn.within = (start, end) => {
+ inputs.chaiWithin(actualLength, start, end, modifiers)
+ return withModifiers(modifiers)
+ }
+
+ lengthOfFn.least = (n) => {
+ inputs.chaiAtLeast(actualLength, n, modifiers)
+ return withModifiers(modifiers)
+ }
+
+ lengthOfFn.most = (n) => {
+ inputs.chaiAtMost(actualLength, n, modifiers)
+ return withModifiers(modifiers)
+ }
+
+ lengthOfFn.greaterThan = (n) => {
+ inputs.chaiAbove(actualLength, n, modifiers)
+ return withModifiers(modifiers)
+ }
+
+ lengthOfFn.lessThan = (n) => {
+ inputs.chaiBelow(actualLength, n, modifiers)
+ return withModifiers(modifiers)
+ }
+
+ lengthOfFn.gte = (n) => {
+ inputs.chaiAtLeast(actualLength, n, modifiers)
+ return withModifiers(modifiers)
+ }
+
+ lengthOfFn.lte = (n) => {
+ inputs.chaiAtMost(actualLength, n, modifiers)
+ return withModifiers(modifiers)
+ }
+
+ // Add .at language chain for .lengthOf.at.least() and .lengthOf.at.most()
+ Object.defineProperty(lengthOfFn, "at", {
+ get: () => {
+ const atProxy = withModifiers(modifiers + " at")
+ atProxy.least = (n) => {
+ inputs.chaiAtLeast(actualLength, n, modifiers)
+ return withModifiers(modifiers)
+ }
+ atProxy.most = (n) => {
+ inputs.chaiAtMost(actualLength, n, modifiers)
+ return withModifiers(modifiers)
+ }
+ return atProxy
+ },
+ configurable: true,
+ enumerable: false,
+ })
+ }
+
+ return lengthOfFn
+ },
+ configurable: true,
+ enumerable: false,
+ })
+ // .length as getter property for chaining: .length.above(), .length.below(), .length.within()
+ // Also supports .length(n) as method for exact length
+ Object.defineProperty(proxy, "length", {
+ get: () => {
+ // Extract actual length value
+ let actualLength
+ try {
+ actualLength = expectVal.length
+ } catch (_e) {
+ actualLength = undefined
+ }
+
+ // Create callable function for exact length: .length(n)
+ const lengthProxy = (length) => {
+ // PRE-CHECK PATTERN: Extract size from Set/Map before serialization
+ let actualSize, typeName, serializedContent
+ if (expectVal instanceof Set) {
+ actualSize = expectVal.size
+ typeName = "Set"
+ serializedContent = Array.from(expectVal)
+ } else if (expectVal instanceof Map) {
+ actualSize = expectVal.size
+ typeName = "Map"
+ serializedContent = Array.from(expectVal)
+ }
+ inputs.chaiLengthOf(
+ serializedContent || expectVal,
+ length,
+ modifiers,
+ "length",
+ actualSize,
+ typeName
+ )
+ // Return proxy wrapping the LENGTH VALUE for chaining (.which, .that, etc.)
+ return globalThis.__createChaiProxy(
+ actualLength,
+ inputs,
+ modifiers + ` length ${length}`
+ )
+ }
+
+ // Add comparison methods for chaining
+ lengthProxy.above = (n) => {
+ inputs.chaiAbove(actualLength, n, modifiers)
+ return withModifiers(modifiers)
+ }
+
+ lengthProxy.below = (n) => {
+ inputs.chaiBelow(actualLength, n, modifiers)
+ return withModifiers(modifiers)
+ }
+
+ lengthProxy.within = (start, end) => {
+ inputs.chaiWithin(actualLength, start, end, modifiers)
+ return withModifiers(modifiers)
+ }
+
+ lengthProxy.least = (n) => {
+ inputs.chaiAtLeast(actualLength, n, modifiers)
+ return withModifiers(modifiers)
+ }
+
+ lengthProxy.most = (n) => {
+ inputs.chaiAtMost(actualLength, n, modifiers)
+ return withModifiers(modifiers)
+ }
+
+ lengthProxy.greaterThan = (n) => {
+ inputs.chaiAbove(actualLength, n, modifiers)
+ return withModifiers(modifiers)
+ }
+
+ lengthProxy.lessThan = (n) => {
+ inputs.chaiBelow(actualLength, n, modifiers)
+ return withModifiers(modifiers)
+ }
+
+ lengthProxy.gte = (n) => {
+ inputs.chaiAtLeast(actualLength, n, modifiers)
+ return withModifiers(modifiers)
+ }
+
+ lengthProxy.lte = (n) => {
+ inputs.chaiAtMost(actualLength, n, modifiers)
+ return withModifiers(modifiers)
+ }
+
+ // Add .at language chain for .length.at.least() and .length.at.most()
+ Object.defineProperty(lengthProxy, "at", {
+ get: () => {
+ const atProxy = withModifiers(modifiers + " at")
+ atProxy.least = (n) => {
+ inputs.chaiAtLeast(actualLength, n, modifiers)
+ return withModifiers(modifiers)
+ }
+ atProxy.most = (n) => {
+ inputs.chaiAtMost(actualLength, n, modifiers)
+ return withModifiers(modifiers)
+ }
+ return atProxy
+ },
+ configurable: true,
+ enumerable: false,
+ })
+
+ return lengthProxy
+ },
+ configurable: true,
+ enumerable: false,
+ })
+
+ proxy.property = (prop, val) => {
+ // PRE-CHECK PATTERN: Special handling for Map/Set size property
+ // Map and Set serialize as {} losing .size property, so extract it first
+ if (
+ prop === "size" &&
+ (expectVal instanceof Map || expectVal instanceof Set)
+ ) {
+ const actualSize = expectVal.size
+ // Call chaiProperty with actual size value
+ inputs.chaiProperty(
+ { size: actualSize }, // Wrap size in object
+ prop,
+ val,
+ modifiers
+ )
+ // For chaining, return proxy wrapping the size value
+ return globalThis.__createChaiProxy(actualSize, inputs, modifiers)
+ }
+
+ // PRE-CHECK PATTERN: Check property existence (including inherited) BEFORE serialization
+ // The 'in' operator checks for both own and inherited properties
+ let hasProperty = undefined
+ if (expectVal !== null && typeof expectVal === "object") {
+ hasProperty = prop in expectVal
+ }
+
+ // When val is provided, assert the property value directly
+ inputs.chaiProperty(expectVal, prop, val, modifiers, hasProperty)
+
+ // For chaining (.that, .which), we need to return a proxy wrapping the PROPERTY VALUE
+ // Extract the property value from the object
+ let propertyValue
+ try {
+ propertyValue = expectVal[prop]
+ } catch (_e) {
+ propertyValue = undefined
+ }
+
+ // Return a new proxy wrapping the property value, not the original object
+ // This allows: .property('a').that.equals(1) to work
+ return globalThis.__createChaiProxy(propertyValue, inputs, modifiers)
+ }
+ proxy.ownProperty = (prop) => {
+ // PRE-CHECK PATTERN: Check hasOwnProperty BEFORE serialization
+ // Prototype chain is lost when objects cross sandbox boundary
+ // Only pass isOwnProperty when value is an object
+ let isOwnProperty = undefined
+ if (expectVal !== null && typeof expectVal === "object") {
+ isOwnProperty = Object.prototype.hasOwnProperty.call(expectVal, prop)
+ }
+
+ inputs.chaiOwnProperty(expectVal, prop, modifiers, isOwnProperty)
+
+ // For chaining, return proxy wrapping the property value
+ let propertyValue
+ try {
+ propertyValue = expectVal[prop]
+ } catch (_e) {
+ propertyValue = undefined
+ }
+
+ return globalThis.__createChaiProxy(propertyValue, inputs, modifiers)
+ }
+ proxy.ownPropertyDescriptor = (prop, descriptor) => {
+ // PRE-CHECK PATTERN: Check property descriptor in sandbox before serialization
+ let hasDescriptor = false
+ let actualDescriptor = null
+ let matchesExpected = false
+
+ try {
+ actualDescriptor = Object.getOwnPropertyDescriptor(expectVal, prop)
+ hasDescriptor = actualDescriptor !== undefined
+
+ // If descriptor argument provided, check if it matches
+ if (hasDescriptor && descriptor !== undefined) {
+ matchesExpected = true
+ // Compare each property of the descriptor
+ for (const key in descriptor) {
+ if (descriptor[key] !== actualDescriptor[key]) {
+ matchesExpected = false
+ break
+ }
+ }
+ }
+ } catch (_e) {
+ hasDescriptor = false
+ }
+
+ inputs.chaiOwnPropertyDescriptor(
+ expectVal,
+ prop,
+ descriptor,
+ modifiers,
+ hasDescriptor,
+ matchesExpected
+ )
+
+ // Return a proxy that can chain with .that to access the descriptor
+ const descriptorProxy = withModifiers(modifiers)
+ // Delete existing .that property if it exists, then redefine it
+ delete descriptorProxy.that
+ Object.defineProperty(descriptorProxy, "that", {
+ configurable: true,
+ enumerable: false,
+ get: () => {
+ // Return a new Chai proxy wrapping the descriptor itself
+ return globalThis.__createChaiProxy(actualDescriptor, inputs, " to")
+ },
+ })
+ return descriptorProxy
+ }
+
+ proxy.above = (n) => {
+ inputs.chaiAbove(expectVal, n, modifiers)
+ return withModifiers(modifiers + ` above ${n}`)
+ }
+ proxy.gt = (n) => {
+ inputs.chaiAbove(expectVal, n, modifiers)
+ return withModifiers(modifiers + ` above ${n}`)
+ }
+ proxy.greaterThan = (n) => {
+ inputs.chaiAbove(expectVal, n, modifiers)
+ return withModifiers(modifiers + ` above ${n}`)
+ }
+ proxy.greaterThanOrEqual = (n) => {
+ inputs.chaiAtLeast(expectVal, n, modifiers)
+ return withModifiers(modifiers + ` at least ${n}`)
+ }
+ proxy.gte = (n) => {
+ inputs.chaiAtLeast(expectVal, n, modifiers)
+ return withModifiers(modifiers + ` at least ${n}`)
+ }
+
+ proxy.below = (n) => {
+ inputs.chaiBelow(expectVal, n, modifiers)
+ return withModifiers(modifiers + ` below ${n}`)
+ }
+ proxy.lt = (n) => {
+ inputs.chaiBelow(expectVal, n, modifiers)
+ return withModifiers(modifiers + ` below ${n}`)
+ }
+ proxy.lessThan = (n) => {
+ inputs.chaiBelow(expectVal, n, modifiers)
+ return withModifiers(modifiers + ` below ${n}`)
+ }
+ proxy.lessThanOrEqual = (n) => {
+ inputs.chaiAtMost(expectVal, n, modifiers)
+ return withModifiers(modifiers + ` at most ${n}`)
+ }
+ proxy.lte = (n) => {
+ inputs.chaiAtMost(expectVal, n, modifiers)
+ return withModifiers(modifiers + ` at most ${n}`)
+ }
+
+ proxy.within = (start, end) => {
+ inputs.chaiWithin(expectVal, start, end, modifiers)
+ return withModifiers(modifiers)
+ }
+ proxy.closeTo = (expected, delta) => {
+ inputs.chaiCloseTo(expectVal, expected, delta, modifiers, "closeTo")
+ return withModifiers(modifiers)
+ }
+ proxy.approximately = (expected, delta) => {
+ inputs.chaiCloseTo(
+ expectVal,
+ expected,
+ delta,
+ modifiers,
+ "approximately"
+ )
+ return withModifiers(modifiers)
+ }
+
+ proxy.keys = (...keys) => {
+ // Support both keys('a', 'b') and keys(['a', 'b'])
+ const keysArray =
+ keys.length === 1 && Array.isArray(keys[0]) ? keys[0] : keys
+ inputs.chaiKeys(expectVal, keysArray, modifiers)
+ return withModifiers(modifiers)
+ }
+ proxy.key = (key) => {
+ // Support both key('a') and key(['a'])
+ const keysArray = Array.isArray(key) ? key : [key]
+ inputs.chaiKeys(expectVal, keysArray, modifiers)
+ return withModifiers(modifiers)
+ }
+
+ proxy.match = (pattern) => {
+ // PRE-CHECK PATTERN: Extract RegExp source and flags before serialization
+ let regexSource, regexFlags
+ if (pattern instanceof RegExp) {
+ regexSource = pattern.source
+ regexFlags = pattern.flags
+ }
+ return inputs.chaiMatch(
+ expectVal,
+ pattern,
+ modifiers,
+ regexSource,
+ regexFlags
+ )
+ }
+ proxy.matches = (pattern) => {
+ // PRE-CHECK PATTERN: Extract RegExp source and flags before serialization
+ let regexSource, regexFlags
+ if (pattern instanceof RegExp) {
+ regexSource = pattern.source
+ regexFlags = pattern.flags
+ }
+ return inputs.chaiMatch(
+ expectVal,
+ pattern,
+ modifiers,
+ regexSource,
+ regexFlags
+ )
+ }
+ proxy.string = (substring) =>
+ inputs.chaiString(expectVal, substring, modifiers)
+
+ proxy.members = (members) =>
+ inputs.chaiMembers(expectVal, members, modifiers)
+ proxy.oneOf = (list) => inputs.chaiOneOf(expectVal, list, modifiers)
+
+ proxy.throw = (errorLike, errMsgMatcher) => {
+ // PRE-CHECK PATTERN: Execute function in sandbox, pass results
+ let threwError = false
+ let errorTypeName = null
+ let errorMessage = null
+
+ if (typeof expectVal === "function") {
+ try {
+ expectVal()
+ } catch (e) {
+ threwError = true
+ errorTypeName = e.constructor?.name || "Error"
+ errorMessage = e.message || String(e)
+ }
+ }
+
+ // Handle parameter interpretation like Chai does:
+ // .throw() - no params
+ // .throw(ErrorType) - constructor function
+ // .throw('message') - string message
+ // .throw(/pattern/) - regex pattern
+ // .throw(ErrorType, 'message') - constructor + message
+ // .throw(ErrorType, /pattern/) - constructor + regex
+
+ let actualErrorLike = errorLike
+ let actualErrMsgMatcher = errMsgMatcher
+
+ // If first param is string or regex but no second param,
+ // treat first param as message matcher, not error type
+ if (errorLike !== undefined && errMsgMatcher === undefined) {
+ if (typeof errorLike === "string" || errorLike instanceof RegExp) {
+ actualErrorLike = undefined
+ actualErrMsgMatcher = errorLike
+ }
+ }
+
+ // Get error type name for matching
+ let expectedTypeName = null
+ if (actualErrorLike && typeof actualErrorLike === "function") {
+ expectedTypeName = actualErrorLike.name
+ }
+
+ // Convert RegExp to serializable form or pass string message
+ let regexSource, regexFlags
+ let isRegexMatcher = false
+ if (actualErrMsgMatcher instanceof RegExp) {
+ regexSource = actualErrMsgMatcher.source
+ regexFlags = actualErrMsgMatcher.flags
+ isRegexMatcher = true
+ }
+
+ return inputs.chaiThrow(
+ expectVal,
+ threwError,
+ errorTypeName,
+ errorMessage,
+ expectedTypeName,
+ actualErrMsgMatcher,
+ regexSource,
+ regexFlags,
+ isRegexMatcher,
+ modifiers
+ )
+ }
+ proxy.throws = (errorLike, errMsgMatcher) => {
+ // PRE-CHECK PATTERN: Execute function in sandbox, pass results
+ let threwError = false
+ let errorTypeName = null
+ let errorMessage = null
+
+ if (typeof expectVal === "function") {
+ try {
+ expectVal()
+ } catch (e) {
+ threwError = true
+ errorTypeName = e.constructor?.name || "Error"
+ errorMessage = e.message || String(e)
+ }
+ }
+
+ // Handle parameter interpretation like Chai does
+ let actualErrorLike = errorLike
+ let actualErrMsgMatcher = errMsgMatcher
+
+ // If first param is string or regex but no second param,
+ // treat first param as message matcher, not error type
+ if (errorLike !== undefined && errMsgMatcher === undefined) {
+ if (typeof errorLike === "string" || errorLike instanceof RegExp) {
+ actualErrorLike = undefined
+ actualErrMsgMatcher = errorLike
+ }
+ }
+
+ // Get error type name for matching
+ let expectedTypeName = null
+ if (actualErrorLike && typeof actualErrorLike === "function") {
+ expectedTypeName = actualErrorLike.name
+ }
+
+ // Convert RegExp to serializable form or pass string message
+ let regexSource, regexFlags
+ let isRegexMatcher = false
+ if (actualErrMsgMatcher instanceof RegExp) {
+ regexSource = actualErrMsgMatcher.source
+ regexFlags = actualErrMsgMatcher.flags
+ isRegexMatcher = true
+ }
+
+ return inputs.chaiThrow(
+ expectVal,
+ threwError,
+ errorTypeName,
+ errorMessage,
+ expectedTypeName,
+ actualErrMsgMatcher,
+ regexSource,
+ regexFlags,
+ isRegexMatcher,
+ modifiers
+ )
+ }
+ proxy.Throw = (errorLike, errMsgMatcher) => {
+ // PRE-CHECK PATTERN: Execute function in sandbox, pass results
+ let threwError = false
+ let errorTypeName = null
+ let errorMessage = null
+
+ if (typeof expectVal === "function") {
+ try {
+ expectVal()
+ } catch (e) {
+ threwError = true
+ errorTypeName = e.constructor?.name || "Error"
+ errorMessage = e.message || String(e)
+ }
+ }
+
+ // Handle parameter interpretation like Chai does
+ let actualErrorLike = errorLike
+ let actualErrMsgMatcher = errMsgMatcher
+
+ // If first param is string or regex but no second param,
+ // treat first param as message matcher, not error type
+ if (errorLike !== undefined && errMsgMatcher === undefined) {
+ if (typeof errorLike === "string" || errorLike instanceof RegExp) {
+ actualErrorLike = undefined
+ actualErrMsgMatcher = errorLike
+ }
+ }
+
+ // Get error type name for matching
+ let expectedTypeName = null
+ if (actualErrorLike && typeof actualErrorLike === "function") {
+ expectedTypeName = actualErrorLike.name
+ }
+
+ // Convert RegExp to serializable form or pass string message
+ let regexSource, regexFlags
+ let isRegexMatcher = false
+ if (actualErrMsgMatcher instanceof RegExp) {
+ regexSource = actualErrMsgMatcher.source
+ regexFlags = actualErrMsgMatcher.flags
+ isRegexMatcher = true
+ }
+
+ return inputs.chaiThrow(
+ expectVal,
+ threwError,
+ errorTypeName,
+ errorMessage,
+ expectedTypeName,
+ actualErrMsgMatcher,
+ regexSource,
+ regexFlags,
+ isRegexMatcher,
+ modifiers
+ )
+ }
+
+ // PRE-CHECK PATTERN: Check method existence BEFORE serialization
+ proxy.respondTo = (method) => {
+ // Check if method exists on value or its prototype/constructor
+ // When .itself modifier is present, check for static methods on the constructor itself
+ const hasItselfModifier = String(modifiers).includes("itself")
+ let hasMethod = false
+
+ if (hasItselfModifier) {
+ // .itself.respondTo() checks for static methods directly on the constructor/class
+ hasMethod = typeof expectVal?.[method] === "function"
+ } else {
+ // Regular .respondTo() checks instance methods (on value or prototype)
+ hasMethod =
+ typeof expectVal?.[method] === "function" ||
+ typeof expectVal?.prototype?.[method] === "function"
+ }
+
+ return inputs.chaiRespondTo(expectVal, method, modifiers, hasMethod)
+ }
+ proxy.respondsTo = (method) => {
+ // Check if method exists on value or its prototype/constructor
+ // When .itself modifier is present, check for static methods on the constructor itself
+ const hasItselfModifier = String(modifiers).includes("itself")
+ let hasMethod = false
+
+ if (hasItselfModifier) {
+ // .itself.respondTo() checks for static methods directly on the constructor/class
+ hasMethod = typeof expectVal?.[method] === "function"
+ } else {
+ // Regular .respondTo() checks instance methods (on value or prototype)
+ hasMethod =
+ typeof expectVal?.[method] === "function" ||
+ typeof expectVal?.prototype?.[method] === "function"
+ }
+
+ return inputs.chaiRespondTo(expectVal, method, modifiers, hasMethod)
+ }
+
+ proxy.satisfy = (matcher) => {
+ // PRE-CHECK PATTERN: Execute matcher function in sandbox, pass result
+ let satisfyResult = false
+ let matcherString = String(matcher)
+
+ if (typeof matcher === "function") {
+ try {
+ satisfyResult = Boolean(matcher(expectVal))
+ } catch (_e) {
+ satisfyResult = false
+ }
+ }
+
+ return inputs.chaiSatisfy(
+ expectVal,
+ satisfyResult,
+ matcherString,
+ modifiers
+ )
+ }
+ proxy.satisfies = (matcher) => {
+ // PRE-CHECK PATTERN: Execute matcher function in sandbox, pass result
+ let satisfyResult = false
+ let matcherString = String(matcher)
+
+ if (typeof matcher === "function") {
+ try {
+ satisfyResult = Boolean(matcher(expectVal))
+ } catch (_e) {
+ satisfyResult = false
+ }
+ }
+
+ return inputs.chaiSatisfy(
+ expectVal,
+ satisfyResult,
+ matcherString,
+ modifiers
+ )
+ }
+
+ // PRE-CHECK PATTERN: change/increase/decrease assertions
+ proxy.change = (obj, prop) => {
+ // Support both patterns:
+ // 1. change(getter) - getter function pattern
+ // 2. change(obj, prop) - object + property pattern
+ let initialValue
+ let finalValue
+ let changed = false
+ let delta = 0
+ let propName = prop
+
+ try {
+ // Pattern 1: Single argument that is a function (getter)
+ if (typeof obj === "function" && prop === undefined) {
+ initialValue = obj()
+ if (typeof expectVal === "function") {
+ expectVal()
+ }
+ finalValue = obj()
+ propName = "value" // Use generic name for getter pattern
+ }
+ // Pattern 2: Object + property name
+ else {
+ initialValue = obj[prop]
+ if (typeof expectVal === "function") {
+ expectVal()
+ }
+ finalValue = obj[prop]
+ }
+
+ changed = initialValue !== finalValue
+ if (
+ typeof initialValue === "number" &&
+ typeof finalValue === "number"
+ ) {
+ delta = finalValue - initialValue
+ }
+ } catch (_e) {
+ changed = false
+ }
+
+ // Call the assertion (adds result to testStack)
+ inputs.chaiChange(propName, modifiers, changed, delta)
+
+ // Return a proxy with .by() method for chaining
+ return {
+ by: (expectedDelta) => {
+ inputs.chaiChangeBy(
+ propName,
+ modifiers,
+ changed,
+ delta,
+ expectedDelta
+ )
+ },
+ }
+ }
+
+ proxy.increase = (obj, prop) => {
+ // Support both patterns:
+ // 1. increase(getter) - getter function pattern
+ // 2. increase(obj, prop) - object + property pattern
+ let initialValue
+ let finalValue
+ let increased = false
+ let delta = 0
+ let propName = prop
+
+ try {
+ // Pattern 1: Single argument that is a function (getter)
+ if (typeof obj === "function" && prop === undefined) {
+ initialValue = obj()
+ if (typeof expectVal === "function") {
+ expectVal()
+ }
+ finalValue = obj()
+ propName = "value" // Use generic name for getter pattern
+ }
+ // Pattern 2: Object + property name
+ else {
+ initialValue = obj[prop]
+ if (typeof expectVal === "function") {
+ expectVal()
+ }
+ finalValue = obj[prop]
+ }
+
+ if (
+ typeof initialValue === "number" &&
+ typeof finalValue === "number"
+ ) {
+ delta = finalValue - initialValue
+ increased = delta > 0
+ }
+ } catch (_e) {
+ increased = false
+ }
+
+ // Call the assertion (adds result to testStack)
+ inputs.chaiIncrease(propName, modifiers, increased, delta)
+
+ // Return a proxy with .by() method for chaining
+ return {
+ by: (expectedDelta) => {
+ inputs.chaiIncreaseBy(
+ propName,
+ modifiers,
+ increased,
+ delta,
+ expectedDelta
+ )
+ },
+ }
+ }
+
+ proxy.decrease = (obj, prop) => {
+ // Support both patterns:
+ // 1. decrease(getter) - getter function pattern
+ // 2. decrease(obj, prop) - object + property pattern
+ let initialValue
+ let finalValue
+ let decreased = false
+ let delta = 0
+ let propName = prop
+
+ try {
+ // Pattern 1: Single argument that is a function (getter)
+ if (typeof obj === "function" && prop === undefined) {
+ initialValue = obj()
+ if (typeof expectVal === "function") {
+ expectVal()
+ }
+ finalValue = obj()
+ propName = "value" // Use generic name for getter pattern
+ }
+ // Pattern 2: Object + property name
+ else {
+ initialValue = obj[prop]
+ if (typeof expectVal === "function") {
+ expectVal()
+ }
+ finalValue = obj[prop]
+ }
+
+ if (
+ typeof initialValue === "number" &&
+ typeof finalValue === "number"
+ ) {
+ delta = finalValue - initialValue
+ decreased = delta < 0
+ }
+ } catch (_e) {
+ decreased = false
+ }
+
+ // Call the assertion (adds result to testStack)
+ inputs.chaiDecrease(propName, modifiers, decreased, delta)
+
+ // Return a proxy with .by() method for chaining
+ return {
+ by: (expectedDelta) => {
+ inputs.chaiDecreaseBy(
+ propName,
+ modifiers,
+ decreased,
+ delta,
+ expectedDelta
+ )
+ },
+ }
+ }
+
+ // PRE-CHECK PATTERN: Check instanceof BEFORE serialization (for custom classes)
+ proxy.instanceof = function (constructor) {
+ // Debug logging
+ if (typeof inputs.chaiInstanceOf !== "function") {
+ throw new Error(
+ "inputs.chaiInstanceOf is not a function: " +
+ typeof inputs.chaiInstanceOf
+ )
+ }
+
+ // CRITICAL: Perform instanceof check HERE in the sandbox before serialization
+ // This is essential for custom user-defined classes to work correctly
+ const actualInstanceCheck = expectVal instanceof constructor
+
+ // Get the actual type using Object.prototype.toString for built-ins
+ const objectType = Object.prototype.toString.call(expectVal)
+ // Get constructor name before serialization (constructors don't cross boundary)
+ let constructorName = "Unknown"
+ try {
+ if (constructor && typeof constructor.name === "string") {
+ constructorName = constructor.name
+ } else {
+ constructorName = String(constructor)
+ }
+ } catch (_e) {
+ constructorName = String(constructor)
+ }
+
+ // PRE-FORMAT: Create display string for Set/Map before serialization
+ let displayValue = null
+ if (expectVal instanceof Set) {
+ const values = Array.from(expectVal).slice(0, 10)
+ displayValue =
+ values.length > 0 ? `new Set([${values.join(", ")}])` : "new Set()"
+ } else if (expectVal instanceof Map) {
+ const entries = Array.from(expectVal.entries()).slice(0, 3)
+ if (entries.length > 0) {
+ const formatted = entries.map(([k, v]) => `['${k}', ${v}]`)
+ displayValue = `new Map([${formatted.join(", ")}])`
+ } else {
+ displayValue = "new Map()"
+ }
+ }
+
+ return inputs.chaiInstanceOf(
+ expectVal,
+ constructorName,
+ modifiers,
+ objectType,
+ displayValue,
+ actualInstanceCheck // Pass the pre-checked result!
+ )
+ }
+ proxy.instanceOf = function (constructor) {
+ // CRITICAL: Perform instanceof check HERE in the sandbox before serialization
+ const actualInstanceCheck = expectVal instanceof constructor
+
+ // Get the actual type using Object.prototype.toString for built-ins
+ const objectType = Object.prototype.toString.call(expectVal)
+ // Get constructor name before serialization (constructors don't cross boundary)
+ let constructorName = "Unknown"
+ try {
+ if (constructor && typeof constructor.name === "string") {
+ constructorName = constructor.name
+ } else {
+ constructorName = String(constructor)
+ }
+ } catch (_e) {
+ constructorName = String(constructor)
+ }
+
+ // PRE-FORMAT: Create display string for Set/Map before serialization
+ let displayValue = null
+ if (expectVal instanceof Set) {
+ const values = Array.from(expectVal).slice(0, 10)
+ displayValue =
+ values.length > 0 ? `new Set([${values.join(", ")}])` : "new Set()"
+ } else if (expectVal instanceof Map) {
+ const entries = Array.from(expectVal.entries()).slice(0, 3)
+ if (entries.length > 0) {
+ const formatted = entries.map(([k, v]) => `['${k}', ${v}]`)
+ displayValue = `new Map([${formatted.join(", ")}])`
+ } else {
+ displayValue = "new Map()"
+ }
+ }
+
+ return inputs.chaiInstanceOf(
+ expectVal,
+ constructorName,
+ modifiers,
+ objectType,
+ displayValue,
+ actualInstanceCheck // Pass the pre-checked result!
+ )
+ }
+
+ // Assertion getters
+ Object.defineProperty(proxy, "ok", {
+ get: () => {
+ inputs.chaiOk(expectVal, modifiers)
+ return withModifiers(modifiers)
+ },
+ })
+ Object.defineProperty(proxy, "true", {
+ get: () => {
+ inputs.chaiTrue(expectVal, modifiers)
+ return withModifiers(modifiers)
+ },
+ })
+ Object.defineProperty(proxy, "false", {
+ get: () => {
+ inputs.chaiFalse(expectVal, modifiers)
+ return withModifiers(modifiers)
+ },
+ })
+ Object.defineProperty(proxy, "null", {
+ get: () => {
+ inputs.chaiNull(expectVal, modifiers)
+ return withModifiers(modifiers)
+ },
+ })
+ Object.defineProperty(proxy, "undefined", {
+ get: () => {
+ inputs.chaiUndefined(expectVal, modifiers)
+ return withModifiers(modifiers)
+ },
+ })
+ Object.defineProperty(proxy, "NaN", {
+ get: () => {
+ inputs.chaiNaN(expectVal, modifiers)
+ return withModifiers(modifiers)
+ },
+ })
+ Object.defineProperty(proxy, "exist", {
+ get: () => {
+ inputs.chaiExist(expectVal, modifiers)
+ return withModifiers(modifiers)
+ },
+ })
+ Object.defineProperty(proxy, "empty", {
+ get: () => {
+ // PRE-CHECK PATTERN: Pass type info for Set/Map before serialization
+ let typeName = null
+ let actualSize = null
+ if (expectVal instanceof Set) {
+ typeName = "Set"
+ actualSize = expectVal.size
+ } else if (expectVal instanceof Map) {
+ typeName = "Map"
+ actualSize = expectVal.size
+ }
+ inputs.chaiEmpty(expectVal, modifiers, typeName, actualSize)
+ return withModifiers(modifiers)
+ },
+ })
+ Object.defineProperty(proxy, "finite", {
+ get: () => {
+ inputs.chaiFinite(expectVal, modifiers)
+ return withModifiers(modifiers)
+ },
+ })
+ Object.defineProperty(proxy, "arguments", {
+ get: () => {
+ // PRE-CHECK PATTERN: Check if value is arguments object before serialization
+ const isArguments =
+ Object.prototype.toString.call(expectVal) === "[object Arguments]"
+ inputs.chaiArguments(expectVal, modifiers + " arguments", isArguments)
+ return withModifiers(modifiers)
+ },
+ })
+ Object.defineProperty(proxy, "Arguments", {
+ get: () => {
+ // PRE-CHECK PATTERN: Check if value is arguments object before serialization
+ const isArguments =
+ Object.prototype.toString.call(expectVal) === "[object Arguments]"
+ inputs.chaiArguments(expectVal, modifiers + " Arguments", isArguments)
+ return withModifiers(modifiers)
+ },
+ })
+ // PRE-CHECK PATTERN: Check object state in sandbox BEFORE serialization
+ Object.defineProperty(proxy, "extensible", {
+ get: () => {
+ // Check extensibility in sandbox context before value crosses boundary
+ const isExtensible = Object.isExtensible(expectVal)
+ // Pass both the value and the pre-checked boolean
+ inputs.chaiExtensible(expectVal, modifiers, isExtensible)
+ return withModifiers(modifiers)
+ },
+ })
+ Object.defineProperty(proxy, "sealed", {
+ get: () => {
+ // Check sealed state in sandbox context before value crosses boundary
+ const isSealed = Object.isSealed(expectVal)
+ // Pass both the value and the pre-checked boolean
+ inputs.chaiSealed(expectVal, modifiers, isSealed)
+ return withModifiers(modifiers)
+ },
+ })
+ Object.defineProperty(proxy, "frozen", {
+ get: () => {
+ // Check frozen state in sandbox context before value crosses boundary
+ const isFrozen = Object.isFrozen(expectVal)
+ // Pass both the value and the pre-checked boolean
+ inputs.chaiFrozen(expectVal, modifiers, isFrozen)
+ return withModifiers(modifiers)
+ },
+ })
+
+ // Language chains - return new proxy
+ Object.defineProperty(proxy, "to", {
+ get: () => withModifiers(" to"),
+ })
+ Object.defineProperty(proxy, "be", {
+ get: () => withModifiers(modifiers + " be"),
+ })
+ Object.defineProperty(proxy, "been", {
+ get: () => withModifiers(modifiers + " been"),
+ })
+ Object.defineProperty(proxy, "is", {
+ get: () => withModifiers(modifiers + " is"),
+ })
+ Object.defineProperty(proxy, "that", {
+ configurable: true, // Allow ownPropertyDescriptor to override this
+ get: () => withModifiers(modifiers + " that"),
+ })
+ Object.defineProperty(proxy, "which", {
+ get: () => withModifiers(modifiers + " which"),
+ })
+ Object.defineProperty(proxy, "and", {
+ get: () => withModifiers(modifiers + " and"),
+ })
+ Object.defineProperty(proxy, "has", {
+ get: () => withModifiers(modifiers + " has"),
+ })
+ Object.defineProperty(proxy, "have", {
+ get: () => {
+ const haveProxy = withModifiers(modifiers + " have")
+ // Add ownPropertyDescriptor method to have proxy
+ haveProxy.ownPropertyDescriptor = (prop, descriptor) => {
+ // PRE-CHECK PATTERN: Check property descriptor in sandbox before serialization
+ let hasDescriptor = false
+ let actualDescriptor = null
+ let matchesExpected = false
+
+ try {
+ actualDescriptor = Object.getOwnPropertyDescriptor(
+ expectVal,
+ prop
+ )
+ hasDescriptor = actualDescriptor !== undefined
+
+ // If descriptor argument provided, check if it matches
+ if (hasDescriptor && descriptor !== undefined) {
+ matchesExpected = true
+ // Compare each property of the descriptor
+ for (const key in descriptor) {
+ if (descriptor[key] !== actualDescriptor[key]) {
+ matchesExpected = false
+ break
+ }
+ }
+ }
+ } catch (_e) {
+ hasDescriptor = false
+ }
+
+ inputs.chaiOwnPropertyDescriptor(
+ expectVal,
+ prop,
+ descriptor,
+ modifiers + " have",
+ hasDescriptor,
+ matchesExpected
+ )
+
+ // Return a proxy that can chain with .that to access the descriptor
+ const descriptorProxy = withModifiers(modifiers + " have")
+ // Delete existing .that property if it exists, then redefine it
+ delete descriptorProxy.that
+ Object.defineProperty(descriptorProxy, "that", {
+ configurable: true,
+ enumerable: false,
+ get: () => {
+ // Return a new Chai proxy wrapping the descriptor itself
+ return globalThis.__createChaiProxy(
+ actualDescriptor,
+ inputs,
+ " to"
+ )
+ },
+ })
+ return descriptorProxy
+ }
+ return haveProxy
+ },
+ })
+ Object.defineProperty(proxy, "with", {
+ get: () => withModifiers(modifiers + " with"),
+ })
+ Object.defineProperty(proxy, "at", {
+ get: () => {
+ const atProxy = withModifiers(modifiers + " at")
+ atProxy.least = (n) => inputs.chaiAtLeast(expectVal, n, modifiers)
+ atProxy.most = (n) => inputs.chaiAtMost(expectVal, n, modifiers)
+ return atProxy
+ },
+ })
+ Object.defineProperty(proxy, "of", {
+ get: () => withModifiers(modifiers + " of"),
+ })
+ Object.defineProperty(proxy, "same", {
+ get: () => withModifiers(modifiers + " same"),
+ })
+ Object.defineProperty(proxy, "but", {
+ get: () => withModifiers(modifiers + " but"),
+ })
+ Object.defineProperty(proxy, "does", {
+ get: () => withModifiers(modifiers + " does"),
+ })
+ Object.defineProperty(proxy, "itself", {
+ get: () => withModifiers(modifiers + " itself"),
+ })
+
+ // Modifiers - return new proxy with updated modifiers
+ Object.defineProperty(proxy, "not", {
+ get: () => withModifiers(" to not"),
+ })
+ Object.defineProperty(proxy, "deep", {
+ get: () => {
+ const newModifiers = modifiers + " deep"
+ const deepProxy = withModifiers(newModifiers)
+ deepProxy.property = (prop, val) => {
+ if (val !== undefined) {
+ inputs.chaiDeepOwnProperty(expectVal, prop, val, newModifiers)
+ } else {
+ inputs.chaiProperty(expectVal, prop, val, newModifiers)
+ }
+ }
+ deepProxy.members = (members) =>
+ inputs.chaiDeepMembers(expectVal, members, newModifiers)
+ deepProxy.include = (item) => {
+ inputs.chaiDeepInclude(expectVal, item, newModifiers)
+ return withModifiers(newModifiers)
+ }
+ return deepProxy
+ },
+ })
+ Object.defineProperty(proxy, "own", {
+ get: () => {
+ const newModifiers = modifiers + " own"
+ const ownProxy = withModifiers(newModifiers)
+ ownProxy.property = (prop, val) => {
+ if (val !== undefined) {
+ inputs.chaiDeepOwnProperty(expectVal, prop, val, newModifiers)
+ } else {
+ // PRE-CHECK PATTERN: Check hasOwnProperty BEFORE serialization
+ // Only pass isOwnProperty when value is an object
+ let isOwnProperty = undefined
+ if (expectVal !== null && typeof expectVal === "object") {
+ isOwnProperty = Object.prototype.hasOwnProperty.call(
+ expectVal,
+ prop
+ )
+ }
+ inputs.chaiOwnProperty(
+ expectVal,
+ prop,
+ newModifiers,
+ isOwnProperty
+ )
+ }
+ }
+ return ownProxy
+ },
+ })
+ Object.defineProperty(proxy, "nested", {
+ get: () => {
+ const newModifiers = modifiers + " nested"
+ const nestedProxy = withModifiers(newModifiers)
+ nestedProxy.property = (prop, val) =>
+ inputs.chaiNestedProperty(expectVal, prop, val, newModifiers)
+ // Add .include() for .nested.include() pattern
+ nestedProxy.include = (obj) => {
+ inputs.chaiNestedInclude(expectVal, obj, newModifiers)
+ return withModifiers(newModifiers)
+ }
+ return nestedProxy
+ },
+ })
+ Object.defineProperty(proxy, "ordered", {
+ get: () => {
+ const newModifiers = modifiers + " ordered"
+ const orderedProxy = withModifiers(newModifiers)
+ orderedProxy.members = (members) =>
+ inputs.chaiOrderedMembers(expectVal, members, newModifiers)
+ return orderedProxy
+ },
+ })
+ Object.defineProperty(proxy, "all", {
+ get: () => {
+ const newModifiers = modifiers + " all"
+ const allProxy = withModifiers(newModifiers)
+ allProxy.keys = (...keys) =>
+ inputs.chaiAllKeys(expectVal, keys, newModifiers)
+ return allProxy
+ },
+ })
+ Object.defineProperty(proxy, "any", {
+ get: () => {
+ const newModifiers = modifiers + " any"
+ const anyProxy = withModifiers(newModifiers)
+ anyProxy.keys = (...keys) =>
+ inputs.chaiAnyKeys(expectVal, keys, newModifiers)
+ return anyProxy
+ },
+ })
+ // .include is defined above as a method with .members property
+
+ // Postman custom Chai assertions (jsonSchema, charset, cookie, jsonPath)
+ // These are Postman-specific extensions that work with pm.expect()
+
+ proxy.jsonSchema = (schema) => {
+ // Basic JSON Schema validation (supports common keywords)
+ const validateSchema = (data, schema) => {
+ // Type validation
+ if (schema.type) {
+ const actualType = Array.isArray(data)
+ ? "array"
+ : data === null
+ ? "null"
+ : typeof data
+ if (actualType !== schema.type) {
+ return `Expected type ${schema.type}, got ${actualType}`
+ }
+ }
+
+ // Required properties
+ if (schema.required && Array.isArray(schema.required)) {
+ for (const prop of schema.required) {
+ if (!(prop in data)) {
+ return `Required property '${prop}' is missing`
+ }
+ }
+ }
+
+ // Properties validation
+ if (schema.properties) {
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
+ if (key in data) {
+ const error = validateSchema(data[key], propSchema)
+ if (error) return `Property '${key}': ${error}`
+ }
+ }
+ }
+
+ // Enum validation
+ if (schema.enum) {
+ if (!schema.enum.includes(data)) {
+ return `Value must be one of: ${schema.enum.join(", ")}`
+ }
+ }
+
+ // Number validation
+ if (typeof data === "number") {
+ if (schema.minimum !== undefined && data < schema.minimum) {
+ return `Value ${data} is below minimum ${schema.minimum}`
+ }
+ if (schema.maximum !== undefined && data > schema.maximum) {
+ return `Value ${data} exceeds maximum ${schema.maximum}`
+ }
+ }
+
+ // String validation
+ if (typeof data === "string") {
+ if (
+ schema.minLength !== undefined &&
+ data.length < schema.minLength
+ ) {
+ return `String length ${data.length} is below minimum ${schema.minLength}`
+ }
+ if (
+ schema.maxLength !== undefined &&
+ data.length > schema.maxLength
+ ) {
+ return `String length ${data.length} exceeds maximum ${schema.maxLength}`
+ }
+ if (schema.pattern) {
+ const regex = new RegExp(schema.pattern)
+ if (!regex.test(data)) {
+ return `String does not match pattern ${schema.pattern}`
+ }
+ }
+ }
+
+ // Array validation
+ if (Array.isArray(data)) {
+ if (
+ schema.minItems !== undefined &&
+ data.length < schema.minItems
+ ) {
+ return `Array length ${data.length} is below minimum ${schema.minItems}`
+ }
+ if (
+ schema.maxItems !== undefined &&
+ data.length > schema.maxItems
+ ) {
+ return `Array length ${data.length} exceeds maximum ${schema.maxItems}`
+ }
+ if (schema.items) {
+ for (let i = 0; i < data.length; i++) {
+ const error = validateSchema(data[i], schema.items)
+ if (error) return `Item [${i}]: ${error}`
+ }
+ }
+ }
+
+ return null // Validation passed
+ }
+
+ const error = validateSchema(expectVal, schema)
+ if (error) {
+ throw new Error(`JSON Schema validation failed: ${error}`)
+ }
+ return withModifiers(modifiers)
+ }
+
+ proxy.charset = (expectedCharset) => {
+ // expectVal should be a string (typically Content-Type header)
+ if (typeof expectVal !== "string") {
+ throw new Error(
+ "charset() expects a string value (typically Content-Type header)"
+ )
+ }
+
+ const lowerExpected = expectedCharset.toLowerCase()
+ const lowerActual = expectVal.toLowerCase()
+
+ if (!lowerActual.includes(lowerExpected)) {
+ throw new Error(
+ `Expected charset "${expectedCharset}" not found in "${expectVal}"`
+ )
+ }
+ return withModifiers(modifiers)
+ }
+
+ proxy.cookie = (cookieName, cookieValue) => {
+ // This works when expectVal is pm.response or similar
+ // For the Chai extension, we need to check if cookies exist
+ // This is typically used as: pm.expect(pm.response).to.have.cookie('name')
+ // But since we're on a Chai proxy, we assume expectVal is the response object
+
+ if (!expectVal || typeof expectVal !== "object" || !expectVal.cookies) {
+ throw new Error(
+ "cookie() assertion requires a response object with cookies"
+ )
+ }
+
+ const hasCookie = expectVal.cookies.has(cookieName)
+ if (!hasCookie) {
+ throw new Error(`Cookie "${cookieName}" not found in response`)
+ }
+
+ if (cookieValue !== undefined) {
+ const actualValue = expectVal.cookies.get(cookieName)
+ if (actualValue !== cookieValue) {
+ throw new Error(
+ `Cookie "${cookieName}" has value "${actualValue}", expected "${cookieValue}"`
+ )
+ }
+ }
+ return withModifiers(modifiers)
+ }
+
+ // Legacy pw.expect API compatibility methods
+ // These provide backward compatibility for tests using the old pw.expect API
+ // Legacy API compatibility - delegate to existing legacy functions
+ // These handle errors properly by returning test results instead of throwing
+ // Check modifiers to determine if negation is needed
+ const isNegated = modifiers.includes("not")
+
+ proxy.toBe = (expectedVal) =>
+ isNegated
+ ? inputs.expectNotToBe(expectVal, expectedVal)
+ : inputs.expectToBe(expectVal, expectedVal)
+
+ proxy.toBeLevel2xx = () =>
+ isNegated
+ ? inputs.expectNotToBeLevel2xx(expectVal)
+ : inputs.expectToBeLevel2xx(expectVal)
+
+ proxy.toBeLevel3xx = () =>
+ isNegated
+ ? inputs.expectNotToBeLevel3xx(expectVal)
+ : inputs.expectToBeLevel3xx(expectVal)
+
+ proxy.toBeLevel4xx = () =>
+ isNegated
+ ? inputs.expectNotToBeLevel4xx(expectVal)
+ : inputs.expectToBeLevel4xx(expectVal)
+
+ proxy.toBeLevel5xx = () =>
+ isNegated
+ ? inputs.expectNotToBeLevel5xx(expectVal)
+ : inputs.expectToBeLevel5xx(expectVal)
+
+ proxy.toBeType = (expectedType) => {
+ const isDateInstance = expectVal instanceof Date
+ return isNegated
+ ? inputs.expectNotToBeType(expectVal, expectedType, isDateInstance)
+ : inputs.expectToBeType(expectVal, expectedType, isDateInstance)
+ }
+
+ proxy.toHaveLength = (expectedLength) =>
+ isNegated
+ ? inputs.expectNotToHaveLength(expectVal, expectedLength)
+ : inputs.expectToHaveLength(expectVal, expectedLength)
+
+ proxy.toInclude = (needle) =>
+ isNegated
+ ? inputs.expectNotToInclude(expectVal, needle)
+ : inputs.expectToInclude(expectVal, needle)
+
+ return proxy
+ }
+ }
+
const toJSON = (input) => {
if (input == null) return null
@@ -97,6 +1983,131 @@
})
})
+ // Add utility methods to hoppResponse
+ Object.defineProperty(hoppResponse, "text", {
+ value: () => toText(body),
+ writable: false,
+ enumerable: true,
+ configurable: false,
+ })
+
+ Object.defineProperty(hoppResponse, "json", {
+ value: () => toJSON(body),
+ writable: false,
+ enumerable: true,
+ configurable: false,
+ })
+
+ Object.defineProperty(hoppResponse, "reason", {
+ value: () => {
+ // Return HTTP reason phrase without status code
+ // statusText might be "200 OK" or just "OK"
+ const text = statusText || ""
+ // If it starts with a number (status code), extract just the reason
+ const match = text.match(/^\d+\s+(.+)$/)
+ return match ? match[1] : text
+ },
+ writable: false,
+ enumerable: true,
+ configurable: false,
+ })
+
+ Object.defineProperty(hoppResponse, "dataURI", {
+ value: () => {
+ // Convert response to data URI format
+ try {
+ const bytes = toBytes(body)
+ const contentTypeHeader = headers.find(
+ (h) => h.key.toLowerCase() === "content-type"
+ )
+ const mimeType = contentTypeHeader
+ ? contentTypeHeader.value.split(";")[0].trim()
+ : "application/octet-stream"
+
+ // Convert bytes to base64
+ let base64 = ""
+ if (bytes && typeof bytes === "object") {
+ // Handle both array-like and object representations
+ const byteArray = Array.isArray(bytes)
+ ? bytes
+ : Object.keys(bytes)
+ .filter((k) => !isNaN(k))
+ .map((k) => bytes[k])
+
+ // Convert to binary string
+ let binary = ""
+ for (let i = 0; i < byteArray.length; i++) {
+ binary += String.fromCharCode(byteArray[i])
+ }
+
+ // Convert to base64 using btoa if available
+ if (typeof btoa !== "undefined") {
+ base64 = btoa(binary)
+ } else {
+ // Fallback: manual base64 encoding
+ const chars =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
+ for (let i = 0; i < binary.length; i += 3) {
+ const b1 = binary.charCodeAt(i) & 0xff
+ const b2 = i + 1 < binary.length ? binary.charCodeAt(i + 1) : 0
+ const b3 = i + 2 < binary.length ? binary.charCodeAt(i + 2) : 0
+
+ const enc1 = b1 >> 2
+ const enc2 = ((b1 & 3) << 4) | (b2 >> 4)
+ const enc3 = ((b2 & 15) << 2) | (b3 >> 6)
+ const enc4 = b3 & 63
+
+ base64 += chars[enc1] + chars[enc2]
+ base64 += i + 1 < binary.length ? chars[enc3] : "="
+ base64 += i + 2 < binary.length ? chars[enc4] : "="
+ }
+ }
+ }
+
+ return `data:${mimeType};base64,${base64}`
+ } catch (_e) {
+ // Fallback: return text as data URI
+ try {
+ const text = toText(body)
+ return `data:text/plain;charset=utf-8,${encodeURIComponent(text)}`
+ } catch {
+ return "data:,"
+ }
+ }
+ },
+ writable: false,
+ enumerable: true,
+ configurable: false,
+ })
+
+ Object.defineProperty(hoppResponse, "jsonp", {
+ value: (callbackName = "callback") => {
+ // Parse JSONP response by extracting JSON from callback wrapper
+ try {
+ const text = toText(body)
+
+ // Match pattern: callbackName({...})
+ const regex = new RegExp(
+ `^\\s*${callbackName}\\s*\\((.*)\\)\\s*;?\\s*$`,
+ "s"
+ )
+ const match = text.match(regex)
+
+ if (match && match[1]) {
+ return JSON.parse(match[1])
+ }
+
+ // If no callback wrapper, try parsing as JSON directly
+ return JSON.parse(text)
+ } catch (e) {
+ throw new Error(`Failed to parse JSONP response: ${e.message}`)
+ }
+ },
+ writable: false,
+ enumerable: true,
+ configurable: false,
+ })
+
// Freeze the entire response object
Object.freeze(hoppResponse)
@@ -109,6 +2120,7 @@
resolve: (key) => inputs.envResolve(key),
},
expect: (expectVal) => {
+ // Legacy expectation system only (no Chai for backward compatibility)
const isDateInstance = expectVal instanceof Date
const expectation = {
@@ -183,42 +2195,113 @@
// 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
+ }
+
+ // Helper function for PM namespace setters with marker handling
+ const pmSetWithMarkers = (key, value, source) => {
+ if (typeof value === "undefined") {
+ return inputs.pmEnvSetAny(key, UNDEFINED_MARKER, { source })
+ } else if (value === null) {
+ return inputs.pmEnvSetAny(key, NULL_MARKER, { source })
+ } else {
+ return inputs.pmEnvSetAny(key, value, { source })
+ }
+ }
+
+ // Helper function for PM namespace getters with tracking
+ const pmGetWithTracking = (getRawFn, trackingSet, key) => {
+ const value = getRawFn(key)
+ // Return undefined for missing keys, preserve null for explicit null values
+ if (value === null && (!trackingSet || !trackingSet.has(key))) {
+ return undefined
+ }
+ return value
+ }
+
globalThis.hopp = {
env: {
- get: (key) =>
- inputs.envGetResolve(key, { fallbackToNull: true, source: "all" }),
- getRaw: (key) =>
- inputs.envGet(key, { fallbackToNull: true, source: "all" }),
+ get: (key) => {
+ const value = inputs.envGetResolve(key, {
+ fallbackToNull: true,
+ source: "all",
+ })
+ return convertMarkerToValue(value)
+ },
+ getRaw: (key) => {
+ const value = inputs.envGet(key, {
+ fallbackToNull: true,
+ source: "all",
+ })
+ return convertMarkerToValue(value)
+ },
set: (key, value) => inputs.envSet(key, value),
delete: (key) => inputs.envUnset(key),
reset: (key) => inputs.envReset(key),
- getInitialRaw: (key) => inputs.envGetInitialRaw(key),
+ getInitialRaw: (key) => {
+ const value = inputs.envGetInitialRaw(key)
+ return convertMarkerToValue(value)
+ },
setInitial: (key, value) => inputs.envSetInitial(key, value),
active: {
- get: (key) =>
- inputs.envGetResolve(key, { fallbackToNull: true, source: "active" }),
- getRaw: (key) =>
- inputs.envGet(key, { fallbackToNull: true, source: "active" }),
+ get: (key) => {
+ const value = inputs.envGetResolve(key, {
+ fallbackToNull: true,
+ source: "active",
+ })
+ return convertMarkerToValue(value)
+ },
+ getRaw: (key) => {
+ const value = inputs.envGet(key, {
+ fallbackToNull: true,
+ source: "active",
+ })
+ return convertMarkerToValue(value)
+ },
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) =>
- inputs.envGetInitialRaw(key, { source: "active" }),
+ getInitialRaw: (key) => {
+ const value = inputs.envGetInitialRaw(key, { source: "active" })
+ return convertMarkerToValue(value)
+ },
setInitial: (key, value) =>
inputs.envSetInitial(key, value, { source: "active" }),
},
global: {
- get: (key) =>
- inputs.envGetResolve(key, { fallbackToNull: true, source: "global" }),
- getRaw: (key) =>
- inputs.envGet(key, { fallbackToNull: true, source: "global" }),
+ get: (key) => {
+ const value = inputs.envGetResolve(key, {
+ fallbackToNull: true,
+ source: "global",
+ })
+ return convertMarkerToValue(value)
+ },
+ getRaw: (key) => {
+ const value = inputs.envGet(key, {
+ fallbackToNull: true,
+ source: "global",
+ })
+ return convertMarkerToValue(value)
+ },
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) =>
- inputs.envGetInitialRaw(key, { source: "global" }),
+ getInitialRaw: (key) => {
+ const value = inputs.envGetInitialRaw(key, { source: "global" })
+ return convertMarkerToValue(value)
+ },
setInitial: (key, value) =>
inputs.envSetInitial(key, value, { source: "global" }),
},
@@ -232,39 +2315,69 @@
delete: (domain, name) => inputs.cookieDelete(domain, name),
clear: (domain) => inputs.cookieClear(domain),
},
- expect: (expectVal) => {
- const isDateInstance = expectVal instanceof Date
+ expect: Object.assign(
+ (expectVal) => {
+ // Use Chai if available
+ if (inputs.chaiEqual) {
+ return globalThis.__createChaiProxy(expectVal, inputs)
+ }
- const expectation = {
- toBe: (expectedVal) => inputs.expectToBe(expectVal, expectedVal),
- toBeLevel2xx: () => inputs.expectToBeLevel2xx(expectVal),
- toBeLevel3xx: () => inputs.expectToBeLevel3xx(expectVal),
- toBeLevel4xx: () => inputs.expectToBeLevel4xx(expectVal),
- toBeLevel5xx: () => inputs.expectToBeLevel5xx(expectVal),
- toBeType: (expectedType) =>
- inputs.expectToBeType(expectVal, expectedType, isDateInstance),
- toHaveLength: (expectedLength) =>
- inputs.expectToHaveLength(expectVal, expectedLength),
- toInclude: (needle) => inputs.expectToInclude(expectVal, needle),
- }
+ // Fallback to legacy expectation system
+ const isDateInstance = expectVal instanceof Date
- Object.defineProperty(expectation, "not", {
- get: () => ({
- toBe: (expectedVal) => inputs.expectNotToBe(expectVal, expectedVal),
- toBeLevel2xx: () => inputs.expectNotToBeLevel2xx(expectVal),
- toBeLevel3xx: () => inputs.expectNotToBeLevel3xx(expectVal),
- toBeLevel4xx: () => inputs.expectNotToBeLevel4xx(expectVal),
- toBeLevel5xx: () => inputs.expectNotToBeLevel5xx(expectVal),
+ const expectation = {
+ toBe: (expectedVal) => inputs.expectToBe(expectVal, expectedVal),
+ toBeLevel2xx: () => inputs.expectToBeLevel2xx(expectVal),
+ toBeLevel3xx: () => inputs.expectToBeLevel3xx(expectVal),
+ toBeLevel4xx: () => inputs.expectToBeLevel4xx(expectVal),
+ toBeLevel5xx: () => inputs.expectToBeLevel5xx(expectVal),
toBeType: (expectedType) =>
- inputs.expectNotToBeType(expectVal, expectedType, isDateInstance),
+ inputs.expectToBeType(expectVal, expectedType, isDateInstance),
toHaveLength: (expectedLength) =>
- inputs.expectNotToHaveLength(expectVal, expectedLength),
- toInclude: (needle) => inputs.expectNotToInclude(expectVal, needle),
- }),
- })
+ inputs.expectToHaveLength(expectVal, expectedLength),
+ toInclude: (needle) => inputs.expectToInclude(expectVal, needle),
+ }
- return expectation
- },
+ Object.defineProperty(expectation, "not", {
+ get: () => ({
+ toBe: (expectedVal) => inputs.expectNotToBe(expectVal, expectedVal),
+ toBeLevel2xx: () => inputs.expectNotToBeLevel2xx(expectVal),
+ toBeLevel3xx: () => inputs.expectNotToBeLevel3xx(expectVal),
+ toBeLevel4xx: () => inputs.expectNotToBeLevel4xx(expectVal),
+ toBeLevel5xx: () => inputs.expectNotToBeLevel5xx(expectVal),
+ toBeType: (expectedType) =>
+ inputs.expectNotToBeType(expectVal, expectedType, isDateInstance),
+ toHaveLength: (expectedLength) =>
+ inputs.expectNotToHaveLength(expectVal, expectedLength),
+ toInclude: (needle) => inputs.expectNotToInclude(expectVal, needle),
+ }),
+ })
+
+ return expectation
+ },
+ {
+ // expect.fail() - Chai compatibility
+ // Supports multiple signatures:
+ // expect.fail()
+ // expect.fail(message)
+ // expect.fail(actual, expected)
+ // expect.fail(actual, expected, message)
+ // expect.fail(actual, expected, message, operator)
+ fail: (actual, expected, message, operator) => {
+ if (inputs.chaiFail) {
+ inputs.chaiFail(actual, expected, message, operator)
+ } else {
+ // Fallback: throw an error with the message
+ const errorMessage =
+ message ||
+ (actual !== undefined && expected !== undefined
+ ? `expected ${actual} to ${operator || "equal"} ${expected}`
+ : "expect.fail()")
+ throw new Error(errorMessage)
+ }
+ },
+ }
+ ),
test: (descriptor, testFn) => {
inputs.preTest(descriptor)
testFn()
@@ -273,37 +2386,222 @@
response: hoppResponse,
}
+ // Initialize tracking Sets for PM namespace from incoming envs
+ // This allows toObject() to work even when variables were set in previous scripts
+ if (!globalThis.__pmEnvKeys) {
+ globalThis.__pmEnvKeys = new Set()
+ }
+ if (!globalThis.__pmGlobalKeys) {
+ globalThis.__pmGlobalKeys = new Set()
+ }
+
+ // Populate tracking Sets from inputs.envs if available
+ // This allows toObject() to work even when variables were set in previous scripts
+ if (inputs && inputs.envs) {
+ // Track all selected/active environment variables
+ if (inputs.envs.selected && Array.isArray(inputs.envs.selected)) {
+ inputs.envs.selected.forEach((envVar) => {
+ if (envVar && envVar.key && envVar.currentValue !== undefined) {
+ globalThis.__pmEnvKeys.add(envVar.key)
+ }
+ })
+ }
+ // Track all global variables
+ if (inputs.envs.global && Array.isArray(inputs.envs.global)) {
+ inputs.envs.global.forEach((envVar) => {
+ if (envVar && envVar.key && envVar.currentValue !== undefined) {
+ globalThis.__pmGlobalKeys.add(envVar.key)
+ }
+ })
+ }
+ }
+
// PM Namespace - Postman Compatibility Layer
globalThis.pm = {
environment: {
- get: (key) => globalThis.hopp.env.active.get(key),
- set: (key, value) => globalThis.hopp.env.active.set(key, value),
- unset: (key) => globalThis.hopp.env.active.delete(key),
+ name: "active",
+ get: (key) => {
+ return pmGetWithTracking(
+ globalThis.hopp.env.active.getRaw,
+ globalThis.__pmEnvKeys,
+ key
+ )
+ },
+ set: (key, value) => {
+ // Track the key for clear() and toObject()
+ if (!globalThis.__pmEnvKeys) {
+ globalThis.__pmEnvKeys = new Set()
+ }
+ globalThis.__pmEnvKeys.add(key)
+ return pmSetWithMarkers(key, value, "active")
+ },
+ unset: (key) => {
+ // Remove from tracking when unset
+ if (globalThis.__pmEnvKeys) {
+ globalThis.__pmEnvKeys.delete(key)
+ }
+ return globalThis.hopp.env.active.delete(key)
+ },
has: (key) => globalThis.hopp.env.active.get(key) !== null,
clear: () => {
- throw new Error("pm.environment.clear() not yet implemented")
+ // Clear ALL environment variables (not just tracked ones)
+ // Get all keys from the inputs array
+ if (typeof inputs !== "undefined" && inputs.getAllSelectedEnvs) {
+ const selectedEnvs = inputs.getAllSelectedEnvs()
+ if (Array.isArray(selectedEnvs)) {
+ selectedEnvs.forEach((envVar) => {
+ if (envVar && envVar.key) {
+ globalThis.hopp.env.active.delete(envVar.key)
+ }
+ })
+ }
+ }
+
+ // Also clear any tracked keys
+ if (globalThis.__pmEnvKeys) {
+ globalThis.__pmEnvKeys.forEach((key) => {
+ globalThis.hopp.env.active.delete(key)
+ })
+ globalThis.__pmEnvKeys.clear()
+ }
},
toObject: () => {
- throw new Error("pm.environment.toObject() not yet implemented")
+ // Return all environment variables as an object
+ // Read from getAllSelectedEnvs but verify each key still exists in the Map
+ // (to respect deletions made via unset() or clear())
+ const result = {}
+
+ if (typeof inputs !== "undefined" && inputs.getAllSelectedEnvs) {
+ const selectedEnvs = inputs.getAllSelectedEnvs()
+ if (Array.isArray(selectedEnvs)) {
+ selectedEnvs.forEach((envVar) => {
+ if (envVar && envVar.key) {
+ // Check if this key still exists in the active environment
+ // (it might have been deleted via unset() or clear())
+ const currentValue = globalThis.hopp.env.active.get(envVar.key)
+ if (currentValue !== null) {
+ result[envVar.key] = currentValue
+ }
+ }
+ })
+ }
+ }
+
+ return result
+ },
+ replaceIn: (template) => {
+ // Replace {{varName}} with actual values
+ if (typeof template !== "string") return template
+ return template.replace(/\{\{([^}]+)\}\}/g, (match, varName) => {
+ const value = globalThis.hopp.env.active.get(varName.trim())
+ return value !== null ? value : match
+ })
},
},
globals: {
- get: (key) => globalThis.hopp.env.global.get(key),
- set: (key, value) => globalThis.hopp.env.global.set(key, value),
- unset: (key) => globalThis.hopp.env.global.delete(key),
+ get: (key) => {
+ return pmGetWithTracking(
+ globalThis.hopp.env.global.getRaw,
+ globalThis.__pmGlobalKeys,
+ key
+ )
+ },
+ set: (key, value) => {
+ // Track the key for clear() and toObject()
+ if (!globalThis.__pmGlobalKeys) {
+ globalThis.__pmGlobalKeys = new Set()
+ }
+ globalThis.__pmGlobalKeys.add(key)
+ return pmSetWithMarkers(key, value, "global")
+ },
+ unset: (key) => {
+ // Remove from tracking when unset
+ if (globalThis.__pmGlobalKeys) {
+ globalThis.__pmGlobalKeys.delete(key)
+ }
+ return globalThis.hopp.env.global.delete(key)
+ },
has: (key) => globalThis.hopp.env.global.get(key) !== null,
clear: () => {
- throw new Error("pm.globals.clear() not yet implemented")
+ // Clear ALL global variables (not just tracked ones)
+ // Get all keys from the inputs array
+ if (typeof inputs !== "undefined" && inputs.getAllGlobalEnvs) {
+ const globalEnvs = inputs.getAllGlobalEnvs()
+ if (Array.isArray(globalEnvs)) {
+ globalEnvs.forEach((envVar) => {
+ if (envVar && envVar.key) {
+ globalThis.hopp.env.global.delete(envVar.key)
+ }
+ })
+ }
+ }
+
+ // Also clear any tracked keys
+ if (globalThis.__pmGlobalKeys) {
+ globalThis.__pmGlobalKeys.forEach((key) => {
+ globalThis.hopp.env.global.delete(key)
+ })
+ globalThis.__pmGlobalKeys.clear()
+ }
},
toObject: () => {
- throw new Error("pm.globals.toObject() not yet implemented")
+ // Return all global variables as an object
+ // Read from getAllGlobalEnvs but verify each key still exists in the Map
+ // (to respect deletions made via unset() or clear())
+ const result = {}
+
+ if (typeof inputs !== "undefined" && inputs.getAllGlobalEnvs) {
+ const globalEnvs = inputs.getAllGlobalEnvs()
+ if (Array.isArray(globalEnvs)) {
+ globalEnvs.forEach((envVar) => {
+ if (envVar && envVar.key) {
+ // Check if this key still exists in the global environment
+ const currentValue = globalThis.hopp.env.global.get(envVar.key)
+ if (currentValue !== null) {
+ result[envVar.key] = currentValue
+ }
+ }
+ })
+ }
+ }
+
+ return result
+ },
+ replaceIn: (template) => {
+ // Replace {{varName}} with actual values
+ if (typeof template !== "string") return template
+ return template.replace(/\{\{([^}]+)\}\}/g, (match, varName) => {
+ const value = globalThis.hopp.env.global.get(varName.trim())
+ return value !== null ? value : match
+ })
},
},
variables: {
- get: (key) => globalThis.hopp.env.get(key),
- set: (key, value) => globalThis.hopp.env.active.set(key, value),
+ get: (key) => {
+ // pm.variables searches both active and global scopes
+ // Try active first
+ let value = globalThis.hopp.env.active.getRaw(key)
+ let isTracked =
+ globalThis.__pmEnvKeys && globalThis.__pmEnvKeys.has(key)
+
+ // If not found in active, try global
+ if (value === null && !isTracked) {
+ value = globalThis.hopp.env.global.getRaw(key)
+ isTracked =
+ globalThis.__pmGlobalKeys && globalThis.__pmGlobalKeys.has(key)
+ }
+
+ // Return undefined for missing keys, preserve null for explicit null values
+ if (value === null && !isTracked) {
+ return undefined // Key doesn't exist in either scope
+ }
+ return value // Returns null (from NULL_MARKER) or actual value
+ },
+ set: (key, value) => {
+ return pmSetWithMarkers(key, value, "active")
+ },
has: (key) => globalThis.hopp.env.get(key) !== null,
replaceIn: (template) => {
if (typeof template !== "string") return template
@@ -312,14 +2610,287 @@
return value !== null ? value : match
})
},
+ toObject: () => {
+ // Variables scope includes both environment and global with precedence
+ // Read from arrays but verify keys still exist in Maps (respect deletions)
+ const result = {}
+
+ // Add global variables first
+ if (typeof inputs !== "undefined" && inputs.getAllGlobalEnvs) {
+ const globalEnvs = inputs.getAllGlobalEnvs()
+ if (Array.isArray(globalEnvs)) {
+ globalEnvs.forEach((envVar) => {
+ if (envVar && envVar.key) {
+ const currentValue = globalThis.hopp.env.global.get(envVar.key)
+ if (currentValue !== null) {
+ result[envVar.key] = currentValue
+ }
+ }
+ })
+ }
+ }
+
+ // Then override with environment variables (environment takes precedence)
+ if (typeof inputs !== "undefined" && inputs.getAllSelectedEnvs) {
+ const selectedEnvs = inputs.getAllSelectedEnvs()
+ if (Array.isArray(selectedEnvs)) {
+ selectedEnvs.forEach((envVar) => {
+ if (envVar && envVar.key) {
+ const currentValue = globalThis.hopp.env.active.get(envVar.key)
+ if (currentValue !== null) {
+ result[envVar.key] = currentValue
+ }
+ }
+ })
+ }
+ }
+
+ return result
+ },
},
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 withModifiers(modifiers)
+ },
+
+ get proxy() {
+ return withModifiers(modifiers)
+ },
+
+ // URL - Read-only with Postman-compatible structure (no setters in post-request)
get url() {
- const urlString = globalThis.hopp.request.url
- return {
- toString: () => urlString,
+ const urlObj = {
+ // toString reads current URL dynamically
+ toString: () => globalThis.hopp.request.url,
+
+ _parseUrl: () => {
+ const urlString = globalThis.hopp.request.url
+ try {
+ const parsed = new URL(urlString)
+ return {
+ protocol: parsed.protocol.slice(0, -1),
+ host: parsed.hostname.split("."),
+ port:
+ parsed.port || (parsed.protocol === "https:" ? "443" : "80"),
+ path: parsed.pathname.split("/").filter(Boolean),
+ queryParams: Array.from(parsed.searchParams.entries()).map(
+ ([key, value]) => ({ key, value })
+ ),
+ }
+ } catch {
+ return {
+ protocol: "https",
+ host: [],
+ port: "443",
+ path: [],
+ queryParams: [],
+ }
+ }
+ },
}
+
+ // Read-only properties (no setters in post-request script)
+ Object.defineProperty(urlObj, "protocol", {
+ get: () => urlObj._parseUrl().protocol,
+ enumerable: true,
+ })
+
+ Object.defineProperty(urlObj, "host", {
+ get: () => urlObj._parseUrl().host,
+ enumerable: true,
+ })
+
+ Object.defineProperty(urlObj, "path", {
+ get: () => urlObj._parseUrl().path,
+ enumerable: true,
+ })
+
+ // Postman-compatible URL helper methods (read-only in post-request)
+ urlObj.getHost = () => urlObj._parseUrl().host.join(".")
+
+ urlObj.getPath = (_unresolved = false) => {
+ const pathArray = urlObj._parseUrl().path
+ return pathArray.length > 0 ? "/" + pathArray.join("/") : "/"
+ }
+
+ urlObj.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
+ }
+
+ urlObj.getQueryString = (_options = {}) => {
+ const params = urlObj._parseUrl().queryParams
+ if (params.length === 0) return ""
+ return params
+ .map(
+ (p) =>
+ `${encodeURIComponent(p.key)}=${encodeURIComponent(p.value)}`
+ )
+ .join("&")
+ }
+
+ urlObj.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
+ }
+
+ // hostname property (string alias for host array)
+ Object.defineProperty(urlObj, "hostname", {
+ get: () => urlObj._parseUrl().host.join("."),
+ 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 ""
+ }
+ },
+ enumerable: true,
+ })
+
+ Object.defineProperty(urlObj, "query", {
+ get: () => {
+ return {
+ all: () => {
+ // Parse current URL dynamically
+ const parsed = urlObj._parseUrl()
+ const result = {}
+
+ // Handle duplicate keys by converting to arrays
+ parsed.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
+ },
+
+ // PropertyList methods (read-only in post-request)
+ 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)
+ },
+
+ toObject: () => {
+ const parsed = urlObj._parseUrl()
+ const result = {}
+
+ // Handle duplicate keys by converting to arrays
+ parsed.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
+ },
+
+ 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 (read-only)
+ 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
+ },
+ }
+ },
+ enumerable: true,
+ })
+
+ return urlObj
},
get method() {
@@ -348,6 +2919,69 @@
})
return result
},
+
+ // PropertyList methods (read-only in post-request)
+ toObject: () => {
+ const result = {}
+ globalThis.hopp.request.headers.forEach((header) => {
+ result[header.key] = header.value
+ })
+ return result
+ },
+
+ 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 (read-only)
+ 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
+ },
}
},
@@ -365,21 +2999,176 @@
return globalThis.hopp.response.statusCode
},
get status() {
- return globalThis.hopp.response.statusText
+ // Get status text from response, fallback to standard reason phrase if empty
+ const statusText = globalThis.hopp.response.statusText
+ if (statusText && statusText.trim() !== "") {
+ return statusText
+ }
+
+ // Map status code to standard HTTP reason phrase (for HTTP/2 or empty responses)
+ const statusCode = globalThis.hopp.response.statusCode
+ const standardReasonPhrases = {
+ 100: "Continue",
+ 101: "Switching Protocols",
+ 102: "Processing",
+ 103: "Early Hints",
+ 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",
+ 300: "Multiple Choices",
+ 301: "Moved Permanently",
+ 302: "Found",
+ 303: "See Other",
+ 304: "Not Modified",
+ 305: "Use Proxy",
+ 307: "Temporary Redirect",
+ 308: "Permanent Redirect",
+ 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",
+ 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",
+ }
+
+ return standardReasonPhrases[statusCode] || ""
},
get responseTime() {
return globalThis.hopp.response.responseTime
},
+ get responseSize() {
+ // Calculate response size from body bytes
+ try {
+ // Check if response and body exist
+ if (
+ !globalThis.hopp ||
+ !globalThis.hopp.response ||
+ !globalThis.hopp.response.body
+ ) {
+ return withModifiers(modifiers)
+ }
+
+ // Try bytes() first
+ if (typeof globalThis.hopp.response.body.bytes === "function") {
+ const bytes = globalThis.hopp.response.body.bytes()
+ if (bytes && typeof bytes.length === "number") {
+ return bytes.length
+ }
+ }
+
+ // Fallback: calculate from text representation
+ if (typeof globalThis.hopp.response.body.asText === "function") {
+ const text = globalThis.hopp.response.body.asText()
+ if (typeof text === "string") {
+ // Use TextEncoder if available
+ if (typeof TextEncoder !== "undefined") {
+ try {
+ const encoder = new TextEncoder()
+ const encoded = encoder.encode(text)
+
+ // Try standard Uint8Array properties first
+ if (
+ encoded &&
+ typeof encoded.length === "number" &&
+ encoded.length > 0
+ ) {
+ return encoded.length
+ } else if (
+ encoded &&
+ typeof encoded.byteLength === "number" &&
+ encoded.byteLength > 0
+ ) {
+ return encoded.byteLength
+ } else if (encoded && Object.keys(encoded).length > 0) {
+ // QuickJS represents Uint8Array as object with numeric keys
+ // Count numeric keys to get byte length
+ const len = Object.keys(encoded).filter(
+ (k) => !isNaN(k)
+ ).length
+ if (len > 0) {
+ return len
+ }
+ }
+ } catch (_e) {
+ // Fall through to manual calculation
+ }
+ }
+
+ // Fallback: manual UTF-8 byte length calculation
+ let byteLength = 0
+ for (let i = 0; i < text.length; i++) {
+ const code = text.charCodeAt(i)
+ if (code < 0x80) byteLength += 1
+ else if (code < 0x800) byteLength += 2
+ else if (code < 0x10000) byteLength += 3
+ else byteLength += 4
+ }
+ return byteLength
+ }
+ }
+
+ return withModifiers(modifiers)
+ } catch (_e) {
+ return withModifiers(modifiers)
+ }
+ },
text: () => globalThis.hopp.response.body.asText(),
json: () => globalThis.hopp.response.body.asJSON(),
- stream: globalThis.hopp.response.body.bytes(),
+ get stream() {
+ return globalThis.hopp.response.body.bytes()
+ },
+ reason: inputs.responseReason,
+ dataURI: inputs.responseDataURI,
+ jsonp: (callbackName) => inputs.responseJsonp(callbackName),
headers: {
get: (name) => {
const headers = globalThis.hopp.response.headers
const header = headers.find(
(h) => h.key.toLowerCase() === name.toLowerCase()
)
- return header ? header.value : null
+ // NOTE: Postman returns undefined for non-existent headers, not null
+ return header ? header.value : undefined
},
has: (name) => {
const headers = globalThis.hopp.response.headers
@@ -394,14 +3183,419 @@
},
},
cookies: {
- get: (_name) => {
- throw new Error("pm.response.cookies.get() not yet implemented")
+ get: (name) => {
+ // Parse cookies from Set-Cookie headers
+ const headers = globalThis.hopp.response.headers
+ const setCookieHeaders = headers.filter(
+ (h) => h.key.toLowerCase() === "set-cookie"
+ )
+
+ for (const header of setCookieHeaders) {
+ const cookieStr = header.value
+ const cookieName = cookieStr.split("=")[0].trim()
+ if (cookieName === name) {
+ // Extract cookie value (everything after first =, before first ;)
+ const parts = cookieStr.split(";")
+ const [, ...valueRest] = parts[0].split("=")
+ const value = valueRest.join("=").trim()
+
+ // Return just the value string, matching Postman behavior
+ return value
+ }
+ }
+ return null
},
- has: (_name) => {
- throw new Error("pm.response.cookies.has() not yet implemented")
+ has: (name) => {
+ const headers = globalThis.hopp.response.headers
+ const setCookieHeaders = headers.filter(
+ (h) => h.key.toLowerCase() === "set-cookie"
+ )
+
+ for (const header of setCookieHeaders) {
+ const cookieName = header.value.split("=")[0].trim()
+ if (cookieName === name) {
+ return true
+ }
+ }
+ return false
},
toObject: () => {
- throw new Error("pm.response.cookies.toObject() not yet implemented")
+ const headers = globalThis.hopp.response.headers
+ const setCookieHeaders = headers.filter(
+ (h) => h.key.toLowerCase() === "set-cookie"
+ )
+
+ const cookies = {}
+ for (const header of setCookieHeaders) {
+ const parts = header.value.split(";")
+ const [nameValue] = parts
+ const [name, ...valueRest] = nameValue.split("=")
+ const value = valueRest.join("=").trim()
+ cookies[name.trim()] = value
+ }
+ return cookies
+ },
+ },
+
+ // Postman BDD-style assertion helpers
+ to: {
+ have: {
+ status: (expectedCode) => {
+ const actual = globalThis.hopp.response.statusCode
+ globalThis.hopp.expect(actual).to.equal(expectedCode)
+ },
+ header: (headerName, headerValue) => {
+ const headers = globalThis.hopp.response.headers
+ const header = headers.find(
+ (h) => h.key.toLowerCase() === headerName.toLowerCase()
+ )
+ if (headerValue !== undefined) {
+ globalThis.hopp
+ .expect(header ? header.value : null)
+ .to.equal(headerValue)
+ } else {
+ globalThis.hopp.expect(header).to.exist
+ }
+ },
+ body: (expectedBody) => {
+ const actualBody = globalThis.hopp.response.body.asText()
+ globalThis.hopp.expect(actualBody).to.equal(expectedBody)
+ },
+ jsonBody: (keyOrSchema, expectedValue) => {
+ const jsonData = globalThis.hopp.response.body.asJSON()
+ if (keyOrSchema === undefined) {
+ // No arguments: assert response is JSON object
+ globalThis.hopp.expect(jsonData).to.be.an("object")
+ } else if (typeof keyOrSchema === "string") {
+ // Key provided
+ if (expectedValue !== undefined) {
+ // Key and value: assert property equals value
+ globalThis.hopp
+ .expect(jsonData[keyOrSchema])
+ .to.equal(expectedValue)
+ } else {
+ // Key only: assert property exists
+ globalThis.hopp.expect(jsonData).to.have.property(keyOrSchema)
+ }
+ } else if (typeof keyOrSchema === "object") {
+ // Schema validation (basic deep equality check)
+ globalThis.hopp.expect(jsonData).to.deep.equal(keyOrSchema)
+ }
+ },
+ responseTime: {
+ below: (ms) => {
+ const actual = globalThis.hopp.response.responseTime
+ globalThis.hopp.expect(actual).to.be.below(ms)
+ },
+ above: (ms) => {
+ const actual = globalThis.hopp.response.responseTime
+ globalThis.hopp.expect(actual).to.be.above(ms)
+ },
+ },
+ jsonSchema: (schema) => {
+ // Basic JSON Schema validation (supports common keywords)
+ const jsonData = globalThis.hopp.response.body.asJSON()
+
+ const validateSchema = (data, schema) => {
+ // Type validation
+ if (schema.type) {
+ const actualType = Array.isArray(data)
+ ? "array"
+ : data === null
+ ? "null"
+ : typeof data
+ if (actualType !== schema.type) {
+ return `Expected type ${schema.type}, got ${actualType}`
+ }
+ }
+
+ // Required properties
+ if (schema.required && Array.isArray(schema.required)) {
+ for (const prop of schema.required) {
+ if (!(prop in data)) {
+ return `Required property '${prop}' is missing`
+ }
+ }
+ }
+
+ // Properties validation
+ if (schema.properties && typeof data === "object") {
+ for (const prop in schema.properties) {
+ if (prop in data) {
+ const error = validateSchema(
+ data[prop],
+ schema.properties[prop]
+ )
+ if (error) return error
+ }
+ }
+ }
+
+ // Array validation
+ if (schema.items && Array.isArray(data)) {
+ for (const item of data) {
+ const error = validateSchema(item, schema.items)
+ if (error) return error
+ }
+ }
+
+ // Enum validation
+ if (schema.enum && Array.isArray(schema.enum)) {
+ if (!schema.enum.includes(data)) {
+ return `Value must be one of ${JSON.stringify(schema.enum)}`
+ }
+ }
+
+ // Min/max validation
+ if (typeof data === "number") {
+ if (schema.minimum !== undefined && data < schema.minimum) {
+ return `Value must be >= ${schema.minimum}`
+ }
+ if (schema.maximum !== undefined && data > schema.maximum) {
+ return `Value must be <= ${schema.maximum}`
+ }
+ }
+
+ // String length validation
+ if (typeof data === "string") {
+ if (
+ schema.minLength !== undefined &&
+ data.length < schema.minLength
+ ) {
+ return `String length must be >= ${schema.minLength}`
+ }
+ if (
+ schema.maxLength !== undefined &&
+ data.length > schema.maxLength
+ ) {
+ return `String length must be <= ${schema.maxLength}`
+ }
+ if (schema.pattern) {
+ const regex = new RegExp(schema.pattern)
+ if (!regex.test(data)) {
+ return `String must match pattern ${schema.pattern}`
+ }
+ }
+ }
+
+ // Array length validation
+ if (Array.isArray(data)) {
+ if (
+ schema.minItems !== undefined &&
+ data.length < schema.minItems
+ ) {
+ return `Array must have >= ${schema.minItems} items`
+ }
+ if (
+ schema.maxItems !== undefined &&
+ data.length > schema.maxItems
+ ) {
+ return `Array must have <= ${schema.maxItems} items`
+ }
+ }
+
+ return null
+ }
+
+ const error = validateSchema(jsonData, schema)
+ if (error) {
+ // Schema validation failed - this would throw in Postman,
+ // but we record it as a test failure instead for better UX
+ throw new Error(error)
+ }
+ // On success, no assertion is recorded (Postman behavior)
+ },
+ charset: (expectedCharset) => {
+ const headers = globalThis.hopp.response.headers
+ const contentType = headers.find(
+ (h) => h.key.toLowerCase() === "content-type"
+ )
+ const contentTypeValue = contentType ? contentType.value : ""
+ const charsetMatch = contentTypeValue.match(/charset=([^;]+)/)
+ const actualCharset = charsetMatch
+ ? charsetMatch[1].trim().toLowerCase()
+ : null
+ globalThis.hopp
+ .expect(actualCharset)
+ .to.equal(expectedCharset.toLowerCase())
+ },
+ cookie: (cookieName, cookieValue) => {
+ const headers = globalThis.hopp.response.headers
+ const setCookieHeaders = headers.filter(
+ (h) => h.key.toLowerCase() === "set-cookie"
+ )
+
+ let found = false
+ let actualValue = null
+ for (const header of setCookieHeaders) {
+ const cookieStr = header.value
+ const name = cookieStr.split("=")[0].trim()
+ if (name === cookieName) {
+ found = true
+ const [, ...valueRest] = cookieStr.split("=")
+ actualValue = valueRest.join("=").split(";")[0].trim()
+ break
+ }
+ }
+
+ if (!found) {
+ // Cookie not found - record expectation failure
+ globalThis.hopp.expect(found, `Cookie '${cookieName}' not found`)
+ .to.be.true
+ } else if (cookieValue !== undefined) {
+ // Cookie found and value specified - check value
+ globalThis.hopp.expect(actualValue).to.equal(cookieValue)
+ } else {
+ // Cookie found and no value specified - just assert it exists
+ globalThis.hopp.expect(found).to.be.true
+ }
+ },
+ jsonPath: (path, expectedValue) => {
+ // Basic JSONPath implementation (supports simple paths)
+ const jsonData = globalThis.hopp.response.body.asJSON()
+
+ const evaluatePath = (data, path) => {
+ // Remove leading $ if present
+ const cleanPath = path.startsWith("$") ? path.slice(1) : path
+ if (!cleanPath || cleanPath === ".") {
+ return { success: true, value: data }
+ }
+
+ // Split by dots and brackets, handling array indices
+ const parts = cleanPath
+ .replace(/\[(\d+)\]/g, ".$1")
+ .split(".")
+ .filter((p) => p)
+
+ let current = data
+ for (const part of parts) {
+ if (current === null || current === undefined) {
+ return {
+ success: false,
+ error: `Cannot access property '${part}' of ${current}`,
+ }
+ }
+ if (Array.isArray(current)) {
+ const index = parseInt(part)
+ if (isNaN(index) || index >= current.length) {
+ return {
+ success: false,
+ error: `Array index '${part}' out of bounds`,
+ }
+ }
+ current = current[index]
+ } else if (typeof current === "object") {
+ if (!(part in current)) {
+ return {
+ success: false,
+ error: `Property '${part}' not found`,
+ }
+ }
+ current = current[part]
+ } else {
+ return {
+ success: false,
+ error: `Cannot access property '${part}' of ${typeof current}`,
+ }
+ }
+ }
+ return { success: true, value: current }
+ }
+
+ const result = evaluatePath(jsonData, path)
+ if (!result.success) {
+ throw new Error(result.error)
+ }
+
+ if (expectedValue !== undefined) {
+ globalThis.hopp.expect(result.value).to.deep.equal(expectedValue)
+ } else {
+ globalThis.hopp.expect(result.value).to.exist
+ }
+ },
+ },
+ be: {
+ // Status code convenience methods
+ ok: () => {
+ const code = globalThis.hopp.response.statusCode
+ globalThis.hopp.expect(code >= 200 && code < 300).to.be.true
+ },
+ success: () => {
+ // Alias for ok - validates 2xx status codes
+ const code = globalThis.hopp.response.statusCode
+ globalThis.hopp.expect(code >= 200 && code < 300).to.be.true
+ },
+ accepted: () => {
+ const code = globalThis.hopp.response.statusCode
+ globalThis.hopp.expect(code).to.equal(202)
+ },
+ badRequest: () => {
+ const code = globalThis.hopp.response.statusCode
+ globalThis.hopp.expect(code).to.equal(400)
+ },
+ unauthorized: () => {
+ const code = globalThis.hopp.response.statusCode
+ globalThis.hopp.expect(code).to.equal(401)
+ },
+ forbidden: () => {
+ const code = globalThis.hopp.response.statusCode
+ globalThis.hopp.expect(code).to.equal(403)
+ },
+ notFound: () => {
+ const code = globalThis.hopp.response.statusCode
+ globalThis.hopp.expect(code).to.equal(404)
+ },
+ rateLimited: () => {
+ const code = globalThis.hopp.response.statusCode
+ globalThis.hopp.expect(code).to.equal(429)
+ },
+ clientError: () => {
+ // Validates 4xx status codes
+ const code = globalThis.hopp.response.statusCode
+ globalThis.hopp.expect(code >= 400 && code < 500).to.be.true
+ },
+ serverError: () => {
+ const code = globalThis.hopp.response.statusCode
+ globalThis.hopp.expect(code >= 500 && code < 600).to.be.true
+ },
+ // Content type checks
+ json: () => {
+ const headers = globalThis.hopp.response.headers
+ const contentType = headers.find(
+ (h) => h.key.toLowerCase() === "content-type"
+ )
+ globalThis.hopp
+ .expect(contentType ? contentType.value : "")
+ .to.include("application/json")
+ },
+ html: () => {
+ const headers = globalThis.hopp.response.headers
+ const contentType = headers.find(
+ (h) => h.key.toLowerCase() === "content-type"
+ )
+ globalThis.hopp
+ .expect(contentType ? contentType.value : "")
+ .to.include("text/html")
+ },
+ xml: () => {
+ const headers = globalThis.hopp.response.headers
+ const contentType = headers.find(
+ (h) => h.key.toLowerCase() === "content-type"
+ )
+ const ct = contentType ? contentType.value : ""
+ globalThis.hopp.expect(
+ ct.includes("application/xml") || ct.includes("text/xml")
+ ).to.be.true
+ },
+ text: () => {
+ const headers = globalThis.hopp.response.headers
+ const contentType = headers.find(
+ (h) => h.key.toLowerCase() === "content-type"
+ )
+ globalThis.hopp
+ .expect(contentType ? contentType.value : "")
+ .to.include("text/plain")
+ },
},
},
},
@@ -423,11 +3617,14 @@
},
test: (name, fn) => globalThis.hopp.test(name, fn),
- expect: (value) => globalThis.hopp.expect(value),
+ expect: Object.assign((value) => globalThis.hopp.expect(value), {
+ // pm.expect.fail() - Postman compatibility
+ fail: globalThis.hopp.expect.fail,
+ }),
// Script context information
info: {
- eventName: "post-request", // post-request context
+ eventName: "test", // Postman uses "test" for test scripts, not "post-request"
get requestName() {
return inputs.pmInfoRequestName()
},
@@ -452,40 +3649,6 @@
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)"
- )
- },
- },
-
// Postman Vault (unsupported)
vault: {
get: () => {
@@ -505,6 +3668,20 @@
},
},
+ // 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: () => {
@@ -532,15 +3709,86 @@
"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)
+ // Collection variables (unsupported)
+ collectionVariables: {
+ get: () => {
+ throw new Error(
+ "pm.collectionVariables.get() is not supported in Hoppscotch (Workspace feature)"
+ )
+ },
+ set: () => {
+ throw new Error(
+ "pm.collectionVariables.set() is not supported in Hoppscotch (Workspace feature)"
+ )
+ },
+ unset: () => {
+ throw new Error(
+ "pm.collectionVariables.unset() is not supported in Hoppscotch (Workspace feature)"
+ )
+ },
+ has: () => {
+ throw new Error(
+ "pm.collectionVariables.has() is not supported in Hoppscotch (Workspace feature)"
+ )
+ },
+ clear: () => {
+ throw new Error(
+ "pm.collectionVariables.clear() is not supported in Hoppscotch (Workspace feature)"
+ )
+ },
+ toObject: () => {
+ throw new Error(
+ "pm.collectionVariables.toObject() is not supported in Hoppscotch (Workspace feature)"
+ )
+ },
+ replaceIn: () => {
+ throw new Error(
+ "pm.collectionVariables.replaceIn() is not supported in Hoppscotch (Workspace feature)"
+ )
+ },
+ },
+
+ // Execution control
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)`
+ )
},
}
}
diff --git a/packages/hoppscotch-js-sandbox/src/bootstrap-code/pre-request.js b/packages/hoppscotch-js-sandbox/src/bootstrap-code/pre-request.js
index e8e341de..f29164d7 100644
--- a/packages/hoppscotch-js-sandbox/src/bootstrap-code/pre-request.js
+++ b/packages/hoppscotch-js-sandbox/src/bootstrap-code/pre-request.js
@@ -4,8 +4,8 @@
"use strict"
globalThis.pw = {
env: {
- get: (key) => inputs.envGet(key),
- getResolve: (key) => inputs.envGetResolve(key),
+ 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),
@@ -50,42 +50,99 @@
// 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) =>
- inputs.envGetResolve(key, { fallbackToNull: true, source: "all" }),
- getRaw: (key) =>
- inputs.envGet(key, { fallbackToNull: true, source: "all" }),
+ 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) => inputs.envGetInitialRaw(key),
+ getInitialRaw: (key) => {
+ return convertMarkerToValue(inputs.envGetInitialRaw(key))
+ },
setInitial: (key, value) => inputs.envSetInitial(key, value),
active: {
- get: (key) =>
- inputs.envGetResolve(key, { fallbackToNull: true, source: "active" }),
- getRaw: (key) =>
- inputs.envGet(key, { fallbackToNull: true, source: "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) =>
- inputs.envGetInitialRaw(key, { source: "active" }),
+ getInitialRaw: (key) => {
+ return convertMarkerToValue(
+ inputs.envGetInitialRaw(key, { source: "active" })
+ )
+ },
setInitial: (key, value) =>
inputs.envSetInitial(key, value, { source: "active" }),
},
global: {
- get: (key) =>
- inputs.envGetResolve(key, { fallbackToNull: true, source: "global" }),
- getRaw: (key) =>
- inputs.envGet(key, { fallbackToNull: true, source: "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) =>
- inputs.envGetInitialRaw(key, { source: "global" }),
+ getInitialRaw: (key) => {
+ return convertMarkerToValue(
+ inputs.envGetInitialRaw(key, { source: "global" })
+ )
+ },
setInitial: (key, value) =>
inputs.envSetInitial(key, value, { source: "global" }),
},
@@ -104,34 +161,100 @@
// PM Namespace - Postman Compatibility Layer
globalThis.pm = {
environment: {
- get: (key) => globalThis.hopp.env.active.get(key),
- set: (key, value) => globalThis.hopp.env.active.set(key, value),
+ 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: () => {
- throw new Error("pm.environment.clear() not yet implemented")
+ // Get all active environment variables and delete them
+ const envVars = inputs.getAllSelectedEnvs()
+ envVars.forEach((envVar) => {
+ globalThis.hopp.env.active.delete(envVar.key)
+ })
},
toObject: () => {
- throw new Error("pm.environment.toObject() not yet implemented")
+ // 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) => globalThis.hopp.env.global.get(key),
- set: (key, value) => globalThis.hopp.env.global.set(key, value),
+ 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: () => {
- throw new Error("pm.globals.clear() not yet implemented")
+ // Get all global environment variables and delete them
+ const envVars = inputs.getAllGlobalEnvs()
+ envVars.forEach((envVar) => {
+ globalThis.hopp.env.global.delete(envVar.key)
+ })
},
toObject: () => {
- throw new Error("pm.globals.toObject() not yet implemented")
+ // 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) => globalThis.hopp.env.get(key),
- set: (key, value) => globalThis.hopp.env.active.set(key, value),
+ 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
@@ -143,19 +266,596 @@
},
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() {
- const urlString = globalThis.hopp.request.url
- return {
- toString: () => urlString,
+ // 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(
@@ -163,12 +863,14 @@
)
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) => {
@@ -176,16 +878,291 @@
})
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() {
- return globalThis.hopp.request.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")
+ }
+ },
},
// Script context information
@@ -268,6 +1245,20 @@
},
},
+ // 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: () => {
diff --git a/packages/hoppscotch-js-sandbox/src/cage-modules/namespaces/pm-namespace.ts b/packages/hoppscotch-js-sandbox/src/cage-modules/namespaces/pm-namespace.ts
index 9737a89b..fa308cb4 100644
--- a/packages/hoppscotch-js-sandbox/src/cage-modules/namespaces/pm-namespace.ts
+++ b/packages/hoppscotch-js-sandbox/src/cage-modules/namespaces/pm-namespace.ts
@@ -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
}),
}
}
diff --git a/packages/hoppscotch-js-sandbox/src/cage-modules/namespaces/pw-namespace.ts b/packages/hoppscotch-js-sandbox/src/cage-modules/namespaces/pw-namespace.ts
index 7308c06a..2e3d6847 100644
--- a/packages/hoppscotch-js-sandbox/src/cage-modules/namespaces/pw-namespace.ts
+++ b/packages/hoppscotch-js-sandbox/src/cage-modules/namespaces/pw-namespace.ts
@@ -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
}
diff --git a/packages/hoppscotch-js-sandbox/src/cage-modules/scripting-modules.ts b/packages/hoppscotch-js-sandbox/src/cage-modules/scripting-modules.ts
index 59db7e1c..8cb33eac 100644
--- a/packages/hoppscotch-js-sandbox/src/cage-modules/scripting-modules.ts
+++ b/packages/hoppscotch-js-sandbox/src/cage-modules/scripting-modules.ts
@@ -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)
+ }
+ ),
}
}
diff --git a/packages/hoppscotch-js-sandbox/src/cage-modules/utils/base-inputs.ts b/packages/hoppscotch-js-sandbox/src/cage-modules/utils/base-inputs.ts
index fa32f57a..b6238f3f 100644
--- a/packages/hoppscotch-js-sandbox/src/cage-modules/utils/base-inputs.ts
+++ b/packages/hoppscotch-js-sandbox/src/cage-modules/utils/base-inputs.ts
@@ -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,
}
}
diff --git a/packages/hoppscotch-js-sandbox/src/cage-modules/utils/chai-helpers.ts b/packages/hoppscotch-js-sandbox/src/cage-modules/utils/chai-helpers.ts
new file mode 100644
index 00000000..dfbe1222
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/cage-modules/utils/chai-helpers.ts
@@ -0,0 +1,2126 @@
+import { CageModuleCtx, defineSandboxFn } from "faraday-cage/modules"
+import * as chai from "chai"
+import { TestDescriptor, SandboxValue } from "~/types"
+
+/**
+ * Creates Chai-based assertion methods that can be used across the sandbox boundary
+ * Each method wraps actual Chai.js assertions and records results to the test stack
+ */
+export const createChaiMethods: (
+ ctx: CageModuleCtx,
+ testStack: TestDescriptor[]
+) => Record = (ctx, testStack) => {
+ /**
+ * Helper to execute a Chai assertion and record the result
+ */
+ const executeChaiAssertion = (assertionFn: () => void, message: string) => {
+ if (testStack.length === 0) return
+
+ try {
+ assertionFn()
+ // Record success
+ testStack[testStack.length - 1].expectResults.push({
+ status: "pass",
+ message,
+ })
+ } catch (_error: any) {
+ // Record failure but DON'T throw - allow test to continue
+ testStack[testStack.length - 1].expectResults.push({
+ status: "fail",
+ message,
+ })
+ // Don't throw - let the test continue to execute all assertions
+ }
+ }
+
+ /**
+ * Helper to format values for display in messages
+ */
+ // Helper to apply modifiers (not, deep, include, etc.) to a Chai assertion
+ const applyModifiers = (value: SandboxValue, modifiers: string) => {
+ let assertion: any = chai.expect(value)
+ const isNot = modifiers.includes("not")
+ const isDeep = modifiers.includes("deep")
+ const isInclude = modifiers.includes("include")
+
+ if (isNot) assertion = assertion.to.not
+ else assertion = assertion.to
+
+ if (isInclude) assertion = assertion.include
+ if (isDeep) assertion = assertion.deep
+
+ return assertion
+ }
+
+ const formatValue = (val: unknown): string => {
+ if (val === null) return "null"
+ if (val === undefined) return "undefined"
+ // Handle BigInt
+ if (typeof val === "bigint") return String(val) + "n"
+ // Handle Symbol
+ if (typeof val === "symbol") return String(val)
+ // Handle functions (including constructors) - return as-is without quotes
+ if (typeof val === "function") {
+ // For named constructors, return just the name
+ if (val.name && /^[A-Z]/.test(val.name)) {
+ return val.name
+ }
+ // For other functions, return the string representation without quotes
+ return String(val)
+ }
+ // Handle strings that look like functions/constructors (serialized from sandbox)
+ if (typeof val === "string") {
+ const trimmed = val.trim()
+
+ // Check for pre-formatted special values (Set, Map, RegExp patterns)
+ if (trimmed.startsWith("new Set(") || trimmed.startsWith("new Map(")) {
+ return val // Return without quotes - already formatted
+ }
+ if (trimmed.match(/^\/.*\/[gimsuvy]*$/)) {
+ return val // Return regex pattern without quotes - already formatted
+ }
+
+ // Check for constructor names (capitalized identifiers)
+ // Only match known built-in constructors to avoid matching regular strings like "Alice"
+ const knownConstructors = [
+ "Array",
+ "Object",
+ "String",
+ "Number",
+ "Boolean",
+ "Date",
+ "RegExp",
+ "Error",
+ "TypeError",
+ "RangeError",
+ "ReferenceError",
+ "SyntaxError",
+ "Set",
+ "Map",
+ "WeakSet",
+ "WeakMap",
+ "Promise",
+ "Symbol",
+ "Function",
+ ]
+ // Also allow any identifier ending with "Error" (for custom error types)
+ const isErrorConstructor =
+ /^[A-Z][a-zA-Z0-9]*Error$/.test(trimmed) ||
+ knownConstructors.includes(trimmed)
+
+ if (/^[A-Z][a-zA-Z0-9]*$/.test(trimmed) && isErrorConstructor) {
+ return trimmed // Return constructor name without quotes
+ }
+
+ // Check if string looks like a function definition
+ if (
+ trimmed.startsWith("function") ||
+ trimmed.match(/^\(.*\)\s*=>/) ||
+ trimmed.match(/^[a-zA-Z_$][\w$]*\s*\(/)
+ ) {
+ // Keep the original function structure instead of simplifying to [Function]
+ // This preserves function signatures like "function Cat() {}" in messages
+ return trimmed
+ }
+
+ // Check for native code functions
+ if (trimmed.includes("[native code]")) {
+ // Extract function name from "function TypeEr[ror() {\n [native code]\n}"
+ const nameMatch = trimmed.match(/function\s+([A-Z][a-zA-Z0-9]*)\s*\(/)
+ if (nameMatch) {
+ return nameMatch[1]
+ }
+ }
+
+ return `'${val}'`
+ }
+ if (typeof val === "number") {
+ if (isNaN(val)) return "NaN"
+ if (val === Infinity) return "Infinity"
+ if (val === -Infinity) return "-Infinity"
+ if (val === Math.PI) return "Math.PI"
+ if (val === Math.E) return "Math.E"
+ 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(", ")}]` // Space after comma for readability
+ }
+ if (typeof val === "object") {
+ try {
+ // Handle special object types
+ if (val instanceof Map) {
+ const entries = Array.from(val.entries()).slice(0, 3)
+ if (entries.length === 0) return "new Map()"
+ // Fix Map formatting for .map((entry: unknown) => ...)
+ const formatted = entries.map((entry: unknown) => {
+ const [key, value] = entry as [unknown, unknown]
+ return `[${key}, ${value}]`
+ })
+ return `new Map([${formatted.join(", ")}])`
+ }
+ if (val instanceof Set) {
+ const values = Array.from(val).slice(0, 10)
+ if (values.length === 0) return "new Set()"
+ return `new Set([${values.map(formatValue).join(", ")}])`
+ }
+ if (val instanceof Date) return `new Date(${val.toISOString()})`
+ if (val instanceof RegExp) return val.toString()
+
+ // Check constructor name for objects that lost their prototype
+ const constructorName = (val as { constructor?: { name?: string } })
+ .constructor?.name
+ if (constructorName && constructorName !== "Object") {
+ // Special handling for Set/Map that lost prototype but have size property
+ const objWithSize = val as { size?: unknown }
+ if (
+ constructorName === "Set" &&
+ typeof objWithSize.size === "number"
+ ) {
+ return `new Set()`
+ }
+ if (
+ constructorName === "Map" &&
+ typeof objWithSize.size === "number"
+ ) {
+ return `new Map()`
+ }
+ // Special handling for RegExp that lost prototype
+ const objWithRegex = val as { source?: unknown; flags?: unknown }
+ if (
+ constructorName === "RegExp" &&
+ typeof objWithRegex.source === "string"
+ ) {
+ const flags = objWithRegex.flags || ""
+ return `/${objWithRegex.source}/${flags}`
+ }
+ return `new ${constructorName}()`
+ }
+
+ // Check if it's a RegExp that lost its prototype
+ const objWithRegex = val as { source?: unknown; flags?: unknown }
+ if (typeof objWithRegex.source === "string") {
+ const flags = objWithRegex.flags || ""
+ return `/${objWithRegex.source}/${flags}`
+ }
+
+ // Check if it's a Set or Map that lost its prototype
+ const objWithMethods = val as {
+ size?: unknown
+ has?: unknown
+ forEach?: unknown
+ }
+ if (
+ typeof objWithMethods.size === "number" &&
+ typeof objWithMethods.has === "function"
+ ) {
+ // Likely a Set or Map
+ return objWithMethods.forEach ? "new Set()" : "new Map()"
+ }
+
+ const keys = Object.keys(val)
+ if (keys.length === 0) return "{}"
+ const pairs = keys
+ .slice(0, 5)
+ .map(
+ (k) => `${k}: ${formatValue((val as Record)[k])}`
+ )
+ return `{${pairs.join(", ")}}`
+ } catch {
+ return "[object Object]"
+ }
+ }
+ if (typeof val === "function") {
+ return val.name || "[Function]"
+ }
+ return String(val)
+ }
+
+ /**
+ * Build message with modifiers
+ * Cleans up duplicate words and formats properly
+ */
+ const buildMessage = (
+ value: SandboxValue,
+ modifiers: string,
+ assertion: string,
+ args: unknown[] = []
+ ): string => {
+ const valueStr = formatValue(value)
+
+ // Clean up modifiers
+ let cleanModifiers = modifiers
+ .replace(/\s+/g, " ") // normalize whitespace
+ .trim()
+
+ // Remove language chain words that don't add meaning to assertions
+ // Handle "that has" and "that does" carefully
+ const hasTypePrefix = cleanModifiers.match(
+ /\b(array|object|number|string|boolean|function)\s+that\s+has\b/
+ )
+
+ if (!hasTypePrefix) {
+ // Standalone "that has" -> "have", "that does" -> ""
+ cleanModifiers = cleanModifiers.replace(/\bthat\s+has\b/g, "have")
+ cleanModifiers = cleanModifiers.replace(/\bthat\s+does\b/g, "")
+ // Replace standalone "has" with "have"
+ cleanModifiers = cleanModifiers.replace(/\bhas\b/g, "have")
+ } else {
+ // Keep "that has" when preceded by a type (e.g., "be an array that has lengthOf")
+ // Just remove "that does"
+ cleanModifiers = cleanModifiers.replace(/\bthat\s+does\b/g, "")
+ }
+
+ // Replace "is" with "be"
+ cleanModifiers = cleanModifiers.replace(/\bis\b/g, "be")
+
+ // Remove remaining pure language chains (but keep "itself" as it's meaningful)
+ const languageChains = ["which", "does", "but"]
+ languageChains.forEach((word) => {
+ cleanModifiers = cleanModifiers.replace(
+ new RegExp(`\\b${word}\\b`, "g"),
+ ""
+ )
+ })
+
+ // Clean up extra spaces after removing words
+ cleanModifiers = cleanModifiers.replace(/\s+/g, " ").trim()
+
+ // Remove duplicate consecutive words (e.g., "be be" -> "be", "have have" -> "have")
+ cleanModifiers = cleanModifiers.replace(/\b(\w+)(\s+\1\b)+/g, "$1")
+
+ // Ensure it starts with "to" if not empty
+ if (cleanModifiers && !cleanModifiers.startsWith("to")) {
+ cleanModifiers = "to " + cleanModifiers
+ } else if (!cleanModifiers) {
+ cleanModifiers = "to"
+ }
+
+ // Build base message
+ let message = `Expected ${valueStr} ${cleanModifiers}`
+
+ // Add assertion, checking if it duplicates the last word in modifiers
+ const modifierWords = cleanModifiers.trim().split(/\s+/)
+ const lastModWord = modifierWords[modifierWords.length - 1]
+ const firstAssertWord = assertion.split(/\s+/)[0]
+
+ if (lastModWord === firstAssertWord) {
+ // Skip the duplicate word in assertion
+ const assertionRest = assertion.substring(firstAssertWord.length).trim()
+ if (assertionRest) {
+ message += ` ${assertionRest}`
+ }
+ } else {
+ message += ` ${assertion}`
+ }
+
+ if (args.length > 0) {
+ // Special handling for keys assertions
+ if (assertion === "keys") {
+ let keys = args
+ // If first arg is an array and it's the only arg, flatten it
+ if (args.length === 1 && Array.isArray(args[0])) {
+ keys = args[0]
+ }
+ // Format keys with quotes (including numbers)
+ const keyStrs = keys.map((k) =>
+ typeof k === "number" ? `'${k}'` : formatValue(k)
+ )
+ message += ` ${keyStrs.join(", ")}`
+ } else if (assertion === "members") {
+ // Format members - if first arg is already an array, don't double-wrap
+ if (args.length === 1 && Array.isArray(args[0])) {
+ // Single array argument - format its contents directly
+ const memberStrs = args[0].map(formatValue)
+ message += ` [${memberStrs.join(", ")}]`
+ } else {
+ // Multiple arguments or non-array - wrap them
+ const argStrs = args.map(formatValue)
+ message += ` [${argStrs.join(", ")}]`
+ }
+ } else {
+ const argStrs = args.map(formatValue)
+ // Add comma separator for property-like assertions, space for others
+ const separator = assertion.includes("property") ? ", " : " "
+ message += `${separator}${argStrs.join(", ")}`
+ }
+ }
+
+ return message
+ }
+
+ // Return all Chai methods wrapped with defineSandboxFn
+ return {
+ // Equality assertions
+ chaiEqual: defineSandboxFn(ctx, "chaiEqual", ((
+ value: SandboxValue,
+ expected: SandboxValue,
+ modifiers: SandboxValue,
+ methodName: SandboxValue,
+ isSameReference?: boolean,
+ typeInfo?: SandboxValue
+ ) => {
+ const mods = modifiers || " to"
+ const isDeep = String(mods).includes("deep")
+
+ // PRE-CHECK PATTERN: Use reference equality check from sandbox
+ // ONLY applies to .equal() WITHOUT .deep modifier
+ // Special handling: Date/RegExp have typeInfo even after serialization to strings
+ if (
+ !isDeep &&
+ isSameReference !== undefined &&
+ (typeInfo ||
+ (typeof value === "object" &&
+ value !== null &&
+ typeof expected === "object" &&
+ expected !== null))
+ ) {
+ const isNegated = String(mods).includes("not")
+ const shouldPass = isNegated ? !isSameReference : isSameReference
+
+ // For Date/RegExp, use type info to display proper values
+ let displayValue = formatValue(value)
+ let displayExpected = formatValue(expected)
+
+ if (typeInfo) {
+ const info = typeInfo as {
+ type?: string
+ valueTime?: unknown
+ expectedTime?: unknown
+ valueSource?: unknown
+ valueFlags?: unknown
+ expectedSource?: unknown
+ expectedFlags?: unknown
+ }
+ if (info.type === "Date") {
+ displayValue = new Date(info.valueTime as number).toISOString()
+ displayExpected = new Date(
+ info.expectedTime as number
+ ).toISOString()
+ } else if (info.type === "RegExp") {
+ displayValue = `/${info.valueSource}/${info.valueFlags}`
+ displayExpected = `/${info.expectedSource}/${info.expectedFlags}`
+ }
+ }
+
+ executeChaiAssertion(
+ () => {
+ if (!shouldPass) {
+ throw new Error(
+ `Expected ${displayValue}${String(mods)} ${String(methodName || "equal")} ${displayExpected}`
+ )
+ }
+ },
+ buildMessage(
+ displayValue,
+ String(mods),
+ String(methodName || "equal"),
+ [displayExpected]
+ )
+ )
+ } else {
+ // For primitives, .deep.equal(), or when reference info not available, use default Chai behavior
+ executeChaiAssertion(
+ () => applyModifiers(value, mods).equal(expected),
+ buildMessage(value, String(mods), methodName || "equal", [expected])
+ )
+ }
+ }) as any),
+
+ chaiEql: defineSandboxFn(ctx, "chaiEql", ((
+ value: SandboxValue,
+ expected: SandboxValue,
+ modifiers?: SandboxValue,
+ valueMetadata?: SandboxValue,
+ expectedMetadata?: SandboxValue
+ ) => {
+ const mods = modifiers || " to"
+
+ // PRE-CHECK PATTERN: Use metadata for special objects (RegExp, Date)
+ if (valueMetadata && expectedMetadata) {
+ const valueMeta = valueMetadata as {
+ type?: string
+ source?: unknown
+ flags?: unknown
+ time?: unknown
+ }
+ const expectedMeta = expectedMetadata as {
+ type?: string
+ source?: unknown
+ flags?: unknown
+ time?: unknown
+ }
+ const isNegated = String(mods).includes("not")
+ let matches = false
+
+ if (valueMeta.type === "RegExp") {
+ // Compare RegExp by source and flags
+ matches =
+ valueMeta.source === expectedMeta.source &&
+ valueMeta.flags === expectedMeta.flags
+ } else if (valueMeta.type === "Date") {
+ // Compare Date by timestamp
+ matches = valueMeta.time === expectedMeta.time
+ }
+
+ const shouldPass = isNegated ? !matches : matches
+
+ executeChaiAssertion(
+ () => {
+ if (!shouldPass) {
+ const displayValue =
+ valueMeta.type === "RegExp"
+ ? `/${valueMeta.source}/${valueMeta.flags}`
+ : new Date(valueMeta.time as number).toISOString()
+ const displayExpected =
+ expectedMeta.type === "RegExp"
+ ? `/${expectedMeta.source}/${expectedMeta.flags}`
+ : new Date(expectedMeta.time as number).toISOString()
+
+ throw new Error(
+ `Expected ${displayValue}${String(mods)} eql ${displayExpected}`
+ )
+ }
+ },
+ valueMeta.type === "RegExp"
+ ? buildMessage(
+ `/${valueMeta.source}/${valueMeta.flags}`,
+ mods,
+ "eql",
+ [`/${expectedMeta.source}/${expectedMeta.flags}`]
+ )
+ : buildMessage(value, mods, "eql", [expected])
+ )
+ } else {
+ // Default behavior for non-special objects
+ executeChaiAssertion(
+ () => applyModifiers(value, mods).eql(expected),
+ buildMessage(value, String(mods), "eql", [expected])
+ )
+ }
+ }) as any),
+
+ chaiDeepEqual: defineSandboxFn(
+ ctx,
+ "chaiDeepEqual",
+ (
+ value: SandboxValue,
+ expected: SandboxValue,
+ modifiers?: SandboxValue
+ ) => {
+ const mods = modifiers || " to"
+ executeChaiAssertion(
+ () => applyModifiers(value, mods).deep.equal(expected),
+ buildMessage(value, String(mods), "deep equal", [expected])
+ )
+ }
+ ),
+
+ // Type assertions
+ chaiTypeOf: defineSandboxFn(
+ ctx,
+ "chaiTypeOf",
+ (
+ value: SandboxValue,
+ type: SandboxValue,
+ modifiers?: SandboxValue,
+ preCheckedType?: unknown
+ ) => {
+ const mods = modifiers || " to"
+ const isNegated = String(mods).includes("not")
+
+ // Use "an" for vowel sounds, "a" otherwise
+ const article = /^[aeiou]/i.test(type) ? "an" : "a"
+
+ // PRE-CHECK PATTERN: Use pre-checked typeof result from sandbox
+ // After serialization, functions may not be recognized as functions by Chai
+ if (preCheckedType !== undefined) {
+ let matches = String(preCheckedType) === String(type)
+
+ // Special case: Arrays are both 'array' and 'object' in JavaScript
+ // typeof [] === 'object', but Chai also recognizes 'array'
+ if (preCheckedType === "array" && type === "object") {
+ matches = true // Arrays should pass for both 'array' and 'object'
+ }
+
+ const shouldPass = isNegated ? !matches : matches
+
+ if (testStack.length === 0) return
+ testStack[testStack.length - 1].expectResults.push({
+ status: shouldPass ? "pass" : "fail",
+ message: buildMessage(value, mods, `${article} ${type}`),
+ })
+ } else {
+ // Fallback to Chai's type checking if no pre-check available
+ executeChaiAssertion(
+ () => applyModifiers(value, mods).be.a(type),
+ buildMessage(value, String(mods), `${article} ${type}`)
+ )
+ }
+ }
+ ),
+
+ chaiInstanceOf: defineSandboxFn(
+ ctx,
+ "chaiInstanceOf",
+ function (
+ value,
+ constructorName,
+ modifiers,
+ preCheckedType,
+ displayValue,
+ actualInstanceCheck
+ ) {
+ const mods = modifiers || " to"
+ const isNegated = String(mods).includes("not")
+
+ // PRE-CHECK PATTERN: Use Object.prototype.toString result from sandbox
+ // We receive the constructor NAME as a string, not the constructor itself
+ // Map of constructor names to their Object.prototype.toString signatures
+ const builtInTypes: Record = {
+ Array: "[object Array]",
+ Date: "[object Date]",
+ Error: "[object Error]",
+ RegExp: "[object RegExp]",
+ Set: "[object Set]",
+ Map: "[object Map]",
+ TypeError: "[object Error]",
+ RangeError: "[object Error]",
+ ReferenceError: "[object Error]",
+ SyntaxError: "[object Error]",
+ }
+
+ // constructorName is already a string from the sandbox
+ const constructorNameStr = String(constructorName)
+
+ // Check if this is a built-in type we can detect
+ const expectedType = builtInTypes[constructorNameStr]
+ const isBuiltIn = expectedType !== undefined
+
+ // Use pre-checked type if available, otherwise fall back to actual check
+ const actualType =
+ preCheckedType || Object.prototype.toString.call(value)
+
+ let isInstance: boolean
+ // Special case: Object constructor
+ // In JavaScript, all objects (arrays, plain objects, dates, etc.) are instanceof Object
+ // Check if value is an object type (not null, not undefined, not primitives)
+ if (constructorNameStr === "Object") {
+ const valueType = typeof value
+ const isObjectType =
+ value !== null &&
+ value !== undefined &&
+ (valueType === "object" || valueType === "function")
+ isInstance = isObjectType
+ } else if (isBuiltIn) {
+ // For built-in types, compare Object.prototype.toString results
+ isInstance = actualType === expectedType
+ } else {
+ // For custom types, use the pre-checked result from sandbox
+ // The instanceof check was performed before serialization
+ isInstance = Boolean(actualInstanceCheck)
+ }
+
+ const shouldPass = isNegated ? !isInstance : isInstance
+
+ // Use displayValue if provided (for Set/Map), otherwise use value
+ const valueToDisplay = displayValue || value
+
+ executeChaiAssertion(
+ () => {
+ if (!shouldPass) {
+ throw new Error(
+ `Expected ${formatValue(valueToDisplay)}${String(mods)} be an instanceof ${constructorNameStr}`
+ )
+ }
+ },
+ buildMessage(valueToDisplay, String(mods), `be an instanceof`, [
+ constructorNameStr,
+ ])
+ )
+ }
+ ),
+
+ // Truthiness assertions
+ chaiOk: defineSandboxFn(
+ ctx,
+ "chaiOk",
+ (value: SandboxValue, modifiers?: SandboxValue) => {
+ const mods = modifiers || " to"
+ executeChaiAssertion(
+ () => {
+ const assertion = applyModifiers(value, mods)
+ void assertion.be.ok
+ },
+ buildMessage(value, String(mods), "ok")
+ )
+ }
+ ),
+
+ chaiTrue: defineSandboxFn(
+ ctx,
+ "chaiTrue",
+ (value: SandboxValue, modifiers?: SandboxValue) => {
+ const mods = modifiers || " to"
+ executeChaiAssertion(
+ () => {
+ const assertion = applyModifiers(value, mods)
+ void assertion.be.true
+ },
+ buildMessage(value, String(mods), "true")
+ )
+ }
+ ),
+
+ chaiFalse: defineSandboxFn(
+ ctx,
+ "chaiFalse",
+ (value: SandboxValue, modifiers?: SandboxValue) => {
+ const mods = modifiers || " to"
+ executeChaiAssertion(
+ () => {
+ const assertion = applyModifiers(value, mods)
+ void assertion.be.false
+ },
+ buildMessage(value, String(mods), "false")
+ )
+ }
+ ),
+
+ // Nullish assertions
+ chaiNull: defineSandboxFn(
+ ctx,
+ "chaiNull",
+ (value: SandboxValue, modifiers?: SandboxValue) => {
+ const mods = modifiers || " to"
+ executeChaiAssertion(
+ () => {
+ const assertion = applyModifiers(value, mods)
+ void assertion.be.null
+ },
+ buildMessage(value, String(mods), "null")
+ )
+ }
+ ),
+
+ chaiUndefined: defineSandboxFn(
+ ctx,
+ "chaiUndefined",
+ (value: SandboxValue, modifiers?: SandboxValue) => {
+ const mods = modifiers || " to"
+ executeChaiAssertion(
+ () => {
+ const assertion = applyModifiers(value, mods)
+ void assertion.be.undefined
+ },
+ buildMessage(value, String(mods), "undefined")
+ )
+ }
+ ),
+
+ chaiNaN: defineSandboxFn(
+ ctx,
+ "chaiNaN",
+ (value: SandboxValue, modifiers?: SandboxValue) => {
+ const mods = modifiers || " to"
+ executeChaiAssertion(
+ () => {
+ const assertion = applyModifiers(value, mods)
+ void assertion.be.NaN
+ },
+ buildMessage(value, String(mods), "NaN")
+ )
+ }
+ ),
+
+ chaiExist: defineSandboxFn(
+ ctx,
+ "chaiExist",
+ (value: SandboxValue, modifiers?: SandboxValue) => {
+ const mods = modifiers || " to"
+ executeChaiAssertion(
+ () => {
+ const assertion = applyModifiers(value, mods)
+ void assertion.exist
+ },
+ buildMessage(value, String(mods), "exist")
+ )
+ }
+ ),
+
+ // Emptiness assertions
+ chaiEmpty: defineSandboxFn(
+ ctx,
+ "chaiEmpty",
+ (
+ value: SandboxValue,
+ modifiers?: SandboxValue,
+ typeName?: unknown,
+ actualSize?: unknown
+ ) => {
+ const mods = modifiers || " to"
+ // Special handling for Set/Map
+ let isEmpty = false
+ let displayValue = value
+
+ if (typeName === "Set" || typeName === "Map") {
+ // Use pre-checked size from sandbox
+ isEmpty = actualSize === 0
+ if (actualSize === 0) {
+ displayValue = "{}"
+ } else {
+ // After serialization, Set/Map become {}, but we have the info they weren't empty
+ displayValue = value
+ }
+ } else if (value instanceof Set) {
+ isEmpty = value.size === 0
+ displayValue =
+ value.size === 0
+ ? "new Set()"
+ : `new Set([${Array.from(value).join(", ")}])`
+ } else if (value instanceof Map) {
+ isEmpty = value.size === 0
+ displayValue =
+ value.size === 0
+ ? "new Map()"
+ : `new Map([${Array.from(value.entries())
+ .map((entry: unknown) => {
+ const [key, value] = entry as [unknown, unknown]
+ return `[${key}, ${value}]`
+ })
+ .join(", ")}])`
+ } else if (Array.isArray(value)) {
+ isEmpty = value.length === 0
+ } else if (typeof value === "object" && value !== null) {
+ isEmpty = Object.keys(value).length === 0
+ } else if (typeof value === "string") {
+ isEmpty = value.length === 0
+ }
+ const isNegated = String(mods).includes("not")
+ const pass = isNegated ? !isEmpty : isEmpty
+ if (testStack.length === 0) return
+ testStack[testStack.length - 1].expectResults.push({
+ status: pass ? "pass" : "fail",
+ message: buildMessage(displayValue, mods, "empty"),
+ })
+ }
+ ),
+
+ // Inclusion assertions
+ chaiInclude: defineSandboxFn(
+ ctx,
+ "chaiInclude",
+ (value: SandboxValue, item: SandboxValue, modifiers?: SandboxValue) => {
+ const mods = modifiers || " to"
+ executeChaiAssertion(
+ () => applyModifiers(value, mods).include(item),
+ buildMessage(value, String(mods), "include", [item])
+ )
+ }
+ ),
+
+ chaiDeepInclude: defineSandboxFn(
+ ctx,
+ "chaiDeepInclude",
+ (value: SandboxValue, item: SandboxValue, modifiers?: SandboxValue) => {
+ const mods = modifiers || " to"
+ executeChaiAssertion(
+ () => applyModifiers(value, mods).deep.include(item),
+ buildMessage(value, String(mods), "deep include", [item])
+ )
+ }
+ ),
+
+ chaiIncludeKeys: defineSandboxFn(
+ ctx,
+ "chaiIncludeKeys",
+ (value: SandboxValue, keys: SandboxValue, modifiers?: SandboxValue) => {
+ const mods = modifiers || " to"
+ executeChaiAssertion(
+ () => {
+ const assertion = applyModifiers(value, mods)
+ assertion.include.keys(...keys)
+ },
+ buildMessage(value, String(mods), "include keys", keys)
+ )
+ }
+ ),
+
+ // Length assertions
+ chaiLengthOf: defineSandboxFn(
+ ctx,
+ "chaiLengthOf",
+ function (
+ value: SandboxValue,
+ length: unknown,
+ modifiers: SandboxValue,
+ methodName: SandboxValue,
+ actualSize?: unknown,
+ typeName?: unknown
+ ) {
+ const mods = (modifiers || " to") as string
+ const assertion = mods.trim().endsWith("has")
+ ? methodName || "lengthOf"
+ : `have ${methodName || "lengthOf"}`
+ if (actualSize !== undefined && typeName) {
+ if (testStack.length === 0) return
+ const matches = Number(actualSize) === Number(length)
+ const negated = mods.includes("not")
+ const pass = negated ? !matches : matches
+ let displayValue = value
+ if (typeof typeName === "string") {
+ if (typeName.includes("Set")) {
+ displayValue = `new Set([${Array.from(value).join(", ")}])`
+ } else if (typeName.includes("Map")) {
+ displayValue = `new Map([${Array.from(value.entries())
+ .map((entry: unknown) => {
+ const [key, value] = entry as [unknown, unknown]
+ return `[${key}, ${value}]`
+ })
+ .join(", ")}])`
+ }
+ }
+ testStack[testStack.length - 1].expectResults.push({
+ status: pass ? "pass" : "fail",
+ message: buildMessage(displayValue, mods, assertion, [length]),
+ })
+ } else if (value instanceof Set) {
+ if (testStack.length === 0) return
+ const matches = value.size === Number(length)
+ const negated = mods.includes("not")
+ const pass = negated ? !matches : matches
+ const displayValue = `new Set([${Array.from(value).join(", ")}])`
+ testStack[testStack.length - 1].expectResults.push({
+ status: pass ? "pass" : "fail",
+ message: buildMessage(displayValue, mods, assertion, [length]),
+ })
+ } else if (value instanceof Map) {
+ if (testStack.length === 0) return
+ const matches = value.size === Number(length)
+ const negated = mods.includes("not")
+ const pass = negated ? !matches : matches
+ const displayValue = `new Map([${Array.from(value.entries())
+ .map((entry: unknown) => {
+ const [key, value] = entry as [unknown, unknown]
+ return `[${key}, ${value}]`
+ })
+ .join(", ")}])`
+ testStack[testStack.length - 1].expectResults.push({
+ status: pass ? "pass" : "fail",
+ message: buildMessage(displayValue, mods, assertion, [length]),
+ })
+ } else {
+ // Handle negation for regular arrays/strings
+ const negated = mods.includes("not")
+ executeChaiAssertion(
+ () => {
+ const expectChain = chai.expect(value)
+ if (negated) {
+ expectChain.to.not.have.lengthOf(Number(length))
+ } else {
+ expectChain.to.have.lengthOf(Number(length))
+ }
+ },
+ buildMessage(value, String(mods), assertion, [length])
+ )
+ }
+ }
+ ),
+
+ // Property assertions
+ chaiProperty: defineSandboxFn(ctx, "chaiProperty", ((
+ value: SandboxValue,
+ property: SandboxValue,
+ propertyValue?: SandboxValue,
+ modifiers?: SandboxValue,
+ hasProperty?: boolean
+ ) => {
+ const mods = modifiers || " to"
+ const hasValue = propertyValue !== undefined
+
+ // PRE-CHECK PATTERN: Use property existence check from sandbox
+ // ONLY when checking existence (no value assertion)
+ if (
+ !hasValue &&
+ hasProperty !== undefined &&
+ typeof value === "object" &&
+ value !== null
+ ) {
+ const isNegated = String(mods).includes("not")
+ const shouldPass = isNegated ? !hasProperty : hasProperty
+
+ executeChaiAssertion(
+ () => {
+ if (!shouldPass) {
+ throw new Error(
+ `Expected ${formatValue(value)}${String(mods)} have property '${property}'`
+ )
+ }
+ },
+ buildMessage(value, String(mods), `property '${property}'`)
+ )
+ } else {
+ // For primitives, value assertions, or when property existence info not available
+ executeChaiAssertion(
+ () => {
+ const assertion = applyModifiers(value, mods)
+ if (hasValue) {
+ assertion.have.property(property, propertyValue)
+ } else {
+ assertion.have.property(property)
+ }
+ },
+ hasValue
+ ? buildMessage(value, mods, `property '${property}'`, [
+ propertyValue,
+ ])
+ : buildMessage(value, mods, `property '${property}'`)
+ )
+ }
+ }) as any),
+
+ chaiOwnProperty: defineSandboxFn(ctx, "chaiOwnProperty", ((
+ value: SandboxValue,
+ property: SandboxValue,
+ modifiers?: SandboxValue,
+ isOwnProperty?: boolean
+ ) => {
+ const mods = modifiers || " to"
+
+ // PRE-CHECK PATTERN: Use hasOwnProperty check from sandbox
+ // When prototype chain info is available, use it directly
+ if (
+ isOwnProperty !== undefined &&
+ typeof value === "object" &&
+ value !== null
+ ) {
+ const isNegated = String(mods).includes("not")
+ const shouldPass = isNegated ? !isOwnProperty : isOwnProperty
+
+ executeChaiAssertion(
+ () => {
+ if (!shouldPass) {
+ throw new Error(
+ `Expected ${formatValue(value)}${String(mods)} have own property '${property}'`
+ )
+ }
+ },
+ buildMessage(value, String(mods), `own property '${property}'`)
+ )
+ } else {
+ // For primitives or when hasOwnProperty info not available, use default Chai behavior
+ executeChaiAssertion(
+ () => {
+ const assertion = applyModifiers(value, mods)
+ assertion.have.own.property(property)
+ },
+ buildMessage(value, String(mods), `own property '${property}'`)
+ )
+ }
+ }) as any),
+
+ chaiDeepOwnProperty: defineSandboxFn(
+ ctx,
+ "chaiDeepOwnProperty",
+ (
+ value: SandboxValue,
+ property: SandboxValue,
+ propertyValue: SandboxValue,
+ modifiers?: SandboxValue
+ ) => {
+ const mods = modifiers || " to"
+ executeChaiAssertion(
+ () =>
+ chai
+ .expect(value)
+ .to.have.deep.own.property(property, propertyValue),
+ buildMessage(value, String(mods), `property '${property}'`, [
+ propertyValue,
+ ])
+ )
+ }
+ ),
+
+ chaiNestedProperty: defineSandboxFn(
+ ctx,
+ "chaiNestedProperty",
+ (
+ value: SandboxValue,
+ property: SandboxValue,
+ propertyValue?: SandboxValue,
+ modifiers?: SandboxValue
+ ) => {
+ const mods = modifiers || " to"
+ const hasValue = propertyValue !== undefined
+ executeChaiAssertion(
+ () => {
+ const assertion = applyModifiers(value, mods)
+ if (hasValue) {
+ assertion.have.nested.property(property, propertyValue)
+ } else {
+ assertion.have.nested.property(property)
+ }
+ },
+ hasValue
+ ? buildMessage(value, mods, `property '${property}'`, [
+ propertyValue,
+ ])
+ : buildMessage(value, mods, `property '${property}'`)
+ )
+ }
+ ),
+
+ chaiNestedInclude: defineSandboxFn(
+ ctx,
+ "chaiNestedInclude",
+ (value: SandboxValue, obj: unknown, modifiers?: SandboxValue) => {
+ const mods = modifiers || " to"
+ executeChaiAssertion(
+ () => {
+ const assertion = applyModifiers(value, mods)
+ assertion.nested.include(obj)
+ },
+ buildMessage(value, String(mods), "nested include", [obj])
+ )
+ }
+ ),
+
+ // Numerical comparisons
+ chaiAbove: defineSandboxFn(
+ ctx,
+ "chaiAbove",
+ (value: SandboxValue, n: unknown, modifiers?: SandboxValue) => {
+ const mods = modifiers || " to"
+ executeChaiAssertion(
+ () => applyModifiers(value, mods).be.above(n),
+ buildMessage(value, String(mods), "above", [n])
+ )
+ }
+ ),
+
+ chaiBelow: defineSandboxFn(
+ ctx,
+ "chaiBelow",
+ (value: SandboxValue, n: unknown, modifiers?: SandboxValue) => {
+ const mods = modifiers || " to"
+ executeChaiAssertion(
+ () => applyModifiers(value, mods).be.below(n),
+ buildMessage(value, String(mods), "below", [n])
+ )
+ }
+ ),
+
+ chaiAtLeast: defineSandboxFn(
+ ctx,
+ "chaiAtLeast",
+ (value: SandboxValue, n: unknown, modifiers?: SandboxValue) => {
+ const mods = modifiers || " to"
+ executeChaiAssertion(
+ () => applyModifiers(value, mods).be.at.least(n),
+ buildMessage(value, String(mods), "at least", [n])
+ )
+ }
+ ),
+
+ chaiAtMost: defineSandboxFn(
+ ctx,
+ "chaiAtMost",
+ (value: SandboxValue, n: unknown, modifiers?: SandboxValue) => {
+ const mods = modifiers || " to"
+ executeChaiAssertion(
+ () => applyModifiers(value, mods).be.at.most(n),
+ buildMessage(value, String(mods), "at most", [n])
+ )
+ }
+ ),
+
+ chaiWithin: defineSandboxFn(
+ ctx,
+ "chaiWithin",
+ (
+ value: SandboxValue,
+ start: unknown,
+ end: unknown,
+ modifiers?: SandboxValue
+ ) => {
+ const mods = modifiers || " to"
+ executeChaiAssertion(
+ () => applyModifiers(value, mods).be.within(start, end),
+ buildMessage(value, String(mods), "within", [start, end])
+ )
+ }
+ ),
+
+ chaiCloseTo: defineSandboxFn(
+ ctx,
+ "chaiCloseTo",
+ (
+ value: SandboxValue,
+ expected: SandboxValue,
+ delta: unknown,
+ modifiers?: SandboxValue,
+ methodName?: unknown
+ ) => {
+ const mods = modifiers || " to"
+ const method = methodName || "closeTo"
+ executeChaiAssertion(
+ () => applyModifiers(value, mods).be.closeTo(expected, delta),
+ buildMessage(value, String(mods), String(method), [expected, delta])
+ )
+ }
+ ),
+
+ // Keys assertions
+ chaiKeys: defineSandboxFn(
+ ctx,
+ "chaiKeys",
+ (value: SandboxValue, keys: SandboxValue, modifiers?: SandboxValue) => {
+ const mods = modifiers || " to"
+ executeChaiAssertion(
+ () => {
+ const assertion = applyModifiers(value, mods)
+ assertion.have.keys(...keys)
+ },
+ buildMessage(value, String(mods), "keys", keys)
+ )
+ }
+ ),
+
+ chaiAllKeys: defineSandboxFn(
+ ctx,
+ "chaiAllKeys",
+ (value: SandboxValue, keys: SandboxValue, modifiers?: SandboxValue) => {
+ const mods = modifiers || " to"
+ executeChaiAssertion(
+ () => {
+ const assertion = applyModifiers(value, mods)
+ assertion.have.all.keys(...keys)
+ },
+ buildMessage(value, String(mods), "keys", keys)
+ )
+ }
+ ),
+
+ chaiAnyKeys: defineSandboxFn(
+ ctx,
+ "chaiAnyKeys",
+ (value: SandboxValue, keys: SandboxValue, modifiers?: SandboxValue) => {
+ const mods = modifiers || " to"
+ executeChaiAssertion(
+ () => {
+ const assertion = applyModifiers(value, mods)
+ assertion.have.any.keys(...keys)
+ },
+ buildMessage(value, String(mods), "keys", keys)
+ )
+ }
+ ),
+
+ // String/Pattern matching
+ chaiMatch: defineSandboxFn(
+ ctx,
+ "chaiMatch",
+ function (
+ value: SandboxValue,
+ pattern: unknown,
+ modifiers: SandboxValue,
+ regexSource?: unknown,
+ regexFlags?: unknown
+ ) {
+ const mods = modifiers || " to"
+ let actualPattern = pattern
+ let displayPattern = pattern
+ if (regexSource !== undefined) {
+ actualPattern = new RegExp(
+ String(regexSource),
+ String(regexFlags || "")
+ )
+ displayPattern = `/${regexSource}/${regexFlags || ""}`
+ }
+ const isNegated = String(mods).includes("not")
+ let matched = false
+ try {
+ matched = Boolean(
+ actualPattern instanceof RegExp
+ ? actualPattern.test(value)
+ : String(value).match(String(actualPattern))
+ )
+ } catch {
+ matched = false
+ }
+ const pass = isNegated ? !matched : matched
+ if (testStack.length === 0) return
+ const displayValue = typeof value === "string" ? value : String(value)
+ const notStr = isNegated ? " not" : ""
+ testStack[testStack.length - 1].expectResults.push({
+ status: pass ? "pass" : "fail",
+ message: `Expected '${displayValue}' to${notStr} match ${displayPattern}`,
+ })
+ }
+ ),
+ chaiString: defineSandboxFn(
+ ctx,
+ "chaiString",
+ function (
+ value: SandboxValue,
+ substring: unknown,
+ modifiers?: SandboxValue
+ ) {
+ const mods = String(modifiers || " to")
+ const isNegated = mods.includes("not")
+ const valueStr = String(value)
+ const hasSubstring = valueStr.includes(String(substring))
+ const shouldPass = isNegated ? !hasSubstring : hasSubstring
+
+ if (testStack.length === 0) return
+
+ testStack[testStack.length - 1].expectResults.push({
+ status: shouldPass ? "pass" : "fail",
+ message: buildMessage(value, mods, "have string", [`'${substring}'`]),
+ })
+ }
+ ),
+
+ // Members assertions
+ chaiMembers: defineSandboxFn(
+ ctx,
+ "chaiMembers",
+ (
+ value: SandboxValue,
+ members: SandboxValue,
+ modifiers?: SandboxValue
+ ) => {
+ const mods = modifiers || " to"
+ executeChaiAssertion(
+ () => {
+ const assertion = applyModifiers(value, mods)
+ assertion.have.members(members)
+ },
+ buildMessage(value, String(mods), "members", [members])
+ )
+ }
+ ),
+
+ chaiIncludeMembers: defineSandboxFn(
+ ctx,
+ "chaiIncludeMembers",
+ (
+ value: SandboxValue,
+ members: SandboxValue,
+ modifiers?: SandboxValue
+ ) => {
+ const mods = modifiers || " to"
+ executeChaiAssertion(
+ () => {
+ const assertion = applyModifiers(value, mods)
+ assertion.include.members(members)
+ },
+ buildMessage(value, String(mods), "include members", [members])
+ )
+ }
+ ),
+
+ chaiOrderedMembers: defineSandboxFn(
+ ctx,
+ "chaiOrderedMembers",
+ (
+ value: SandboxValue,
+ members: SandboxValue,
+ modifiers?: SandboxValue
+ ) => {
+ const mods = modifiers || " to"
+ executeChaiAssertion(
+ () => {
+ const assertion = applyModifiers(value, mods)
+ assertion.have.ordered.members(members)
+ },
+ buildMessage(value, String(mods), "members", [...members])
+ )
+ }
+ ),
+
+ chaiDeepMembers: defineSandboxFn(
+ ctx,
+ "chaiDeepMembers",
+ (
+ value: SandboxValue,
+ members: SandboxValue,
+ modifiers?: SandboxValue
+ ) => {
+ const mods = modifiers || " to"
+ executeChaiAssertion(
+ () => {
+ const assertion = applyModifiers(value, mods)
+ assertion.have.deep.members(members)
+ },
+ buildMessage(value, String(mods), "members", [members])
+ )
+ }
+ ),
+
+ // OneOf assertion
+ chaiOneOf: defineSandboxFn(
+ ctx,
+ "chaiOneOf",
+ (value: SandboxValue, list: unknown, modifiers?: SandboxValue) => {
+ const mods = modifiers || " to"
+ const isInclude = String(mods).includes("include")
+ const assertion = isInclude ? "include oneOf" : "oneOf"
+ executeChaiAssertion(
+ () => {
+ const assertion = applyModifiers(value, mods)
+ if (isInclude) {
+ assertion.include.oneOf(list)
+ } else {
+ assertion.be.oneOf(list)
+ }
+ },
+ buildMessage(value, String(mods), assertion, [list])
+ )
+ }
+ ),
+
+ // Object state assertions
+ // PRE-CHECKING PATTERN: Check object state BEFORE serialization
+ // The pre-checking is done in post-request.js (sandbox context) and passed as a third parameter
+ // This is because after serialization, objects lose their frozen/sealed/extensible state
+ chaiExtensible: defineSandboxFn(
+ ctx,
+ "chaiExtensible",
+ (value, modifiers, preCheckedResult) => {
+ const mods = modifiers || " to"
+ const isNegated = String(mods).includes("not")
+ // Use the pre-checked result from sandbox (before serialization)
+ const isExtensible =
+ preCheckedResult !== undefined
+ ? preCheckedResult
+ : Object.isExtensible(value)
+ const shouldPass = isNegated ? !isExtensible : isExtensible
+
+ executeChaiAssertion(
+ () => {
+ if (!shouldPass) {
+ throw new Error(
+ `Expected ${formatValue(value)}${mods} be extensible`
+ )
+ }
+ },
+ // For buildMessage calls for extensible, sealed, frozen, pass "{}" if value is an empty object, otherwise String(value)
+ buildMessage(
+ Object.keys(value as object).length === 0 ? "{}" : String(value),
+ String(mods),
+ "extensible"
+ )
+ )
+ }
+ ),
+
+ chaiSealed: defineSandboxFn(
+ ctx,
+ "chaiSealed",
+ (value, modifiers, preCheckedResult) => {
+ const mods = modifiers || " to"
+ const isNegated = String(mods).includes("not")
+ // Use the pre-checked result from sandbox (before serialization)
+ const isSealed =
+ preCheckedResult !== undefined
+ ? preCheckedResult
+ : Object.isSealed(value)
+ const shouldPass = isNegated ? !isSealed : isSealed
+
+ executeChaiAssertion(
+ () => {
+ if (!shouldPass) {
+ throw new Error(`Expected ${formatValue(value)}${mods} be sealed`)
+ }
+ },
+ // For buildMessage calls for extensible, sealed, frozen, pass "{}" if value is an empty object, otherwise String(value)
+ buildMessage(
+ Object.keys(value as object).length === 0 ? "{}" : String(value),
+ String(mods),
+ "sealed"
+ )
+ )
+ }
+ ),
+
+ chaiFrozen: defineSandboxFn(
+ ctx,
+ "chaiFrozen",
+ (value, modifiers, preCheckedResult) => {
+ const mods = modifiers || " to"
+ const isNegated = String(mods).includes("not")
+ // Use the pre-checked result from sandbox (before serialization)
+ const isFrozen =
+ preCheckedResult !== undefined
+ ? preCheckedResult
+ : Object.isFrozen(value)
+ const shouldPass = isNegated ? !isFrozen : isFrozen
+
+ executeChaiAssertion(
+ () => {
+ if (!shouldPass) {
+ throw new Error(`Expected ${formatValue(value)}${mods} be frozen`)
+ }
+ },
+ // For buildMessage calls for extensible, sealed, frozen, pass "{}" if value is an empty object, otherwise String(value)
+ buildMessage(
+ Object.keys(value as object).length === 0 ? "{}" : String(value),
+ String(mods),
+ "frozen"
+ )
+ )
+ }
+ ),
+
+ // Number state assertions
+ chaiFinite: defineSandboxFn(
+ ctx,
+ "chaiFinite",
+ (value: SandboxValue, modifiers?: SandboxValue) => {
+ const mods = modifiers || " to"
+ executeChaiAssertion(
+ () => {
+ const assertion = applyModifiers(value, mods)
+ void assertion.be.finite
+ },
+ buildMessage(value, String(mods), "finite")
+ )
+ }
+ ),
+
+ // Arguments object assertion
+ chaiArguments: defineSandboxFn(
+ ctx,
+ "chaiArguments",
+ (
+ value: SandboxValue,
+ modifiers?: SandboxValue,
+ preCheckedResult?: unknown
+ ) => {
+ const mods = modifiers || " to"
+ const isNegated = String(mods).includes("not")
+ const isArguments =
+ preCheckedResult !== undefined
+ ? preCheckedResult
+ : Object.prototype.toString.call(value) === "[object Arguments]"
+ const shouldPass = isNegated ? !isArguments : isArguments
+ // Extract "arguments" or "Arguments" from modifiers
+ const assertionName =
+ mods.match(/\b(arguments|Arguments)\b/)?.[1] || "arguments"
+ if (testStack.length === 0) return
+ testStack[testStack.length - 1].expectResults.push({
+ status: shouldPass ? "pass" : "fail",
+ message: buildMessage(value, mods, assertionName),
+ })
+ }
+ ),
+
+ // Property descriptor assertion
+ chaiOwnPropertyDescriptor: defineSandboxFn(
+ ctx,
+ "chaiOwnPropertyDescriptor",
+ function (
+ _value,
+ prop,
+ descriptor,
+ modifiers,
+ hasDescriptor,
+ matchesExpected
+ ) {
+ const mods = modifiers || " to"
+ const isNegated = String(mods).includes("not")
+
+ // PRE-CHECK PATTERN: Use pre-checked results from sandbox
+ // After serialization, property descriptors lose metadata
+ let shouldPass = false
+
+ if (descriptor !== undefined) {
+ // When descriptor comparison is provided
+ if (isNegated) {
+ shouldPass = !hasDescriptor || !matchesExpected
+ } else {
+ shouldPass = Boolean(hasDescriptor && matchesExpected)
+ }
+ } else {
+ // When only checking for existence
+ if (isNegated) {
+ shouldPass = !hasDescriptor
+ } else {
+ shouldPass = Boolean(hasDescriptor)
+ }
+ }
+
+ if (testStack.length === 0) return
+ testStack[testStack.length - 1].expectResults.push({
+ status: shouldPass ? "pass" : "fail",
+ message: `Expected {}${mods} ownPropertyDescriptor '${prop}'`,
+ })
+ }
+ ),
+
+ // Include deep ordered members
+ chaiIncludeDeepOrderedMembers: defineSandboxFn(
+ ctx,
+ "chaiIncludeDeepOrderedMembers",
+ function (
+ value: SandboxValue,
+ members: SandboxValue,
+ modifiers?: SandboxValue
+ ) {
+ const mods = modifiers || " to"
+ const isNegated = String(mods).includes("not")
+ let pass = false
+ try {
+ chai.expect(value).to.include.deep.ordered.members(members)
+ pass = !isNegated
+ } catch {
+ pass = isNegated
+ }
+ if (testStack.length === 0) return
+ testStack[testStack.length - 1].expectResults.push({
+ status: pass ? "pass" : "fail",
+ message: buildMessage(value, mods, "members", [...members]),
+ })
+ }
+ ),
+
+ chaiRespondTo: defineSandboxFn(
+ ctx,
+ "chaiRespondTo",
+ function (value, method, modifiers, preCheckedResult) {
+ const mods = modifiers || " to"
+ const isNegated = String(mods).includes("not")
+ // PRE-CHECK PATTERN: Use pre-checked result from sandbox
+ // After serialization, functions become strings and method checks fail
+ const hasMethod =
+ preCheckedResult !== undefined
+ ? preCheckedResult
+ : typeof (value as any)?.[method as string] === "function" ||
+ typeof (value as any)?.prototype?.[method as string] ===
+ "function"
+ const shouldPass = isNegated ? !hasMethod : hasMethod
+ executeChaiAssertion(
+ () => {
+ if (!shouldPass) {
+ throw new Error(
+ `Expected ${formatValue(value)}${String(mods)} respondTo '${method}'`
+ )
+ }
+ },
+ buildMessage(value, String(mods), `respondTo '${method}'`)
+ )
+ }
+ ),
+
+ // Function throw assertion
+ chaiThrow: defineSandboxFn(
+ ctx,
+ "chaiThrow",
+ function (
+ fn: unknown,
+ threwError: unknown,
+ errorTypeName: unknown,
+ errorMessage: unknown,
+ expectedTypeName: unknown,
+ errMsgMatcher: unknown,
+ regexSource: unknown,
+ regexFlags: unknown,
+ isRegexMatcher: unknown,
+ modifiers: SandboxValue
+ ) {
+ const mods = String(modifiers || " to")
+ const isNegated = mods.includes("not")
+
+ // Determine what we're checking for
+ const hasErrorType =
+ expectedTypeName !== null && expectedTypeName !== undefined
+ const hasErrorMessage =
+ errMsgMatcher !== undefined && errMsgMatcher !== null
+ const hasRegexPattern = Boolean(isRegexMatcher)
+
+ // Check if assertion should pass
+ let shouldPass = false
+
+ if (isNegated) {
+ // For negated (.not.throw), logic depends on what's being checked
+ if (hasErrorType) {
+ // .not.throw(ErrorType) should pass if:
+ // 1. Didn't throw at all, OR
+ // 2. Threw a different error type
+ shouldPass =
+ !threwError || String(errorTypeName) !== String(expectedTypeName)
+ } else if (hasErrorMessage || hasRegexPattern) {
+ // .not.throw('message') should pass if:
+ // 1. Didn't throw at all, OR
+ // 2. Threw with a different message
+ if (!threwError) {
+ shouldPass = true
+ } else if (hasRegexPattern) {
+ const regex = new RegExp(
+ String(regexSource),
+ String(regexFlags || "")
+ )
+ shouldPass = !regex.test(String(errorMessage || ""))
+ } else {
+ const errMsg = String(errorMessage || "")
+ const matchMsg = String(errMsgMatcher || "")
+ shouldPass = errMsg !== matchMsg && !errMsg.includes(matchMsg)
+ }
+ } else {
+ // .not.throw() with no args should pass if did NOT throw
+ shouldPass = !threwError
+ }
+ } else {
+ // For positive assertion, should pass if threw error
+ shouldPass = Boolean(threwError)
+
+ // Additional checks if error was thrown
+ if (threwError && hasErrorType) {
+ shouldPass =
+ shouldPass && String(errorTypeName) === String(expectedTypeName)
+ }
+
+ if (threwError && hasErrorMessage && !hasRegexPattern) {
+ // String message match
+ const errMsg = String(errorMessage || "")
+ const matchMsg = String(errMsgMatcher || "")
+ shouldPass =
+ shouldPass && (errMsg === matchMsg || errMsg.includes(matchMsg))
+ }
+
+ if (threwError && hasRegexPattern) {
+ // RegExp message match
+ const regex = new RegExp(
+ String(regexSource),
+ String(regexFlags || "")
+ )
+ shouldPass = shouldPass && regex.test(String(errorMessage || ""))
+ }
+ }
+
+ // Build appropriate message
+ const messageArgs: string[] = []
+ if (hasErrorType && hasErrorMessage) {
+ if (hasRegexPattern) {
+ messageArgs.push(
+ String(expectedTypeName),
+ `/${regexSource}/${regexFlags || ""}`
+ )
+ } else {
+ messageArgs.push(String(expectedTypeName), String(errMsgMatcher))
+ }
+ } else if (hasErrorType) {
+ messageArgs.push(String(expectedTypeName))
+ } else if (hasErrorMessage) {
+ if (hasRegexPattern) {
+ messageArgs.push(`/${regexSource}/${regexFlags || ""}`)
+ } else {
+ messageArgs.push(String(errMsgMatcher))
+ }
+ }
+
+ if (testStack.length === 0) return
+ testStack[testStack.length - 1].expectResults.push({
+ status: shouldPass ? "pass" : "fail",
+ message: buildMessage(fn, mods, "throw", messageArgs.filter(Boolean)),
+ })
+ }
+ ),
+
+ // Function satisfy assertion
+ chaiSatisfy: defineSandboxFn(
+ ctx,
+ "chaiSatisfy",
+ function (
+ value: SandboxValue,
+ satisfyResult: unknown,
+ matcherString: unknown,
+ modifiers: SandboxValue
+ ) {
+ const mods = String(modifiers || " to")
+ const isNegated = mods.includes("not")
+ const passed = Boolean(satisfyResult)
+ const shouldPass = isNegated ? !passed : passed
+
+ if (testStack.length === 0) return
+ testStack[testStack.length - 1].expectResults.push({
+ status: shouldPass ? "pass" : "fail",
+ message: buildMessage(value, mods, "satisfy", [
+ String(matcherString),
+ ]),
+ })
+ }
+ ),
+
+ // change/increase/decrease assertions (pre-check pattern - function already executed in sandbox)
+ chaiChange: defineSandboxFn(
+ ctx,
+ "chaiChange",
+ function (prop, modifiers, changed, _delta) {
+ const mods = String(modifiers || " to")
+ const isNegated = mods.includes("not")
+ const shouldPass = isNegated ? !changed : changed
+
+ if (testStack.length === 0) return
+
+ testStack[testStack.length - 1].expectResults.push({
+ status: shouldPass ? "pass" : "fail",
+ message: `Expected [Function]${mods} change {}.'${prop}'`,
+ })
+ }
+ ),
+
+ chaiChangeBy: defineSandboxFn(
+ ctx,
+ "chaiChangeBy",
+ function (prop, modifiers, changed, delta, expectedDelta) {
+ const mods = String(modifiers || " to")
+ const isNegated = mods.includes("not")
+ const numDelta = Number(delta)
+ const actualDelta = Math.abs(numDelta)
+ const numExpectedDelta = Number(expectedDelta)
+ const deltaMatches =
+ Math.abs(actualDelta - Math.abs(numExpectedDelta)) < 0.0001
+ const byPasses = changed && deltaMatches
+ const byShouldPass = isNegated ? !byPasses : byPasses
+
+ if (testStack.length === 0) return
+
+ // Update the last result (from chaiChange)
+ const lastResult =
+ testStack[testStack.length - 1].expectResults[
+ testStack[testStack.length - 1].expectResults.length - 1
+ ]
+ lastResult.status = byShouldPass ? "pass" : "fail"
+ lastResult.message = `Expected [Function]${mods} change {}.'${prop}' by ${numExpectedDelta}`
+ }
+ ),
+
+ chaiIncrease: defineSandboxFn(
+ ctx,
+ "chaiIncrease",
+ function (prop, modifiers, increased, _delta) {
+ const mods = String(modifiers || " to")
+ const isNegated = mods.includes("not")
+ const shouldPass = isNegated ? !increased : increased
+
+ if (testStack.length === 0) return
+
+ testStack[testStack.length - 1].expectResults.push({
+ status: shouldPass ? "pass" : "fail",
+ message: `Expected [Function]${mods} increase {}.'${prop}'`,
+ })
+ }
+ ),
+
+ chaiIncreaseBy: defineSandboxFn(
+ ctx,
+ "chaiIncreaseBy",
+ function (prop, modifiers, increased, delta, expectedDelta) {
+ const mods = String(modifiers || " to")
+ const isNegated = mods.includes("not")
+ const numDelta = Number(delta)
+ const numExpectedDelta = Number(expectedDelta)
+ const deltaMatches = Math.abs(numDelta - numExpectedDelta) < 0.0001
+ const byPasses = increased && deltaMatches
+ const byShouldPass = isNegated ? !byPasses : byPasses
+
+ if (testStack.length === 0) return
+
+ // Update the last result (from chaiIncrease)
+ const lastResult =
+ testStack[testStack.length - 1].expectResults[
+ testStack[testStack.length - 1].expectResults.length - 1
+ ]
+ lastResult.status = byShouldPass ? "pass" : "fail"
+ lastResult.message = `Expected [Function]${mods} increase {}.'${prop}' by ${numExpectedDelta}`
+ }
+ ),
+
+ chaiDecrease: defineSandboxFn(
+ ctx,
+ "chaiDecrease",
+ function (prop, modifiers, decreased, _delta) {
+ const mods = String(modifiers || " to")
+ const isNegated = mods.includes("not")
+ const shouldPass = isNegated ? !decreased : decreased
+
+ if (testStack.length === 0) return
+
+ testStack[testStack.length - 1].expectResults.push({
+ status: shouldPass ? "pass" : "fail",
+ message: `Expected [Function]${mods} decrease {}.'${prop}'`,
+ })
+ }
+ ),
+
+ chaiDecreaseBy: defineSandboxFn(
+ ctx,
+ "chaiDecreaseBy",
+ function (prop, modifiers, decreased, delta, expectedDelta) {
+ const mods = String(modifiers || " to")
+ const isNegated = mods.includes("not")
+ const numDelta = Number(delta)
+ const numExpectedDelta = Number(expectedDelta)
+ const deltaMatches =
+ Math.abs(Math.abs(numDelta) - numExpectedDelta) < 0.0001
+ const byPasses = decreased && deltaMatches
+ const byShouldPass = isNegated ? !byPasses : byPasses
+
+ if (testStack.length === 0) return
+
+ // Update the last result (from chaiDecrease)
+ const lastResult =
+ testStack[testStack.length - 1].expectResults[
+ testStack[testStack.length - 1].expectResults.length - 1
+ ]
+ lastResult.status = byShouldPass ? "pass" : "fail"
+ lastResult.message = `Expected [Function]${mods} decrease {}.'${prop}' by ${numExpectedDelta}`
+ }
+ ),
+
+ // Custom Postman-specific methods
+ chaiJsonSchema: defineSandboxFn(
+ ctx,
+ "chaiJsonSchema",
+ function (
+ value: SandboxValue,
+ schema: SandboxValue,
+ modifiers: SandboxValue
+ ) {
+ const mods = modifiers || " to"
+ const isNegated = String(mods).includes("not")
+
+ // Validation helper
+ const validateSchema = (
+ data: SandboxValue,
+ schema: SandboxValue
+ ): boolean => {
+ // Type validation
+ if (schema.type !== undefined) {
+ const actualType = Array.isArray(data) ? "array" : typeof data
+ if (actualType !== schema.type) return false
+ }
+
+ // Required properties
+ if (schema.required && Array.isArray(schema.required)) {
+ for (const key of schema.required) {
+ if (!(key in data)) return false
+ }
+ }
+
+ // Properties validation
+ if (schema.properties && typeof data === "object" && data !== null) {
+ for (const key in schema.properties) {
+ if (key in data) {
+ const propSchema = schema.properties[key]
+ if (!validateSchema(data[key], propSchema)) return false
+ }
+ }
+ }
+
+ return true
+ }
+
+ const passes = validateSchema(value, schema)
+ const shouldPass = isNegated ? !passes : passes
+
+ if (testStack.length === 0) return
+
+ testStack[testStack.length - 1].expectResults.push({
+ status: shouldPass ? "pass" : "fail",
+ message: buildMessage(value, mods, "jsonSchema", [schema]),
+ })
+ }
+ ),
+
+ chaiCharset: defineSandboxFn(
+ ctx,
+ "chaiCharset",
+ function (
+ value: SandboxValue,
+ expectedCharset: unknown,
+ modifiers: SandboxValue
+ ) {
+ const mods = modifiers || " to"
+ const isNegated = String(mods).includes("not")
+
+ // For response body testing, check Content-Type header charset
+ const passes = typeof value === "string" // Simplified check
+
+ const shouldPass = isNegated ? !passes : passes
+
+ if (testStack.length === 0) return
+
+ testStack[testStack.length - 1].expectResults.push({
+ status: shouldPass ? "pass" : "fail",
+ message: buildMessage(value, mods, "charset", [expectedCharset]),
+ })
+ }
+ ),
+
+ chaiCookie: defineSandboxFn(
+ ctx,
+ "chaiCookie",
+ function (
+ value: SandboxValue,
+ cookieName: SandboxValue,
+ cookieValue: unknown,
+ modifiers: SandboxValue
+ ) {
+ const mods = modifiers || " to"
+ const isNegated = String(mods).includes("not")
+
+ // Simplified: check if cookie-like structure has name
+ const hasCookie =
+ typeof value === "object" && value !== null && cookieName in value
+ const valueMatches =
+ cookieValue === undefined || value[cookieName] === cookieValue
+
+ const passes = hasCookie && valueMatches
+ const shouldPass = isNegated ? !passes : passes
+
+ if (testStack.length === 0) return
+
+ const args =
+ cookieValue !== undefined ? [cookieName, cookieValue] : [cookieName]
+ testStack[testStack.length - 1].expectResults.push({
+ status: shouldPass ? "pass" : "fail",
+ message: buildMessage(value, mods, "cookie", args),
+ })
+ }
+ ),
+
+ chaiJsonPath: defineSandboxFn(
+ ctx,
+ "chaiJsonPath",
+ function (
+ value: SandboxValue,
+ path: SandboxValue,
+ expectedValue: SandboxValue,
+ modifiers: SandboxValue
+ ) {
+ const mods = modifiers || " to"
+ const isNegated = String(mods).includes("not")
+
+ // Enhanced JSONPath evaluation (supports $.path, array indices, wildcards)
+ const evaluatePath = (data: SandboxValue, path: string): unknown => {
+ let pathStr = String(path).trim()
+
+ // Remove leading $. if present
+ if (pathStr.startsWith("$.")) {
+ pathStr = pathStr.substring(2)
+ } else if (pathStr.startsWith("$")) {
+ pathStr = pathStr.substring(1)
+ }
+
+ // Handle empty path (just "$")
+ if (!pathStr || pathStr === "") {
+ return data
+ }
+
+ let current: unknown = data
+ const segments = pathStr.split(/\.|\[/).filter(Boolean)
+
+ for (let segment of segments) {
+ // Remove trailing ] for array indices
+ segment = segment.replace(/\]$/, "")
+
+ if (segment === "*") {
+ // Wildcard - return array of all values
+ if (Array.isArray(current)) {
+ return current // For array[*], return the array itself
+ } else if (typeof current === "object" && current !== null) {
+ return Object.values(current)
+ }
+ return undefined
+ } else if (/^\d+$/.test(segment)) {
+ // Numeric index
+ const index = parseInt(segment, 10)
+ if (
+ Array.isArray(current) &&
+ index >= 0 &&
+ index < current.length
+ ) {
+ current = current[index]
+ } else {
+ return undefined
+ }
+ } else {
+ // Object property
+ if (
+ current &&
+ typeof current === "object" &&
+ segment in current
+ ) {
+ current = (current as Record)[segment]
+ } else {
+ return undefined
+ }
+ }
+ }
+
+ return current
+ }
+
+ const actualValue = evaluatePath(value, path)
+ const passes =
+ expectedValue === undefined
+ ? actualValue !== undefined
+ : actualValue === expectedValue
+
+ const shouldPass = isNegated ? !passes : passes
+
+ if (testStack.length === 0) return
+
+ const args =
+ expectedValue !== undefined ? [path, expectedValue] : [path]
+ testStack[testStack.length - 1].expectResults.push({
+ status: shouldPass ? "pass" : "fail",
+ message: buildMessage(value, mods, "jsonPath", args),
+ })
+ }
+ ),
+
+ // expect.fail() - Force a test failure
+ // Supports multiple signatures:
+ // expect.fail()
+ // expect.fail(message)
+ // expect.fail(actual, expected)
+ // expect.fail(actual, expected, message)
+ // expect.fail(actual, expected, message, operator)
+ chaiFail: defineSandboxFn(ctx, "chaiFail", (...args: unknown[]) => {
+ if (testStack.length === 0) return
+
+ const [actual, expected, message, operator] = args
+ let errorMessage: string
+
+ // Handle different call signatures
+ if (actual === undefined && expected === undefined) {
+ // expect.fail() - no arguments
+ errorMessage = "expect.fail()"
+ } else if (
+ expected === undefined &&
+ message === undefined &&
+ operator === undefined
+ ) {
+ // expect.fail(message) - single string argument
+ errorMessage = typeof actual === "string" ? actual : "expect.fail()"
+ } else {
+ // expect.fail(actual, expected) or full signature
+ errorMessage =
+ (message as string) ||
+ `expected ${actual !== undefined ? actual : "undefined"} to ${(operator as string) || "equal"} ${expected !== undefined ? expected : "undefined"}`
+ }
+
+ // Always record as failure
+ testStack[testStack.length - 1].expectResults.push({
+ status: "fail",
+ message: errorMessage,
+ })
+ }),
+ }
+}
diff --git a/packages/hoppscotch-js-sandbox/src/cage-modules/utils/expectation-helpers.ts b/packages/hoppscotch-js-sandbox/src/cage-modules/utils/expectation-helpers.ts
index 5f369d1b..4d022ae5 100644
--- a/packages/hoppscotch-js-sandbox/src/cage-modules/utils/expectation-helpers.ts
+++ b/packages/hoppscotch-js-sandbox/src/cage-modules/utils/expectation-helpers.ts
@@ -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)
}
),
diff --git a/packages/hoppscotch-js-sandbox/src/cage-modules/utils/request-setters.ts b/packages/hoppscotch-js-sandbox/src/cage-modules/utils/request-setters.ts
index 05273f5b..d7b73791 100644
--- a/packages/hoppscotch-js-sandbox/src/cage-modules/utils/request-setters.ts
+++ b/packages/hoppscotch-js-sandbox/src/cage-modules/utils/request-setters.ts
@@ -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)
}
),
diff --git a/packages/hoppscotch-js-sandbox/src/constants/http-status-codes.ts b/packages/hoppscotch-js-sandbox/src/constants/http-status-codes.ts
new file mode 100644
index 00000000..7f88a712
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/constants/http-status-codes.ts
@@ -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> = {
+ // 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
+}
diff --git a/packages/hoppscotch-js-sandbox/src/constants/sandbox-markers.ts b/packages/hoppscotch-js-sandbox/src/constants/sandbox-markers.ts
new file mode 100644
index 00000000..67756a7d
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/constants/sandbox-markers.ts
@@ -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
+}
diff --git a/packages/hoppscotch-js-sandbox/src/types/index.ts b/packages/hoppscotch-js-sandbox/src/types/index.ts
index 4268538e..f7979da7 100644
--- a/packages/hoppscotch-js-sandbox/src/types/index.ts
+++ b/packages/hoppscotch-js-sandbox/src/types/index.ts
@@ -6,6 +6,34 @@ import type { EnvAPIOptions } from "~/utils/shared"
// Infer the return type of defineSandboxFn from faraday-cage
type SandboxFunction = ReturnType
+/**
+ * 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
}
diff --git a/packages/hoppscotch-js-sandbox/src/utils/chai-integration.ts b/packages/hoppscotch-js-sandbox/src/utils/chai-integration.ts
new file mode 100644
index 00000000..c8e2c43b
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/utils/chai-integration.ts
@@ -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)
+}
diff --git a/packages/hoppscotch-js-sandbox/src/utils/pre-request.ts b/packages/hoppscotch-js-sandbox/src/utils/pre-request.ts
index ba4383ea..206e1ae1 100644
--- a/packages/hoppscotch-js-sandbox/src/utils/pre-request.ts
+++ b/packages/hoppscotch-js-sandbox/src/utils/pre-request.ts
@@ -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) => {
diff --git a/packages/hoppscotch-js-sandbox/src/utils/shared.ts b/packages/hoppscotch-js-sandbox/src/utils/shared.ts
index d95a3ff4..7ed653f2 100644
--- a/packages/hoppscotch-js-sandbox/src/utils/shared.ts
+++ b/packages/hoppscotch-js-sandbox/src/utils/shared.ts
@@ -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>(
* @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
},
}
}
diff --git a/packages/hoppscotch-js-sandbox/src/utils/test-helpers.ts b/packages/hoppscotch-js-sandbox/src/utils/test-helpers.ts
new file mode 100644
index 00000000..ed1e3c3f
--- /dev/null
+++ b/packages/hoppscotch-js-sandbox/src/utils/test-helpers.ts
@@ -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 = 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 = 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,
+ 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 = defaultRequest
+) =>
+ pipe(
+ runTestScript(script, {
+ envs,
+ request,
+ response,
+ }),
+ TE.map((x: TestResult) => x.envs)
+ )
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6aac8d74..b6922e39 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -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