E2E Testing with Feature Flags
One of FlagBridge's key differentiators is first-class support for testing feature flags in your E2E and integration test suite.
The core idea: each test creates an isolated session with specific flag overrides. These overrides only affect requests that include the session token — all other users and tests are unaffected.
Test A (session: sess_A) → new-checkout-flow = true
Test B (session: sess_B) → new-checkout-flow = false
Production traffic → new-checkout-flow = [real evaluation]INFO
You need a Test API key (fb_test_) for the testing endpoints. Create one in your project settings or via the Authentication API.
How it works
- Before each test, create a session via the Testing API
- Set flag overrides for the specific flags your test needs
- Pass the session token to your app (via cookie, header, or query param)
- Your app evaluates flags normally — but gets the overridden values
- After the test, destroy the session
The session token is passed through your app in X-FlagBridge-Session header. The FlagBridge SDK reads this header automatically.
Playwright
Setup
Install the helper:
pnpm add -D @flagbridge/sdk-nodeCreate a fixtures/flags.ts file:
// tests/fixtures/flags.ts
import { test as base } from '@playwright/test';
import { createTestingClient } from '@flagbridge/sdk-node/testing';
type FlagFixtures = {
flags: {
override(flagKey: string, value: boolean | string): Promise<void>;
sessionToken: string;
};
};
const testClient = createTestingClient({
apiKey: process.env.FLAGBRIDGE_TEST_API_KEY!,
baseUrl: process.env.FLAGBRIDGE_BASE_URL ?? 'http://localhost:8080',
});
export const test = base.extend<FlagFixtures>({
flags: async ({ page }, use) => {
const session = await testClient.createSession();
await use({
override: (flagKey, value) => session.override(flagKey, value),
sessionToken: session.token,
});
// Pass the session token to the app via header
await page.setExtraHTTPHeaders({
'X-FlagBridge-Session': session.token,
});
await session.destroy();
},
});
export { expect } from '@playwright/test';WARNING
Call page.setExtraHTTPHeaders before navigating to any page so every request includes the session token.
Writing tests
// tests/checkout.spec.ts
import { test, expect } from './fixtures/flags';
test('new checkout flow is shown when flag is enabled', async ({ page, flags }) => {
await flags.override('new-checkout-flow', true);
// Set the header before navigation
await page.setExtraHTTPHeaders({
'X-FlagBridge-Session': flags.sessionToken,
});
await page.goto('/checkout');
await expect(page.getByTestId('checkout-v2')).toBeVisible();
await expect(page.getByTestId('checkout-v1')).not.toBeVisible();
});
test('old checkout flow is shown when flag is disabled', async ({ page, flags }) => {
await flags.override('new-checkout-flow', false);
await page.setExtraHTTPHeaders({
'X-FlagBridge-Session': flags.sessionToken,
});
await page.goto('/checkout');
await expect(page.getByTestId('checkout-v1')).toBeVisible();
});
test('A/B test shows correct variant', async ({ page, flags }) => {
await flags.override('checkout-button-color', 'green');
await page.setExtraHTTPHeaders({
'X-FlagBridge-Session': flags.sessionToken,
});
await page.goto('/checkout');
const button = page.getByRole('button', { name: 'Complete Purchase' });
await expect(button).toHaveCSS('background-color', 'rgb(34, 197, 94)');
});Using cookies instead of headers
If your app reads the session token from a cookie instead of a header, set it with page.context().addCookies():
// After creating the session
await page.context().addCookies([{
name: 'flagbridge_session',
value: session.token,
domain: 'localhost',
path: '/',
}]);Update your app's FlagBridge middleware to read from the cookie:
// Express middleware
app.use((req, res, next) => {
const sessionToken =
req.headers['x-flagbridge-session'] as string ||
req.cookies['flagbridge_session'];
if (sessionToken) {
req.flagBridgeSession = sessionToken;
}
next();
});Cypress
Setup
pnpm add -D @flagbridge/sdk-nodeCreate cypress/support/flags.ts:
// cypress/support/flags.ts
import { createTestingClient } from '@flagbridge/sdk-node/testing';
const testClient = createTestingClient({
apiKey: Cypress.env('FLAGBRIDGE_TEST_API_KEY'),
baseUrl: Cypress.env('FLAGBRIDGE_BASE_URL') ?? 'http://localhost:8080',
});
let currentSession: Awaited<ReturnType<typeof testClient.createSession>> | null = null;
Cypress.Commands.add('createFlagSession', async () => {
currentSession = await testClient.createSession();
cy.intercept('**', (req) => {
req.headers['X-FlagBridge-Session'] = currentSession!.token;
});
});
Cypress.Commands.add('overrideFlag', (flagKey: string, value: boolean | string) => {
if (!currentSession) throw new Error('Call cy.createFlagSession() first');
return cy.wrap(currentSession.override(flagKey, value));
});
Cypress.Commands.add('destroyFlagSession', () => {
if (currentSession) {
return cy.wrap(currentSession.destroy().then(() => { currentSession = null; }));
}
});// cypress/support/index.d.ts
declare global {
namespace Cypress {
interface Chainable {
createFlagSession(): Chainable<void>;
overrideFlag(flagKey: string, value: boolean | string): Chainable<void>;
destroyFlagSession(): Chainable<void>;
}
}
}Writing tests
// cypress/e2e/checkout.cy.ts
describe('Checkout flow', () => {
beforeEach(() => {
cy.createFlagSession();
});
afterEach(() => {
cy.destroyFlagSession();
});
it('shows new checkout when flag is enabled', () => {
cy.overrideFlag('new-checkout-flow', true);
cy.visit('/checkout');
cy.findByTestId('checkout-v2').should('be.visible');
});
it('shows old checkout when flag is disabled', () => {
cy.overrideFlag('new-checkout-flow', false);
cy.visit('/checkout');
cy.findByTestId('checkout-v1').should('be.visible');
});
});Vitest (integration tests)
For server-side integration tests, use the testing client directly:
// src/checkout/__tests__/checkout.test.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { createTestingClient } from '@flagbridge/sdk-node/testing';
import { createApp } from '../app';
import request from 'supertest';
const testClient = createTestingClient({
apiKey: process.env.FLAGBRIDGE_TEST_API_KEY!,
baseUrl: process.env.FLAGBRIDGE_BASE_URL ?? 'http://localhost:8080',
});
describe('GET /checkout', () => {
let session: Awaited<ReturnType<typeof testClient.createSession>>;
beforeEach(async () => {
session = await testClient.createSession();
});
afterEach(async () => {
await session.destroy();
});
it('renders new checkout when flag is enabled', async () => {
await session.override('new-checkout-flow', true);
const app = createApp();
const res = await request(app)
.get('/checkout')
.set('X-FlagBridge-Session', session.token)
.set('Cookie', 'user_id=user-123');
expect(res.status).toBe(200);
expect(res.text).toContain('data-testid="checkout-v2"');
});
it('renders old checkout when flag is disabled', async () => {
await session.override('new-checkout-flow', false);
const app = createApp();
const res = await request(app)
.get('/checkout')
.set('X-FlagBridge-Session', session.token)
.set('Cookie', 'user_id=user-123');
expect(res.status).toBe(200);
expect(res.text).toContain('data-testid="checkout-v1"');
});
});CI/CD configuration
Add your test API key to your CI environment:
# GitHub Actions
env:
FLAGBRIDGE_TEST_API_KEY: ${{ secrets.FLAGBRIDGE_TEST_API_KEY }}
FLAGBRIDGE_BASE_URL: http://localhost:8080If using FlagBridge Cloud, point FLAGBRIDGE_BASE_URL to your Cloud endpoint — test sessions are isolated per API key, so CI tests never affect production.
# .github/workflows/e2e.yml
env:
FLAGBRIDGE_TEST_API_KEY: ${{ secrets.FLAGBRIDGE_TEST_API_KEY }}
FLAGBRIDGE_BASE_URL: https://api.flagbridge.ioINFO
Test sessions automatically expire after 1 hour. In CI, sessions are created and destroyed per test, so you don't need to worry about cleanup if a test run crashes.
Best practices
- Create one session per test — sessions are cheap and isolation prevents flaky tests
- Only override the flags your test cares about — other flags evaluate normally
- Destroy sessions in
afterEach— avoids TTL-based accumulation - Use Test keys in CI — never use Admin or Live keys in your test pipeline
- Test both flag states — always write tests for both enabled and disabled cases
- Use
data-testidattributes — makes assertions independent of copy changes
Testing API reference
See the full Testing API reference for all endpoints.
