fix: match default font families in headless chromium (#11340)
This commit is contained in:
parent
d8db785c0a
commit
abd7084bcc
|
|
@ -40,7 +40,6 @@ export class CRBrowser extends Browser {
|
|||
_backgroundPages = new Map<string, CRPage>();
|
||||
_serviceWorkers = new Map<string, CRServiceWorker>();
|
||||
_devtools?: CRDevTools;
|
||||
_isMac = false;
|
||||
private _version = '';
|
||||
|
||||
private _tracingRecording = false;
|
||||
|
|
@ -49,6 +48,8 @@ export class CRBrowser extends Browser {
|
|||
private _userAgent: string = '';
|
||||
|
||||
static async connect(transport: ConnectionTransport, options: BrowserOptions, devtools?: CRDevTools): Promise<CRBrowser> {
|
||||
// Make a copy in case we need to update `headful` property below.
|
||||
options = { ...options };
|
||||
const connection = new CRConnection(transport, options.protocolLogger, options.browserLogsCollector);
|
||||
const browser = new CRBrowser(connection, options);
|
||||
browser._devtools = devtools;
|
||||
|
|
@ -57,9 +58,11 @@ export class CRBrowser extends Browser {
|
|||
await (options as any).__testHookOnConnectToBrowser();
|
||||
|
||||
const version = await session.send('Browser.getVersion');
|
||||
browser._isMac = version.userAgent.includes('Macintosh');
|
||||
browser._version = version.product.substring(version.product.indexOf('/') + 1);
|
||||
browser._userAgent = version.userAgent;
|
||||
// We don't trust the option as it may lie in case of connectOverCDP where remote browser
|
||||
// may have been launched with different options.
|
||||
browser.options.headful = !version.userAgent.includes('Headless');
|
||||
if (!options.persistent) {
|
||||
await session.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true });
|
||||
return browser;
|
||||
|
|
@ -123,6 +126,14 @@ export class CRBrowser extends Browser {
|
|||
return this._userAgent;
|
||||
}
|
||||
|
||||
_platform(): 'mac' | 'linux' | 'win' {
|
||||
if (this._userAgent.includes('Windows'))
|
||||
return 'win';
|
||||
if (this._userAgent.includes('Macintosh'))
|
||||
return 'mac';
|
||||
return 'linux';
|
||||
}
|
||||
|
||||
isClank(): boolean {
|
||||
return this.options.name === 'clank';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,32 +15,32 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import { eventsHelper, RegisteredListener } from '../../utils/eventsHelper';
|
||||
import { registry } from '../../utils/registry';
|
||||
import { rewriteErrorMessage } from '../../utils/stackTrace';
|
||||
import { assert, createGuid, headersArrayToObject } from '../../utils/utils';
|
||||
import * as dialog from '../dialog';
|
||||
import * as dom from '../dom';
|
||||
import * as frames from '../frames';
|
||||
import { helper } from '../helper';
|
||||
import { eventsHelper, RegisteredListener } from '../../utils/eventsHelper';
|
||||
import * as network from '../network';
|
||||
import { CRSession, CRConnection, CRSessionEvents } from './crConnection';
|
||||
import { CRExecutionContext } from './crExecutionContext';
|
||||
import { CRNetworkManager } from './crNetworkManager';
|
||||
import { Page, Worker, PageBinding } from '../page';
|
||||
import { Protocol } from './protocol';
|
||||
import { toConsoleMessageLocation, exceptionToError, releaseObject } from './crProtocolHelper';
|
||||
import * as dialog from '../dialog';
|
||||
import { PageDelegate } from '../page';
|
||||
import path from 'path';
|
||||
import { RawMouseImpl, RawKeyboardImpl, RawTouchscreenImpl } from './crInput';
|
||||
import { getAccessibilityTree } from './crAccessibility';
|
||||
import { CRCoverage } from './crCoverage';
|
||||
import { CRPDF } from './crPdf';
|
||||
import { CRBrowserContext } from './crBrowser';
|
||||
import * as types from '../types';
|
||||
import { rewriteErrorMessage } from '../../utils/stackTrace';
|
||||
import { assert, headersArrayToObject, createGuid } from '../../utils/utils';
|
||||
import { VideoRecorder } from './videoRecorder';
|
||||
import { Page, PageBinding, PageDelegate, Worker } from '../page';
|
||||
import { Progress } from '../progress';
|
||||
import * as types from '../types';
|
||||
import { getAccessibilityTree } from './crAccessibility';
|
||||
import { CRBrowserContext } from './crBrowser';
|
||||
import { CRConnection, CRSession, CRSessionEvents } from './crConnection';
|
||||
import { CRCoverage } from './crCoverage';
|
||||
import { DragManager } from './crDragDrop';
|
||||
import { registry } from '../../utils/registry';
|
||||
import { CRExecutionContext } from './crExecutionContext';
|
||||
import { RawKeyboardImpl, RawMouseImpl, RawTouchscreenImpl } from './crInput';
|
||||
import { CRNetworkManager } from './crNetworkManager';
|
||||
import { CRPDF } from './crPdf';
|
||||
import { exceptionToError, releaseObject, toConsoleMessageLocation } from './crProtocolHelper';
|
||||
import { platformToFontFamilies } from './defaultFontFamilies';
|
||||
import { Protocol } from './protocol';
|
||||
import { VideoRecorder } from './videoRecorder';
|
||||
|
||||
|
||||
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
|
||||
|
|
@ -79,7 +79,7 @@ export class CRPage implements PageDelegate {
|
|||
this._opener = opener;
|
||||
this._isBackgroundPage = isBackgroundPage;
|
||||
const dragManager = new DragManager(this);
|
||||
this.rawKeyboard = new RawKeyboardImpl(client, browserContext._browser._isMac, dragManager);
|
||||
this.rawKeyboard = new RawKeyboardImpl(client, browserContext._browser._platform() === 'mac', dragManager);
|
||||
this.rawMouse = new RawMouseImpl(this, client, dragManager);
|
||||
this.rawTouchscreen = new RawTouchscreenImpl(client);
|
||||
this._pdf = new CRPDF(client);
|
||||
|
|
@ -512,6 +512,8 @@ class FrameSession {
|
|||
promises.push(emulateLocale(this._client, options.locale));
|
||||
if (options.timezoneId)
|
||||
promises.push(emulateTimezone(this._client, options.timezoneId));
|
||||
if (!this._crPage._browserContext._browser.options.headful)
|
||||
promises.push(this._setDefaultFontFamilies(this._client));
|
||||
promises.push(this._updateGeolocation(true));
|
||||
promises.push(this._updateExtraHTTPHeaders(true));
|
||||
promises.push(this._updateRequestInterception());
|
||||
|
|
@ -1017,6 +1019,11 @@ class FrameSession {
|
|||
await this._client.send('Emulation.setEmulatedMedia', { media: this._page._state.mediaType || '', features });
|
||||
}
|
||||
|
||||
private async _setDefaultFontFamilies(session: CRSession) {
|
||||
const fontFamilies = platformToFontFamilies[this._crPage._browserContext._browser._platform()];
|
||||
await session.send('Page.setFontFamilies', fontFamilies);
|
||||
}
|
||||
|
||||
async _updateRequestInterception(): Promise<void> {
|
||||
await this._networkManager.setRequestInterception(this._page._needsRequestInterception());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,157 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Protocol } from './protocol';
|
||||
|
||||
// DO NOT EDIT: this map is generated from Chromium source code by utils/generate_chromium_default_font_families.js
|
||||
export const platformToFontFamilies: { [key in 'linux'|'mac'|'win']: Protocol.Page.setFontFamiliesParameters } = {
|
||||
'linux': {
|
||||
'fontFamilies': {
|
||||
'standard': 'Times New Roman',
|
||||
'fixed': 'Monospace',
|
||||
'serif': 'Times New Roman',
|
||||
'sansSerif': 'Arial',
|
||||
'cursive': 'Comic Sans MS',
|
||||
'fantasy': 'Impact',
|
||||
'pictograph': 'Times New Roman'
|
||||
}
|
||||
},
|
||||
'mac': {
|
||||
'fontFamilies': {
|
||||
'standard': 'Times',
|
||||
'fixed': 'Courier',
|
||||
'serif': 'Times',
|
||||
'sansSerif': 'Helvetica',
|
||||
'cursive': 'Apple Chancery',
|
||||
'fantasy': 'Papyrus',
|
||||
'pictograph': 'Apple Color Emoji'
|
||||
},
|
||||
'forScripts': [
|
||||
{
|
||||
'script': 'jpan',
|
||||
'fontFamilies': {
|
||||
'standard': 'Hiragino Kaku Gothic ProN',
|
||||
'fixed': 'Osaka-Mono',
|
||||
'serif': 'Hiragino Mincho ProN',
|
||||
'sansSerif': 'Hiragino Kaku Gothic ProN'
|
||||
}
|
||||
},
|
||||
{
|
||||
'script': 'hang',
|
||||
'fontFamilies': {
|
||||
'standard': 'Apple SD Gothic Neo',
|
||||
'serif': 'AppleMyungjo',
|
||||
'sansSerif': 'Apple SD Gothic Neo'
|
||||
}
|
||||
},
|
||||
{
|
||||
'script': 'hans',
|
||||
'fontFamilies': {
|
||||
'standard': ',PingFang SC,STHeiti',
|
||||
'serif': 'Songti SC',
|
||||
'sansSerif': ',PingFang SC,STHeiti',
|
||||
'cursive': 'Kaiti SC'
|
||||
}
|
||||
},
|
||||
{
|
||||
'script': 'hant',
|
||||
'fontFamilies': {
|
||||
'standard': ',PingFang TC,Heiti TC',
|
||||
'serif': 'Songti TC',
|
||||
'sansSerif': ',PingFang TC,Heiti TC',
|
||||
'cursive': 'Kaiti TC'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
'win': {
|
||||
'fontFamilies': {
|
||||
'standard': 'Times New Roman',
|
||||
'fixed': 'Consolas',
|
||||
'serif': 'Times New Roman',
|
||||
'sansSerif': 'Arial',
|
||||
'cursive': 'Comic Sans MS',
|
||||
'fantasy': 'Impact',
|
||||
'pictograph': 'Segoe UI Symbol'
|
||||
},
|
||||
'forScripts': [
|
||||
{
|
||||
'script': 'cyrl',
|
||||
'fontFamilies': {
|
||||
'standard': 'Times New Roman',
|
||||
'fixed': 'Courier New',
|
||||
'serif': 'Times New Roman',
|
||||
'sansSerif': 'Arial'
|
||||
}
|
||||
},
|
||||
{
|
||||
'script': 'arab',
|
||||
'fontFamilies': {
|
||||
'fixed': 'Courier New',
|
||||
'sansSerif': 'Segoe UI'
|
||||
}
|
||||
},
|
||||
{
|
||||
'script': 'grek',
|
||||
'fontFamilies': {
|
||||
'standard': 'Times New Roman',
|
||||
'fixed': 'Courier New',
|
||||
'serif': 'Times New Roman',
|
||||
'sansSerif': 'Arial'
|
||||
}
|
||||
},
|
||||
{
|
||||
'script': 'jpan',
|
||||
'fontFamilies': {
|
||||
'standard': ',Meiryo,Yu Gothic',
|
||||
'fixed': 'MS Gothic',
|
||||
'serif': ',Yu Mincho,MS PMincho',
|
||||
'sansSerif': ',Meiryo,Yu Gothic'
|
||||
}
|
||||
},
|
||||
{
|
||||
'script': 'hang',
|
||||
'fontFamilies': {
|
||||
'standard': 'Malgun Gothic',
|
||||
'fixed': 'Gulimche',
|
||||
'serif': 'Batang',
|
||||
'sansSerif': 'Malgun Gothic',
|
||||
'cursive': 'Gungsuh'
|
||||
}
|
||||
},
|
||||
{
|
||||
'script': 'hans',
|
||||
'fontFamilies': {
|
||||
'standard': 'Microsoft YaHei',
|
||||
'fixed': 'NSimsun',
|
||||
'serif': 'Simsun',
|
||||
'sansSerif': 'Microsoft YaHei',
|
||||
'cursive': 'KaiTi'
|
||||
}
|
||||
},
|
||||
{
|
||||
'script': 'hant',
|
||||
'fontFamilies': {
|
||||
'standard': 'Microsoft JhengHei',
|
||||
'fixed': 'MingLiU',
|
||||
'serif': 'PMingLiU',
|
||||
'sansSerif': 'Microsoft JhengHei',
|
||||
'cursive': 'DFKai-SB'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
117
utils/generate_chromium_default_font_families.js
Normal file
117
utils/generate_chromium_default_font_families.js
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// @ts-check
|
||||
const xml2js = require('xml2js');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { argv } = require('process');
|
||||
|
||||
// From https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/prefs/prefs_tab_helper.cc;l=130;drc=62b77bef90de54f0136b51935fa2d5814a1b4da9
|
||||
// and https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/platform/text/locale_to_script_mapping.cc;l=44;drc=befcb6de95fb8c88c162ce1f64111f6c17351b13
|
||||
// note that some suffixes like _JAPANESE, _KOREAN don't have matching icu codes.
|
||||
const codeToScriptName = new Map([
|
||||
['ARABIC', 'arab'],
|
||||
['CYRILLIC', 'cyrl'],
|
||||
['GREEK', 'grek'],
|
||||
['JAPANESE', 'jpan'],
|
||||
['KOREAN', 'hang'],
|
||||
['SIMPLIFIED_HAN', 'hans'],
|
||||
['TRADITIONAL_HAN', 'hant'],
|
||||
]);
|
||||
|
||||
const idToProtocol = new Map([
|
||||
['IDS_STANDARD_FONT_FAMILY', 'standard'],
|
||||
['IDS_SANS_SERIF_FONT_FAMILY','sansSerif'],
|
||||
['IDS_SERIF_FONT_FAMILY', 'serif'],
|
||||
['IDS_CURSIVE_FONT_FAMILY', 'cursive'],
|
||||
['IDS_FANTASY_FONT_FAMILY', 'fantasy'],
|
||||
['IDS_FIXED_FONT_FAMILY', 'fixed'],
|
||||
['IDS_PICTOGRAPH_FONT_FAMILY', 'pictograph']
|
||||
]);
|
||||
|
||||
class ScriptFontFamilies {
|
||||
scriptToFontFamilies = new Map();
|
||||
|
||||
setFont(scriptName, familyName, value) {
|
||||
let fontFamilies = this.scriptToFontFamilies.get(scriptName);
|
||||
if (!fontFamilies) {
|
||||
fontFamilies = {};
|
||||
this.scriptToFontFamilies.set(scriptName, fontFamilies);
|
||||
}
|
||||
fontFamilies[familyName] = value;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const forScripts = Array.from(this.scriptToFontFamilies.entries()).filter(([name, _]) => !!name).map(([script, fontFamilies]) => ({ script, fontFamilies }));
|
||||
return {
|
||||
fontFamilies: this.scriptToFontFamilies.get(''),
|
||||
forScripts: forScripts.length ? forScripts : undefined
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (argv.length < 3)
|
||||
throw new Error('Expected path to "chromium/src" checkout as first argument')
|
||||
|
||||
// Upstream files location is https://chromium.googlesource.com/chromium/src/+/main/chrome/app/resources/locale_settings_linux.grd
|
||||
const resourceDir = path.join(argv[2], 'chrome/app/resources/');
|
||||
if (!fs.existsSync(resourceDir))
|
||||
throw new Error(`Path ${resourceDir} does not exist`);
|
||||
|
||||
function parseXML(xml) {
|
||||
let result;
|
||||
xml2js.parseString(xml, {trim: true}, (err, r) => result = r);
|
||||
return result;
|
||||
}
|
||||
|
||||
const result = {};
|
||||
for (const platform of ['linux', 'mac', 'win']) {
|
||||
const f = path.join(resourceDir, `locale_settings_${platform}.grd`);
|
||||
const xmlDataStr = fs.readFileSync(f);
|
||||
let jsonObj = parseXML(xmlDataStr);
|
||||
if (!jsonObj)
|
||||
throw new Error('Failed to parse ' + f);
|
||||
const fontFamilies = new ScriptFontFamilies();
|
||||
const defaults = jsonObj.grit.release[0].messages[0].message;
|
||||
defaults.forEach(e => {
|
||||
const name = e['$']['name'];
|
||||
let scriptName = '';
|
||||
let familyName;
|
||||
for (const id of idToProtocol.keys()) {
|
||||
if (!name.startsWith(id))
|
||||
continue;
|
||||
familyName = idToProtocol.get(id);
|
||||
if (name !== id) {
|
||||
const suffix = name.substring(id.length + 1);
|
||||
// We don't support this, see https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/prefs/prefs_tab_helper.cc;l=384-390;drc=62b77bef90de54f0136b51935fa2d5814a1b4da9
|
||||
if (suffix === 'ALT_WIN')
|
||||
continue;
|
||||
scriptName = codeToScriptName.get(suffix);
|
||||
if (!scriptName)
|
||||
throw new Error('NO Script name for: ' + suffix);
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Skip things like IDS_NTP_FONT_FAMILY, IDS_MINIMUM_FONT_SIZE etc.
|
||||
if (!familyName)
|
||||
return;
|
||||
fontFamilies.setFont(scriptName, familyName, e['_'])
|
||||
});
|
||||
result[platform] = fontFamilies.toJSON();
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(result, null, 2).replaceAll('"', `'`));
|
||||
Loading…
Reference in a new issue