mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-09 06:55:03 +00:00
* feat: add description column to various tables and enhance UI interactions - Introduced a description column in Headers, VarsTable, and other components to allow multi-line descriptions. - Added toggle buttons to show/hide the description column in relevant tables. - Updated EditableTable component to support dynamic row addition with customizable labels. - Enhanced UI for better user experience with consistent button styles and spacing adjustments. * chore: resize fix handles * fix: restrict column check for last index * refactor: remove unwanted style and fix bottom padding of the last child in the table row * tests: improve testability and fix impacted locators * tests: fix locators for value fields * fix: improve newline handling in description prefix * fix: ts changes * chore: remove redundant code and fix description roundtrip * chore: remove redundant check * tests(bru-lang): tests for description and annotation * test: improve locators and tests * tests: descriptions * chore: re-add description setup * fix: re-add description cols * chore: fix the double click reset * fix: account for hidden sidebar in request pane width calculation * refactor: ui/ux fixes for data type toggle * chore: inline common deps into bruno-lang and bruno-schema * chore: tests * chore: fix ux for descriptions * fix: layout for environment vars table * fix: ensure correct row selection for multipart file upload in tests * feat: enhance UID assignment for request headers and variables in collections * chore: simplify * chore: update pkg * chore: abstract the e2e utils * chore: fix exports * tests: fix locators for datatype vars * fix: ux issue with secrets in environment table * tests: update locators for collection headers and vars descriptions * tests: update locators to use data attributes for datatype selectors * chore: update default css width for actions column * chore: remove tooltip for string type warnings * fix: reduce name widths * refactor: update annotation serialization to use single-quote delimiters for descriptions * refactor(tests): simplify description formatting in jsonToEnv tests * feat: add multiline description escaping and unescaping functions with tests * refactor: clean up imports and improve multiline text block handling * refactor: update locators to use new test IDs for environment variable editors * refactor: streamline header input handling and improve environment variable row access * refactor: update e2e-test job to support self-hosted runners * refactor: update E2E test runner configuration to include e2e and macOS environments --------- Co-authored-by: Pragadesh-45 <temporaryg7904@gmail.com>
1049 lines
24 KiB
JavaScript
1049 lines
24 KiB
JavaScript
const parser = require('../src/bruToJson');
|
|
|
|
describe('bruToJson parser', () => {
|
|
describe('body:ws', () => {
|
|
it('infers message and settings | smoke', () => {
|
|
const input = `
|
|
body:ws {
|
|
type: json
|
|
name: message 1
|
|
content: '''
|
|
{"foo":"bar"}
|
|
'''
|
|
}
|
|
|
|
settings {
|
|
timeout: 30
|
|
}
|
|
`;
|
|
|
|
const expected = {
|
|
body: {
|
|
mode: 'ws',
|
|
ws: [
|
|
{
|
|
content: '{"foo":"bar"}',
|
|
name: 'message 1',
|
|
type: 'json',
|
|
selected: false
|
|
}
|
|
]
|
|
},
|
|
settings: {
|
|
encodeUrl: false,
|
|
timeout: 30
|
|
}
|
|
};
|
|
|
|
const output = parser(input);
|
|
expect(output).toEqual(expected);
|
|
});
|
|
|
|
it('parses a single message flagged with selected: true', () => {
|
|
const input = `
|
|
body:ws {
|
|
type: json
|
|
name: message 1
|
|
selected: true
|
|
content: '''
|
|
{"foo":"bar"}
|
|
'''
|
|
}
|
|
`;
|
|
|
|
const expected = {
|
|
body: {
|
|
mode: 'ws',
|
|
ws: [
|
|
{
|
|
content: '{"foo":"bar"}',
|
|
name: 'message 1',
|
|
type: 'json',
|
|
selected: true
|
|
}
|
|
]
|
|
}
|
|
};
|
|
|
|
const output = parser(input);
|
|
expect(output).toEqual(expected);
|
|
});
|
|
|
|
it('parses multiple messages with none marked as selected', () => {
|
|
const input = `
|
|
body:ws {
|
|
name: message 1
|
|
type: json
|
|
content: '''
|
|
{"action":"subscribe"}
|
|
'''
|
|
}
|
|
|
|
body:ws {
|
|
name: message 2
|
|
type: text
|
|
content: '''
|
|
hello world
|
|
'''
|
|
}
|
|
`;
|
|
|
|
const expected = {
|
|
body: {
|
|
mode: 'ws',
|
|
ws: [
|
|
{
|
|
name: 'message 1',
|
|
type: 'json',
|
|
content: '{"action":"subscribe"}',
|
|
selected: false
|
|
},
|
|
{
|
|
name: 'message 2',
|
|
type: 'text',
|
|
content: 'hello world',
|
|
selected: false
|
|
}
|
|
]
|
|
}
|
|
};
|
|
|
|
const output = parser(input);
|
|
expect(output).toEqual(expected);
|
|
});
|
|
|
|
it('parses multiple messages with exactly one marked as selected', () => {
|
|
const input = `
|
|
body:ws {
|
|
name: message 1
|
|
type: json
|
|
content: '''
|
|
{"action":"subscribe"}
|
|
'''
|
|
}
|
|
|
|
body:ws {
|
|
name: message 2
|
|
type: text
|
|
selected: true
|
|
content: '''
|
|
hello world
|
|
'''
|
|
}
|
|
|
|
body:ws {
|
|
name: message 3
|
|
type: xml
|
|
content: '''
|
|
<ping/>
|
|
'''
|
|
}
|
|
`;
|
|
|
|
const expected = {
|
|
body: {
|
|
mode: 'ws',
|
|
ws: [
|
|
{
|
|
name: 'message 1',
|
|
type: 'json',
|
|
content: '{"action":"subscribe"}',
|
|
selected: false
|
|
},
|
|
{
|
|
name: 'message 2',
|
|
type: 'text',
|
|
content: 'hello world',
|
|
selected: true
|
|
},
|
|
{
|
|
name: 'message 3',
|
|
type: 'xml',
|
|
content: '<ping/>',
|
|
selected: false
|
|
}
|
|
]
|
|
}
|
|
};
|
|
|
|
const output = parser(input);
|
|
expect(output).toEqual(expected);
|
|
});
|
|
|
|
it('treats selected: false as not selected', () => {
|
|
const input = `
|
|
body:ws {
|
|
name: message 1
|
|
type: text
|
|
selected: false
|
|
content: '''
|
|
hello
|
|
'''
|
|
}
|
|
`;
|
|
|
|
const output = parser(input);
|
|
expect(output.body.ws[0].selected).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('body:grpc', () => {
|
|
it('parses message content with name and content', () => {
|
|
const input = `
|
|
body:grpc {
|
|
name: message 1
|
|
content: '''
|
|
{"foo":"bar"}
|
|
'''
|
|
}
|
|
`;
|
|
|
|
const expected = {
|
|
body: {
|
|
mode: 'grpc',
|
|
grpc: [
|
|
{
|
|
content: '{"foo":"bar"}',
|
|
name: 'message 1'
|
|
}
|
|
]
|
|
}
|
|
};
|
|
|
|
const output = parser(input);
|
|
expect(output).toEqual(expected);
|
|
});
|
|
|
|
it('parses message with variables in content', () => {
|
|
const input = `
|
|
body:grpc {
|
|
name: message 1
|
|
content: '''
|
|
{"id":{{userId}},"name":"{{userName}}"}
|
|
'''
|
|
}
|
|
`;
|
|
|
|
const expected = {
|
|
body: {
|
|
mode: 'grpc',
|
|
grpc: [
|
|
{
|
|
content: '{"id":{{userId}},"name":"{{userName}}"}',
|
|
name: 'message 1'
|
|
}
|
|
]
|
|
}
|
|
};
|
|
|
|
const output = parser(input);
|
|
expect(output).toEqual(expected);
|
|
});
|
|
});
|
|
|
|
describe('vars:pre-request decorators', () => {
|
|
it('should parse all dataType decorators', () => {
|
|
const input = `
|
|
vars:pre-request {
|
|
apiKey: abc123
|
|
@number
|
|
port: 3000
|
|
@boolean
|
|
isEnabled: true
|
|
@object
|
|
config: '''
|
|
{
|
|
"name": "John Doe",
|
|
"age": 30
|
|
}
|
|
'''
|
|
}
|
|
`;
|
|
|
|
const output = parser(input);
|
|
expect(output.vars.req).toEqual([
|
|
{ name: 'apiKey', value: 'abc123', enabled: true, local: false },
|
|
{ name: 'port', value: 3000, enabled: true, local: false, annotations: [{ name: 'number' }], dataType: 'number' },
|
|
{ name: 'isEnabled', value: true, enabled: true, local: false, annotations: [{ name: 'boolean' }], dataType: 'boolean' },
|
|
{ name: 'config', value: { name: 'John Doe', age: 30 }, enabled: true, local: false, annotations: [{ name: 'object' }], dataType: 'object' }
|
|
]);
|
|
});
|
|
|
|
it('should parse @description with multiline value', () => {
|
|
const input = `
|
|
vars:pre-request {
|
|
@description('''
|
|
This is a certificate
|
|
Use this when making request
|
|
''')
|
|
certificate: some-value
|
|
url: https://example.com
|
|
}
|
|
`;
|
|
|
|
const output = parser(input);
|
|
expect(output.vars.req).toEqual([
|
|
{ name: 'certificate', value: 'some-value', enabled: true, local: false, annotations: [{ name: 'description', value: 'This is a certificate\nUse this when making request' }], description: 'This is a certificate\nUse this when making request' },
|
|
{ name: 'url', value: 'https://example.com', enabled: true, local: false }
|
|
]);
|
|
});
|
|
|
|
it('should parse combined @object and @description on a multiline var', () => {
|
|
const input = `
|
|
vars:pre-request {
|
|
@object
|
|
@description('''
|
|
This is a certificate
|
|
Use this when making request
|
|
''')
|
|
certificate: '''
|
|
{
|
|
"name": "John Doe",
|
|
"age": 30,
|
|
"email": "john.doe@example.com",
|
|
"signature": "John Doe"
|
|
}
|
|
'''
|
|
@number
|
|
@description('server port')
|
|
~port: 8080
|
|
}
|
|
`;
|
|
|
|
const output = parser(input);
|
|
expect(output.vars.req).toEqual([
|
|
{
|
|
name: 'certificate',
|
|
value: { name: 'John Doe', age: 30, email: 'john.doe@example.com', signature: 'John Doe' },
|
|
enabled: true,
|
|
local: false,
|
|
annotations: [
|
|
{ name: 'object' },
|
|
{ name: 'description', value: 'This is a certificate\nUse this when making request' }
|
|
],
|
|
dataType: 'object',
|
|
description: 'This is a certificate\nUse this when making request'
|
|
},
|
|
{
|
|
name: 'port',
|
|
value: 8080,
|
|
enabled: false,
|
|
local: false,
|
|
annotations: [
|
|
{ name: 'number' },
|
|
{ name: 'description', value: 'server port' }
|
|
],
|
|
dataType: 'number',
|
|
description: 'server port'
|
|
}
|
|
]);
|
|
});
|
|
|
|
it('should keep only the last dataType decorator when multiple are present', () => {
|
|
const input = `
|
|
vars:pre-request {
|
|
@object
|
|
@number
|
|
port: 3000
|
|
}
|
|
`;
|
|
|
|
const output = parser(input);
|
|
expect(output.vars.req).toEqual([
|
|
{ name: 'port', value: 3000, enabled: true, local: false, annotations: [{ name: 'object' }, { name: 'number' }], dataType: 'number' }
|
|
]);
|
|
});
|
|
|
|
it('should preserve the declared dataType and the raw value when coercion is impossible', () => {
|
|
// The UI's DataTypeSelector surfaces a warning icon for these rows; the
|
|
// declared dataType is retained so the user sees their intent.
|
|
const input = `
|
|
vars:pre-request {
|
|
@number
|
|
port: not-a-number
|
|
@boolean
|
|
flag: maybe
|
|
@object
|
|
config: plain
|
|
}
|
|
`;
|
|
|
|
const output = parser(input);
|
|
expect(output.vars.req).toEqual([
|
|
{ name: 'port', value: 'not-a-number', enabled: true, local: false, annotations: [{ name: 'number' }], dataType: 'number' },
|
|
{ name: 'flag', value: 'maybe', enabled: true, local: false, annotations: [{ name: 'boolean' }], dataType: 'boolean' },
|
|
{ name: 'config', value: 'plain', enabled: true, local: false, annotations: [{ name: 'object' }], dataType: 'object' }
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('multi-line values', () => {
|
|
it('parses multi-line values in URL, headers, params, and vars', () => {
|
|
const input = `
|
|
meta {
|
|
name: new-line
|
|
type: http
|
|
seq: 1
|
|
}
|
|
|
|
get {
|
|
url: '''
|
|
https://httpbin.io/anything?foo=hello
|
|
world
|
|
'''
|
|
body: none
|
|
auth: oauth2
|
|
}
|
|
|
|
params:query {
|
|
foo: '''
|
|
hello
|
|
world
|
|
'''
|
|
}
|
|
|
|
headers {
|
|
"test header": '''
|
|
t1
|
|
t2
|
|
'''
|
|
}
|
|
|
|
vars:pre-request {
|
|
test-var: '''
|
|
t1
|
|
t2
|
|
'''
|
|
}
|
|
`;
|
|
|
|
const expected = {
|
|
meta: {
|
|
name: 'new-line',
|
|
type: 'http',
|
|
seq: '1'
|
|
},
|
|
http: {
|
|
method: 'get',
|
|
url: 'https://httpbin.io/anything?foo=hello\nworld',
|
|
body: 'none',
|
|
auth: 'oauth2'
|
|
},
|
|
params: [
|
|
{
|
|
name: 'foo',
|
|
value: 'hello\nworld',
|
|
enabled: true,
|
|
type: 'query'
|
|
}
|
|
],
|
|
headers: [
|
|
{
|
|
name: 'test header',
|
|
value: 't1\nt2',
|
|
enabled: true
|
|
}
|
|
],
|
|
vars: {
|
|
req: [
|
|
{
|
|
name: 'test-var',
|
|
value: 't1\nt2',
|
|
enabled: true,
|
|
local: false
|
|
}
|
|
]
|
|
}
|
|
};
|
|
|
|
const output = parser(input);
|
|
expect(output).toEqual(expected);
|
|
});
|
|
|
|
it('parses multiline body parts with content type annotation', () => {
|
|
const input = `
|
|
body:multipart-form {
|
|
filePart: '''
|
|
Line1
|
|
Line2
|
|
''' @contentType(text/plain)
|
|
}
|
|
`;
|
|
|
|
const expected = {
|
|
body: {
|
|
multipartForm: [
|
|
{
|
|
name: 'filePart',
|
|
value: 'Line1\nLine2',
|
|
enabled: true,
|
|
type: 'text',
|
|
contentType: 'text/plain'
|
|
}
|
|
]
|
|
}
|
|
};
|
|
|
|
const output = parser(input);
|
|
expect(output).toEqual(expected);
|
|
});
|
|
|
|
it('parses an empty multipart-form file value as an empty array', () => {
|
|
const input = `
|
|
body:multipart-form {
|
|
file: @file()
|
|
}
|
|
`;
|
|
|
|
const expected = {
|
|
body: {
|
|
multipartForm: [
|
|
{
|
|
name: 'file',
|
|
value: [],
|
|
enabled: true,
|
|
type: 'file',
|
|
contentType: ''
|
|
}
|
|
]
|
|
}
|
|
};
|
|
|
|
const output = parser(input);
|
|
expect(output).toEqual(expected);
|
|
});
|
|
|
|
it('drops empty entries when parsing multiple multipart-form file paths', () => {
|
|
const input = `
|
|
body:multipart-form {
|
|
file: @file(a.txt||b.txt)
|
|
}
|
|
`;
|
|
|
|
const expected = {
|
|
body: {
|
|
multipartForm: [
|
|
{
|
|
name: 'file',
|
|
value: ['a.txt', 'b.txt'],
|
|
enabled: true,
|
|
type: 'file',
|
|
contentType: ''
|
|
}
|
|
]
|
|
}
|
|
};
|
|
|
|
const output = parser(input);
|
|
expect(output).toEqual(expected);
|
|
});
|
|
});
|
|
|
|
describe('description annotation', () => {
|
|
it('parses @description in headers', () => {
|
|
const input = `
|
|
headers {
|
|
@description('''API key for auth.''')
|
|
Authorization: Bearer xxx
|
|
@description("Single-line desc")
|
|
X-Custom: val
|
|
}`;
|
|
|
|
const output = parser(input);
|
|
expect(output.headers).toHaveLength(2);
|
|
expect(output.headers[0]).toMatchObject({
|
|
name: 'Authorization',
|
|
value: 'Bearer xxx',
|
|
enabled: true,
|
|
annotations: [
|
|
{
|
|
name: 'description',
|
|
value: 'API key for auth.'
|
|
}
|
|
],
|
|
description: 'API key for auth.'
|
|
});
|
|
expect(output.headers[1]).toMatchObject({
|
|
name: 'X-Custom',
|
|
value: 'val',
|
|
enabled: true,
|
|
annotations: [
|
|
{
|
|
name: 'description',
|
|
value: 'Single-line desc'
|
|
}
|
|
],
|
|
description: 'Single-line desc'
|
|
});
|
|
});
|
|
|
|
it('parses @description in params and body form-urlencoded', () => {
|
|
const input = `
|
|
params:query {
|
|
@description('''Search term.''')
|
|
q: search
|
|
}
|
|
body:form-urlencoded {
|
|
@description("Field description")
|
|
field: value
|
|
}`;
|
|
|
|
const output = parser(input);
|
|
expect(output.params).toHaveLength(1);
|
|
expect(output.params[0]).toMatchObject({
|
|
name: 'q',
|
|
value: 'search',
|
|
type: 'query',
|
|
annotations: [
|
|
{
|
|
name: 'description',
|
|
value: 'Search term.'
|
|
}
|
|
],
|
|
description: 'Search term.'
|
|
});
|
|
expect(output.body.formUrlEncoded).toHaveLength(1);
|
|
expect(output.body.formUrlEncoded[0]).toMatchObject({
|
|
name: 'field',
|
|
value: 'value',
|
|
enabled: true,
|
|
annotations: [
|
|
{
|
|
name: 'description',
|
|
value: 'Field description'
|
|
}
|
|
],
|
|
description: 'Field description'
|
|
});
|
|
});
|
|
|
|
it('parses @description in vars:pre-request and vars:post-response', () => {
|
|
const input = `
|
|
vars:pre-request {
|
|
@description("Pre-request auth token")
|
|
token: secret
|
|
}
|
|
vars:post-response {
|
|
@description("Saved ID from response")
|
|
saved: res.body.id
|
|
}`;
|
|
|
|
const output = parser(input);
|
|
expect(output.vars).toBeDefined();
|
|
expect(output.vars.req).toHaveLength(1);
|
|
expect(output.vars.req[0]).toMatchObject({
|
|
name: 'token',
|
|
value: 'secret',
|
|
enabled: true,
|
|
local: false,
|
|
annotations: [
|
|
{
|
|
name: 'description',
|
|
value: 'Pre-request auth token'
|
|
}
|
|
],
|
|
description: 'Pre-request auth token'
|
|
});
|
|
expect(output.vars.res).toHaveLength(1);
|
|
expect(output.vars.res[0]).toMatchObject({
|
|
name: 'saved',
|
|
value: 'res.body.id',
|
|
enabled: true,
|
|
local: false,
|
|
annotations: [
|
|
{
|
|
name: 'description',
|
|
value: 'Saved ID from response'
|
|
}
|
|
],
|
|
description: 'Saved ID from response'
|
|
});
|
|
});
|
|
|
|
it('parses @description in assert', () => {
|
|
const input = `
|
|
assert {
|
|
@description("Expect success status")
|
|
res.body.status: eq 200
|
|
@description("Response must have data")
|
|
res.body.data: isDefined
|
|
}`;
|
|
|
|
const output = parser(input);
|
|
expect(output.assertions).toHaveLength(2);
|
|
expect(output.assertions[0]).toMatchObject({
|
|
name: 'res.body.status',
|
|
value: 'eq 200',
|
|
enabled: true,
|
|
annotations: [
|
|
{
|
|
name: 'description',
|
|
value: 'Expect success status'
|
|
}
|
|
],
|
|
description: 'Expect success status'
|
|
});
|
|
expect(output.assertions[1]).toMatchObject({
|
|
name: 'res.body.data',
|
|
value: 'isDefined',
|
|
enabled: true,
|
|
annotations: [
|
|
{
|
|
name: 'description',
|
|
value: 'Response must have data'
|
|
}
|
|
],
|
|
description: 'Response must have data'
|
|
});
|
|
});
|
|
|
|
it('parses double-quoted @description with escaped newline', () => {
|
|
const input = `
|
|
headers {
|
|
@description("Line one\\nLine two")
|
|
X-Note: v
|
|
}`;
|
|
|
|
const output = parser(input);
|
|
expect(output.headers).toHaveLength(1);
|
|
expect(output.headers[0]).toMatchObject({
|
|
name: 'X-Note',
|
|
value: 'v',
|
|
enabled: true,
|
|
annotations: [
|
|
{
|
|
name: 'description',
|
|
value: 'Line one\nLine two'
|
|
}
|
|
],
|
|
description: 'Line one\nLine two'
|
|
});
|
|
});
|
|
|
|
it('parses triple-quoted @description with literal newlines', () => {
|
|
const input = `
|
|
headers {
|
|
@description('''
|
|
Line one
|
|
Line two
|
|
''')
|
|
X-Note: v
|
|
}`;
|
|
|
|
const output = parser(input);
|
|
expect(output.headers).toHaveLength(1);
|
|
expect(output.headers[0]).toMatchObject({
|
|
name: 'X-Note',
|
|
value: 'v',
|
|
enabled: true,
|
|
annotations: [
|
|
{
|
|
name: 'description',
|
|
value: 'Line one\nLine two'
|
|
}
|
|
],
|
|
description: 'Line one\nLine two'
|
|
});
|
|
});
|
|
|
|
it('parses escaped characters in descriptions', () => {
|
|
const input = `
|
|
headers {
|
|
@description("Say \\"hello\\"")
|
|
X-Quote: val
|
|
@description("Path: \\\\usr\\\\bin")
|
|
X-Backslash: val
|
|
@description("Line1\\nLine2")
|
|
X-Newline: val
|
|
}
|
|
params:query {
|
|
@description("Escaped \\" quote")
|
|
q: x
|
|
}
|
|
body:form-urlencoded {
|
|
@description("\\\\ and \\" and \\\\n")
|
|
f: v
|
|
}`;
|
|
|
|
const output = parser(input);
|
|
expect(output.headers).toHaveLength(3);
|
|
expect(output.headers[0]).toMatchObject({
|
|
name: 'X-Quote',
|
|
value: 'val',
|
|
enabled: true,
|
|
annotations: [
|
|
{
|
|
name: 'description',
|
|
value: 'Say "hello"'
|
|
}
|
|
],
|
|
description: 'Say "hello"'
|
|
});
|
|
expect(output.headers[1]).toMatchObject({
|
|
name: 'X-Backslash',
|
|
value: 'val',
|
|
enabled: true,
|
|
annotations: [
|
|
{
|
|
name: 'description',
|
|
value: 'Path: \\usr\\bin'
|
|
}
|
|
],
|
|
description: 'Path: \\usr\\bin'
|
|
});
|
|
expect(output.headers[2]).toMatchObject({
|
|
name: 'X-Newline',
|
|
value: 'val',
|
|
enabled: true,
|
|
annotations: [
|
|
{
|
|
name: 'description',
|
|
value: 'Line1\nLine2'
|
|
}
|
|
],
|
|
description: 'Line1\nLine2'
|
|
});
|
|
expect(output.params[0]).toMatchObject({
|
|
name: 'q',
|
|
value: 'x',
|
|
type: 'query',
|
|
annotations: [
|
|
{
|
|
name: 'description',
|
|
value: 'Escaped " quote'
|
|
}
|
|
],
|
|
description: 'Escaped " quote'
|
|
});
|
|
expect(output.body.formUrlEncoded[0]).toMatchObject({
|
|
name: 'f',
|
|
value: 'v',
|
|
enabled: true,
|
|
annotations: [
|
|
{
|
|
name: 'description',
|
|
value: '\\ and " and \\n'
|
|
}
|
|
],
|
|
description: '\\ and " and \\n'
|
|
});
|
|
});
|
|
|
|
it('parses emoji in triple-quoted prefix description', () => {
|
|
const input = `
|
|
headers {
|
|
@description('''Auth token 🔑''')
|
|
Authorization: Bearer xxx
|
|
@description('''Region 🌍 selector''')
|
|
X-Region: us-east
|
|
}`;
|
|
|
|
const output = parser(input);
|
|
expect(output.headers[0]).toMatchObject({
|
|
name: 'Authorization',
|
|
value: 'Bearer xxx',
|
|
annotations: [
|
|
{
|
|
name: 'description',
|
|
value: 'Auth token 🔑'
|
|
}
|
|
],
|
|
description: 'Auth token 🔑'
|
|
});
|
|
expect(output.headers[1]).toMatchObject({
|
|
name: 'X-Region',
|
|
value: 'us-east',
|
|
annotations: [
|
|
{
|
|
name: 'description',
|
|
value: 'Region 🌍 selector'
|
|
}
|
|
],
|
|
description: 'Region 🌍 selector'
|
|
});
|
|
});
|
|
|
|
it('parses emoji in double-quoted description', () => {
|
|
const input = `
|
|
vars:pre-request {
|
|
@description("API key 🔐 required")
|
|
token: secret
|
|
}
|
|
assert {
|
|
@description("Status check ✅")
|
|
res.status: eq 200
|
|
}`;
|
|
|
|
const output = parser(input);
|
|
expect(output.vars.req[0]).toMatchObject({
|
|
name: 'token',
|
|
value: 'secret',
|
|
annotations: [
|
|
{
|
|
name: 'description',
|
|
value: 'API key 🔐 required'
|
|
}
|
|
],
|
|
description: 'API key 🔐 required'
|
|
});
|
|
expect(output.assertions[0]).toMatchObject({
|
|
name: 'res.status',
|
|
value: 'eq 200',
|
|
annotations: [
|
|
{
|
|
name: 'description',
|
|
value: 'Status check ✅'
|
|
}
|
|
],
|
|
description: 'Status check ✅'
|
|
});
|
|
});
|
|
|
|
it('parses emoji in multiline triple-quoted prefix description', () => {
|
|
const input = `
|
|
headers {
|
|
@description('''
|
|
Launch 🚀
|
|
Second line
|
|
''')
|
|
X-Launch: val
|
|
}`;
|
|
|
|
const output = parser(input);
|
|
expect(output.headers[0]).toMatchObject({
|
|
name: 'X-Launch',
|
|
value: 'val',
|
|
annotations: [
|
|
{
|
|
name: 'description',
|
|
value: 'Launch 🚀\nSecond line'
|
|
}
|
|
],
|
|
description: 'Launch 🚀\nSecond line'
|
|
});
|
|
});
|
|
|
|
it('parses \\r\\n escape sequence in double-quoted description as CRLF', () => {
|
|
const input = `
|
|
headers {
|
|
@description("Line one\\r\\nLine two")
|
|
X-Note: v
|
|
}`;
|
|
|
|
const output = parser(input);
|
|
expect(output.headers[0]).toMatchObject({
|
|
name: 'X-Note',
|
|
value: 'v',
|
|
annotations: [
|
|
{
|
|
name: 'description',
|
|
value: 'Line one\r\nLine two'
|
|
}
|
|
],
|
|
description: 'Line one\r\nLine two'
|
|
});
|
|
});
|
|
|
|
it('parses \\n escape sequence in double-quoted description as LF', () => {
|
|
const input = `
|
|
headers {
|
|
@description("First\\nSecond\\nThird")
|
|
X-Note: v
|
|
}`;
|
|
|
|
const output = parser(input);
|
|
expect(output.headers[0]).toMatchObject({
|
|
name: 'X-Note',
|
|
value: 'v',
|
|
annotations: [
|
|
{
|
|
name: 'description',
|
|
value: 'First\nSecond\nThird'
|
|
}
|
|
],
|
|
description: 'First\nSecond\nThird'
|
|
});
|
|
});
|
|
|
|
it('parses triple-quoted prefix with CRLF file line endings', () => {
|
|
// Simulate a .bru file saved with Windows CRLF line endings
|
|
const input = 'headers {\r\n @description(\'\'\'Line one\'\'\')\r\n X-Note: val\r\n}';
|
|
|
|
const output = parser(input);
|
|
expect(output.headers[0]).toMatchObject({
|
|
name: 'X-Note',
|
|
value: 'val',
|
|
annotations: [
|
|
{
|
|
name: 'description',
|
|
value: 'Line one'
|
|
}
|
|
],
|
|
description: 'Line one'
|
|
});
|
|
});
|
|
|
|
it('parses multiline triple-quoted prefix with CRLF file line endings', () => {
|
|
const input = 'headers {\r\n @description(\'\'\'\r\n Line one\r\n Line two\r\n \'\'\')\r\n X-Note: val\r\n}';
|
|
|
|
const output = parser(input);
|
|
expect(output.headers[0]).toMatchObject({
|
|
name: 'X-Note',
|
|
value: 'val',
|
|
annotations: [
|
|
{
|
|
name: 'description',
|
|
value: 'Line one\r\nLine two'
|
|
}
|
|
],
|
|
description: 'Line one\r\nLine two'
|
|
});
|
|
});
|
|
|
|
it('parses multiline triple-quoted prefix with CRLF across three lines', () => {
|
|
const input = 'headers {\r\n @description(\'\'\'\r\n Line one\r\n Line two\r\n Line three\r\n \'\'\')\r\n X-Note: val\r\n}';
|
|
|
|
const output = parser(input);
|
|
expect(output.headers[0]).toMatchObject({
|
|
name: 'X-Note',
|
|
value: 'val',
|
|
annotations: [
|
|
{
|
|
name: 'description',
|
|
value: 'Line one\r\nLine two\r\nLine three'
|
|
}
|
|
],
|
|
description: 'Line one\r\nLine two\r\nLine three'
|
|
});
|
|
});
|
|
|
|
it('multiple consecutive @description prefixes stack as annotations on the next row', () => {
|
|
const input = `
|
|
headers {
|
|
@description('''hello''')
|
|
@description('''hi''')
|
|
a: b
|
|
}`;
|
|
|
|
const output = parser(input);
|
|
expect(output.headers).toHaveLength(1);
|
|
expect(output.headers[0]).toMatchObject({
|
|
name: 'a',
|
|
value: 'b',
|
|
enabled: true,
|
|
annotations: [
|
|
{
|
|
name: 'description',
|
|
value: 'hello'
|
|
},
|
|
{
|
|
name: 'description',
|
|
value: 'hi'
|
|
}
|
|
],
|
|
description: 'hello'
|
|
});
|
|
});
|
|
});
|
|
});
|