Skip to content

Commit 7233e76

Browse files
committed
feat(webkit): launch WebKit in WSL directly over WebSocket transport
Launch the Linux WebKit build inside the "playwright" WSL distribution via wsl.exe with --remote-debugging-port=0 and connect to the printed ws:// endpoint from the Windows host (reachable thanks to WSL localhost forwarding). This replaces the reverted intermediate proxy server. Make killForTests wait for the transport disconnect so the kill appears atomic to clients, add a tests_webkit_wsl workflow that runs the WebKit suite headed and headless on the self-hosted Windows pool, and update library/page test expectations for the webkit-wsl channel. Reference: #37036
1 parent 51d86cc commit 7233e76

12 files changed

Lines changed: 168 additions & 46 deletions

File tree

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
name: "tests webkit wsl"
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- release-*
8+
pull_request:
9+
paths-ignore:
10+
- 'browser_patches/**'
11+
- 'docs/**'
12+
- 'packages/playwright/src/mcp/**'
13+
- 'tests/mcp/**'
14+
branches:
15+
- main
16+
- release-*
17+
workflow_dispatch:
18+
19+
env:
20+
# Force terminal colors. @see https://www.npmjs.com/package/colors
21+
FORCE_COLOR: 1
22+
23+
permissions:
24+
id-token: write # This is required for OIDC login (azure/login) to succeed
25+
contents: read # This is required for actions/checkout to succeed
26+
27+
jobs:
28+
test_webkit_wsl:
29+
name: "Tests @ WebKit WSL ${{ matrix.headed && '(headed)' || '(headless)' }}"
30+
# WebKit in WSL needs a Windows host with WSL2 mirrored networking, which the
31+
# GitHub-hosted windows-latest image does not support. Use the self-hosted pool.
32+
runs-on: ["self-hosted", "1ES.Pool=DevDivPlaywrightWindows11"]
33+
strategy:
34+
fail-fast: false
35+
matrix:
36+
headed: [true, false]
37+
steps:
38+
- uses: actions/checkout@v6
39+
- uses: actions/setup-node@v6
40+
with:
41+
node-version: 20
42+
# WebKit in WSL only reaches servers on the Windows host with mirrored networking.
43+
- name: Enable WSL2 networkingMode=mirrored
44+
shell: powershell
45+
run: Add-Content -Path $env:USERPROFILE\.wslconfig -Value "[wsl2]`nnetworkingMode=mirrored"
46+
- run: npm ci
47+
env:
48+
DEBUG: pw:install
49+
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
50+
- run: npm run build
51+
- run: npx playwright install webkit-wsl ffmpeg
52+
- name: Run tests
53+
run: npm run wtest -- ${{ matrix.headed && '--headed' || '' }}
54+
env:
55+
PWTEST_CHANNEL: webkit-wsl
56+
PW_TAG: "@webkit-wsl-${{ matrix.headed && 'headed' || 'headless' }}"
57+
- name: Azure Login
58+
if: ${{ !cancelled() && github.event_name == 'push' && github.repository == 'microsoft/playwright' }}
59+
uses: azure/login@v2
60+
with:
61+
client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
62+
tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
63+
subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
64+
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
65+
if: ${{ !cancelled() && github.event_name == 'push' && github.repository == 'microsoft/playwright' }}
66+
shell: bash
67+
- uses: actions/upload-artifact@v7
68+
if: ${{ !cancelled() }}
69+
with:
70+
name: webkit-wsl-${{ matrix.headed && 'headed' || 'headless' }}-results
71+
path: test-results

packages/playwright-core/src/server/browser.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ export abstract class Browser extends SdkObject {
199199

200200
async killForTests(progress: Progress) {
201201
await progress.race(this.options.browserProcess.kill());
202+
// With WebSocket transport the disconnect is not necessarily dispatched before the
203+
// process exit, wait for it explicitly to make the kill appear atomic to the clients.
204+
if (this.isConnected())
205+
await progress.race(new Promise(x => this.once(Browser.Events.Disconnected, x)));
202206
}
203207
}
204208

