Use Case: Automated Testing

E2E and browser automation testing require stable, clean browser environments. Browser Forest provides cloud-hosted browser instances that let Playwright, Puppeteer, and other testing frameworks connect directly via CDP, eliminating local environment differences, CI configuration complexity, and concurrent resource contention.

Key Advantages

Running Tests LocallyBrowser Forest Cloud Testing
Every CI machine must install a browserZero config, on-demand browser instances
Concurrency limited by machine CPU/memoryHorizontal scaling, run hundreds of tests simultaneously
Inconsistent OS and Chrome versionsUnified versions, reproducible test results
Different fingerprint characteristics per environmentAnti-detection patches, bypass anti-bot measures

Approach A: Playwright Connecting to Cloud Browsers

Playwright supports connecting to already-running browsers via CDP endpoint. The testing framework stays the same — just replace chromium.launch() with chromium.connectOverCDP().

Install Dependencies

npm install playwright @playwright/test

Connect and Run Tests

import { chromium } from 'playwright';

const API_KEY = 'bf_live_xxxxxxxx';
const API_BASE = 'https://bf.mktindex.com/api';

async function runTest() {
  // 1. Create Session
  const res = await fetch(`${API_BASE}/v1/sessions`, {
    method: 'POST',
    headers: { 'X-API-Key': API_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({
      idleTimeoutSeconds: 120,
      os: 'windows',
    }),
  });
  const session = await res.json();
  console.log('Session created:', session.id);

  // 2. Connect via CDP (WSS endpoint proxied by Nginx)
  const wsUrl = `wss://bf.mktindex.com/ws/session/${session.id}`;
  const browser = await chromium.connectOverCDP(wsUrl);
  const page = await browser.newPage();

  try {
    // 3. Run test logic
    await page.goto('https://example.com/login');
    await page.fill('#username', '[email protected]');
    await page.fill('#password', 'password123');
    await page.click('button[type=submit]');
    await page.waitForURL('**/dashboard');

    const title = await page.title();
    console.assert(title.includes('Dashboard'), 'Login failed');
    console.log('✓ Login test passed');

    // Verify user info
    const userName = await page.textContent('.user-name');
    console.assert(userName?.includes('testuser'), 'User name mismatch');
    console.log('✓ User info verified');
  } finally {
    // 4. Disconnect and destroy Session
    await browser.close();
    await fetch(`${API_BASE}/v1/sessions/${session.id}`, {
      method: 'DELETE',
      headers: { 'X-API-Key': API_KEY },
    });
  }
}

runTest().catch(console.error);

Approach B: Playwright Test Integration (Recommended)

Integrate Browser Forest into the Playwright Test framework via a custom fixture. Test files look identical to running locally — just switch config to seamlessly toggle between local and cloud.

Custom Fixture (fixtures.ts)

// fixtures.ts
import { test as base, chromium, type BrowserContext } from '@playwright/test';

const API_KEY = process.env.BF_API_KEY!;
const API_BASE = 'https://bf.mktindex.com/api';

type BFFixtures = {
  bfContext: BrowserContext;
};

export const test = base.extend<BFFixtures>({
  bfContext: async ({}, use) => {
    // Create Session
    const res = await fetch(`${API_BASE}/v1/sessions`, {
      method: 'POST',
      headers: { 'X-API-Key': API_KEY, 'Content-Type': 'application/json' },
      body: JSON.stringify({ idleTimeoutSeconds: 300 }),
    });
    const session = await res.json();

    // Connect
    const wsUrl = `wss://bf.mktindex.com/ws/session/${session.id}`;
    const browser = await chromium.connectOverCDP(wsUrl);
    const context = browser.contexts()[0];

    // Run test
    await use(context);

    // Cleanup
    await browser.close();
    await fetch(`${API_BASE}/v1/sessions/${session.id}`, {
      method: 'DELETE',
      headers: { 'X-API-Key': API_KEY },
    });
  },
});

export { expect } from '@playwright/test';

Test File (e2e/login.spec.ts)

// e2e/login.spec.ts
import { test, expect } from '../fixtures';

test('User login flow', async ({ bfContext }) => {
  const page = await bfContext.newPage();

  await page.goto('https://example.com/login');
  await page.fill('#email', '[email protected]');
  await page.fill('#password', 'secret');
  await page.click('[data-testid=submit]');

  await expect(page).toHaveURL(/dashboard/);
  await expect(page.locator('.welcome')).toContainText('Welcome');
});

test('Add item to cart', async ({ bfContext }) => {
  const page = await bfContext.newPage();

  await page.goto('https://shop.example.com/product/123');
  await page.click('#add-to-cart');
  await page.click('#cart-icon');

  await expect(page.locator('.cart-count')).toHaveText('1');
});

Concurrent Execution (playwright.config.ts)

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  workers: 10,           // Run 10 tests in parallel, each with its own Session
  retries: 2,
  timeout: 60_000,
  reporter: [['html', { outputFolder: 'playwright-report' }]],
});
Note: With workers: N, each worker corresponds to an independent cloud Session with no interference. Session count is limited by your API Key quota — contact us for higher concurrency plans.

