|
| 1 | + |
| 2 | +# @rushstack/playwright-browser-tunnel |
| 3 | + |
| 4 | +Run a Playwright browser server in one environment and drive it from another environment by forwarding Playwright’s WebSocket traffic through a tunnel. |
| 5 | + |
| 6 | +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”. |
| 7 | + |
| 8 | +## Relationship to the Playwright on Codespaces VS Code extension |
| 9 | + |
| 10 | +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](../../vscode-extensions/playwright-on-codespaces-vscode-extension)). |
| 11 | + |
| 12 | +In a typical Codespaces workflow: |
| 13 | + |
| 14 | +- Your **tests** run inside the Codespace and call `tunneledBrowserConnection()`. |
| 15 | +- `tunneledBrowserConnection()` starts a WebSocket server (by default on port `3000`) that a browser host can attach to. |
| 16 | +- The VS Code extension runs on the **UI side** and starts a `PlaywrightTunnel` which connects to `ws://127.0.0.1:3000`. |
| 17 | + - In Codespaces, this works when port `3000` is forwarded to your local machine (VS Code port forwarding makes the remote port reachable as `localhost:3000`). |
| 18 | +- Once connected, the extension hosts the actual Playwright browser process locally, while your tests continue to run remotely. |
| 19 | + |
| 20 | +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. |
| 21 | + |
| 22 | +### Detecting whether the VS Code extension is present |
| 23 | + |
| 24 | +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). |
| 25 | + |
| 26 | +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. |
| 27 | + |
| 28 | +On the remote side, `isExtensionInstalledAsync()` checks for that marker file and returns `true` if it exists: |
| 29 | + |
| 30 | +```ts |
| 31 | +import { isExtensionInstalledAsync } from '@rushstack/playwright-browser-tunnel'; |
| 32 | + |
| 33 | +if (!(await isExtensionInstalledAsync())) { |
| 34 | + throw new Error('Playwright on Codespaces extension is not installed/active in this environment'); |
| 35 | +} |
| 36 | +``` |
| 37 | + |
| 38 | + |
| 39 | +## Requirements |
| 40 | + |
| 41 | +- Node.js `>= 20` (see `engines` in `package.json`) |
| 42 | +- A compatible Playwright version (this package is built/tested with Playwright `1.56.x`) |
| 43 | + |
| 44 | +## Exports |
| 45 | + |
| 46 | +From [src/index.ts](src/index.ts): |
| 47 | + |
| 48 | +- `PlaywrightTunnel` (class) |
| 49 | +- `IPlaywrightTunnelOptions` (type) |
| 50 | +- `TunnelStatus` (type) |
| 51 | +- `BrowserNames` (type) |
| 52 | +- `tunneledBrowserConnection()` (function) |
| 53 | +- `tunneledBrowser()` (function) |
| 54 | +- `IDisposableTunneledBrowserConnection` (type) |
| 55 | +- `isExtensionInstalledAsync()` (function) |
| 56 | + |
| 57 | +## Usage |
| 58 | + |
| 59 | +There are two pieces: |
| 60 | + |
| 61 | +1) **Browser host**: run a `PlaywrightTunnel` to launch the real browser server and forward messages. |
| 62 | +2) **Test runner**: create a local endpoint via `tunneledBrowserConnection()` that your Playwright client can connect to (it forwards to the browser host). |
| 63 | + |
| 64 | +### 1) Browser host: run the tunnel |
| 65 | + |
| 66 | +Use `PlaywrightTunnel` in the environment where you want the browser process to run. |
| 67 | + |
| 68 | +```ts |
| 69 | +import { ConsoleTerminalProvider, Terminal, TerminalProviderSeverity } from '@rushstack/terminal'; |
| 70 | +import { PlaywrightTunnel } from '@rushstack/playwright-browser-tunnel'; |
| 71 | +import path from 'node:path'; |
| 72 | +import os from 'node:os'; |
| 73 | + |
| 74 | +const terminalProvider = new ConsoleTerminalProvider(); |
| 75 | +const terminal = new Terminal(terminalProvider); |
| 76 | + |
| 77 | +const tunnel = new PlaywrightTunnel({ |
| 78 | + mode: 'wait-for-incoming-connection', |
| 79 | + listenPort: 3000, |
| 80 | + tmpPath: path.join(os.tmpdir(), 'playwright-browser-tunnel'), |
| 81 | + terminal, |
| 82 | + onStatusChange: (status) => terminal.writeLine(`status: ${status}`) |
| 83 | +}); |
| 84 | + |
| 85 | +await tunnel.startAsync({ keepRunning: true }); |
| 86 | +``` |
| 87 | + |
| 88 | +Notes: |
| 89 | + |
| 90 | +- `mode: 'wait-for-incoming-connection'` starts a WebSocket server and waits for the other side to connect. |
| 91 | +- `mode: 'poll-connection'` repeatedly attempts to connect to a WebSocket endpoint you provide (`wsEndpoint`). |
| 92 | +- `tmpPath` is used as a working directory to install the requested `playwright-core` version and run its CLI. |
| 93 | + |
| 94 | +### 2) Test runner: create a local endpoint to connect() |
| 95 | + |
| 96 | +Use `tunneledBrowserConnection()` in the environment where your tests run. |
| 97 | + |
| 98 | +It starts: |
| 99 | + |
| 100 | +- a **remote** WebSocket server (port `3000`) that the browser host connects to |
| 101 | +- a **local** WebSocket endpoint (random port) that your Playwright client connects to |
| 102 | + |
| 103 | +```ts |
| 104 | +import { tunneledBrowserConnection } from '@rushstack/playwright-browser-tunnel'; |
| 105 | +import playwright from 'playwright-core'; |
| 106 | + |
| 107 | +using connection = await tunneledBrowserConnection(); |
| 108 | + |
| 109 | +// Build the connect URL with query parameters consumed by the local proxy. |
| 110 | +const url = new URL(connection.remoteEndpoint); |
| 111 | +url.searchParams.set('browser', 'chromium'); |
| 112 | +url.searchParams.set('launchOptions', JSON.stringify({ headless: true })); |
| 113 | + |
| 114 | +const browser = await playwright.chromium.connect(url.toString()); |
| 115 | +// ...run tests... |
| 116 | +await browser.close(); |
| 117 | +``` |
| 118 | + |
| 119 | +## Development |
| 120 | + |
| 121 | +- Build: `rush build --to playwright-browser-tunnel` |
| 122 | +- Demo script (if configured): `rushx demo` |
| 123 | + |
| 124 | +## Troubleshooting |
| 125 | + |
| 126 | +- If the tunnel is stuck in `waiting-for-connection`, ensure the counterpart process is reachable and ports are forwarded correctly. |
| 127 | +- If browser installation is slow/repeated, ensure `tmpPath` is stable and writable for the host environment. |
| 128 | + |
0 commit comments