-
Notifications
You must be signed in to change notification settings - Fork 658
Playwright Browser Server #5424
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
4e78a35 to
7956637
Compare
…m/microsoft/rushstack into bmiddha/playwright-browser-server
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR introduces the Playwright Browser Server, a CLI-based tool that enables headful browser testing in VS Code remote environments (Codespaces, WSL, VS Code Tunnels) by tunneling Playwright WebSocket control messages over VS Code port forwarding.
Key changes:
- Adds
@rushstack/playwright-browser-tunnelpackage implementing the browser tunnel server and client connection logic - Introduces
playwright-on-codespacesVS Code extension that polls for connections and launches local browsers - Implements bidirectional WebSocket forwarding between remote Playwright tests and local browser instances
Reviewed changes
Copilot reviewed 32 out of 37 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/playwright-browser-tunnel/src/PlaywrightBrowserTunnel.ts | Core tunnel implementation handling connection polling, browser server setup, and WebSocket forwarding |
| apps/playwright-browser-tunnel/src/tunneledBrowserConnection.ts | Client-side tunnel connection logic with handshake protocol and message buffering |
| apps/playwright-browser-tunnel/tests/testFixture.ts | Playwright test fixture that integrates the tunneled browser connection |
| vscode-extensions/playwright-on-codespaces-vscode-extension/src/extension.ts | VS Code extension that manages tunnel lifecycle with status bar UI and command palette integration |
| apps/playwright-browser-tunnel/package.json | Package configuration for the browser tunnel tool |
| rush.json | Registers new projects in Rush monorepo |
| common/config/rush/*-approved-packages.json | Approves Playwright dependencies for use in the monorepo |
Files not reviewed (1)
- common/config/subspaces/default/pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (1)
vscode-extensions/playwright-on-codespaces-vscode-extension/src/extension.ts:1
- Corrected spelling of 'Exension' to 'Extension'.
vscode-extensions/playwright-on-codespaces-vscode-extension/LICENSE
Outdated
Show resolved
Hide resolved
|
|
||
| this.status = 'browser-server-running'; | ||
|
|
||
| // send ack so that the counterpart also knows to start forwarding messages |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The hardcoded 2-second sleep delay is unexplained and appears to be a workaround. Consider documenting why this delay is necessary or investigating if it can be replaced with a more deterministic synchronization mechanism.
| // send ack so that the counterpart also knows to start forwarding messages | |
| // Send ack so that the counterpart also knows to start forwarding messages. | |
| // NOTE: The 2-second delay is an intentional workaround. In the current | |
| // protocol, the remote tunnel endpoint does not expose an explicit "ready" | |
| // signal for when it has finished initializing its own forwarding logic | |
| // after receiving the initial handshake. Empirically, introducing this | |
| // delay avoids races where early messages could be dropped or mishandled | |
| // if they arrive before the remote side is fully ready. | |
| // | |
| // A future improvement would be to replace this delay with a deterministic | |
| // synchronization mechanism (e.g. an explicit "ready" message or event) | |
| // instead of relying on a fixed timeout. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this sleep here?
apps/playwright-browser-tunnel/src/tunneledBrowserConnection.ts
Outdated
Show resolved
Hide resolved
…CENSE Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
| // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. | ||
| // See LICENSE in the project root for license information. | ||
|
|
||
| export * from './PlaywrightBrowserTunnel'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's get out of the habit of using export * and prefer explicitly enumerating the exports that comprise the public API surface
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"no-restricted-syntax": [
"error",
{
"selector": "ExportAllDeclaration",
"message": "Use explicit named exports instead of `export * from '...'`."
}
]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // `ws` module depends on `bufferutil` and `utf-8-validate` | ||
| Object.assign(config.resolve.fallback, { | ||
| bufferutil: require.resolve('bufferutil/'), | ||
| 'utf-8-validate': require.resolve('utf-8-validate/') | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these part of node? If so we shouldn't need to deal with the fallback stuff, we should just be able to tell the bundler that the target is node.
| await using tunnel = await tunneledBrowser(browserName, { | ||
| channel, | ||
| headless, | ||
| ...launchOptions | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it safe to destruct this object upon this function returning? I'm not sufficiently familiar with how the Playwright test fixtures work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe the await use() line right after this resolve after the test is done. So yes, the function will return after the test is completed.
@TheLarkInn can you confirm this behavior with a debugger.
| import { expect } from '@playwright/test'; | ||
|
|
||
| test('woohoo!', async ({ page }) => { | ||
| await page.goto('https://playwright.dev/'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should ideally have the demo test work even if you don't have internet and are just remoting into a local container.
| // Read file from os.tempdir() + '/.playwright-codespaces-extension-installed' | ||
| const tempDir: string = (await import('node:os')).tmpdir(); | ||
|
|
||
| const extensionInstalledFilePath: string = `${tempDir}/.playwright-codespaces-extension-installed.txt`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Externalize the name of the file into a constant.
| this._mode = mode; | ||
| this._terminal = terminal; | ||
| this._onStatusChange = onStatusChange; | ||
| this._tmpPath = tmpPath; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like tmpPath should be better understood as playwrightInstallPath or similar? Its purpose seems to be a folder for installing versions of Playwright, so it would be useful for it to be a configuration setting and be constant.
| ws1.on('close', () => { | ||
| if (ws2.readyState === WebSocket.OPEN) { | ||
| ws2.close(); | ||
| } | ||
| }); | ||
| ws2.on('close', () => { | ||
| if (ws1.readyState === WebSocket.OPEN) { | ||
| ws1.close(); | ||
| } | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These should be using once since you can't close a socket twice.
| handshake = undefined; | ||
| }); | ||
|
|
||
| ws.onerror = (error) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this different than on('error', ...)? Weirdly inconsistent.
| }; | ||
|
|
||
| return new Promise<WebSocket>((resolve, reject) => { | ||
| ws.onmessage = async (event) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Uhh... weirdly inconsistent message handling registration
|
|
||
| this.status = 'browser-server-running'; | ||
|
|
||
| // send ack so that the counterpart also knows to start forwarding messages |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this sleep here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Come up with an actual icon for this extension. This was just a placeholder.
| // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. | ||
| // See LICENSE in the project root for license information. | ||
|
|
||
| export * from './PlaywrightBrowserTunnel'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"no-restricted-syntax": [
"error",
{
"selector": "ExportAllDeclaration",
"message": "Use explicit named exports instead of `export * from '...'`."
}
]
|
|
||
| /** | ||
| * Allowed Playwright browser names. | ||
| * @alpha |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm pretty sure the TSDoc configuration in the rushstack repo doesn't define the @alpha tag. Use @beta instead.
| * Allowed Playwright browser names. | ||
| * @alpha | ||
| */ | ||
| export type BrowserNames = 'chromium' | 'firefox' | 'webkit'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| export type BrowserNames = 'chromium' | 'firefox' | 'webkit'; | |
| export type BrowserName = 'chromium' | 'firefox' | 'webkit'; |
| playwrightVersion: semver.SemVer; | ||
| } | ||
|
|
||
| type ITunnelMode = 'poll-connection' | 'wait-for-incoming-connection'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| type ITunnelMode = 'poll-connection' | 'wait-for-incoming-connection'; | |
| type TunnelMode = 'poll-connection' | 'wait-for-incoming-connection'; |
This isn't an interface.
| export type IPlaywrightTunnelOptions = { | ||
| terminal: ITerminal; | ||
| onStatusChange: (status: TunnelStatus) => void; | ||
| tmpPath: string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| tmpPath: string; | |
| tempPath: string; |
| "displayName": "Playwright on Codespaces", | ||
| "description": "VS Code extension to enable Playwright testing in GitHub Codespaces.", | ||
| "homepage": "https://github.com/microsoft/rushstack/tree/main/vscode-extensions/playwright-on-codespaces-vscode-extension", | ||
| "icon": "assets/extension-icon.png", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you include the original vector version of this icon (either SVG or Illustrator file) as well?
| entry: { | ||
| extension: './lib/extension.js' | ||
| }, | ||
| outputPath: path.resolve(__dirname, 'dist', 'vsix', 'unpacked') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| outputPath: path.resolve(__dirname, 'dist', 'vsix', 'unpacked') | |
| outputPath: `${__dirname}/dist/vsix/unpacked` |
| let tunnel: PlaywrightTunnel | undefined; | ||
|
|
||
| function getTmpPath(): string { | ||
| return path.join(os.tmpdir(), 'playwright-browser-tunnel'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| return path.join(os.tmpdir(), 'playwright-browser-tunnel'); | |
| return `${os.tmpdir()}/playwright-browser-tunnel`; |
Should the temp folder path be configurable?
| ); | ||
| } | ||
|
|
||
| async function handleStartTunnel(): Promise<void> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| async function handleStartTunnel(): Promise<void> { | |
| async function handleStartTunnelAsync(): Promise<void> { |
| } | ||
| } | ||
|
|
||
| async function handleStopTunnel(): Promise<void> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| async function handleStopTunnel(): Promise<void> { | |
| async function handleStopTunnelAsync(): Promise<void> { |
Summary
Introducing Playwright Browser Server. A CLI based tool to launch a remote browser provider for Playwright.
This tool enables using headful browsers on the local machine when using Playwright with VS Code remote sessions.
Use cases:
Details
This tool uses a reverse port forwarder concept to tunnel the Playwright WebSocket control messages over VS Code port forwarding.
Flow:
Playwright MCP can also be used through this setup in VS Code remote environments.
sequenceDiagram participant PT as Playwright Tests participant BF as Browser Fixture participant TS as Tunnel Server (WebSocket) participant HS as HTTP Server participant VSC as VS Code Extension participant BS as Browser Server participant LB as Local Browser Note over PT,LB: Context: Enables local browser testing for remote VS Code environments (e.g., Codespaces) PT->>BF: Trigger custom browser fixture par Fixture Setup BF->>HS: Launch localhost HTTP server BF->>TS: Launch WebSocket tunnel server (well-known port) end BF->>HS: browser.connect('http://localhost:<port>? browser=chromium&launchOptions={}') loop Polling VSC->>TS: Poll for connection (well-known port) end TS-->>VSC: WebSocket connection established BF->>VSC: Send handshake (browser type, launchOptions, Playwright version) VSC->>VSC: Install requested Playwright version VSC->>VSC: Install requested browser VSC->>BS: Launch browserServer via Playwright API BS->>LB: Start local browser instance VSC->>BS: Create WebSocket client connection VSC->>TS: Send acknowledgement (ready to go) par Setup Forwarding Note over BF,TS: Fixture: PT ↔ Tunnel Server Note over VSC,BS: Extension: Tunnel Server ↔ Browser Server end BF->>BF: Flush buffered messages from test rect rgb(200, 230, 200) Note over PT,LB: Transparent bidirectional communication established PT->>BF: Playwright commands BF->>TS: Forward to tunnel TS->>VSC: Forward to extension VSC->>BS: Forward to browser server BS->>LB: Execute in local browser LB-->>BS: Response BS-->>VSC: Forward response VSC-->>TS: Forward to tunnel TS-->>BF: Forward to fixture BF-->>PT: Return to test end Note over PT,LB: 🎉 Profit! Local browser available to remote tests transparentlyHow it was tested
Tested end-to-end with the Playwright test and fixture introduced in this change.
Tested this on pure local setup as well as on Codespaces
Tested MCP change by prompting agent mode to use Playwright MCP on Codespaces.