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

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

View File

@@ -0,0 +1,29 @@
opencollection: "1.0.0"
info:
name: col-description-yml
request:
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
variables:
- name: baseUrl
value: https://example.com
description: Single-line var desc
- name: apiKey
value: abc123
description: |-
Var line one
Var line two
- name: plain
value: no-desc
bundled: false

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,41 @@
import { test, expect } from '../../../playwright';
import { openCollectionSettings, focusCollectionSettingsTab, selectCollectionPaneTab } from '../../utils/page';
test.describe('Collection Settings Descriptions (YAML) - Read', () => {
test('reads descriptions from headers and vars in a pre-existing opencollection.yml', async ({
pageWithUserData: page
}) => {
test.setTimeout(30_000);
await openCollectionSettings(page, 'col-description-yml');
await focusCollectionSettingsTab(page);
await selectCollectionPaneTab(page, 'headers');
const headerRows = page.getByTestId('collection-headers').locator('tbody tr');
const versionDescEditor = headerRows.nth(0).getByTestId('column-description').locator('.CodeMirror');
await expect(versionDescEditor.locator('.CodeMirror-line').first()).toHaveText('Single-line header desc');
const multiDescEditor = headerRows.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');
const plainDescEditor = headerRows.nth(2).getByTestId('column-description').locator('.CodeMirror');
await expect(plainDescEditor.locator('.CodeMirror-line').first()).toHaveText('');
await selectCollectionPaneTab(page, 'vars');
const varRows = page.getByTestId('collection-vars-req').locator('tbody tr');
const baseUrlDescEditor = varRows.nth(0).locator('.CodeMirror').nth(1);
await expect(baseUrlDescEditor.locator('.CodeMirror-line').first()).toHaveText('Single-line var desc');
const apiKeyDescEditor = varRows.nth(1).locator('.CodeMirror').nth(1);
await expect(apiKeyDescEditor.locator('.CodeMirror-line').nth(0)).toHaveText('Var line one');
await expect(apiKeyDescEditor.locator('.CodeMirror-line').nth(1)).toHaveText('Var line two');
const plainVarDescEditor = varRows.nth(2).locator('.CodeMirror').nth(1);
await expect(plainVarDescEditor.locator('.CodeMirror-line').first()).toHaveText('');
});
});

View File

