| // Licensed to the Software Freedom Conservancy (SFC) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The SFC licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| /** |
| * @fileoverview Defines the {@linkplain Driver WebDriver} client for Firefox. |
| * Before using this module, you must download the latest |
| * [geckodriver release] and ensure it can be found on your system [PATH]. |
| * |
| * Each FirefoxDriver instance will be created with an anonymous profile, |
| * ensuring browser historys do not share session data (cookies, history, cache, |
| * offline storage, etc.) |
| * |
| * __Customizing the Firefox Profile__ |
| * |
| * The profile used for each WebDriver session may be configured using the |
| * {@linkplain Options} class. For example, you may install an extension, like |
| * Firebug: |
| * |
| * const {Builder} = require('selenium-webdriver'); |
| * const firefox = require('selenium-webdriver/firefox'); |
| * |
| * let options = new firefox.Options() |
| * .addExtensions('/path/to/firebug.xpi') |
| * .setPreference('extensions.firebug.showChromeErrors', true); |
| * |
| * let driver = new Builder() |
| * .forBrowser('firefox') |
| * .setFirefoxOptions(options) |
| * .build(); |
| * |
| * The {@linkplain Options} class may also be used to configure WebDriver based |
| * on a pre-existing browser profile: |
| * |
| * let profile = '/usr/local/home/bob/.mozilla/firefox/3fgog75h.testing'; |
| * let options = new firefox.Options().setProfile(profile); |
| * |
| * The FirefoxDriver will _never_ modify a pre-existing profile; instead it will |
| * create a copy for it to modify. By extension, there are certain browser |
| * preferences that are required for WebDriver to function properly and they |
| * will always be overwritten. |
| * |
| * __Using a Custom Firefox Binary__ |
| * |
| * On Windows and MacOS, the FirefoxDriver will search for Firefox in its |
| * default installation location: |
| * |
| * - Windows: C:\Program Files and C:\Program Files (x86). |
| * - MacOS: /Applications/Firefox.app |
| * |
| * For Linux, Firefox will always be located on the PATH: `$(where firefox)`. |
| * |
| * You can provide a custom location for Firefox by setting the binary in the |
| * {@link Options}:setBinary method. |
| * |
| * const {Builder} = require('selenium-webdriver'); |
| * const firefox = require('selenium-webdriver/firefox'); |
| * |
| * let options = new firefox.Options() |
| * .setBinary('/my/firefox/install/dir/firefox'); |
| * let driver = new Builder() |
| * .forBrowser('firefox') |
| * .setFirefoxOptions(options) |
| * .build(); |
| * |
| * __Remote Testing__ |
| * |
| * You may customize the Firefox binary and profile when running against a |
| * remote Selenium server. Your custom profile will be packaged as a zip and |
| * transferred to the remote host for use. The profile will be transferred |
| * _once for each new session_. The performance impact should be minimal if |
| * you've only configured a few extra browser preferences. If you have a large |
| * profile with several extensions, you should consider installing it on the |
| * remote host and defining its path via the {@link Options} class. Custom |
| * binaries are never copied to remote machines and must be referenced by |
| * installation path. |
| * |
| * const {Builder} = require('selenium-webdriver'); |
| * const firefox = require('selenium-webdriver/firefox'); |
| * |
| * let options = new firefox.Options() |
| * .setProfile('/profile/path/on/remote/host') |
| * .setBinary('/install/dir/on/remote/host/firefox'); |
| * |
| * let driver = new Builder() |
| * .forBrowser('firefox') |
| * .usingServer('http://127.0.0.1:4444/wd/hub') |
| * .setFirefoxOptions(options) |
| * .build(); |
| * |
| * [geckodriver release]: https://github.com/mozilla/geckodriver/releases/ |
| * [PATH]: http://en.wikipedia.org/wiki/PATH_%28variable%29 |
| * |
| * @module selenium-webdriver/firefox |
| */ |
| |
| 'use strict' |
| |
| const fs = require('node:fs') |
| const path = require('node:path') |
| const Symbols = require('./lib/symbols') |
| const command = require('./lib/command') |
| const http = require('./http') |
| const io = require('./io') |
| const remote = require('./remote') |
| const webdriver = require('./lib/webdriver') |
| const zip = require('./io/zip') |
| const { Browser, Capabilities, Capability } = require('./lib/capabilities') |
| const { Zip } = require('./io/zip') |
| const { getBinaryPaths } = require('./common/driverFinder') |
| const { findFreePort } = require('./net/portprober') |
| const FIREFOX_CAPABILITY_KEY = 'moz:firefoxOptions' |
| |
| /** |
| * Thrown when there an add-on is malformed. |
| * @final |
| */ |
| class AddonFormatError extends Error { |
| /** @param {string} msg The error message. */ |
| constructor(msg) { |
| super(msg) |
| /** @override */ |
| this.name = this.constructor.name |
| } |
| } |
| |
| /** |
| * Installs an extension to the given directory. |
| * @param {string} extension Path to the xpi extension file to install. |
| * @param {string} dir Path to the directory to install the extension in. |
| * @return {!Promise<string>} A promise for the add-on ID once |
| * installed. |
| */ |
| async function installExtension(extension, dir) { |
| const ext = extension.slice(-4) |
| if (ext !== '.xpi' && ext !== '.zip') { |
| throw Error('File name does not end in ".zip" or ".xpi": ' + ext) |
| } |
| |
| let archive = await zip.load(extension) |
| if (!archive.has('manifest.json')) { |
| throw new AddonFormatError(`Couldn't find manifest.json in ${extension}`) |
| } |
| |
| let buf = await archive.getFile('manifest.json') |
| let parsedJSON = JSON.parse(buf.toString('utf8')) |
| |
| let { browser_specific_settings } = |
| /** @type {{browser_specific_settings:{gecko:{id:string}}}} */ |
| parsedJSON |
| |
| if (browser_specific_settings && browser_specific_settings.gecko) { |
| /* browser_specific_settings is an alternative to applications |
| * It is meant to facilitate cross-browser plugins since Firefox48 |
| * see https://bugzilla.mozilla.org/show_bug.cgi?id=1262005 |
| */ |
| parsedJSON.applications = browser_specific_settings |
| } |
| |
| let { applications } = |
| /** @type {{applications:{gecko:{id:string}}}} */ |
| parsedJSON |
| if (!(applications && applications.gecko && applications.gecko.id)) { |
| throw new AddonFormatError(`Could not find add-on ID for ${extension}`) |
| } |
| |
| await io.copy(extension, `${path.join(dir, applications.gecko.id)}.xpi`) |
| return applications.gecko.id |
| } |
| |
| class Profile { |
| constructor() { |
| /** @private {?string} */ |
| this.template_ = null |
| |
| /** @private {!Array<string>} */ |
| this.extensions_ = [] |
| } |
| |
| addExtensions(/** !Array<string> */ paths) { |
| this.extensions_ = this.extensions_.concat(...paths) |
| } |
| |
| /** |
| * @return {(!Promise<string>|undefined)} a promise for a base64 encoded |
| * profile, or undefined if there's no data to include. |
| */ |
| [Symbols.serialize]() { |
| if (this.template_ || this.extensions_.length) { |
| return buildProfile(this.template_, this.extensions_) |
| } |
| return undefined |
| } |
| } |
| |
| /** |
| * @param {?string} template path to an existing profile to use as a template. |
| * @param {!Array<string>} extensions paths to extensions to install in the new |
| * profile. |
| * @return {!Promise<string>} a promise for the base64 encoded profile. |
| */ |
| async function buildProfile(template, extensions) { |
| let dir = template |
| |
| if (extensions.length) { |
| dir = await io.tmpDir() |
| if (template) { |
| await io.copyDir(/** @type {string} */ (template), dir, /(parent\.lock|lock|\.parentlock)/) |
| } |
| |
| const extensionsDir = path.join(dir, 'extensions') |
| await io.mkdir(extensionsDir) |
| |
| for (let i = 0; i < extensions.length; i++) { |
| await installExtension(extensions[i], extensionsDir) |
| } |
| } |
| |
| let zip = new Zip() |
| return zip |
| .addDir(dir) |
| .then(() => zip.toBuffer()) |
| .then((buf) => buf.toString('base64')) |
| } |
| |
| /** |
| * Configuration options for the FirefoxDriver. |
| */ |
| class Options extends Capabilities { |
| /** |
| * @param {(Capabilities|Map<string, ?>|Object)=} other Another set of |
| * capabilities to initialize this instance from. |
| */ |
| constructor(other) { |
| super(other) |
| this.setBrowserName(Browser.FIREFOX) |
| // https://fxdx.dev/deprecating-cdp-support-in-firefox-embracing-the-future-with-webdriver-bidi/. |
| // Enable BiDi only |
| this.setPreference('remote.active-protocols', 1) |
| } |
| |
| /** |
| * @return {!Object} |
| * @private |
| */ |
| firefoxOptions_() { |
| let options = this.get(FIREFOX_CAPABILITY_KEY) |
| if (!options) { |
| options = {} |
| this.set(FIREFOX_CAPABILITY_KEY, options) |
| } |
| return options |
| } |
| |
| /** |
| * @return {!Profile} |
| * @private |
| */ |
| profile_() { |
| let options = this.firefoxOptions_() |
| if (!options.profile) { |
| options.profile = new Profile() |
| } |
| return options.profile |
| } |
| |
| /** |
| * Specify additional command line arguments that should be used when starting |
| * the Firefox browser. |
| * |
| * @param {...(string|!Array<string>)} args The arguments to include. |
| * @return {!Options} A self reference. |
| */ |
| addArguments(...args) { |
| if (args.length) { |
| let options = this.firefoxOptions_() |
| options.args = options.args ? options.args.concat(...args) : args |
| } |
| return this |
| } |
| |
| /** |
| * Sets the initial window size |
| * |
| * @param {{width: number, height: number}} size The desired window size. |
| * @return {!Options} A self reference. |
| * @throws {TypeError} if width or height is unspecified, not a number, or |
| * less than or equal to 0. |
| */ |
| windowSize({ width, height }) { |
| function checkArg(arg) { |
| if (typeof arg !== 'number' || arg <= 0) { |
| throw TypeError('Arguments must be {width, height} with numbers > 0') |
| } |
| } |
| |
| checkArg(width) |
| checkArg(height) |
| return this.addArguments(`--width=${width}`, `--height=${height}`) |
| } |
| |
| /** |
| * Add extensions that should be installed when starting Firefox. |
| * |
| * @param {...string} paths The paths to the extension XPI files to install. |
| * @return {!Options} A self reference. |
| */ |
| addExtensions(...paths) { |
| this.profile_().addExtensions(paths) |
| return this |
| } |
| |
| /** |
| * @param {string} key the preference key. |
| * @param {(string|number|boolean)} value the preference value. |
| * @return {!Options} A self reference. |
| * @throws {TypeError} if either the key or value has an invalid type. |
| */ |
| setPreference(key, value) { |
| if (typeof key !== 'string') { |
| throw TypeError(`key must be a string, but got ${typeof key}`) |
| } |
| if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') { |
| throw TypeError(`value must be a string, number, or boolean, but got ${typeof value}`) |
| } |
| let options = this.firefoxOptions_() |
| options.prefs = options.prefs || {} |
| options.prefs[key] = value |
| return this |
| } |
| |
| /** |
| * Sets the path to an existing profile to use as a template for new browser |
| * sessions. This profile will be copied for each new session - changes will |
| * not be applied to the profile itself. |
| * |
| * @param {string} profile The profile to use. |
| * @return {!Options} A self reference. |
| * @throws {TypeError} if profile is not a string. |
| */ |
| setProfile(profile) { |
| if (typeof profile !== 'string') { |
| throw TypeError(`profile must be a string, but got ${typeof profile}`) |
| } |
| this.profile_().template_ = profile |
| return this |
| } |
| |
| /** |
| * Sets the binary to use. The binary may be specified as the path to a |
| * Firefox executable. |
| * |
| * @param {(string)} binary The binary to use. |
| * @return {!Options} A self reference. |
| * @throws {TypeError} If `binary` is an invalid type. |
| */ |
| setBinary(binary) { |
| if (binary instanceof Channel || typeof binary === 'string') { |
| this.firefoxOptions_().binary = binary |
| return this |
| } |
| throw TypeError('binary must be a string path ') |
| } |
| |
| /** |
| * Enables Mobile start up features |
| * |
| * @param {string} androidPackage The package to use |
| * @return {!Options} A self reference |
| */ |
| enableMobile(androidPackage = 'org.mozilla.firefox', androidActivity = null, deviceSerial = null) { |
| this.firefoxOptions_().androidPackage = androidPackage |
| |
| if (androidActivity) { |
| this.firefoxOptions_().androidActivity = androidActivity |
| } |
| if (deviceSerial) { |
| this.firefoxOptions_().deviceSerial = deviceSerial |
| } |
| return this |
| } |
| |
| /** |
| * Enables moz:debuggerAddress for firefox cdp |
| */ |
| enableDebugger() { |
| return this.set('moz:debuggerAddress', true) |
| } |
| |
| /** |
| * Enable bidi connection |
| * @returns {!Capabilities} |
| */ |
| enableBidi() { |
| return this.set('webSocketUrl', true) |
| } |
| } |
| |
| /** |
| * Enum of available command contexts. |
| * |
| * Command contexts are specific to Marionette, and may be used with the |
| * {@link #context=} method. Contexts allow you to direct all subsequent |
| * commands to either "content" (default) or "chrome". The latter gives |
| * you elevated security permissions. |
| * |
| * @enum {string} |
| */ |
| const Context = { |
| CONTENT: 'content', |
| CHROME: 'chrome', |
| } |
| |
| /** |
| * @param {string} file Path to the file to find, relative to the program files |
| * root. |
| * @return {!Promise<?string>} A promise for the located executable. |
| * The promise will resolve to {@code null} if Firefox was not found. |
| */ |
| function findInProgramFiles(file) { |
| let files = [ |
| process.env['PROGRAMFILES'] || 'C:\\Program Files', |
| process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)', |
| ].map((prefix) => path.join(prefix, file)) |
| return io.exists(files[0]).then(function (exists) { |
| return exists |
| ? files[0] |
| : io.exists(files[1]).then(function (exists) { |
| return exists ? files[1] : null |
| }) |
| }) |
| } |
| |
| /** @enum {string} */ |
| const ExtensionCommand = { |
| GET_CONTEXT: 'getContext', |
| SET_CONTEXT: 'setContext', |
| INSTALL_ADDON: 'install addon', |
| UNINSTALL_ADDON: 'uninstall addon', |
| FULL_PAGE_SCREENSHOT: 'fullPage screenshot', |
| } |
| |
| /** |
| * Creates a command executor with support for Marionette's custom commands. |
| * @param {!Promise<string>} serverUrl The server's URL. |
| * @return {!command.Executor} The new command executor. |
| */ |
| function createExecutor(serverUrl) { |
| let client = serverUrl.then((url) => new http.HttpClient(url)) |
| let executor = new http.Executor(client) |
| configureExecutor(executor) |
| return executor |
| } |
| |
| /** |
| * Configures the given executor with Firefox-specific commands. |
| * @param {!http.Executor} executor the executor to configure. |
| */ |
| function configureExecutor(executor) { |
| executor.defineCommand(ExtensionCommand.GET_CONTEXT, 'GET', '/session/:sessionId/moz/context') |
| |
| executor.defineCommand(ExtensionCommand.SET_CONTEXT, 'POST', '/session/:sessionId/moz/context') |
| |
| executor.defineCommand(ExtensionCommand.INSTALL_ADDON, 'POST', '/session/:sessionId/moz/addon/install') |
| |
| executor.defineCommand(ExtensionCommand.UNINSTALL_ADDON, 'POST', '/session/:sessionId/moz/addon/uninstall') |
| |
| executor.defineCommand(ExtensionCommand.FULL_PAGE_SCREENSHOT, 'GET', '/session/:sessionId/moz/screenshot/full') |
| } |
| |
| /** |
| * Creates {@link selenium-webdriver/remote.DriverService} instances that manage |
| * a [geckodriver](https://github.com/mozilla/geckodriver) server in a child |
| * process. |
| */ |
| class ServiceBuilder extends remote.DriverService.Builder { |
| /** |
| * @param {string=} opt_exe Path to the server executable to use. If omitted, |
| * the builder will attempt to locate the geckodriver on the system PATH. |
| */ |
| constructor(opt_exe) { |
| super(opt_exe) |
| this.setLoopback(true) // Required. |
| } |
| |
| /** |
| * Enables verbose logging. |
| * |
| * @param {boolean=} opt_trace Whether to enable trace-level logging. By |
| * default, only debug logging is enabled. |
| * @return {!ServiceBuilder} A self reference. |
| */ |
| enableVerboseLogging(opt_trace) { |
| return this.addArguments(opt_trace ? '-vv' : '-v') |
| } |
| |
| /** |
| * Overrides the parent build() method to add the websocket port argument |
| * for Firefox when not connecting to an existing instance. |
| * |
| * @return {!DriverService} A new driver service instance. |
| */ |
| build() { |
| let port = this.options_.port || findFreePort() |
| let argsPromise = Promise.resolve(port).then((port) => { |
| // Start with the default --port argument. |
| let args = this.options_.args.concat(`--port=${port}`) |
| // If the "--connect-existing" flag is not set, add the websocket port. |
| if (!this.options_.args.some((arg) => arg === '--connect-existing')) { |
| return findFreePort().then((wsPort) => { |
| args.push(`--websocket-port=${wsPort}`) |
| return args |
| }) |
| } |
| return args |
| }) |
| |
| let options = Object.assign({}, this.options_, { args: argsPromise, port }) |
| return new remote.DriverService(this.exe_, options) |
| } |
| } |
| |
| /** |
| * A WebDriver client for Firefox. |
| */ |
| class Driver extends webdriver.WebDriver { |
| /** |
| * Creates a new Firefox session. |
| * |
| * @param {(Options|Capabilities|Object)=} opt_config The |
| * configuration options for this driver, specified as either an |
| * {@link Options} or {@link Capabilities}, or as a raw hash object. |
| * @param {(http.Executor|remote.DriverService)=} opt_executor Either a |
| * pre-configured command executor to use for communicating with an |
| * externally managed remote end (which is assumed to already be running), |
| * or the `DriverService` to use to start the geckodriver in a child |
| * process. |
| * |
| * If an executor is provided, care should e taken not to use reuse it with |
| * other clients as its internal command mappings will be updated to support |
| * Firefox-specific commands. |
| * |
| * _This parameter may only be used with Mozilla's GeckoDriver._ |
| * |
| * @throws {Error} If a custom command executor is provided and the driver is |
| * configured to use the legacy FirefoxDriver from the Selenium project. |
| * @return {!Driver} A new driver instance. |
| */ |
| static createSession(opt_config, opt_executor) { |
| let caps = opt_config instanceof Capabilities ? opt_config : new Options(opt_config) |
| |
| let firefoxBrowserPath = null |
| |
| let executor |
| let onQuit |
| |
| if (opt_executor instanceof http.Executor) { |
| executor = opt_executor |
| configureExecutor(executor) |
| } else if (opt_executor instanceof remote.DriverService) { |
| if (!opt_executor.getExecutable()) { |
| const { driverPath, browserPath } = getBinaryPaths(caps) |
| opt_executor.setExecutable(driverPath) |
| firefoxBrowserPath = browserPath |
| } |
| executor = createExecutor(opt_executor.start()) |
| onQuit = () => opt_executor.kill() |
| } else { |
| let service = new ServiceBuilder().build() |
| if (!service.getExecutable()) { |
| const { driverPath, browserPath } = getBinaryPaths(caps) |
| service.setExecutable(driverPath) |
| firefoxBrowserPath = browserPath |
| } |
| executor = createExecutor(service.start()) |
| onQuit = () => service.kill() |
| } |
| |
| if (firefoxBrowserPath) { |
| const vendorOptions = caps.get(FIREFOX_CAPABILITY_KEY) |
| if (vendorOptions) { |
| vendorOptions['binary'] = firefoxBrowserPath |
| caps.set(FIREFOX_CAPABILITY_KEY, vendorOptions) |
| } else { |
| caps.set(FIREFOX_CAPABILITY_KEY, { binary: firefoxBrowserPath }) |
| } |
| caps.delete(Capability.BROWSER_VERSION) |
| } |
| |
| return /** @type {!Driver} */ (super.createSession(executor, caps, onQuit)) |
| } |
| |
| /** |
| * This function is a no-op as file detectors are not supported by this |
| * implementation. |
| * @override |
| */ |
| setFileDetector() {} |
| |
| /** |
| * Get the context that is currently in effect. |
| * |
| * @return {!Promise<Context>} Current context. |
| */ |
| getContext() { |
| return this.execute(new command.Command(ExtensionCommand.GET_CONTEXT)) |
| } |
| |
| /** |
| * Changes target context for commands between chrome- and content. |
| * |
| * Changing the current context has a stateful impact on all subsequent |
| * commands. The {@link Context.CONTENT} context has normal web |
| * platform document permissions, as if you would evaluate arbitrary |
| * JavaScript. The {@link Context.CHROME} context gets elevated |
| * permissions that lets you manipulate the browser chrome itself, |
| * with full access to the XUL toolkit. |
| * |
| * Use your powers wisely. |
| * |
| * @param {!Promise<void>} ctx The context to switch to. |
| */ |
| setContext(ctx) { |
| return this.execute(new command.Command(ExtensionCommand.SET_CONTEXT).setParameter('context', ctx)) |
| } |
| |
| /** |
| * Installs a new addon with the current session. This function will return an |
| * ID that may later be used to {@linkplain #uninstallAddon uninstall} the |
| * addon. |
| * |
| * |
| * @param {string} path Path on the local filesystem to the web extension to |
| * install. |
| * @param {boolean} temporary Flag indicating whether the extension should be |
| * installed temporarily - gets removed on restart |
| * @return {!Promise<string>} A promise that will resolve to an ID for the |
| * newly installed addon. |
| * @see #uninstallAddon |
| */ |
| async installAddon(path, temporary = false) { |
| let stats = fs.statSync(path) |
| let buf |
| if (stats.isDirectory()) { |
| let zip = new Zip() |
| await zip.addDir(path) |
| buf = await zip.toBuffer('DEFLATE') |
| } else { |
| buf = await io.read(path) |
| } |
| return this.execute( |
| new command.Command(ExtensionCommand.INSTALL_ADDON) |
| .setParameter('addon', buf.toString('base64')) |
| .setParameter('temporary', temporary), |
| ) |
| } |
| |
| /** |
| * Uninstalls an addon from the current browser session's profile. |
| * |
| * @param {(string|!Promise<string>)} id ID of the addon to uninstall. |
| * @return {!Promise} A promise that will resolve when the operation has |
| * completed. |
| * @see #installAddon |
| */ |
| async uninstallAddon(id) { |
| id = await Promise.resolve(id) |
| return this.execute(new command.Command(ExtensionCommand.UNINSTALL_ADDON).setParameter('id', id)) |
| } |
| |
| /** |
| * Take full page screenshot of the visible region |
| * |
| * @return {!Promise<string>} A promise that will be |
| * resolved to the screenshot as a base-64 encoded PNG. |
| */ |
| takeFullPageScreenshot() { |
| return this.execute(new command.Command(ExtensionCommand.FULL_PAGE_SCREENSHOT)) |
| } |
| } |
| |
| /** |
| * Provides methods for locating the executable for a Firefox release channel |
| * on Windows and MacOS. For other systems (i.e. Linux), Firefox will always |
| * be located on the system PATH. |
| * @deprecated Instead of using this class, you should configure the |
| * {@link Options} with the appropriate binary location or let Selenium |
| * Manager handle it for you. |
| * @final |
| */ |
| class Channel { |
| /** |
| * @param {string} darwin The path to check when running on MacOS. |
| * @param {string} win32 The path to check when running on Windows. |
| */ |
| constructor(darwin, win32) { |
| /** @private @const */ this.darwin_ = darwin |
| /** @private @const */ this.win32_ = win32 |
| /** @private {Promise<string>} */ |
| this.found_ = null |
| } |
| |
| /** |
| * Attempts to locate the Firefox executable for this release channel. This |
| * will first check the default installation location for the channel before |
| * checking the user's PATH. The returned promise will be rejected if Firefox |
| * can not be found. |
| * |
| * @return {!Promise<string>} A promise for the location of the located |
| * Firefox executable. |
| */ |
| locate() { |
| if (this.found_) { |
| return this.found_ |
| } |
| |
| let found |
| switch (process.platform) { |
| case 'darwin': |
| found = io.exists(this.darwin_).then((exists) => (exists ? this.darwin_ : io.findInPath('firefox'))) |
| break |
| |
| case 'win32': |
| found = findInProgramFiles(this.win32_).then((found) => found || io.findInPath('firefox.exe')) |
| break |
| |
| default: |
| found = Promise.resolve(io.findInPath('firefox')) |
| break |
| } |
| |
| this.found_ = found.then((found) => { |
| if (found) { |
| // TODO: verify version info. |
| return found |
| } |
| throw Error('Could not locate Firefox on the current system') |
| }) |
| return this.found_ |
| } |
| |
| /** @return {!Promise<string>} */ |
| [Symbols.serialize]() { |
| return this.locate() |
| } |
| } |
| |
| /** |
| * Firefox's developer channel. |
| * @const |
| * @see <https://www.mozilla.org/en-US/firefox/channel/desktop/#developer> |
| */ |
| Channel.DEV = new Channel( |
| '/Applications/Firefox Developer Edition.app/Contents/MacOS/firefox', |
| 'Firefox Developer Edition\\firefox.exe', |
| ) |
| |
| /** |
| * Firefox's beta channel. Note this is provided mainly for convenience as |
| * the beta channel has the same installation location as the main release |
| * channel. |
| * @const |
| * @see <https://www.mozilla.org/en-US/firefox/channel/desktop/#beta> |
| */ |
| Channel.BETA = new Channel('/Applications/Firefox.app/Contents/MacOS/firefox', 'Mozilla Firefox\\firefox.exe') |
| |
| /** |
| * Firefox's release channel. |
| * @const |
| * @see <https://www.mozilla.org/en-US/firefox/desktop/> |
| */ |
| Channel.RELEASE = new Channel('/Applications/Firefox.app/Contents/MacOS/firefox', 'Mozilla Firefox\\firefox.exe') |
| |
| /** |
| * Firefox's nightly release channel. |
| * @const |
| * @see <https://www.mozilla.org/en-US/firefox/channel/desktop/#nightly> |
| */ |
| Channel.NIGHTLY = new Channel('/Applications/Firefox Nightly.app/Contents/MacOS/firefox', 'Nightly\\firefox.exe') |
| |
| // PUBLIC API |
| |
| module.exports = { |
| Channel, |
| Context, |
| Driver, |
| Options, |
| ServiceBuilder, |
| } |