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
18 changes: 18 additions & 0 deletions docs/src/api/class-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -4648,6 +4648,24 @@ Will throw an error if the page is closed before the download event is fired.
* langs: python
- returns: <[EventContextManager]<[Download]>>

**Usage**

```python async
async with page.expect_download() as download_info:
await page.get_by_text("Download").click()

download = await download_info.value
print(download.url)
```

```python sync
with page.expect_download() as download_info:
page.get_by_text("Download").click()

download = download_info.value
print(download.url)
```

### param: Page.waitForDownload.action = %%-csharp-wait-for-event-action-%%
* since: v1.12

Expand Down
5 changes: 5 additions & 0 deletions packages/isomorphic/urlMatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,11 @@ function resolveGlobBase(baseURL: string | undefined, match: string): string {
// Preserve explicit schema as is as it may affect trailing slashes after domain.
return token;
}
// Components without glob metacharacters are literal, so let them round-trip
// through new URL() to preserve normalization (default ports such as :80/:443,
// percent-encoding, IDN hosts). Only opaque tokens defeat that normalization.
if (!/[*?{}\\]/.test(token))
return token;
const questionIndex = token.indexOf('?');
if (questionIndex === -1)
return mapToken(token, `$_${index}_$`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ const disabledFeatures = [
'PaintHolding',
// See https://github.com/microsoft/playwright/issues/32230
'ThirdPartyStoragePartitioning',
// Chromium 149 rejects re-applying the `origin` header on a redirect (as request interception
// does) with net::ERR_INVALID_ARGUMENT. See https://github.com/microsoft/playwright/issues/41690
'BlockOriginHeaderModificationOnRedirect',
// See https://github.com/microsoft/playwright/issues/16126
'Translate',
// See https://issues.chromium.org/u/1/issues/435410220
Expand Down
7 changes: 2 additions & 5 deletions packages/playwright-core/src/server/webkit/wkPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,13 +215,13 @@ export class WKPage implements PageDelegate {
}
if (this._page.fileChooserIntercepted())
promises.push(session.send('Page.setInterceptFileChooserDialog', { enabled: true }));
promises.push(session.send('Page.overrideSetting', { setting: 'DeviceOrientationEventEnabled', value: contextOptions.isMobile }));
promises.push(session.send('Page.overrideSetting', { setting: 'FullScreenEnabled', value: !contextOptions.isMobile }));
promises.push(session.send('Page.overrideSetting', { setting: 'NotificationsEnabled', value: !contextOptions.isMobile }));
promises.push(session.send('Page.overrideSetting', { setting: 'PointerLockEnabled', value: !contextOptions.isMobile }));
promises.push(session.send('Page.overrideSetting', { setting: 'InputTypeMonthEnabled', value: contextOptions.isMobile }));
promises.push(session.send('Page.overrideSetting', { setting: 'InputTypeWeekEnabled', value: contextOptions.isMobile }));
promises.push(session.send('Page.overrideSetting', { setting: 'FixedBackgroundsPaintRelativeToDocument', value: contextOptions.isMobile }));
promises.push(session.send('Page.overrideSetting', { setting: 'PushAPIEnabled', value: !contextOptions.isMobile }));
await Promise.all(promises);
}

Expand Down Expand Up @@ -795,11 +795,8 @@ export class WKPage implements PageDelegate {

private _calculateBootstrapScript(): string {
const scripts: string[] = [];
if (!this._page.browserContext._options.isMobile) {
if (!this._page.browserContext._options.isMobile)
scripts.push('delete window.orientation');
scripts.push('delete window.ondevicemotion');
scripts.push('delete window.ondeviceorientation');
}
scripts.push('if (!window.safari) window.safari = { pushNotification: { toString() { return "[object SafariRemoteNotification]"; } } };');
scripts.push('if (!window.GestureEvent) window.GestureEvent = function GestureEvent() {};');
scripts.push(this._publicKeyCredentialScript());
Expand Down
4 changes: 3 additions & 1 deletion packages/playwright-core/src/tools/backend/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ export class Context {
await this.newTab();
if (crashed)
this._currentTab!.logErrorMessage('Page crashed and was reset to about:blank.');
await this._currentTab!.waitForInitialized();
return this._currentTab!;
}

Expand Down Expand Up @@ -236,8 +237,9 @@ export class Context {
const suffix = this._video.fileNames.length ? `-${this._video.fileNames.length}` : '';
let fileName = this._video.fileName;
if (fileName && suffix) {
const dir = path.dirname(fileName);
const ext = path.extname(fileName);
fileName = path.basename(fileName, ext) + suffix + ext;
fileName = path.join(dir, path.basename(fileName, ext) + suffix + ext);
}
this._video.fileNames.push(fileName);
await page.screencast.start({ path: fileName, ...this._video.params });
Expand Down
4 changes: 4 additions & 0 deletions packages/playwright-core/src/tools/backend/tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ export class Tab extends EventEmitter<TabEventsInterface> {
this._consoleLog.stop();
}

async waitForInitialized() {
await this._initializedPromise;
}

static forPage(page: playwright.Page): Tab | undefined {
// eslint-disable-next-line no-restricted-syntax
return (page as any)[tabSymbol];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"ambientlight": false,
"applicationcache": false,
"audio": {
"ogg": "",
"ogg": "probably",
"mp3": "probably",
"opus": "probably",
"wav": "probably",
Expand Down Expand Up @@ -103,7 +103,7 @@
"flexwrap": true,
"focusvisible": true,
"focuswithin": true,
"fontdisplay": false,
"fontdisplay": true,
"fontface": true,
"generatedcontent": true,
"cssgradients": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"ambientlight": false,
"applicationcache": false,
"audio": {
"ogg": "",
"ogg": "probably",
"mp3": "probably",
"opus": "probably",
"wav": "probably",
Expand Down Expand Up @@ -103,7 +103,7 @@
"flexwrap": true,
"focusvisible": true,
"focuswithin": true,
"fontdisplay": false,
"fontdisplay": true,
"fontface": true,
"generatedcontent": true,
"cssgradients": true,
Expand Down Expand Up @@ -342,7 +342,7 @@
"webm": "probably",
"vp9": "probably",
"hls": "probably",
"av1": ""
"av1": "probably"
},
"videocrossorigin": true,
"videoloop": true,
Expand Down
16 changes: 11 additions & 5 deletions tests/library/modernizr.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,12 @@ it('Safari Desktop', async ({ browser, browserName, platform, httpsServer, chann
deviceScaleFactor: 2,
ignoreHTTPSErrors: true,
});
const { actual, expected } = await checkFeatures('safari-18', context, httpsServer);
const { actual, expected } = await checkFeatures('safari-26', context, httpsServer);

expected.pushmanager = false;
expected.devicemotion2 = false;
expected.deviceorientation3 = false;
// Shipping Safari exposes `font-display` as a settable CSS property (element.style.fontDisplay === ''),
// but open-source WebKit keeps it a @font-face descriptor only, so it is undefined here. No runtime
// flag bridges the gap, hence the override.
expected.fontdisplay = false;

delete expected.webglextensions;
delete actual.webglextensions;
Expand Down Expand Up @@ -96,7 +97,7 @@ it('Mobile Safari', async ({ playwright, browser, browserName, platform, httpsSe
...iPhone,
ignoreHTTPSErrors: true,
});
const { actual, expected } = await checkFeatures('mobile-safari-18', context, httpsServer);
const { actual, expected } = await checkFeatures('mobile-safari-26', context, httpsServer);

{
// All platforms.
Expand All @@ -108,6 +109,11 @@ it('Mobile Safari', async ({ playwright, browser, browserName, platform, httpsSe
expected.mediasource = true;
expected.scrolltooptions = false;

// Shipping Safari exposes `font-display` as a settable CSS property (element.style.fontDisplay === ''),
// but open-source WebKit keeps it a @font-face descriptor only, so it is undefined here. No runtime
// flag bridges the gap, hence the override.
expected.fontdisplay = false;

delete expected.webglextensions;
delete actual.webglextensions;
expected.audio = !!expected.audio;
Expand Down
4 changes: 2 additions & 2 deletions tests/mcp/cli-devtools.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ test('tracing-start-stop', async ({ cli, server }, testInfo) => {

test('video-start-stop', async ({ cli, server }) => {
await cli('open', server.HELLO_WORLD);
const { output: videoStartOutput } = await cli('video-start', 'video.webm', '--size=400x300');
const { output: videoStartOutput } = await cli('video-start', 'recordings/video.webm', '--size=400x300');
expect(videoStartOutput).toContain('Video recording started.');
const { output: tabNewOutput } = await cli('tab-new');
expect(tabNewOutput).toContain('1: (current) [](about:blank)');
Expand All @@ -225,7 +225,7 @@ test('video-start-stop', async ({ cli, server }) => {
const { output: tabCloseOutput } = await cli('tab-close');
expect(tabCloseOutput).toContain(`0: (current) [](${server.EMPTY_PAGE})`);
const { output: videoStopOutput } = await cli('video-stop');
expect(videoStopOutput).toContain(`### Result\n- [Video](./video.webm)\n- [Video](./video-1.webm)`);
expect(videoStopOutput).toContain(`### Result\n- [Video](recordings${path.sep}video.webm)\n- [Video](recordings${path.sep}video-1.webm)`);
});

test('video-chapter', async ({ cli, server }) => {
Expand Down
21 changes: 21 additions & 0 deletions tests/mcp/init-page.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,27 @@ test('init-page surfaces load errors instead of silently dropping them', async (
expect(JSON.stringify(response.content)).toContain('boom from initPage');
});

test('init-page is applied before the first page screenshot', async ({ startClient }) => {
const initPagePath = test.info().outputPath('screenshotInitPage.ts');
await fs.promises.writeFile(initPagePath, `
export default async ({ page }) => {
page.screenshot = async () => {
throw new Error('initPage screenshot hook applied');
};
};
`);

const { client } = await startClient({
args: [`--init-page=${initPagePath}`],
});
const response: any = await client.callTool({
name: 'browser_take_screenshot',
arguments: {},
});
expect(response.isError).toBe(true);
expect(JSON.stringify(response.content)).toContain('initPage screenshot hook applied');
});

test('--init-page w/ --init-script', async ({ startClient, server }) => {
server.setContent('/', `
<div>Hello world</div>
Expand Down
9 changes: 9 additions & 0 deletions tests/page/interception.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,15 @@ it('should work with glob', async () => {
expect(urlMatches(undefined, 'https://playwright.dev/foobar', 'https://playwright.dev/fooBAR')).toBeFalsy();
expect(urlMatches(undefined, 'https://playwright.dev/foobar?a=b', 'https://playwright.dev/foobar?A=B')).toBeFalsy();

// Literal globs are normalized through new URL(), so explicit default ports,
// percent-encoding and IDN hosts match request.url() which is already normalized.
expect(urlMatches(undefined, 'http://example.com/path', 'http://example.com:80/path')).toBeTruthy();
expect(urlMatches(undefined, 'https://example.com/path', 'https://example.com:443/path')).toBeTruthy();
expect(urlMatches(undefined, 'http://example.com:8080/path', 'http://example.com:8080/path')).toBeTruthy();
expect(urlMatches(undefined, 'http://localhost/', 'http://localhost:80/**')).toBeTruthy();
expect(urlMatches(undefined, 'http://example.com/foo%20bar', 'http://example.com/foo bar')).toBeTruthy();
expect(urlMatches(undefined, 'http://xn--mnchen-3ya.de/', 'http://münchen.de/')).toBeTruthy();

expect(urlMatches(undefined, 'https://localhost:3000/?a=b', '**/?a=b')).toBeTruthy();
expect(urlMatches(undefined, 'https://localhost:3000/?a=b', '**?a=b')).toBeTruthy();
expect(urlMatches(undefined, 'https://localhost:3000/?a=b', '**=b')).toBeTruthy();
Expand Down
29 changes: 29 additions & 0 deletions tests/page/page-request-continue.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,35 @@ it('propagate headers cross origin redirect after interception', {
expect.soft(serverRequest.headers['custom']).toBe('foo');
});

it('continue should pass on 307 cross-origin redirect', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41690' }
}, async ({ page, server, isAndroid }) => {
it.skip(isAndroid, 'No cross-process on Android');

server.setRoute('/final', (request, response) => {
response.writeHead(200, { 'content-type': 'text/html' });
response.end('<!doctype html><title>final</title><p>ok</p>');
});
// Cross-origin 307 redirect that preserves the POST method.
server.setRoute('/redirect307', (request, response) => {
response.writeHead(307, { location: `${server.PREFIX}/final` });
response.end();
});

await page.goto(server.PREFIX + '/empty.html');
await page.setContent(`
<form id="f" method="POST" action="${server.CROSS_PROCESS_PREFIX}/redirect307">
<input type="submit">
</form>`);

await page.route('**/*', route => route.continue());
await Promise.all([
page.waitForURL(`${server.PREFIX}/final`),
page.locator('input').click(),
]);
await expect(page.locator('p')).toHaveText('ok');
});

it('should intercept css variable with background url', async ({ page, server }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/19158' });

Expand Down
30 changes: 15 additions & 15 deletions tests/playwright-test/stable-test-runner/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion tests/playwright-test/stable-test-runner/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"private": true,
"dependencies": {
"@playwright/test": "^1.62.0-alpha-2026-06-29"
"@playwright/test": "^1.62.0-alpha-2026-07-06"
}
}
Loading