Skip to content

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

  1. Before each test, create a session via the Testing API
  2. Set flag overrides for the specific flags your test needs
  3. Pass the session token to your app (via cookie, header, or query param)
  4. Your app evaluates flags normally — but gets the overridden values
  5. 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:

bash
pnpm add -D @flagbridge/sdk-node

Create a fixtures/flags.ts file:

typescript
// 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

typescript
// 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():

typescript
// 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:

typescript
// 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

bash
pnpm add -D @flagbridge/sdk-node

Create cypress/support/flags.ts:

typescript
// 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; }));
  }
});
typescript
// 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

typescript
// 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:

typescript
// 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:

yaml
# GitHub Actions
env:
  FLAGBRIDGE_TEST_API_KEY: ${{ secrets.FLAGBRIDGE_TEST_API_KEY }}
  FLAGBRIDGE_BASE_URL: http://localhost:8080

If using FlagBridge Cloud, point FLAGBRIDGE_BASE_URL to your Cloud endpoint — test sessions are isolated per API key, so CI tests never affect production.

yaml
# .github/workflows/e2e.yml
env:
  FLAGBRIDGE_TEST_API_KEY: ${{ secrets.FLAGBRIDGE_TEST_API_KEY }}
  FLAGBRIDGE_BASE_URL: https://api.flagbridge.io

INFO

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-testid attributes — makes assertions independent of copy changes

Testing API reference

See the full Testing API reference for all endpoints.