Skip to content
Open
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
21 changes: 21 additions & 0 deletions packages/playwright-core/src/server/browserContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,27 @@ export function normalizeProxySettings(proxy: types.ProxySettings): types.ProxyS
return { ...proxy, server, bypass };
}

// Maps a user agent string to the corresponding navigator.platform value.
// Used by Firefox and WebKit; Chromium uses its own navigatorPlatform()
// in crPage.ts which operates on already-parsed UserAgentMetadata.
export function navigatorPlatformFromUA(ua: string): string | undefined {
if (!ua)
return undefined;
if (ua.includes('Android'))
return ua.includes('x86') ? 'Linux x86_64' : 'Linux armv8l';
if (ua.includes('iPhone OS'))
return 'iPhone';
if (ua.includes('iPad'))
return 'iPad';
if (ua.includes('Mac OS X'))
return 'MacIntel';
if (ua.includes('Windows'))
return 'Win32';
if (ua.toLowerCase().includes('linux'))
return ua.includes('aarch64') ? 'Linux aarch64' : 'Linux x86_64';
return undefined;
}

const paramsThatAllowContextReuse: (keyof channels.BrowserNewContextForReuseParams)[] = [
'colorScheme',
'forcedColors',
Expand Down
18 changes: 16 additions & 2 deletions packages/playwright-core/src/server/chromium/crPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -988,10 +988,13 @@ class FrameSession {

async _updateUserAgent(): Promise<void> {
const options = this._crPage._browserContext._options;
const userAgentMetadata = calculateUserAgentMetadata(options);
const emulateUAPlatform = !process.env.PLAYWRIGHT_NO_UA_PLATFORM;
await this._client.send('Emulation.setUserAgentOverride', {
userAgent: options.userAgent || '',
acceptLanguage: options.locale,
userAgentMetadata: calculateUserAgentMetadata(options),
platform: emulateUAPlatform && userAgentMetadata ? navigatorPlatform(userAgentMetadata, options.userAgent || '') : undefined,
userAgentMetadata,
});
}

Expand Down Expand Up @@ -1199,7 +1202,18 @@ function calculateUserAgentMetadata(options: types.BrowserContextOptions) {
} else if (ua.toLowerCase().includes('linux')) {
metadata.platform = 'Linux';
}
if (ua.includes('ARM'))
if (ua.includes('ARM') || ua.includes('aarch64'))
metadata.architecture = 'arm';
return metadata;
}

function navigatorPlatform(metadata: Protocol.Emulation.UserAgentMetadata, ua: string): string {
switch (metadata.platform) {
case 'Android': return metadata.architecture === 'arm' ? 'Linux armv8l' : 'Linux x86_64';
case 'iOS': return ua.includes('iPad') ? 'iPad' : 'iPhone';
case 'macOS': return 'MacIntel';
case 'Linux': return metadata.architecture === 'arm' ? 'Linux aarch64' : 'Linux x86_64';
case 'Windows': return 'Win32';
default: return metadata.platform;
}
}
8 changes: 7 additions & 1 deletion packages/playwright-core/src/server/firefox/ffBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

import { assert } from '../../utils';
import { Browser } from '../browser';
import { BrowserContext, verifyGeolocation } from '../browserContext';
import { BrowserContext, navigatorPlatformFromUA, verifyGeolocation } from '../browserContext';
import * as network from '../network';
import { ConnectionEvents, FFConnection } from './ffConnection';
import { FFPage } from './ffPage';
Expand Down Expand Up @@ -191,6 +191,10 @@ export class FFBrowserContext extends BrowserContext {
promises.push(this._browser.session.send('Browser.setTouchOverride', { browserContextId, hasTouch: true }));
if (this._options.userAgent)
promises.push(this._browser.session.send('Browser.setUserAgentOverride', { browserContextId, userAgent: this._options.userAgent }));
if (this._options.userAgent && !process.env.PLAYWRIGHT_NO_UA_PLATFORM) {
const platform = navigatorPlatformFromUA(this._options.userAgent);
promises.push(this._browser.session.send('Browser.setPlatformOverride', { browserContextId, platform: platform || null }));
}
if (this._options.bypassCSP)
promises.push(this._browser.session.send('Browser.setBypassCSP', { browserContextId, bypassCSP: true }));
if (this._options.ignoreHTTPSErrors || this._options.internalIgnoreHTTPSErrors)
Expand Down Expand Up @@ -325,6 +329,8 @@ export class FFBrowserContext extends BrowserContext {

async setUserAgent(userAgent: string | undefined): Promise<void> {
await this._browser.session.send('Browser.setUserAgentOverride', { browserContextId: this._browserContextId, userAgent: userAgent || null });
const platform = userAgent && !process.env.PLAYWRIGHT_NO_UA_PLATFORM ? navigatorPlatformFromUA(userAgent) : null;
await this._browser.session.send('Browser.setPlatformOverride', { browserContextId: this._browserContextId, platform: platform || null });
}

async doUpdateOffline(): Promise<void> {
Expand Down
7 changes: 7 additions & 0 deletions packages/playwright-core/src/server/webkit/wkPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { eventsHelper } from '../utils/eventsHelper';
import { hostPlatform } from '../utils/hostPlatform';
import { splitErrorMessage } from '../../utils/isomorphic/stackTrace';
import { PNG, jpegjs } from '../../utilsBundle';
import { navigatorPlatformFromUA } from '../browserContext';
import * as dialog from '../dialog';
import * as dom from '../dom';
import { TargetClosedError } from '../errors';
Expand Down Expand Up @@ -691,6 +692,12 @@ export class WKPage implements PageDelegate {
async updateUserAgent(): Promise<void> {
const contextOptions = this._browserContext._options;
this._updateState('Page.overrideUserAgent', { value: contextOptions.userAgent });
if (contextOptions.userAgent && !process.env.PLAYWRIGHT_NO_UA_PLATFORM) {
const platform = navigatorPlatformFromUA(contextOptions.userAgent);
this._updateState('Page.overridePlatform', platform ? { value: platform } : { });
} else {
this._updateState('Page.overridePlatform', { });
}
}

async bringToFront(): Promise<void> {
Expand Down
61 changes: 61 additions & 0 deletions tests/library/browsercontext-user-agent.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,38 @@ it('custom user agent for download', async ({ server, contextFactory, browserVer
expect(req.headers['user-agent']).toBe('MyCustomUA');
});

it('should override navigator.platform to match custom user agent', async ({ browser, server }) => {
{
const context = await browser.newContext({
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
});
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
expect.soft(await page.evaluate(() => navigator.platform)).toBe('Win32');
await context.close();
}

{
const context = await browser.newContext({
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
});
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
expect.soft(await page.evaluate(() => navigator.platform)).toBe('MacIntel');
await context.close();
}

{
const context = await browser.newContext({
userAgent: 'Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36',
});
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
expect.soft(await page.evaluate(() => navigator.platform)).toBe('Linux armv8l');
await context.close();
}
});

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');

Expand Down Expand Up @@ -139,6 +171,35 @@ it('should work for navigator.userAgentData and sec-ch-ua headers', async ({ pla
expect.soft(await page.evaluate(() => (window.navigator as any).userAgentData.toJSON())).toEqual(
expect.objectContaining({ mobile: true, platform: 'Android' })
);
expect.soft(await page.evaluate(() => navigator.platform)).toBe('Linux armv8l');
await context.close();
}

{
const context = await browser.newContext(playwright.devices['Desktop Chrome']);
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-platform']).toBe(`"Windows"`);
expect.soft(await page.evaluate(() => (window.navigator as any).userAgentData.toJSON())).toEqual(
expect.objectContaining({ mobile: false, platform: 'Windows' })
);
expect.soft(await page.evaluate(() => navigator.platform)).toBe('Win32');
await context.close();
}

{
const context = await browser.newContext({
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
});
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
expect.soft(await page.evaluate(() => (window.navigator as any).userAgentData.toJSON())).toEqual(
expect.objectContaining({ platform: 'macOS' })
);
expect.soft(await page.evaluate(() => navigator.platform)).toBe('MacIntel');
await context.close();
}
});
Loading