| // 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 {execSync} from 'node:child_process'; | |
| import type * as puppeteer from 'puppeteer-core'; | |
| const MAX_LOGIN_ATTEMPTS = 2; | |
| export async function chromeLogin(page: puppeteer.Page, username: string, password?: string): Promise<void> { | |
| for (let attempt = 1; attempt <= MAX_LOGIN_ATTEMPTS; attempt++) { | |
| try { | |
| await page.goto('https://accounts.google.com', {waitUntil: 'networkidle2'}); | |
| if (page.url().includes('myaccount.google.com')) { | |
| return; | |
| } | |
| await fillGoogleLoginPage(page, username, password); | |
| await page.waitForFunction(() => window.location.href.includes('myaccount.google.com')); | |
| return; | |
| } catch (error) { | |
| if (attempt < MAX_LOGIN_ATTEMPTS) { | |
| console.warn(`Google login attempt ${attempt}/${MAX_LOGIN_ATTEMPTS} failed; retrying...`, error); | |
| continue; | |
| } | |
| throw error; | |
| } | |
| } | |
| } | |
| export async function fillGoogleLoginPage(page: puppeteer.Page, username: string, password?: string): Promise<void> { | |
| const resolvedPassword = password ?? getOtaPassword(username); | |
| const nextBtn = await page.waitForSelector('#identifierNext, #passwordNext', {visible: true}); | |
| if (!nextBtn) { | |
| throw new Error('Could not find Next button during login'); | |
| } | |
| const isIdentifier = await nextBtn.evaluate((el: Element) => el.id === 'identifierNext'); | |
| if (isIdentifier) { | |
| await typeTextIn(page, 'input[name="identifier"]', username); | |
| await page.click('#identifierNext'); | |
| } | |
| const renderedInput = await page.waitForSelector('input[type="password"], input[name="contact"]', {visible: true}); | |
| const isContactChallenge = | |
| await renderedInput?.evaluate((el: Element) => (el as HTMLInputElement).name === 'contact'); | |
| if (isContactChallenge) { | |
| throw new Error('Account verification from trusted contact required!'); | |
| } | |
| await typeTextIn(page, 'input[type="password"]', resolvedPassword); | |
| await page.click('#passwordNext'); | |
| } | |
| export async function typeTextIn(page: puppeteer.Page, selector: string, value: string): Promise<void> { | |
| await page.waitForSelector(selector, {visible: true}); | |
| await page.focus(selector); | |
| await page.keyboard.type(value); | |
| } | |
| export async function configureDevToolsPreferences(page: puppeteer.Page): Promise<void> { | |
| const devtoolsPage = await page.openDevTools(); | |
| if (!devtoolsPage) { | |
| throw new Error('Could not open DevTools page!'); | |
| } | |
| await devtoolsPage.waitForFunction(() => window.location.origin.startsWith('devtools://'), {timeout: 1000}) | |
| .catch(() => {}); | |
| await devtoolsPage.evaluate(() => { | |
| localStorage.setItem('devToolsAiDebug', JSON.stringify({assistance: true, gca: false, codeGenComplete: false})); | |
| }); | |
| await devtoolsPage.close().catch((e: unknown) => console.error('Failed to close DevTools page:', e)); | |
| } | |
| export function getOtaPassword(email: string): string { | |
| const cmd = | |
| `stubby call blade:identity-testaccount-service-prod TestaccountService.ChangeTestAccountPassword 'email: "${ | |
| email}" generate_string_password: true' --output_json`; | |
| try { | |
| const output = execSync(cmd, {encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe']}); | |
| const parsed = JSON.parse(output); | |
| if (!parsed || typeof parsed !== 'object' || !parsed.string_password) { | |
| throw new Error(`Could not find string_password in stubby JSON output:\n${output}`); | |
| } | |
| return parsed.string_password; | |
| } catch (error) { | |
| console.error(`Failed to get password for ${email}.`); | |
| if (error && typeof error === 'object' && 'stderr' in error) { | |
| console.error(`stubby error:\n${error.stderr}`); | |
| } | |
| throw error; | |
| } | |
| } |