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
4 changes: 4 additions & 0 deletions .changeset/e2e-test-server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
---

add a test server so e2e tests can check real playground requests
4 changes: 2 additions & 2 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export default [
files: [
'eslint.config.mjs',
'packages/**/src/**/*.{ts,tsx}',
'packages/**/e2e/**/*.{ts,tsx}'
'packages/**/e2e/**/*.{ts,tsx,mjs}'
],
// Lint only changed lines. `diff/ci` diffs against the PR base but no-ops
// when CI is unset, so it cannot replace `diff/diff` locally.
Expand Down Expand Up @@ -145,7 +145,7 @@ export default [
// Tests and dev scripts legitimately log.
files: [
'packages/**/*.{test,spec}.{ts,tsx}',
'packages/**/e2e/**/*.{ts,tsx}'
'packages/**/e2e/**/*.{ts,tsx,mjs}'
],
rules: {
'no-console': 'off'
Expand Down
24 changes: 22 additions & 2 deletions packages/bruno-api-docs/e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Two rules shape every test here:
| Request page — Execution Context (scripts, variables, asserts, tests) | `tests/request/request-execution-context.spec.ts` |
| Script page — title, breadcrumb, source code | `tests/script/script.spec.ts` |
| Light/dark theme switch | `tests/theming/theme-toggle.spec.ts` |
| Playground requests on the wire — headers sent, CORS-blocked requests | `tests/playground/request-on-the-wire.spec.ts` |

## Running the tests

Expand All @@ -31,7 +32,25 @@ npm run test:e2e # run headless
npm run test:e2e:ui # run in Playwright's interactive UI
```

Playwright starts the dev server for you (see `webServer` in `playwright.config.ts`).
Playwright starts the dev server and the test server for you (see `webServer` in
`playwright.config.ts`).

## Test server

`server/test-server.mjs` is a small Node server with no dependencies, on
`http://127.0.0.1:3900` (`testServerURL` in `config/app.config.ts`). It is a different
origin from the app, so the browser applies real CORS rules to every request the
playground sends to it.

| Route | Returns |
|-------|---------|
| `/echo` | 200, JSON of the method, path, query, headers and body it received |
| `/cors/deny` | 200 with no CORS headers, so the browser blocks it |
| `/health` | 200 `ok`, used by Playwright to know the server is up |

Every route except `/cors/deny` answers CORS and preflight requests. Use it for what the
playground sends over the network; keep `page.route` stubs for tests about how a response
is shown. Run it on its own with `node e2e/server/test-server.mjs`.

## Folder layout

Expand Down Expand Up @@ -76,7 +95,8 @@ e2e/
├── playwright/ # the test harness
│ ├── pages.fixture.ts # defines the fixtures
│ └── index.ts # merges them; the single import for specs
├── config/app.config.ts # base URL + how to start the app
├── server/test-server.mjs # local HTTP server for network tests
├── config/app.config.ts # base URL, test server URL + how to start the app
└── tsconfig.json # TypeScript settings for this folder
```

Expand Down
5 changes: 4 additions & 1 deletion packages/bruno-api-docs/e2e/config/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ export interface AppConfig {
baseURL: string;
/** Command Playwright runs to start the app (the `webServer`). */
webServerCommand: string;
/** Origin of the e2e test server (`e2e/server/test-server.mjs`), separate from the app so CORS applies. */
testServerURL: string;
}

export const appConfig: AppConfig = {
baseURL: 'http://127.0.0.1:3001',
webServerCommand: 'npm run dev'
webServerCommand: 'npm run dev',
testServerURL: 'http://127.0.0.1:3900'
};
49 changes: 49 additions & 0 deletions packages/bruno-api-docs/e2e/server/test-server.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { createServer } from 'node:http';

const port = Number(process.env.PORT ?? 3900);

const corsHeaders = (req) => ({
'Access-Control-Allow-Origin': req.headers.origin ?? '*',
'Access-Control-Allow-Methods': req.headers['access-control-request-method'] ?? '*',
'Access-Control-Allow-Headers': req.headers['access-control-request-headers'] ?? '*',
'Access-Control-Expose-Headers': '*',
'Vary': 'Origin'
});

const readBody = async (req) => {
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
return Buffer.concat(chunks).toString('utf8');
};

const send = (res, status, headers, body = '') => {
res.writeHead(status, headers);
res.end(body);
};

const handle = async (req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`);

if (url.pathname === '/cors/deny') return send(res, 200, { 'Content-Type': 'text/plain' }, 'denied');

const cors = corsHeaders(req);
if (req.method === 'OPTIONS') return send(res, 204, cors);

if (url.pathname === '/health') return send(res, 200, { ...cors, 'Content-Type': 'text/plain' }, 'ok');

if (url.pathname === '/echo') {
const echoed = {
method: req.method,
path: url.pathname,
query: Object.fromEntries(url.searchParams),
headers: req.headers,
body: await readBody(req)
};
return send(res, 200, { ...cors, 'Content-Type': 'application/json' }, JSON.stringify(echoed));
}

send(res, 404, { ...cors, 'Content-Type': 'text/plain' }, 'not found');
};

createServer((req, res) => handle(req, res).catch(() => res.destroy()))
.listen(port, '127.0.0.1', () => console.log(`test server on http://127.0.0.1:${port}`));
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { test, expect } from '../../playwright';
import { appConfig } from '../../config/app.config';

const ECHO_URL = `${appConfig.testServerURL}/echo?page=1`;
const DENY_URL = `${appConfig.testServerURL}/cors/deny`;

test.describe('what the playground sends over the network', () => {
test.beforeEach(async ({ playground }) => {
await playground.open('bottom');
await playground.openRequest('get users');
});

test('collection and request headers both reach the server', async ({ page, playground, responsePane }) => {
await playground.urlInput.fill(ECHO_URL);
await expect(playground.urlInput).toHaveValue(ECHO_URL);

const echoed = page.waitForResponse((response) =>
response.url().startsWith(`${appConfig.testServerURL}/echo`) && response.request().method() === 'GET');
await responsePane.send();
const { headers } = await (await echoed).json();

expect(headers['collection-header']).toBe('collection-header-value');
expect(headers['accept']).toBe('application/json');
await expect(responsePane.status).toContainText('200');
});

test('a request the server does not allow cross-origin shows Request blocked', async ({ playground, responsePane }) => {
await playground.urlInput.fill(DENY_URL);
await expect(playground.urlInput).toHaveValue(DENY_URL);

await responsePane.send();

await expect(responsePane.errorTitle).toHaveText('Request blocked');
await expect(responsePane.errorMessage).toContainText('usually CORS');
});
});
18 changes: 13 additions & 5 deletions packages/bruno-api-docs/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,17 @@ export default defineConfig({
use: { ...devices['Desktop Chrome'] },
},
],
webServer: {
command: appConfig.webServerCommand,
url: appConfig.baseURL,
reuseExistingServer: !process.env.CI,
},
webServer: [
{
command: appConfig.webServerCommand,
url: appConfig.baseURL,
reuseExistingServer: !process.env.CI,
},
{
command: 'node e2e/server/test-server.mjs',
url: `${appConfig.testServerURL}/health`,
env: { PORT: new URL(appConfig.testServerURL).port },
reuseExistingServer: !process.env.CI,
},
],
});
Loading