From bc83d7084c17a0b787c30b3c874e7a35a5576042 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 25 Jan 2024 07:34:11 -0800 Subject: [PATCH] fix(chromium): emulate navigator.userAgentData along with UA (#29159) Fixes #28989, fixes #29139. --- .../src/server/chromium/crPage.ts | 51 ++++++++++++++++++- .../library/browsercontext-user-agent.spec.ts | 36 +++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index 1dd5237368..2c0d3f94b4 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -1094,7 +1094,11 @@ class FrameSession { async _updateUserAgent(): Promise { const options = this._crPage._browserContext._options; - await this._client.send('Emulation.setUserAgentOverride', { userAgent: options.userAgent || '', acceptLanguage: options.locale }); + await this._client.send('Emulation.setUserAgentOverride', { + userAgent: options.userAgent || '', + acceptLanguage: options.locale, + userAgentMetadata: calculateUserAgentMetadata(options), + }); } private async _setDefaultFontFamilies(session: CRSession) { @@ -1257,3 +1261,48 @@ async function emulateTimezone(session: CRSession, timezoneId: string) { } const contextDelegateSymbol = Symbol('delegate'); + +// Chromium reference: https://source.chromium.org/chromium/chromium/src/+/main:components/embedder_support/user_agent_utils.cc;l=434;drc=70a6711e08e9f9e0d8e4c48e9ba5cab62eb010c2 +function calculateUserAgentMetadata(options: channels.BrowserNewContextParams) { + const ua = options.userAgent; + if (!ua) + return undefined; + const metadata: Protocol.Emulation.UserAgentMetadata = { + mobile: !!options.isMobile, + model: '', + architecture: 'x64', + platform: 'Windows', + platformVersion: '', + }; + const androidMatch = ua.match(/Android (\d+(\.\d+)?(\.\d+)?)/); + const iPhoneMatch = ua.match(/iPhone OS (\d+(_\d+)?)/); + const iPadMatch = ua.match(/iPad; CPU OS (\d+(_\d+)?)/); + const macOSMatch = ua.match(/Mac OS X (\d+(_\d+)?(_\d+)?)/); + const windowsMatch = ua.match(/Windows\D+(\d+(\.\d+)?(\.\d+)?)/); + if (androidMatch) { + metadata.platform = 'Android'; + metadata.platformVersion = androidMatch[1]; + metadata.architecture = 'arm'; + } else if (iPhoneMatch) { + metadata.platform = 'iOS'; + metadata.platformVersion = iPhoneMatch[1]; + metadata.architecture = 'arm'; + } else if (iPadMatch) { + metadata.platform = 'iOS'; + metadata.platformVersion = iPadMatch[1]; + metadata.architecture = 'arm'; + } else if (macOSMatch) { + metadata.platform = 'macOS'; + metadata.platformVersion = macOSMatch[1]; + if (!ua.includes('Intel')) + metadata.architecture = 'arm'; + } else if (windowsMatch) { + metadata.platform = 'Windows'; + metadata.platformVersion = windowsMatch[1]; + } else if (ua.toLowerCase().includes('linux')) { + metadata.platform = 'Linux'; + } + if (ua.includes('ARM')) + metadata.architecture = 'arm'; + return metadata; +} diff --git a/tests/library/browsercontext-user-agent.spec.ts b/tests/library/browsercontext-user-agent.spec.ts index 3e38a4865a..d71399067a 100644 --- a/tests/library/browsercontext-user-agent.spec.ts +++ b/tests/library/browsercontext-user-agent.spec.ts @@ -106,3 +106,39 @@ it('custom user agent for download', async ({ server, contextFactory, browserVer const req = await serverRequest; expect(req.headers['user-agent']).toBe('MyCustomUA'); }); + +it('should work for navigator.userAgentData and sec-ch-ua headers', async ({ playwright, browserName, browser, server }) => { + it.skip(browserName !== 'chromium', 'This API is Chromium-only'); + + { + const context = await browser.newContext(); + const page = await context.newPage(); + const [request] = await Promise.all([ + server.waitForRequest('/empty.html'), + page.goto(server.EMPTY_PAGE), + ]); + expect.soft(request.headers['sec-ch-ua']).toContain(`"Chromium"`); + expect.soft(request.headers['sec-ch-ua-mobile']).toBe(`?0`); + expect.soft(request.headers['sec-ch-ua-platform']).toBeTruthy(); + expect.soft(await page.evaluate(() => (window.navigator as any).userAgentData.toJSON())).toEqual( + expect.objectContaining({ mobile: false }) + ); + await context.close(); + } + + { + const context = await browser.newContext(playwright.devices['Pixel 7']); + const page = await context.newPage(); + const [request] = await Promise.all([ + server.waitForRequest('/empty.html'), + page.goto(server.EMPTY_PAGE), + ]); + expect.soft(request.headers['sec-ch-ua']).toContain(`"Chromium"`); + expect.soft(request.headers['sec-ch-ua-mobile']).toBe(`?1`); + expect.soft(request.headers['sec-ch-ua-platform']).toBe(`"Android"`); + expect.soft(await page.evaluate(() => (window.navigator as any).userAgentData.toJSON())).toEqual( + expect.objectContaining({ mobile: true, platform: 'Android' }) + ); + await context.close(); + } +});