Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions .github/workflows/tests_webkit_wsl.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: "tests webkit wsl"

on:
push:
branches:
- main
- release-*
pull_request:
paths-ignore:
- 'browser_patches/**'
- 'docs/**'
- 'packages/playwright/src/mcp/**'
- 'tests/mcp/**'
branches:
- main
- release-*
workflow_dispatch:

env:
# Force terminal colors. @see https://www.npmjs.com/package/colors
FORCE_COLOR: 1

permissions:
id-token: write # This is required for OIDC login (azure/login) to succeed
contents: read # This is required for actions/checkout to succeed

jobs:
test_webkit_wsl:
name: "Tests @ WebKit WSL ${{ matrix.headed && '(headed)' || '(headless)' }}"
# WebKit in WSL needs a Windows host with WSL2 mirrored networking, which the
# GitHub-hosted windows-latest image does not support. Use the self-hosted pool.
runs-on: ["self-hosted", "1ES.Pool=DevDivPlaywrightWindows11"]
strategy:
fail-fast: false
matrix:
headed: [true, false]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 20
# WebKit in WSL only reaches servers on the Windows host with mirrored networking.
- name: Enable WSL2 networkingMode=mirrored
shell: powershell
run: Add-Content -Path $env:USERPROFILE\.wslconfig -Value "[wsl2]`nnetworkingMode=mirrored"
- run: npm ci
env:
DEBUG: pw:install
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build
- run: npx playwright install webkit-wsl
- name: Run tests
run: npm run wtest -- ${{ matrix.headed && '--headed' || '' }}
env:
PWTEST_CHANNEL: webkit-wsl
PW_TAG: "@webkit-wsl-${{ matrix.headed && 'headed' || 'headless' }}"
- name: Azure Login
if: ${{ !cancelled() && github.event_name == 'push' && github.repository == 'microsoft/playwright' }}
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
if: ${{ !cancelled() && github.event_name == 'push' && github.repository == 'microsoft/playwright' }}
shell: bash
- uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
name: webkit-wsl-${{ matrix.headed && 'headed' || 'headless' }}-results
path: test-results
2 changes: 2 additions & 0 deletions packages/playwright-core/src/server/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@ export abstract class Browser extends SdkObject {

async killForTests(progress: Progress) {
await progress.race(this.options.browserProcess.kill());
if (this.isConnected())
await progress.race(new Promise(x => this.once(Browser.Events.Disconnected, x)));
}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/playwright-core/src/server/browserType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ export abstract class BrowserType extends SdkObject {
const updatedLog = this.doRewriteStartupLog(log);
throw new Error(`Failed to launch the browser process.\nBrowser logs:\n${updatedLog}`);
}
if (!this.supportsPipeTransport()) {
if (!this.supportsPipeTransport(options)) {
transport = await WebSocketTransport.connect(progress, wsEndpoint!);
} else {
const stdio = launchedProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream];
Expand Down Expand Up @@ -337,7 +337,7 @@ export abstract class BrowserType extends SdkObject {
async prepareUserDataDir(options: types.LaunchOptions, userDataDir: string): Promise<void> {
}

supportsPipeTransport(): boolean {
supportsPipeTransport(options: types.LaunchOptions): boolean {
return true;
}

Expand Down
11 changes: 9 additions & 2 deletions packages/playwright-core/src/server/registry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -897,12 +897,19 @@ export class Registry {
_dependencyGroup: 'webkit',
_isHermeticInstallation: true,
});
const wslExecutable = process.platform === 'win32' ? path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'wsl.exe') : undefined;
this._executables.push({
name: 'webkit-wsl',
browserName: 'webkit',
directory: webkit.dir,
executablePath: () => webkitExecutable,
executablePathOrDie: (sdkLanguage: string) => executablePathOrDie('webkit', webkitExecutable, webkit.installByDefault, sdkLanguage),
executablePath: () => wslExecutable,
executablePathOrDie: () => {
if (!wslExecutable)
throw new Error(`webkit-wsl is only supported on Windows`);
return wslExecutable;
},
// WebKit is installed inside the WSL distribution by install_webkit_wsl.ps1.
wslExecutablePath: `/home/pwuser/.cache/ms-playwright/webkit-${webkit.revision}/pw_run.sh`,
installType: 'download-on-demand',
title: 'Webkit in WSL',
_validateHostRequirements: (sdkLanguage: string) => Promise.resolve(),
Expand Down
51 changes: 45 additions & 6 deletions packages/playwright-core/src/server/webkit/webkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@

import path from 'path';

import { ManualPromise } from '@isomorphic/manualPromise';
import { wrapInASCIIBox } from '@utils/ascii';
import { spawnAsync } from '@utils/spawnAsync';
import { kBrowserCloseMessageId } from './wkConnection';
import { Browser } from '../browser';
import { BrowserType, kNoXServerRunningError } from '../browserType';
import { registry } from '../registry';
import { WKBrowser } from './wkBrowser';
import { connectOverRDP } from './webview/wvBrowser';

Expand All @@ -30,8 +32,14 @@ import type { SdkObject } from '../instrumentation';
import type { Progress } from '../progress';
import type { ConnectionTransport } from '../transport';
import type * as types from '../types';
import type { RecentLogsCollector } from '@utils/debugLogger';
import type * as channels from '@protocol/channels';

// Must be kept in sync with bin/install_webkit_wsl.ps1 that provisions the distribution.
const kWSLDistribution = 'playwright';
const kWSLUser = 'pwuser';
const kWSLHome = '/home/pwuser';

export class WebKit extends BrowserType {
constructor(parent: SdkObject) {
super(parent, 'webkit');
Expand All @@ -48,10 +56,27 @@ export class WebKit extends BrowserType {
override amendEnvironment(env: NodeJS.ProcessEnv, userDataDir: string, isPersistent: boolean, options: types.LaunchOptions): NodeJS.ProcessEnv {
return {
...env,
CURL_COOKIE_JAR_PATH: process.platform === 'win32' && isPersistent ? path.join(userDataDir, 'cookiejar.db') : undefined,
// Cookie jar is only used by the Windows port of WebKit.
CURL_COOKIE_JAR_PATH: process.platform === 'win32' && options.channel !== 'webkit-wsl' && isPersistent ? path.join(userDataDir, 'cookiejar.db') : undefined,
};
}

override supportsPipeTransport(options: types.LaunchOptions): boolean {
return options.channel !== 'webkit-wsl';
}

override async waitForReadyState(options: types.LaunchOptions, browserLogsCollector: RecentLogsCollector): Promise<{ wsEndpoint?: string }> {
if (options.channel !== 'webkit-wsl')
return {};
const result = new ManualPromise<{ wsEndpoint?: string }>();
browserLogsCollector.onMessage(message => {
const match = message.match(/Playwright listening on (ws:\/\/\S+)/);
if (match)
result.resolve({ wsEndpoint: match[1] });
});
return result;
}

override doRewriteStartupLog(logs: string): string {
if (logs.includes('Failed to open display') || logs.includes('cannot open display'))
logs = '\n' + wrapInASCIIBox(kNoXServerRunningError, 1);
Expand All @@ -70,14 +95,28 @@ export class WebKit extends BrowserType {
throw this._createUserDataDirArgMisuseError('--user-data-dir');
if (args.find(arg => !arg.startsWith('-')))
throw new Error('Arguments can not specify page to be opened');
const webkitArguments = ['--inspector-pipe'];
const isWSL = options.channel === 'webkit-wsl';
const webkitArguments = [isWSL ? '--remote-debugging-port=0' : '--inspector-pipe'];

if (isWSL) {
if (options.executablePath)
throw new Error('Cannot specify executablePath when using the "webkit-wsl" channel.');
// The actual command is `wsl.exe -- <linux executable> <browser args>`.
webkitArguments.unshift(
'-d', kWSLDistribution,
'-u', kWSLUser,
'--cd', kWSLHome,
'--',
registry.findExecutable('webkit-wsl')!.wslExecutablePath!,
);
}

if (process.platform === 'win32' && options.channel !== 'webkit-wsl')
if (process.platform === 'win32' && !isWSL)
webkitArguments.push('--disable-accelerated-compositing');
if (headless)
webkitArguments.push('--headless');
if (isPersistent)
webkitArguments.push(`--user-data-dir=${options.channel === 'webkit-wsl' ? await translatePathToWSL(userDataDir) : userDataDir}`);
webkitArguments.push(`--user-data-dir=${isWSL ? await translatePathToWSL(userDataDir) : userDataDir}`);
else
webkitArguments.push(`--no-startup-window`);
const proxy = options.proxyOverride || options.proxy;
Expand All @@ -86,7 +125,7 @@ export class WebKit extends BrowserType {
webkitArguments.push(`--proxy=${proxy.server}`);
if (proxy.bypass)
webkitArguments.push(`--proxy-bypass-list=${proxy.bypass}`);
} else if (process.platform === 'linux' || (process.platform === 'win32' && options.channel === 'webkit-wsl')) {
} else if (process.platform === 'linux' || isWSL) {
webkitArguments.push(`--proxy=${proxy.server}`);
if (proxy.bypass)
webkitArguments.push(...proxy.bypass.split(',').map(t => `--ignore-host=${t}`));
Expand All @@ -106,6 +145,6 @@ export class WebKit extends BrowserType {
}

export async function translatePathToWSL(path: string): Promise<string> {
const { stdout } = await spawnAsync('wsl.exe', ['-d', 'playwright', '--cd', '/home/pwuser', 'wslpath', path.replace(/\\/g, '\\\\')]);
const { stdout } = await spawnAsync('wsl.exe', ['-d', kWSLDistribution, '--cd', kWSLHome, 'wslpath', path.replace(/\\/g, '\\\\')]);
return stdout.toString().trim();
}
3 changes: 2 additions & 1 deletion tests/library/defaultbrowsercontext-2.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,9 @@ it('should accept relative userDataDir', async ({ createUserDataDir, browserType
await context.close();
});

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

const userDataDir = await createUserDataDir();
const browserContext = await browserType.launchPersistentContext(userDataDir);
Expand Down
35 changes: 22 additions & 13 deletions tests/library/har-websocket.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ it('should include websocket handshake headers and status', async ({ contextFact
expect(responseHeaderNames).toContain('sec-websocket-accept');
});

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

it('should embed websocket messages', async ({ contextFactory, server }, testInfo) => {
await testWebSocketMessages(contextFactory, server, testInfo, 'embed');
it('should embed websocket messages', async ({ contextFactory, server, channel }, testInfo) => {
await testWebSocketMessages(contextFactory, server, testInfo, 'embed', channel);
});

it('should attach websocket messages', async ({ contextFactory, server }, testInfo) => {
await testWebSocketMessages(contextFactory, server, testInfo, 'attach');
it('should attach websocket messages', async ({ contextFactory, server, channel }, testInfo) => {
await testWebSocketMessages(contextFactory, server, testInfo, 'attach', channel);
});

it('should attach websocket messages for a still open websocket after stopping', async ({ contextFactory, server }, testInfo) => {
it('should attach websocket messages for a still open websocket after stopping', async ({ contextFactory, server, channel }, testInfo) => {
const incomingText = 'incoming';
const incomingBinary = [0x01, 0x02, 0x03, 0x04];
const outgoingText = 'outgoing';
Expand Down Expand Up @@ -271,9 +275,13 @@ it('should attach websocket messages for a still open websocket after stopping',
{ type: 'send', opcode: 2, data: outgoingBinary },
{ type: 'receive', opcode: 2, data: incomingBinary },
]);
for (const m of messages) {
expect(m.time).toBeGreaterThanOrEqual(beforeMs - 1);
expect(m.time).toBeLessThanOrEqual(afterMs + 1);
// The WSL VM clock drifts relative to the Windows host clock, so the browser-reported
// message times cannot be compared against the host wall clock.
if (channel !== 'webkit-wsl') {
for (const m of messages) {
expect(m.time).toBeGreaterThanOrEqual(beforeMs - 1);
expect(m.time).toBeLessThanOrEqual(afterMs + 1);
}
}
expect(messages[0].time).toBeLessThanOrEqual(messages[1].time);
expect(wsEntry.time).toBeGreaterThanOrEqual(messages[messages.length - 1].time - messages[0].time);
Expand All @@ -283,7 +291,8 @@ it('should omit websocket messages', async ({ contextFactory, server }, testInfo
await testWebSocketMessages(contextFactory, server, testInfo, 'omit');
});

it('should record websocket connection failure', async ({ contextFactory, server }, testInfo) => {
it('should record websocket connection failure', async ({ contextFactory, server, channel }, testInfo) => {
it.skip(channel === 'webkit-wsl', 'Connection to an unbound localhost port from WSL is not refused in mirrored networking mode');
// Reserve a port and immediately release it so the WebSocket connect attempt is refused.
const portReservation = net.createServer();
await new Promise<void>(resolve => portReservation.listen(0, '127.0.0.1', () => resolve()));
Expand Down
4 changes: 2 additions & 2 deletions tests/library/har.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -624,8 +624,8 @@ it('should have connection details', async ({ contextFactory, server, browserNam
expect(securityDetails).toEqual({});
});

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

const { page, getLog } = await pageWithHar(contextFactory, testInfo);
Expand Down
15 changes: 0 additions & 15 deletions tests/library/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,6 @@ const reporters = () => {
return result;
};

let connectOptions: any;
let webServer: Config['webServer'];

if (channel === 'webkit-wsl') {
connectOptions = { wsEndpoint: 'ws://localhost:3777/' };
webServer = {
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\'',
url: 'http://localhost:3777',
};
}

const config: Config<PlaywrightWorkerOptions & PlaywrightTestOptions & TestModeWorkerOptions> = {
testDir,
outputDir,
Expand All @@ -80,10 +69,6 @@ const config: Config<PlaywrightWorkerOptions & PlaywrightTestOptions & TestModeW
reporter: reporters(),
tag: process.env.PW_TAG,
projects: [],
use: {
connectOptions,
},
webServer,
};

const browserNames = ['chromium', 'webkit', 'firefox'] as BrowserName[];
Expand Down
4 changes: 2 additions & 2 deletions tests/page/page-event-pageerror.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ it('clearPageErrors should work', async ({ page }) => {

it('should fire illegal character error', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/38388' },
}, async ({ page, server, browserName, isWindows }) => {
}, async ({ page, server, browserName, isWindows, channel }) => {
server.setRoute('/error.html', (req, res) => {
res.end(`
<!doctype html>
Expand All @@ -223,7 +223,7 @@ it('should fire illegal character error', {
]);
if (browserName === 'chromium')
expect(error.message).toContain('Invalid or unexpected token');
else if (browserName === 'webkit' && isWindows)
else if (browserName === 'webkit' && isWindows && channel !== 'webkit-wsl')
expect(error.message).toContain('No identifiers allowed directly after numeric literal');
else if (browserName === 'webkit')
expect(error.message).toContain('Invalid character');
Expand Down
4 changes: 2 additions & 2 deletions tests/page/page-fill.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@ it('should fill color input', async ({ page }) => {
expect(await page.$eval('input', input => input.value)).toBe('#aaaaaa');
});

it('should fill color input case insensitive', async ({ page, browserName, isWindows }) => {
it('should fill color input case insensitive', async ({ page, browserName, isWindows, channel }) => {
await page.setContent('<input type=color value="#e66465">');
await page.fill('input', '#AbCd00');
if (browserName === 'webkit' && isWindows)
if (browserName === 'webkit' && isWindows && channel !== 'webkit-wsl')
expect(await page.$eval('input', input => input.value)).toBe('#AbCd00');
else
expect(await page.$eval('input', input => input.value)).toBe('#abcd00');
Expand Down
3 changes: 2 additions & 1 deletion tests/page/workers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,9 @@ it('should report console logs', async function({ page }) {
expect(page.url()).not.toContain('blob');
});

it('should have timestamp on worker console messages', async function({ page, isAndroid }) {
it('should have timestamp on worker console messages', async function({ page, isAndroid, channel }) {
it.skip(isAndroid, 'there is a time difference between android emulator and host machine');
it.skip(channel === 'webkit-wsl', 'there is a time difference between WSL VM and host machine');

const before = Date.now() - 1; // Account for the rounding of fractional timestamps.
const [message] = await Promise.all([
Expand Down
Loading