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