From 08f92bfc6d147ec25769a1ad5c46ca6ddaef29c3 Mon Sep 17 00:00:00 2001 From: Arpit Bruno Date: Fri, 16 Oct 2026 12:41:37 +0530 Subject: [PATCH 1/6] add a no-dependency test server for network tests --- eslint.config.mjs | 4 +- .../bruno-api-docs/e2e/server/test-server.mjs | 46 +++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 packages/bruno-api-docs/e2e/server/test-server.mjs diff --git a/eslint.config.mjs b/eslint.config.mjs index f992bef5..4de1ea4e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -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. @@ -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' diff --git a/packages/bruno-api-docs/e2e/server/test-server.mjs b/packages/bruno-api-docs/e2e/server/test-server.mjs new file mode 100644 index 00000000..19666c89 --- /dev/null +++ b/packages/bruno-api-docs/e2e/server/test-server.mjs @@ -0,0 +1,46 @@ +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); +}; + +createServer(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'); +}).listen(port, '127.0.0.1', () => console.log(`test server on http://127.0.0.1:${port}`)); From 022f87f5dc74fc3cd169e10a72f8b39e89b1318f Mon Sep 17 00:00:00 2001 From: Arpit Bruno Date: Fri, 16 Oct 2026 13:16:23 +0530 Subject: [PATCH 2/6] start the test server alongside the app in playwright --- .../bruno-api-docs/e2e/config/app.config.ts | 5 ++++- packages/bruno-api-docs/playwright.config.ts | 18 +++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/bruno-api-docs/e2e/config/app.config.ts b/packages/bruno-api-docs/e2e/config/app.config.ts index 89324194..c72463de 100644 --- a/packages/bruno-api-docs/e2e/config/app.config.ts +++ b/packages/bruno-api-docs/e2e/config/app.config.ts @@ -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' }; diff --git a/packages/bruno-api-docs/playwright.config.ts b/packages/bruno-api-docs/playwright.config.ts index 02942c61..3ad373a3 100644 --- a/packages/bruno-api-docs/playwright.config.ts +++ b/packages/bruno-api-docs/playwright.config.ts @@ -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, + }, + ], }); From f77e6284713a80ddf9a7e7b6bba45c93251fcdb7 Mon Sep 17 00:00:00 2001 From: Arpit Bruno Date: Fri, 16 Oct 2026 13:16:58 +0530 Subject: [PATCH 3/6] check collection and request headers reach the server --- .../playground/request-on-the-wire.spec.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 packages/bruno-api-docs/e2e/tests/playground/request-on-the-wire.spec.ts diff --git a/packages/bruno-api-docs/e2e/tests/playground/request-on-the-wire.spec.ts b/packages/bruno-api-docs/e2e/tests/playground/request-on-the-wire.spec.ts new file mode 100644 index 00000000..29f24aa0 --- /dev/null +++ b/packages/bruno-api-docs/e2e/tests/playground/request-on-the-wire.spec.ts @@ -0,0 +1,25 @@ +import { test, expect } from '../../playwright'; +import { appConfig } from '../../config/app.config'; + +const ECHO_URL = `${appConfig.testServerURL}/echo?page=1`; + +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'); + }); +}); From 2ede81f4b78784edbc5d5a82f00b9a039358ac4b Mon Sep 17 00:00:00 2001 From: Arpit Bruno Date: Fri, 16 Oct 2026 13:17:43 +0530 Subject: [PATCH 4/6] check a cors-blocked request shows request blocked --- .../e2e/tests/playground/request-on-the-wire.spec.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/bruno-api-docs/e2e/tests/playground/request-on-the-wire.spec.ts b/packages/bruno-api-docs/e2e/tests/playground/request-on-the-wire.spec.ts index 29f24aa0..b5f427f0 100644 --- a/packages/bruno-api-docs/e2e/tests/playground/request-on-the-wire.spec.ts +++ b/packages/bruno-api-docs/e2e/tests/playground/request-on-the-wire.spec.ts @@ -2,6 +2,7 @@ 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 }) => { @@ -22,4 +23,14 @@ test.describe('what the playground sends over the network', () => { 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'); + }); }); From 24224b06332e0e9cd1ff8a711d7df71c82c05483 Mon Sep 17 00:00:00 2001 From: Arpit Bruno Date: Fri, 16 Oct 2026 13:18:08 +0530 Subject: [PATCH 5/6] document the test server and add an empty changeset --- .changeset/e2e-test-server.md | 4 ++++ packages/bruno-api-docs/e2e/README.md | 24 ++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 .changeset/e2e-test-server.md diff --git a/.changeset/e2e-test-server.md b/.changeset/e2e-test-server.md new file mode 100644 index 00000000..6eef1802 --- /dev/null +++ b/.changeset/e2e-test-server.md @@ -0,0 +1,4 @@ +--- +--- + +add a test server so e2e tests can check real playground requests diff --git a/packages/bruno-api-docs/e2e/README.md b/packages/bruno-api-docs/e2e/README.md index 24990aff..5b3288dd 100644 --- a/packages/bruno-api-docs/e2e/README.md +++ b/packages/bruno-api-docs/e2e/README.md @@ -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 @@ -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 @@ -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 ``` From 93ecfa41ff4594500619ff58876dfc2086e4fa96 Mon Sep 17 00:00:00 2001 From: Arpit Bruno Date: Fri, 16 Oct 2026 13:20:12 +0530 Subject: [PATCH 6/6] keep the test server up when a client aborts a request --- packages/bruno-api-docs/e2e/server/test-server.mjs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/bruno-api-docs/e2e/server/test-server.mjs b/packages/bruno-api-docs/e2e/server/test-server.mjs index 19666c89..47ed7ca6 100644 --- a/packages/bruno-api-docs/e2e/server/test-server.mjs +++ b/packages/bruno-api-docs/e2e/server/test-server.mjs @@ -21,7 +21,7 @@ const send = (res, status, headers, body = '') => { res.end(body); }; -createServer(async (req, res) => { +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'); @@ -43,4 +43,7 @@ createServer(async (req, res) => { } send(res, 404, { ...cors, 'Content-Type': 'text/plain' }, 'not found'); -}).listen(port, '127.0.0.1', () => console.log(`test server on http://127.0.0.1:${port}`)); +}; + +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}`));