packages/playwright-core/src/server/browserType.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ export abstract class BrowserType extends SdkObject {
273273
const updatedLog = this.doRewriteStartupLog(log);
274274
throw new Error(`Failed to launch the browser process.\nBrowser logs:\n${updatedLog}`);
275275
}
276-
if (!this.supportsPipeTransport()) {
276+
if (!this.supportsPipeTransport(options)) {
277277
transport = await WebSocketTransport.connect(progress, wsEndpoint!);
278278
} else {
279279
const stdio = launchedProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream];
@@ -337,7 +337,7 @@ export abstract class BrowserType extends SdkObject {
337337
async prepareUserDataDir(options: types.LaunchOptions, userDataDir: string): Promise<void> {
338338
}
339339

340-
supportsPipeTransport(): boolean {
340+
supportsPipeTransport(options: types.LaunchOptions): boolean {
341341
return true;
342342
}
343343

packages/playwright-core/src/server/registry/index.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -897,12 +897,19 @@ export class Registry {
897897
_dependencyGroup: 'webkit',
898898
_isHermeticInstallation: true,
899899
});
900+
const wslExecutable = process.platform === 'win32' ? path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'wsl.exe') : undefined;
900901
this._executables.push({
901902
name: 'webkit-wsl',
902903
browserName: 'webkit',
903904
directory: webkit.dir,
904-
executablePath: () => webkitExecutable,
905-
executablePathOrDie: (sdkLanguage: string) => executablePathOrDie('webkit', webkitExecutable, webkit.installByDefault, sdkLanguage),
905+
executablePath: () => wslExecutable,
906+
executablePathOrDie: () => {
907+
if (!wslExecutable)
908+
throw new Error(`webkit-wsl is only supported on Windows`);
909+
return wslExecutable;
910+
},
911+
// WebKit is installed inside the WSL distribution by install_webkit_wsl.ps1.
912+
wslExecutablePath: `/home/pwuser/.cache/ms-playwright/webkit-${webkit.revision}/pw_run.sh`,
906913
installType: 'download-on-demand',
907914
title: 'Webkit in WSL',
908915
_validateHostRequirements: (sdkLanguage: string) => Promise.resolve(),

packages/playwright-core/src/server/webkit/webkit.ts

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@
1717

1818
import path from 'path';
1919

20+
import { ManualPromise } from '@isomorphic/manualPromise';
2021
import { wrapInASCIIBox } from '@utils/ascii';
2122
import { spawnAsync } from '@utils/spawnAsync';
2223
import { kBrowserCloseMessageId } from './wkConnection';
2324
import { Browser } from '../browser';
2425
import { BrowserType, kNoXServerRunningError } from '../browserType';
26+
import { registry } from '../registry';
2527
import { WKBrowser } from './wkBrowser';
2628
import { connectOverRDP } from './webview/wvBrowser';
2729

@@ -30,8 +32,14 @@ import type { SdkObject } from '../instrumentation';
3032
import type { Progress } from '../progress';
3133
import type { ConnectionTransport } from '../transport';
3234
import type * as types from '../types';
35+
import type { RecentLogsCollector } from '@utils/debugLogger';
3336
import type * as channels from '@protocol/channels';
3437

38+
// Must be kept in sync with bin/install_webkit_wsl.ps1 that provisions the distribution.
39+
const kWSLDistribution = 'playwright';
40+
const kWSLUser = 'pwuser';
41+
const kWSLHome = '/home/pwuser';
42+
3543
export class WebKit extends BrowserType {
3644
constructor(parent: SdkObject) {
3745
super(parent, 'webkit');
@@ -48,10 +56,29 @@ export class WebKit extends BrowserType {
4856
override amendEnvironment(env: NodeJS.ProcessEnv, userDataDir: string, isPersistent: boolean, options: types.LaunchOptions): NodeJS.ProcessEnv {
4957
return {
5058
...env,
51-
CURL_COOKIE_JAR_PATH: process.platform === 'win32' && isPersistent ? path.join(userDataDir, 'cookiejar.db') : undefined,
59+
// Cookie jar is only used by the Windows port of WebKit.
60+
CURL_COOKIE_JAR_PATH: process.platform === 'win32' && options.channel !== 'webkit-wsl' && isPersistent ? path.join(userDataDir, 'cookiejar.db') : undefined,
5261
};
5362
}
5463

64+
override supportsPipeTransport(options: types.LaunchOptions): boolean {
65+
return options.channel !== 'webkit-wsl';
66+
}
67+
68+
override async waitForReadyState(options: types.LaunchOptions, browserLogsCollector: RecentLogsCollector): Promise<{ wsEndpoint?: string }> {
69+
if (options.channel !== 'webkit-wsl')
70+
return {};
71+
const result = new ManualPromise<{ wsEndpoint?: string }>();
72+
browserLogsCollector.onMessage(message => {
73+
// The browser is listening on the loopback interface inside WSL, the endpoint
74+
// is reachable from the Windows host thanks to WSL localhost forwarding.
75+
const match = message.match(/Playwright listening on (ws:\/\/\S+)/);
76+
if (match)
77+
result.resolve({ wsEndpoint: match[1] });
78+
});
79+
return result;
80+
}
81+
5582
override doRewriteStartupLog(logs: string): string {
5683
if (logs.includes('Failed to open display') || logs.includes('cannot open display'))
5784
logs = '\n' + wrapInASCIIBox(kNoXServerRunningError, 1);
@@ -70,14 +97,31 @@ export class WebKit extends BrowserType {
7097
throw this._createUserDataDirArgMisuseError('--user-data-dir');
7198
if (args.find(arg => !arg.startsWith('-')))
7299
throw new Error('Arguments can not specify page to be opened');
73-
const webkitArguments = ['--inspector-pipe'];
100+
const isWSL = options.channel === 'webkit-wsl';
101+
// wsl.exe does not forward extra file descriptors to the Linux process, so the pipe
102+
// transport cannot work across the WSL boundary. Instead, the browser exposes the
103+
// protocol over a WebSocket server and we connect to it from the Windows host.
104+
const webkitArguments = [isWSL ? '--remote-debugging-port=0' : '--inspector-pipe'];
105+
106+
if (isWSL) {
107+
if (options.executablePath)
108+
throw new Error('Cannot specify executablePath when using the "webkit-wsl" channel.');
109+
// The actual command is `wsl.exe -- <linux executable> <browser args>`.
110+
webkitArguments.unshift(
111+
'-d', kWSLDistribution,
112+
'-u', kWSLUser,
113+
'--cd', kWSLHome,
114+
'--',
115+
registry.findExecutable('webkit-wsl')!.wslExecutablePath!,
116+
);
117+
}
74118

75-
if (process.platform === 'win32' && options.channel !== 'webkit-wsl')
119+
if (process.platform === 'win32' && !isWSL)
76120
webkitArguments.push('--disable-accelerated-compositing');
77121
if (headless)
78122
webkitArguments.push('--headless');
79123
if (isPersistent)
80-
webkitArguments.push(`--user-data-dir=${options.channel === 'webkit-wsl' ? await translatePathToWSL(userDataDir) : userDataDir}`);
124+
webkitArguments.push(`--user-data-dir=${isWSL ? await translatePathToWSL(userDataDir) : userDataDir}`);
81125
else
82126
webkitArguments.push(`--no-startup-window`);
83127
const proxy = options.proxyOverride || options.proxy;
@@ -86,7 +130,7 @@ export class WebKit extends BrowserType {
86130
webkitArguments.push(`--proxy=${proxy.server}`);
87131
if (proxy.bypass)
88132
webkitArguments.push(`--proxy-bypass-list=${proxy.bypass}`);
89-
} else if (process.platform === 'linux' || (process.platform === 'win32' && options.channel === 'webkit-wsl')) {
133+
} else if (process.platform === 'linux' || isWSL) {
90134
webkitArguments.push(`--proxy=${proxy.server}`);
91135
if (proxy.bypass)
92136
webkitArguments.push(...proxy.bypass.split(',').map(t => `--ignore-host=${t}`));
@@ -106,6 +150,6 @@ export class WebKit extends BrowserType {
106150
}
107151

108152
export async function translatePathToWSL(path: string): Promise<string> {
109-
const { stdout } = await spawnAsync('wsl.exe', ['-d', 'playwright', '--cd', '/home/pwuser', 'wslpath', path.replace(/\\/g, '\\\\')]);
153+
const { stdout } = await spawnAsync('wsl.exe', ['-d', kWSLDistribution, '--cd', kWSLHome, 'wslpath', path.replace(/\\/g, '\\\\')]);
110154
return stdout.toString().trim();
111155
}

tests/library/defaultbrowsercontext-2.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,9 @@ it('should accept relative userDataDir', async ({ createUserDataDir, browserType
108108
await context.close();
109109
});
110110

111-
it('should restore state from userDataDir', async ({ browserType, server, createUserDataDir }) => {
111+
it('should restore state from userDataDir', async ({ browserType, server, createUserDataDir, channel }) => {
112112
it.slow();
113+
it.fixme(channel === 'webkit-wsl', 'Pending local storage writes are lost on close, see https://github.com/microsoft/playwright-browsers/issues/2275');
113114

114115
const userDataDir = await createUserDataDir();
115116
const browserContext = await browserType.launchPersistentContext(userDataDir);

tests/library/har-websocket.spec.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ it('should include websocket handshake headers and status', async ({ contextFact
130130
expect(responseHeaderNames).toContain('sec-websocket-accept');
131131
});
132132

133-
async function testWebSocketMessages(contextFactory, server, testInfo, content) {
133+
async function testWebSocketMessages(contextFactory, server, testInfo, content, channel?) {
134134
const incomingText = ['x'.repeat(125), 'x'.repeat(126), 'x'.repeat(2 ** 16)];
135135
const incomingBinary = [(new Array(125)).fill(0x01), (new Array(126)).fill(0x01), (new Array(2 ** 16)).fill(0x01)];
136136
const outgoingText = ['y'.repeat(125), 'y'.repeat(126), 'y'.repeat(2 ** 16)];
@@ -198,23 +198,27 @@ async function testWebSocketMessages(contextFactory, server, testInfo, content)
198198
...outgoingText.map(m => ({ type: 'send', opcode: 1, data: m })),
199199
...outgoingBinary.map(m => ({ type: 'send', opcode: 2, data: m })),
200200
]);
201-
for (const m of messages) {
202-
expect(m.time).toBeGreaterThanOrEqual(beforeMs - 1);
203-
expect(m.time).toBeLessThanOrEqual(afterMs + 1);
201+
// The WSL VM clock drifts relative to the Windows host clock, so the browser-reported
202+
// message times cannot be compared against the host wall clock.
203+
if (channel !== 'webkit-wsl') {
204+
for (const m of messages) {
205+
expect(m.time).toBeGreaterThanOrEqual(beforeMs - 1);
206+
expect(m.time).toBeLessThanOrEqual(afterMs + 1);
207+
}
204208
}
205209
expect(messages[0].time).toBeLessThanOrEqual(messages[1].time);
206210
expect(wsEntry.time).toBeGreaterThanOrEqual(messages[messages.length - 1].time - messages[0].time);
207211
}
208212

209-
it('should embed websocket messages', async ({ contextFactory, server }, testInfo) => {
210-
await testWebSocketMessages(contextFactory, server, testInfo, 'embed');
213+
it('should embed websocket messages', async ({ contextFactory, server, channel }, testInfo) => {
214+
await testWebSocketMessages(contextFactory, server, testInfo, 'embed', channel);
211215
});
212216

213-
it('should attach websocket messages', async ({ contextFactory, server }, testInfo) => {
214-
await testWebSocketMessages(contextFactory, server, testInfo, 'attach');
217+
it('should attach websocket messages', async ({ contextFactory, server, channel }, testInfo) => {
218+
await testWebSocketMessages(contextFactory, server, testInfo, 'attach', channel);
215219
});
216220

217-
it('should attach websocket messages for a still open websocket after stopping', async ({ contextFactory, server }, testInfo) => {
221+
it('should attach websocket messages for a still open websocket after stopping', async ({ contextFactory, server, channel }, testInfo) => {
218222
const incomingText = 'incoming';
219223
const incomingBinary = [0x01, 0x02, 0x03, 0x04];
220224
const outgoingText = 'outgoing';
@@ -271,9 +275,13 @@ it('should attach websocket messages for a still open websocket after stopping',
271275
{ type: 'send', opcode: 2, data: outgoingBinary },
272276
{ type: 'receive', opcode: 2, data: incomingBinary },
273277
]);
274-
for (const m of messages) {
275-
expect(m.time).toBeGreaterThanOrEqual(beforeMs - 1);
276-
expect(m.time).toBeLessThanOrEqual(afterMs + 1);
278+
// The WSL VM clock drifts relative to the Windows host clock, so the browser-reported
279+
// message times cannot be compared against the host wall clock.
280+
if (channel !== 'webkit-wsl') {
281+
for (const m of messages) {
282+
expect(m.time).toBeGreaterThanOrEqual(beforeMs - 1);
283+
expect(m.time).toBeLessThanOrEqual(afterMs + 1);
284+
}
277285
}
278286
expect(messages[0].time).toBeLessThanOrEqual(messages[1].time);
279287
expect(wsEntry.time).toBeGreaterThanOrEqual(messages[messages.length - 1].time - messages[0].time);
@@ -283,7 +291,8 @@ it('should omit websocket messages', async ({ contextFactory, server }, testInfo
283291
await testWebSocketMessages(contextFactory, server, testInfo, 'omit');
284292
});
285293

286-
it('should record websocket connection failure', async ({ contextFactory, server }, testInfo) => {
294+
it('should record websocket connection failure', async ({ contextFactory, server, channel }, testInfo) => {
295+
it.skip(channel === 'webkit-wsl', 'Connection to an unbound localhost port from WSL is not refused in mirrored networking mode');
287296
// Reserve a port and immediately release it so the WebSocket connect attempt is refused.
288297
const portReservation = net.createServer();
289298
await new Promise<void>(resolve => portReservation.listen(0, '127.0.0.1', () => resolve()));

tests/library/har.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -624,8 +624,8 @@ it('should have connection details', async ({ contextFactory, server, browserNam
624624
expect(securityDetails).toEqual({});
625625
});
626626

627-
it('should have security details', async ({ contextFactory, httpsServer, browserName, platform, mode, isFrozenWebkit }, testInfo) => {
628-
it.fail(browserName === 'webkit' && platform === 'win32');
627+
it('should have security details', async ({ contextFactory, httpsServer, browserName, platform, mode, channel, isFrozenWebkit }, testInfo) => {
628+
it.fail(browserName === 'webkit' && platform === 'win32' && channel !== 'webkit-wsl');
629629
it.skip(isFrozenWebkit);
630630

631631
const { page, getLog } = await pageWithHar(contextFactory, testInfo);

tests/library/playwright.config.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,6 @@ const reporters = () => {
5353
return result;
5454
};
5555

56-
let connectOptions: any;
57-
let webServer: Config['webServer'];
58-
59-
if (channel === 'webkit-wsl') {
60-
connectOptions = { wsEndpoint: 'ws://localhost:3777/' };
61-
webServer = {
62-
command: 'set PWTEST_UNDER_TEST=1 && set WSLENV=PWTEST_UNDER_TEST && wsl.exe -d playwright -u pwuser -- bash -lc \'/home/pwuser/node/bin/npx playwright run-server --port=3777\'',
63-
url: 'http://localhost:3777',
64-
};
65-
}
66-
6756
const config: Config<PlaywrightWorkerOptions & PlaywrightTestOptions & TestModeWorkerOptions> = {
6857
testDir,
6958
outputDir,
@@ -80,10 +69,6 @@ const config: Config<PlaywrightWorkerOptions & PlaywrightTestOptions & TestModeW
8069
reporter: reporters(),
8170
tag: process.env.PW_TAG,
8271
projects: [],
83-
use: {
84-
connectOptions,
85-
},
86-
webServer,
8772
};
8873

8974
const browserNames = ['chromium', 'webkit', 'firefox'] as BrowserName[];

tests/page/page-event-pageerror.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ it('clearPageErrors should work', async ({ page }) => {
197197

198198
it('should fire illegal character error', {
199199
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/38388' },
200-
}, async ({ page, server, browserName, isWindows }) => {
200+
}, async ({ page, server, browserName, isWindows, channel }) => {
201201
server.setRoute('/error.html', (req, res) => {
202202
res.end(`
203203
<!doctype html>
@@ -223,7 +223,7 @@ it('should fire illegal character error', {
223223
]);
224224
if (browserName === 'chromium')
225225
expect(error.message).toContain('Invalid or unexpected token');
226-
else if (browserName === 'webkit' && isWindows)
226+
else if (browserName === 'webkit' && isWindows && channel !== 'webkit-wsl')
227227
expect(error.message).toContain('No identifiers allowed directly after numeric literal');
228228
else if (browserName === 'webkit')
229229
expect(error.message).toContain('Invalid character');

0 commit comments

Comments
 (0)