Testing
Blueprint uses Vitest for unit, integration tests and Playwright for end-to-end (E2E) tests.
Blueprint provides shared Playwright utilities and helpers to make writing end-to-end tests easier.
Identifying Test Files​
Test files in Blueprint are identified by the .spec.ts or .spec.tsx file extensions.
Test Suite Structure​
The Blueprint test suites consist of the following:
- End-to-end (E2E) tests
- Unit tests
- Integration tests
End-to-End Tests​
End-to-end testing differs from unit testing, as it focuses on testing the entire application workflow from start to finish to ensure that it behaves as expected.
Blueprint uses Playwright to run E2E tests. It supports all modern rendering engines including Chromium, WebKit, and Firefox.
For more details on Playwright configurations, refer to the Playwright documentation.
These tests are located in the /src/test/playwright folder.
Running End-to-End Tests​
You can run the E2E tests using the following commands inside the Foundation app. Make sure you're testing against the latest build of the app, then run all E2E tests. For example from the repo root with --filter=foundation, or from apps/foundation:
Build the app and run all E2E tests:
# Option 1: Build and test in one step
pnpm test:e2e:with-build
or
# Option 2: Build and test in two steps
pnpm build
# Starts the server and run the tests
pnpm test:e2e
-
Run E2E tests in Chromium only:
pnpm build
pnpm test:e2e:chromiumor
pnpm test:e2e:chromium:with-build
Debugging End-to-End Tests​
To debug the E2E tests, add the --debug flag to the run command. For example:
pnpm test:e2e --debug
Accessibility​
checkPageA11y runs in two ways:
- Post-hook on every test — wired into the custom
pagefixture (utils/fixtures.ts). Any test using the project'stestexport gets an automatic a11y check at the end, against whatever page state the test leaves behind. Opt-out viaskipA11y: true. - Dedicated route suite (
tests/a11y/routes.spec.ts) — visits every app route explicitly in both light and dark themes.
Each check covers:
- axe WCAG 2.2 AA — critical/serious/moderate violations are hard failures; minor violations are advisory annotations. Includes axe's built-in
target-sizerule (WCAG 2.5.8 touch targets). - Reflow (WCAG 1.4.10) — no horizontal scrolling at a 320 px viewport.
Storybook also runs axe on every story via @storybook/addon-a11y on the same WCAG 2.2 AA tag set, catching component-level issues in isolation.
Note: Always import
testfromutils/fixtures.ts(orutils/index.ts), not directly from@playwright/test. The custom fixture wires in the automatic post-test a11y check — importing from Playwright directly bypasses it.
Unit and Integration Tests​
Unit and integration tests are located in the /src/tests folder. These test files have the .spec.ts or .spec.tsx extensions.
Running Unit and Integration Tests​
You can run all unit and integration tests (excluding Playwright tests) using the following command inside the Foundation app. For example: apps/foundation:
pnpm test
Playwright helpers (shared utilities)​
Blueprint provides shared Playwright utilities that make it easy to mock client-side, server-side, and Edge Runtime requests in the same test, using MSW.
The utilities are already wired into Foundation, so most of this is reference material. It's useful when:
- Wiring E2E tests into a new app inside the monorepo
- Understanding what a specific fixture does when reading an existing test
- Adding a new fixture or extending an existing one
What it does​
- Spins up a headless Next.js server per Playwright worker
- Intercepts network traffic with MSW on both Node and the browser
- Proxies middleware (Edge Runtime) requests through a local Express proxy so they can be mocked too
- Provides fixtures for Edge Config, feature flags, GraphQL helpers, and more
Setup​
1. Playwright config​
Create/extend playwright.config.ts:
import { devices, type PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
testDir: './src/tests/playwright',
fullyParallel: true,
timeout: 60000 * 10, // 10 minutes
expect: { timeout: 20000 }, // 20 seconds
retries: process.env.CI ? 2 : 0,
workers: process.env.CI || process.env.PLAYWRIGHT_DEV_MODE ? 1 : undefined,
use: {
locale: 'en-GB',
timezoneId: 'Europe/London',
trace: 'on-first-retry',
ignoreHTTPSErrors: true,
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }
],
};
export default config;
2. Helpers file​
Create src/lib/playwright/test.ts and always import test/expect from there:
import { resolve } from 'node:path';
import { env } from '@/src/lib/env';
import { createPlaywrightHelpers } from '@krakentech/blueprint-playwright';
export const { test, expect } = createPlaywrightHelpers({
nextServerOptions: {
dir: resolve(__dirname, '../../..'), // Next.js root
dev: process.env.PLAYWRIGHT_DEV_MODE === 'true',
quiet: true,
experimentalHttpsServer: true,
},
graphqlEndpoint: env.NEXT_PUBLIC_KRAKEN_API_URL,
});
3. package.json scripts​
{
"test:e2e": "NEXT_PUBLIC_PLAYWRIGHT_MODE=production playwright test",
"test:e2e:build": "NEXT_PUBLIC_PLAYWRIGHT_MODE=production NODE_ENV=production pnpm build:partial && pnpm test:e2e",
"test:e2e:build:ui": "pnpm test:e2e:build --ui",
"test:e2e:dev": "rm -rf .next && NEXT_PUBLIC_PLAYWRIGHT_MODE=development playwright test",
"test:e2e:dev:ui": "pnpm test:e2e:dev --ui",
"test:e2e:ui": "pnpm test:e2e --ui"
}
How it works under the hood​
-
Worker bootstrap
- A headless Next.js server is started (
_nextServer). - An Edge Proxy (Express) starts on
127.0.0.1:<random>. - MSW spins up in Node (
_serverWorker).
- A headless Next.js server is started (
-
Per-test setup
_environmentinjectsx-playwright-edge-portso your middleware knows where to send requests.clientWorkerconverts any MSW handler topage.route.- Extra helpers (
edgeConfig,flags,waitForGraphQLResponse,baseURL) are registered.
-
Runtime
- Browser → intercepted by
clientWorker. - SSR / API routes → intercepted by
serverWorker. - Middleware (Edge) → hits the local proxy → mocked if a handler exists, otherwise forwarded to the real API.
- Browser → intercepted by
Examples​
Client-side render + client mock​
import { test, expect } from '@/lib/playwright/test';
import { graphql, HttpResponse } from 'msw';
import { ViewerQuery } from '@/handlers/queries'; // gql.tada document
test('client mock', async ({ page, clientWorker }) => {
await clientWorker.use(
graphql.query(ViewerQuery, () =>
HttpResponse.json({ data: { viewer: { id: '42' } } }),
),
);
await page.goto('/user'); // Client side public route
await expect(page.getByText('42')).toBeVisible();
});
Server-side render + middleware mock​
import { test, expect } from '@/lib/playwright/test';
import { graphql, HttpResponse } from 'msw';
import { ObtainKrakenTokenMutation } from '@/handlers/mutations';
import { ObtainKrakenTokenMutationMock } from '@/lib/mocks';
test('SSR + middleware mocks', async ({ page, serverWorker }) => {
serverWorker.use(
graphql.mutation(ObtainKrakenTokenMutation, () =>
HttpResponse.json(ObtainKrakenTokenMutationMock),
),
graphql.query('Viewer', () =>
HttpResponse.json({ data: { viewer: { id: '42' } } }),
),
);
await page.goto('/dashboard'); // Server side protected route
await expect(page.getByText('42')).toBeVisible();
});
Playwright helpers — API reference​
createPlaywrightHelpers​
Factory that returns test and expect enhanced with custom fixtures.
import { createPlaywrightHelpers } from '@krakentech/blueprint-playwright';
const { test, expect } = createPlaywrightHelpers({
nextServerOptions: { /* PlaywrightNextServerOptions */ },
graphqlEndpoint: process.env.NEXT_PUBLIC_KRAKEN_API_URL,
});
Signature:
function createPlaywrightHelpers(
options: CreatePlaywrightHelpersOptions,
): {
test: typeof playwright.test;
expect: typeof playwright.expect;
}
CreatePlaywrightHelpersOptions​
| Option | Type | Default | Description |
|---|---|---|---|
nextServerOptions | PlaywrightNextServerOptions | – | Parameters to boot a Next.js server per worker. |
graphqlEndpoint | string | – | Real GraphQL endpoint to use as fallback when a middleware call is not mocked. |
defaultEdgeConfig | EdgeConfigItems | {} | Initial data for the Edge Config store mock. |
defaultFlags | EdgeConfigItems | {} | Initial data for the feature-flags endpoint mock. |
defaultClientHandlers | RequestHandler[] | [] | MSW handlers automatically applied to every client request. |
defaultServerHandlers | RequestHandler[] | [] | MSW handlers automatically applied to every server request. |
PlaywrightNextServerOptions​
| Key | Type | Description |
|---|---|---|
dir | string | Path to your Next.js root. |
dev | boolean | Run the server in dev mode (next dev) instead of production build. Use with an env variable such as process.env.NODE_ENV === "development". |
quiet | boolean | Silence Next.js logs. |
experimentalHttpsServer | boolean | Enable the experimental HTTPS server. |
conf | NextConfig | (optional) Pass an already-imported next.config. Needed when using next.config.ts. |
Fixtures​
All fixtures are injected automatically once you use test from the helpers file — no extra imports required. They're grouped by scope (worker vs test).
Worker-scope​
| Name | Description |
|---|---|
_nextServer | Boots and stops a Next.js server in the same worker. |
_edgeProxy | Starts an Express server that exposes a /playwright-edge-server endpoint. Used by middleware to avoid requests to the real GraphQL endpoint. |
_serverWorker | setupServer instance that mocks every Node fetch/XHR. |
connectOptions | Patches wsEndpoint so the VSCode Playwright extension can attach even when MSW is intercepting sockets. |
All worker fixtures have
auto: true, so they start before the first test of the file and stop once the worker ends.
Test-scope​
| Name | Description |
|---|---|
_environment | Adds x-playwright-edge-port header so your middleware can reach the local proxy. |
serverWorker | Same instance as _serverWorker but reset on every test. Use serverWorker.use() to add handlers. |
clientWorker | Converts any MSW handler to page.route. Use clientWorker.use() to mock browser calls. |
edgeConfig | edgeConfig.use(data) – mocks Edge Config /items and /item/:key. |
flags | flags.use(data) – mocks /api/vercel/flags. |
waitForGraphQLResponse | Await the next GraphQL response containing a specific operation. |
baseURL | String with the local Next.js server URL (e.g. http://127.0.0.1:46001). |
Combined example:
test('mock all the things', async ({ page, serverWorker, clientWorker, edgeConfig, flags }) => {
serverWorker.use(
graphql.query('GetSettings', () =>
HttpResponse.json({ data: { settings: { theme: 'dark' } } }),
),
);
await clientWorker.use(
http.get('/api/notifications', () =>
HttpResponse.json({ data: [] }),
),
);
edgeConfig.use({ featureX: 'A' });
flags.use({ newOnboarding: true });
await page.goto('/');
});
playwrightMiddleware​
Helper that picks the correct GraphQL endpoint inside Next.js middleware (Edge Runtime).
import { playwrightMiddleware } from '@krakentech/blueprint-playwright/middleware';
export function middleware(req: NextRequest) {
const { graphqlEndpoint } = playwrightMiddleware({
req,
enabled: !!process.env.NEXT_PUBLIC_PLAYWRIGHT_MODE,
defaultGraphqlEndpoint: process.env.NEXT_PUBLIC_KRAKEN_API_URL,
});
// your auth / proxy logic here …
}
During tests the helper detects the x-playwright-edge-port header set by _environment and rewrites the endpoint to the local proxy. In production it simply returns the defaultGraphqlEndpoint.
Gotchas & tips​
Multiple Set-Cookie headers​
When mocking a response that sets multiple cookies, use the Headers constructor:
return HttpResponse.json(
{ data: 'ok' },
{
headers: new Headers([
['Set-Cookie', 'access=token; Path=/; HttpOnly'],
['Set-Cookie', 'refresh=token; Path=/; HttpOnly'],
]),
},
);
clientWorker will parse and add all cookies to the Playwright browser context.
Debugging with VSCode​
If you use the VSCode Playwright extension, keep connect mode enabled — the connectOptions fixture rewrites the wsEndpoint from localhost to 127.0.0.1 so MSW can forward the connection correctly.