blob: ef7d7eefec36f926c434af070d1ecb5d71d2e0d8 [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import { once } from 'node:events';
import puppeteer from 'puppeteer-core';
import { chromeLogin } from '../test/e2e/shared/auth-helper.ts';
/**
* Splits positional rest arguments into extra Chrome flags (`--foo`) and target URLs.
*
* @param {Array<string|number>} restArgs
*/
export function splitRestArgs(restArgs) {
/** @type {string[]} */
const extraChromeFlags = [];
/** @type {string[]} */
const targetUrls = [];
for (const arg of restArgs) {
if (typeof arg !== 'string' || arg.length === 0) {
continue;
}
if (arg.startsWith('-')) {
extraChromeFlags.push(arg);
} else {
targetUrls.push(arg);
}
}
return { extraChromeFlags, targetUrls };
}
/**
* Launches Chrome via Puppeteer, signs into the requested OTA Google account,
* opens requested target URLs (or a fresh New Tab page), disconnects CDP, and
* keeps the session alive until Chrome exits.
*
* @param {{
* binary: string,
* baseArgs: string[],
* env?: NodeJS.ProcessEnv,
* extraChromeFlags?: string[],
* loginFn?: typeof chromeLogin,
* onSignedIn?: (context: {browserProcess: import('node:child_process').ChildProcess|null, profileDir: string}) => Promise<void>|void,
* otaPassword?: string,
* otaUsername: string,
* remoteDebuggingPort?: number,
* targetUrls?: string[],
* userDataDir: string,
* verbose?: boolean,
* }} options
*/
export async function launchWithOtaSignIn(options) {
const {
binary,
baseArgs,
env = process.env,
extraChromeFlags = [],
loginFn = chromeLogin,
onSignedIn,
otaPassword = '',
otaUsername,
remoteDebuggingPort,
targetUrls = [],
userDataDir,
verbose = false,
} = options;
if (!userDataDir) {
throw new Error(
'The `--ota-username` command line switch must be accompanied by the `--user-data-dir` switch.',
);
}
const launchArgs = [
...baseArgs,
`--user-data-dir=${userDataDir}`,
'--auto-accept-browser-signin-for-tests',
'--allow-browser-signin=true',
...extraChromeFlags,
'about:blank',
];
if (verbose) {
console.debug(
'Launch Chrome with OTA sign-in: %s %s',
binary,
launchArgs.join(' '),
);
}
const browser = await puppeteer.launch({
executablePath: binary,
ignoreDefaultArgs: true,
pipe: remoteDebuggingPort === undefined,
defaultViewport: null,
dumpio: verbose,
env,
args: launchArgs,
});
try {
const pages = await browser.pages();
const loginPage = pages[0] ?? (await browser.newPage());
console.log(`Signing in to Chrome as ${otaUsername}...`);
await loginFn(loginPage, otaUsername, otaPassword || undefined);
// Open target tabs before closing `loginPage` so Chrome never observes
// zero open tabs and exits prematurely.
const urlsToOpen =
targetUrls.length > 0 ? targetUrls : ['chrome://newtab/'];
const cdp = await browser.target().createCDPSession();
try {
for (const targetUrl of urlsToOpen) {
await cdp.send('Target.createTarget', { url: targetUrl });
}
} finally {
await cdp.detach().catch(() => {});
}
await loginPage.close().catch(() => {});
console.log(`Signed in to Chrome as ${otaUsername}.`);
} catch (error) {
await browser.close().catch(() => {});
throw error;
}
// Puppeteer is only needed for the initial few seconds to run `chromeLogin()`
// and open the initial tab(s). After sign-in completes, the developer uses
// Chrome interactively to test DevTools. Leaving Puppeteer connected for the
// whole session keeps `TargetManager`'s `Target.setAutoAttach`
// (`waitForDebuggerOnStart: true`) and `Target.setDiscoverTargets` active
// across every tab/worker/DevTools target the user opens, which can
// interfere with DevTools' own CDP sessions and accumulate state in Node.
//
// Disarm TargetManager's auto-attach and target discovery on the root
// connection before disconnecting so pipe-mode sessions (whose fds remain
// open while Chrome runs) do not pause newly created non-page targets.
const rootConnection =
/** @type {{send?: (method: string, params?: object) => Promise<unknown>}} */ (
/** @type {unknown} */ (browser)
)._connection;
if (rootConnection && typeof rootConnection.send === 'function') {
await rootConnection
.send('Target.setAutoAttach', {
autoAttach: false,
waitForDebuggerOnStart: false,
flatten: true,
})
.catch(() => {});
await rootConnection
.send('Target.setDiscoverTargets', {
discover: false,
})
.catch(() => {});
}
const browserProcess = browser.process();
await browser.disconnect();
if (onSignedIn) {
await onSignedIn({
browserProcess,
profileDir: userDataDir,
});
}
if (
browserProcess &&
browserProcess.exitCode === null &&
browserProcess.signalCode === null
) {
await once(browserProcess, 'exit');
}
}