| /** |
| * @license |
| * Copyright 2023 Google Inc. |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| import fs from 'fs'; |
| import {rename, unlink, mkdtemp} from 'fs/promises'; |
| import os from 'os'; |
| import path from 'path'; |
| |
| import { |
| Browser as SupportedBrowsers, |
| createProfile, |
| Cache, |
| detectBrowserPlatform, |
| Browser, |
| } from '@puppeteer/browsers'; |
| |
| import {debugError} from '../common/util.js'; |
| import {assert} from '../util/assert.js'; |
| |
| import type { |
| BrowserLaunchArgumentOptions, |
| PuppeteerNodeLaunchOptions, |
| } from './LaunchOptions.js'; |
| import {ProductLauncher, type ResolvedLaunchArgs} from './ProductLauncher.js'; |
| import type {PuppeteerNode} from './PuppeteerNode.js'; |
| import {rm} from './util/fs.js'; |
| |
| /** |
| * @internal |
| */ |
| export class FirefoxLauncher extends ProductLauncher { |
| constructor(puppeteer: PuppeteerNode) { |
| super(puppeteer, 'firefox'); |
| } |
| |
| static getPreferences( |
| extraPrefsFirefox?: Record<string, unknown>, |
| protocol?: 'cdp' | 'webDriverBiDi' |
| ): Record<string, unknown> { |
| return { |
| ...extraPrefsFirefox, |
| ...(protocol === 'webDriverBiDi' |
| ? { |
| // Only enable the WebDriver BiDi protocol |
| 'remote.active-protocols': 1, |
| } |
| : { |
| // Do not close the window when the last tab gets closed |
| 'browser.tabs.closeWindowWithLastTab': false, |
| // Prevent various error message on the console |
| // jest-puppeteer asserts that no error message is emitted by the console |
| 'network.cookie.cookieBehavior': 0, |
| // Temporarily force disable BFCache in parent (https://bit.ly/bug-1732263) |
| 'fission.bfcacheInParent': false, |
| // Only enable the CDP protocol |
| 'remote.active-protocols': 2, |
| }), |
| // Force all web content to use a single content process. TODO: remove |
| // this once Firefox supports mouse event dispatch from the main frame |
| // context. Once this happens, webContentIsolationStrategy should only |
| // be set for CDP. See |
| // https://bugzilla.mozilla.org/show_bug.cgi?id=1773393 |
| 'fission.webContentIsolationStrategy': 0, |
| }; |
| } |
| |
| /** |
| * @internal |
| */ |
| override async computeLaunchArguments( |
| options: PuppeteerNodeLaunchOptions = {} |
| ): Promise<ResolvedLaunchArgs> { |
| const { |
| ignoreDefaultArgs = false, |
| args = [], |
| executablePath, |
| pipe = false, |
| extraPrefsFirefox = {}, |
| debuggingPort = null, |
| } = options; |
| |
| const firefoxArguments = []; |
| if (!ignoreDefaultArgs) { |
| firefoxArguments.push(...this.defaultArgs(options)); |
| } else if (Array.isArray(ignoreDefaultArgs)) { |
| firefoxArguments.push( |
| ...this.defaultArgs(options).filter(arg => { |
| return !ignoreDefaultArgs.includes(arg); |
| }) |
| ); |
| } else { |
| firefoxArguments.push(...args); |
| } |
| |
| if ( |
| !firefoxArguments.some(argument => { |
| return argument.startsWith('--remote-debugging-'); |
| }) |
| ) { |
| if (pipe) { |
| assert( |
| debuggingPort === null, |
| 'Browser should be launched with either pipe or debugging port - not both.' |
| ); |
| } |
| firefoxArguments.push(`--remote-debugging-port=${debuggingPort || 0}`); |
| } |
| |
| let userDataDir: string | undefined; |
| let isTempUserDataDir = true; |
| |
| // Check for the profile argument, which will always be set even |
| // with a custom directory specified via the userDataDir option. |
| const profileArgIndex = firefoxArguments.findIndex(arg => { |
| return ['-profile', '--profile'].includes(arg); |
| }); |
| |
| if (profileArgIndex !== -1) { |
| userDataDir = firefoxArguments[profileArgIndex + 1]; |
| if (!userDataDir || !fs.existsSync(userDataDir)) { |
| throw new Error(`Firefox profile not found at '${userDataDir}'`); |
| } |
| |
| // When using a custom Firefox profile it needs to be populated |
| // with required preferences. |
| isTempUserDataDir = false; |
| } else { |
| userDataDir = await mkdtemp(this.getProfilePath()); |
| firefoxArguments.push('--profile'); |
| firefoxArguments.push(userDataDir); |
| } |
| |
| await createProfile(SupportedBrowsers.FIREFOX, { |
| path: userDataDir, |
| preferences: FirefoxLauncher.getPreferences( |
| extraPrefsFirefox, |
| options.protocol |
| ), |
| }); |
| |
| let firefoxExecutable: string; |
| if (this.puppeteer._isPuppeteerCore || executablePath) { |
| assert( |
| executablePath, |
| `An \`executablePath\` must be specified for \`puppeteer-core\`` |
| ); |
| firefoxExecutable = executablePath; |
| } else { |
| firefoxExecutable = this.executablePath(); |
| } |
| |
| return { |
| isTempUserDataDir, |
| userDataDir, |
| args: firefoxArguments, |
| executablePath: firefoxExecutable, |
| }; |
| } |
| |
| /** |
| * @internal |
| */ |
| override async cleanUserDataDir( |
| userDataDir: string, |
| opts: {isTemp: boolean} |
| ): Promise<void> { |
| if (opts.isTemp) { |
| try { |
| await rm(userDataDir); |
| } catch (error) { |
| debugError(error); |
| throw error; |
| } |
| } else { |
| try { |
| // When an existing user profile has been used remove the user |
| // preferences file and restore possibly backuped preferences. |
| await unlink(path.join(userDataDir, 'user.js')); |
| |
| const prefsBackupPath = path.join(userDataDir, 'prefs.js.puppeteer'); |
| if (fs.existsSync(prefsBackupPath)) { |
| const prefsPath = path.join(userDataDir, 'prefs.js'); |
| await unlink(prefsPath); |
| await rename(prefsBackupPath, prefsPath); |
| } |
| } catch (error) { |
| debugError(error); |
| } |
| } |
| } |
| |
| override executablePath(): string { |
| // replace 'latest' placeholder with actual downloaded revision |
| if (this.puppeteer.browserRevision === 'latest') { |
| const cache = new Cache(this.puppeteer.defaultDownloadPath!); |
| const installedFirefox = cache.getInstalledBrowsers().find(browser => { |
| return ( |
| browser.platform === detectBrowserPlatform() && |
| browser.browser === Browser.FIREFOX |
| ); |
| }); |
| if (installedFirefox) { |
| this.actualBrowserRevision = installedFirefox.buildId; |
| } |
| } |
| return this.resolveExecutablePath(); |
| } |
| |
| override defaultArgs(options: BrowserLaunchArgumentOptions = {}): string[] { |
| const { |
| devtools = false, |
| headless = !devtools, |
| args = [], |
| userDataDir = null, |
| } = options; |
| |
| const firefoxArguments = ['--no-remote']; |
| |
| switch (os.platform()) { |
| case 'darwin': |
| firefoxArguments.push('--foreground'); |
| break; |
| case 'win32': |
| firefoxArguments.push('--wait-for-browser'); |
| break; |
| } |
| if (userDataDir) { |
| firefoxArguments.push('--profile'); |
| firefoxArguments.push(userDataDir); |
| } |
| if (headless) { |
| firefoxArguments.push('--headless'); |
| } |
| if (devtools) { |
| firefoxArguments.push('--devtools'); |
| } |
| if ( |
| args.every(arg => { |
| return arg.startsWith('-'); |
| }) |
| ) { |
| firefoxArguments.push('about:blank'); |
| } |
| firefoxArguments.push(...args); |
| return firefoxArguments; |
| } |
| } |