@@ -0,0 +1,42 @@
import * as fs from 'fs';
import * as path from 'path';
import { test, expect } from '../../../playwright';
test.describe('Collection Settings Descriptions (YAML) - Write (Headers)', () => {
test('writes a multiline description to a header and persists it to opencollection.yml', async ({
pageWithUserData: page,
collectionFixturePath
}) => {
await page.locator('#sidebar-collection-name').filter({ hasText: 'col-description-yml' }).click();
await expect(page.locator('.request-tab .tab-label').filter({ hasText: 'Collection' })).toBeVisible();
await page.getByTestId('collection-settings-tab-headers').click();
const headersTable = page.getByTestId('collection-headers');
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.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('Collection Settings saved successfully')).toBeVisible({ timeout: 5000 });
const ymlPath = path.join(collectionFixturePath!, 'opencollection.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,51 @@
import * as fs from 'fs';
import * as path from 'path';
import { test, expect } from '../../../playwright';
test.describe('Collection Settings Descriptions (YAML) - Write (Vars)', () => {
test('writes a multiline description to a pre-request var and persists it to opencollection.yml', async ({
pageWithUserData: page,
collectionFixturePath
}) => {
await page.locator('#sidebar-collection-name').filter({ hasText: 'col-description-yml' }).click();
await expect(page.locator('.request-tab .tab-label').filter({ hasText: 'Collection' })).toBeVisible();
await page.getByTestId('collection-settings-tab-vars').click();
await expect(page.getByTestId('collection-vars-req').locator('tbody tr').first()).toBeVisible();
await page.evaluate(() => {
const table = document.querySelector('[data-testid="collection-vars-req"]');
const rows = table?.querySelectorAll('tbody tr') ?? [];
const targetRow = Array.from(rows).find((row) => {
const input = row.querySelector('[data-testid="column-name"] input') as HTMLInputElement;
return input?.value === 'plain';
});
if (!targetRow) throw new Error('\'plain\' var row not found in pre-request vars table');
const cms = targetRow.querySelectorAll('.CodeMirror');
const cm = (cms[1] as any)?.CodeMirror;
if (!cm) throw new Error('Description CodeMirror not found in plain row');
cm.setValue('First line\nSecond line');
});
const varsTable = page.getByTestId('collection-vars-req');
const plainRowIndex = await varsTable.locator('[data-testid="column-name"] input').evaluateAll(
(inputs) => inputs.findIndex((el) => (el as HTMLInputElement).value === 'plain')
);
if (plainRowIndex === -1) throw new Error('\'plain\' var not found for assertion');
const descCell = varsTable.locator('tbody tr').nth(plainRowIndex).getByTestId('column-description');
await expect(descCell.locator('.CodeMirror-line').nth(0)).toHaveText('First line');
await expect(descCell.locator('.CodeMirror-line').nth(1)).toHaveText('Second line');
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('Collection Settings saved successfully')).toBeVisible({ timeout: 5000 });
const ymlPath = path.join(collectionFixturePath!, 'opencollection.yml');
const fileContent = fs.readFileSync(ymlPath, 'utf8');
expect(fileContent).toContain('First line');
expect(fileContent).toContain('Second line');
});
});

View File

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

View File

@@ -0,0 +1,27 @@
meta {
name: col-description
type: collection
version: 1.0.0
}
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''')
baseUrl: https://example.com
@description('''
Var line one
Var line two
''')
apiKey: abc123
plain: no-desc
}

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,41 @@
import { test, expect } from '../../../playwright';
import { openCollectionSettings, focusCollectionSettingsTab, selectCollectionPaneTab } from '../../utils/page';
test.describe('Collection Settings Descriptions - Read', () => {
test('reads descriptions from headers and vars in a pre-existing collection.bru', async ({
pageWithUserData: page
}) => {
test.setTimeout(30_000);
await openCollectionSettings(page, 'col-description');
await focusCollectionSettingsTab(page);
await selectCollectionPaneTab(page, 'headers');
const headerRows = page.getByTestId('collection-headers').locator('tbody tr');
const versionDescEditor = headerRows.nth(0).getByTestId('column-description').locator('.CodeMirror');
await expect(versionDescEditor.locator('.CodeMirror-line').first()).toHaveText('Single-line header desc');
const multiDescEditor = headerRows.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');
const plainDescEditor = headerRows.nth(2).getByTestId('column-description').locator('.CodeMirror');
await expect(plainDescEditor.locator('.CodeMirror-line').first()).toHaveText('');
await selectCollectionPaneTab(page, 'vars');
const varRows = page.getByTestId('collection-vars-req').locator('tbody tr');
const baseUrlDescEditor = varRows.nth(0).getByTestId('column-description').locator('.CodeMirror');
await expect(baseUrlDescEditor.locator('.CodeMirror-line').first()).toHaveText('Single-line var desc');
const apiKeyDescEditor = varRows.nth(1).getByTestId('column-description').locator('.CodeMirror');
await expect(apiKeyDescEditor.locator('.CodeMirror-line').nth(0)).toHaveText('Var line one');
await expect(apiKeyDescEditor.locator('.CodeMirror-line').nth(1)).toHaveText('Var line two');
const plainVarDescEditor = varRows.nth(2).getByTestId('column-description').locator('.CodeMirror');
await expect(plainVarDescEditor.locator('.CodeMirror-line').first()).toHaveText('');
});
});

View File

@@ -0,0 +1,53 @@
import * as fs from 'fs';
import * as path from 'path';
import { test, expect } from '../../../playwright';
test.describe('Collection Settings Descriptions - Write (Headers)', () => {
test('writes a multiline description to a header and persists it to collection.bru', async ({
pageWithUserData: page,
collectionFixturePath
}) => {
// Open collection settings by clicking the collection name in the sidebar
await page
.locator('#sidebar-collection-name')
.filter({ hasText: 'col-description' })
.click();
// The tab label always reads "Collection" regardless of the collection name
await expect(page.locator('.request-tab .tab-label').filter({ hasText: 'Collection' })).toBeVisible();
await page.locator('.tab.headers').click();
// Find the X-Plain row by its header name, not by position
const headersTable = page.getByTestId('collection-headers');
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');
// Use CodeMirror's JS API via locator.evaluate() so the `change` event fires synchronously
// and the EditableTable onChange chain updates Redux state before we click Save.
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');
});
// Wait for CodeMirror DOM to reflect both lines before saving
await expect(descCell.locator('.CodeMirror-line').nth(0)).toHaveText('First line');
await expect(descCell.locator('.CodeMirror-line').nth(1)).toHaveText('Second line');
// Save and confirm
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('Collection Settings saved successfully')).toBeVisible({ timeout: 5000 });
// Verify the description was written to collection.bru
const collectionBruPath = path.join(collectionFixturePath!, 'collection.bru');
const fileContent = fs.readFileSync(collectionBruPath, 'utf8');
expect(fileContent).toContain('@description');
expect(fileContent).toContain('First line');
expect(fileContent).toContain('Second line');
});
});

View File

@@ -0,0 +1,64 @@
import * as fs from 'fs';
import * as path from 'path';
import { test, expect } from '../../../playwright';
test.describe('Collection Settings Descriptions - Write (Vars)', () => {
test('writes a multiline description to a pre-request var and persists it to collection.bru', async ({
pageWithUserData: page,
collectionFixturePath
}) => {
// Open collection settings
await page
.locator('#sidebar-collection-name')
.filter({ hasText: 'col-description' })
.click();
// The tab label always reads "Collection" regardless of the collection name
await expect(page.locator('.request-tab .tab-label').filter({ hasText: 'Collection' })).toBeVisible();
await page.locator('.tab.vars').click();
// Wait for the pre-request vars table to render its rows
await expect(page.locator('table:first-of-type tbody tr').first()).toBeVisible();
// Find the 'plain' row by checking name input values, not by position.
// Use CodeMirror's JS API so the `change` event fires synchronously and
// the EditableTable onChange chain updates Redux state before we click Save.
await page.evaluate(() => {
const rows = document.querySelectorAll('table:first-of-type tbody tr');
const targetRow = Array.from(rows).find((row) => {
const input = row.querySelector('[data-testid="column-name"] input') as HTMLInputElement;
return input?.value === 'plain';
});
if (!targetRow) throw new Error('\'plain\' var row not found in pre-request vars table');
// Var rows have two CodeMirrors: value (index 0) and description (index 1)
const cms = targetRow.querySelectorAll('.CodeMirror');
const cm = (cms[1] as any)?.CodeMirror;
if (!cm) throw new Error('Description CodeMirror not found in plain row');
cm.setValue('First line\nSecond line');
});
// Find the 'plain' row in Playwright to assert both CM lines are reflected
const varsTable = page.getByTestId('collection-vars-req');
const plainRowIndex = await varsTable.locator('[data-testid="column-name"] input').evaluateAll(
(inputs) => inputs.findIndex((el) => (el as HTMLInputElement).value === 'plain')
);
if (plainRowIndex === -1) throw new Error('\'plain\' var not found for assertion');
const descCell = varsTable.locator('tbody tr').nth(plainRowIndex).getByTestId('column-description');
await expect(descCell.locator('.CodeMirror-line').nth(0)).toHaveText('First line');
await expect(descCell.locator('.CodeMirror-line').nth(1)).toHaveText('Second line');
// The vars section has a single "Save" button shared by pre and post tables
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('Collection Settings saved successfully')).toBeVisible({ timeout: 5000 });
// Verify the description was written to collection.bru
const collectionBruPath = path.join(collectionFixturePath!, 'collection.bru');
const fileContent = fs.readFileSync(collectionBruPath, 'utf8');
expect(fileContent).toContain('First line');
expect(fileContent).toContain('Second line');
});
});

View File

@@ -193,8 +193,7 @@ test.describe.serial('Draft indicator in collection and folder settings', () =>
const varNameInput = varRow.locator('input[type="text"]');
await varNameInput.click();
await varNameInput.fill('testVar');
const varValueEditor = varRow.locator('.CodeMirror');
const varValueEditor = varRow.getByTestId(/^test-multiline-editor-\d+\.value$/);
await varValueEditor.click();
await page.keyboard.type('testValue');
@@ -308,7 +307,7 @@ test.describe.serial('Draft indicator in collection and folder settings', () =>
await varNameInput.click();
await varNameInput.fill('folderVar');
const folderVarValueEditor = varRow.locator('.CodeMirror');
const folderVarValueEditor = varRow.getByTestId(/^test-multiline-editor-\d+\.value$/);
await folderVarValueEditor.click();
await page.keyboard.type('folderValue');

View File

@@ -115,9 +115,9 @@ test.describe('bru.setEnvVar(name, value) - secret variable persistence', () =>
await expect(locators.environment.varRow('apiToken')).toBeVisible();
await locators.environment.varRowEyeToggle('apiToken').click();
await expect(locators.environment.varRow('apiToken').locator('.CodeMirror').first())
await expect(locators.environment.varRowValueEditor('apiToken'))
.toHaveClass(/CodeMirror-empty/);
await expect(locators.environment.varRow('apiToken').locator('.CodeMirror'))
await expect(locators.environment.varRowValueEditor('apiToken'))
.not.toContainText(NEW_VALUE);
await closeEnvironmentPanel(page, 'collection');
@@ -158,7 +158,7 @@ test.describe('bru.setEnvVar(name, value) - secret variable persistence', () =>
await expect(envTab.locator('.close-gradient')).not.toHaveClass(/has-changes/);
await locators.environment.varRowEyeToggle('apiToken').click();
await expect(locators.environment.varRow('apiToken').locator('.CodeMirror'))
await expect(locators.environment.varRowValueEditor('apiToken'))
.toContainText(NEW_VALUE);
await closeEnvironmentPanel(page, 'collection');
@@ -213,7 +213,7 @@ test.describe('bru.setEnvVar(name, value) - secret variable persistence', () =>
await locators.environment.secretsTab().click();
await expect(locators.environment.varRow('apiToken')).toBeVisible();
await locators.environment.varRowEyeToggle('apiToken').click();
await expect(locators.environment.varRow('apiToken').locator('.CodeMirror'))
await expect(locators.environment.varRowValueEditor('apiToken'))
.toContainText(INITIAL_VALUE);
await closeEnvironmentPanel(page, 'collection');
@@ -244,9 +244,9 @@ test.describe('bru.setEnvVar(name, value) - secret variable persistence', () =>
await expect(envTab.locator('.close-gradient')).not.toHaveClass(/has-changes/);
await locators.environment.varRowEyeToggle('apiToken').click();
await expect(locators.environment.varRow('apiToken').locator('.CodeMirror'))
await expect(locators.environment.varRowValueEditor('apiToken'))
.toContainText(NEW_VALUE);
await expect(locators.environment.varRow('apiToken').locator('.CodeMirror'))
await expect(locators.environment.varRowValueEditor('apiToken'))
.not.toContainText(INITIAL_VALUE);
await closeEnvironmentPanel(page, 'collection');

View File

@@ -74,17 +74,17 @@ test.describe.serial('bru.setEnvVar(name, value, { persist: true }) — typed va
await expect(envTab).toBeVisible();
await expect(
newPage.locator('[data-testid="env-var-row-typed_num"] .type-label').first()
).toHaveText('number', { timeout: 5000 });
newPage.locator('[data-testid="env-var-row-typed_num"] [data-testid="datatype-selector-trigger"]').first()
).toHaveAttribute('data-selected-type', 'number', { timeout: 5000 });
await expect(
newPage.locator('[data-testid="env-var-row-typed_bool"] .type-label').first()
).toHaveText('boolean');
newPage.locator('[data-testid="env-var-row-typed_bool"] [data-testid="datatype-selector-trigger"]').first()
).toHaveAttribute('data-selected-type', 'boolean');
await expect(
newPage.locator('[data-testid="env-var-row-typed_obj"] .type-label').first()
).toHaveText('object');
newPage.locator('[data-testid="env-var-row-typed_obj"] [data-testid="datatype-selector-trigger"]').first()
).toHaveAttribute('data-selected-type', 'object');
await expect(
newPage.locator('[data-testid="env-var-row-typed_str"] .type-label').first()
).toHaveText('string');
newPage.locator('[data-testid="env-var-row-typed_str"] [data-testid="datatype-selector-trigger"]').first()
).toHaveAttribute('data-selected-type', 'string');
await newPage.getByTestId('responsive-tab-secrets').click();
@@ -93,8 +93,8 @@ test.describe.serial('bru.setEnvVar(name, value, { persist: true }) — typed va
).toBeVisible();
// Reloaded with the inferred @number type — the selector shows 'number'.
await expect(
newPage.locator('[data-testid="env-var-row-existing_secret"] .type-label')
).toHaveText('number');
newPage.locator('[data-testid="env-var-row-existing_secret"] [data-testid="datatype-selector-trigger"]')
).toHaveAttribute('data-selected-type', 'number');
await envTab.hover();
await envTab.getByTestId('request-tab-close-icon').click({ force: true });

View File

@@ -72,9 +72,9 @@ test.describe('bru.setGlobalEnvVar(name, value) - secret variable persistence (w
await expect(locators.environment.varRow('apiToken')).toBeVisible();
await locators.environment.varRowEyeToggle('apiToken').click();
await expect(locators.environment.varRow('apiToken').locator('.CodeMirror').first())
await expect(locators.environment.varRowValueEditor('apiToken'))
.toHaveClass(/CodeMirror-empty/);
await expect(locators.environment.varRow('apiToken').locator('.CodeMirror'))
await expect(locators.environment.varRowValueEditor('apiToken'))
.not.toContainText(NEW_VALUE);
await closeEnvironmentPanel(page, 'global');
@@ -117,7 +117,7 @@ test.describe('bru.setGlobalEnvVar(name, value) - secret variable persistence (w
await expect(envTab.locator('.close-gradient')).not.toHaveClass(/has-changes/);
await locators.environment.varRowEyeToggle('apiToken').click();
await expect(locators.environment.varRow('apiToken').locator('.CodeMirror'))
await expect(locators.environment.varRowValueEditor('apiToken'))
.toContainText(NEW_VALUE);
await closeEnvironmentPanel(page, 'global');
@@ -171,7 +171,7 @@ test.describe('bru.setGlobalEnvVar(name, value) - secret variable persistence (w
await locators.environment.secretsTab().click();
await expect(locators.environment.varRow('apiToken')).toBeVisible();
await locators.environment.varRowEyeToggle('apiToken').click();
await expect(locators.environment.varRow('apiToken').locator('.CodeMirror'))
await expect(locators.environment.varRowValueEditor('apiToken'))
.toContainText(INITIAL_VALUE);
await closeEnvironmentPanel(page, 'global');
@@ -202,9 +202,9 @@ test.describe('bru.setGlobalEnvVar(name, value) - secret variable persistence (w
await expect(envTab.locator('.close-gradient')).not.toHaveClass(/has-changes/);
await locators.environment.varRowEyeToggle('apiToken').click();
await expect(locators.environment.varRow('apiToken').locator('.CodeMirror'))
await expect(locators.environment.varRowValueEditor('apiToken'))
.toContainText(NEW_VALUE);
await expect(locators.environment.varRow('apiToken').locator('.CodeMirror'))
await expect(locators.environment.varRowValueEditor('apiToken'))
.not.toContainText(INITIAL_VALUE);
await closeEnvironmentPanel(page, 'global');

View File

@@ -61,7 +61,7 @@ const expectTypeLabel = async (page: Page, name: string, label: string) => {
const locators = buildCommonLocators(page);
const row = locators.environment.varRow(name);
await scrollVirtuosoRowIntoView(page, row);
await expect(locators.dataTypeSelector.typeLabel(row)).toHaveText(label, { timeout: SLOW_RENDER_TIMEOUT_MS });
await expect(locators.dataTypeSelector.typeLabel(row)).toHaveAttribute('data-selected-type', label, { timeout: SLOW_RENDER_TIMEOUT_MS });
};
const expectMismatchVisible = async (page: Page, name: string) => {

View File

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

View File

@@ -0,0 +1,13 @@
name: WithDescriptions
variables:
- name: host
value: http://localhost:3000
description: Single-line desc
- name: token
value: abc123
description: |-
Line one
Line two
- name: plain
value: no-description

View File

@@ -0,0 +1,5 @@
opencollection: "1.0.0"
info:
name: env-description-yml
bundled: false

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,46 @@
import { test, expect, Page } from '../../../playwright';
import { buildCommonLocators } from '../../utils/page/locators';
const openEnvironmentConfigure = async (page: Page, envName: string) => {
const locators = buildCommonLocators(page);
await locators.environment.currentEnvironment().click();
await expect(locators.environment.envOption(envName)).toBeVisible();
await locators.environment.envOption(envName).click();
await expect(locators.environment.currentEnvironment().filter({ hasText: envName })).toBeVisible();
await locators.environment.currentEnvironment().click();
await expect(locators.dropdown.item('Configure')).toBeVisible();
await locators.dropdown.item('Configure').click();
await expect(locators.tabs.requestTab('Environments')).toBeVisible();
};
test.describe('Environment Variable Descriptions (YAML) - Read', () => {
test('reads single-line and multiline descriptions from opencollection.yml environments', async ({
pageWithUserData: page
}) => {
test.setTimeout(30_000);
const collection = page
.getByTestId('collections')
.locator('#sidebar-collection-name')
.filter({ hasText: 'env-description-yml' });
await expect(collection).toBeVisible();
await collection.click();
await openEnvironmentConfigure(page, 'WithDescriptions');
const locators = buildCommonLocators(page);
await expect(page.locator('input[name="0.name"]')).toHaveValue('host');
const hostDesc = locators.environment.variableDescriptionEditor(0);
await expect(hostDesc.locator('.CodeMirror-line').first()).toHaveText('Single-line desc');
await expect(page.locator('input[name="1.name"]')).toHaveValue('token');
const tokenDesc = locators.environment.variableDescriptionEditor(1);
await expect(tokenDesc.locator('.CodeMirror-line').nth(0)).toHaveText('Line one');
await expect(tokenDesc.locator('.CodeMirror-line').nth(1)).toHaveText('Line two');
await expect(page.locator('input[name="2.name"]')).toHaveValue('plain');
const plainDesc = locators.environment.variableDescriptionEditor(2);
await expect(plainDesc.locator('.CodeMirror-line').first()).toHaveText('');
});
});

View File

@@ -0,0 +1,59 @@
import * as fs from 'fs';
import * as path from 'path';
import { test, expect, Page } from '../../../playwright';
import { buildCommonLocators } from '../../utils/page/locators';
const openEnvironmentConfigure = async (page: Page, envName: string) => {
const locators = buildCommonLocators(page);
await locators.environment.currentEnvironment().click();
await expect(locators.environment.envOption(envName)).toBeVisible();
await locators.environment.envOption(envName).click();
await expect(locators.environment.currentEnvironment().filter({ hasText: envName })).toBeVisible();
await locators.environment.currentEnvironment().click();
await expect(locators.dropdown.item('Configure')).toBeVisible();
await locators.dropdown.item('Configure').click();
await expect(locators.tabs.requestTab('Environments')).toBeVisible();
};
test.describe('Environment Variable Descriptions (YAML) - Write', () => {
test('writes a multiline description and persists it to the .yml file', async ({
pageWithUserData: page,
collectionFixturePath
}) => {
test.setTimeout(30_000);
const collection = page
.getByTestId('collections')
.locator('#sidebar-collection-name')
.filter({ hasText: 'env-description-yml' });
await expect(collection).toBeVisible();
await collection.click();
await openEnvironmentConfigure(page, 'WithDescriptions');
await page.evaluate((rowIndex) => {
const rows = document.querySelectorAll('tr');
for (const row of rows) {
if (row.querySelector(`input[name="${rowIndex}.name"]`)) {
const cms = row.querySelectorAll('.CodeMirror');
const cm = (cms[1] as any)?.CodeMirror;
if (cm) cm.setValue('First line\nSecond line');
break;
}
}
}, 2);
const plainDescEditor = buildCommonLocators(page).environment.variableDescriptionEditor(2);
await expect(plainDescEditor.locator('.CodeMirror-line').nth(0)).toHaveText('First line');
await page.waitForTimeout(200);
await page.getByTestId('save-env').click();
await expect(page.getByText('Changes saved successfully')).toBeVisible({ timeout: 5000 });
const envFilePath = path.join(collectionFixturePath!, 'environments', 'WithDescriptions.yml');
const fileContent = fs.readFileSync(envFilePath, 'utf8');
expect(fileContent).toContain('First line');
expect(fileContent).toContain('Second line');
});
});

View File

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

View File

@@ -0,0 +1,13 @@
vars {
@description('''Single-line desc''')
host: http://localhost:3000
@description('''empty description''')
@description('''
Line one
Line two
''')
token: abc123
plain: no-description
@description("has ''' triple quotes inside")
tricky: value
}

View File

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

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,64 @@
import { test, expect, Page } from '../../../playwright';
import { buildCommonLocators } from '../../utils/page/locators';
const openEnvironmentConfigure = async (page: Page, envName: string) => {
const locators = buildCommonLocators(page);
await locators.environment.currentEnvironment().click();
await expect(locators.environment.envOption(envName)).toBeVisible();
await locators.environment.envOption(envName).click();
await expect(locators.environment.currentEnvironment().filter({ hasText: envName })).toBeVisible();
await locators.environment.currentEnvironment().click();
await expect(locators.dropdown.item('Configure')).toBeVisible();
await locators.dropdown.item('Configure').click();
await expect(locators.tabs.requestTab('Environments')).toBeVisible();
};
test.describe('Environment Variable Descriptions - Read', () => {
test('reads single-line and multiline descriptions from a pre-existing environment file', async ({
pageWithUserData: page
}) => {
test.setTimeout(30_000);
const collection = page
.getByTestId('collections')
.locator('#sidebar-collection-name')
.filter({ hasText: 'env-description' });
await expect(collection).toBeVisible();
await collection.click();
await openEnvironmentConfigure(page, 'WithDescriptions');
const locators = buildCommonLocators(page);
// row 0: host — single-line description
await expect(page.locator('input[name="0.name"]')).toHaveValue('host');
const hostDesc = locators.environment.variableDescriptionEditor(0);
await expect(hostDesc).toBeVisible();
await expect(hostDesc.locator('.CodeMirror-line').first()).toHaveText('Single-line desc');
// row 1: orphaned @description('''empty description''') — empty name/value, description is 'empty description'
await expect(page.locator('input[name="1.name"]')).toHaveValue('');
const orphanDesc = locators.environment.variableDescriptionEditor(1);
await expect(orphanDesc).toBeVisible();
await expect(orphanDesc.locator('.CodeMirror-line').first()).toHaveText('empty description');
// row 2: token — gets the last (multiline) description; the first was consumed by the orphaned row
await expect(page.locator('input[name="2.name"]')).toHaveValue('token');
const tokenDesc = locators.environment.variableDescriptionEditor(2);
await expect(tokenDesc).toBeVisible();
await expect(tokenDesc.locator('.CodeMirror-line').nth(0)).toHaveText('Line one');
await expect(tokenDesc.locator('.CodeMirror-line').nth(1)).toHaveText('Line two');
// row 3: plain — no description (editor is empty)
await expect(page.locator('input[name="3.name"]')).toHaveValue('plain');
const plainDesc = locators.environment.variableDescriptionEditor(3);
await expect(plainDesc).toBeVisible();
await expect(plainDesc.locator('.CodeMirror-line').first()).toHaveText('');
// row 4: tricky — description containing ''' (stored as triple-quoted @description('''...'''))
await expect(page.locator('input[name="4.name"]')).toHaveValue('tricky');
const trickyDesc = locators.environment.variableDescriptionEditor(4);
await expect(trickyDesc).toBeVisible();
await expect(trickyDesc.locator('.CodeMirror-line').first()).toHaveText('has \'\'\' triple quotes inside');
});
});

View File

@@ -0,0 +1,65 @@
import * as fs from 'fs';
import * as path from 'path';
import { test, expect, Page } from '../../../playwright';
import { buildCommonLocators } from '../../utils/page/locators';
const openEnvironmentConfigure = async (page: Page, envName: string) => {
const locators = buildCommonLocators(page);
await locators.environment.currentEnvironment().click();
await expect(locators.environment.envOption(envName)).toBeVisible();
await locators.environment.envOption(envName).click();
await expect(locators.environment.currentEnvironment().filter({ hasText: envName })).toBeVisible();
await locators.environment.currentEnvironment().click();
await expect(locators.dropdown.item('Configure')).toBeVisible();
await locators.dropdown.item('Configure').click();
await expect(locators.tabs.requestTab('Environments')).toBeVisible();
};
test.describe('Environment Variable Descriptions - Write', () => {
test('writes a multiline description and persists it to the .bru file', async ({
pageWithUserData: page,
collectionFixturePath
}) => {
test.setTimeout(30_000);
const collection = page
.getByTestId('collections')
.locator('#sidebar-collection-name')
.filter({ hasText: 'env-description' });
await expect(collection).toBeVisible();
await collection.click();
await openEnvironmentConfigure(page, 'WithDescriptions');
// Set the description value directly via CodeMirror's JS API so that the `change`
// event fires synchronously and formik receives the update before we save.
// keyboard.type / insertText are unreliable in Electron because synthetic key events
// don't always go through CodeMirror's textarea input handler.
await page.evaluate((rowIndex) => {
const rows = document.querySelectorAll('tr');
for (const row of rows) {
if (row.querySelector(`input[name="${rowIndex}.name"]`)) {
const cms = row.querySelectorAll('.CodeMirror');
const cm = (cms[1] as any)?.CodeMirror; // description editor is the 2nd CodeMirror
if (cm) cm.setValue('First line\nSecond line');
break;
}
}
}, 2);
// Wait for the CodeMirror DOM to reflect the value, then give React a tick to flush formik
const plainDescEditor = buildCommonLocators(page).environment.variableDescriptionEditor(2);
await expect(plainDescEditor.locator('.CodeMirror-line').nth(0)).toHaveText('First line');
await page.waitForTimeout(200);
await page.getByTestId('save-env').click();
await expect(page.getByText('Changes saved successfully')).toBeVisible({ timeout: 5000 });
// The IPC write completes before the toast resolves, so the file is ready to read.
const envFilePath = path.join(collectionFixturePath!, 'environments', 'WithDescriptions.bru');
const fileContent = fs.readFileSync(envFilePath, 'utf8');
expect(fileContent).toContain('First line');
expect(fileContent).toContain('Second line');
});
});

View File

@@ -1,7 +1,12 @@
vars {
@description('''Single-line host desc''')
host: http://localhost:3000
}
vars:secret [
@description('''
Secret line one
Secret line two
''')
secretToken
]
]

View File

@@ -1,7 +1,12 @@
vars {
@description('''Single-line host desc''')
host: https://echo.usebruno.com
}
vars:secret [
@description('''
Secret line one
Secret line two
''')
secretToken
]
]

