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

@@ -475,6 +475,7 @@ const deleteCollectionFromOverview = async (page: Page, collectionName: string)
type ImportCollectionOptions = {
expectedCollectionName?: string;
expectIssues?: boolean;
sidebarTimeout?: number;
};
const importCollection = async (
@@ -515,7 +516,7 @@ const importCollection = async (
if (options.expectedCollectionName) {
await expect(
page.locator('#sidebar-collection-name').filter({ hasText: options.expectedCollectionName })
).toBeVisible();
).toBeVisible({ timeout: options.sidebarTimeout ?? 5000 });
}
// Wait for import issues toast if expected
@@ -802,7 +803,7 @@ const addRowToActiveTab = async (page: Page, name: string, value: string) => {
const row = page.getByTestId(`env-var-row-${name}`);
await row.waitFor({ state: 'visible' });
const codeMirror = row.locator('.CodeMirror');
const codeMirror = row.getByTestId(/^test-multiline-editor-\d+\.value$/).locator('.CodeMirror').first();
await codeMirror.scrollIntoViewIfNeeded();
await codeMirror.click();
await page.keyboard.type(value);
@@ -1356,25 +1357,30 @@ const addMultipartFileToLastRow = async (page: Page, electronApp: ElectronApplic
await test.step(`Add multipart file "${path.basename(filePath)}"`, async () => {
await mockBrowseFiles(electronApp, [filePath]);
const table = buildCommonLocators(page).table('editable-table');
const table = buildCommonLocators(page).table('multipart-form-table');
// The last row is the empty "add" row. Capture its index now, because once
// we set a file the table appends a new empty row — so `.last()` would jump
// to that new row instead of staying on the one we just filled.
const rowIndex = (await table.allRows().count()) - 1;
let rowIndex = (await table.allRows().count()) - 1;
const targetRow = table.allRows().nth(rowIndex);
await expect(targetRow.locator('.upload-btn')).toBeVisible();
await targetRow.locator('.upload-btn').click();
await expect(targetRow.locator('.file-value-cell')).toBeVisible();
const inlineChip = targetRow.getByTestId('multipart-file-chip').filter({ hasText: path.basename(filePath) });
const summary = targetRow.getByTestId('multipart-file-summary');
if (rowIndex < 0) {
rowIndex = 0;
}
await expect(targetRow.getByTestId('multipart-file-upload')).toBeVisible();
await targetRow.getByTestId('multipart-file-upload').click();
const specificRow = table.allRows().nth(rowIndex);
await expect(specificRow.locator('.file-value-cell')).toBeVisible({ timeout: 10000 });
const inlineChip = specificRow.getByTestId('multipart-file-chip').filter({ hasText: path.basename(filePath) });
const summary = specificRow.getByTestId('multipart-file-summary');
await expect(inlineChip.or(summary)).toBeVisible();
});
};
const removeFirstMultipartFile = async (page: Page) => {
await test.step('Remove first multipart file', async () => {
const table = buildCommonLocators(page).table('editable-table');
const table = buildCommonLocators(page).table('multipart-form-table');
const firstRow = table.allRows().first();
await expect(firstRow.locator('.file-value-cell')).toBeVisible();
@@ -1869,6 +1875,36 @@ const readField = async (page: Page, labelText: string): Promise<string> => {
return editor.evaluate((el: any) => (el as any).CodeMirror?.getValue() ?? '');
};
const openFolderSettings = async (page: Page, collectionName: string, folderName = 'api') => {
await test.step(`Open folder settings for "${folderName}" in collection "${collectionName}"`, async () => {
const collectionRow = page.locator('#sidebar-collection-name').filter({ hasText: collectionName });
await expect(collectionRow).toBeVisible();
const folderRow = page
.getByTestId('collections')
.locator('.collection-item-name')
.filter({ hasText: folderName });
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: folderName })).toBeVisible();
});
};
const setTableRowDescriptionValue = async (rowLocator: Locator, value: string) => {
const descCell = rowLocator.getByTestId('column-description');
await descCell.evaluate((el: any, val: string) => {
const cmEl = el.querySelector('.CodeMirror');
if (!cmEl) throw new Error('No CodeMirror in description cell');
const cm = (cmEl as any).CodeMirror;
if (!cm) throw new Error('CodeMirror instance not found');
cm.setValue(val);
}, value);
};
const createExampleFromSidebar = async (page: Page, requestName: string, exampleName: string, description: string = '') => {
const requestRow = page.locator('.collection-item-name').filter({ hasText: requestName }).first();
@@ -2343,6 +2379,8 @@ export {
openRequestInFolder,
setUrlEncoding,
generateCollectionDocs,
openFolderSettings,
setTableRowDescriptionValue,
setAppCode,
enableApp,
exitApp,

View File

@@ -87,23 +87,34 @@ export const buildCommonLocators = (page: Page) => ({
// share a name (e.g. enabled + disabled twins after a script write).
varRowsByValue: (name: string, value: string | RegExp) =>
page.getByTestId(`env-var-row-${name}`)
.filter({ has: page.locator('.CodeMirror-line', { hasText: value }) }),
.filter({ has: page.getByTestId(/^test-multiline-editor-\d+\.value$/).locator('.CodeMirror-line', { hasText: value }) }),
// Each env-var row has an `enabled` and a `secret` checkbox; target the latter
// by its `<index>.secret` name (the formik index is dynamic).
varRowSecretCheckbox: (name: string) => page.getByTestId(`env-var-row-${name}`).locator('input[name$=".secret"]'),
// Eye icon that masks/reveals a secret variable's value.
varRowEyeToggle: (name: string) => page.getByTestId(`env-var-row-${name}`).getByTestId('secret-reveal-toggle'),
varRowLine: (name: string) => page.getByTestId(`env-var-row-${name}`).locator('.CodeMirror-line').first(),
varRowValueCell: (name: string) => page.getByTestId(`env-var-row-${name}`).getByTestId(/^test-multiline-editor-\d+\.value$/),
varRowValueEditor: (name: string) =>
page.getByTestId(`env-var-row-${name}`).getByTestId(/^test-multiline-editor-\d+\.value$/).locator('.CodeMirror').first(),
varRowValueLine: (name: string) =>
page.getByTestId(`env-var-row-${name}`).getByTestId(/^test-multiline-editor-\d+\.value$/).locator('.CodeMirror-line').first(),
varRowLine: (name: string) =>
page.getByTestId(`env-var-row-${name}`).getByTestId(/^test-multiline-editor-\d+\.value$/).locator('.CodeMirror-line').first(),
addVariableButton: () => page.getByTestId('add-variable'),
variableNameInput: (index: number) => page.locator(`input[name="${index}.name"]`),
variableSecretCheckbox: (index: number) => page.locator(`input[name="${index}.secret"]`),
variableRow: (index: number) => page.locator('tr').filter({ has: page.locator(`input[name="${index}.name"]`) }),
variableDescriptionEditor: (index: number) =>
page.locator(`[data-testid="test-multiline-editor-${index}.description"]`).locator('.CodeMirror'),
varRowDescriptionEditor: (name: string) =>
page.getByTestId(`env-var-row-${name}`).getByTestId(/^test-multiline-editor-\d+\.description$/).locator('.CodeMirror').first(),
variableRowByName: (name: string) => page.locator('tbody tr').filter({ has: page.locator(`input[value="${name}"]`) }),
// Targets the `.CodeMirror` wrapper (not `.CodeMirror-line`) so single-line and
// multi-line values (e.g. formatted JSON for @object vars) are both covered —
// CodeMirror renders each visual line as a separate `.CodeMirror-line`, so
// matching on the wrapper is the only way to get the full concatenated text.
variableValue: (name: string) => page.locator('tbody tr').filter({ has: page.locator(`input[value="${name}"]`) }).locator('.CodeMirror').first(),
variableValue: (name: string) =>
page.locator('tbody tr').filter({ has: page.locator(`input[value="${name}"]`) }).getByTestId(/^test-multiline-editor-\d+\.value$/).locator('.CodeMirror').first(),
createEnvButton: () => page.locator('button[id="create-env"]'),
settingsCreateButton: () =>
page.locator('.environments-container .sidebar button[title="Create environment"]'),
@@ -130,10 +141,11 @@ export const buildCommonLocators = (page: Page) => ({
codeMirror: {
byTestId: (testId: string) => page.getByTestId(testId).locator('.CodeMirror').first()
},
// The DataTypeSelector renders a `.type-label` trigger per row (request/folder/
// collection vars + env vars) and a MenuDropdown (role=menu) at page scope.
// The DataTypeSelector exposes a stable trigger per row (request/folder/collection
// vars + env vars). Compact mode shows an icon; full mode shows `.type-label`.
dataTypeSelector: {
typeLabel: (row: Locator) => row.locator('.type-label').first(),
trigger: (row: Locator) => row.getByTestId('datatype-selector-trigger'),
typeLabel: (row: Locator) => row.getByTestId('datatype-selector-trigger'),
// Yellow warning icon shown when a value can't be coerced to its dataType.
mismatchIcon: (row: Locator) => row.locator('svg.text-yellow-600'),
menuItem: (type: string) => page.locator('[role="menu"]').last().getByText(type, { exact: true })