Skip to content

[pull] main from microsoft:main #112

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 30, 2025
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 🎭 Playwright

[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-139.0.7258.42-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-140.0.2-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-26.0-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop --> [![Join Discord](https://img.shields.io/badge/join-discord-informational)](https://aka.ms/playwright/discord)
[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-139.0.7258.42-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-141.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-26.0-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop --> [![Join Discord](https://img.shields.io/badge/join-discord-informational)](https://aka.ms/playwright/discord)

## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)

Expand All @@ -10,7 +10,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->139.0.7258.42<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->26.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->140.0.2<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->141.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |

Headless execution is supported for all browsers on all platforms. Check out [system requirements](https://playwright.dev/docs/intro#system-requirements) for details.

Expand Down
2 changes: 1 addition & 1 deletion docs/src/browsers.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ Running 1 test using 1 worker

With the VS Code extension you can run your tests on different browsers by checking the checkbox next to the browser name in the Playwright sidebar. These names are defined in your Playwright config file under the projects section. The default config when installing Playwright gives you 3 projects, Chromium, Firefox and WebKit. The first project is selected by default.

![Projects section in VS Code extension](https://github.com/microsoft/playwright/assets/13063165/58fedea6-a2b9-4942-b2c7-2f3d482210cf)
![Projects section in VS Code extension](./images/vscode-projects-section.png)

To run tests on multiple projects(browsers), select each project by checking the checkboxes next to the project name.

Expand Down
Binary file added docs/src/images/vscode-projects-section.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 8 additions & 8 deletions packages/playwright-core/browsers.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,27 @@
},
{
"name": "chromium-tip-of-tree",
"revision": "1353",
"revision": "1354",
"installByDefault": false,
"browserVersion": "140.0.7315.0"
"browserVersion": "140.0.7325.0"
},
{
"name": "chromium-tip-of-tree-headless-shell",
"revision": "1353",
"revision": "1354",
"installByDefault": false,
"browserVersion": "140.0.7315.0"
"browserVersion": "140.0.7325.0"
},
{
"name": "firefox",
"revision": "1489",
"revision": "1490",
"installByDefault": true,
"browserVersion": "140.0.2"
"browserVersion": "141.0"
},
{
"name": "firefox-beta",
"revision": "1484",
"revision": "1485",
"installByDefault": false,
"browserVersion": "140.0b7"
"browserVersion": "142.0b4"
},
{
"name": "webkit",
Expand Down
34 changes: 17 additions & 17 deletions packages/playwright-core/src/server/chromium/crDragDrop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,26 +91,26 @@ export class DragManager {
};
}

await this._crPage._page.safeNonStallingEvaluateInAllFrames(`(${setupDragListeners.toString()})()`, 'utility');
progress.cleanupWhenAborted(() => this._crPage._page.safeNonStallingEvaluateInAllFrames('window.__cleanupDrag && window.__cleanupDrag()', 'utility'));

client.on('Input.dragIntercepted', onDragIntercepted!);
try {
let expectingDrag = false;
await progress.race(this._crPage._page.safeNonStallingEvaluateInAllFrames(`(${setupDragListeners.toString()})()`, 'utility'));
client.on('Input.dragIntercepted', onDragIntercepted!);
await client.send('Input.setInterceptDrags', { enabled: true });
} catch {
// If Input.setInterceptDrags is not supported, just do a regular move.
// This can be removed once we stop supporting old Electron.
client.off('Input.dragIntercepted', onDragIntercepted!);
return moveCallback();
try {
await progress.race(moveCallback());
expectingDrag = (await Promise.all(this._crPage._page.frames().map(async frame => {
return frame.nonStallingEvaluateInExistingContext('window.__cleanupDrag?.()', 'utility').catch(() => false);
}))).some(x => x);
} finally {
client.off('Input.dragIntercepted', onDragIntercepted!);
await client.send('Input.setInterceptDrags', { enabled: false });
}
this._dragState = expectingDrag ? (await dragInterceptedPromise).data : null;
} catch (error) {
// Cleanup without blocking, it will be done before the next playwright action.
this._crPage._page.safeNonStallingEvaluateInAllFrames('window.__cleanupDrag?.()', 'utility').catch(() => {});
throw error;
}
await moveCallback();

const expectingDrag = (await Promise.all(this._crPage._page.frames().map(async frame => {
return frame.nonStallingEvaluateInExistingContext('window.__cleanupDrag && window.__cleanupDrag()', 'utility').catch(() => false);
}))).some(x => x);
this._dragState = expectingDrag ? (await dragInterceptedPromise).data : null;
client.off('Input.dragIntercepted', onDragIntercepted!);
await progress.race(client.send('Input.setInterceptDrags', { enabled: false }));

if (this._dragState) {
await progress.race(this._crPage._mainFrameSession._client.send('Input.dispatchDragEvent', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1702,7 +1702,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Firefox HiDPI": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0.2) Gecko/20100101 Firefox/140.0.2",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:141.0) Gecko/20100101 Firefox/141.0",
"screen": {
"width": 1792,
"height": 1120
Expand Down Expand Up @@ -1762,7 +1762,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Firefox": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0.2) Gecko/20100101 Firefox/140.0.2",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:141.0) Gecko/20100101 Firefox/141.0",
"screen": {
"width": 1920,
"height": 1080
Expand Down
136 changes: 67 additions & 69 deletions packages/playwright-core/src/server/electron/electron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import os from 'os';
import path from 'path';
import * as readline from 'readline';

import { ManualPromise, removeFolders } from '../../utils';
import { ManualPromise } from '../../utils';
import { wrapInASCIIBox } from '../utils/ascii';
import { RecentLogsCollector } from '../utils/debugLogger';
import { eventsHelper } from '../utils/eventsHelper';
Expand Down Expand Up @@ -166,8 +166,6 @@ export class Electron extends SdkObject {
}

const artifactsDir = await progress.race(fs.promises.mkdtemp(ARTIFACTS_FOLDER));
progress.cleanupWhenAborted(() => removeFolders([artifactsDir]));

const browserLogsCollector = new RecentLogsCollector();
const env = options.env ? envArrayToObject(options.env) : process.env;

Expand Down Expand Up @@ -243,76 +241,76 @@ export class Electron extends SdkObject {
const chromeMatchPromise = waitForLine(progress, launchedProcess, /^DevTools listening on (ws:\/\/.*)$/);
const debuggerDisconnectPromise = waitForLine(progress, launchedProcess, /Waiting for the debugger to disconnect\.\.\./);

const nodeMatch = await nodeMatchPromise;
const nodeTransport = await WebSocketTransport.connect(progress, nodeMatch[1]);
progress.cleanupWhenAborted(() => nodeTransport.close());
const nodeConnection = new CRConnection(this, nodeTransport, helper.debugProtocolLogger(), browserLogsCollector);
try {
const nodeMatch = await nodeMatchPromise;
const nodeTransport = await WebSocketTransport.connect(progress, nodeMatch[1]);
const nodeConnection = new CRConnection(this, nodeTransport, helper.debugProtocolLogger(), browserLogsCollector);
// Immediately release exiting process under debug.
debuggerDisconnectPromise.then(() => {
nodeTransport.close();
}).catch(() => {});

// Immediately release exiting process under debug.
debuggerDisconnectPromise.then(() => {
nodeTransport.close();
}).catch(() => {});
const chromeMatch = await Promise.race([
chromeMatchPromise,
waitForXserverError,
]) as RegExpMatchArray;
const chromeTransport = await WebSocketTransport.connect(progress, chromeMatch[1]);
progress.cleanupWhenAborted(() => chromeTransport.close());
const browserProcess: BrowserProcess = {
onclose: undefined,
process: launchedProcess,
close: gracefullyClose,
kill
};
const contextOptions: types.BrowserContextOptions = {
...options,
noDefaultViewport: true,
};
const browserOptions: BrowserOptions = {
name: 'electron',
isChromium: true,
headful: true,
persistent: contextOptions,
browserProcess,
protocolLogger: helper.debugProtocolLogger(),
browserLogsCollector,
artifactsDir,
downloadsPath: artifactsDir,
tracesDir: options.tracesDir || artifactsDir,
originalLaunchOptions: {},
};
validateBrowserContextOptions(contextOptions, browserOptions);
const browser = await progress.race(CRBrowser.connect(this.attribution.playwright, chromeTransport, browserOptions));
app = new ElectronApplication(this, browser, nodeConnection, launchedProcess);
await progress.race(app.initialize());
return app;
const chromeMatch = await Promise.race([
chromeMatchPromise,
waitForXserverError,
]);
const chromeTransport = await WebSocketTransport.connect(progress, chromeMatch[1]);
const browserProcess: BrowserProcess = {
onclose: undefined,
process: launchedProcess,
close: gracefullyClose,
kill
};
const contextOptions: types.BrowserContextOptions = {
...options,
noDefaultViewport: true,
};
const browserOptions: BrowserOptions = {
name: 'electron',
isChromium: true,
headful: true,
persistent: contextOptions,
browserProcess,
protocolLogger: helper.debugProtocolLogger(),
browserLogsCollector,
artifactsDir,
downloadsPath: artifactsDir,
tracesDir: options.tracesDir || artifactsDir,
originalLaunchOptions: {},
};
validateBrowserContextOptions(contextOptions, browserOptions);
const browser = await progress.race(CRBrowser.connect(this.attribution.playwright, chromeTransport, browserOptions));
app = new ElectronApplication(this, browser, nodeConnection, launchedProcess);
await progress.race(app.initialize());
return app;
} catch (error) {
await kill();
throw error;
}
}
}

function waitForLine(progress: Progress, process: childProcess.ChildProcess, regex: RegExp): Promise<RegExpMatchArray> {
return progress.race(new Promise((resolve, reject) => {
const rl = readline.createInterface({ input: process.stderr! });
const failError = new Error('Process failed to launch!');
const listeners = [
eventsHelper.addEventListener(rl, 'line', onLine),
eventsHelper.addEventListener(rl, 'close', reject.bind(null, failError)),
eventsHelper.addEventListener(process, 'exit', reject.bind(null, failError)),
// It is Ok to remove error handler because we did not create process and there is another listener.
eventsHelper.addEventListener(process, 'error', reject.bind(null, failError))
];
async function waitForLine(progress: Progress, process: childProcess.ChildProcess, regex: RegExp) {
const promise = new ManualPromise<RegExpMatchArray>();
const rl = readline.createInterface({ input: process.stderr! });
const failError = new Error('Process failed to launch!');
const listeners = [
eventsHelper.addEventListener(rl, 'line', onLine),
eventsHelper.addEventListener(rl, 'close', () => promise.reject(failError)),
eventsHelper.addEventListener(process, 'exit', () => promise.reject(failError)),
// It is Ok to remove error handler because we did not create process and there is another listener.
eventsHelper.addEventListener(process, 'error', () => promise.reject(failError)),
];

progress.cleanupWhenAborted(cleanup);

function onLine(line: string) {
const match = line.match(regex);
if (!match)
return;
cleanup();
resolve(match);
}
function onLine(line: string) {
const match = line.match(regex);
if (match)
promise.resolve(match);
}

function cleanup() {
eventsHelper.removeEventListeners(listeners);
}
}));
try {
return await progress.race(promise);
} finally {
eventsHelper.removeEventListeners(listeners);
}
}
9 changes: 7 additions & 2 deletions packages/playwright-core/src/server/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ export abstract class APIRequestContext extends SdkObject {
};
this.emit(APIRequestContext.Events.Request, requestEvent);

let destroyRequest: (() => void) | undefined;
const resultPromise = new Promise<SendRequestResult>((fulfill, reject) => {
const requestConstructor: ((url: URL, options: http.RequestOptions, callback?: (res: http.IncomingMessage) => void) => http.ClientRequest)
= (url.protocol === 'https:' ? https : http).request;
Expand Down Expand Up @@ -483,7 +484,7 @@ export abstract class APIRequestContext extends SdkObject {
body.on('end', notifyBodyFinished);
});
request.on('error', reject);
progress.cleanupWhenAborted(() => request.destroy());
destroyRequest = () => request.destroy();

listeners.push(
eventsHelper.addEventListener(this, APIRequestContext.Events.Dispose, () => {
Expand Down Expand Up @@ -539,7 +540,11 @@ export abstract class APIRequestContext extends SdkObject {
request.write(postData);
request.end();
});
return progress.race(resultPromise);

return progress.race(resultPromise).catch(error => {
destroyRequest?.();
throw error;
});
}

private _getHttpCredentials(url: URL) {
Expand Down
11 changes: 4 additions & 7 deletions packages/playwright-core/src/server/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,22 +57,19 @@ class Helper {

static waitForEvent(progress: Progress, emitter: EventEmitter, event: string | symbol, predicate?: Function): { promise: Promise<any>, dispose: () => void } {
const listeners: RegisteredListener[] = [];
const promise = new Promise((resolve, reject) => {
const dispose = () => eventsHelper.removeEventListeners(listeners);
const promise = progress.race(new Promise((resolve, reject) => {
listeners.push(eventsHelper.addEventListener(emitter, event, eventArg => {
try {
if (predicate && !predicate(eventArg))
return;
eventsHelper.removeEventListeners(listeners);
resolve(eventArg);
} catch (e) {
eventsHelper.removeEventListeners(listeners);
reject(e);
}
}));
});
const dispose = () => eventsHelper.removeEventListeners(listeners);
progress.cleanupWhenAborted(dispose);
return { promise: progress.race(promise), dispose };
})).finally(() => dispose());
return { promise, dispose };
}

static secondsToRoundishMillis(value: number): number {
Expand Down
20 changes: 11 additions & 9 deletions packages/playwright-core/src/server/localUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,16 +179,18 @@ export function harClose(harBackends: Map<string, HarBackend>, params: channels.
export async function harUnzip(progress: Progress, params: channels.LocalUtilsHarUnzipParams): Promise<void> {
const dir = path.dirname(params.zipFile);
const zipFile = new ZipFile(params.zipFile);
progress.cleanupWhenAborted(() => zipFile.close());
for (const entry of await progress.race(zipFile.entries())) {
const buffer = await progress.race(zipFile.read(entry));
if (entry === 'har.har')
await progress.race(fs.promises.writeFile(params.harFile, buffer));
else
await progress.race(fs.promises.writeFile(path.join(dir, entry), buffer));
try {
for (const entry of await progress.race(zipFile.entries())) {
const buffer = await progress.race(zipFile.read(entry));
if (entry === 'har.har')
await progress.race(fs.promises.writeFile(params.harFile, buffer));
else
await progress.race(fs.promises.writeFile(path.join(dir, entry), buffer));
}
await progress.race(fs.promises.unlink(params.zipFile));
} finally {
zipFile.close();
}
await progress.race(fs.promises.unlink(params.zipFile));
zipFile.close();
}

export async function tracingStarted(progress: Progress, stackSessions: Map<string, StackSession>, params: channels.LocalUtilsTracingStartedParams): Promise<channels.LocalUtilsTracingStartedResult> {
Expand Down
Loading
Loading