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 Locally | Browser Forest Cloud Testing |
|---|---|
| Every CI machine must install a browser | Zero config, on-demand browser instances |
| Concurrency limited by machine CPU/memory | Horizontal scaling, run hundreds of tests simultaneously |
| Inconsistent OS and Chrome versions | Unified versions, reproducible test results |
| Different fingerprint characteristics per environment | Anti-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' }]],
});
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/
API Parameter Reference
| Parameter | Type | Default | Description |
|---|---|---|---|
| timeoutSeconds | integer | 300 | Maximum Session lifetime (seconds) |
| idleTimeoutSeconds | integer | 60 | Auto-destroy after idle timeout (seconds) |
| os | string | random | windows / macos / linux — controls UA and platform fingerprint |
| userMetadata | object | null | Custom key-value pairs stored with the Session |
| contextId | string | null | Reuse persistent Cookie/Storage context |
| proxy | object | null | Proxy config { server, username, password } |