diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index 3f0febb599edc..7cf23b76637ff 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -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 diff --git a/packages/isomorphic/urlMatch.ts b/packages/isomorphic/urlMatch.ts index 30cd9a95694d0..a054f1476f65c 100644 --- a/packages/isomorphic/urlMatch.ts +++ b/packages/isomorphic/urlMatch.ts @@ -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}_$`); diff --git a/packages/playwright-core/src/server/chromium/chromiumSwitches.ts b/packages/playwright-core/src/server/chromium/chromiumSwitches.ts index aade807fc3e65..7543642c624c9 100644 --- a/packages/playwright-core/src/server/chromium/chromiumSwitches.ts +++ b/packages/playwright-core/src/server/chromium/chromiumSwitches.ts @@ -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 diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index dcc17c8b14f25..06ceb0ac289c9 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -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); } @@ -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()); diff --git a/packages/playwright-core/src/tools/backend/context.ts b/packages/playwright-core/src/tools/backend/context.ts index 8d0c7399dcc9b..45da952fff94f 100644 --- a/packages/playwright-core/src/tools/backend/context.ts +++ b/packages/playwright-core/src/tools/backend/context.ts @@ -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!; } @@ -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 }); diff --git a/packages/playwright-core/src/tools/backend/tab.ts b/packages/playwright-core/src/tools/backend/tab.ts index 676f5da4e9040..441f8a9f75b00 100644 --- a/packages/playwright-core/src/tools/backend/tab.ts +++ b/packages/playwright-core/src/tools/backend/tab.ts @@ -148,6 +148,10 @@ export class Tab extends EventEmitter { 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]; diff --git a/tests/assets/modernizr/mobile-safari-18.json b/tests/assets/modernizr/mobile-safari-26.json similarity index 99% rename from tests/assets/modernizr/mobile-safari-18.json rename to tests/assets/modernizr/mobile-safari-26.json index cff9199bb260f..57aaebc27f68b 100644 --- a/tests/assets/modernizr/mobile-safari-18.json +++ b/tests/assets/modernizr/mobile-safari-26.json @@ -17,7 +17,7 @@ "ambientlight": false, "applicationcache": false, "audio": { - "ogg": "", + "ogg": "probably", "mp3": "probably", "opus": "probably", "wav": "probably", @@ -103,7 +103,7 @@ "flexwrap": true, "focusvisible": true, "focuswithin": true, - "fontdisplay": false, + "fontdisplay": true, "fontface": true, "generatedcontent": true, "cssgradients": true, diff --git a/tests/assets/modernizr/safari-18.json b/tests/assets/modernizr/safari-26.json similarity index 99% rename from tests/assets/modernizr/safari-18.json rename to tests/assets/modernizr/safari-26.json index 508a4fa9776eb..4bbfa95d167ce 100644 --- a/tests/assets/modernizr/safari-18.json +++ b/tests/assets/modernizr/safari-26.json @@ -17,7 +17,7 @@ "ambientlight": false, "applicationcache": false, "audio": { - "ogg": "", + "ogg": "probably", "mp3": "probably", "opus": "probably", "wav": "probably", @@ -103,7 +103,7 @@ "flexwrap": true, "focusvisible": true, "focuswithin": true, - "fontdisplay": false, + "fontdisplay": true, "fontface": true, "generatedcontent": true, "cssgradients": true, @@ -342,7 +342,7 @@ "webm": "probably", "vp9": "probably", "hls": "probably", - "av1": "" + "av1": "probably" }, "videocrossorigin": true, "videoloop": true, diff --git a/tests/library/modernizr.spec.ts b/tests/library/modernizr.spec.ts index 9a3bdc65d02b1..e8aec532bc1e1 100644 --- a/tests/library/modernizr.spec.ts +++ b/tests/library/modernizr.spec.ts @@ -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; @@ -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. @@ -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; diff --git a/tests/mcp/cli-devtools.spec.ts b/tests/mcp/cli-devtools.spec.ts index 7718e6e714af9..4be4836af79c2 100644 --- a/tests/mcp/cli-devtools.spec.ts +++ b/tests/mcp/cli-devtools.spec.ts @@ -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)'); @@ -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 }) => { diff --git a/tests/mcp/init-page.spec.ts b/tests/mcp/init-page.spec.ts index c2ea53aaafc59..6e708116a0f36 100644 --- a/tests/mcp/init-page.spec.ts +++ b/tests/mcp/init-page.spec.ts @@ -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('/', `
Hello world
diff --git a/tests/page/interception.spec.ts b/tests/page/interception.spec.ts index 0d3fbdd02e074..d8efbf3cb0714 100644 --- a/tests/page/interception.spec.ts +++ b/tests/page/interception.spec.ts @@ -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(); diff --git a/tests/page/page-request-continue.spec.ts b/tests/page/page-request-continue.spec.ts index 4226f7374bfb1..48f9458012b46 100644 --- a/tests/page/page-request-continue.spec.ts +++ b/tests/page/page-request-continue.spec.ts @@ -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('final

ok

'); + }); + // 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(` +
+ +
`); + + 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' }); diff --git a/tests/playwright-test/stable-test-runner/package-lock.json b/tests/playwright-test/stable-test-runner/package-lock.json index 59efec0832872..052d2f9a2612a 100644 --- a/tests/playwright-test/stable-test-runner/package-lock.json +++ b/tests/playwright-test/stable-test-runner/package-lock.json @@ -5,22 +5,22 @@ "packages": { "": { "dependencies": { - "@playwright/test": "^1.62.0-alpha-2026-06-29" + "@playwright/test": "^1.62.0-alpha-2026-07-06" } }, "node_modules/@playwright/test": { - "version": "1.62.0-alpha-2026-06-29", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.0-alpha-2026-06-29.tgz", - "integrity": "sha512-g9foBBuWf7IoALxyck3GJjjl/dsUewmxXz8a3JNgKgqgHFOZou9DZA7isi0hLU9Lz+75LeBSxN92E+aI7KAbUQ==", + "version": "1.62.0-alpha-2026-07-06", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.0-alpha-2026-07-06.tgz", + "integrity": "sha512-tNsihJtAWjTsDy/O+UyTMAaGnPCEb+tuynXS6kD1XKis8HXpeG0aq0AC8LgIjsnN4hFSrLuTD1vpWmJ5HUoLiQ==", "license": "Apache-2.0", "dependencies": { - "playwright": "1.62.0-alpha-2026-06-29" + "playwright": "1.62.0-alpha-2026-07-06" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/fsevents": { @@ -38,33 +38,33 @@ } }, "node_modules/playwright": { - "version": "1.62.0-alpha-2026-06-29", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.0-alpha-2026-06-29.tgz", - "integrity": "sha512-ugaMuh6rAOqmbaxrM7+XQSs7M6yPXykq5gVugJPClHcJ4Cqxmky5rDHhrCt4wmaILUeMdI7kPBCx3zMx2Qu+bg==", + "version": "1.62.0-alpha-2026-07-06", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.0-alpha-2026-07-06.tgz", + "integrity": "sha512-M8JUIAGzM8hRbtmoS8GIirVowegGdYadarL5piCeJA+4JkPwmZkyQnvR/T1LZQBb/eq66p+K1H71D2jGzaIBkQ==", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.62.0-alpha-2026-06-29" + "playwright-core": "1.62.0-alpha-2026-07-06" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=18" + "node": ">=20" }, "optionalDependencies": { "fsevents": "2.3.2" } }, "node_modules/playwright-core": { - "version": "1.62.0-alpha-2026-06-29", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.0-alpha-2026-06-29.tgz", - "integrity": "sha512-/13EvW8l9Bmvt9nKHey+OcvZ8nob2FM8BCKBsUNwbXgmT4Hc0XX2b6mOErU0RBYNedOCa9q15USI7SY9K17v4w==", + "version": "1.62.0-alpha-2026-07-06", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.0-alpha-2026-07-06.tgz", + "integrity": "sha512-kHvSsUXFy2VsU6AZQTY+5vj/1ZytyoYV9b7PTwu2Bv+Lt2IGs97iRC1fsVwIlBIpRDd/mO/B1FWuK6fEWU7hBQ==", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" }, "engines": { - "node": ">=18" + "node": ">=20" } } } diff --git a/tests/playwright-test/stable-test-runner/package.json b/tests/playwright-test/stable-test-runner/package.json index fe267d72ceb44..5b483b3ba3d44 100644 --- a/tests/playwright-test/stable-test-runner/package.json +++ b/tests/playwright-test/stable-test-runner/package.json @@ -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" } }