blob: 67769b5ee05d788c39462bdb407e166248358d3a [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import * as url from 'node:url';
import * as puppeteer from 'puppeteer-core';
import {setupBrowserProcessIO} from '../../conductor/events.js';
import {GEN_DIR} from '../../conductor/paths.js';
import {TestConfig} from '../../conductor/test_config.js';
export class BrowserWrapper {
browser: puppeteer.Browser;
constructor(b: puppeteer.Browser) {
this.browser = b;
}
get connected() {
return this.browser.connected;
}
async createBrowserContext() {
return await this.browser.createBrowserContext();
}
copyCrashDumps() {
const crashesPath = this.#getCrashpadDir();
if (!fs.existsSync(crashesPath)) {
// TODO (liviurau): Determine where exactly does Crashpad store the dumps on
// Linux and Windows.
console.error('No crash dumps found at location ', crashesPath);
return;
}
for (const file of fs.readdirSync(crashesPath)) {
console.error('Collecting crash dump:', file);
fs.copyFileSync(path.join(crashesPath, file), path.join(TestConfig.artifactsDir, file));
}
}
#getCrashpadDir() {
// TODO (liviurau): generate a tmp dir and pass when launching puppeteer
// instead of parsing it out of args
const userDataArg = this.browser.process()?.spawnargs.find(arg => arg.startsWith('--user-data-dir='));
if (userDataArg) {
const configuredPath = path.join(userDataArg.split('=')[1], 'Crashpad', 'pending');
// `--user-data-dir` generally does not contain Craspad files on any
// platform. In the future this might get properly aligned so we search
// here first.
if (fs.existsSync(configuredPath)) {
return configuredPath;
}
}
const homeDir = os.homedir();
const platform = os.platform();
switch (platform) {
case 'darwin':
return path.join(
homeDir, 'Library', 'Application Support', 'Google', 'Chrome for Testing', 'Crashpad', 'pending');
case 'win32': {
const localAppData = path.join(
process.env.LOCALAPPDATA ?? '', 'Google', 'Chrome for Testing', 'User Data', 'Crashpad', 'pending');
if (fs.existsSync(localAppData)) {
return localAppData;
}
return path.join(
homeDir, 'AppData', 'Local', 'Google', 'Chrome for Testing', 'User Data', 'Crashpad', 'pending');
}
case 'linux':
return path.join(homeDir, '.config', 'google-chrome-for-testing', 'Crashpad', 'pending');
default:
throw new Error(`Unsupported platform: ${platform}`);
}
}
}
export class Launcher {
static async browserSetup(settings: BrowserSettings, serverPort: number) {
const browser = await Launcher.launchChrome(settings, serverPort);
setupBrowserProcessIO(browser);
return new BrowserWrapper(browser);
}
private static launchChrome(settings: BrowserSettings, serverPort: number) {
const frontEndDirectory = url.pathToFileURL(path.join(GEN_DIR, 'front_end'));
const disabledFeatures = settings.disabledFeatures?.slice() ?? [];
const launchArgs = [
'--remote-allow-origins=*',
'--remote-debugging-port=0',
'--enable-experimental-web-platform-features',
// This fingerprint may be generated from the certificate using
// openssl x509 -noout -pubkey -in scripts/hosted_mode/cert.pem | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64
'--ignore-certificate-errors-spki-list=KLy6vv6synForXwI6lDIl+D3ZrMV6Y1EMTY6YpOcAos=',
'--site-per-process', // Default on Desktop anyway, but ensure that we always use out-of-process frames when we intend to.
'--host-resolver-rules=MAP *.test 127.0.0.1',
'--disable-gpu',
`--disable-features=${disabledFeatures.join(',')}`,
`--custom-devtools-frontend=${frontEndDirectory}`,
'--enable-crash-reporter',
// This has no effect (see https://crbug.com/435638630)
`--crash-dumps-dir=${TestConfig.artifactsDir}`,
`--privacy-sandbox-enrollment-overrides=https://localhost:${serverPort}`,
];
const headless = TestConfig.headless;
// CDP commands in e2e and interaction should not generally take
// more than 20 seconds.
const protocolTimeout = TestConfig.debug ? 0 : 20_000;
const executablePath = TestConfig.chromeBinary;
const opts: puppeteer.LaunchOptions = {
headless,
executablePath,
dumpio: !headless || Boolean(process.env['LUCI_CONTEXT']),
protocolTimeout,
networkEnabled: false,
pipe: true,
ignoreDefaultArgs: [
'--disable-crash-reporter',
'--disable-breakpad',
],
};
TestConfig.configureChrome(executablePath);
const viewportWidth = 1280;
const viewportHeight = 720;
// Adding some offset to the window size used in the headful mode
// so to account for the size of the browser UI.
// Values are chosen by trial and error to make sure that the window
// size is not much bigger than the viewport but so that the entire
// viewport is visible.
const windowWidth = viewportWidth + 50;
const windowHeight = viewportHeight + 200;
// Always set the default viewport because setting only the window size for
// headful mode would result in much smaller actual viewport.
opts.defaultViewport = {width: viewportWidth, height: viewportHeight};
// Toggle either viewport or window size depending on headless vs not.
if (!headless) {
launchArgs.push(`--window-size=${windowWidth},${windowHeight}`);
}
const enabledFeatures = settings.enabledFeatures?.slice() ?? [];
// TODO: remove
const envChromeFeatures = process.env['CHROME_FEATURES'];
if (envChromeFeatures) {
enabledFeatures.push(envChromeFeatures);
}
launchArgs.push(`--enable-features=${enabledFeatures.join(',')}`);
opts.args = launchArgs;
return puppeteer.launch(opts);
}
}
export interface BrowserSettings {
enabledFeatures: string[];
disabledFeatures: string[];
}
export const DEFAULT_BROWSER_SETTINGS: BrowserSettings = {
// LINT.IfChange(features)
enabledFeatures: [
'PartitionedCookies',
'SharedStorageAPI',
'FencedFrames',
'PrivacySandboxAdsAPIsOverride',
'AutofillEnableDevtoolsIssues',
'DevToolsVeLogging:testing/true',
'CADisplayLink',
],
disabledFeatures: [
'PMProcessPriorityPolicy', // crbug.com/361252079
'MojoChannelAssociatedSendUsesRunOrPostTask', // crbug.com/376228320
'RasterInducingScroll', // crbug.com/381055647
'CompositeBackgroundColorAnimation', // crbug.com/381055647
'ScriptSrcHashesV1', // crbug.com/443216445
'RenderDocument', // crbug.com/444369637
]
// LINT.ThenChange(/test/conductor/hooks.ts:features)
};