diff --git a/packages/bruno-api-docs/e2e/components/environments/env-editor.component.ts b/packages/bruno-api-docs/e2e/components/environments/env-editor.component.ts index 25d4342b..e842c077 100644 --- a/packages/bruno-api-docs/e2e/components/environments/env-editor.component.ts +++ b/packages/bruno-api-docs/e2e/components/environments/env-editor.component.ts @@ -2,10 +2,22 @@ import type { Locator } from '@playwright/test'; import { BaseComponent } from '../base.component'; export class EnvEditorComponent extends BaseComponent { + // Each card and its inputs carry a per-card index (`-card-0`, `-name-input-0`, …) so an individual + // card is uniquely addressable; the regexes collect every card's field, ordered by index in the DOM. readonly cards = this.page.getByTestId('env-var-cards'); - readonly cardItems = this.cards.locator('.env-card'); - readonly nameInputs = this.cards.getByPlaceholder('Name'); - readonly valueInputs = this.cards.getByPlaceholder('Value'); + readonly cardItems = this.cards.getByTestId(/^env-var-cards-card-\d+$/); + readonly nameInputs = this.cards.getByTestId(/^env-var-cards-name-input-\d+$/); + readonly valueInputs = this.cards.getByTestId(/^env-var-cards-value-input-\d+$/); + readonly descriptionInputs = this.cards.getByTestId(/^env-var-cards-description-input-\d+$/); + + async selectEnvironment(name: string): Promise { + await this.page.getByTestId(`env-pill-${name}`).click(); + } + + /** The card at a given index, addressed directly by its unique test id. */ + card(index: number): Locator { + return this.cards.getByTestId(`env-var-cards-card-${index}`); + } /** The per-variable enable/disable checkbox, addressed by the variable name via its aria-label. */ enableToggle(name: string): Locator { diff --git a/packages/bruno-api-docs/e2e/components/key-value-table/key-value-table.component.ts b/packages/bruno-api-docs/e2e/components/key-value-table/key-value-table.component.ts index ab45e001..35a18b14 100644 --- a/packages/bruno-api-docs/e2e/components/key-value-table/key-value-table.component.ts +++ b/packages/bruno-api-docs/e2e/components/key-value-table/key-value-table.component.ts @@ -7,8 +7,11 @@ export class KeyValueTableComponent extends BaseComponent { readonly table: Locator; readonly nameInputs: Locator; readonly valueInputs: Locator; + readonly descriptionInputs: Locator; + readonly descriptionHeader: Locator; readonly cellErrors: Locator; readonly autocomplete: Locator; + readonly resizeHandles: Locator; constructor(page: Page, testId = 'key-value-table') { super(page, page.getByTestId(testId)); @@ -16,12 +19,20 @@ export class KeyValueTableComponent extends BaseComponent { this.table = page.getByTestId(`${testId}-table`); this.nameInputs = page.getByTestId(`${testId}-name-input`); this.valueInputs = page.getByTestId(`${testId}-value-input`); + this.descriptionInputs = page.getByTestId(`${testId}-description-input`); + this.descriptionHeader = page.getByTestId(`${testId}-description-header`); this.cellErrors = page.getByTestId(`${testId}-error`); this.autocomplete = page.getByTestId('variable-autocomplete'); + this.resizeHandles = this.table.locator('.col-resize-handle'); } /** The per-row enable/disable checkbox, addressed by the row name via its aria-label. */ enableToggle(name: string): Locator { return this.table.getByRole('checkbox', { name: `Enable ${name}` }); } + + /** A column's header cell, addressed by its `col-*` class — used to measure the column width. */ + columnHeader(colClass: string): Locator { + return this.table.locator(`thead th.${colClass}`); + } } diff --git a/packages/bruno-api-docs/e2e/components/playground.component.ts b/packages/bruno-api-docs/e2e/components/playground.component.ts index 87a75bf8..46644ee0 100644 --- a/packages/bruno-api-docs/e2e/components/playground.component.ts +++ b/packages/bruno-api-docs/e2e/components/playground.component.ts @@ -7,6 +7,7 @@ import type { DockMode } from '../../src/utils/playgroundDock'; export class PlaygroundComponent extends BaseComponent { readonly keyValueTable = new KeyValueTableComponent(this.page); + readonly preRequestVars = new KeyValueTableComponent(this.page, 'variables-pre-request'); // The Auth tab lives inside the playground request pane; open it with selectTab('auth'). readonly auth = new RequestAuthComponent(this.page); readonly preRequestScriptEditor = new CodeEditorComponent(this.page, 'scripts-editor-pre-request'); @@ -58,6 +59,11 @@ export class PlaygroundComponent extends BaseComponent { return this.sidebarPanel.getByTestId('sidebar-example').filter({ hasText: exampleName }); } + async open(dock: DockMode = 'bottom'): Promise { + await this.page.goto(`/#/?pg=1&dock=${dock}`); + await this.runner.waitFor({ state: 'visible' }); + } + sidebarItem(name: string): Locator { return this.treeItems.filter({ hasText: name }).first(); } @@ -88,11 +94,6 @@ export class PlaygroundComponent extends BaseComponent { return this.page.getByTestId(`playground-dock-${mode}-panel`); } - async open(dock: DockMode = 'bottom'): Promise { - await this.page.goto(`/#/?pg=1&dock=${dock}`); - await this.runner.waitFor({ state: 'visible' }); - } - async openRequest(name: string): Promise { await this.sidebarItem(name).click(); await this.view.waitFor({ state: 'visible' }); diff --git a/packages/bruno-api-docs/e2e/tests/playground/env-add-variable.spec.ts b/packages/bruno-api-docs/e2e/tests/playground/env-add-variable.spec.ts index cdaea6bc..c7a4faa4 100644 --- a/packages/bruno-api-docs/e2e/tests/playground/env-add-variable.spec.ts +++ b/packages/bruno-api-docs/e2e/tests/playground/env-add-variable.spec.ts @@ -38,4 +38,33 @@ test.describe('Environment variables — adding rows (card view)', () => { await expect(toggle).toBeChecked(); await expect(card).not.toHaveClass(disabled); }); + + test('a variable description is editable below the value and persists across an environment switch', async ({ + playground, + envEditor + }) => { + await playground.open('inline'); + await playground.openEnvironments(); + + // Each variable card carries a Description field (placeholder "Description") under its value. + const description = envEditor.descriptionInputs.first(); + await description.fill('The API host'); + await expect(description).toHaveValue('The API host'); + + // Switching environments and back re-derives the rows from the store; the edit was committed. + await envEditor.selectEnvironment('Prod'); + await envEditor.selectEnvironment('Local'); + await expect(envEditor.descriptionInputs.first()).toHaveValue('The API host'); + }); +}); + +test.describe('Environment variables — descriptions (desktop table)', () => { + test.use({ viewport: { width: 1400, height: 900 } }); + + test('the non-inline table renders a Description column for variables', async ({ page, playground }) => { + // A non-inline dock uses the full KeyValueTable (not the compact cards). + await playground.open('bottom'); + await playground.openEnvironments(); + await expect(page.getByRole('columnheader', { name: 'Description' })).toBeVisible(); + }); }); diff --git a/packages/bruno-api-docs/e2e/tests/playground/keyvalue-table.spec.ts b/packages/bruno-api-docs/e2e/tests/playground/keyvalue-table.spec.ts index fa98c474..38dfcd51 100644 --- a/packages/bruno-api-docs/e2e/tests/playground/keyvalue-table.spec.ts +++ b/packages/bruno-api-docs/e2e/tests/playground/keyvalue-table.spec.ts @@ -71,4 +71,21 @@ test.describe('KeyValueTable — tooltips & mobile scroll', () => { await toggle.check(); await expect(toggle).toBeChecked(); }); + + test('columns are resizable by dragging a header divider', async ({ page, playground }) => { + const { keyValueTable } = playground; + const valueHeader = keyValueTable.columnHeader('col-value'); + const before = (await valueHeader.boundingBox())!.width; + + // Drag the Name/Value divider (the first handle) to the right: Name grows and Value shrinks by + // the same amount (zero-sum), so the Value column gets measurably narrower. + const handle = keyValueTable.resizeHandles.first(); + const box = (await handle.boundingBox())!; + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 + 80, box.y + box.height / 2, { steps: 6 }); + await page.mouse.up(); + + await expect.poll(async () => (await valueHeader.boundingBox())!.width).toBeLessThan(before - 30); + }); }); diff --git a/packages/bruno-api-docs/e2e/tests/playground/playground-descriptions.spec.ts b/packages/bruno-api-docs/e2e/tests/playground/playground-descriptions.spec.ts new file mode 100644 index 00000000..2293398f --- /dev/null +++ b/packages/bruno-api-docs/e2e/tests/playground/playground-descriptions.spec.ts @@ -0,0 +1,72 @@ +import { test, expect } from '../../playwright'; + +// A request whose params, headers, variables and form body all carry authored descriptions. +const DESCRIBED = '/?fixture=descriptions#/?pg=1&dock=bottom'; + +test.describe('Playground — field descriptions', () => { + test.beforeEach(async ({ page, playground }) => { + await page.goto(DESCRIBED); + await playground.openRequest('Described Request'); + }); + + test('query params show a Description column with the authored text', async ({ playground }) => { + await playground.selectTab('params'); + await expect(playground.keyValueTable.descriptionHeader).toBeVisible(); + await expect(playground.keyValueTable.descriptionInputs.first()).toHaveValue( + 'Filter orders by their fulfilment status' + ); + }); + + test('headers show descriptions, normalizing the legacy {content} object form to text', async ({ playground }) => { + await playground.selectTab('headers'); + const descriptions = playground.keyValueTable.descriptionInputs; + await expect(descriptions.nth(0)).toHaveValue('Bearer access token for the API'); + await expect(descriptions.nth(1)).toHaveValue('Correlation id echoed back on the response'); + }); + + test('pre-request variables show the authored description', async ({ playground }) => { + await playground.selectTab('variables'); + await expect(playground.preRequestVars.descriptionInputs.first()).toHaveValue('The order identifier under test'); + }); + + test('form-urlencoded body fields show the authored description', async ({ playground }) => { + await playground.selectTab('body'); + await expect(playground.keyValueTable.descriptionInputs.first()).toHaveValue('The OAuth2 grant type to use'); + }); + + test('a description is editable and the edit persists across a tab switch', async ({ playground }) => { + await playground.selectTab('params'); + const description = playground.keyValueTable.descriptionInputs.first(); + await description.fill('Only open, unfulfilled orders'); + await expect(description).toHaveValue('Only open, unfulfilled orders'); + + // Leaving the tab unmounts the table; returning re-reads from the edited request state. + await playground.selectTab('headers'); + await playground.selectTab('params'); + await expect(playground.keyValueTable.descriptionInputs.first()).toHaveValue('Only open, unfulfilled orders'); + }); + + test('a description is multiline — pressing Enter starts a new line', async ({ page, playground }) => { + await playground.selectTab('params'); + const description = playground.keyValueTable.descriptionInputs.first(); + await description.click(); + await description.press('ControlOrMeta+a'); + await page.keyboard.type('first line'); + await page.keyboard.press('Enter'); + await page.keyboard.type('second line'); + await expect(description).toHaveValue('first line\nsecond line'); + }); + + test('a description does not soft-wrap — long text stays on one line until Enter', async ({ page, playground }) => { + await playground.selectTab('params'); + const description = playground.keyValueTable.descriptionInputs.first(); + await description.click(); + await description.press('ControlOrMeta+a'); + const oneLineHeight = (await description.boundingBox())!.height; + + await page.keyboard.type( + 'this is a very long single line of description text that would wrap onto several lines if soft wrapping were enabled in this column' + ); + expect((await description.boundingBox())!.height).toBeLessThan(oneLineHeight + 4); + }); +}); diff --git a/packages/bruno-api-docs/src/components/HighlightedInput/HighlightedInput.tsx b/packages/bruno-api-docs/src/components/HighlightedInput/HighlightedInput.tsx index 08282f13..75b757c0 100644 --- a/packages/bruno-api-docs/src/components/HighlightedInput/HighlightedInput.tsx +++ b/packages/bruno-api-docs/src/components/HighlightedInput/HighlightedInput.tsx @@ -25,6 +25,7 @@ interface HighlightedInputProps { title?: string; testId?: string; multiline?: boolean; + noWrap?: boolean; } interface HoveredToken { @@ -75,7 +76,8 @@ export const HighlightedInput: React.FC = ({ variablesAutocomplete = true, title, testId, - multiline = false + multiline = false, + noWrap = false }) => { const inputRef = useRef(null); const mirrorRef = useRef(null); @@ -178,6 +180,11 @@ export const HighlightedInput: React.FC = ({ if (!multiline || !el) return; el.style.height = 'auto'; el.style.height = `${el.scrollHeight}px`; + const mirror = mirrorRef.current; + if (mirror) { + mirror.scrollTop = el.scrollTop; + mirror.scrollLeft = el.scrollLeft; + } }, [value, multiline]); useLayoutEffect(() => { @@ -352,14 +359,20 @@ export const HighlightedInput: React.FC = ({ return ( - {multiline ?