feat(webkit): allow running WebKit via WSL on Windows (#36358)

This commit is contained in:
Max Schmitt 2025-09-09 12:34:30 +02:00 committed by GitHub
parent ec61c0324f
commit 573441cad7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 470 additions and 124 deletions

View File

@ -0,0 +1,35 @@
$ErrorActionPreference = 'Stop'
# WebKit WSL Installation Script
# See webkit-wsl-transport-server.ts for the complete architecture diagram.
# This script sets up a WSL distribution that will be used to run WebKit.
$Distribution = "playwright"
$Username = "pwuser"
$distributions = (wsl --list --quiet) -split "\r?\n"
if ($distributions -contains $Distribution) {
Write-Host "WSL distribution '$Distribution' already exists. Skipping installation."
} else {
Write-Host "Installing new WSL distribution '$Distribution'..."
$VhdSize = "10GB"
wsl --install -d Ubuntu-24.04 --name $Distribution --no-launch --vhd-size $VhdSize
wsl -d $Distribution -u root adduser --gecos GECOS --disabled-password $Username
}
$pwshDirname = (Resolve-Path -Path $PSScriptRoot).Path;
$playwrightCoreRoot = Resolve-Path (Join-Path $pwshDirname "..")
$initScript = @"
if [ ! -f "/home/$Username/node/bin/node" ]; then
mkdir -p /home/$Username/node
curl -fsSL https://nodejs.org/dist/v22.17.0/node-v22.17.0-linux-x64.tar.xz -o /home/$Username/node/node-v22.17.0-linux-x64.tar.xz
tar -xJf /home/$Username/node/node-v22.17.0-linux-x64.tar.xz -C /home/$Username/node --strip-components=1
fi
/home/$Username/node/bin/node cli.js install-deps webkit
cp lib/server/webkit/wsl/webkit-wsl-transport-client.js /home/$Username/
sudo -u $Username PLAYWRIGHT_SKIP_BROWSER_GC=1 /home/$Username/node/bin/node cli.js install webkit
"@ -replace "\r\n", "`n"
wsl -d $Distribution --cd $playwrightCoreRoot -u root -- bash -c "$initScript"
Write-Host "Done!"

View File

@ -92,7 +92,7 @@ export class BidiChromium extends BrowserType {
return false;
}
override defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[] {
override async defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string) {
const chromeArguments = this._innerDefaultArgs(options);
chromeArguments.push(`--user-data-dir=${userDataDir}`);
chromeArguments.push('--remote-debugging-port=0');

View File

@ -92,7 +92,7 @@ export class BidiFirefox extends BrowserType {
});
}
override defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[] {
override async defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string) {
const { args = [], headless } = options;
const userDataDirArg = args.find(arg => arg.startsWith('-profile') || arg.startsWith('--profile'));
if (userDataDirArg)

View File

@ -174,9 +174,9 @@ export abstract class BrowserType extends SdkObject {
if (ignoreAllDefaultArgs)
browserArguments.push(...args);
else if (ignoreDefaultArgs)
browserArguments.push(...this.defaultArgs(options, isPersistent, userDataDir).filter(arg => ignoreDefaultArgs.indexOf(arg) === -1));
browserArguments.push(...(await this.defaultArgs(options, isPersistent, userDataDir)).filter(arg => ignoreDefaultArgs.indexOf(arg) === -1));
else
browserArguments.push(...this.defaultArgs(options, isPersistent, userDataDir));
browserArguments.push(...await this.defaultArgs(options, isPersistent, userDataDir));
let executable: string;
if (executablePath) {
@ -212,7 +212,7 @@ export abstract class BrowserType extends SdkObject {
const { launchedProcess, gracefullyClose, kill } = await launchProcess({
command: prepared.executable,
args: prepared.browserArguments,
env: this.amendEnvironment(env, prepared.userDataDir, isPersistent),
env: this.amendEnvironment(env, prepared.userDataDir, isPersistent, options),
handleSIGINT,
handleSIGTERM,
handleSIGHUP,
@ -338,9 +338,9 @@ export abstract class BrowserType extends SdkObject {
return options.channel || this._name;
}
abstract defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[];
abstract defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): Promise<string[]>;
abstract connectToTransport(transport: ConnectionTransport, options: BrowserOptions, browserLogsCollector: RecentLogsCollector): Promise<Browser>;
abstract amendEnvironment(env: NodeJS.ProcessEnv, userDataDir: string, isPersistent: boolean): NodeJS.ProcessEnv;
abstract amendEnvironment(env: NodeJS.ProcessEnv, userDataDir: string, isPersistent: boolean, options: types.LaunchOptions): NodeJS.ProcessEnv;
abstract doRewriteStartupLog(error: ProtocolError): ProtocolError;
abstract attemptToGracefullyCloseBrowser(transport: ConnectionTransport): void;
}

View File

@ -280,7 +280,7 @@ export class Chromium extends BrowserType {
}
}
override defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[] {
override async defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string) {
const chromeArguments = this._innerDefaultArgs(options);
chromeArguments.push(`--user-data-dir=${userDataDir}`);
if (options.cdpPort !== undefined)

View File

@ -69,7 +69,7 @@ export class Firefox extends BrowserType {
transport.send(message);
}
override defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[] {
override async defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string) {
const { args = [], headless } = options;
const userDataDirArg = args.find(arg => arg.startsWith('-profile') || arg.startsWith('--profile'));
if (userDataDirArg)

View File

@ -510,7 +510,7 @@ const allDownloadable = ['android', 'chromium', 'firefox', 'webkit', 'ffmpeg', '
export interface Executable {
type: 'browser' | 'tool' | 'channel';
name: BrowserName | InternalTool | ChromiumChannel | BidiChannel;
name: BrowserName | InternalTool | ChromiumChannel | BidiChannel | 'webkit-wsl';
browserName: BrowserName | undefined;
installType: 'download-by-default' | 'download-on-demand' | 'install-script' | 'none';
directory: string | undefined;
@ -519,6 +519,7 @@ export interface Executable {
executablePathOrDie(sdkLanguage: string): string;
executablePath(sdkLanguage: string): string | undefined;
_validateHostRequirements(sdkLanguage: string): Promise<void>;
wslExecutablePath?: string
}
interface ExecutableImpl extends Executable {
@ -816,6 +817,31 @@ export class Registry {
_dependencyGroup: 'webkit',
_isHermeticInstallation: true,
});
this._executables.push({
type: 'channel',
name: 'webkit-wsl',
browserName: 'webkit',
directory: webkit.dir,
executablePath: () => process.execPath,
executablePathOrDie: () => process.execPath,
wslExecutablePath: `/home/pwuser/.cache/ms-playwright/webkit-${webkit.revision}/pw_run.sh`,
installType: 'download-on-demand',
_validateHostRequirements: (sdkLanguage: string) => Promise.resolve(),
_isHermeticInstallation: true,
_install: async () => {
if (process.platform !== 'win32')
throw new Error(`WebKit via WSL is only supported on Windows`);
const script = path.join(BIN_PATH, 'install_webkit_wsl.ps1');
const { code } = await spawnAsync('powershell.exe', [
'-ExecutionPolicy', 'Bypass',
'-File', script,
], {
stdio: 'inherit',
});
if (code !== 0)
throw new Error(`Failed to install WebKit via WSL`);
},
});
const ffmpeg = descriptors.find(d => d.name === 'ffmpeg')!;
const ffmpegExecutable = findExecutablePath(ffmpeg.dir, 'ffmpeg');

View File

@ -21,6 +21,8 @@ import { kBrowserCloseMessageId } from './wkConnection';
import { wrapInASCIIBox } from '../utils/ascii';
import { BrowserType, kNoXServerRunningError } from '../browserType';
import { WKBrowser } from '../webkit/wkBrowser';
import { spawnAsync } from '../utils/spawnAsync';
import { registry } from '../registry';
import type { BrowserOptions } from '../browser';
import type { SdkObject } from '../instrumentation';
@ -37,10 +39,11 @@ export class WebKit extends BrowserType {
return WKBrowser.connect(this.attribution.playwright, transport, options);
}
override amendEnvironment(env: NodeJS.ProcessEnv, userDataDir: string, isPersistent: boolean): NodeJS.ProcessEnv {
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,
WEBKIT_EXECUTABLE: options.channel === 'webkit-wsl' ? registry.findExecutable('webkit-wsl')!.wslExecutablePath! : undefined
};
}
@ -57,7 +60,7 @@ export class WebKit extends BrowserType {
transport.send({ method: 'Playwright.close', params: {}, id: kBrowserCloseMessageId });
}
override defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[] {
override async defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): Promise<string[]> {
const { args = [], headless } = options;
const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir'));
if (userDataDirArg)
@ -65,12 +68,21 @@ export class WebKit extends BrowserType {
if (args.find(arg => !arg.startsWith('-')))
throw new Error('Arguments can not specify page to be opened');
const webkitArguments = ['--inspector-pipe'];
if (process.platform === 'win32')
if (options.channel === 'webkit-wsl') {
if (options.executablePath)
throw new Error('Cannot specify executablePath when using the "webkit-wsl" channel.');
webkitArguments.unshift(
path.join(__dirname, 'wsl/webkit-wsl-transport-server.js'),
);
}
if (process.platform === 'win32' && options.channel !== 'webkit-wsl')
webkitArguments.push('--disable-accelerated-compositing');
if (headless)
webkitArguments.push('--headless');
if (isPersistent)
webkitArguments.push(`--user-data-dir=${userDataDir}`);
webkitArguments.push(`--user-data-dir=${options.channel === 'webkit-wsl' ? await translatePathToWSL(userDataDir) : userDataDir}`);
else
webkitArguments.push(`--no-startup-window`);
const proxy = options.proxyOverride || options.proxy;
@ -79,7 +91,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') {
} else if (process.platform === 'linux' || (process.platform === 'win32' && options.channel === 'webkit-wsl')) {
webkitArguments.push(`--proxy=${proxy.server}`);
if (proxy.bypass)
webkitArguments.push(...proxy.bypass.split(',').map(t => `--ignore-host=${t}`));
@ -97,3 +109,8 @@ export class WebKit extends BrowserType {
return webkitArguments;
}
}
export async function translatePathToWSL(path: string): Promise<string> {
const { stdout } = await spawnAsync('wsl.exe', ['-d', 'playwright', '--cd', '/home/pwuser', 'wslpath', path.replace(/\\/g, '\\\\')]);
return stdout.toString().trim();
}

View File

@ -22,6 +22,7 @@ import * as network from '../network';
import { WKConnection, WKSession, kPageProxyMessageReceived } from './wkConnection';
import { WKPage } from './wkPage';
import { TargetClosedError } from '../errors';
import { translatePathToWSL } from './webkit';
import type { BrowserOptions } from '../browser';
import type { SdkObject } from '../instrumentation';
@ -87,7 +88,7 @@ export class WKBrowser extends Browser {
const createOptions = proxy ? {
// Enable socks5 hostname resolution on Windows.
// See https://github.com/microsoft/playwright/issues/20451
proxyServer: process.platform === 'win32' ? proxy.server.replace(/^socks5:\/\//, 'socks5h://') : proxy.server,
proxyServer: process.platform === 'win32' && this.attribution.browser?.options.channel !== 'webkit-wsl' ? proxy.server.replace(/^socks5:\/\//, 'socks5h://') : proxy.server,
proxyBypassList: proxy.bypass
} : undefined;
const { browserContextId } = await this._browserSession.send('Playwright.createContext', createOptions);
@ -227,7 +228,7 @@ export class WKBrowserContext extends BrowserContext {
const promises: Promise<any>[] = [super._initialize()];
promises.push(this._browser._browserSession.send('Playwright.setDownloadBehavior', {
behavior: this._options.acceptDownloads === 'accept' ? 'allow' : 'deny',
downloadPath: this._browser.options.downloadsPath,
downloadPath: this._browser.options.channel === 'webkit-wsl' ? await translatePathToWSL(this._browser.options.downloadsPath) : this._browser.options.downloadsPath,
browserContextId
}));
if (this._options.ignoreHTTPSErrors || this._options.internalIgnoreHTTPSErrors)

View File

@ -39,6 +39,7 @@ import { WKInterceptableRequest, WKRouteImpl } from './wkInterceptableRequest';
import { WKProvisionalPage } from './wkProvisionalPage';
import { WKWorkers } from './wkWorkers';
import { debugLogger } from '../utils/debugLogger';
import { translatePathToWSL } from './webkit';
import type { Protocol } from './protocol';
import type { WKBrowserContext } from './wkBrowser';
@ -842,7 +843,7 @@ export class WKPage implements PageDelegate {
private async _startVideo(options: types.PageScreencastOptions): Promise<void> {
assert(!this._recordingVideoFile);
const { screencastId } = await this._pageProxySession.send('Screencast.startVideo', {
file: options.outputFile,
file: this._browserContext._browser.options.channel === 'webkit-wsl' ? await translatePathToWSL(options.outputFile) : options.outputFile,
width: options.width,
height: options.height,
toolbarHeight: this._toolbarHeight()
@ -976,6 +977,8 @@ export class WKPage implements PageDelegate {
async setInputFilePaths(handle: dom.ElementHandle<HTMLInputElement>, paths: string[]): Promise<void> {
const pageProxyId = this._pageProxySession.sessionId;
const objectId = handle._objectId;
if (this._browserContext._browser?.options.channel === 'webkit-wsl')
paths = await Promise.all(paths.map(path => translatePathToWSL(path)));
await Promise.all([
this._pageProxySession.connection.browserSession.send('Playwright.grantFileReadAccess', { pageProxyId, paths }),
this._session.send('DOM.setInputFiles', { objectId, paths })

View File

@ -0,0 +1,86 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// @ts-check
/* eslint-disable no-restricted-properties */
/* eslint-disable no-console */
// WebKit WSL Transport Client - runs inside WSL/Linux
// See webkit-wsl-transport-server.ts for the complete architecture diagram.
// This client connects to the TCP server and bridges it to WebKit via fd3/fd4 pipes.
import net from 'net';
import fs from 'fs';
import { spawn, spawnSync } from 'child_process';
(async () => {
const { PW_WSL_BRIDGE_PORT: socketPort, ...childEnv } = process.env;
if (!socketPort)
throw new Error('PW_WSL_BRIDGE_PORT env var is not set');
const [executable, ...args] = process.argv.slice(2);
if (!(await fs.promises.stat(executable)).isFile())
throw new Error(`Executable does not exist. Did you update Playwright recently? Make sure to run npx playwright install webkit-wsl`);
const address = (() => {
const res = spawnSync('/usr/bin/wslinfo', ['--networking-mode'], { encoding: 'utf8' });
if (res.error || res.status !== 0)
throw new Error(`Failed to run /usr/bin/wslinfo --networking-mode: ${res.error?.message || res.stderr || res.status}`);
if (res.stdout.trim() === 'nat') {
const ipRes = spawnSync('/usr/sbin/ip', ['route', 'show'], { encoding: 'utf8' });
if (ipRes.error || ipRes.status !== 0)
throw new Error(`Failed to run ip route show: ${ipRes.error?.message || ipRes.stderr || ipRes.status}`);
const ip = ipRes.stdout.trim().split('\n').find(line => line.includes('default'))?.split(' ')[2];
if (!ip)
throw new Error('Could not determine WSL IP address (NAT mode).');
return ip;
}
return '127.0.0.1';
})();
const socket = net.createConnection(parseInt(socketPort, 10), address);
// Disable Nagle's algorithm to reduce latency for small, frequent messages.
socket.setNoDelay(true);
await new Promise((resolve, reject) => {
socket.on('connect', resolve);
socket.on('error', reject);
});
const child = spawn(executable, args, {
stdio: ['inherit', 'inherit', 'inherit', 'pipe', 'pipe'],
env: childEnv,
});
const [childOutput, childInput] = [child.stdio[3] as NodeJS.WritableStream, child.stdio[4] as NodeJS.ReadableStream];
socket.pipe(childOutput);
childInput.pipe(socket);
socket.on('end', () => child.kill());
child.on('exit', exitCode => {
socket.end();
process.exit(exitCode || 0);
});
await new Promise((resolve, reject) => {
child.on('exit', resolve);
child.on('error', reject);
});
})().catch(error => {
console.error('Error occurred:', error);
process.exit(1);
});

View File

@ -0,0 +1,165 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable no-restricted-properties */
/* eslint-disable no-console */
import net from 'net';
import path from 'path';
import { spawn } from 'child_process';
// WebKit WSL Transport Architecture Diagram:
//
// ┌─────────────────┐ fd3/fd4 ┌──────────────────────┐
// │ Playwright │◄──────────────►│ webkit-wsl-transport │
// │ │ (pipes) │ server.ts │
// └─────────────────┘ │ (Windows/Host) │
// └──────────┬───────────┘
// │ spawns
// ▼
// ┌──────────────────────┐
// │ wsl.exe │
// │ -d playwright │
// └──────────┬───────────┘
// │ starts
// ▼
// ┌─────────────────┐ TCP socket ┌──────────────────────┐ fd3/fd4 ┌─────────────┐
// │ TCP Server │◄───────────────►│ webkit-wsl-transport │◄──────────────►│ WebKit │
// │ (port forwarded │ over WSL │ client.ts │ (pipes) │ Browser │
// │ via env var) │ boundary │ (WSL/Linux) │ │ Process │
// └─────────────────┘ └──────────────────────┘ └─────────────┘
//
// The TCP server bridges fd3/fd4 pipes across the WSL boundary because wsl.exe
// only supports forwarding up to 3 file descriptors (stdin/stdout/stderr).
//
// Data flow: Playwright ↔ fd3/fd4 ↔ TCP socket ↔ WSL network ↔ TCP socket ↔ fd3/fd4 ↔ WebKit
//
// Start a TCP server to bridge between parent (fd3/fd4) and the WSL child process.
// This is needed because wsl.exe only supports up to 3 forwarded fds, so we can't
// pass extra pipes directly and must tunnel them over a socket instead.
(async () => {
const argv = process.argv.slice(2);
if (!argv.length) {
console.error(`Usage: node ${path.basename(__filename)} <executable> [args...]`);
process.exit(1);
}
// Use net.Socket instead of fs.createReadStream/WriteStream to avoid hanging at shutdown.
// fs streams use libuv's async fs API which spawns FSReqCallbacks in the threadpool.
// If fs operations are pending (e.g. waiting for EOF), Node's event loop stays referenced
// and the process never exits. net.Socket integrates with libuv's event loop directly,
// making reads/writes non-blocking and allowing clean shutdown via destroy().
const parentIn = new net.Socket({ fd: 3, readable: true, writable: false }); // parent -> us
const parentOut = new net.Socket({ fd: 4, readable: false, writable: true }); // us -> parent
const server = net.createServer();
let socket: net.Socket | null = null;
server.on('connection', s => {
if (socket) {
log('Extra connection received, destroying.');
socket.destroy();
return;
}
socket = s;
// Disable Nagle's algorithm to reduce latency for small, frequent messages.
socket.setNoDelay(true);
log('Client connected, wiring pipes.');
socket.pipe(parentOut);
parentIn.pipe(socket);
socket.on('close', () => {
log('Socket closed');
socket = null;
});
});
await new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(0, () => resolve(null));
});
const address = server.address();
if (!address || typeof address === 'string') {
console.error('Failed to obtain listening address');
process.exit(1);
}
const port = address.port;
log('Server listening on', port);
// Spawn child process with augmented env. PW_WSL_BRIDGE_PORT is added to WSLENV
// so this environment variable is propagated across Windows <-> WSL boundaries.
// This does not forward the TCP port itself, only the env var containing it.
const env = {
...process.env,
// WSLENV is a colon-delimited list of environment variables that should be included when launching WSL processes from Win32 or Win32 processes from WSL
WSLENV: 'PW_WSL_BRIDGE_PORT',
PW_WSL_BRIDGE_PORT: String(port),
};
let shuttingDown = false;
const child = spawn('wsl.exe', [
'-d',
'playwright',
'--cd',
'/home/pwuser',
'/home/pwuser/node/bin/node',
'/home/pwuser/webkit-wsl-transport-client.js',
process.env.WEBKIT_EXECUTABLE || '',
...argv,
], {
env,
stdio: ['inherit', 'inherit', 'inherit'], // no fd3/fd4 here; they stay only in this wrapper
});
log('Spawned child pid', child.pid);
child.on('close', (code, signal) => {
log('Child exit', { code, signal });
// Use actual exit code, or 128, or fallback to 1 for unknown signals
const exitCode = code ?? (signal ? 128 : 0);
shutdown(exitCode);
});
child.on('error', err => {
console.error('Child process failed to start:', err);
shutdown(1);
});
await new Promise(resolve => child.once('close', resolve));
async function shutdown(code = 0) {
if (shuttingDown)
return;
shuttingDown = true;
server.close();
parentIn.destroy();
parentOut.destroy();
socket?.destroy();
await new Promise(resolve => server.once('close', resolve));
process.exit(code);
}
function log(...args: any[]) {
console.error(new Date(), `[${path.basename(__filename)}]`, ...args);
}
})().catch(error => {
console.error('Error occurred:', error);
process.exit(1);
});

View File

@ -73,10 +73,10 @@ const test = baseTest.extend<BrowserTestTestFixtures, BrowserTestWorkerFixtures>
await run(false);
}, { scope: 'worker' }],
defaultSameSiteCookieValue: [async ({ browserName, platform, macVersion }, run) => {
defaultSameSiteCookieValue: [async ({ browserName, platform, channel }, run) => {
if (browserName === 'chromium' || browserName as any === '_bidiChromium' || browserName as any === '_bidiFirefox')
await run('Lax');
else if (browserName === 'webkit' && platform === 'linux')
else if (browserName === 'webkit' && (platform === 'linux' || channel === 'webkit-wsl'))
await run('Lax');
else if (browserName === 'webkit')
await run('None'); // Windows + older macOS

View File

@ -53,13 +53,13 @@ export async function verifyViewport(page: Page, width: number, height: number)
expect(await page.evaluate('window.innerHeight')).toBe(height);
}
export function expectedSSLError(browserName: string, platform: string): RegExp {
export function expectedSSLError(browserName: string, platform: string, channel: string): RegExp {
if (browserName === 'chromium')
return /net::(ERR_CERT_AUTHORITY_INVALID|ERR_CERT_INVALID)/;
if (browserName === 'webkit') {
if (platform === 'darwin')
return /The certificate for this server is invalid/;
else if (platform === 'win32')
else if (platform === 'win32' && channel !== 'webkit-wsl')
return /SSL peer certificate or SSH remote key was not OK/;
else
return /Unacceptable TLS certificate|Operation was cancelled/;

View File

@ -65,7 +65,7 @@ it('should add cookies with empty value', async ({ context, page, server }) => {
expect(await page.evaluate(() => document.cookie)).toEqual('marker=');
});
it('should set cookies with SameSite attribute and no secure attribute', async ({ context, browserName, isWindows, isLinux, defaultSameSiteCookieValue }) => {
it('should set cookies with SameSite attribute and no secure attribute', async ({ context, browserName, isWindows, isLinux, defaultSameSiteCookieValue, channel }) => {
// Use domain instead of URL to ensure that the `secure` attribute is not set.
await context.addCookies([{
domain: 'foo.com',
@ -101,7 +101,7 @@ it('should set cookies with SameSite attribute and no secure attribute', async (
httpOnly: false,
secure: false,
sameSite: defaultSameSiteCookieValue,
}, ...(browserName === 'chromium' || (browserName === 'webkit' && isLinux) ? [] : [{
}, ...(browserName === 'chromium' || (browserName === 'webkit' && (isLinux || channel === 'webkit-wsl')) ? [] : [{
name: 'same-site-none',
value: '1',
domain: 'foo.com',
@ -118,7 +118,7 @@ it('should set cookies with SameSite attribute and no secure attribute', async (
expires: -1,
httpOnly: false,
secure: false,
sameSite: (browserName === 'webkit' && isWindows) ? 'None' : 'Lax',
sameSite: (browserName === 'webkit' && isWindows && channel !== 'webkit-wsl') ? 'None' : 'Lax',
}, {
name: 'same-site-strict',
value: '1',
@ -127,7 +127,7 @@ it('should set cookies with SameSite attribute and no secure attribute', async (
expires: -1,
httpOnly: false,
secure: false,
sameSite: (browserName === 'webkit' && isWindows) ? 'None' : 'Strict',
sameSite: (browserName === 'webkit' && isWindows && channel !== 'webkit-wsl') ? 'None' : 'Strict',
}]));
});
@ -309,7 +309,7 @@ it('should set cookie with reasonable defaults', async ({ context, server, defau
}]);
});
it('should set a cookie with a path', async ({ context, page, server, browserName, isWindows }) => {
it('should set a cookie with a path', async ({ context, page, server, browserName, isWindows, channel }) => {
await page.goto(server.PREFIX + '/grid.html');
await context.addCookies([{
domain: server.HOSTNAME,
@ -326,7 +326,7 @@ it('should set a cookie with a path', async ({ context, page, server, browserNam
expires: -1,
httpOnly: false,
secure: false,
sameSite: (browserName === 'webkit' && isWindows) ? 'None' : 'Lax',
sameSite: (browserName === 'webkit' && isWindows && channel !== 'webkit-wsl') ? 'None' : 'Lax',
}]);
expect(await page.evaluate('document.cookie')).toBe('gridcookie=GRID');
await page.goto(server.EMPTY_PAGE);
@ -384,7 +384,7 @@ it('should be able to set unsecure cookie for HTTP website', async ({ context, p
expect(cookie.secure).toBe(false);
});
it('should set a cookie on a different domain', async ({ context, page, server, browserName, isWindows }) => {
it('should set a cookie on a different domain', async ({ context, page, server, browserName, isWindows, channel }) => {
await page.goto(server.EMPTY_PAGE);
await context.addCookies([{
url: 'https://www.example.com',
@ -401,7 +401,7 @@ it('should set a cookie on a different domain', async ({ context, page, server,
expires: -1,
httpOnly: false,
secure: true,
sameSite: (browserName === 'webkit' && isWindows) ? 'None' : 'Lax',
sameSite: (browserName === 'webkit' && isWindows && channel !== 'webkit-wsl') ? 'None' : 'Lax',
}]);
});

View File

@ -111,7 +111,7 @@ function expectPartitionKey(cookies: Cookie[], name: string, partitionKey: strin
throw new Error(`Cookie ${name} has partitionKey ${cookie.partitionKey} but expected ${partitionKey}.`);
}
async function runNonPartitionedTest(page: Page, httpsServer: TestServer, browserName: string, isMac: boolean, isLinux: boolean, urls: TestUrls) {
async function runNonPartitionedTest(page: Page, httpsServer: TestServer, browserName: string, isMac: boolean, isLinux: boolean, channel: string, urls: TestUrls) {
addCommonCookieHandlers(httpsServer, urls);
httpsServer.setRoute('/set-cookie.html', (req, res) => {
res.setHeader('Set-Cookie', `${req.headers.referer ? 'frame' : 'top-level'}=value; SameSite=None; Path=/; Secure;`);
@ -136,14 +136,14 @@ async function runNonPartitionedTest(page: Page, httpsServer: TestServer, browse
await page.goto(urls.set_origin2_origin1);
await page.goto(urls.read_origin2_origin1);
const expectedThirdParty = browserName === 'webkit' && isMac ?
'Received cookie: undefined' : browserName === 'webkit' && isLinux ?
'Received cookie: undefined' : browserName === 'webkit' && (isLinux || channel === 'webkit-wsl') ?
'Received cookie: top-level=value' :
'Received cookie: frame=value; top-level=value';
await expect(frameBody).toHaveText(expectedThirdParty, { timeout: 1000 });
// Check again the top-level cookie.
await page.goto(urls.read_origin1);
const expectedTopLevel = browserName === 'webkit' && (isMac || isLinux) ?
const expectedTopLevel = browserName === 'webkit' && (isMac || isLinux || channel === 'webkit-wsl') ?
'Received cookie: top-level=value' :
'Received cookie: frame=value; top-level=value';
expect(await page.locator('body').textContent()).toBe(expectedTopLevel);
@ -154,13 +154,13 @@ async function runNonPartitionedTest(page: Page, httpsServer: TestServer, browse
};
}
test(`third party non-partitioned cookies`, async ({ page, browserName, httpsServer, isMac, isLinux, urls }) => {
await runNonPartitionedTest(page, httpsServer, browserName, isMac, isLinux, urls);
test(`third party non-partitioned cookies`, async ({ page, browserName, httpsServer, isMac, isLinux, urls, channel }) => {
await runNonPartitionedTest(page, httpsServer, browserName, isMac, isLinux, channel, urls);
});
test(`save/load third party non-partitioned cookies`, async ({ page, browserName, httpsServer, isMac, isLinux, browser, urls }) => {
test(`save/load third party non-partitioned cookies`, async ({ page, browserName, httpsServer, isMac, isLinux, browser, urls, channel }) => {
// Run the test to populate the cookies.
const { expectedTopLevel, expectedThirdParty } = await runNonPartitionedTest(page, httpsServer, browserName, isMac, isLinux, urls);
const { expectedTopLevel, expectedThirdParty } = await runNonPartitionedTest(page, httpsServer, browserName, isMac, isLinux, channel, urls);
async function checkCookies(page: Page) {
// Check top-level cookie first.

View File

@ -83,8 +83,8 @@ it('should properly report httpOnly cookie', async ({ context, page, server }) =
expect(cookies[0].httpOnly).toBe(true);
});
it('should properly report "Strict" sameSite cookie', async ({ context, page, server, browserName, platform }) => {
it.fail(browserName === 'webkit' && platform === 'win32');
it('should properly report "Strict" sameSite cookie', async ({ context, page, server, browserName, platform, channel }) => {
it.fail(browserName === 'webkit' && platform === 'win32' && channel !== 'webkit-wsl');
server.setRoute('/empty.html', (req, res) => {
res.setHeader('Set-Cookie', 'name=value;SameSite=Strict');
@ -96,8 +96,8 @@ it('should properly report "Strict" sameSite cookie', async ({ context, page, se
expect(cookies[0].sameSite).toBe('Strict');
});
it('should properly report "Lax" sameSite cookie', async ({ context, page, server, browserName, platform }) => {
it.fail(browserName === 'webkit' && platform === 'win32');
it('should properly report "Lax" sameSite cookie', async ({ context, page, server, browserName, platform, channel }) => {
it.fail(browserName === 'webkit' && platform === 'win32' && channel !== 'webkit-wsl');
server.setRoute('/empty.html', (req, res) => {
res.setHeader('Set-Cookie', 'name=value;SameSite=Lax');
@ -142,7 +142,7 @@ it('should get multiple cookies', async ({ context, page, server, defaultSameSit
]));
});
it('should get cookies from multiple urls', async ({ context, browserName, isWindows }) => {
it('should get cookies from multiple urls', async ({ context, browserName, isWindows, channel }) => {
await context.addCookies([{
url: 'https://foo.com',
name: 'doggo',
@ -168,7 +168,7 @@ it('should get cookies from multiple urls', async ({ context, browserName, isWin
expires: -1,
httpOnly: false,
secure: true,
sameSite: (browserName === 'webkit' && isWindows) ? 'None' : 'Lax',
sameSite: (browserName === 'webkit' && isWindows && channel !== 'webkit-wsl') ? 'None' : 'Lax',
}, {
name: 'doggo',
value: 'woofs',
@ -181,7 +181,7 @@ it('should get cookies from multiple urls', async ({ context, browserName, isWin
}]));
});
it('should work with subdomain cookie', async ({ context, browserName, isWindows }) => {
it('should work with subdomain cookie', async ({ context, browserName, isWindows, channel }) => {
await context.addCookies([{
domain: '.foo.com',
path: '/',
@ -198,7 +198,7 @@ it('should work with subdomain cookie', async ({ context, browserName, isWindows
expires: -1,
httpOnly: false,
secure: true,
sameSite: (browserName === 'webkit' && isWindows) ? 'None' : 'Lax',
sameSite: (browserName === 'webkit' && isWindows && channel !== 'webkit-wsl') ? 'None' : 'Lax',
}]);
expect(await context.cookies('https://sub.foo.com')).toEqual([{
name: 'doggo',
@ -208,7 +208,7 @@ it('should work with subdomain cookie', async ({ context, browserName, isWindows
expires: -1,
httpOnly: false,
secure: true,
sameSite: (browserName === 'webkit' && isWindows) ? 'None' : 'Lax',
sameSite: (browserName === 'webkit' && isWindows && channel !== 'webkit-wsl') ? 'None' : 'Lax',
}]);
});
@ -227,7 +227,7 @@ it('should return cookies with empty value', async ({ context, page, server }) =
]);
});
it('should return secure cookies based on HTTP(S) protocol', async ({ context, browserName, isWindows }) => {
it('should return secure cookies based on HTTP(S) protocol', async ({ context, browserName, isWindows, channel }) => {
await context.addCookies([{
url: 'https://foo.com',
name: 'doggo',
@ -250,7 +250,7 @@ it('should return secure cookies based on HTTP(S) protocol', async ({ context, b
expires: -1,
httpOnly: false,
secure: false,
sameSite: (browserName === 'webkit' && isWindows) ? 'None' : 'Lax',
sameSite: (browserName === 'webkit' && isWindows && channel !== 'webkit-wsl') ? 'None' : 'Lax',
}, {
name: 'doggo',
value: 'woofs',
@ -259,7 +259,7 @@ it('should return secure cookies based on HTTP(S) protocol', async ({ context, b
expires: -1,
httpOnly: false,
secure: true,
sameSite: (browserName === 'webkit' && isWindows) ? 'None' : 'Lax',
sameSite: (browserName === 'webkit' && isWindows && channel !== 'webkit-wsl') ? 'None' : 'Lax',
}]));
expect(await context.cookies('http://foo.com/')).toEqual([{
name: 'catto',
@ -269,7 +269,7 @@ it('should return secure cookies based on HTTP(S) protocol', async ({ context, b
expires: -1,
httpOnly: false,
secure: false,
sameSite: (browserName === 'webkit' && isWindows) ? 'None' : 'Lax',
sameSite: (browserName === 'webkit' && isWindows && channel !== 'webkit-wsl') ? 'None' : 'Lax',
}]);
});
@ -346,7 +346,7 @@ it('should support requestStorageAccess', async ({ page, server, channel, browse
expect(serverRequest.headers.cookie).toBe('name=value');
}
} else {
if (isLinux && browserName === 'webkit')
if (browserName === 'webkit' && (isLinux || channel === 'webkit-wsl'))
expect(await frame.evaluate(() => document.hasStorageAccess())).toBeTruthy();
else
expect(await frame.evaluate(() => document.hasStorageAccess())).toBeFalsy();
@ -355,7 +355,7 @@ it('should support requestStorageAccess', async ({ page, server, channel, browse
server.waitForRequest('/title.html'),
frame.evaluate(() => fetch('/title.html'))
]);
if (isWindows && browserName === 'webkit')
if (isWindows && browserName === 'webkit' && channel !== 'webkit-wsl')
expect(serverRequest.headers.cookie).toBe('name=value');
else
expect(serverRequest.headers.cookie).toBeFalsy();
@ -367,7 +367,7 @@ it('should support requestStorageAccess', async ({ page, server, channel, browse
server.waitForRequest('/title.html'),
frame.evaluate(() => fetch('/title.html'))
]);
if (isLinux && browserName === 'webkit')
if (browserName === 'webkit' && (isLinux || channel === 'webkit-wsl'))
expect(serverRequest.headers.cookie).toBe(undefined);
else
expect(serverRequest.headers.cookie).toBe('name=value');

View File

@ -400,7 +400,7 @@ it('should remove cookie with expires far in the past', async ({ page, server })
expect(serverRequest.headers.cookie).toBeFalsy();
});
it('should handle cookies on redirects', async ({ context, server, browserName, isWindows }) => {
it('should handle cookies on redirects', async ({ context, server, browserName, isWindows, channel }) => {
server.setRoute('/redirect1', (req, res) => {
res.setHeader('Set-Cookie', 'r1=v1;SameSite=Lax');
res.writeHead(301, { location: '/a/b/redirect2' });
@ -436,7 +436,7 @@ it('should handle cookies on redirects', async ({ context, server, browserName,
const cookies = await context.cookies();
expect(new Set(cookies)).toEqual(new Set([
{
'sameSite': (browserName === 'webkit' && isWindows) ? 'None' : 'Lax',
'sameSite': (browserName === 'webkit' && isWindows && channel !== 'webkit-wsl') ? 'None' : 'Lax',
'name': 'r2',
'value': 'v2',
'domain': server.HOSTNAME,
@ -446,7 +446,7 @@ it('should handle cookies on redirects', async ({ context, server, browserName,
'secure': false
},
{
'sameSite': (browserName === 'webkit' && isWindows) ? 'None' : 'Lax',
'sameSite': (browserName === 'webkit' && isWindows && channel !== 'webkit-wsl') ? 'None' : 'Lax',
'name': 'r1',
'value': 'v1',
'domain': server.HOSTNAME,
@ -1201,7 +1201,8 @@ it('context request should export same storage state as context', async ({ conte
expect(pageState).toEqual(contextState);
});
it('should send secure cookie over http for localhost', async ({ page, server }) => {
it('should send secure cookie over http for localhost', async ({ page, server, channel }) => {
it.skip(channel === 'webkit-wsl');
server.setRoute('/setcookie.html', (req, res) => {
res.setHeader('Set-Cookie', ['a=v; secure']);
res.end();
@ -1277,7 +1278,7 @@ it('should work with connectOverCDP', async ({ browserName, browserType, server
}
});
it('should support SameSite cookie attribute over https', async ({ contextFactory, httpsServer, browserName, isWindows }) => {
it('should support SameSite cookie attribute over https', async ({ contextFactory, httpsServer, browserName, isWindows, channel }) => {
// Cookies with SameSite=None must also specify the Secure attribute. WebKit navigation
// to HTTP url will fail if the response contains a cookie with Secure attribute, so
// we do HTTPS navigation.
@ -1291,7 +1292,7 @@ it('should support SameSite cookie attribute over https', async ({ contextFactor
});
await page.request.get(httpsServer.EMPTY_PAGE);
const [cookie] = await page.context().cookies();
if (browserName === 'webkit' && isWindows)
if (browserName === 'webkit' && isWindows && channel !== 'webkit-wsl')
expect(cookie.sameSite).toBe('None');
else
expect(cookie.sameSite).toBe(value);
@ -1301,7 +1302,7 @@ it('should support SameSite cookie attribute over https', async ({ contextFactor
it('should set domain=localhost cookie', async ({ context, server, browserName, isWindows }) => {
server.setRoute('/empty.html', (req, res) => {
res.setHeader('Set-Cookie', `name=val; Domain=localhost; Path=/;`);
res.setHeader('Set-Cookie', `name=val; Domain=${server.HOSTNAME}; Path=/;`);
res.end();
});
await context.request.get(server.EMPTY_PAGE);
@ -1322,7 +1323,7 @@ it('fetch should not throw on long set-cookie value', async ({ context, server }
expect(cookies.map(c => c.name)).toContain('bar');
});
it('should support set-cookie with SameSite and without Secure attribute over HTTP', async ({ page, server, browserName, isWindows, isLinux }) => {
it('should support set-cookie with SameSite and without Secure attribute over HTTP', async ({ page, server, browserName, isWindows, isLinux, channel }) => {
for (const value of ['None', 'Lax', 'Strict']) {
await it.step(`SameSite=${value}`, async () => {
server.setRoute('/empty.html', (req, res) => {
@ -1333,9 +1334,9 @@ it('should support set-cookie with SameSite and without Secure attribute over HT
const [cookie] = await page.context().cookies();
if (browserName === 'chromium' && value === 'None')
expect(cookie).toBeFalsy();
else if (browserName === 'webkit' && isLinux && value === 'None')
else if (browserName === 'webkit' && (isLinux || channel === 'webkit-wsl') && value === 'None')
expect(cookie).toBeFalsy();
else if (browserName === 'webkit' && isWindows)
else if (browserName === 'webkit' && isWindows && channel !== 'webkit-wsl')
expect(cookie.sameSite).toBe('None');
else
expect(cookie.sameSite).toBe(value);

View File

@ -64,7 +64,7 @@ it('should use proxy', async ({ contextFactory, server, proxyServer }) => {
await context.close();
});
it('should send secure cookies to subdomain.localhost', async ({ contextFactory, browserName, server, isWindows, proxyServer }) => {
it('should send secure cookies to subdomain.localhost', async ({ contextFactory, browserName, server, isWindows, proxyServer, channel }) => {
proxyServer.forwardTo(server.PORT);
const context = await contextFactory({
proxy: { server: proxyServer.HOST },
@ -88,7 +88,7 @@ it('should send secure cookies to subdomain.localhost', async ({ contextFactory,
name: 'non-secure',
domain: 'subdomain.localhost',
},
...((browserName === 'webkit') && !isWindows ? [] : [{
...((browserName === 'webkit' && (!isWindows || channel === 'webkit-wsl')) ? [] : [{
name: 'secure',
domain: 'subdomain.localhost',
}]),
@ -102,8 +102,8 @@ it('should send secure cookies to subdomain.localhost', async ({ contextFactory,
it('should set cookie for top-level domain', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/18362' }
}, async ({ contextFactory, server, proxyServer, browserName, isLinux }) => {
it.fixme(browserName === 'webkit' && isLinux);
}, async ({ contextFactory, server, proxyServer, browserName, isLinux, channel }) => {
it.fixme(browserName === 'webkit' && (isLinux || channel === 'webkit-wsl'));
proxyServer.forwardTo(server.PORT, { allowConnectRequests: true });
const context = await contextFactory({
@ -172,8 +172,9 @@ it.describe('should proxy local network requests', () => {
});
it('should use ipv6 proxy', async ({ contextFactory, server, proxyServer, browserName }) => {
it('should use ipv6 proxy', async ({ contextFactory, server, proxyServer, browserName, channel }) => {
it.fail(browserName === 'firefox', 'page.goto: NS_ERROR_UNKNOWN_HOST');
it.fail(channel === 'webkit-wsl', 'WebKit on WSL does not support IPv6: https://github.com/microsoft/WSL/issues/10803');
proxyServer.forwardTo(server.PORT);
const context = await contextFactory({
proxy: { server: `[0:0:0:0:0:0:0:1]:${proxyServer.PORT}` }

View File

@ -159,8 +159,9 @@ for (const kind of ['launchServer', 'run-server'] as const) {
}
});
test('should be able to visit ipv6', async ({ connect, startRemoteServer, ipV6ServerPort }) => {
test('should be able to visit ipv6', async ({ connect, startRemoteServer, ipV6ServerPort, channel }) => {
test.fail(!!process.env.INSIDE_DOCKER, 'docker does not support IPv6 by default');
test.fail(channel === 'webkit-wsl', 'WebKit on WSL does not support IPv6: https://github.com/microsoft/WSL/issues/10803');
const remoteServer = await startRemoteServer(kind);
const browser = await connect(remoteServer.wsEndpoint());
const page = await browser.newPage();
@ -185,8 +186,9 @@ for (const kind of ['launchServer', 'run-server'] as const) {
(browserType as any)._playwright._defaultLaunchOptions.headless = headless;
});
test('should be able to visit ipv6 through localhost', async ({ connect, startRemoteServer, ipV6ServerPort }) => {
test('should be able to visit ipv6 through localhost', async ({ connect, startRemoteServer, ipV6ServerPort, channel }) => {
test.fail(!!process.env.INSIDE_DOCKER, 'docker does not support IPv6 by default');
test.skip(channel === 'webkit-wsl', 'WebKit on WSL does not support IPv6: https://github.com/microsoft/WSL/issues/10803');
const remoteServer = await startRemoteServer(kind);
const browser = await connect(remoteServer.wsEndpoint());
const page = await browser.newPage();

View File

@ -61,12 +61,15 @@ it('should throw if page argument is passed', async ({ browserType, browserName
expect(waitError!.message).toContain('can not specify page');
});
it('should reject if launched browser fails immediately', async ({ mode, browserType, asset, isWindows }) => {
it('should reject if launched browser fails immediately', async ({ mode, browserType, asset, isWindows, channel }) => {
it.skip(mode.startsWith('service'));
let waitError: Error | undefined;
await browserType.launch({ executablePath: asset('dummy_bad_browser_executable.js') }).catch(e => waitError = e);
expect(waitError!.message).toContain(isWindows ? 'browserType.launch: spawn UNKNOWN' : 'Browser logs:');
if (channel === 'webkit-wsl')
expect(waitError!.message).toContain('Cannot specify executablePath when using the \"webkit-wsl\" channel.');
else
expect(waitError!.message).toContain(isWindows ? 'browserType.launch: spawn UNKNOWN' : 'Browser logs:');
});
it('should reject if executable path is invalid', async ({ browserType, mode }) => {

View File

@ -63,7 +63,7 @@ it('context.addCookies() should work', async ({ server, launchPersistent, browse
expires: -1,
httpOnly: false,
secure: false,
sameSite: (browserName === 'webkit' && isWindows) ? 'None' : 'Lax',
sameSite: (browserName === 'webkit' && isWindows && channel !== 'webkit-wsl') ? 'None' : 'Lax',
}]);
});

View File

@ -157,7 +157,7 @@ it('should have passed URL when launching with ignoreDefaultArgs: true', async (
it.skip(mode !== 'default');
const userDataDir = await createUserDataDir();
const args = toImpl(browserType).defaultArgs((browserType as any)._playwright._defaultLaunchOptions, 'persistent', userDataDir, 0).filter(a => a !== 'about:blank');
const args = (await toImpl(browserType).defaultArgs((browserType as any)._playwright._defaultLaunchOptions, 'persistent', userDataDir, 0)).filter(a => a !== 'about:blank');
const options = {
args: browserName === 'firefox' ? [...args, '-new-tab', server.EMPTY_PAGE] : [...args, server.EMPTY_PAGE],
ignoreDefaultArgs: true,

View File

@ -420,8 +420,8 @@ it.describe('download event', () => {
]).toContain(saveError.message);
});
it('should close the context without awaiting the download', async ({ browser, server, browserName, platform }, testInfo) => {
it.skip(browserName === 'webkit' && platform === 'linux', 'WebKit on linux does not convert to the download immediately upon receiving headers');
it('should close the context without awaiting the download', async ({ browser, server, browserName, platform, channel }, testInfo) => {
it.skip(browserName === 'webkit' && (platform === 'linux' || channel === 'webkit-wsl'), 'WebKit on linux does not convert to the download immediately upon receiving headers');
server.setRoute('/downloadStall', (req, res) => {
res.setHeader('Content-Type', 'application/octet-stream');
@ -455,8 +455,8 @@ it.describe('download event', () => {
]).toContain(saveError.message);
});
it('should throw if browser dies', async ({ server, browserType, browserName, platform }, testInfo) => {
it.skip(browserName === 'webkit' && platform === 'linux', 'WebKit on linux does not convert to the download immediately upon receiving headers');
it('should throw if browser dies', async ({ server, browserType, browserName, platform, channel }, testInfo) => {
it.skip(browserName === 'webkit' && (platform === 'linux' || channel === 'webkit-wsl'), 'WebKit on linux does not convert to the download immediately upon receiving headers');
server.setRoute('/downloadStall', (req, res) => {
res.setHeader('Content-Type', 'application/octet-stream');
res.setHeader('Content-Disposition', 'attachment; filename=file.txt');

View File

@ -660,8 +660,8 @@ it('should return server address directly from response', async ({ page, server,
}
});
it('should return security details directly from response', async ({ contextFactory, httpsServer, browserName, platform }) => {
it.fail(browserName === 'webkit' && platform === 'linux', 'https://github.com/microsoft/playwright/issues/6759');
it('should return security details directly from response', async ({ contextFactory, httpsServer, browserName, platform, channel }) => {
it.fail(browserName === 'webkit' && (platform === 'linux' || channel === 'webkit-wsl'), 'https://github.com/microsoft/playwright/issues/6759');
const context = await contextFactory({ ignoreHTTPSErrors: true });
const page = await context.newPage();

View File

@ -46,7 +46,7 @@ function normalizeCode(code: string): string {
return code.replace(/\s+/g, ' ').trim();
}
test('should click', async ({ context, browserName, platform }) => {
test('should click', async ({ context, browserName, platform, channel }) => {
const log = await startRecording(context);
const page = await context.newPage();
await page.setContent(`<button onclick="console.log('click')">Submit</button>`);
@ -60,7 +60,7 @@ test('should click', async ({ context, browserName, platform }) => {
selector: 'internal:role=button[name="Submit"i]',
ref: 'e2',
// Safari does not focus after a click: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button#clicking_and_focus
ariaSnapshot: (browserName === 'webkit' && (platform === 'darwin' || platform === 'win32')) ? '- button "Submit" [ref=e2]' : '- button "Submit" [active] [ref=e2]',
ariaSnapshot: (browserName === 'webkit' && (platform === 'darwin' || (platform === 'win32' && channel !== 'webkit-wsl'))) ? '- button "Submit" [ref=e2]' : '- button "Submit" [active] [ref=e2]',
}),
startTime: expect.any(Number),
})
@ -69,7 +69,7 @@ test('should click', async ({ context, browserName, platform }) => {
expect(normalizeCode(clickActions[0].code)).toEqual(`await page.getByRole('button', { name: 'Submit' }).click();`);
});
test('should double click', async ({ context, browserName, platform }) => {
test('should double click', async ({ context, browserName, platform, channel }) => {
const log = await startRecording(context);
const page = await context.newPage();
await page.setContent(`<button onclick="console.log('click')" ondblclick="console.log('dblclick')">Submit</button>`);
@ -84,7 +84,7 @@ test('should double click', async ({ context, browserName, platform }) => {
selector: 'internal:role=button[name="Submit"i]',
ref: 'e2',
// Safari does not focus after a click: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button#clicking_and_focus
ariaSnapshot: (browserName === 'webkit' && (platform === 'darwin' || platform === 'win32')) ? '- button "Submit" [ref=e2]' : '- button "Submit" [active] [ref=e2]',
ariaSnapshot: (browserName === 'webkit' && (platform === 'darwin' || (platform === 'win32' && channel !== 'webkit-wsl'))) ? '- button "Submit" [ref=e2]' : '- button "Submit" [active] [ref=e2]',
}),
startTime: expect.any(Number),
})
@ -93,7 +93,7 @@ test('should double click', async ({ context, browserName, platform }) => {
expect(normalizeCode(clickActions[0].code)).toEqual(`await page.getByRole('button', { name: 'Submit' }).dblclick();`);
});
test('should right click', async ({ context, browserName, platform }) => {
test('should right click', async ({ context, browserName, platform, channel }) => {
const log = await startRecording(context);
const page = await context.newPage();
await page.setContent(`<button oncontextmenu="console.log('contextmenu')">Submit</button>`);
@ -108,7 +108,7 @@ test('should right click', async ({ context, browserName, platform }) => {
selector: 'internal:role=button[name="Submit"i]',
ref: 'e2',
// Safari does not focus after a click: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button#clicking_and_focus
ariaSnapshot: (browserName === 'webkit' && (platform === 'darwin' || platform === 'win32')) ? '- button "Submit" [ref=e2]' : '- button "Submit" [active] [ref=e2]',
ariaSnapshot: (browserName === 'webkit' && (platform === 'darwin' || (platform === 'win32' && channel !== 'webkit-wsl'))) ? '- button "Submit" [ref=e2]' : '- button "Submit" [active] [ref=e2]',
}),
startTime: expect.any(Number),
})

View File

@ -33,7 +33,7 @@ async function checkFeatures(name: string, context: BrowserContext, server: Test
}
}
it('Safari Desktop', async ({ browser, browserName, platform, httpsServer, headless }) => {
it('Safari Desktop', async ({ browser, browserName, platform, httpsServer, headless, channel }) => {
it.skip(browserName !== 'webkit');
it.skip(browserName === 'webkit' && platform === 'darwin' && os.arch() === 'x64', 'Modernizr uses WebGL which is not available on Intel macOS - https://bugs.webkit.org/show_bug.cgi?id=278277');
it.skip(browserName === 'webkit' && hostPlatform.startsWith('ubuntu20.04'), 'Ubuntu 20.04 is frozen');
@ -56,7 +56,7 @@ it('Safari Desktop', async ({ browser, browserName, platform, httpsServer, headl
expected.video = !!expected.video;
actual.video = !!actual.video;
if (platform === 'linux') {
if (platform === 'linux' || channel === 'webkit-wsl') {
expected.speechrecognition = false;
expected.mediastream = false;
if (headless)
@ -67,7 +67,7 @@ it('Safari Desktop', async ({ browser, browserName, platform, httpsServer, headl
delete expected.variablefonts;
}
if (platform === 'win32') {
if (platform === 'win32' && channel !== 'webkit-wsl') {
expected.getusermedia = false;
expected.peerconnection = false;
expected.speechrecognition = false;
@ -91,7 +91,7 @@ it('Safari Desktop', async ({ browser, browserName, platform, httpsServer, headl
expect(actual).toEqual(expected);
});
it('Mobile Safari', async ({ playwright, browser, browserName, platform, httpsServer, headless }) => {
it('Mobile Safari', async ({ playwright, browser, browserName, platform, httpsServer, headless, channel }) => {
it.skip(browserName !== 'webkit');
it.skip(browserName === 'webkit' && platform === 'darwin' && os.arch() === 'x64', 'Modernizr uses WebGL which is not available on Intel macOS - https://bugs.webkit.org/show_bug.cgi?id=278277');
it.skip(browserName === 'webkit' && hostPlatform.startsWith('ubuntu20.04'), 'Ubuntu 20.04 is frozen');
@ -120,7 +120,7 @@ it('Mobile Safari', async ({ playwright, browser, browserName, platform, httpsSe
actual.video = !!actual.video;
}
if (platform === 'linux') {
if (platform === 'linux' || channel === 'webkit-wsl') {
expected.speechrecognition = false;
expected.mediastream = false;
if (headless)
@ -131,7 +131,7 @@ it('Mobile Safari', async ({ playwright, browser, browserName, platform, httpsSe
delete expected.variablefonts;
}
if (platform === 'win32') {
if (platform === 'win32' && channel !== 'webkit-wsl') {
expected.getusermedia = false;
expected.peerconnection = false;
expected.speechrecognition = false;

View File

@ -32,8 +32,9 @@ const test = testBase.extend<{ crash: () => void }, { dummy: string }>({
dummy: ['', { scope: 'worker' }],
});
test.beforeEach(({ platform, browserName }) => {
test.slow(platform === 'linux' && browserName === 'webkit', 'WebKit/Linux tests are consistently slower on some Linux environments. Most likely WebContent process is not getting terminated properly and is causing the slowdown.');
test.beforeEach(({ platform, browserName, channel }) => {
test.slow(platform === 'linux' && (browserName === 'webkit'), 'WebKit/Linux tests are consistently slower on some Linux environments. Most likely WebContent process is not getting terminated properly and is causing the slowdown.');
test.fixme(channel === 'webkit-wsl', 'WebKit on WSL is even slower than above ^^ - skipping for now');
});
test('should emit crash event when page crashes', async ({ page, crash }) => {

View File

@ -323,7 +323,7 @@ it('should use SOCKS proxy for websocket requests', async ({ browserType, server
await closeProxyServer();
});
it('should use http proxy for websocket requests', async ({ browserName, browserType, server, proxyServer, isWindows, isMac, macVersion }) => {
it('should use http proxy for websocket requests', async ({ browserName, browserType, server, proxyServer, isWindows, isMac, macVersion, channel }) => {
it.skip(isMac && macVersion === 13, 'Times out on Mac 13');
proxyServer.forwardTo(server.PORT, { allowConnectRequests: true });
@ -352,7 +352,7 @@ it('should use http proxy for websocket requests', async ({ browserName, browser
// WebKit does not use CONNECT for websockets, but other browsers do.
if (browserName === 'webkit')
expect(proxyServer.wsUrls).toContain(isWindows ? '/ws' : 'ws://fake-localhost-127-0-0-1.nip.io:1337/ws');
expect(proxyServer.wsUrls).toContain((isWindows && !channel) ? '/ws' : 'ws://fake-localhost-127-0-0-1.nip.io:1337/ws');
else
expect(proxyServer.connectHosts).toContain('fake-localhost-127-0-0-1.nip.io:1337');

View File

@ -1463,7 +1463,8 @@ test('should remove noscript when javaScriptEnabled is set to true', async ({ br
await expect(frame.getByText('Enable JavaScript to run this app.')).toBeHidden();
});
test('should open snapshot in new browser context', async ({ browser, page, runAndTrace, server }) => {
test('should open snapshot in new browser context', async ({ browser, page, runAndTrace, server, channel }) => {
test.skip(channel === 'webkit-wsl', 'Trace Viewer opens via ipv6 address which is not supported in WSL');
const traceViewer = await runAndTrace(async () => {
await page.goto(server.EMPTY_PAGE);
await page.setContent('hello');

View File

@ -42,7 +42,7 @@ it('Page.Events.Response @smoke', async ({ page, server }) => {
expect(responses[0].request()).toBeTruthy();
});
it('Page.Events.RequestFailed @smoke', async ({ page, server, browserName, platform }) => {
it('Page.Events.RequestFailed @smoke', async ({ page, server, browserName, platform, channel }) => {
server.setRoute('/one-style.css', (req, res) => {
res.setHeader('Content-Type', 'text/css');
res.connection.destroy();
@ -57,7 +57,7 @@ it('Page.Events.RequestFailed @smoke', async ({ page, server, browserName, platf
if (browserName === 'chromium' || browserName === '_bidiChromium') {
expect(failedRequests[0].failure().errorText).toBe('net::ERR_EMPTY_RESPONSE');
} else if (browserName === 'webkit') {
if (platform === 'linux')
if (platform === 'linux' || channel === 'webkit-wsl')
expect(failedRequests[0].failure().errorText).toMatch(/(Message Corrupt)|(Connection terminated unexpectedly)/i);
else if (platform === 'darwin')
expect(failedRequests[0].failure().errorText).toBe('The network connection was lost.');

View File

@ -24,9 +24,10 @@ it('should work @smoke', async ({ page, server }) => {
expect(page.url()).toBe(server.EMPTY_PAGE);
});
it('should work with file URL', async ({ page, asset, isAndroid, mode }) => {
it('should work with file URL', async ({ page, asset, isAndroid, mode, channel }) => {
it.skip(isAndroid, 'No files on Android');
it.skip(mode.startsWith('service'));
it.skip(channel === 'webkit-wsl', 'separate filesystem on wsl');
const fileurl = url.pathToFileURL(asset('empty.html')).href;
await page.goto(fileurl);
@ -34,9 +35,10 @@ it('should work with file URL', async ({ page, asset, isAndroid, mode }) => {
expect(page.frames().length).toBe(1);
});
it('should work with file URL with subframes', async ({ page, asset, isAndroid, mode }) => {
it('should work with file URL with subframes', async ({ page, asset, isAndroid, mode, channel }) => {
it.skip(isAndroid, 'No files on Android');
it.skip(mode.startsWith('service'));
it.skip(channel === 'webkit-wsl', 'separate filesystem on wsl');
const fileurl = url.pathToFileURL(asset('frames/two-frames.html')).href;
await page.goto(fileurl);
@ -301,7 +303,7 @@ it('should fail when navigating to bad url', async ({ mode, page, browserName })
expect(error.message).toContain('Invalid url');
});
it('should fail when navigating to bad SSL', async ({ page, browserName, httpsServer, platform }) => {
it('should fail when navigating to bad SSL', async ({ page, browserName, httpsServer, platform, channel }) => {
// Make sure that network events do not emit 'undefined'.
// @see https://crbug.com/750469
page.on('request', request => expect(request).toBeTruthy());
@ -309,15 +311,15 @@ it('should fail when navigating to bad SSL', async ({ page, browserName, httpsSe
page.on('requestfailed', request => expect(request).toBeTruthy());
let error = null;
await page.goto(httpsServer.EMPTY_PAGE).catch(e => error = e);
expect(error.message).toMatch(expectedSSLError(browserName, platform));
expect(error.message).toMatch(expectedSSLError(browserName, platform, channel));
});
it('should fail when navigating to bad SSL after redirects', async ({ page, browserName, server, httpsServer, platform }) => {
it('should fail when navigating to bad SSL after redirects', async ({ page, browserName, server, httpsServer, platform, channel }) => {
server.setRedirect('/redirect/1.html', '/redirect/2.html');
server.setRedirect('/redirect/2.html', '/empty.html');
let error = null;
await page.goto(httpsServer.PREFIX + '/redirect/1.html').catch(e => error = e);
expect(error.message).toMatch(expectedSSLError(browserName, platform));
expect(error.message).toMatch(expectedSSLError(browserName, platform, channel));
});
it('should not crash when navigating to bad SSL after a cross origin navigation', async ({ page, server, httpsServer }) => {
@ -337,7 +339,7 @@ it('should throw if networkidle2 is passed as an option', async ({ page, server
expect(error.message).toContain(`waitUntil: expected one of (load|domcontentloaded|networkidle|commit)`);
});
it('should fail when main resources failed to load', async ({ page, browserName, isWindows, mode }) => {
it('should fail when main resources failed to load', async ({ page, browserName, isWindows, mode, channel }) => {
let error = null;
await page.goto('http://localhost:44123/non-existing-url').catch(e => error = e);
if (browserName === 'chromium') {
@ -347,7 +349,7 @@ it('should fail when main resources failed to load', async ({ page, browserName,
expect(error.message).toContain('net::ERR_CONNECTION_REFUSED');
} else if (browserName === 'webkit' && isWindows && mode === 'service2') {
expect(error.message).toContain(`proxy handshake error`);
} else if (browserName === 'webkit' && isWindows) {
} else if (browserName === 'webkit' && isWindows && channel !== 'webkit-wsl') {
expect(error.message).toContain(`Could not connect to server`);
} else if (browserName === 'webkit') {
if (mode === 'service2')
@ -730,9 +732,9 @@ it('should work with lazy loading iframes', async ({ page, server, isAndroid })
expect(page.frames().length).toBe(2);
});
it('should report raw buffer for main resource', async ({ page, server, browserName, platform }) => {
it('should report raw buffer for main resource', async ({ page, server, browserName, platform, channel }) => {
it.fail(browserName === 'chromium', 'Chromium sends main resource as text');
it.fail(browserName === 'webkit' && platform === 'win32', 'Same here');
it.fail(browserName === 'webkit' && platform === 'win32' && channel !== 'webkit-wsl', 'Same here');
server.setRoute('/empty.html', (req, res) => {
res.statusCode = 200;

View File

@ -52,9 +52,10 @@ it('page.goBack should work with HistoryAPI', async ({ page, server }) => {
expect(page.url()).toBe(server.PREFIX + '/first.html');
});
it('page.goBack should work for file urls', async ({ page, server, asset, browserName, platform, isAndroid, mode }) => {
it('page.goBack should work for file urls', async ({ page, server, asset, channel, isAndroid, mode }) => {
it.skip(isAndroid, 'No files on Android');
it.skip(mode.startsWith('service'));
it.skip(channel === 'webkit-wsl');
const url1 = url.pathToFileURL(asset('consolelog.html')).href;
const url2 = server.PREFIX + '/consolelog.html';

View File

@ -496,8 +496,8 @@ it('should support simple cut-pasting', async ({ page }) => {
expect(await page.evaluate(() => document.querySelector('div').textContent)).toBe('123123');
});
it('should support undo-redo', async ({ page, browserName, isLinux }) => {
it.fixme(browserName === 'webkit' && isLinux, 'https://github.com/microsoft/playwright/issues/12000');
it('should support undo-redo', async ({ page, browserName, isLinux, channel }) => {
it.fixme(browserName === 'webkit' && isLinux || channel === 'webkit-wsl', 'https://github.com/microsoft/playwright/issues/12000');
await page.setContent(`<div contenteditable></div>`);
const div = page.locator('div');
await expect(div).toHaveText('');

View File

@ -88,9 +88,9 @@ it('should return headers', async ({ page, server, browserName }) => {
expect(response.request().headers()['user-agent']).toContain('WebKit');
});
it('should get the same headers as the server', async ({ page, server, browserName, platform, isElectron, browserMajorVersion }) => {
it('should get the same headers as the server', async ({ page, server, browserName, platform, isElectron, browserMajorVersion, channel }) => {
it.skip(isElectron && browserMajorVersion < 99, 'This needs Chromium >= 99');
it.fail(browserName === 'webkit' && platform === 'win32', 'Curl does not show accept-encoding and accept-language');
it.fail(browserName === 'webkit' && platform === 'win32' && channel !== 'webkit-wsl', 'Curl does not show accept-encoding and accept-language');
let serverRequest;
server.setRoute('/empty.html', (request, response) => {
serverRequest = request;
@ -101,9 +101,9 @@ it('should get the same headers as the server', async ({ page, server, browserNa
expect(headers).toEqual(adjustServerHeaders(serverRequest.headers, browserName));
});
it('should not return allHeaders() until they are available', async ({ page, server, browserName, platform, isElectron, browserMajorVersion }) => {
it('should not return allHeaders() until they are available', async ({ page, server, browserName, platform, isElectron, browserMajorVersion, channel }) => {
it.skip(isElectron && browserMajorVersion < 99, 'This needs Chromium >= 99');
it.fail(browserName === 'webkit' && platform === 'win32', 'Curl does not show accept-encoding and accept-language');
it.fail(browserName === 'webkit' && platform === 'win32' && channel !== 'webkit-wsl', 'Curl does not show accept-encoding and accept-language');
let requestHeadersPromise;
page.on('request', request => requestHeadersPromise = request.allHeaders());
@ -126,9 +126,9 @@ it('should not return allHeaders() until they are available', async ({ page, ser
expect(responseHeaders['foo']).toBe('bar');
});
it('should get the same headers as the server CORS', async ({ page, server, browserName, platform, isElectron, browserMajorVersion, }) => {
it('should get the same headers as the server CORS', async ({ page, server, browserName, platform, isElectron, browserMajorVersion, channel }) => {
it.skip(isElectron && browserMajorVersion < 99, 'This needs Chromium >= 99');
it.fail(browserName === 'webkit' && platform === 'win32', 'Curl does not show accept-encoding and accept-language');
it.fail(browserName === 'webkit' && platform === 'win32' && channel !== 'webkit-wsl', 'Curl does not show accept-encoding and accept-language');
await page.goto(server.PREFIX + '/empty.html');
let serverRequest;
@ -392,7 +392,7 @@ it('should report raw headers', async ({ page, server, browserName, platform, is
expectedHeaders = [];
for (let i = 0; i < req.rawHeaders.length; i += 2)
expectedHeaders.push({ name: req.rawHeaders[i], value: req.rawHeaders[i + 1] });
if (browserName === 'webkit' && platform === 'win32') {
if (browserName === 'webkit' && platform === 'win32' && channel !== 'webkit-wsl') {
expectedHeaders = expectedHeaders.filter(({ name }) => name.toLowerCase() !== 'accept-encoding');
// Convert "value": "en-US, en-US" => "en-US"
expectedHeaders = expectedHeaders.map(e => {

View File

@ -230,10 +230,11 @@ it('should behave the same way for headers and allHeaders', async ({ page, serve
expect(allHeaders['name-b']).toEqual('v4');
});
it('should provide a Response with a file URL', async ({ page, asset, isAndroid, isElectron, isWindows, browserName, mode }) => {
it('should provide a Response with a file URL', async ({ page, asset, isAndroid, isElectron, isWindows, browserName, mode, channel }) => {
it.skip(isAndroid, 'No files on Android');
it.skip(browserName === 'firefox', 'Firefox does return null for file:// URLs');
it.skip(mode.startsWith('service'));
it.skip(channel === 'webkit-wsl');
const fileurl = url.pathToFileURL(asset('frames/two-frames.html')).href;
const response = await page.goto(fileurl);

View File

@ -280,14 +280,14 @@ it.describe('page screenshot', () => {
expect(screenshot).toMatchSnapshot('screenshot-clip-odd-size.png');
});
it('should work for canvas', async ({ page, server, isElectron, isMac, isLinux, macVersion, browserName, isHeadlessShell, headless }) => {
it('should work for canvas', async ({ page, server, isElectron, isMac, isLinux, macVersion, browserName, isHeadlessShell, headless, channel }) => {
it.fixme(isElectron && isMac, 'Fails on the bots');
it.fixme(browserName === 'webkit' && isLinux && !headless, 'WebKit has slightly different corners on gtk4.');
await page.setViewportSize({ width: 500, height: 500 });
await page.goto(server.PREFIX + '/screenshots/canvas.html');
const screenshot = await page.screenshot();
if ((!isHeadlessShell && browserName === 'chromium' && isMac && os.arch() === 'arm64' && macVersion >= 14) ||
(browserName === 'webkit' && isLinux && os.arch() === 'x64'))
(browserName === 'webkit' && isLinux && os.arch() === 'x64') || channel === 'webkit-wsl')
expect(screenshot).toMatchSnapshot('screenshot-canvas-with-accurate-corners.png');
else
expect(screenshot).toMatchSnapshot('screenshot-canvas.png');

View File

@ -82,14 +82,14 @@ it('should work with clicking on anchor links', async ({ page, server }) => {
expect(page.url()).toBe(server.EMPTY_PAGE + '#foobar');
});
it('should work with clicking on links which do not commit navigation', async ({ page, server, httpsServer, browserName, platform }) => {
it('should work with clicking on links which do not commit navigation', async ({ page, server, httpsServer, browserName, platform, channel }) => {
await page.goto(server.EMPTY_PAGE);
await page.setContent(`<a href='${httpsServer.EMPTY_PAGE}'>foobar</a>`);
const [error] = await Promise.all([
page.waitForNavigation().catch(e => e),
page.click('a'),
]);
expect(error.message).toMatch(expectedSSLError(browserName, platform));
expect(error.message).toMatch(expectedSSLError(browserName, platform, channel));
});
it('should work with history.pushState()', async ({ page, server }) => {