Run a Playwright browser server in one environment and drive it from another environment by forwarding Playwright’s WebSocket traffic through a tunnel.
This package is intended for remote development / CI scenarios (for example: Codespaces, devcontainers, or a separate “browser host” machine) where you want tests to run “here” but the actual browser process to run “there”.
This package is the core tunneling/runtime layer used by the Playwright on Codespaces VS Code extension (located at vscode-extensions/playwright-on-codespaces-vscode-extension).
In a typical Codespaces workflow:
- Your tests run inside the Codespace and call
tunneledBrowserConnection(). tunneledBrowserConnection()starts a WebSocket server (by default on port3000) that a browser host can attach to.- The VS Code extension runs on the UI side and starts a
PlaywrightTunnelwhich connects tows://127.0.0.1:3000.- In Codespaces, this works when port
3000is forwarded to your local machine (VS Code port forwarding makes the remote port reachable aslocalhost:3000).
- In Codespaces, this works when port
- Once connected, the extension hosts the actual Playwright browser process locally, while your tests continue to run remotely.
The extension provides a UI wrapper around this library (start/stop commands, status bar state, and logs), while @rushstack/playwright-browser-tunnel provides the underlying protocol forwarding and browser lifecycle management.
Some remote test fixtures want to detect whether the Playwright on Codespaces extension is installed/active (for example, to skip local-browser-only scenarios when the extension isn’t available).
The extension writes a marker file named .playwright-codespaces-extension-installed.txt into the remote environment’s os.tmpdir() using VS Code’s remote filesystem APIs.
On the remote side, isExtensionInstalledAsync() checks for that marker file and returns true if it exists:
import { isExtensionInstalledAsync } from '@rushstack/playwright-browser-tunnel';
if (!(await isExtensionInstalledAsync())) {
throw new Error('Playwright on Codespaces extension is not installed/active in this environment');
}- Node.js
>= 20(seeenginesinpackage.json) - A compatible Playwright version (this package is built/tested with Playwright
1.56.x)
From src/index.ts:
PlaywrightTunnel(class)IPlaywrightTunnelOptions(type)TunnelStatus(type)BrowserNames(type)tunneledBrowserConnection()(function)tunneledBrowser()(function)IDisposableTunneledBrowserConnection(type)isExtensionInstalledAsync()(function)
There are two pieces:
- Browser host: run a
PlaywrightTunnelto launch the real browser server and forward messages. - Test runner: create a local endpoint via
tunneledBrowserConnection()that your Playwright client can connect to (it forwards to the browser host).
Use PlaywrightTunnel in the environment where you want the browser process to run.
import { ConsoleTerminalProvider, Terminal, TerminalProviderSeverity } from '@rushstack/terminal';
import { PlaywrightTunnel } from '@rushstack/playwright-browser-tunnel';
import path from 'node:path';
import os from 'node:os';
const terminalProvider = new ConsoleTerminalProvider();
const terminal = new Terminal(terminalProvider);
const tunnel = new PlaywrightTunnel({
mode: 'wait-for-incoming-connection',
listenPort: 3000,
tmpPath: path.join(os.tmpdir(), 'playwright-browser-tunnel'),
terminal,
onStatusChange: (status) => terminal.writeLine(`status: ${status}`)
});
await tunnel.startAsync({ keepRunning: true });Notes:
mode: 'wait-for-incoming-connection'starts a WebSocket server and waits for the other side to connect.mode: 'poll-connection'repeatedly attempts to connect to a WebSocket endpoint you provide (wsEndpoint).tmpPathis used as a working directory to install the requestedplaywright-coreversion and run its CLI.
Use tunneledBrowserConnection() in the environment where your tests run.
It starts:
- a remote WebSocket server (port
3000) that the browser host connects to - a local WebSocket endpoint (random port) that your Playwright client connects to
import { tunneledBrowserConnection } from '@rushstack/playwright-browser-tunnel';
import playwright from 'playwright-core';
using connection = await tunneledBrowserConnection();
// Build the connect URL with query parameters consumed by the local proxy.
const url = new URL(connection.remoteEndpoint);
url.searchParams.set('browser', 'chromium');
url.searchParams.set('launchOptions', JSON.stringify({ headless: true }));
const browser = await playwright.chromium.connect(url.toString());
// ...run tests...
await browser.close();- Build:
rush build --to playwright-browser-tunnel - Demo script (if configured):
rushx demo
- If the tunnel is stuck in
waiting-for-connection, ensure the counterpart process is reachable and ports are forwarded correctly. - If browser installation is slow/repeated, ensure
tmpPathis stable and writable for the host environment.