BrowserStack Tutorial: Automated Cross-Browser Testing with Playwright
Cross-browser testing is one of those tasks that sounds straightforward until you try to do it properly. Running your test suite against Chrome, Firefox, Safari, Edge — on Windows, macOS, and iOS — requires either a small fleet of machines or a cloud testing platform. BrowserStack is the most widely used cloud testing service for this, and it has first-class support for Playwright. This tutorial walks through everything: account setup, connecting Playwright to BrowserStack Automate, configuring browser capabilities, local testing tunnels, parallel runs, and CI integration.
What BrowserStack Automate Does
BrowserStack Automate gives you remote access to real browsers running on real operating systems. When you run tests through BrowserStack, your test code executes locally but the browser opens on a BrowserStack machine in their data center. You see the results (screenshots, videos, logs) in their dashboard. You never install Chrome on Windows yourself — BrowserStack maintains the grid.
The key difference from spinning up a Selenium Grid yourself is that BrowserStack covers hundreds of browser/OS combinations and handles all infrastructure maintenance. The tradeoff is cost and the fact that your tests are running on remote machines, which adds latency and creates compliance considerations for some organizations.
Account Setup and Credentials
Sign up at browserstack.com. The free plan gives you 100 minutes per month, which is enough for experimentation. After signing in, navigate to Automate in the top menu, then look for your credentials under Access Key.
You will see two values you need for every test run:
BROWSERSTACK_USERNAME— your account username (not your email)BROWSERSTACK_ACCESS_KEY— a long alphanumeric string
Store these as environment variables, never hardcode them:
export BROWSERSTACK_USERNAME=<span class="hljs-string">"your_username_here"
<span class="hljs-built_in">export BROWSERSTACK_ACCESS_KEY=<span class="hljs-string">"your_access_key_here"For CI pipelines, add them as encrypted secrets in your CI provider (GitHub Actions secrets, CircleCI environment variables, etc.).
Installing the BrowserStack Playwright SDK
BrowserStack provides an SDK that handles the WebSocket connection and capability mapping automatically. Install it alongside Playwright:
npm install --save-dev @playwright/test browserstack-node-sdkCreate a browserstack.yml configuration file in your project root. This is the modern way to configure BrowserStack — it replaces the older capability-string approach:
# browserstack.yml
userName: ${BROWSERSTACK_USERNAME}
accessKey: ${BROWSERSTACK_ACCESS_KEY}
buildName: "my-app-tests"
projectName: "My Application"
browsers:
- browser: chrome
browser_version: latest
os: Windows
os_version: 11
- browser: firefox
browser_version: latest
os: Windows
os_version: 11
- browser: safari
browser_version: latest
os: OS X
os_version: Sonoma
- browser: edge
browser_version: latest
os: Windows
os_version: 11
parallelsPerPlatform: 2
browserstackLocal: falseRunning Playwright Tests on BrowserStack
Your existing Playwright tests require minimal changes to run on BrowserStack. The SDK intercepts test execution and routes browsers to the BrowserStack cloud.
Here is a typical Playwright test:
// tests/login.spec.ts
import { test, expect } from "@playwright/test";
test.describe("Login flow", () => {
test("should login with valid credentials", async ({ page }) => {
await page.goto("https://your-app.com/login");
await page.fill('[data-testid="email-input"]', "test@example.com");
await page.fill('[data-testid="password-input"]', "SecurePass123");
await page.click('[data-testid="login-button"]');
await expect(page).toHaveURL(/dashboard/);
await expect(page.locator('[data-testid="user-greeting"]')).toContainText(
"Welcome"
);
});
test("should show error for invalid credentials", async ({ page }) => {
await page.goto("https://your-app.com/login");
await page.fill('[data-testid="email-input"]', "wrong@example.com");
await page.fill('[data-testid="password-input"]', "wrongpassword");
await page.click('[data-testid="login-button"]');
await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
await expect(page.locator('[data-testid="error-message"]')).toContainText(
"Invalid credentials"
);
});
});Run these tests on BrowserStack using the SDK runner:
npx browserstack-node-sdk playwright testThe SDK reads browserstack.yml, spawns one browser instance per configured platform, and runs your tests in parallel across them. Results appear in the BrowserStack Automate dashboard at automate.browserstack.com/builds.
Configuring Capabilities the Old Way (Selenium Protocol)
If you have existing Selenium tests or need more granular control, BrowserStack also supports the WebDriver protocol with explicit capability objects. This is the pre-SDK approach and still works for Selenium WebDriver tests:
// selenium-browserstack.js
const { Builder } = require("selenium-webdriver");
const capabilities = {
"bstack:options": {
userName: process.env.BROWSERSTACK_USERNAME,
accessKey: process.env.BROWSERSTACK_ACCESS_KEY,
os: "Windows",
osVersion: "11",
buildName: "selenium-build-001",
sessionName: "Login test - Chrome Win11",
debug: true,
networkLogs: true,
consoleLogs: "verbose",
},
browserName: "Chrome",
browserVersion: "latest",
};
const driver = await new Builder()
.usingServer("https://hub.browserstack.com/wd/hub")
.withCapabilities(capabilities)
.build();
try {
await driver.get("https://your-app.com");
const title = await driver.getTitle();
console.log("Page title:", title);
} finally {
await driver.quit();
}The bstack:options namespace is where all BrowserStack-specific configuration lives. Key options you will use regularly:
| Option | Description |
|---|---|
os / osVersion |
Operating system and version |
browserVersion |
Specific browser version or latest / latest-1 |
buildName |
Groups test sessions into a build in the dashboard |
sessionName |
Label for an individual test session |
debug |
Enables visual logs (screenshots at each step) |
networkLogs |
Captures HAR network logs |
consoleLogs |
Browser console log level (verbose, info, warning, errors) |
video |
Record video of the session (default: true) |
Local Testing with BrowserStackLocal
Testing against a localhost server or an app behind a firewall requires the BrowserStackLocal tunnel. The tunnel creates a secure connection between BrowserStack's machines and your local network.
Install the tunnel binary:
npm install --save-dev browserstack-localStart the tunnel programmatically in a global setup file:
// playwright.config.ts
import { defineConfig } from "@playwright/test";
import { BrowserStackLocal } from "browserstack-local";
export default defineConfig({
globalSetup: "./global-setup.ts",
globalTeardown: "./global-teardown.ts",
// rest of your config
});// global-setup.ts
import { BrowserStackLocal } from "browserstack-local";
let bsLocal: BrowserStackLocal;
async function globalSetup() {
bsLocal = new BrowserStackLocal();
await new Promise<void>((resolve, reject) => {
bsLocal.start(
{
key: process.env.BROWSERSTACK_ACCESS_KEY,
forceLocal: true,
localIdentifier: "my-local-tunnel",
},
(error) => {
if (error) reject(error);
else {
console.log("BrowserStackLocal tunnel started");
resolve();
}
}
);
});
// Store reference for teardown
(global as any).__BS_LOCAL__ = bsLocal;
}
export default globalSetup;// global-teardown.ts
async function globalTeardown() {
const bsLocal = (global as any).__BS_LOCAL__;
if (bsLocal) {
await new Promise<void>((resolve) => bsLocal.stop(resolve));
console.log("BrowserStackLocal tunnel stopped");
}
}
export default globalTeardown;Update browserstack.yml to enable local testing:
browserstackLocal: true
browserstackLocalOptions:
localIdentifier: "my-local-tunnel"Now your tests can hit http://localhost:3000 and BrowserStack will route that traffic through the tunnel to your machine.
Test Observability Dashboard
BrowserStack's Test Observability feature (separate from Automate) provides a smart test analytics layer. It tracks flaky tests, groups failures by root cause, and shows trends over time.
Enable it in browserstack.yml:
testObservability: true
testObservabilityOptions:
projectName: "My Application"
buildName: "build-${CI_BUILD_NUMBER}"The dashboard shows:
- Flaky test detection — tests that pass and fail intermittently without code changes
- Failure clustering — groups failures by error message and stack trace, so 15 tests failing with the same
TimeoutErrorshow as one issue - Build comparison — diff failures between two builds to isolate what changed
- Smart re-run — re-run only failed tests with one click
For teams running large test suites, this is the feature that makes BrowserStack worth paying for beyond raw browser access.
Parallel Testing Configuration
Parallelism in BrowserStack works at two levels: across browser/OS combinations (platform parallelism) and within a single browser (test parallelism).
In browserstack.yml:
parallelsPerPlatform: 5This runs up to 5 tests simultaneously on each configured browser. If you have 4 browsers configured and 5 parallel slots per browser, you are using 20 concurrent sessions. Check your BrowserStack plan's concurrent session limit.
For Playwright specifically, also configure Playwright's own worker count:
// playwright.config.ts
import { defineConfig } from "@playwright/test";
export default defineConfig({
workers: process.env.CI ? 4 : 2,
retries: process.env.CI ? 2 : 0,
use: {
baseURL: process.env.BASE_URL || "https://your-app.com",
screenshot: "only-on-failure",
video: "retain-on-failure",
trace: "on-first-retry",
},
});The BrowserStack SDK respects Playwright's worker setting and multiplies it by the number of browser platforms. Ten workers times four browsers equals forty parallel requests to the API — make sure your plan supports it.
BrowserStack App Automate for Mobile Testing
For teams also testing mobile apps, BrowserStack App Automate runs Appium tests against real iOS and Android devices. The setup is similar: upload your .apk or .ipa file, configure device capabilities, and run your Appium tests.
// app-automate-example.js
const capabilities = {
"bstack:options": {
userName: process.env.BROWSERSTACK_USERNAME,
accessKey: process.env.BROWSERSTACK_ACCESS_KEY,
deviceName: "iPhone 15",
osVersion: "17",
appiumVersion: "2.0.1",
buildName: "ios-app-tests",
},
app: "bs://your-uploaded-app-hash",
platformName: "iOS",
"appium:automationName": "XCUITest",
};Upload your app binary before running tests:
curl -u "$BROWSERSTACK_USERNAME:<span class="hljs-variable">$BROWSERSTACK_ACCESS_KEY" \
-X POST <span class="hljs-string">"https://api-cloud.browserstack.com/app-automate/upload" \
-F <span class="hljs-string">"file=@/path/to/your/app.ipa"App Automate is priced separately from Automate (the web browser product).
GitHub Actions Integration
Here is a complete GitHub Actions workflow that runs your Playwright tests on BrowserStack:
# .github/workflows/browserstack.yml
name: BrowserStack Cross-Browser Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
cross-browser-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers (for local fallback)
run: npx playwright install --with-deps chromium
- name: Run BrowserStack tests
env:
BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }}
BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
BASE_URL: ${{ secrets.STAGING_URL }}
CI: true
CI_BUILD_NUMBER: ${{ github.run_number }}
run: npx browserstack-node-sdk playwright test
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30Add BROWSERSTACK_USERNAME and BROWSERSTACK_ACCESS_KEY as GitHub Actions secrets in your repository settings. Never put them in the workflow file directly.
To save BrowserStack minutes on every PR, you can run a quick local Playwright check first and only send to BrowserStack on merges to main:
jobs:
local-smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npx playwright test --grep @smoke
env:
BASE_URL: ${{ secrets.STAGING_URL }}
cross-browser:
needs: local-smoke
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
# BrowserStack steps herePricing Overview
BrowserStack pricing (as of 2025) is session-based for the Automate product:
- Free: 100 minutes/month, 1 parallel session, no video, manual testing only
- Automate: Starts around $29/month for 1 parallel session, unlimited minutes
- Team plans: Scale by parallel sessions — $149/month for 5 parallels is a common starting point for small teams
The math to watch: if your test suite takes 20 minutes with 1 parallel session, adding 5 parallels brings it to 4 minutes. But you pay for all 5 sessions simultaneously. Calculate cost per CI run against your team's time value.
App Automate (mobile) is priced separately and generally costs more per session than web Automate.
Enterprise plans include dedicated infrastructure, SLA guarantees, SSO, and volume discounts. If you have compliance requirements around where your tests can run, the enterprise plan also gives you options for private cloud deployments.
Common Troubleshooting
Tests time out connecting to BrowserStack: Check that your BROWSERSTACK_ACCESS_KEY is correct and that your network allows outbound WebSocket connections to hub.browserstack.com:443.
Local tunnel not connecting: Ensure no other tunnel process is running with the same localIdentifier. Kill stale processes with pkill -f BrowserStackLocal before starting a new session.
Safari tests failing only on BrowserStack: Safari on BrowserStack runs in a macOS VM. Shadow DOM, certain CSS transforms, and contenteditable elements behave differently in Safari than in Chrome. Add explicit waits and verify your selectors work in real Safari locally first.
Sessions appearing in dashboard but no results: Your test may be crashing before connecting to the remote browser. Check the BrowserStack session logs for connection errors, and verify your browserstack.yml syntax with the BrowserStack YAML validator.
Cross-browser testing with BrowserStack and Playwright is one of those setups where the initial investment in configuration pays off quickly. Once the pipeline runs automatically on every push, browser regressions get caught before they reach users — without anyone manually opening browsers on multiple machines.