View File

@@ -10,7 +10,8 @@
"value": "http://localhost:3000",
"type": "text",
"enabled": true,
"secret": false
"secret": false,
"description": "Single-line host desc"
},
{
"uid": "3WEAWueN0Ov99uOX0uuuM",
@@ -18,7 +19,8 @@
"value": "",
"type": "text",
"enabled": true,
"secret": true
"secret": true,
"description": "Secret line one\nSecret line two"
}
]
},
@@ -32,7 +34,8 @@
"value": "https://echo.usebruno.com",
"type": "text",
"enabled": true,
"secret": false
"secret": false,
"description": "Single-line host desc"
},
{
"uid": "fSxTRpngl8fxkhrl3z7hA",
@@ -40,7 +43,8 @@
"value": "",
"type": "text",
"enabled": true,
"secret": true
"secret": true,
"description": "Secret line one\nSecret line two"
}
]
}

View File

@@ -13,14 +13,16 @@
"value": "http://localhost:3000",
"type": "text",
"enabled": true,
"secret": false
"secret": false,
"description": "Single-line host desc"
},
{
"name": "secretToken",
"value": "",
"type": "text",
"enabled": true,
"secret": true
"secret": true,
"description": "Secret line one\nSecret line two"
}
]
},
@@ -32,14 +34,16 @@
"value": "https://echo.usebruno.com",
"type": "text",
"enabled": true,
"secret": false
"secret": false,
"description": "Single-line host desc"
},
{
"name": "secretToken",
"value": "",
"type": "text",
"enabled": true,
"secret": true
"secret": true,
"description": "Secret line one\nSecret line two"
}
]
}

