refactor: remove idx from HeaderList, extend ReadOnlyPropertyList, add positional method translations (#7917)

* refactor: remove 'idx' method from headerList and update related tests

- Eliminated the 'idx' method from both req.headerList and res.headerList to streamline header management.
- Updated associated tests and documentation to reflect the removal, ensuring clarity in the API usage and maintaining consistency across the header management system.

* refactor: block unimplemented HeaderList methods with error messages

- Added error handling for unimplemented methods in HeaderList, including idx, add, upsert, remove, each, prepend, insert, and insertAfter.
- Each method now throws a descriptive error indicating the appropriate alternative methods to use, enhancing clarity in the API and guiding users towards correct usage.

* refactor: update error message in idx() method of HeaderList for clarity

- Modified the error message in the idx() method to guide users towards using all()[index] or get(name) instead of the unsupported idx() method, enhancing the clarity of the API documentation.

* refactor: update HeaderList to extend ReadOnlyPropertyList and remove unimplemented methods

- Changed HeaderList to extend ReadOnlyPropertyList instead of PropertyList, streamlining its functionality.
- Removed unimplemented methods (prepend, insert, insertAfter) from HeaderList, clarifying the API and guiding users towards using supported methods.
- Updated related tests to reflect these changes, ensuring consistency and accuracy in header management.

* test: add translations for pm.request.headers methods in request tests

- Introduced new tests to validate the translation of pm.request.headers methods (prepend, insert, insertAfter) to their corresponding req.headerList.append method.
- Enhanced existing tests to ensure accurate conversion and functionality of header management in the Bruno converters.

* feat: enhance header translation methods for pm.request.headers

- Added translations for additional pm.request.headers methods (get, has, one, all, count, indexOf, find, filter, each, map, reduce, toObject, clear) to their corresponding req.headerList methods.
- Updated tests to validate the new translations and ensure accurate header management functionality in the Bruno converters.
This commit is contained in:
sanish chirayath
2026-05-06 18:54:54 +05:30
committed by GitHub
parent 50d3862ea3
commit d39d5ef575
13 changed files with 140 additions and 41 deletions

View File

@@ -43,7 +43,6 @@ const STATIC_API_HINTS = {
'req.headerList.get(name)',
'req.headerList.one(name)',
'req.headerList.all()',
'req.headerList.idx(index)',
'req.headerList.count()',
'req.headerList.has(name)',
'req.headerList.has(name, value)',
@@ -88,7 +87,6 @@ const STATIC_API_HINTS = {
'res.headerList.get(name)',
'res.headerList.one(name)',
'res.headerList.all()',
'res.headerList.idx(index)',
'res.headerList.count()',
'res.headerList.has(name)',
'res.headerList.has(name, value)',

View File

@@ -34,8 +34,48 @@ const replacements = {
'pm\\.environment\\.toObject\\(': 'bru.getAllEnvVars(',
'pm\\.environment\\.clear\\(': 'bru.deleteAllEnvVars(',
'pm\\.variables\\.toObject\\(': 'bru.getAllVars(',
// Request header PropertyList methods
'pm\\.request\\.headers\\.remove\\(': 'req.deleteHeader(',
'pm\\.request\\.headers\\.get\\(': 'req.headerList.get(',
'pm\\.request\\.headers\\.has\\(': 'req.headerList.has(',
'pm\\.request\\.headers\\.one\\(': 'req.headerList.one(',
'pm\\.request\\.headers\\.all\\(': 'req.headerList.all(',
'pm\\.request\\.headers\\.count\\(': 'req.headerList.count(',
'pm\\.request\\.headers\\.indexOf\\(': 'req.headerList.indexOf(',
'pm\\.request\\.headers\\.find\\(': 'req.headerList.find(',
'pm\\.request\\.headers\\.filter\\(': 'req.headerList.filter(',
'pm\\.request\\.headers\\.each\\(': 'req.headerList.forEach(',
'pm\\.request\\.headers\\.map\\(': 'req.headerList.map(',
'pm\\.request\\.headers\\.reduce\\(': 'req.headerList.reduce(',
'pm\\.request\\.headers\\.toObject\\(': 'req.headerList.toObject(',
'pm\\.request\\.headers\\.clear\\(': 'req.headerList.clear(',
'pm\\.request\\.headers\\.add\\(': 'req.headerList.append(',
'pm\\.request\\.headers\\.upsert\\(': 'req.headerList.set(',
'pm\\.request\\.headers\\.toString\\(': 'req.headerList.toString(',
'pm\\.request\\.headers\\.toJSON\\(': 'req.headerList.toJSON(',
'pm\\.request\\.headers\\.populate\\(': 'req.headerList.populate(',
'pm\\.request\\.headers\\.repopulate\\(': 'req.headerList.repopulate(',
'pm\\.request\\.headers\\.assimilate\\(': 'req.headerList.assimilate(',
// Lossy: positional inserts map to append (position irrelevant for headers)
// Note: regex can't drop the second arg, so it passes through as-is
'pm\\.request\\.headers\\.prepend\\(': 'req.headerList.append(',
'pm\\.request\\.headers\\.insert\\(': 'req.headerList.append(',
'pm\\.request\\.headers\\.insertAfter\\(': 'req.headerList.append(',
// Response header PropertyList methods
'pm\\.response\\.headers\\.get\\(': 'res.getHeader(',
'pm\\.response\\.headers\\.has\\(': 'res.headerList.has(',
'pm\\.response\\.headers\\.one\\(': 'res.headerList.one(',
'pm\\.response\\.headers\\.all\\(': 'res.headerList.all(',
'pm\\.response\\.headers\\.count\\(': 'res.headerList.count(',
'pm\\.response\\.headers\\.indexOf\\(': 'res.headerList.indexOf(',
'pm\\.response\\.headers\\.find\\(': 'res.headerList.find(',
'pm\\.response\\.headers\\.filter\\(': 'res.headerList.filter(',
'pm\\.response\\.headers\\.each\\(': 'res.headerList.forEach(',
'pm\\.response\\.headers\\.map\\(': 'res.headerList.map(',
'pm\\.response\\.headers\\.reduce\\(': 'res.headerList.reduce(',
'pm\\.response\\.headers\\.toObject\\(': 'res.headerList.toObject(',
'pm\\.response\\.headers\\.toString\\(': 'res.headerList.toString(',
'pm\\.response\\.headers\\.toJSON\\(': 'res.headerList.toJSON(',
'pm\\.response\\.to\\.have\\.jsonSchema\\(': 'expect(res.getBody()).to.have.jsonSchema(',
'pm\\.response\\.to\\.not\\.have\\.jsonSchema\\(': 'expect(res.getBody()).to.not.have.jsonSchema(',
'pm\\.response\\.not\\.to\\.have\\.jsonSchema\\(': 'expect(res.getBody()).not.to.have.jsonSchema(',

View File

@@ -90,6 +90,8 @@ const simpleTranslations = {
'req.headerList.map': 'pm.request.headers.map',
'req.headerList.reduce': 'pm.request.headers.reduce',
'req.headerList.toObject': 'pm.request.headers.toObject',
'req.headerList.toString': 'pm.request.headers.toString',
'req.headerList.toJSON': 'pm.request.headers.toJSON',
'req.headerList.append': 'pm.request.headers.add',
'req.headerList.set': 'pm.request.headers.upsert',
'req.headerList.delete': 'pm.request.headers.remove',
@@ -130,6 +132,8 @@ const simpleTranslations = {
'res.headerList.map': 'pm.response.headers.map',
'res.headerList.reduce': 'pm.response.headers.reduce',
'res.headerList.toObject': 'pm.response.headers.toObject',
'res.headerList.toString': 'pm.response.headers.toString',
'res.headerList.toJSON': 'pm.response.headers.toJSON',
// Cookies jar
'bru.cookies.jar': 'pm.cookies.jar',

View File

@@ -65,6 +65,8 @@ const simpleTranslations = {
'pm.request.headers.map': 'req.headerList.map',
'pm.request.headers.reduce': 'req.headerList.reduce',
'pm.request.headers.toObject': 'req.headerList.toObject',
'pm.request.headers.toString': 'req.headerList.toString',
'pm.request.headers.toJSON': 'req.headerList.toJSON',
'pm.request.headers.clear': 'req.headerList.clear',
// Response headers PropertyList methods (read-only)
@@ -79,6 +81,8 @@ const simpleTranslations = {
'pm.response.headers.map': 'res.headerList.map',
'pm.response.headers.reduce': 'res.headerList.reduce',
'pm.response.headers.toObject': 'res.headerList.toObject',
'pm.response.headers.toString': 'res.headerList.toString',
'pm.response.headers.toJSON': 'res.headerList.toJSON',
// Request properties (pm.request.*)
'pm.request.url.getHost': 'req.getHost',
@@ -409,6 +413,32 @@ const complexTransformations = [
}
},
// Lossy: positional header inserts → append (only keep the first arg, drop positional ref)
// pm.request.headers.prepend(item) -> req.headerList.append(item)
{
pattern: 'pm.request.headers.prepend',
transform: (path, j) => {
const args = path.parent.value.arguments;
return j.callExpression(j.identifier('req.headerList.append'), args.length > 0 ? [args[0]] : []);
}
},
// pm.request.headers.insert(item, before) -> req.headerList.append(item)
{
pattern: 'pm.request.headers.insert',
transform: (path, j) => {
const args = path.parent.value.arguments;
return j.callExpression(j.identifier('req.headerList.append'), args.length > 0 ? [args[0]] : []);
}
},
// pm.request.headers.insertAfter(item, after) -> req.headerList.append(item)
{
pattern: 'pm.request.headers.insertAfter',
transform: (path, j) => {
const args = path.parent.value.arguments;
return j.callExpression(j.identifier('req.headerList.append'), args.length > 0 ? [args[0]] : []);
}
},
// pm.response.to.be.ok -> expect(res.getStatus()).to.be.within(200, 299)
{
pattern: 'pm.response.to.be.ok',

View File

@@ -292,6 +292,18 @@ console.log("Headers:", JSON.stringify(pm.request.headers));
expect(translatedCode).toBe('const obj = pm.request.headers.toObject();');
});
it('should translate req.headerList.toString to pm.request.headers.toString', () => {
const code = 'const str = req.headerList.toString();';
const translatedCode = translateBruToPostman(code);
expect(translatedCode).toBe('const str = pm.request.headers.toString();');
});
it('should translate req.headerList.toJSON to pm.request.headers.toJSON', () => {
const code = 'const json = req.headerList.toJSON();';
const translatedCode = translateBruToPostman(code);
expect(translatedCode).toBe('const json = pm.request.headers.toJSON();');
});
it('should translate req.headerList.set to pm.request.headers.upsert', () => {
const code = 'req.headerList.set({key: "X-Custom", value: "updated"});';
const translatedCode = translateBruToPostman(code);

View File

@@ -201,9 +201,39 @@ describe('Request Translation', () => {
expect(translatedCode).toBe('req.headerList.clear();');
});
it('should translate pm.request.headers.prepend to req.headerList.append', () => {
const code = 'pm.request.headers.prepend({key: "X-First", value: "1"});';
const translatedCode = translateCode(code);
expect(translatedCode).toBe('req.headerList.append({key: "X-First", value: "1"});');
});
it('should translate pm.request.headers.insert to req.headerList.append (drops positional ref)', () => {
const code = 'pm.request.headers.insert({key: "X-Mid", value: "2"}, "ref");';
const translatedCode = translateCode(code);
expect(translatedCode).toBe('req.headerList.append({key: "X-Mid", value: "2"});');
});
it('should translate pm.request.headers.insertAfter to req.headerList.append (drops positional ref)', () => {
const code = 'pm.request.headers.insertAfter({key: "X-After", value: "3"}, "ref");';
const translatedCode = translateCode(code);
expect(translatedCode).toBe('req.headerList.append({key: "X-After", value: "3"});');
});
it('should translate pm.request.headers.toObject to req.headerList.toObject', () => {
const code = 'const obj = pm.request.headers.toObject();';
const translatedCode = translateCode(code);
expect(translatedCode).toBe('const obj = req.headerList.toObject();');
});
it('should translate pm.request.headers.toString to req.headerList.toString', () => {
const code = 'const str = pm.request.headers.toString();';
const translatedCode = translateCode(code);
expect(translatedCode).toBe('const str = req.headerList.toString();');
});
it('should translate pm.request.headers.toJSON to req.headerList.toJSON', () => {
const code = 'const json = pm.request.headers.toJSON();';
const translatedCode = translateCode(code);
expect(translatedCode).toBe('const json = req.headerList.toJSON();');
});
});

View File

@@ -894,4 +894,16 @@ describe('Response Translation', () => {
const translatedCode = translateCode(code);
expect(translatedCode).toBe('const obj = res.headerList.toObject();');
});
it('should translate pm.response.headers.toString to res.headerList.toString', () => {
const code = 'const str = pm.response.headers.toString();';
const translatedCode = translateCode(code);
expect(translatedCode).toBe('const str = res.headerList.toString();');
});
it('should translate pm.response.headers.toJSON to res.headerList.toJSON', () => {
const code = 'const json = pm.response.headers.toJSON();';
const translatedCode = translateCode(code);
expect(translatedCode).toBe('const json = res.headerList.toJSON();');
});
});

View File

@@ -1,4 +1,3 @@
const PropertyList = require('./property-list');
const ReadOnlyPropertyList = require('./readonly-property-list');
/**
@@ -78,7 +77,7 @@ const ReadOnlyPropertyList = require('./readonly-property-list');
* | `repopulate(items)` | Clears all, then populates with new items |
* | `assimilate(source, prune?)` | Merges headers; prune removes items not in source |
*/
class HeaderList extends PropertyList {
class HeaderList extends ReadOnlyPropertyList {
#req;
#writable;
@@ -151,6 +150,12 @@ class HeaderList extends PropertyList {
return { key: str.substring(0, idx).trim(), value: str.substring(idx + 1).trim() };
}
// ── Blocked inherited methods ─────────────────────────────────────────
// These are inherited from ReadOnlyPropertyList but are not part of
// the HeaderList API. Set to undefined so they are not callable.
idx = undefined;
each = undefined;
// ── Read method overrides (case-insensitive) ──────────────────────────
/**

View File

@@ -38,7 +38,7 @@ const addBrunoRequestShimToContext = (vm, req) => {
const { evalCode: headersEvalCode } = createPropertyListBridge(vm, req.headerList, headerListObj, {
globalPath: 'globalThis.req.headerList',
syncReadMethods: ['get', 'has', 'count', 'indexOf', 'toObject', 'toString'],
syncReadObjectMethods: ['one', 'all', 'idx', 'toJSON'],
syncReadObjectMethods: ['one', 'all', 'toJSON'],
syncWriteMethods: ['append', 'set', 'delete', 'clear', 'populate', 'repopulate', 'assimilate'],
withIterators: true
});
@@ -197,7 +197,7 @@ const addBrunoRequestShimToContext = (vm, req) => {
// Wrapped in a block to avoid const redeclaration conflicts with other evalCode blocks
// The bridge generates `each` (shared with CookieList); alias `forEach` for HeaderList's MDN-style API
if (headersEvalCode) {
vm.evalCode(`{ ${headersEvalCode} globalThis.req.headerList.forEach = globalThis.req.headerList.each; }`);
vm.evalCode(`{ ${headersEvalCode} globalThis.req.headerList.forEach = globalThis.req.headerList.each; delete globalThis.req.headerList.each; }`);
}
};

View File

@@ -63,7 +63,7 @@ const addBrunoResponseShimToContext = (vm, res) => {
const bridge = createPropertyListBridge(vm, res.headerList, headerListObj, {
globalPath: 'globalThis.res.headerList',
syncReadMethods: ['get', 'has', 'count', 'indexOf', 'toObject', 'toString'],
syncReadObjectMethods: ['one', 'all', 'idx', 'toJSON'],
syncReadObjectMethods: ['one', 'all', 'toJSON'],
syncWriteMethods: ['append', 'set', 'delete', 'clear', 'populate', 'repopulate', 'assimilate'],
withIterators: true
});
@@ -133,7 +133,7 @@ const addBrunoResponseShimToContext = (vm, res) => {
// Wrapped in a block to avoid const redeclaration conflicts with req.headerList's evalCode
// The bridge generates `each` (shared with CookieList); alias `forEach` for HeaderList's MDN-style API
if (resHeadersEvalCode) {
vm.evalCode(`{ ${resHeadersEvalCode} globalThis.res.headerList.forEach = globalThis.res.headerList.each; }`);
vm.evalCode(`{ ${resHeadersEvalCode} globalThis.res.headerList.forEach = globalThis.res.headerList.each; delete globalThis.res.headerList.each; }`);
}
};

View File

@@ -1,5 +1,4 @@
const HeaderList = require('../src/header-list');
const PropertyList = require('../src/property-list');
const ReadOnlyPropertyList = require('../src/readonly-property-list');
const BrunoRequest = require('../src/bruno-request');
const BrunoResponse = require('../src/bruno-response');
@@ -19,10 +18,9 @@ describe('HeaderList (req.headerList)', () => {
// ── Inheritance ────────────────────────────────────────────────────────
test('extends PropertyList and ReadOnlyPropertyList', () => {
test('extends ReadOnlyPropertyList', () => {
const { list } = createReqHeaders();
expect(list).toBeInstanceOf(ReadOnlyPropertyList);
expect(list).toBeInstanceOf(PropertyList);
expect(list).toBeInstanceOf(HeaderList);
});
@@ -73,17 +71,6 @@ describe('HeaderList (req.headerList)', () => {
expect(a1).not.toBe(a2);
});
test('idx() returns header at position', () => {
const { list } = createReqHeaders();
expect(list.idx(0)).toEqual({ key: 'Content-Type', value: 'application/json' });
expect(list.idx(2)).toEqual({ key: 'Accept', value: '*/*' });
});
test('idx() returns undefined for out-of-bounds', () => {
const { list } = createReqHeaders();
expect(list.idx(10)).toBeUndefined();
});
test('count() returns number of headers', () => {
const { list } = createReqHeaders();
expect(list.count()).toBe(3);
@@ -923,11 +910,6 @@ describe('Response Headers (res.headerList)', () => {
expect(headerList.count()).toBe(3);
});
test('idx() returns header at position', () => {
const { headerList } = createResHeaders();
expect(headerList.idx(1)).toEqual({ key: 'x-request-id', value: 'abc-123' });
});
test('indexOf() finds structurally-equal header', () => {
const { headerList } = createResHeaders();
expect(headerList.indexOf({ key: 'content-type', value: 'application/json' })).toBe(0);

View File

@@ -44,13 +44,6 @@ tests {
expect(keys).to.include('x-custom');
});
test("req.headerList.idx(index)", function() {
const first = req.headerList.idx(0);
expect(first).to.have.property('key');
expect(first).to.have.property('value');
expect(req.headerList.idx(-1)).to.be.undefined;
});
test("req.headerList.count()", function() {
expect(req.headerList.count()).to.be.at.least(3);
});

View File

@@ -40,13 +40,6 @@ tests {
expect(keys).to.include('x-powered-by');
});
test("res.headerList.idx(index)", function() {
const first = res.headerList.idx(0);
expect(first).to.have.property('key');
expect(first).to.have.property('value');
expect(res.headerList.idx(-1)).to.be.undefined;
});
test("res.headerList.count()", function() {
expect(res.headerList.count()).to.be.at.least(1);
});