/** * Seeded Chrome profile for Puppeteer tests. * * Why this exists (#4430) * ======================= * The CI test credentials (test_admin / testpass123) appear in Chrome's * breached-credential database. After a SUCCESSFUL login with such a pair, * Chrome's password leak detection opens a tab-modal "password found in a * data breach" dialog. In headless the dialog is invisible, but it still * swallows ALL real (CDP-dispatched) mouse and keyboard input for that tab, * permanently: page.type()/page.click() silently do nothing on every page * visited after login, while page.evaluate(), Input.insertText and JS * .click() keep working. That is exactly the "CDP input not delivered to * /chat/ in headless" failure — /chat/ was a red herring; any tab that * performed the login is affected (failed logins and fresh tabs are not). * * Leak detection has no working command-line switch in current Chrome * (--disable-features=PasswordLeakDetection no longer has any effect), so * the only reliable off-switch is the profile preference * profile.password_manager_leak_detection. This helper creates a throwaway * user-data-dir with that pref (plus credentials_enable_service=false to * also suppress the save-password bubble) for puppeteer.launch(). */ const fs = require('fs'); const os = require('os'); const path = require('path'); const createdDirs = []; /** * Create a temporary Chrome user-data-dir whose Default profile disables * password leak detection and the password manager. Each call returns a * fresh directory, so concurrent browser launches never fight over a * profile lock. Directories are best-effort removed on process exit. * * @returns {string} Absolute path to pass as puppeteer's userDataDir. */ function createSeededChromeProfile() { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ldr-chrome-profile-')); fs.mkdirSync(path.join(dir, 'Default'), { recursive: true }); fs.writeFileSync( path.join(dir, 'Default', 'Preferences'), JSON.stringify({ // Pref names map to JSON paths by their dots, so the two // password-manager prefs land at different depths on purpose: // kCredentialsEnableService is registered as the top-level // "credentials_enable_service", while leak detection is // "profile.password_manager_leak_detection". profile: { password_manager_leak_detection: false, password_manager_enabled: false, }, credentials_enable_service: false, }) ); createdDirs.push(dir); return dir; } function cleanupProfiles() { for (const dir of createdDirs.splice(0)) { try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_) { // Best effort — CI containers are ephemeral anyway. } } } process.on('exit', cleanupProfiles); // 'exit' does not fire on signals (local Ctrl-C, CI step timeouts). // Clean up, then re-raise so the default termination behavior — and any // other handlers' exit codes — still apply. for (const sig of ['SIGINT', 'SIGTERM']) { process.once(sig, () => { cleanupProfiles(); process.kill(process.pid, sig); }); } module.exports = { createSeededChromeProfile };