View File

@@ -6,14 +6,16 @@
"value": "http://localhost:3000",
"type": "text",
"enabled": true,
"secret": false
"secret": false,
"description": "Single-line host desc"
},
{
"name": "secretToken",
"value": "",
"type": "text",
"enabled": true,
"secret": true
"secret": true,
"description": "Secret line one\nSecret line two"
}
],
"info": {

View File

@@ -6,14 +6,16 @@
"value": "https://echo.usebruno.com",
"type": "text",
"enabled": true,
"secret": false
"secret": false,
"description": "Single-line host desc"
},
{
"name": "secretToken",
"value": "",
"type": "text",
"enabled": true,
"secret": true
"secret": true,
"description": "Secret line one\nSecret line two"
}
],
"info": {

View File

@@ -13,14 +13,16 @@
"value": "http://localhost:3000",
"type": "text",
"enabled": true,
"secret": false
"secret": false,
"description": "Single-line host desc"
},
{
"name": "secretToken",
"value": "",
"type": "text",
"enabled": true,
"secret": true
"secret": true,
"description": "Secret line one\nSecret line two"
}
]
},
@@ -32,14 +34,16 @@
"value": "https://echo.usebruno.com",
"type": "text",
"enabled": true,
"secret": false
"secret": false,
"description": "Single-line host desc"
},
{
"name": "secretToken",
"value": "",
"type": "text",
"enabled": true,
"secret": true
"secret": true,
"description": "Secret line one\nSecret line two"
}
]
}

