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 @@ -10,13 +10,15 @@ export class CodeEditorComponent extends BaseComponent {
readonly copyButton: Locator;
readonly suggestions: Locator;
private readonly surface: Locator;
private readonly focused: Locator;
private readonly lines: Locator;
private readonly ready: Locator;

constructor(page: Page, testId: string) {
super(page, page.getByTestId(testId));
this.copyButton = this.root.getByTestId(`${testId}-copy`);
this.surface = this.root.locator('.monaco-editor');
this.focused = this.root.locator('.monaco-editor.focused');
this.lines = this.root.locator('.view-lines');
this.suggestions = page.locator('.suggest-widget.visible');
this.ready = page.locator(`[data-testid="${testId}"][data-editor-ready="true"]`);
Expand All @@ -29,5 +31,11 @@ export class CodeEditorComponent extends BaseComponent {
// editor to report ready before typing — otherwise the trigger fires against an untagged model.
await this.ready.waitFor({ state: 'attached', timeout: 20000 });
await this.lines.click();
await this.focused.waitFor({ state: 'attached', timeout: 20000 });
}

async typeAndSuggest(text: string): Promise<void> {
await this.page.keyboard.type(text, { delay: 30 });
await this.page.keyboard.press('Control+Space');
}
}
21 changes: 17 additions & 4 deletions packages/bruno-api-docs/e2e/tests/folder/folder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,24 @@ test.describe('Folder page', () => {
await expect(folderPage.requestCount).toHaveText('3 requests');
});

test('shows the "No folder configuration" empty state when the folder has no config', async ({ folderPage }) => {
test('shows config inherited from the collection even when the folder has no own config', async ({ folderPage }) => {
await folderPage.open(['Realtime']);
await expect(folderPage.emptyState).toBeVisible();
await expect(folderPage.emptyState).toContainText('No folder configuration');
await expect(folderPage.configuration.root).toBeHidden();
await expect(folderPage.configuration.root).toBeVisible();
await expect(folderPage.emptyState).toBeHidden();

await test.step('inherited headers are shown with a count chip and a goto-source link', async () => {
await expect(folderPage.configuration.headers).toBeVisible();
await expect(folderPage.configuration.headers).toContainText('collection-header');
await expect(folderPage.configuration.headers).toContainText('header inherited');
await expect(folderPage.configuration.headers.getByTestId('inherited-source').first()).toBeVisible();
});

await test.step('inherited variables are shown, excluding disabled ones', async () => {
await expect(folderPage.configuration.vars).toBeVisible();
await expect(folderPage.configuration.vars).toContainText('collection_pre_var');
await expect(folderPage.configuration.vars).toContainText('1 var inherited');
await expect(folderPage.configuration.vars).not.toContainText('collection-var-value');
});
});

test('renders the configuration with inherited auth and folder-level scripts', async ({ folderPage }) => {
Expand Down
51 changes: 42 additions & 9 deletions packages/bruno-api-docs/e2e/tests/overview/overview.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,10 @@ test.describe('Collection Overview', () => {
});
});

test('mobile: every Disabled chip stays pinned at the row end without overflowing', async ({ overviewPage, page }) => {
test('mobile: values truncate so the Disabled chip stays pinned in the card (no whole-table scroll)', async ({
overviewPage,
page
}) => {
await page.setViewportSize({ width: 360, height: 800 });
const { configuration } = overviewPage;

Expand All @@ -107,17 +110,47 @@ test.describe('Collection Overview', () => {
expect(chip.x).toBeGreaterThanOrEqual(valueBox.x + valueBox.width);
});

await test.step(`row ${i}: the chip stays within the config card, never clipped or overflowing`, () => {
expect(chip.x + chip.width).toBeLessThanOrEqual(card.x + card.width);
await test.step(`row ${i}: the chip stays pinned within the card, never scrolled off`, () => {
expect(chip.x + chip.width).toBeLessThanOrEqual(card.x + card.width + 1);
});
}

await test.step('the table does not scroll on mobile — the value truncates instead', async () => {
const info = await configuration.root
.locator('.property-table')
.first()
.evaluate((el) => ({ scrollWidth: el.scrollWidth, clientWidth: el.clientWidth }));
expect(info.scrollWidth).toBeLessThanOrEqual(info.clientWidth + 1);
});
});

await test.step(`row ${i}: the value keeps its truncation styling, so a long value ellipsizes rather than pushing the chip off`, async () => {
const style = await value.evaluate((el) => {
const cs = getComputedStyle(el);
return { overflow: cs.overflow, textOverflow: cs.textOverflow, whiteSpace: cs.whiteSpace };
});
expect(style).toEqual({ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' });
test('desktop: every property table (single- and multi-row) scrolls as one unit when a value overflows', async ({
overviewPage,
page
}) => {
await page.setViewportSize({ width: 1280, height: 800 });
const tables = overviewPage.configuration.root.locator('.property-table');
const count = await tables.count();
expect(count).toBeGreaterThan(0);

for (let i = 0; i < count; i += 1) {
const table = tables.nth(i);
const rowCount = await table.locator('.property-row').count();
await table.getByTestId('property-value').first().evaluate((el) => {
el.textContent = 'x'.repeat(400);
});
const info = await table.evaluate((el) => {
const cs = getComputedStyle(el);
return {
overflowX: cs.overflowX,
borderRightWidth: cs.borderRightWidth,
scrollWidth: el.scrollWidth,
clientWidth: el.clientWidth
};
});
expect(info.overflowX, `table ${i} (${rowCount} rows) overflowX`).toBe('auto');
expect(info.scrollWidth, `table ${i} (${rowCount} rows) should scroll`).toBeGreaterThan(info.clientWidth);
expect(info.borderRightWidth, `table ${i} (${rowCount} rows) keeps its right border`).not.toBe('0px');
}
});
});
26 changes: 26 additions & 0 deletions packages/bruno-api-docs/e2e/tests/request/request-examples.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,32 @@ test.describe('Request page — Examples', () => {
await expect(examples.requestBody(OK_EXAMPLE)).toContainText('10');
});

test('aligns the Query heading with its values, with padding consistent with the Headers tab', async ({
requestPage
}) => {
const { examples } = requestPage;
const card = examples.example(OK_EXAMPLE);

const params = await card.locator('.request-params-group').first().evaluate((el) => {
const heading = el.querySelector('.request-params-heading') as HTMLElement;
const key = el.querySelector('.property-key') as HTMLElement;
return {
heading: Math.round(heading.getBoundingClientRect().left),
key: Math.round(key.getBoundingClientRect().left)
};
});
// The Query heading lines up with its own values.
expect(params.key).toBe(params.heading);

// ...and the values keep the same left padding as the Headers tab (consistent across sections).
await examples.selectRequestTab(OK_EXAMPLE, 'headers');
const headerKey = await card
.locator('.property-row .property-key')
.first()
.evaluate((el) => Math.round(el.getBoundingClientRect().left));
expect(headerKey).toBe(params.key);
});

test('switches to the Headers tab to reveal the request headers', async ({ requestPage }) => {
const { examples } = requestPage;
await examples.selectRequestTab(OK_EXAMPLE, 'headers');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,21 @@ test.describe('Scripts editor autocomplete', () => {
await playground.selectTab('scripts');
});

test('offers req/bru completions in the pre-request script editor', async ({ page, playground }) => {
test('offers req/bru completions in the pre-request script editor', async ({ playground }) => {
const editor = playground.preRequestScriptEditor;
await editor.focus();
await page.keyboard.type('bru.getEnvVar');
await editor.typeAndSuggest('bru.getEnvVar');

await expect(editor.suggestions).toBeVisible();
await expect(editor.suggestions).toContainText('getEnvVar(key)');
});

test('offers res completions in the post-response script editor', async ({ page, playground }) => {
test('offers res completions in the post-response script editor', async ({ playground }) => {
await playground.selectScriptTab('post-response');

const editor = playground.postResponseScriptEditor;
await editor.focus();
await page.keyboard.type('res.getBody');
await editor.typeAndSuggest('res.getBody');

await expect(editor.suggestions).toBeVisible();
await expect(editor.suggestions).toContainText('getBody()');
Expand Down
27 changes: 27 additions & 0 deletions packages/bruno-api-docs/src/assets/icons/GoToIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import type { SVGProps } from 'react';

type GoToIconProps = Omit<SVGProps<SVGSVGElement>, 'color'> & {
width?: number | string;
height?: number | string;
color?: string;
};

export const GoToIcon: React.FC<GoToIconProps> = ({ width = 18, height = 18, color = 'currentColor', ...rest }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width}
height={height}
viewBox="0 0 24 24"
fill="none"
stroke={color}
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden={true}
{...rest}
>
<path d="M7 17 17 7" />
<path d="M7 7h10v10" />
</svg>
);
1 change: 1 addition & 0 deletions packages/bruno-api-docs/src/assets/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export * from './HamburgerIcon';
export * from './OverflowIcon';
export * from './BrunoGlyph';
export * from './GlobeIcon';
export * from './GoToIcon';
export * from './CubeIcon';
export * from './ChevronLeftIcon';
export * from './ChevronRightIcon';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,31 @@
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { describe, it, expect } from 'vitest';
import { useRenderToDom } from '../../hooks/useRenderToDom';
import { getByTestId } from '../../test-utils/dom';
import { ContentTypeBadge } from './ContentTypeBadge';

describe('ContentTypeBadge', () => {
it('renders the label', () => {
expect(renderToStaticMarkup(<ContentTypeBadge label="application/json" />)).toContain('application/json');
});

it('is a plain, non-interactive chip by default', () => {
const root = useRenderToDom(<ContentTypeBadge label="Inherited" testId="chip" />);
const chip = getByTestId(root, 'chip');
expect(chip.getAttribute('role')).toBeFalsy();
expect(chip.classNames).not.toContain('content-type-badge--interactive');
});

it('becomes a keyboard-focusable button when given onClick, keeping the same chip and a title', () => {
const root = useRenderToDom(
<ContentTypeBadge label="Inherited from collection" title="Inherited from collection: My API" onClick={() => {}} testId="chip" />
);
const chip = getByTestId(root, 'chip');
expect(chip.text).toContain('Inherited from collection');
expect(chip.getAttribute('role')).toBe('button');
expect(chip.getAttribute('tabindex')).toBe('0');
expect(chip.getAttribute('title')).toBe('Inherited from collection: My API');
expect(chip.classNames).toContain('content-type-badge--interactive');
});
});
Original file line number Diff line number Diff line change
@@ -1,15 +1,39 @@
import React from 'react';
import { cx } from '../../utils/cx';
import { StyledWrapper } from './StyledWrapper';

interface ContentTypeBadgeProps {
label: string;
className?: string;
onClick?: () => void;
title?: string;
testId?: string;
}

export const ContentTypeBadge: React.FC<ContentTypeBadgeProps> = ({ label, className }) => (
<StyledWrapper className={['content-type-badge', className].filter(Boolean).join(' ')}>
{label}
</StyledWrapper>
);
export const ContentTypeBadge: React.FC<ContentTypeBadgeProps> = ({ label, className, onClick, title, testId }) => {
const interactive = Boolean(onClick);
return (
<StyledWrapper
className={cx('content-type-badge', { 'content-type-badge--interactive': interactive }, className)}
title={title}
data-testid={testId}
onClick={onClick}
role={interactive ? 'button' : undefined}
tabIndex={interactive ? 0 : undefined}
onKeyDown={
interactive
? (event: React.KeyboardEvent) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
onClick!();
}
}
: undefined
}
>
{label}
</StyledWrapper>
);
};

export default ContentTypeBadge;
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,14 @@ export const StyledWrapper = styled.span`
line-height: 1;
color: var(--oc-colors-text-muted);
background-color: var(--badge-bg);

&.content-type-badge--interactive {
cursor: pointer;
border: none;
transition: color 0.12s ease, background-color 0.12s ease;
}
&.content-type-badge--interactive:hover,
&.content-type-badge--interactive:focus-visible {
color: var(--primary-color);
}
`;
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,21 @@ describe('ExecutionContext', () => {
expect(query(query(root, tabSelector('tests')), '.tab-count').text).toBe('2');
});

it('counts inherited vars in the tab count and shows an "N vars inherited" chip on the Variables line', () => {
const source = { level: 'folder' as const, name: 'Folder A', uuid: 'f1' };
const root = useRenderToDom(
full({
inheritedPreVars: [{ name: 'baseUrl', value: 'x', source }],
inheritedPostVars: [{ name: 'traceId', expression: 'res.headers.trace', source }]
})
);
expect(query(query(root, tabSelector('variables')), '.tab-count').text).toBe('4');
expect(query(root, '[data-testid="execution-context"]').text).toContain('2 vars inherited');
const panel = query(root, panelSelector('variables'));
expect(panel.text).toContain('baseUrl');
expect(panel.querySelector('[data-testid="inherited-source"]')).not.toBeNull();
});

it('mounts only the active tab panel (Variables first)', () => {
const root = useRenderToDom(full());
expect(query(root, panelSelector('variables')).text).toContain('sessionId');
Expand Down
Loading
Loading