feat: variable descriptions (#8269)

* 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>
This commit is contained in:
Sid
2026-07-07 21:43:39 +05:30
committed by GitHub
parent 958ca49156
commit 0bba66a590
177 changed files with 6570 additions and 482 deletions

View File

@@ -458,7 +458,7 @@ test.describe('Scroll Position Persistence', () => {
test('Request Headers - scroll persists with many headers across tab switches', async ({ pageWithUserData: page }) => {
await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
const scrollContainer = '.flex-boundary';
const firstVisibleRowLocator = () => page.getByTestId('editable-table').locator('table > tbody > tr:nth-child(2)');
const firstVisibleRowLocator = () => page.getByTestId('request-headers-table').locator('table > tbody > tr[data-index]').first();
await test.step('Setup request and navigate to Headers tab', async () => {
await openCollection(page, 'scroll-fixtures');
@@ -1174,7 +1174,7 @@ test.describe('Scroll Position Persistence', () => {
await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
const locators = buildCommonLocators(page);
const scrollContainer = '.collection-settings-content';
const firstVisibleRowLocator = () => page.getByTestId('editable-table').locator('table > tbody > tr:nth-child(2)');
const firstVisibleRowLocator = () => page.getByTestId('collection-headers').locator('table > tbody > tr[data-index]').first();
await test.step('Open collection settings and navigate to Headers tab', async () => {
await openCollection(page, 'scroll-fixtures');

View File

@@ -0,0 +1,5 @@
{
"version": "1",
"name": "req-description-yml",
"type": "collection"
}

View File

@@ -0,0 +1,27 @@
info:
name: formurlencoded-with-descriptions
type: http
seq: 2
http:
method: POST
url: https://example.com/form
body:
type: form-urlencoded
data:
- name: username
value: alice
description: Single-line form desc
- name: password
value: secret
description: |-
Multi-line form desc line one
Multi-line form desc line two
- name: plain-field
value: no-desc
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,30 @@
info:
name: multipart-with-descriptions
type: http
seq: 3
http:
method: POST
url: https://example.com/upload
body:
type: multipart-form
data:
- name: username
type: text
value: alice
description: Single-line field desc
- name: email
type: text
value: alice@example.com
description: |-
Multi-line field desc line one
Multi-line field desc line two
- name: plain-field
type: text
value: no-desc
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,3 @@
opencollection: "1.0.0"
info:
name: req-description-yml

View File

@@ -0,0 +1,66 @@
info:
name: request-with-descriptions
type: http
seq: 1
http:
method: GET
url: https://example.com/api
headers:
- name: X-Version
value: "2.0"
description: Single-line header desc
- name: X-Multi
value: value
description: |-
Header line one
Header line two
- name: X-Plain
value: plain-value
params:
- name: q
value: search
type: query
description: Single-line query desc
- name: page
value: "1"
type: query
description: |-
Multi-line query desc line one
Multi-line query desc line two
- name: plain-query
value: value
type: query
runtime:
variables:
- name: apiKey
value: secret
description: Single-line var desc
- name: baseUrl
value: https://example.com
description: |-
Multi-line var desc line one
Multi-line var desc line two
- name: plainVar
value: value
assertions:
- expression: res.status
operator: eq
value: "200"
description: Single-line assert desc
- expression: res.body.ok
operator: eq
value: "true"
description: |-
Multi-line assert line one
Multi-line assert line two
- expression: plainAssert
operator: eq
value: foo
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,10 @@
{
"collections": [
{
"path": "{{collectionPath}}",
"securityConfig": {
"jsSandboxMode": "safe"
}
}
]
}

View File

@@ -0,0 +1,28 @@
{
"maximized": false,
"lastOpenedCollections": [
"{{collectionPath}}"
],
"request": {
"sslVerification": false,
"customCaCertificate": {
"enabled": false,
"filePath": null
}
},
"font": {
"codeFont": "default"
},
"proxy": {
"enabled": false,
"protocol": "http",
"hostname": "",
"port": "",
"auth": {
"enabled": false,
"username": "",
"password": ""
},
"bypassProxy": ""
}
}

View File

@@ -0,0 +1,106 @@
import { test, expect } from '../../../playwright';
import { openRequest, selectRequestPaneTab } from '../../utils/page';
const COLLECTION = 'req-description-yml';
test.describe('Request Description (YAML) - Read', () => {
test('reads descriptions from request headers in a pre-existing .yml file', async ({
pageWithUserData: page
}) => {
test.setTimeout(30_000);
await openRequest(page, COLLECTION, 'request-with-descriptions');
await selectRequestPaneTab(page, 'Headers');
const headersTable = page.getByTestId('request-headers-table');
const rows = headersTable.locator('tbody tr');
// row 0: X-Version — single-line description
const versionDescEditor = rows.nth(0).getByTestId('column-description').locator('.CodeMirror');
await expect(versionDescEditor.locator('.CodeMirror-line').first()).toHaveText('Single-line header desc');
// row 1: X-Multi — multiline description
const multiDescEditor = rows.nth(1).getByTestId('column-description').locator('.CodeMirror');
await expect(multiDescEditor.locator('.CodeMirror-line').nth(0)).toHaveText('Header line one');
await expect(multiDescEditor.locator('.CodeMirror-line').nth(1)).toHaveText('Header line two');
// row 2: X-Plain — no description (editor is empty)
const plainDescEditor = rows.nth(2).getByTestId('column-description').locator('.CodeMirror');
await expect(plainDescEditor.locator('.CodeMirror-line').first()).toHaveText('');
});
test('reads descriptions from request query params in a pre-existing .yml file', async ({
pageWithUserData: page
}) => {
test.setTimeout(30_000);
await openRequest(page, COLLECTION, 'request-with-descriptions');
await selectRequestPaneTab(page, 'Params');
const queryTable = page.getByTestId('query-params-table');
const rows = queryTable.locator('tbody tr');
// row 0: q — single-line description
const qDescEditor = rows.nth(0).getByTestId('column-description').locator('.CodeMirror');
await expect(qDescEditor.locator('.CodeMirror-line').first()).toHaveText('Single-line query desc');
// row 1: page — multiline description
const pageDescEditor = rows.nth(1).getByTestId('column-description').locator('.CodeMirror');
await expect(pageDescEditor.locator('.CodeMirror-line').nth(0)).toHaveText('Multi-line query desc line one');
await expect(pageDescEditor.locator('.CodeMirror-line').nth(1)).toHaveText('Multi-line query desc line two');
// row 2: plain-query — no description
const plainDescEditor = rows.nth(2).getByTestId('column-description').locator('.CodeMirror');
await expect(plainDescEditor.locator('.CodeMirror-line').first()).toHaveText('');
});
test('reads descriptions from multipart form fields in a pre-existing .yml file', async ({
pageWithUserData: page
}) => {
test.setTimeout(30_000);
await openRequest(page, COLLECTION, 'multipart-with-descriptions');
await selectRequestPaneTab(page, 'Body');
const multipartTable = page.getByTestId('multipart-form-table');
const rows = multipartTable.locator('tbody tr');
// row 0: username — single-line description
const usernameDescEditor = rows.nth(0).getByTestId('column-description').locator('.CodeMirror');
await expect(usernameDescEditor.locator('.CodeMirror-line').first()).toHaveText('Single-line field desc');
// row 1: email — multiline description
const emailDescEditor = rows.nth(1).getByTestId('column-description').locator('.CodeMirror');
await expect(emailDescEditor.locator('.CodeMirror-line').nth(0)).toHaveText('Multi-line field desc line one');
await expect(emailDescEditor.locator('.CodeMirror-line').nth(1)).toHaveText('Multi-line field desc line two');
// row 2: plain-field — no description
const plainDescEditor = rows.nth(2).getByTestId('column-description').locator('.CodeMirror');
await expect(plainDescEditor.locator('.CodeMirror-line').first()).toHaveText('');
});
test('reads descriptions from form-urlencoded fields in a pre-existing .yml file', async ({
pageWithUserData: page
}) => {
test.setTimeout(30_000);
await openRequest(page, COLLECTION, 'formurlencoded-with-descriptions');
await selectRequestPaneTab(page, 'Body');
const formTable = page.getByTestId('form-urlencoded-table');
const rows = formTable.locator('tbody tr');
// row 0: username — single-line description
const usernameDescEditor = rows.nth(0).getByTestId('column-description').locator('.CodeMirror');
await expect(usernameDescEditor.locator('.CodeMirror-line').first()).toHaveText('Single-line form desc');
// row 1: password — multiline description
const passwordDescEditor = rows.nth(1).getByTestId('column-description').locator('.CodeMirror');
await expect(passwordDescEditor.locator('.CodeMirror-line').nth(0)).toHaveText('Multi-line form desc line one');
await expect(passwordDescEditor.locator('.CodeMirror-line').nth(1)).toHaveText('Multi-line form desc line two');
// row 2: plain-field — no description
const plainDescEditor = rows.nth(2).getByTestId('column-description').locator('.CodeMirror');
await expect(plainDescEditor.locator('.CodeMirror-line').first()).toHaveText('');
});
});

View File

@@ -0,0 +1,43 @@
import * as fs from 'fs';
import * as path from 'path';
import { test, expect } from '../../../playwright';
import { openRequest, selectRequestPaneTab } from '../../utils/page';
import { buildCommonLocators } from '../../utils/page/locators';
const COLLECTION = 'req-description-yml';
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
test.describe('Request Description (YAML) - Write (Assertions)', () => {
test('writes a multiline description to a request assertion and persists it to the .yml file', async ({
pageWithUserData: page,
collectionFixturePath
}) => {
await openRequest(page, COLLECTION, 'request-with-descriptions');
await selectRequestPaneTab(page, 'Assert');
const assertionsTable = buildCommonLocators(page).table('assertions-table');
const plainAssertRow = assertionsTable.rowByName('plainAssert');
const descCell = plainAssertRow.getByTestId('column-description');
await descCell.evaluate((el) => {
const cmEl = el.querySelector('.CodeMirror');
if (!cmEl) throw new Error('No CodeMirror in plainAssert description cell');
const cm = (cmEl as any).CodeMirror;
if (!cm) throw new Error('CodeMirror instance not found');
cm.setValue('First line\nSecond line');
});
await expect(descCell.locator('.CodeMirror-line').nth(0)).toHaveText('First line');
await expect(descCell.locator('.CodeMirror-line').nth(1)).toHaveText('Second line');
await page.keyboard.press(saveShortcut);
await expect(page.getByText('Request saved successfully')).toBeVisible({ timeout: 5000 });
const ymlPath = path.join(collectionFixturePath!, 'request-with-descriptions.yml');
const fileContent = fs.readFileSync(ymlPath, 'utf8');
expect(fileContent).toContain('description:');
expect(fileContent).toContain('First line');
expect(fileContent).toContain('Second line');
});
});

View File

@@ -0,0 +1,42 @@
import * as fs from 'fs';
import * as path from 'path';
import { test, expect } from '../../../playwright';
import { openRequest, selectRequestPaneTab } from '../../utils/page';
const COLLECTION = 'req-description-yml';
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
test.describe('Request Description (YAML) - Write (Body: form-urlencoded)', () => {
test('writes a multiline description to a form field and persists it to the .yml file', async ({
pageWithUserData: page,
collectionFixturePath
}) => {
await openRequest(page, COLLECTION, 'formurlencoded-with-descriptions');
await selectRequestPaneTab(page, 'Body');
const formTable = page.getByTestId('form-urlencoded-table');
const plainRow = formTable.locator('tbody tr').nth(2);
const descCell = plainRow.getByTestId('column-description');
await descCell.evaluate((el) => {
const cmEl = el.querySelector('.CodeMirror');
if (!cmEl) throw new Error('No CodeMirror in plain-field description cell');
const cm = (cmEl as any).CodeMirror;
if (!cm) throw new Error('CodeMirror instance not found');
cm.setValue('Form line one\nForm line two');
});
await expect(descCell.locator('.CodeMirror-line').nth(0)).toHaveText('Form line one');
await expect(descCell.locator('.CodeMirror-line').nth(1)).toHaveText('Form line two');
await page.keyboard.press(saveShortcut);
await expect(page.getByText('Request saved successfully')).toBeVisible({ timeout: 5000 });
const ymlPath = path.join(collectionFixturePath!, 'formurlencoded-with-descriptions.yml');
const fileContent = fs.readFileSync(ymlPath, 'utf8');
expect(fileContent).toContain('description:');
expect(fileContent).toContain('Form line one');
expect(fileContent).toContain('Form line two');
});
});

View File

@@ -0,0 +1,42 @@
import * as fs from 'fs';
import * as path from 'path';
import { test, expect } from '../../../playwright';
import { openRequest, selectRequestPaneTab } from '../../utils/page';
const COLLECTION = 'req-description-yml';
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
test.describe('Request Description (YAML) - Write (Body: multipart-form)', () => {
test('writes a multiline description to a multipart field and persists it to the .yml file', async ({
pageWithUserData: page,
collectionFixturePath
}) => {
await openRequest(page, COLLECTION, 'multipart-with-descriptions');
await selectRequestPaneTab(page, 'Body');
const multipartTable = page.getByTestId('multipart-form-table');
const plainRow = multipartTable.locator('tbody tr').nth(2);
const descCell = plainRow.getByTestId('column-description');
await descCell.evaluate((el) => {
const cmEl = el.querySelector('.CodeMirror');
if (!cmEl) throw new Error('No CodeMirror in plain-field description cell');
const cm = (cmEl as any).CodeMirror;
if (!cm) throw new Error('CodeMirror instance not found');
cm.setValue('Field line one\nField line two');
});
await expect(descCell.locator('.CodeMirror-line').nth(0)).toHaveText('Field line one');
await expect(descCell.locator('.CodeMirror-line').nth(1)).toHaveText('Field line two');
await page.keyboard.press(saveShortcut);
await expect(page.getByText('Request saved successfully')).toBeVisible({ timeout: 5000 });
const ymlPath = path.join(collectionFixturePath!, 'multipart-with-descriptions.yml');
const fileContent = fs.readFileSync(ymlPath, 'utf8');
expect(fileContent).toContain('description:');
expect(fileContent).toContain('Field line one');
expect(fileContent).toContain('Field line two');
});
});

View File

@@ -0,0 +1,44 @@
import * as fs from 'fs';
import * as path from 'path';
import { test, expect } from '../../../playwright';
import { openRequest, selectRequestPaneTab } from '../../utils/page';
const COLLECTION = 'req-description-yml';
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
test.describe('Request Description (YAML) - Write (Headers)', () => {
test('writes a multiline description to a request header and persists it to the .yml file', async ({
pageWithUserData: page,
collectionFixturePath
}) => {
await openRequest(page, COLLECTION, 'request-with-descriptions');
await selectRequestPaneTab(page, 'Headers');
const headersTable = page.getByTestId('request-headers-table');
const xPlainRow = headersTable.locator('tbody tr').filter({
has: page.locator('[data-testid="column-name"] .CodeMirror-line', { hasText: 'X-Plain' })
});
const descCell = xPlainRow.getByTestId('column-description');
await descCell.evaluate((el) => {
const cmEl = el.querySelector('.CodeMirror');
if (!cmEl) throw new Error('No CodeMirror in X-Plain description cell');
const cm = (cmEl as any).CodeMirror;
if (!cm) throw new Error('CodeMirror instance not found');
cm.setValue('First line\nSecond line');
});
await expect(descCell.locator('.CodeMirror-line').nth(0)).toHaveText('First line');
await expect(descCell.locator('.CodeMirror-line').nth(1)).toHaveText('Second line');
await page.keyboard.press(saveShortcut);
await expect(page.getByText('Request saved successfully')).toBeVisible({ timeout: 5000 });
const ymlPath = path.join(collectionFixturePath!, 'request-with-descriptions.yml');
const fileContent = fs.readFileSync(ymlPath, 'utf8');
expect(fileContent).toContain('description:');
expect(fileContent).toContain('First line');
expect(fileContent).toContain('Second line');
});
});

View File

@@ -0,0 +1,42 @@
import * as fs from 'fs';
import * as path from 'path';
import { test, expect } from '../../../playwright';
import { openRequest, selectRequestPaneTab } from '../../utils/page';
const COLLECTION = 'req-description-yml';
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
test.describe('Request Description (YAML) - Write (Query Params)', () => {
test('writes a multiline description to a query param and persists it to the .yml file', async ({
pageWithUserData: page,
collectionFixturePath
}) => {
await openRequest(page, COLLECTION, 'request-with-descriptions');
await selectRequestPaneTab(page, 'Params');
const queryTable = page.getByTestId('query-params-table');
const plainRow = queryTable.locator('tbody tr').nth(2);
const descCell = plainRow.getByTestId('column-description');
await descCell.evaluate((el) => {
const cmEl = el.querySelector('.CodeMirror');
if (!cmEl) throw new Error('No CodeMirror in plain-query description cell');
const cm = (cmEl as any).CodeMirror;
if (!cm) throw new Error('CodeMirror instance not found');
cm.setValue('Query line one\nQuery line two');
});
await expect(descCell.locator('.CodeMirror-line').nth(0)).toHaveText('Query line one');
await expect(descCell.locator('.CodeMirror-line').nth(1)).toHaveText('Query line two');
await page.keyboard.press(saveShortcut);
await expect(page.getByText('Request saved successfully')).toBeVisible({ timeout: 5000 });
const ymlPath = path.join(collectionFixturePath!, 'request-with-descriptions.yml');
const fileContent = fs.readFileSync(ymlPath, 'utf8');
expect(fileContent).toContain('description:');
expect(fileContent).toContain('Query line one');
expect(fileContent).toContain('Query line two');
});
});

View File

@@ -0,0 +1,43 @@
import * as fs from 'fs';
import * as path from 'path';
import { test, expect } from '../../../playwright';
import { openRequest, selectRequestPaneTab } from '../../utils/page';
import { buildCommonLocators } from '../../utils/page/locators';
const COLLECTION = 'req-description-yml';
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
test.describe('Request Description (YAML) - Write (Vars)', () => {
test('writes a multiline description to a request var and persists it to the .yml file', async ({
pageWithUserData: page,
collectionFixturePath
}) => {
await openRequest(page, COLLECTION, 'request-with-descriptions');
await selectRequestPaneTab(page, 'Vars');
const varsTable = buildCommonLocators(page).table('request-vars-req');
const plainVarRow = varsTable.rowByName('plainVar');
const descCell = plainVarRow.getByTestId('column-description');
await descCell.evaluate((el) => {
const cmEl = el.querySelector('.CodeMirror');
if (!cmEl) throw new Error('No CodeMirror in plainVar description cell');
const cm = (cmEl as any).CodeMirror;
if (!cm) throw new Error('CodeMirror instance not found');
cm.setValue('First line\nSecond line');
});
await expect(descCell.locator('.CodeMirror-line').nth(0)).toHaveText('First line');
await expect(descCell.locator('.CodeMirror-line').nth(1)).toHaveText('Second line');
await page.keyboard.press(saveShortcut);
await expect(page.getByText('Request saved successfully')).toBeVisible({ timeout: 5000 });
const ymlPath = path.join(collectionFixturePath!, 'request-with-descriptions.yml');
const fileContent = fs.readFileSync(ymlPath, 'utf8');
expect(fileContent).toContain('description:');
expect(fileContent).toContain('First line');
expect(fileContent).toContain('Second line');
});
});

View File

@@ -0,0 +1,5 @@
{
"version": "1",
"name": "req-description",
"type": "collection"
}

View File

@@ -0,0 +1,5 @@
meta {
name: req-description
type: collection
version: 1.0.0
}

View File

@@ -0,0 +1,22 @@
meta {
name: formurlencoded-with-descriptions
type: http
seq: 3
}
post {
url: https://example.com/form
body: formUrlEncoded
auth: none
}
body:form-urlencoded {
@description('''Single-line form desc''')
username: alice
@description('''
Multi-line form desc line one
Multi-line form desc line two
''')
password: secret
plain-field: no-desc
}

View File

@@ -0,0 +1,22 @@
meta {
name: multipart-with-descriptions
type: http
seq: 2
}
post {
url: https://example.com/upload
body: multipartForm
auth: none
}
body:multipart-form {
@description('''Single-line field desc''')
username: alice
@description('''
Multi-line field desc line one
Multi-line field desc line two
''')
email: alice@example.com
plain-field: no-desc
}

View File

@@ -0,0 +1,55 @@
meta {
name: request-with-descriptions
type: http
seq: 1
}
get {
url: https://example.com/api
body: none
auth: none
}
params:query {
@description('''Single-line query desc''')
q: search
@description('''
Multi-line query desc line one
Multi-line query desc line two
''')
page: 1
plain-query: value
}
headers {
@description('''Single-line header desc''')
X-Version: 2.0
@description('''
Header line one
Header line two
''')
X-Multi: value
X-Plain: plain-value
}
vars:pre-request {
@description('''Single-line var desc''')
apiKey: secret
@description('''
Multi-line var desc line one
Multi-line var desc line two
''')
baseUrl: https://example.com
plainVar: value
}
assert {
@description('''Single-line assert desc''')
res.status: eq 200
@description('''
Multi-line assert line one
Multi-line assert line two
''')
res.body.ok: eq true
plainAssert: eq foo
}

View File

@@ -0,0 +1,10 @@
{
"collections": [
{
"path": "{{collectionPath}}",
"securityConfig": {
"jsSandboxMode": "safe"
}
}
]
}

View File

@@ -0,0 +1,28 @@
{
"maximized": false,
"lastOpenedCollections": [
"{{collectionPath}}"
],
"request": {
"sslVerification": false,
"customCaCertificate": {
"enabled": false,
"filePath": null
}
},
"font": {
"codeFont": "default"
},
"proxy": {
"enabled": false,
"protocol": "http",
"hostname": "",
"port": "",
"auth": {
"enabled": false,
"username": "",
"password": ""
},
"bypassProxy": ""
}
}

View File

@@ -0,0 +1,150 @@
import { test, expect } from '../../../playwright';
import { openRequest, selectRequestPaneTab } from '../../utils/page';
const COLLECTION = 'req-description';
test.describe('Request Description - Read', () => {
test('reads descriptions from request headers in a pre-existing .bru file', async ({
pageWithUserData: page
}) => {
test.setTimeout(30_000);
await openRequest(page, COLLECTION, 'request-with-descriptions');
await selectRequestPaneTab(page, 'Headers');
const headersTable = page.getByTestId('request-headers-table');
const rows = headersTable.locator('tbody tr');
// row 0: X-Version — single-line description
const versionDescEditor = rows.nth(0).getByTestId('column-description').locator('.CodeMirror');
await expect(versionDescEditor.locator('.CodeMirror-line').first()).toHaveText('Single-line header desc');
// row 1: X-Multi — multiline description
const multiDescEditor = rows.nth(1).getByTestId('column-description').locator('.CodeMirror');
await expect(multiDescEditor.locator('.CodeMirror-line').nth(0)).toHaveText('Header line one');
await expect(multiDescEditor.locator('.CodeMirror-line').nth(1)).toHaveText('Header line two');
// row 2: X-Plain — no description (editor is empty)
const plainDescEditor = rows.nth(2).getByTestId('column-description').locator('.CodeMirror');
await expect(plainDescEditor.locator('.CodeMirror-line').first()).toHaveText('');
});
test('reads descriptions from request query params in a pre-existing .bru file', async ({
pageWithUserData: page
}) => {
test.setTimeout(30_000);
await openRequest(page, COLLECTION, 'request-with-descriptions');
await selectRequestPaneTab(page, 'Params');
const queryTable = page.getByTestId('query-params-table');
const rows = queryTable.locator('tbody tr');
// row 0: q — single-line description
const qDescEditor = rows.nth(0).getByTestId('column-description').locator('.CodeMirror');
await expect(qDescEditor.locator('.CodeMirror-line').first()).toHaveText('Single-line query desc');
// row 1: page — multiline description
const pageDescEditor = rows.nth(1).getByTestId('column-description').locator('.CodeMirror');
await expect(pageDescEditor.locator('.CodeMirror-line').nth(0)).toHaveText('Multi-line query desc line one');
await expect(pageDescEditor.locator('.CodeMirror-line').nth(1)).toHaveText('Multi-line query desc line two');
// row 2: plain-query — no description
const plainDescEditor = rows.nth(2).getByTestId('column-description').locator('.CodeMirror');
await expect(plainDescEditor.locator('.CodeMirror-line').first()).toHaveText('');
});
test('reads descriptions from multipart form fields in a pre-existing .bru file', async ({
pageWithUserData: page
}) => {
test.setTimeout(30_000);
await openRequest(page, COLLECTION, 'multipart-with-descriptions');
await selectRequestPaneTab(page, 'Body');
const multipartTable = page.getByTestId('multipart-form-table');
const rows = multipartTable.locator('tbody tr');
// row 0: username — single-line description
const usernameDescEditor = rows.nth(0).getByTestId('column-description').locator('.CodeMirror');
await expect(usernameDescEditor.locator('.CodeMirror-line').first()).toHaveText('Single-line field desc');
// row 1: email — multiline description
const emailDescEditor = rows.nth(1).getByTestId('column-description').locator('.CodeMirror');
await expect(emailDescEditor.locator('.CodeMirror-line').nth(0)).toHaveText('Multi-line field desc line one');
await expect(emailDescEditor.locator('.CodeMirror-line').nth(1)).toHaveText('Multi-line field desc line two');
// row 2: plain-field — no description
const plainDescEditor = rows.nth(2).getByTestId('column-description').locator('.CodeMirror');
await expect(plainDescEditor.locator('.CodeMirror-line').first()).toHaveText('');
});
test('reads descriptions from form-urlencoded fields in a pre-existing .bru file', async ({
pageWithUserData: page
}) => {
test.setTimeout(30_000);
await openRequest(page, COLLECTION, 'formurlencoded-with-descriptions');
await selectRequestPaneTab(page, 'Body');
const formTable = page.getByTestId('form-urlencoded-table');
const rows = formTable.locator('tbody tr');
// row 0: username — single-line description
const usernameDescEditor = rows.nth(0).getByTestId('column-description').locator('.CodeMirror');
await expect(usernameDescEditor.locator('.CodeMirror-line').first()).toHaveText('Single-line form desc');
// row 1: password — multiline description
const passwordDescEditor = rows.nth(1).getByTestId('column-description').locator('.CodeMirror');
await expect(passwordDescEditor.locator('.CodeMirror-line').nth(0)).toHaveText('Multi-line form desc line one');
await expect(passwordDescEditor.locator('.CodeMirror-line').nth(1)).toHaveText('Multi-line form desc line two');
// row 2: plain-field — no description
const plainDescEditor = rows.nth(2).getByTestId('column-description').locator('.CodeMirror');
await expect(plainDescEditor.locator('.CodeMirror-line').first()).toHaveText('');
});
test('reads descriptions from request vars in a pre-existing .bru file', async ({
pageWithUserData: page
}) => {
test.setTimeout(30_000);
await openRequest(page, COLLECTION, 'request-with-descriptions');
await selectRequestPaneTab(page, 'Vars');
const varsTable = page.getByTestId('request-vars-req');
const rows = varsTable.locator('tbody tr');
const apiKeyDescEditor = rows.nth(0).getByTestId('column-description').locator('.CodeMirror');
await expect(apiKeyDescEditor.locator('.CodeMirror-line').first()).toHaveText('Single-line var desc');
const baseUrlDescEditor = rows.nth(1).getByTestId('column-description').locator('.CodeMirror');
await expect(baseUrlDescEditor.locator('.CodeMirror-line').nth(0)).toHaveText('Multi-line var desc line one');
await expect(baseUrlDescEditor.locator('.CodeMirror-line').nth(1)).toHaveText('Multi-line var desc line two');
const plainDescEditor = rows.nth(2).getByTestId('column-description').locator('.CodeMirror');
await expect(plainDescEditor.locator('.CodeMirror-line').first()).toHaveText('');
});
test('reads descriptions from request assertions in a pre-existing .bru file', async ({
pageWithUserData: page
}) => {
test.setTimeout(30_000);
await openRequest(page, COLLECTION, 'request-with-descriptions');
await selectRequestPaneTab(page, 'Assert');
const assertionsTable = page.getByTestId('assertions-table');
const rows = assertionsTable.locator('tbody tr');
const statusDescEditor = rows.nth(0).getByTestId('column-description').locator('.CodeMirror');
await expect(statusDescEditor.locator('.CodeMirror-line').first()).toHaveText('Single-line assert desc');
const bodyDescEditor = rows.nth(1).getByTestId('column-description').locator('.CodeMirror');
await expect(bodyDescEditor.locator('.CodeMirror-line').nth(0)).toHaveText('Multi-line assert line one');
await expect(bodyDescEditor.locator('.CodeMirror-line').nth(1)).toHaveText('Multi-line assert line two');
const plainDescEditor = rows.nth(2).getByTestId('column-description').locator('.CodeMirror');
await expect(plainDescEditor.locator('.CodeMirror-line').first()).toHaveText('');
});
});

View File

@@ -0,0 +1,43 @@
import * as fs from 'fs';
import * as path from 'path';
import { test, expect } from '../../../playwright';
import { openRequest, selectRequestPaneTab } from '../../utils/page';
import { buildCommonLocators } from '../../utils/page/locators';
const COLLECTION = 'req-description';
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
test.describe('Request Description - Write (Assertions)', () => {
test('writes a multiline description to a request assertion and persists it to the .bru file', async ({
pageWithUserData: page,
collectionFixturePath
}) => {
await openRequest(page, COLLECTION, 'request-with-descriptions');
await selectRequestPaneTab(page, 'Assert');
const assertionsTable = buildCommonLocators(page).table('assertions-table');
const plainAssertRow = assertionsTable.rowByName('plainAssert');
const descCell = plainAssertRow.getByTestId('column-description');
await descCell.evaluate((el) => {
const cmEl = el.querySelector('.CodeMirror');
if (!cmEl) throw new Error('No CodeMirror in plainAssert description cell');
const cm = (cmEl as any).CodeMirror;
if (!cm) throw new Error('CodeMirror instance not found');
cm.setValue('First line\nSecond line');
});
await expect(descCell.locator('.CodeMirror-line').nth(0)).toHaveText('First line');
await expect(descCell.locator('.CodeMirror-line').nth(1)).toHaveText('Second line');
await page.keyboard.press(saveShortcut);
await expect(page.getByText('Request saved successfully')).toBeVisible({ timeout: 5000 });
const bruPath = path.join(collectionFixturePath!, 'request-with-descriptions.bru');
const fileContent = fs.readFileSync(bruPath, 'utf8');
expect(fileContent).toContain('@description');
expect(fileContent).toContain('First line');
expect(fileContent).toContain('Second line');
});
});

View File

@@ -0,0 +1,42 @@
import * as fs from 'fs';
import * as path from 'path';
import { test, expect } from '../../../playwright';
import { openRequest, selectRequestPaneTab } from '../../utils/page';
const COLLECTION = 'req-description';
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
test.describe('Request Description - Write (Body: form-urlencoded)', () => {
test('writes a multiline description to a form field and persists it to the .bru file', async ({
pageWithUserData: page,
collectionFixturePath
}) => {
await openRequest(page, COLLECTION, 'formurlencoded-with-descriptions');
await selectRequestPaneTab(page, 'Body');
const formTable = page.getByTestId('form-urlencoded-table');
const plainRow = formTable.locator('tbody tr').nth(2);
const descCell = plainRow.getByTestId('column-description');
await descCell.evaluate((el) => {
const cmEl = el.querySelector('.CodeMirror');
if (!cmEl) throw new Error('No CodeMirror in plain-field description cell');
const cm = (cmEl as any).CodeMirror;
if (!cm) throw new Error('CodeMirror instance not found');
cm.setValue('Form line one\nForm line two');
});
await expect(descCell.locator('.CodeMirror-line').nth(0)).toHaveText('Form line one');
await expect(descCell.locator('.CodeMirror-line').nth(1)).toHaveText('Form line two');
await page.keyboard.press(saveShortcut);
await expect(page.getByText('Request saved successfully')).toBeVisible({ timeout: 5000 });
const bruPath = path.join(collectionFixturePath!, 'formurlencoded-with-descriptions.bru');
const fileContent = fs.readFileSync(bruPath, 'utf8');
expect(fileContent).toContain('@description');
expect(fileContent).toContain('Form line one');
expect(fileContent).toContain('Form line two');
});
});

View File

@@ -0,0 +1,42 @@
import * as fs from 'fs';
import * as path from 'path';
import { test, expect } from '../../../playwright';
import { openRequest, selectRequestPaneTab } from '../../utils/page';
const COLLECTION = 'req-description';
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
test.describe('Request Description - Write (Body: multipart-form)', () => {
test('writes a multiline description to a multipart field and persists it to the .bru file', async ({
pageWithUserData: page,
collectionFixturePath
}) => {
await openRequest(page, COLLECTION, 'multipart-with-descriptions');
await selectRequestPaneTab(page, 'Body');
const multipartTable = page.getByTestId('multipart-form-table');
const plainRow = multipartTable.locator('tbody tr').nth(2);
const descCell = plainRow.getByTestId('column-description');
await descCell.evaluate((el) => {
const cmEl = el.querySelector('.CodeMirror');
if (!cmEl) throw new Error('No CodeMirror in plain-field description cell');
const cm = (cmEl as any).CodeMirror;
if (!cm) throw new Error('CodeMirror instance not found');
cm.setValue('Field line one\nField line two');
});
await expect(descCell.locator('.CodeMirror-line').nth(0)).toHaveText('Field line one');
await expect(descCell.locator('.CodeMirror-line').nth(1)).toHaveText('Field line two');
await page.keyboard.press(saveShortcut);
await expect(page.getByText('Request saved successfully')).toBeVisible({ timeout: 5000 });
const bruPath = path.join(collectionFixturePath!, 'multipart-with-descriptions.bru');
const fileContent = fs.readFileSync(bruPath, 'utf8');
expect(fileContent).toContain('@description');
expect(fileContent).toContain('Field line one');
expect(fileContent).toContain('Field line two');
});
});

View File

@@ -0,0 +1,63 @@
import * as fs from 'fs';
import * as path from 'path';
import { test, expect } from '../../../playwright';
import { openRequest, selectRequestPaneTab } from '../../utils/page';
import { buildCommonLocators } from '../../utils/page';
const COLLECTION = 'req-description';
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
test.describe('Request Description - Write (Headers, quote escaping)', () => {
test('escapes embedded triple quotes and backslash-quotes in a multiline description and round-trips through disk', async ({
pageWithUserData: page,
collectionFixturePath
}) => {
await openRequest(page, COLLECTION, 'request-with-descriptions');
await selectRequestPaneTab(page, 'Headers');
const headersTable = page.getByTestId('request-headers-table');
const xPlainRow = headersTable.locator('tbody tr').filter({
has: page.locator('[data-testid="column-name"] .CodeMirror-line', { hasText: 'X-Plain' })
});
const descCell = xPlainRow.getByTestId('column-description');
const description = 'Backslash-quote: \\\'end\nTriple quote: \'\'\' embedded\nFinal line';
await descCell.evaluate((el, value) => {
const cmEl = el.querySelector('.CodeMirror');
if (!cmEl) throw new Error('No CodeMirror in X-Plain description cell');
const cm = (cmEl as any).CodeMirror;
if (!cm) throw new Error('CodeMirror instance not found');
cm.setValue(value);
}, description);
await expect(descCell.locator('.CodeMirror-line').nth(0)).toHaveText('Backslash-quote: \\\'end');
await expect(descCell.locator('.CodeMirror-line').nth(1)).toHaveText('Triple quote: \'\'\' embedded');
await expect(descCell.locator('.CodeMirror-line').nth(2)).toHaveText('Final line');
await page.keyboard.press(saveShortcut);
await expect(page.getByText('Request saved successfully')).toBeVisible({ timeout: 5000 });
const bruPath = path.join(collectionFixturePath!, 'request-with-descriptions.bru');
const fileContent = fs.readFileSync(bruPath, 'utf8');
// the ''' delimiter is escaped so it can't be mistaken for the closing block delimiter
expect(fileContent).toContain('\\\'\\\'\\\' embedded');
// the pre-existing backslash-quote is doubled so decoding can tell it apart from the above
expect(fileContent).toContain('\\\\\'end');
// reopen the request from disk to verify the escaped value round-trips back exactly
await buildCommonLocators(page).tabs.closeTab('request-with-descriptions').click({ force: true });
await openRequest(page, COLLECTION, 'request-with-descriptions');
await selectRequestPaneTab(page, 'Headers');
const reopenedRow = page.getByTestId('request-headers-table').locator('tbody tr').filter({
has: page.locator('[data-testid="column-name"] .CodeMirror-line', { hasText: 'X-Plain' })
});
const reopenedDescCell = reopenedRow.getByTestId('column-description');
await expect(reopenedDescCell.locator('.CodeMirror-line').nth(0)).toHaveText('Backslash-quote: \\\'end');
await expect(reopenedDescCell.locator('.CodeMirror-line').nth(1)).toHaveText('Triple quote: \'\'\' embedded');
await expect(reopenedDescCell.locator('.CodeMirror-line').nth(2)).toHaveText('Final line');
});
});

View File

@@ -0,0 +1,44 @@
import * as fs from 'fs';
import * as path from 'path';
import { test, expect } from '../../../playwright';
import { openRequest, selectRequestPaneTab } from '../../utils/page';
const COLLECTION = 'req-description';
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
test.describe('Request Description - Write (Headers)', () => {
test('writes a multiline description to a request header and persists it to the .bru file', async ({
pageWithUserData: page,
collectionFixturePath
}) => {
await openRequest(page, COLLECTION, 'request-with-descriptions');
await selectRequestPaneTab(page, 'Headers');
const headersTable = page.getByTestId('request-headers-table');
const xPlainRow = headersTable.locator('tbody tr').filter({
has: page.locator('[data-testid="column-name"] .CodeMirror-line', { hasText: 'X-Plain' })
});
const descCell = xPlainRow.getByTestId('column-description');
await descCell.evaluate((el) => {
const cmEl = el.querySelector('.CodeMirror');
if (!cmEl) throw new Error('No CodeMirror in X-Plain description cell');
const cm = (cmEl as any).CodeMirror;
if (!cm) throw new Error('CodeMirror instance not found');
cm.setValue('First line\nSecond line');
});
await expect(descCell.locator('.CodeMirror-line').nth(0)).toHaveText('First line');
await expect(descCell.locator('.CodeMirror-line').nth(1)).toHaveText('Second line');
await page.keyboard.press(saveShortcut);
await expect(page.getByText('Request saved successfully')).toBeVisible({ timeout: 5000 });
const bruPath = path.join(collectionFixturePath!, 'request-with-descriptions.bru');
const fileContent = fs.readFileSync(bruPath, 'utf8');
expect(fileContent).toContain('@description');
expect(fileContent).toContain('First line');
expect(fileContent).toContain('Second line');
});
});

View File

@@ -0,0 +1,42 @@
import * as fs from 'fs';
import * as path from 'path';
import { test, expect } from '../../../playwright';
import { openRequest, selectRequestPaneTab } from '../../utils/page';
const COLLECTION = 'req-description';
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
test.describe('Request Description - Write (Query Params)', () => {
test('writes a multiline description to a query param and persists it to the .bru file', async ({
pageWithUserData: page,
collectionFixturePath
}) => {
await openRequest(page, COLLECTION, 'request-with-descriptions');
await selectRequestPaneTab(page, 'Params');
const queryTable = page.getByTestId('query-params-table');
const plainRow = queryTable.locator('tbody tr').nth(2);
const descCell = plainRow.getByTestId('column-description');
await descCell.evaluate((el) => {
const cmEl = el.querySelector('.CodeMirror');
if (!cmEl) throw new Error('No CodeMirror in plain-query description cell');
const cm = (cmEl as any).CodeMirror;
if (!cm) throw new Error('CodeMirror instance not found');
cm.setValue('Query line one\nQuery line two');
});
await expect(descCell.locator('.CodeMirror-line').nth(0)).toHaveText('Query line one');
await expect(descCell.locator('.CodeMirror-line').nth(1)).toHaveText('Query line two');
await page.keyboard.press(saveShortcut);
await expect(page.getByText('Request saved successfully')).toBeVisible({ timeout: 5000 });
const bruPath = path.join(collectionFixturePath!, 'request-with-descriptions.bru');
const fileContent = fs.readFileSync(bruPath, 'utf8');
expect(fileContent).toContain('@description');
expect(fileContent).toContain('Query line one');
expect(fileContent).toContain('Query line two');
});
});

View File

@@ -0,0 +1,43 @@
import * as fs from 'fs';
import * as path from 'path';
import { test, expect } from '../../../playwright';
import { openRequest, selectRequestPaneTab } from '../../utils/page';
import { buildCommonLocators } from '../../utils/page/locators';
const COLLECTION = 'req-description';
const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
test.describe('Request Description - Write (Vars)', () => {
test('writes a multiline description to a request var and persists it to the .bru file', async ({
pageWithUserData: page,
collectionFixturePath
}) => {
await openRequest(page, COLLECTION, 'request-with-descriptions');
await selectRequestPaneTab(page, 'Vars');
const varsTable = buildCommonLocators(page).table('request-vars-req');
const plainVarRow = varsTable.rowByName('plainVar');
const descCell = plainVarRow.getByTestId('column-description');
await descCell.evaluate((el) => {
const cmEl = el.querySelector('.CodeMirror');
if (!cmEl) throw new Error('No CodeMirror in plainVar description cell');
const cm = (cmEl as any).CodeMirror;
if (!cm) throw new Error('CodeMirror instance not found');
cm.setValue('First line\nSecond line');
});
await expect(descCell.locator('.CodeMirror-line').nth(0)).toHaveText('First line');
await expect(descCell.locator('.CodeMirror-line').nth(1)).toHaveText('Second line');
await page.keyboard.press(saveShortcut);
await expect(page.getByText('Request saved successfully')).toBeVisible({ timeout: 5000 });
const bruPath = path.join(collectionFixturePath!, 'request-with-descriptions.bru');
const fileContent = fs.readFileSync(bruPath, 'utf8');
expect(fileContent).toContain('@description');
expect(fileContent).toContain('First line');
expect(fileContent).toContain('Second line');
});
});

View File

@@ -53,9 +53,10 @@ test.describe.serial('Header Validation', () => {
const headerRow = page.locator('table tbody tr').first();
const nameCell = getTableCell(headerRow, 0);
// Clear and enter a valid header name - use triple-click to select all (works cross-platform)
await nameCell.locator('.CodeMirror').click({ clickCount: 3 });
await nameCell.locator('textarea').fill('Valid-Header');
// Clear and enter a valid header name.
await nameCell.locator('.CodeMirror').click();
await page.keyboard.press(process.platform === 'darwin' ? 'Meta+A' : 'Control+A');
await page.keyboard.insertText('Valid-Header');
// Verify the error icon is not visible
const errorIcon = headerRow.locator('.text-red-600');

View File

@@ -148,7 +148,7 @@ value1\r
// The multipart form has an editable table - find and fill the first row
// The name column has placeholder "Key" (defined in MultipartFormParams columns)
const nameInput = page.locator('[data-testid="editable-table"] input[placeholder="Key"]').first();
const nameInput = page.locator('[data-testid="multipart-form-table"] input[placeholder="Key"]').first();
await nameInput.waitFor({ state: 'visible', timeout: 5000 });
await nameInput.click();
await nameInput.fill('testField');

View File

@@ -67,7 +67,7 @@ test.describe('Multipart Form - Multiple File Upload', () => {
// Reset the form to a single empty row before each test.
test.beforeEach(async ({ page }) => {
const table = buildCommonLocators(page).table('editable-table');
const table = buildCommonLocators(page).table('multipart-form-table');
await expect(table.container()).toBeVisible();
let rowCount = await table.allRows().count();
@@ -86,7 +86,7 @@ test.describe('Multipart Form - Multiple File Upload', () => {
(global as any).__mockFilePaths = paths;
}, files);
const table = buildCommonLocators(page).table('editable-table');
const table = buildCommonLocators(page).table('multipart-form-table');
await table.allRows().last().getByTestId('multipart-file-upload').click();
};

View File

@@ -57,11 +57,11 @@ test.describe.serial('Multipart Form - File Select Without Key', () => {
});
test('file select should work on empty row without a key', async ({ page }) => {
const table = buildCommonLocators(page).table('editable-table');
const table = buildCommonLocators(page).table('multipart-form-table');
await test.step('Click upload on empty last row (no key entered)', async () => {
const lastRow = table.allRows().last();
const uploadBtn = lastRow.locator('.upload-btn');
const uploadBtn = lastRow.getByTestId('multipart-file-upload');
await expect(uploadBtn).toBeVisible();
await uploadBtn.click();
});