View File

@@ -6,14 +6,16 @@
"value": "http://localhost:3000",
"type": "text",
"enabled": true,
"secret": false
"secret": false,
"description": "Single-line host desc"
},
{
"name": "secretToken",
"value": "",
"type": "text",
"enabled": true,
"secret": true
"secret": true,
"description": "Secret line one\nSecret line two"
}
],
"info": {

View File

@@ -6,14 +6,16 @@
"value": "https://echo.usebruno.com",
"type": "text",
"enabled": true,
"secret": false
"secret": false,
"description": "Single-line host desc"
},
{
"name": "secretToken",
"value": "",
"type": "text",
"enabled": true,
"secret": true
"secret": true,
"description": "Secret line one\nSecret line two"
}
],
"info": {

View File

@@ -6,14 +6,16 @@
"value": "http://localhost:3000",
"type": "text",
"enabled": true,
"secret": false
"secret": false,
"description": "Single-line host desc"
},
{
"name": "secretToken",
"value": "",
"type": "text",
"enabled": true,
"secret": true
"secret": true,
"description": "Secret line one\nSecret line two"
}
],
"info": {

View File

@@ -1,6 +1,7 @@
import { test, expect } from '../../../../../playwright';
import path from 'path';
import fs from 'fs';
import { buildCommonLocators } from '../../../../utils/page/locators';
test.describe.serial('Collection Environment Import Tests', () => {
test('should import single collection environment', async ({ pageWithUserData: page }) => {
@@ -43,9 +44,17 @@ test.describe.serial('Collection Environment Import Tests', () => {
const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });
await expect(envTab).toBeVisible();
await expect(page.getByRole('row', { name: 'host' }).getByRole('cell').nth(1)).toBeVisible();
const locators = buildCommonLocators(page);
await expect(locators.environment.varRowValueCell('host')).toBeVisible();
const hostDesc = locators.environment.varRowDescriptionEditor('host');
await expect(hostDesc.locator('.CodeMirror-line').first()).toHaveText('Single-line host desc');
await page.getByTestId('responsive-tab-secrets').click();
await expect(page.getByRole('row', { name: 'secretToken' }).getByRole('cell').nth(1)).toBeVisible();
await expect(locators.environment.varRowValueCell('secretToken')).toBeVisible();
const secretDesc = locators.environment.varRowDescriptionEditor('secretToken');
await expect(secretDesc.locator('.CodeMirror-line').nth(0)).toHaveText('Secret line one');
await expect(secretDesc.locator('.CodeMirror-line').nth(1)).toHaveText('Secret line two');
await envTab.hover();
await envTab.getByTestId('request-tab-close-icon').click({ force: true });
@@ -125,11 +134,12 @@ test.describe.serial('Collection Environment Import Tests', () => {
await expect(envTab).toBeVisible();
// Verify prod environment variables
await expect(page.getByRole('row', { name: 'host' }).getByRole('cell').nth(1)).toBeVisible();
const locators = buildCommonLocators(page);
await expect(locators.environment.varRowValueCell('host')).toBeVisible();
// secretToken was imported as a secret, so it lives on the Secrets tab, not Variables.
await page.getByTestId('responsive-tab-secrets').click();
await expect(page.getByRole('row', { name: 'secretToken' }).getByRole('cell').nth(1)).toBeVisible();
await expect(locators.environment.varRowValueCell('secretToken')).toBeVisible();
await envTab.hover();
await envTab.getByTestId('request-tab-close-icon').click({ force: true });

View File

@@ -1,5 +1,6 @@
import { test, expect } from '../../../../../playwright';
import path from 'path';
import { buildCommonLocators } from '../../../../utils/page/locators';
test.describe.serial('Global Environment Import Tests', () => {
test('should import single global environment', async ({ pageWithUserData: page }) => {
@@ -58,9 +59,10 @@ test.describe.serial('Global Environment Import Tests', () => {
await expect(envTab).toBeVisible();
// Verify imported variables
await expect(page.getByRole('row', { name: 'host' }).getByRole('cell').nth(1)).toBeVisible();
const locators = buildCommonLocators(page);
await expect(locators.environment.varRowValueCell('host')).toBeVisible();
await page.getByTestId('responsive-tab-secrets').click();
await expect(page.getByRole('row', { name: 'secretToken' }).getByRole('cell').nth(1)).toBeVisible();
await expect(locators.environment.varRowValueCell('secretToken')).toBeVisible();
await envTab.hover();
await envTab.getByTestId('request-tab-close-icon').click({ force: true });
@@ -142,11 +144,12 @@ test.describe.serial('Global Environment Import Tests', () => {
await expect(envTab).toBeVisible();
// Verify imported variables
await expect(page.getByRole('row', { name: 'host' }).getByRole('cell').nth(1)).toBeVisible();
const locators = buildCommonLocators(page);
await expect(locators.environment.varRowValueCell('host')).toBeVisible();
// secretToken was imported as a secret, so it lives on the Secrets tab, not Variables.
await page.getByTestId('responsive-tab-secrets').click();
await expect(page.getByRole('row', { name: 'secretToken' }).getByRole('cell').nth(1)).toBeVisible();
await expect(locators.environment.varRowValueCell('secretToken')).toBeVisible();
await envTab.hover();
await envTab.getByTestId('request-tab-close-icon').click({ force: true });

View File

@@ -37,7 +37,7 @@ test.describe('Multiline Variables - Write Test', () => {
// Use force:true to bypass Playwright's stability check on the CodeMirror click.
const variableRow = page.locator('tbody tr').filter({ has: page.locator('input[value="multiline_data_json"]') });
await expect(variableRow).toBeVisible();
const codeMirror = variableRow.locator('.CodeMirror');
const codeMirror = variableRow.getByTestId(/^test-multiline-editor-\d+\.value$/);
const jsonValue = `{
"user": {

View File

@@ -0,0 +1,28 @@
info:
name: api
type: folder
seq: 1
request:
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
variables:
- name: baseUrl
value: https://example.com
description: Single-line var desc
- name: apiKey
value: abc123
description: |-
Var line one
Var line two
- name: plain
value: no-desc

View File

@@ -0,0 +1,14 @@
info:
name: ping
type: http
seq: 1
http:
method: GET
url: https://example.com/ping
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

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

View File

@@ -0,0 +1,5 @@
opencollection: "1.0.0"
info:
name: fold-description-yml
bundled: false

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,40 @@
import { test, expect } from '../../../playwright';
import { openFolderSettings } from '../../utils/page';
test.describe('Folder Settings Descriptions (YAML) - Read', () => {
test('reads descriptions from headers and vars in a pre-existing folder.yml', async ({
pageWithUserData: page
}) => {
test.setTimeout(30_000);
await openFolderSettings(page, 'fold-description-yml');
await page.getByTestId('folder-settings-tab-headers').click();
const headerRows = page.locator('table').first().locator('tbody tr');
const versionDescEditor = headerRows.nth(0).getByTestId('column-description').locator('.CodeMirror');
await expect(versionDescEditor.locator('.CodeMirror-line').first()).toHaveText('Single-line header desc');
const multiDescEditor = headerRows.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');
const plainDescEditor = headerRows.nth(2).getByTestId('column-description').locator('.CodeMirror');
await expect(plainDescEditor.locator('.CodeMirror-line').first()).toHaveText('');
await page.getByTestId('folder-settings-tab-vars').click();
const varRows = page.getByTestId('folder-vars-req').locator('tbody tr');
const baseUrlDescEditor = varRows.nth(0).locator('.CodeMirror').nth(1);
await expect(baseUrlDescEditor.locator('.CodeMirror-line').first()).toHaveText('Single-line var desc');
const apiKeyDescEditor = varRows.nth(1).locator('.CodeMirror').nth(1);
await expect(apiKeyDescEditor.locator('.CodeMirror-line').nth(0)).toHaveText('Var line one');
await expect(apiKeyDescEditor.locator('.CodeMirror-line').nth(1)).toHaveText('Var line two');
const plainVarDescEditor = varRows.nth(2).locator('.CodeMirror').nth(1);
await expect(plainVarDescEditor.locator('.CodeMirror-line').first()).toHaveText('');
});
});

View File

@@ -0,0 +1,36 @@
import * as fs from 'fs';
import * as path from 'path';
import { test, expect } from '../../../playwright';
import { openFolderSettings, setTableRowDescriptionValue } from '../../utils/page';
test.describe('Folder Settings Descriptions (YAML) - Write (Headers)', () => {
test('writes a multiline description to a header and persists it to folder.yml', async ({
pageWithUserData: page,
collectionFixturePath
}) => {
await openFolderSettings(page, 'fold-description-yml');
await page.getByTestId('folder-settings-tab-headers').click();
const headersTable = page.locator('table').first();
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 setTableRowDescriptionValue(xPlainRow, '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.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('Folder Settings saved successfully')).toBeVisible({ timeout: 5000 });
const folderYmlPath = path.join(collectionFixturePath!, 'api', 'folder.yml');
const fileContent = fs.readFileSync(folderYmlPath, 'utf8');
expect(fileContent).toContain('description:');
expect(fileContent).toContain('First line');
expect(fileContent).toContain('Second line');
});
});

View File

@@ -0,0 +1,39 @@
import * as fs from 'fs';
import * as path from 'path';
import { test, expect } from '../../../playwright';
import { openFolderSettings, setTableRowDescriptionValue } from '../../utils/page';
test.describe('Folder Settings Descriptions (YAML) - Write (Vars)', () => {
test('writes a multiline description to a pre-request var and persists it to folder.yml', async ({
pageWithUserData: page,
collectionFixturePath
}) => {
await openFolderSettings(page, 'fold-description-yml');
await page.getByTestId('folder-settings-tab-vars').click();
const varsTable = page.getByTestId('folder-vars-req');
await expect(varsTable.locator('tbody tr').first()).toBeVisible();
const plainRowIndex = await varsTable.locator('[data-testid="column-name"] input').evaluateAll(
(inputs) => inputs.findIndex((el) => (el as HTMLInputElement).value === 'plain')
);
if (plainRowIndex === -1) throw new Error('\'plain\' var not found in pre-request vars table');
const plainRow = varsTable.locator('tbody tr').nth(plainRowIndex);
await setTableRowDescriptionValue(plainRow, 'First line\nSecond line');
const descCell = plainRow.getByTestId('column-description');
await expect(descCell.locator('.CodeMirror-line').nth(0)).toHaveText('First line');
await expect(descCell.locator('.CodeMirror-line').nth(1)).toHaveText('Second line');
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('Folder Settings saved successfully')).toBeVisible({ timeout: 5000 });
const folderYmlPath = path.join(collectionFixturePath!, 'api', 'folder.yml');
const fileContent = fs.readFileSync(folderYmlPath, 'utf8');
expect(fileContent).toContain('First line');
expect(fileContent).toContain('Second line');
});
});

View File

@@ -0,0 +1,25 @@
meta {
name: api
}
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''')
baseUrl: https://example.com
@description('''
Var line one
Var line two
''')
apiKey: abc123
plain: no-desc
}

View File

@@ -0,0 +1,11 @@
meta {
name: ping
type: http
seq: 1
}
get {
url: https://example.com/ping
body: none
auth: none
}

View File

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

View File

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

View File

@@ -0,0 +1 @@
{}

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,40 @@
import { test, expect } from '../../../playwright';
import { openFolderSettings } from '../../utils/page';
test.describe('Folder Settings Descriptions - Read', () => {
test('reads descriptions from headers and vars in a pre-existing folder.bru', async ({
pageWithUserData: page
}) => {
test.setTimeout(30_000);
await openFolderSettings(page, 'fold-description');
await page.getByTestId('folder-settings-tab-headers').click();
const headerRows = page.locator('table').first().locator('tbody tr');
const versionDescEditor = headerRows.nth(0).getByTestId('column-description').locator('.CodeMirror');
await expect(versionDescEditor.locator('.CodeMirror-line').first()).toHaveText('Single-line header desc');
const multiDescEditor = headerRows.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');
const plainDescEditor = headerRows.nth(2).getByTestId('column-description').locator('.CodeMirror');
await expect(plainDescEditor.locator('.CodeMirror-line').first()).toHaveText('');
await page.getByTestId('folder-settings-tab-vars').click();
const varRows = page.getByTestId('folder-vars-req').locator('tbody tr');
const baseUrlDescEditor = varRows.nth(0).locator('.CodeMirror').nth(1);
await expect(baseUrlDescEditor.locator('.CodeMirror-line').first()).toHaveText('Single-line var desc');
const apiKeyDescEditor = varRows.nth(1).locator('.CodeMirror').nth(1);
await expect(apiKeyDescEditor.locator('.CodeMirror-line').nth(0)).toHaveText('Var line one');
await expect(apiKeyDescEditor.locator('.CodeMirror-line').nth(1)).toHaveText('Var line two');
const plainVarDescEditor = varRows.nth(2).locator('.CodeMirror').nth(1);
await expect(plainVarDescEditor.locator('.CodeMirror-line').first()).toHaveText('');
});
});

View File

@@ -0,0 +1,36 @@
import * as fs from 'fs';
import * as path from 'path';
import { test, expect } from '../../../playwright';
import { openFolderSettings, setTableRowDescriptionValue } from '../../utils/page';
test.describe('Folder Settings Descriptions - Write (Headers)', () => {
test('writes a multiline description to a header and persists it to folder.bru', async ({
pageWithUserData: page,
collectionFixturePath
}) => {
await openFolderSettings(page, 'fold-description');
await page.getByTestId('folder-settings-tab-headers').click();
const headersTable = page.locator('table').first();
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 setTableRowDescriptionValue(xPlainRow, '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.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('Folder Settings saved successfully')).toBeVisible({ timeout: 5000 });
const folderBruPath = path.join(collectionFixturePath!, 'api', 'folder.bru');
const fileContent = fs.readFileSync(folderBruPath, 'utf8');
expect(fileContent).toContain('@description');
expect(fileContent).toContain('First line');
expect(fileContent).toContain('Second line');
});
});

View File

@@ -0,0 +1,39 @@
import * as fs from 'fs';
import * as path from 'path';
import { test, expect } from '../../../playwright';
import { openFolderSettings, setTableRowDescriptionValue } from '../../utils/page';
test.describe('Folder Settings Descriptions - Write (Vars)', () => {
test('writes a multiline description to a pre-request var and persists it to folder.bru', async ({
pageWithUserData: page,
collectionFixturePath
}) => {
await openFolderSettings(page, 'fold-description');
await page.getByTestId('folder-settings-tab-vars').click();
const varsTable = page.getByTestId('folder-vars-req');
await expect(varsTable.locator('tbody tr').first()).toBeVisible();
const plainRowIndex = await varsTable.locator('[data-testid="column-name"] input').evaluateAll(
(inputs) => inputs.findIndex((el) => (el as HTMLInputElement).value === 'plain')
);
if (plainRowIndex === -1) throw new Error('\'plain\' var not found in pre-request vars table');
const plainRow = varsTable.locator('tbody tr').nth(plainRowIndex);
await setTableRowDescriptionValue(plainRow, 'First line\nSecond line');
const descCell = plainRow.getByTestId('column-description');
await expect(descCell.locator('.CodeMirror-line').nth(0)).toHaveText('First line');
await expect(descCell.locator('.CodeMirror-line').nth(1)).toHaveText('Second line');
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('Folder Settings saved successfully')).toBeVisible({ timeout: 5000 });
const folderBruPath = path.join(collectionFixturePath!, 'api', 'folder.bru');
const fileContent = fs.readFileSync(folderBruPath, 'utf8');
expect(fileContent).toContain('First line');
expect(fileContent).toContain('Second line');
});
});

View File

@@ -0,0 +1,312 @@
{
"name": "descriptions-imported-bru",
"version": "1",
"items": [
{
"type": "folder",
"name": "folder",
"seq": 1,
"root": {
"meta": {
"name": "folder"
},
"request": {
"headers": [
{
"name": "X-Folder-Version",
"value": "1.0",
"enabled": true,
"description": "Single-line folder header desc"
},
{
"name": "X-Folder-Multi",
"value": "value",
"enabled": true,
"description": "Folder header line one\nFolder header line two"
},
{
"name": "X-Folder-Plain",
"value": "plain",
"enabled": true
}
],
"vars": {
"req": [
{
"name": "folderBaseUrl",
"value": "https://folder.example.com",
"enabled": true,
"description": "Single-line folder var desc"
},
{
"name": "folderApiKey",
"value": "folder-key",
"enabled": true,
"description": "Folder var line one\nFolder var line two"
},
{
"name": "folderPlain",
"value": "no-desc",
"enabled": true
}
],
"res": []
},
"script": {},
"tests": null
}
},
"items": [
{
"type": "http",
"name": "request",
"seq": 1,
"request": {
"url": "https://example.com/api",
"method": "POST",
"headers": [
{
"name": "X-Version",
"value": "2.0",
"enabled": true,
"description": "Single-line header desc"
},
{
"name": "X-Multi",
"value": "value",
"enabled": true,
"description": "Header line one\nHeader line two"
},
{
"name": "X-Plain",
"value": "plain-value",
"enabled": true
}
],
"params": [
{
"name": "q",
"value": "search",
"type": "query",
"enabled": true,
"description": "Single-line query desc"
},
{
"name": "page",
"value": "1",
"type": "query",
"enabled": true,
"description": "Multi-line query desc line one\nMulti-line query desc line two"
},
{
"name": "plain-query",
"value": "value",
"type": "query",
"enabled": true
}
],
"body": {
"mode": "multipartForm",
"formUrlEncoded": [
{
"name": "username",
"value": "alice",
"enabled": true,
"description": "Single-line form desc"
},
{
"name": "email",
"value": "alice@example.com",
"enabled": true,
"description": "Multi-line form desc line one\nMulti-line form desc line two"
},
{
"name": "plain-form",
"value": "value",
"enabled": true
}
],
"multipartForm": [
{
"type": "text",
"name": "username",
"value": "alice",
"enabled": true,
"description": "Single-line field desc"
},
{
"type": "text",
"name": "email",
"value": "alice@example.com",
"enabled": true,
"description": "Multi-line field desc line one\nMulti-line field desc line two"
},
{
"type": "text",
"name": "plain-field",
"value": "value",
"enabled": true
}
],
"file": []
},
"script": {},
"vars": {
"req": [
{
"name": "reqBaseUrl",
"value": "https://example.com",
"enabled": true,
"description": "Single-line req var desc"
},
{
"name": "reqApiKey",
"value": "req-key",
"enabled": true,
"description": "Req var line one\nReq var line two"
},
{
"name": "reqPlain",
"value": "no-desc",
"enabled": true
}
],
"res": []
},
"assertions": [],
"tests": "",
"docs": "",
"auth": {
"mode": "none"
}
}
},
{
"type": "http",
"name": "form-request",
"seq": 2,
"request": {
"url": "https://example.com/form",
"method": "POST",
"headers": [],
"params": [],
"body": {
"mode": "formUrlEncoded",
"formUrlEncoded": [
{
"name": "username",
"value": "alice",
"enabled": true,
"description": "Single-line form desc"
},
{
"name": "email",
"value": "alice@example.com",
"enabled": true,
"description": "Multi-line form desc line one\nMulti-line form desc line two"
},
{
"name": "plain-form",
"value": "value",
"enabled": true
}
],
"multipartForm": [],
"file": []
},
"script": {},
"vars": {},
"assertions": [],
"tests": "",
"docs": "",
"auth": {
"mode": "none"
}
}
}
]
}
],
"environments": [
{
"name": "test_env",
"variables": [
{
"name": "envHost",
"value": "http://localhost:3000",
"type": "text",
"enabled": true,
"secret": false,
"description": "Single-line env desc"
},
{
"name": "envToken",
"value": "abc123",
"type": "text",
"enabled": true,
"secret": false,
"description": "Env line one\nEnv line two"
},
{
"name": "envPlain",
"value": "no-desc",
"type": "text",
"enabled": true,
"secret": false
}
]
}
],
"root": {
"request": {
"headers": [
{
"name": "X-Collection-Version",
"value": "2.0",
"enabled": true,
"description": "Single-line collection header desc"
},
{
"name": "X-Collection-Multi",
"value": "value",
"enabled": true,
"description": "Collection header line one\nCollection header line two"
},
{
"name": "X-Collection-Plain",
"value": "plain-value",
"enabled": true
}
],
"vars": {
"req": [
{
"name": "baseUrl",
"value": "https://example.com",
"enabled": true,
"description": "Single-line collection var desc"
},
{
"name": "apiKey",
"value": "abc123",
"enabled": true,
"description": "Collection var line one\nCollection var line two"
},
{
"name": "plain",
"value": "no-desc",
"enabled": true
}
],
"res": []
},
"script": {},
"tests": null
}
},
"brunoConfig": {
"version": "1",
"name": "descriptions-imported-bru",
"type": "collection"
}
}

View File

@@ -0,0 +1,154 @@
opencollection: "1.0.0"
info:
name: descriptions-imported-yml
config:
environments:
- name: test_env
variables:
- name: envHost
value: http://localhost:3000
description: Single-line env desc
- name: envToken
value: abc123
description: |-
Env line one
Env line two
- name: envPlain
value: no-desc
request:
headers:
- name: X-Collection-Version
value: "2.0"
description: Single-line collection header desc
- name: X-Collection-Multi
value: value
description: |-
Collection header line one
Collection header line two
- name: X-Collection-Plain
value: plain-value
variables:
- name: baseUrl
value: https://example.com
description: Single-line collection var desc
- name: apiKey
value: abc123
description: |-
Collection var line one
Collection var line two
- name: plain
value: no-desc
items:
- info:
type: folder
name: folder
seq: 1
request:
headers:
- name: X-Folder-Version
value: "1.0"
description: Single-line folder header desc
- name: X-Folder-Multi
value: value
description: |-
Folder header line one
Folder header line two
- name: X-Folder-Plain
value: plain
variables:
- name: folderBaseUrl
value: https://folder.example.com
description: Single-line folder var desc
- name: folderApiKey
value: folder-key
description: |-
Folder var line one
Folder var line two
- name: folderPlain
value: no-desc
items:
- info:
type: http
name: request
seq: 1
http:
method: POST
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
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: value
runtime:
variables:
- name: reqBaseUrl
value: https://example.com
description: Single-line req var desc
- name: reqApiKey
value: req-key
description: |-
Req var line one
Req var line two
- name: reqPlain
value: no-desc
- info:
type: http
name: form-request
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: email
value: alice@example.com
description: |-
Multi-line form desc line one
Multi-line form desc line two
- name: plain-form
value: value
bundled: true

View File

@@ -0,0 +1,254 @@
import { test, expect, Page, Locator } from '../../../playwright';
import path from 'path';
import {
closeAllCollections,
expandFolder,
importCollection,
selectRequestPaneTab
} from '../../utils/page';
import { buildCommonLocators } from '../../utils/page/locators';
type DescriptionRow = {
singleLine?: string;
multiLine?: [string, string];
empty?: boolean;
};
const expectDescriptionCell = async (descCell: Locator, expected: DescriptionRow) => {
const cm = descCell.getByTestId('column-description').locator('.CodeMirror');
if (expected.empty) {
await expect(cm.locator('.CodeMirror-line').first()).toHaveText('');
return;
}
if (expected.singleLine) {
await expect(cm.locator('.CodeMirror-line').first()).toHaveText(expected.singleLine);
return;
}
if (expected.multiLine) {
await expect(cm.locator('.CodeMirror-line').nth(0)).toHaveText(expected.multiLine[0]);
await expect(cm.locator('.CodeMirror-line').nth(1)).toHaveText(expected.multiLine[1]);
}
};
const expectVarDescriptionCell = async (row: Locator, expected: DescriptionRow) => {
const cm = row.getByTestId('column-description').locator('.CodeMirror');
if (expected.empty) {
await expect(cm.locator('.CodeMirror-line').first()).toHaveText('');
return;
}
if (expected.singleLine) {
await expect(cm.locator('.CodeMirror-line').first()).toHaveText(expected.singleLine);
return;
}
if (expected.multiLine) {
await expect(cm.locator('.CodeMirror-line').nth(0)).toHaveText(expected.multiLine[0]);
await expect(cm.locator('.CodeMirror-line').nth(1)).toHaveText(expected.multiLine[1]);
}
};
const openImportedRequest = async (page: Page, collectionName: string, requestName = 'request') => {
const locators = buildCommonLocators(page);
const collectionScope = locators.sidebar.collectionScope(collectionName);
const collectionRow = locators.sidebar.collection(collectionName);
await expect(collectionRow).toBeVisible();
const folderRow = collectionScope.locator('.collection-item-name').filter({ hasText: 'folder' });
if (!(await folderRow.isVisible().catch(() => false))) {
await collectionRow.click();
await expect(folderRow).toBeVisible();
}
await expandFolder(page, 'folder');
const requestLink = collectionScope
.getByTestId('sidebar-collection-item-row')
.filter({ has: page.getByText(requestName, { exact: true }) });
await expect(requestLink).toBeVisible();
await requestLink.click();
await expect(locators.tabs.activeRequestTab()).toContainText(requestName);
};
const openCollectionSettings = async (page: Page, collectionName: string) => {
await page.locator('#sidebar-collection-name').filter({ hasText: collectionName }).click();
await expect(page.locator('.request-tab .tab-label').filter({ hasText: 'Collection' })).toBeVisible();
};
const openFolderSettings = async (page: Page, collectionName: string) => {
const locators = buildCommonLocators(page);
const collectionScope = locators.sidebar.collectionScope(collectionName);
const collectionRow = locators.sidebar.collection(collectionName);
await expect(collectionRow).toBeVisible();
const folderRow = collectionScope.locator('.collection-item-name').filter({ hasText: 'folder' });
if (!(await folderRow.isVisible().catch(() => false))) {
await collectionRow.click();
await expect(folderRow).toBeVisible();
}
await folderRow.dblclick();
await expect(page.locator('.request-tab .tab-label').filter({ hasText: 'folder' })).toBeVisible();
};
const openEnvironmentConfigure = async (page: Page, collectionName: string, envName: string) => {
const locators = buildCommonLocators(page);
await locators.sidebar.collection(collectionName).click();
await locators.environment.currentEnvironment().click();
await expect(locators.environment.envOption(envName)).toBeVisible();
await locators.environment.envOption(envName).click();
await expect(locators.environment.currentEnvironment().filter({ hasText: envName })).toBeVisible();
await locators.environment.currentEnvironment().click();
await expect(locators.dropdown.item('Configure')).toBeVisible();
await locators.dropdown.item('Configure').click();
await expect(locators.tabs.requestTab('Environments')).toBeVisible();
};
const assertRequestDescriptions = async (page: Page, collectionName: string) => {
await openImportedRequest(page, collectionName, 'request');
await selectRequestPaneTab(page, 'Headers');
const headerRows = page.getByTestId('request-headers-table').locator('tbody tr');
await expectDescriptionCell(headerRows.nth(0), { singleLine: 'Single-line header desc' });
await expectDescriptionCell(headerRows.nth(1), { multiLine: ['Header line one', 'Header line two'] });
await expectDescriptionCell(headerRows.nth(2), { empty: true });
await selectRequestPaneTab(page, 'Params');
const paramRows = page.getByTestId('query-params-table').locator('tbody tr');
await expectDescriptionCell(paramRows.nth(0), { singleLine: 'Single-line query desc' });
await expectDescriptionCell(paramRows.nth(1), {
multiLine: ['Multi-line query desc line one', 'Multi-line query desc line two']
});
await expectDescriptionCell(paramRows.nth(2), { empty: true });
await selectRequestPaneTab(page, 'Vars');
const reqVarRows = page.getByTestId('request-vars-req').locator('tbody tr');
await expectVarDescriptionCell(reqVarRows.nth(0), { singleLine: 'Single-line req var desc' });
await expectVarDescriptionCell(reqVarRows.nth(1), { multiLine: ['Req var line one', 'Req var line two'] });
await expectVarDescriptionCell(reqVarRows.nth(2), { empty: true });
await selectRequestPaneTab(page, 'Body');
const multipartRows = page.getByTestId('multipart-form-table').locator('tbody tr');
await expectDescriptionCell(multipartRows.nth(0), { singleLine: 'Single-line field desc' });
await expectDescriptionCell(multipartRows.nth(1), {
multiLine: ['Multi-line field desc line one', 'Multi-line field desc line two']
});
await expectDescriptionCell(multipartRows.nth(2), { empty: true });
await openImportedRequest(page, collectionName, 'form-request');
await selectRequestPaneTab(page, 'Body');
const formRows = page.getByTestId('form-urlencoded-table').locator('tbody tr');
await expectDescriptionCell(formRows.nth(0), { singleLine: 'Single-line form desc' });
await expectDescriptionCell(formRows.nth(1), {
multiLine: ['Multi-line form desc line one', 'Multi-line form desc line two']
});
await expectDescriptionCell(formRows.nth(2), { empty: true });
};
const assertCollectionDescriptions = async (page: Page, collectionName: string) => {
await openCollectionSettings(page, collectionName);
await page.getByTestId('collection-settings-tab-headers').click();
const headerTable = page.getByTestId('collection-headers');
const headerRows = headerTable.locator('tbody tr');
await expectDescriptionCell(headerRows.nth(0), { singleLine: 'Single-line collection header desc' });
await expectDescriptionCell(headerRows.nth(1), {
multiLine: ['Collection header line one', 'Collection header line two']
});
await expectDescriptionCell(headerRows.nth(2), { empty: true });
await page.getByTestId('collection-settings-tab-vars').click();
const varRows = page.getByTestId('collection-vars-req').locator('tbody tr');
await expectVarDescriptionCell(varRows.nth(0), { singleLine: 'Single-line collection var desc' });
await expectVarDescriptionCell(varRows.nth(1), {
multiLine: ['Collection var line one', 'Collection var line two']
});
await expectVarDescriptionCell(varRows.nth(2), { empty: true });
};
const assertFolderDescriptions = async (page: Page, collectionName: string) => {
await openFolderSettings(page, collectionName);
await page.getByTestId('folder-settings-tab-headers').click();
const headerRows = page.locator('table').first().locator('tbody tr');
await expectDescriptionCell(headerRows.nth(0), { singleLine: 'Single-line folder header desc' });
await expectDescriptionCell(headerRows.nth(1), {
multiLine: ['Folder header line one', 'Folder header line two']
});
await expectDescriptionCell(headerRows.nth(2), { empty: true });
await page.getByTestId('folder-settings-tab-vars').click();
const varRows = page.getByTestId('folder-vars-req').locator('tbody tr');
await expectVarDescriptionCell(varRows.nth(0), { singleLine: 'Single-line folder var desc' });
await expectVarDescriptionCell(varRows.nth(1), { multiLine: ['Folder var line one', 'Folder var line two'] });
await expectVarDescriptionCell(varRows.nth(2), { empty: true });
};
const assertEnvironmentDescriptions = async (page: Page, collectionName: string) => {
const locators = buildCommonLocators(page);
await openEnvironmentConfigure(page, collectionName, 'test_env');
const hostDesc = locators.environment.variableDescriptionEditor(0);
await expect(hostDesc.locator('.CodeMirror-line').first()).toHaveText('Single-line env desc');
const tokenDesc = locators.environment.variableDescriptionEditor(1);
await expect(tokenDesc.locator('.CodeMirror-line').nth(0)).toHaveText('Env line one');
await expect(tokenDesc.locator('.CodeMirror-line').nth(1)).toHaveText('Env line two');
const plainDesc = locators.environment.variableDescriptionEditor(2);
await expect(plainDesc.locator('.CodeMirror-line').first()).toHaveText('');
};
const runDescriptionImportAssertions = async (page: Page, collectionName: string) => {
await assertRequestDescriptions(page, collectionName);
await assertCollectionDescriptions(page, collectionName);
await assertFolderDescriptions(page, collectionName);
await assertEnvironmentDescriptions(page, collectionName);
};
test.describe.configure({ mode: 'serial' });
test.describe('Description import — Bruno JSON (.bru) collection', () => {
test.afterEach(async ({ page }) => {
await closeAllCollections(page);
});
test('preserves descriptions across request, collection, folder, and environment surfaces', async ({
page,
createTmpDir
}, testInfo) => {
testInfo.setTimeout(120_000);
const fixtureFile = path.resolve(__dirname, 'fixtures', 'descriptions-collection-bru.json');
const collectionLocation = await createTmpDir('descriptions-imported-bru');
await importCollection(page, fixtureFile, collectionLocation, {
expectedCollectionName: 'descriptions-imported-bru',
sidebarTimeout: 15000
});
await runDescriptionImportAssertions(page, 'descriptions-imported-bru');
});
});
test.describe('Description import — OpenCollection (.yml) collection', () => {
test.afterEach(async ({ page }) => {
await closeAllCollections(page);
});
test('preserves descriptions across request, collection, folder, and environment surfaces', async ({
page,
createTmpDir
}, testInfo) => {
testInfo.setTimeout(120_000);
const fixtureFile = path.resolve(__dirname, 'fixtures', 'descriptions-collection.yml');
const collectionLocation = await createTmpDir('descriptions-imported-yml');
await importCollection(page, fixtureFile, collectionLocation, {
expectedCollectionName: 'descriptions-imported-yml',
sidebarTimeout: 15000
});
await runDescriptionImportAssertions(page, 'descriptions-imported-yml');
});
});

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();
});

Some files were not shown because too many files have changed in this diff Show More