Approach C: Puppeteer Connection

Puppeteer also supports connecting to already-running Chrome instances via puppeteer.connect().

import puppeteer from 'puppeteer';

const API_KEY = 'bf_live_xxxxxxxx';

async function runPuppeteerTest() {
  // Create Session
  const res = await fetch('https://bf.mktindex.com/api/v1/sessions', {
    method: 'POST',
    headers: { 'X-API-Key': API_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({ idleTimeoutSeconds: 120 }),
  });
  const { id, cdpUrl } = await res.json();

  // cdpUrl is internal — use the WSS proxy for public access
  const wsUrl = `wss://bf.mktindex.com/ws/session/${id}`;

  const browser = await puppeteer.connect({ browserWSEndpoint: wsUrl });
  const page = await browser.newPage();

  await page.goto('https://example.com');
  const html = await page.content();
  console.log('Page fetched, length:', html.length);

  await browser.disconnect();

  // Destroy Session
  await fetch(`https://bf.mktindex.com/api/v1/sessions/${id}`, {
    method: 'DELETE',
    headers: { 'X-API-Key': API_KEY },
  });
}

runPuppeteerTest().catch(console.error);

OS Fingerprint Simulation

When testing UI differences across operating systems (User-Agent, fonts, platform identifiers), specify the target platform via the os parameter.

// Simulate Windows user
const winSession = await fetch('https://bf.mktindex.com/api/v1/sessions', {
  method: 'POST',
  headers: { 'X-API-Key': API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({ os: 'windows' }),
}).then(r => r.json());

// Simulate macOS user
const macSession = await fetch('https://bf.mktindex.com/api/v1/sessions', {
  method: 'POST',
  headers: { 'X-API-Key': API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({ os: 'macos' }),
}).then(r => r.json());

// Simulate Linux user
const linuxSession = await fetch('https://bf.mktindex.com/api/v1/sessions', {
  method: 'POST',
  headers: { 'X-API-Key': API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({ os: 'linux' }),
}).then(r => r.json());

Associate Test Cases with userMetadata

The userMetadata field can attach arbitrary business identifiers, making it easy to track which test case each Session corresponds to in logs and reports.

const session = await fetch('https://bf.mktindex.com/api/v1/sessions', {
  method: 'POST',
  headers: { 'X-API-Key': API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    idleTimeoutSeconds: 120,
    userMetadata: {
      testSuite: 'checkout-flow',
      testCase: 'TC-042',
      branch: process.env.GIT_BRANCH,
      buildId: process.env.CI_BUILD_ID,
    },
  }),
}).then(r => r.json());

console.log('Running test on session:', session.id);
// userMetadata is readable in GET /v1/sessions/:id response for easy debugging

CI/CD Integration Example

GitHub Actions

# .github/workflows/e2e.yml
name: E2E Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        shard: [1, 2, 3, 4, 5]   # 5 parallel shards
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - name: Run E2E tests (shard ${{ matrix.shard }}/5)
        env:
          BF_API_KEY: ${{ secrets.BF_API_KEY }}
        run: npx playwright test --shard=${{ matrix.shard }}/5
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report-${{ matrix.shard }}
          path: playwright-report/
Note: GitHub Actions' built-in Ubuntu runner requires no browser installation — Node.js can directly call the Browser Forest API. No browser download needed, dramatically shorter CI startup times.

API Parameter Reference

ParameterTypeDefaultDescription
timeoutSecondsinteger300Maximum Session lifetime (seconds)
idleTimeoutSecondsinteger60Auto-destroy after idle timeout (seconds)
osstringrandomwindows / macos / linux — controls UA and platform fingerprint
userMetadataobjectnullCustom key-value pairs stored with the Session
contextIdstringnullReuse persistent Cookie/Storage context
proxyobjectnullProxy config { server, username, password }