Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
SeleniumHQ
GitHub Repository: SeleniumHQ/Selenium
Path: blob/trunk/javascript/selenium-webdriver/firefox.js
1864 views
1
// Licensed to the Software Freedom Conservancy (SFC) under one
2
// or more contributor license agreements. See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership. The SFC licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License. You may obtain a copy of the License at
8
//
9
// http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied. See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
/**
19
* @fileoverview Defines the {@linkplain Driver WebDriver} client for Firefox.
20
* Before using this module, you must download the latest
21
* [geckodriver release] and ensure it can be found on your system [PATH].
22
*
23
* Each FirefoxDriver instance will be created with an anonymous profile,
24
* ensuring browser historys do not share session data (cookies, history, cache,
25
* offline storage, etc.)
26
*
27
* __Customizing the Firefox Profile__
28
*
29
* The profile used for each WebDriver session may be configured using the
30
* {@linkplain Options} class. For example, you may install an extension, like
31
* Firebug:
32
*
33
* const {Builder} = require('selenium-webdriver');
34
* const firefox = require('selenium-webdriver/firefox');
35
*
36
* let options = new firefox.Options()
37
* .addExtensions('/path/to/firebug.xpi')
38
* .setPreference('extensions.firebug.showChromeErrors', true);
39
*
40
* let driver = new Builder()
41
* .forBrowser('firefox')
42
* .setFirefoxOptions(options)
43
* .build();
44
*
45
* The {@linkplain Options} class may also be used to configure WebDriver based
46
* on a pre-existing browser profile:
47
*
48
* let profile = '/usr/local/home/bob/.mozilla/firefox/3fgog75h.testing';
49
* let options = new firefox.Options().setProfile(profile);
50
*
51
* The FirefoxDriver will _never_ modify a pre-existing profile; instead it will
52
* create a copy for it to modify. By extension, there are certain browser
53
* preferences that are required for WebDriver to function properly and they
54
* will always be overwritten.
55
*
56
* __Using a Custom Firefox Binary__
57
*
58
* On Windows and MacOS, the FirefoxDriver will search for Firefox in its
59
* default installation location:
60
*
61
* - Windows: C:\Program Files and C:\Program Files (x86).
62
* - MacOS: /Applications/Firefox.app
63
*
64
* For Linux, Firefox will always be located on the PATH: `$(where firefox)`.
65
*
66
* You can provide a custom location for Firefox by setting the binary in the
67
* {@link Options}:setBinary method.
68
*
69
* const {Builder} = require('selenium-webdriver');
70
* const firefox = require('selenium-webdriver/firefox');
71
*
72
* let options = new firefox.Options()
73
* .setBinary('/my/firefox/install/dir/firefox');
74
* let driver = new Builder()
75
* .forBrowser('firefox')
76
* .setFirefoxOptions(options)
77
* .build();
78
*
79
* __Remote Testing__
80
*
81
* You may customize the Firefox binary and profile when running against a
82
* remote Selenium server. Your custom profile will be packaged as a zip and
83
* transferred to the remote host for use. The profile will be transferred
84
* _once for each new session_. The performance impact should be minimal if
85
* you've only configured a few extra browser preferences. If you have a large
86
* profile with several extensions, you should consider installing it on the
87
* remote host and defining its path via the {@link Options} class. Custom
88
* binaries are never copied to remote machines and must be referenced by
89
* installation path.
90
*
91
* const {Builder} = require('selenium-webdriver');
92
* const firefox = require('selenium-webdriver/firefox');
93
*
94
* let options = new firefox.Options()
95
* .setProfile('/profile/path/on/remote/host')
96
* .setBinary('/install/dir/on/remote/host/firefox');
97
*
98
* let driver = new Builder()
99
* .forBrowser('firefox')
100
* .usingServer('http://127.0.0.1:4444/wd/hub')
101
* .setFirefoxOptions(options)
102
* .build();
103
*
104
* [geckodriver release]: https://github.com/mozilla/geckodriver/releases/
105
* [PATH]: http://en.wikipedia.org/wiki/PATH_%28variable%29
106
*
107
* @module selenium-webdriver/firefox
108
*/
109
110
'use strict'
111
112
const fs = require('node:fs')
113
const path = require('node:path')
114
const Symbols = require('./lib/symbols')
115
const command = require('./lib/command')
116
const http = require('./http')
117
const io = require('./io')
118
const remote = require('./remote')
119
const webdriver = require('./lib/webdriver')
120
const zip = require('./io/zip')
121
const { Browser, Capabilities, Capability } = require('./lib/capabilities')
122
const { Zip } = require('./io/zip')
123
const { getBinaryPaths } = require('./common/driverFinder')
124
const { findFreePort } = require('./net/portprober')
125
const FIREFOX_CAPABILITY_KEY = 'moz:firefoxOptions'
126
127
/**
128
* Thrown when there an add-on is malformed.
129
* @final
130
*/
131
class AddonFormatError extends Error {
132
/** @param {string} msg The error message. */
133
constructor(msg) {
134
super(msg)
135
/** @override */
136
this.name = this.constructor.name
137
}
138
}
139
140
/**
141
* Installs an extension to the given directory.
142
* @param {string} extension Path to the xpi extension file to install.
143
* @param {string} dir Path to the directory to install the extension in.
144
* @return {!Promise<string>} A promise for the add-on ID once
145
* installed.
146
*/
147
async function installExtension(extension, dir) {
148
const ext = extension.slice(-4)
149
if (ext !== '.xpi' && ext !== '.zip') {
150
throw Error('File name does not end in ".zip" or ".xpi": ' + ext)
151
}
152
153
let archive = await zip.load(extension)
154
if (!archive.has('manifest.json')) {
155
throw new AddonFormatError(`Couldn't find manifest.json in ${extension}`)
156
}
157
158
let buf = await archive.getFile('manifest.json')
159
let parsedJSON = JSON.parse(buf.toString('utf8'))
160
161
let { browser_specific_settings } =
162
/** @type {{browser_specific_settings:{gecko:{id:string}}}} */
163
parsedJSON
164
165
if (browser_specific_settings && browser_specific_settings.gecko) {
166
/* browser_specific_settings is an alternative to applications
167
* It is meant to facilitate cross-browser plugins since Firefox48
168
* see https://bugzilla.mozilla.org/show_bug.cgi?id=1262005
169
*/
170
parsedJSON.applications = browser_specific_settings
171
}
172
173
let { applications } =
174
/** @type {{applications:{gecko:{id:string}}}} */
175
parsedJSON
176
if (!(applications && applications.gecko && applications.gecko.id)) {
177
throw new AddonFormatError(`Could not find add-on ID for ${extension}`)
178
}
179
180
await io.copy(extension, `${path.join(dir, applications.gecko.id)}.xpi`)
181
return applications.gecko.id
182
}
183
184
class Profile {
185
constructor() {
186
/** @private {?string} */
187
this.template_ = null
188
189
/** @private {!Array<string>} */
190
this.extensions_ = []
191
}
192
193
addExtensions(/** !Array<string> */ paths) {
194
this.extensions_ = this.extensions_.concat(...paths)
195
}
196
197
/**
198
* @return {(!Promise<string>|undefined)} a promise for a base64 encoded
199
* profile, or undefined if there's no data to include.
200
*/
201
[Symbols.serialize]() {
202
if (this.template_ || this.extensions_.length) {
203
return buildProfile(this.template_, this.extensions_)
204
}
205
return undefined
206
}
207
}
208
209
/**
210
* @param {?string} template path to an existing profile to use as a template.
211
* @param {!Array<string>} extensions paths to extensions to install in the new
212
* profile.
213
* @return {!Promise<string>} a promise for the base64 encoded profile.
214
*/
215
async function buildProfile(template, extensions) {
216
let dir = template
217
218
if (extensions.length) {
219
dir = await io.tmpDir()
220
if (template) {
221
await io.copyDir(/** @type {string} */ (template), dir, /(parent\.lock|lock|\.parentlock)/)
222
}
223
224
const extensionsDir = path.join(dir, 'extensions')
225
await io.mkdir(extensionsDir)
226
227
for (let i = 0; i < extensions.length; i++) {
228
await installExtension(extensions[i], extensionsDir)
229
}
230
}
231
232
let zip = new Zip()
233
return zip
234
.addDir(dir)
235
.then(() => zip.toBuffer())
236
.then((buf) => buf.toString('base64'))
237
}
238
239
/**
240
* Configuration options for the FirefoxDriver.
241
*/
242
class Options extends Capabilities {
243
/**
244
* @param {(Capabilities|Map<string, ?>|Object)=} other Another set of
245
* capabilities to initialize this instance from.
246
*/
247
constructor(other) {
248
super(other)
249
this.setBrowserName(Browser.FIREFOX)
250
// https://fxdx.dev/deprecating-cdp-support-in-firefox-embracing-the-future-with-webdriver-bidi/.
251
// Enable BiDi only
252
this.setPreference('remote.active-protocols', 1)
253
}
254
255
/**
256
* @return {!Object}
257
* @private
258
*/
259
firefoxOptions_() {
260
let options = this.get(FIREFOX_CAPABILITY_KEY)
261
if (!options) {
262
options = {}
263
this.set(FIREFOX_CAPABILITY_KEY, options)
264
}
265
return options
266
}
267
268
/**
269
* @return {!Profile}
270
* @private
271
*/
272
profile_() {
273
let options = this.firefoxOptions_()
274
if (!options.profile) {
275
options.profile = new Profile()
276
}
277
return options.profile
278
}
279
280
/**
281
* Specify additional command line arguments that should be used when starting
282
* the Firefox browser.
283
*
284
* @param {...(string|!Array<string>)} args The arguments to include.
285
* @return {!Options} A self reference.
286
*/
287
addArguments(...args) {
288
if (args.length) {
289
let options = this.firefoxOptions_()
290
options.args = options.args ? options.args.concat(...args) : args
291
}
292
return this
293
}
294
295
/**
296
* Sets the initial window size
297
*
298
* @param {{width: number, height: number}} size The desired window size.
299
* @return {!Options} A self reference.
300
* @throws {TypeError} if width or height is unspecified, not a number, or
301
* less than or equal to 0.
302
*/
303
windowSize({ width, height }) {
304
function checkArg(arg) {
305
if (typeof arg !== 'number' || arg <= 0) {
306
throw TypeError('Arguments must be {width, height} with numbers > 0')
307
}
308
}
309
310
checkArg(width)
311
checkArg(height)
312
return this.addArguments(`--width=${width}`, `--height=${height}`)
313
}
314
315
/**
316
* Add extensions that should be installed when starting Firefox.
317
*
318
* @param {...string} paths The paths to the extension XPI files to install.
319
* @return {!Options} A self reference.
320
*/
321
addExtensions(...paths) {
322
this.profile_().addExtensions(paths)
323
return this
324
}
325
326
/**
327
* @param {string} key the preference key.
328
* @param {(string|number|boolean)} value the preference value.
329
* @return {!Options} A self reference.
330
* @throws {TypeError} if either the key or value has an invalid type.
331
*/
332
setPreference(key, value) {
333
if (typeof key !== 'string') {
334
throw TypeError(`key must be a string, but got ${typeof key}`)
335
}
336
if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
337
throw TypeError(`value must be a string, number, or boolean, but got ${typeof value}`)
338
}
339
let options = this.firefoxOptions_()
340
options.prefs = options.prefs || {}
341
options.prefs[key] = value
342
return this
343
}
344
345
/**
346
* Sets the path to an existing profile to use as a template for new browser
347
* sessions. This profile will be copied for each new session - changes will
348
* not be applied to the profile itself.
349
*
350
* @param {string} profile The profile to use.
351
* @return {!Options} A self reference.
352
* @throws {TypeError} if profile is not a string.
353
*/
354
setProfile(profile) {
355
if (typeof profile !== 'string') {
356
throw TypeError(`profile must be a string, but got ${typeof profile}`)
357
}
358
this.profile_().template_ = profile
359
return this
360
}
361
362
/**
363
* Sets the binary to use. The binary may be specified as the path to a
364
* Firefox executable.
365
*
366
* @param {(string)} binary The binary to use.
367
* @return {!Options} A self reference.
368
* @throws {TypeError} If `binary` is an invalid type.
369
*/
370
setBinary(binary) {
371
if (binary instanceof Channel || typeof binary === 'string') {
372
this.firefoxOptions_().binary = binary
373
return this
374
}
375
throw TypeError('binary must be a string path ')
376
}
377
378
/**
379
* Enables Mobile start up features
380
*
381
* @param {string} androidPackage The package to use
382
* @return {!Options} A self reference
383
*/
384
enableMobile(androidPackage = 'org.mozilla.firefox', androidActivity = null, deviceSerial = null) {
385
this.firefoxOptions_().androidPackage = androidPackage
386
387
if (androidActivity) {
388
this.firefoxOptions_().androidActivity = androidActivity
389
}
390
if (deviceSerial) {
391
this.firefoxOptions_().deviceSerial = deviceSerial
392
}
393
return this
394
}
395
396
/**
397
* Enables moz:debuggerAddress for firefox cdp
398
*/
399
enableDebugger() {
400
return this.set('moz:debuggerAddress', true)
401
}
402
403
/**
404
* Enable bidi connection
405
* @returns {!Capabilities}
406
*/
407
enableBidi() {
408
return this.set('webSocketUrl', true)
409
}
410
}
411
412
/**
413
* Enum of available command contexts.
414
*
415
* Command contexts are specific to Marionette, and may be used with the
416
* {@link #context=} method. Contexts allow you to direct all subsequent
417
* commands to either "content" (default) or "chrome". The latter gives
418
* you elevated security permissions.
419
*
420
* @enum {string}
421
*/
422
const Context = {
423
CONTENT: 'content',
424
CHROME: 'chrome',
425
}
426
427
/**
428
* @param {string} file Path to the file to find, relative to the program files
429
* root.
430
* @return {!Promise<?string>} A promise for the located executable.
431
* The promise will resolve to {@code null} if Firefox was not found.
432
*/
433
function findInProgramFiles(file) {
434
let files = [
435
process.env['PROGRAMFILES'] || 'C:\\Program Files',
436
process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)',
437
].map((prefix) => path.join(prefix, file))
438
return io.exists(files[0]).then(function (exists) {
439
return exists
440
? files[0]
441
: io.exists(files[1]).then(function (exists) {
442
return exists ? files[1] : null
443
})
444
})
445
}
446
447
/** @enum {string} */
448
const ExtensionCommand = {
449
GET_CONTEXT: 'getContext',
450
SET_CONTEXT: 'setContext',
451
INSTALL_ADDON: 'install addon',
452
UNINSTALL_ADDON: 'uninstall addon',
453
FULL_PAGE_SCREENSHOT: 'fullPage screenshot',
454
}
455
456
/**
457
* Creates a command executor with support for Marionette's custom commands.
458
* @param {!Promise<string>} serverUrl The server's URL.
459
* @return {!command.Executor} The new command executor.
460
*/
461
function createExecutor(serverUrl) {
462
let client = serverUrl.then((url) => new http.HttpClient(url))
463
let executor = new http.Executor(client)
464
configureExecutor(executor)
465
return executor
466
}
467
468
/**
469
* Configures the given executor with Firefox-specific commands.
470
* @param {!http.Executor} executor the executor to configure.
471
*/
472
function configureExecutor(executor) {
473
executor.defineCommand(ExtensionCommand.GET_CONTEXT, 'GET', '/session/:sessionId/moz/context')
474
475
executor.defineCommand(ExtensionCommand.SET_CONTEXT, 'POST', '/session/:sessionId/moz/context')
476
477
executor.defineCommand(ExtensionCommand.INSTALL_ADDON, 'POST', '/session/:sessionId/moz/addon/install')
478
479
executor.defineCommand(ExtensionCommand.UNINSTALL_ADDON, 'POST', '/session/:sessionId/moz/addon/uninstall')
480
481
executor.defineCommand(ExtensionCommand.FULL_PAGE_SCREENSHOT, 'GET', '/session/:sessionId/moz/screenshot/full')
482
}
483
484
/**
485
* Creates {@link selenium-webdriver/remote.DriverService} instances that manage
486
* a [geckodriver](https://github.com/mozilla/geckodriver) server in a child
487
* process.
488
*/
489
class ServiceBuilder extends remote.DriverService.Builder {
490
/**
491
* @param {string=} opt_exe Path to the server executable to use. If omitted,
492
* the builder will attempt to locate the geckodriver on the system PATH.
493
*/
494
constructor(opt_exe) {
495
super(opt_exe)
496
this.setLoopback(true) // Required.
497
}
498
499
/**
500
* Enables verbose logging.
501
*
502
* @param {boolean=} opt_trace Whether to enable trace-level logging. By
503
* default, only debug logging is enabled.
504
* @return {!ServiceBuilder} A self reference.
505
*/
506
enableVerboseLogging(opt_trace) {
507
return this.addArguments(opt_trace ? '-vv' : '-v')
508
}
509
510
/**
511
* Overrides the parent build() method to add the websocket port argument
512
* for Firefox when not connecting to an existing instance.
513
*
514
* @return {!DriverService} A new driver service instance.
515
*/
516
build() {
517
let port = this.options_.port || findFreePort()
518
let argsPromise = Promise.resolve(port).then((port) => {
519
// Start with the default --port argument.
520
let args = this.options_.args.concat(`--port=${port}`)
521
// If the "--connect-existing" flag is not set, add the websocket port.
522
if (!this.options_.args.some((arg) => arg === '--connect-existing')) {
523
return findFreePort().then((wsPort) => {
524
args.push(`--websocket-port=${wsPort}`)
525
return args
526
})
527
}
528
return args
529
})
530
531
let options = Object.assign({}, this.options_, { args: argsPromise, port })
532
return new remote.DriverService(this.exe_, options)
533
}
534
}
535
536
/**
537
* A WebDriver client for Firefox.
538
*/
539
class Driver extends webdriver.WebDriver {
540
/**
541
* Creates a new Firefox session.
542
*
543
* @param {(Options|Capabilities|Object)=} opt_config The
544
* configuration options for this driver, specified as either an
545
* {@link Options} or {@link Capabilities}, or as a raw hash object.
546
* @param {(http.Executor|remote.DriverService)=} opt_executor Either a
547
* pre-configured command executor to use for communicating with an
548
* externally managed remote end (which is assumed to already be running),
549
* or the `DriverService` to use to start the geckodriver in a child
550
* process.
551
*
552
* If an executor is provided, care should e taken not to use reuse it with
553
* other clients as its internal command mappings will be updated to support
554
* Firefox-specific commands.
555
*
556
* _This parameter may only be used with Mozilla's GeckoDriver._
557
*
558
* @throws {Error} If a custom command executor is provided and the driver is
559
* configured to use the legacy FirefoxDriver from the Selenium project.
560
* @return {!Driver} A new driver instance.
561
*/
562
static createSession(opt_config, opt_executor) {
563
let caps = opt_config instanceof Capabilities ? opt_config : new Options(opt_config)
564
565
let firefoxBrowserPath = null
566
567
let executor
568
let onQuit
569
570
if (opt_executor instanceof http.Executor) {
571
executor = opt_executor
572
configureExecutor(executor)
573
} else if (opt_executor instanceof remote.DriverService) {
574
if (!opt_executor.getExecutable()) {
575
const { driverPath, browserPath } = getBinaryPaths(caps)
576
opt_executor.setExecutable(driverPath)
577
firefoxBrowserPath = browserPath
578
}
579
executor = createExecutor(opt_executor.start())
580
onQuit = () => opt_executor.kill()
581
} else {
582
let service = new ServiceBuilder().build()
583
if (!service.getExecutable()) {
584
const { driverPath, browserPath } = getBinaryPaths(caps)
585
service.setExecutable(driverPath)
586
firefoxBrowserPath = browserPath
587
}
588
executor = createExecutor(service.start())
589
onQuit = () => service.kill()
590
}
591
592
if (firefoxBrowserPath) {
593
const vendorOptions = caps.get(FIREFOX_CAPABILITY_KEY)
594
if (vendorOptions) {
595
vendorOptions['binary'] = firefoxBrowserPath
596
caps.set(FIREFOX_CAPABILITY_KEY, vendorOptions)
597
} else {
598
caps.set(FIREFOX_CAPABILITY_KEY, { binary: firefoxBrowserPath })
599
}
600
caps.delete(Capability.BROWSER_VERSION)
601
}
602
603
return /** @type {!Driver} */ (super.createSession(executor, caps, onQuit))
604
}
605
606
/**
607
* This function is a no-op as file detectors are not supported by this
608
* implementation.
609
* @override
610
*/
611
setFileDetector() {}
612
613
/**
614
* Get the context that is currently in effect.
615
*
616
* @return {!Promise<Context>} Current context.
617
*/
618
getContext() {
619
return this.execute(new command.Command(ExtensionCommand.GET_CONTEXT))
620
}
621
622
/**
623
* Changes target context for commands between chrome- and content.
624
*
625
* Changing the current context has a stateful impact on all subsequent
626
* commands. The {@link Context.CONTENT} context has normal web
627
* platform document permissions, as if you would evaluate arbitrary
628
* JavaScript. The {@link Context.CHROME} context gets elevated
629
* permissions that lets you manipulate the browser chrome itself,
630
* with full access to the XUL toolkit.
631
*
632
* Use your powers wisely.
633
*
634
* @param {!Promise<void>} ctx The context to switch to.
635
*/
636
setContext(ctx) {
637
return this.execute(new command.Command(ExtensionCommand.SET_CONTEXT).setParameter('context', ctx))
638
}
639
640
/**
641
* Installs a new addon with the current session. This function will return an
642
* ID that may later be used to {@linkplain #uninstallAddon uninstall} the
643
* addon.
644
*
645
*
646
* @param {string} path Path on the local filesystem to the web extension to
647
* install.
648
* @param {boolean} temporary Flag indicating whether the extension should be
649
* installed temporarily - gets removed on restart
650
* @return {!Promise<string>} A promise that will resolve to an ID for the
651
* newly installed addon.
652
* @see #uninstallAddon
653
*/
654
async installAddon(path, temporary = false) {
655
let stats = fs.statSync(path)
656
let buf
657
if (stats.isDirectory()) {
658
let zip = new Zip()
659
await zip.addDir(path)
660
buf = await zip.toBuffer('DEFLATE')
661
} else {
662
buf = await io.read(path)
663
}
664
return this.execute(
665
new command.Command(ExtensionCommand.INSTALL_ADDON)
666
.setParameter('addon', buf.toString('base64'))
667
.setParameter('temporary', temporary),
668
)
669
}
670
671
/**
672
* Uninstalls an addon from the current browser session's profile.
673
*
674
* @param {(string|!Promise<string>)} id ID of the addon to uninstall.
675
* @return {!Promise} A promise that will resolve when the operation has
676
* completed.
677
* @see #installAddon
678
*/
679
async uninstallAddon(id) {
680
id = await Promise.resolve(id)
681
return this.execute(new command.Command(ExtensionCommand.UNINSTALL_ADDON).setParameter('id', id))
682
}
683
684
/**
685
* Take full page screenshot of the visible region
686
*
687
* @return {!Promise<string>} A promise that will be
688
* resolved to the screenshot as a base-64 encoded PNG.
689
*/
690
takeFullPageScreenshot() {
691
return this.execute(new command.Command(ExtensionCommand.FULL_PAGE_SCREENSHOT))
692
}
693
}
694
695
/**
696
* Provides methods for locating the executable for a Firefox release channel
697
* on Windows and MacOS. For other systems (i.e. Linux), Firefox will always
698
* be located on the system PATH.
699
* @deprecated Instead of using this class, you should configure the
700
* {@link Options} with the appropriate binary location or let Selenium
701
* Manager handle it for you.
702
* @final
703
*/
704
class Channel {
705
/**
706
* @param {string} darwin The path to check when running on MacOS.
707
* @param {string} win32 The path to check when running on Windows.
708
*/
709
constructor(darwin, win32) {
710
/** @private @const */ this.darwin_ = darwin
711
/** @private @const */ this.win32_ = win32
712
/** @private {Promise<string>} */
713
this.found_ = null
714
}
715
716
/**
717
* Attempts to locate the Firefox executable for this release channel. This
718
* will first check the default installation location for the channel before
719
* checking the user's PATH. The returned promise will be rejected if Firefox
720
* can not be found.
721
*
722
* @return {!Promise<string>} A promise for the location of the located
723
* Firefox executable.
724
*/
725
locate() {
726
if (this.found_) {
727
return this.found_
728
}
729
730
let found
731
switch (process.platform) {
732
case 'darwin':
733
found = io.exists(this.darwin_).then((exists) => (exists ? this.darwin_ : io.findInPath('firefox')))
734
break
735
736
case 'win32':
737
found = findInProgramFiles(this.win32_).then((found) => found || io.findInPath('firefox.exe'))
738
break
739
740
default:
741
found = Promise.resolve(io.findInPath('firefox'))
742
break
743
}
744
745
this.found_ = found.then((found) => {
746
if (found) {
747
// TODO: verify version info.
748
return found
749
}
750
throw Error('Could not locate Firefox on the current system')
751
})
752
return this.found_
753
}
754
755
/** @return {!Promise<string>} */
756
[Symbols.serialize]() {
757
return this.locate()
758
}
759
}
760
761
/**
762
* Firefox's developer channel.
763
* @const
764
* @see <https://www.mozilla.org/en-US/firefox/channel/desktop/#developer>
765
*/
766
Channel.DEV = new Channel(
767
'/Applications/Firefox Developer Edition.app/Contents/MacOS/firefox',
768
'Firefox Developer Edition\\firefox.exe',
769
)
770
771
/**
772
* Firefox's beta channel. Note this is provided mainly for convenience as
773
* the beta channel has the same installation location as the main release
774
* channel.
775
* @const
776
* @see <https://www.mozilla.org/en-US/firefox/channel/desktop/#beta>
777
*/
778
Channel.BETA = new Channel('/Applications/Firefox.app/Contents/MacOS/firefox', 'Mozilla Firefox\\firefox.exe')
779
780
/**
781
* Firefox's release channel.
782
* @const
783
* @see <https://www.mozilla.org/en-US/firefox/desktop/>
784
*/
785
Channel.RELEASE = new Channel('/Applications/Firefox.app/Contents/MacOS/firefox', 'Mozilla Firefox\\firefox.exe')
786
787
/**
788
* Firefox's nightly release channel.
789
* @const
790
* @see <https://www.mozilla.org/en-US/firefox/channel/desktop/#nightly>
791
*/
792
Channel.NIGHTLY = new Channel('/Applications/Firefox Nightly.app/Contents/MacOS/firefox', 'Nightly\\firefox.exe')
793
794
// PUBLIC API
795
796
module.exports = {
797
Channel,
798
Context,
799
Driver,
800
Options,
801
ServiceBuilder,
802
}
803
804