Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,32 @@ 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));
this.container = page.getByTestId(`${testId}-container`);
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}`);
}
}
11 changes: 6 additions & 5 deletions packages/bruno-api-docs/e2e/components/playground.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -58,6 +59,11 @@ export class PlaygroundComponent extends BaseComponent {
return this.sidebarPanel.getByTestId('sidebar-example').filter({ hasText: exampleName });
}

async open(dock: DockMode = 'bottom'): Promise<void> {
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();
}
Expand Down Expand Up @@ -88,11 +94,6 @@ export class PlaygroundComponent extends BaseComponent {
return this.page.getByTestId(`playground-dock-${mode}-panel`);
}

async open(dock: DockMode = 'bottom'): Promise<void> {
await this.page.goto(`/#/?pg=1&dock=${dock}`);
await this.runner.waitFor({ state: 'visible' });
}

async openRequest(name: string): Promise<void> {
await this.sidebarItem(name).click();
await this.view.waitFor({ state: 'visible' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
Original file line number Diff line number Diff line change
@@ -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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ interface HighlightedInputProps {
title?: string;
testId?: string;
multiline?: boolean;
noWrap?: boolean;
}

interface HoveredToken {
Expand Down Expand Up @@ -75,7 +76,8 @@ export const HighlightedInput: React.FC<HighlightedInputProps> = ({
variablesAutocomplete = true,
title,
testId,
multiline = false
multiline = false,
noWrap = false
}) => {
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement | null>(null);
const mirrorRef = useRef<HTMLDivElement | null>(null);
Expand Down Expand Up @@ -178,6 +180,11 @@ export const HighlightedInput: React.FC<HighlightedInputProps> = ({
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(() => {
Expand Down Expand Up @@ -352,14 +359,20 @@ export const HighlightedInput: React.FC<HighlightedInputProps> = ({

return (
<StyledWrapper
className={`highlight-input${multiline ? ' highlight-input--multiline' : ''}`}
className={`highlight-input${multiline ? ' highlight-input--multiline' : ''}${
multiline && noWrap ? ' highlight-input--nowrap' : ''
}`}
onMouseMove={handleMouseMove}
onMouseLeave={scheduleClose}
>
<div className="highlight-input-mirror" aria-hidden="true" ref={mirrorRef}>
{renderTokens(value, isFound)}
</div>
{multiline ? <textarea {...fieldProps} rows={1} /> : <input {...fieldProps} type="text" />}
{multiline ? (
<textarea {...fieldProps} rows={1} wrap={noWrap ? 'off' : undefined} />
) : (
<input {...fieldProps} type="text" />
)}
{hovered && (
<Portal>
<HoverCard
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,17 @@ export const StyledWrapper = styled.div`
white-space: pre-wrap;
overflow-wrap: break-word;
}

&.highlight-input--nowrap .text-input {
overflow-x: auto;
white-space: pre;
overflow-wrap: normal;
}

&.highlight-input--nowrap .highlight-input-mirror {
white-space: pre;
overflow-wrap: normal;
}
`;

export const HoverCard = styled.div`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
}

.key-value-table thead th {
position: relative;
padding: 0.5rem 0.625rem 0.4375rem;
text-align: left;
font-size: 0.75rem;
Expand All @@ -65,6 +66,29 @@
border-right: none;
}

.key-value-table .col-resize-handle {
position: absolute;
top: 0;
bottom: 0;
height: var(--kvt-height);
right: -0.125rem;
z-index: 2;
width: 0.25rem;
cursor: col-resize;
touch-action: none;
background-color: transparent;
}

.key-value-table .col-resize-handle:hover,
.key-value-table .col-resize-handle.is-resizing {
background-color: var(--primary-color);
}

.key-value-table-wrapper.is-resizing {
cursor: col-resize;
user-select: none;
}

.key-value-table col.col-key {
width: calc(30% + 1.625rem);
}
Expand All @@ -73,10 +97,22 @@
width: auto;
}

.key-value-table col.col-description {
width: 25%;
}

.key-value-table col.col-actions {
width: 3.75rem;
}

.key-value-table .col-description {
vertical-align: middle;
}

.key-value-table .col-description .highlight-input {
width: 100%;
}

.key-value-table tbody tr {
transition: background-color 0.1s ease;
}
Expand Down Expand Up @@ -210,19 +246,13 @@
align-items: center;
gap: 0.5rem;
padding-left: 0.625rem;
}

.key-value-table .value-cell-trailing .delete-button {
margin-right: 1.25rem;
padding-right: 0.625rem;
}

.key-value-table .value-cell:has(.secret-value) .value-cell-trailing {
gap: 0.625rem;
padding-left: 0;
}

.key-value-table .value-cell:has(.secret-value) .value-cell-trailing .delete-button {
margin-right: 0.625rem;
padding-right: 0.625rem;
}

.key-value-table .delete-button {
Expand Down
Loading
Loading