mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-08 14:35:03 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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 })
|
||||
|
||||
Reference in New Issue
Block a user