Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/solid-hairs-cross.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@usebruno/api-docs": patch
---

test(e2e): expand Playwright coverage for example code snippets
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export class CodeSnippetComponent extends BaseComponent {
readonly iconTrigger: Locator;
readonly modal: Locator;
readonly modalCode: Locator;
readonly modalCopyButton: Locator;
readonly modalInterpolate: Locator;

constructor(
Expand All @@ -21,9 +22,32 @@ export class CodeSnippetComponent extends BaseComponent {
this.iconTrigger = this.root.getByTestId(`${base}-trigger`);
this.modal = page.getByTestId(`${base}-modal`);
this.modalCode = this.modal.getByTestId(`${base}-code`);
this.modalCopyButton = this.modal.getByTestId(`${base}-copy`);
this.modalInterpolate = this.modal.getByTestId(`${base}-interpolate-input`);
}

async languageIds(): Promise<string[]> {
return this.tabIds(this.root);
}

async modalLanguageIds(): Promise<string[]> {
return this.tabIds(this.modal);
}

private async tabIds(scope: Locator): Promise<string[]> {
const prefix = `${this.base}-tab-`;
const tabs = scope.getByTestId(new RegExp(`^${prefix}`));
const count = await tabs.count();
const ids: string[] = [];
for (let i = 0; i < count; i++) {
const testId = await tabs.nth(i).getAttribute('data-testid');
if (testId?.startsWith(prefix)) {
ids.push(testId.slice(prefix.length));
}
}
return ids;
}

variableToken(name: string): Locator {
return this.code.getByTestId(`variable-token-${name}`).first();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ export class ExamplesComponent extends BaseComponent {
}

async open(name: string): Promise<void> {
await this.example(name).getByTestId('example-toggle').click();
const toggle = this.example(name).getByTestId('example-toggle');
if ((await toggle.getAttribute('aria-expanded')) === 'true') return;
await toggle.click();
}

// The snippet dialog is portalled to <body>, so it is scoped to the page, not the card.
Expand Down
119 changes: 112 additions & 7 deletions packages/bruno-api-docs/e2e/tests/request/request-examples.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { test, expect } from '../../playwright';
const GET_ALL_CUSTOMERS = ['billing', 'customers', 'Get All Customers'];
const OK_EXAMPLE = '200 OK - first page';
const BAD_REQUEST_EXAMPLE = '400 Bad Request - invalid per_page';
const SNIPPET_LANGUAGES = ['curl', 'javascript', 'python'] as const;
const VARS_REQUEST = '/?fixture=vars#/customers/variables-demo';
const VARS_EXAMPLE = '200 OK';

test.describe('Request page — Examples', () => {
test.beforeEach(async ({ requestPage }) => {
Expand Down Expand Up @@ -92,18 +95,21 @@ test.describe('Request page — Examples', () => {
});

test.describe('Code snippet', () => {
test('offers a Code Snippet trigger on an expanded example', async ({ requestPage }) => {
test('offers a Code Snippet trigger on each saved example', async ({ requestPage }) => {
const { examples } = requestPage;
await expect(examples.snippetButton(OK_EXAMPLE)).toBeVisible();
await expect(examples.snippetButton(OK_EXAMPLE)).toHaveText('Code Snippet');

await examples.open(BAD_REQUEST_EXAMPLE);
await expect(examples.snippetButton(BAD_REQUEST_EXAMPLE)).toBeVisible();
await expect(examples.snippetButton(BAD_REQUEST_EXAMPLE)).toHaveText('Code Snippet');
});

test('opens a dialog with every supported language', async ({ requestPage }) => {
test('opens a dialog with the same languages as the request page Code Snippet', async ({ requestPage }) => {
const { examples } = requestPage;
await examples.openSnippet(OK_EXAMPLE);
await expect(examples.snippetLanguageTab('curl')).toBeVisible();
await expect(examples.snippetLanguageTab('javascript')).toBeVisible();
await expect(examples.snippetLanguageTab('python')).toBeVisible();

expect(await examples.snippet.modalLanguageIds()).toEqual([...SNIPPET_LANGUAGES]);
});

test('shows the example request, and switches language on demand', async ({ requestPage }) => {
Expand All @@ -124,10 +130,22 @@ test.describe('Request page — Examples', () => {
await expect(examples.snippetCode).not.toContainText('per_page=10');
});

test('offers a copy action for the snippet', async ({ requestPage }) => {
test('copied snippet matches the language selected in the modal', async ({ requestPage, page }) => {
await page.context().grantPermissions(['clipboard-read', 'clipboard-write']);
const { examples } = requestPage;
const clipboard = () => page.evaluate(() => navigator.clipboard.readText());
const markers = { curl: 'curl', javascript: 'fetch', python: 'requests' } as const;

await examples.openSnippet(OK_EXAMPLE);
await expect(examples.snippetModal.getByTestId('copy-button')).toBeVisible();
await expect(examples.snippet.modalCopyButton).toBeVisible();

for (const language of SNIPPET_LANGUAGES) {
await examples.snippetLanguageTab(language).click();
await expect(examples.snippetCode).toContainText(markers[language]);
await examples.snippet.modalCopyButton.click();
await expect.poll(clipboard).toContain(markers[language]);
expect(await clipboard()).toContain('per_page=10');
}
});

test('renders only one dialog — the embedded snippet brings no modal of its own', async ({ requestPage, page }) => {
Expand Down Expand Up @@ -156,3 +174,90 @@ test.describe('Request page — Examples', () => {
});
});
});

test.describe('Request page — Examples (no saved examples)', () => {
test('renders no Examples section — and no example Code Snippet button — when the request has none', async ({
requestPage,
page
}) => {
await requestPage.open(['billing', 'customers', 'Get Customers - Filter by Date Range']);

await expect(requestPage.examples.root).toHaveCount(0);
await expect(page.getByTestId('example-code-snippet-trigger')).toHaveCount(0);
});
});

test.describe('Request page — Example code snippet (Show vars)', () => {
test.beforeEach(async ({ requestPage, envSwitcher }) => {
await requestPage.goto(VARS_REQUEST);
await envSwitcher.selectEnvironment('Dev');
});

test('keeps {{var}} placeholders in the example snippet when Show vars is off', async ({
requestPage,
envSwitcher
}) => {
const { examples } = requestPage;
await expect(envSwitcher.showVarsToggle).toHaveAttribute('aria-checked', 'false');

await examples.open(VARS_EXAMPLE);
await examples.openSnippet(VARS_EXAMPLE);
await expect(examples.snippetCode).toContainText('{{host}}');
await expect(examples.snippetCode).toContainText('{{exampleOnly}}');
await expect(examples.snippetCode).not.toContainText('https://api.dev.example.com');
});

test('interpolates example snippet variables when Show vars is on', async ({
requestPage,
envSwitcher,
page
}) => {
await page.context().grantPermissions(['clipboard-read', 'clipboard-write']);
const { examples } = requestPage;
const clipboard = () => page.evaluate(() => navigator.clipboard.readText());

await envSwitcher.toggle();
await expect(envSwitcher.showVarsToggle).toHaveAttribute('aria-checked', 'true');

await examples.open(VARS_EXAMPLE);
await examples.openSnippet(VARS_EXAMPLE);
await expect(examples.snippet.modalInterpolate).toBeChecked();
await expect(examples.snippetCode).toContainText('https://api.dev.example.com/customers');
await expect(examples.snippetCode).toContainText('example-value');
await expect(examples.snippetCode).not.toContainText('{{host}}');
await expect(examples.snippetCode).not.toContainText('{{exampleOnly}}');

await examples.snippet.modalCopyButton.click();
await expect.poll(clipboard).toContain('https://api.dev.example.com/customers');
const copied = await clipboard();
expect(copied).toContain('example-value');
expect(copied).not.toContain('{{host}}');
expect(copied).not.toContain('{{exampleOnly}}');
});

test('Interpolate Variables follows Show vars on open, and interpolates without flipping the page toggle', async ({
requestPage,
envSwitcher
}) => {
const { examples } = requestPage;
await expect(envSwitcher.showVarsToggle).toHaveAttribute('aria-checked', 'false');

await examples.open(VARS_EXAMPLE);
await examples.openSnippet(VARS_EXAMPLE);
await expect(examples.snippet.modalInterpolate).not.toBeChecked();
await expect(examples.snippetCode).toContainText('{{host}}');
await expect(examples.snippetCode).toContainText('{{exampleOnly}}');

await examples.snippet.modalInterpolate.setChecked(true);
Comment thread
lokesh-bruno marked this conversation as resolved.
await expect(examples.snippetCode).toContainText('https://api.dev.example.com/customers');
await expect(examples.snippetCode).toContainText('example-value');
await expect(examples.snippetCode).not.toContainText('{{host}}');
await expect(examples.snippetCode).not.toContainText('{{exampleOnly}}');
await expect(envSwitcher.showVarsToggle).toHaveAttribute('aria-checked', 'false');

await examples.snippet.modalInterpolate.setChecked(false);
await expect(examples.snippetCode).toContainText('{{host}}');
await expect(examples.snippetCode).toContainText('{{exampleOnly}}');
await expect(examples.snippetCode).not.toContainText('https://api.dev.example.com');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export const SnippetTabs: React.FC<SnippetTabsProps> = ({
<ExpandIcon />
</button>
) : (
<CopyButton text={copyText} label="Copy code" className="snippet-copy" />
<CopyButton text={copyText} label="Copy code" className="snippet-copy" testId={`${testId}-copy`} />
)}
</div>
<Code
Expand Down
Loading