index.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576
  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. * @fileoverview Defines the {@linkplain Driver WebDriver} client for Firefox.
  19. * Before using this module, you must download the latest
  20. * [geckodriver release] and ensure it can be found on your system [PATH].
  21. *
  22. * Each FirefoxDriver instance will be created with an anonymous profile,
  23. * ensuring browser historys do not share session data (cookies, history, cache,
  24. * offline storage, etc.)
  25. *
  26. * __Customizing the Firefox Profile__
  27. *
  28. * The {@linkplain Profile} class may be used to configure the browser profile
  29. * used with WebDriver, with functions to install additional
  30. * {@linkplain Profile#addExtension extensions}, configure browser
  31. * {@linkplain Profile#setPreference preferences}, and more. For example, you
  32. * may wish to include Firebug:
  33. *
  34. * const {Builder} = require('selenium-webdriver');
  35. * const firefox = require('selenium-webdriver/firefox');
  36. *
  37. * let profile = new firefox.Profile();
  38. * profile.addExtension('/path/to/firebug.xpi');
  39. * profile.setPreference('extensions.firebug.showChromeErrors', true);
  40. *
  41. * let options = new firefox.Options().setProfile(profile);
  42. * let driver = new Builder()
  43. * .forBrowser('firefox')
  44. * .setFirefoxOptions(options)
  45. * .build();
  46. *
  47. * The {@linkplain Profile} class may also be used to configure WebDriver based
  48. * on a pre-existing browser profile:
  49. *
  50. * let profile = new firefox.Profile(
  51. * '/usr/local/home/bob/.mozilla/firefox/3fgog75h.testing');
  52. * let options = new firefox.Options().setProfile(profile);
  53. *
  54. * The FirefoxDriver will _never_ modify a pre-existing profile; instead it will
  55. * create a copy for it to modify. By extension, there are certain browser
  56. * preferences that are required for WebDriver to function properly and they
  57. * will always be overwritten.
  58. *
  59. * __Using a Custom Firefox Binary__
  60. *
  61. * On Windows and MacOS, the FirefoxDriver will search for Firefox in its
  62. * default installation location:
  63. *
  64. * - Windows: C:\Program Files and C:\Program Files (x86).
  65. * - MacOS: /Applications/Firefox.app
  66. *
  67. * For Linux, Firefox will always be located on the PATH: `$(where firefox)`.
  68. *
  69. * Several methods are provided for starting Firefox with a custom executable.
  70. * First, on Windows and MacOS, you may configure WebDriver to check the default
  71. * install location for a non-release channel. If the requested channel cannot
  72. * be found in its default location, WebDriver will fallback to searching your
  73. * PATH. _Note:_ on Linux, Firefox is _always_ located on your path, regardless
  74. * of the requested channel.
  75. *
  76. * const {Builder} = require('selenium-webdriver');
  77. * const firefox = require('selenium-webdriver/firefox');
  78. *
  79. * let options = new firefox.Options().setBinary(firefox.Channel.NIGHTLY);
  80. * let driver = new Builder()
  81. * .forBrowser('firefox')
  82. * .setFirefoxOptions(options)
  83. * .build();
  84. *
  85. * On all platforms, you may configrue WebDriver to use a Firefox specific
  86. * executable:
  87. *
  88. * let options = new firefox.Options()
  89. * .setBinary('/my/firefox/install/dir/firefox-bin');
  90. *
  91. * __Remote Testing__
  92. *
  93. * You may customize the Firefox binary and profile when running against a
  94. * remote Selenium server. Your custom profile will be packaged as a zip and
  95. * transfered to the remote host for use. The profile will be transferred
  96. * _once for each new session_. The performance impact should be minimal if
  97. * you've only configured a few extra browser preferences. If you have a large
  98. * profile with several extensions, you should consider installing it on the
  99. * remote host and defining its path via the {@link Options} class. Custom
  100. * binaries are never copied to remote machines and must be referenced by
  101. * installation path.
  102. *
  103. * const {Builder} = require('selenium-webdriver');
  104. * const firefox = require('selenium-webdriver/firefox');
  105. *
  106. * let options = new firefox.Options()
  107. * .setProfile('/profile/path/on/remote/host')
  108. * .setBinary('/install/dir/on/remote/host/firefox-bin');
  109. *
  110. * let driver = new Builder()
  111. * .forBrowser('firefox')
  112. * .usingServer('http://127.0.0.1:4444/wd/hub')
  113. * .setFirefoxOptions(options)
  114. * .build();
  115. *
  116. * [geckodriver release]: https://github.com/mozilla/geckodriver/releases/
  117. * [PATH]: http://en.wikipedia.org/wiki/PATH_%28variable%29
  118. */
  119. 'use strict';
  120. const url = require('url');
  121. const {Binary, Channel} = require('./binary'),
  122. Profile = require('./profile').Profile,
  123. http = require('../http'),
  124. httpUtil = require('../http/util'),
  125. io = require('../io'),
  126. capabilities = require('../lib/capabilities'),
  127. command = require('../lib/command'),
  128. logging = require('../lib/logging'),
  129. promise = require('../lib/promise'),
  130. webdriver = require('../lib/webdriver'),
  131. net = require('../net'),
  132. portprober = require('../net/portprober'),
  133. remote = require('../remote');
  134. /**
  135. * Configuration options for the FirefoxDriver.
  136. */
  137. class Options {
  138. constructor() {
  139. /** @private {Profile} */
  140. this.profile_ = null;
  141. /** @private {(Binary|Channel|string|null)} */
  142. this.binary_ = null;
  143. /** @private {!Array<string>} */
  144. this.args_ = [];
  145. /** @private {logging.Preferences} */
  146. this.logPrefs_ = null;
  147. /** @private {?capabilities.ProxyConfig} */
  148. this.proxy_ = null;
  149. }
  150. /**
  151. * Specify additional command line arguments that should be used when starting
  152. * the Firefox browser.
  153. *
  154. * @param {...(string|!Array<string>)} args The arguments to include.
  155. * @return {!Options} A self reference.
  156. */
  157. addArguments(...args) {
  158. this.args_ = this.args_.concat(...args);
  159. return this;
  160. }
  161. /**
  162. * Configures the geckodriver to start Firefox in headless mode.
  163. *
  164. * @return {!Options} A self reference.
  165. */
  166. headless() {
  167. return this.addArguments('-headless');
  168. }
  169. /**
  170. * Sets the initial window size when running in
  171. * {@linkplain #headless headless} mode.
  172. *
  173. * @param {{width: number, height: number}} size The desired window size.
  174. * @return {!Options} A self reference.
  175. * @throws {TypeError} if width or height is unspecified, not a number, or
  176. * less than or equal to 0.
  177. */
  178. windowSize({width, height}) {
  179. function checkArg(arg) {
  180. if (typeof arg !== 'number' || arg <= 0) {
  181. throw TypeError('Arguments must be {width, height} with numbers > 0');
  182. }
  183. }
  184. checkArg(width);
  185. checkArg(height);
  186. return this.addArguments(`--window-size=${width},${height}`);
  187. }
  188. /**
  189. * Sets the profile to use. The profile may be specified as a
  190. * {@link Profile} object or as the path to an existing Firefox profile to use
  191. * as a template.
  192. *
  193. * @param {(string|!Profile)} profile The profile to use.
  194. * @return {!Options} A self reference.
  195. */
  196. setProfile(profile) {
  197. if (typeof profile === 'string') {
  198. profile = new Profile(profile);
  199. }
  200. this.profile_ = profile;
  201. return this;
  202. }
  203. /**
  204. * Sets the binary to use. The binary may be specified as the path to a
  205. * Firefox executable, a specific {@link Channel}, or as a {@link Binary}
  206. * object.
  207. *
  208. * @param {(string|!Binary|!Channel)} binary The binary to use.
  209. * @return {!Options} A self reference.
  210. * @throws {TypeError} If `binary` is an invalid type.
  211. */
  212. setBinary(binary) {
  213. if (binary instanceof Binary
  214. || binary instanceof Channel
  215. || typeof binary === 'string') {
  216. this.binary_ = binary;
  217. return this;
  218. }
  219. throw TypeError(
  220. 'binary must be a string path, Channel, or Binary object');
  221. }
  222. /**
  223. * Sets the logging preferences for the new session.
  224. * @param {logging.Preferences} prefs The logging preferences.
  225. * @return {!Options} A self reference.
  226. */
  227. setLoggingPreferences(prefs) {
  228. this.logPrefs_ = prefs;
  229. return this;
  230. }
  231. /**
  232. * Sets the proxy to use.
  233. *
  234. * @param {capabilities.ProxyConfig} proxy The proxy configuration to use.
  235. * @return {!Options} A self reference.
  236. */
  237. setProxy(proxy) {
  238. this.proxy_ = proxy;
  239. return this;
  240. }
  241. /**
  242. * Converts these options to a {@link capabilities.Capabilities} instance.
  243. *
  244. * @return {!capabilities.Capabilities} A new capabilities object.
  245. */
  246. toCapabilities() {
  247. let caps = capabilities.Capabilities.firefox();
  248. let firefoxOptions = {};
  249. caps.set('moz:firefoxOptions', firefoxOptions);
  250. if (this.logPrefs_) {
  251. caps.set(capabilities.Capability.LOGGING_PREFS, this.logPrefs_);
  252. }
  253. if (this.proxy_) {
  254. caps.set(capabilities.Capability.PROXY, this.proxy_);
  255. }
  256. if (this.args_.length) {
  257. firefoxOptions['args'] = this.args_.concat();
  258. }
  259. if (this.binary_) {
  260. if (this.binary_ instanceof Binary) {
  261. let exe = this.binary_.getExe();
  262. if (exe) {
  263. firefoxOptions['binary'] = exe;
  264. }
  265. let args = this.binary_.getArguments();
  266. if (args.length) {
  267. if (this.args_.length) {
  268. throw Error(
  269. 'You may specify browser arguments with Options.addArguments'
  270. + ' (preferred) or Binary.addArguments, but not both');
  271. }
  272. firefoxOptions['args'] = args;
  273. }
  274. } else if (this.binary_ instanceof Channel) {
  275. firefoxOptions['binary'] = this.binary_.locate();
  276. } else if (typeof this.binary_ === 'string') {
  277. firefoxOptions['binary'] = this.binary_;
  278. }
  279. }
  280. if (this.profile_) {
  281. // If the user specified a template directory or any extensions to
  282. // install, we need to encode the profile as a base64 string (which
  283. // requires writing it to disk first). Otherwise, if the user just
  284. // specified some custom preferences, we can send those directly.
  285. let profile = this.profile_;
  286. if (profile.getTemplateDir() || profile.getExtensions().length) {
  287. firefoxOptions['profile'] = profile.encode();
  288. } else {
  289. let prefs = profile.getPreferences();
  290. if (Object.keys(prefs).length) {
  291. firefoxOptions['prefs'] = prefs;
  292. }
  293. }
  294. }
  295. return caps;
  296. }
  297. }
  298. /**
  299. * Enum of available command contexts.
  300. *
  301. * Command contexts are specific to Marionette, and may be used with the
  302. * {@link #context=} method. Contexts allow you to direct all subsequent
  303. * commands to either "content" (default) or "chrome". The latter gives
  304. * you elevated security permissions.
  305. *
  306. * @enum {string}
  307. */
  308. const Context = {
  309. CONTENT: "content",
  310. CHROME: "chrome",
  311. };
  312. const GECKO_DRIVER_EXE =
  313. process.platform === 'win32' ? 'geckodriver.exe' : 'geckodriver';
  314. /**
  315. * @return {string} .
  316. * @throws {Error}
  317. */
  318. function findGeckoDriver() {
  319. let exe = io.findInPath(GECKO_DRIVER_EXE, true);
  320. if (!exe) {
  321. throw Error(
  322. 'The ' + GECKO_DRIVER_EXE + ' executable could not be found on the current ' +
  323. 'PATH. Please download the latest version from ' +
  324. 'https://github.com/mozilla/geckodriver/releases/ ' +
  325. 'and ensure it can be found on your PATH.');
  326. }
  327. return exe;
  328. }
  329. function normalizeProxyConfiguration(config) {
  330. if ('manual' === config.proxyType) {
  331. if (config.ftpProxy && !config.ftpProxyPort) {
  332. let hostAndPort = net.splitHostAndPort(config.ftpProxy);
  333. config.ftpProxy = hostAndPort.host;
  334. config.ftpProxyPort = hostAndPort.port;
  335. }
  336. if (config.httpProxy && !config.httpProxyPort) {
  337. let hostAndPort = net.splitHostAndPort(config.httpProxy);
  338. config.httpProxy = hostAndPort.host;
  339. config.httpProxyPort = hostAndPort.port;
  340. }
  341. if (config.sslProxy && !config.sslProxyPort) {
  342. let hostAndPort = net.splitHostAndPort(config.sslProxy);
  343. config.sslProxy = hostAndPort.host;
  344. config.sslProxyPort = hostAndPort.port;
  345. }
  346. if (config.socksProxy && !config.socksProxyPort) {
  347. let hostAndPort = net.splitHostAndPort(config.socksProxy);
  348. config.socksProxy = hostAndPort.host;
  349. config.socksProxyPort = hostAndPort.port;
  350. }
  351. } else if ('pac' === config.proxyType) {
  352. if (config.proxyAutoconfigUrl && !config.pacUrl) {
  353. config.pacUrl = config.proxyAutoconfigUrl;
  354. }
  355. }
  356. return config;
  357. }
  358. /** @enum {string} */
  359. const ExtensionCommand = {
  360. GET_CONTEXT: 'getContext',
  361. SET_CONTEXT: 'setContext',
  362. };
  363. /**
  364. * Creates a command executor with support for Marionette's custom commands.
  365. * @param {!Promise<string>} serverUrl The server's URL.
  366. * @return {!command.Executor} The new command executor.
  367. */
  368. function createExecutor(serverUrl) {
  369. let client = serverUrl.then(url => new http.HttpClient(url));
  370. let executor = new http.Executor(client);
  371. configureExecutor(executor);
  372. return executor;
  373. }
  374. /**
  375. * Configures the given executor with Firefox-specific commands.
  376. * @param {!http.Executor} executor the executor to configure.
  377. */
  378. function configureExecutor(executor) {
  379. executor.defineCommand(
  380. ExtensionCommand.GET_CONTEXT,
  381. 'GET',
  382. '/session/:sessionId/moz/context');
  383. executor.defineCommand(
  384. ExtensionCommand.SET_CONTEXT,
  385. 'POST',
  386. '/session/:sessionId/moz/context');
  387. }
  388. /**
  389. * Creates {@link selenium-webdriver/remote.DriverService} instances that manage
  390. * a [geckodriver](https://github.com/mozilla/geckodriver) server in a child
  391. * process.
  392. */
  393. class ServiceBuilder extends remote.DriverService.Builder {
  394. /**
  395. * @param {string=} opt_exe Path to the server executable to use. If omitted,
  396. * the builder will attempt to locate the geckodriver on the system PATH.
  397. */
  398. constructor(opt_exe) {
  399. super(opt_exe || findGeckoDriver());
  400. this.setLoopback(true); // Required.
  401. }
  402. /**
  403. * Enables verbose logging.
  404. *
  405. * @param {boolean=} opt_trace Whether to enable trace-level logging. By
  406. * default, only debug logging is enabled.
  407. * @return {!ServiceBuilder} A self reference.
  408. */
  409. enableVerboseLogging(opt_trace) {
  410. return this.addArguments(opt_trace ? '-vv' : '-v');
  411. }
  412. }
  413. /**
  414. * A WebDriver client for Firefox.
  415. */
  416. class Driver extends webdriver.WebDriver {
  417. /**
  418. * Creates a new Firefox session.
  419. *
  420. * @param {(Options|capabilities.Capabilities|Object)=} opt_config The
  421. * configuration options for this driver, specified as either an
  422. * {@link Options} or {@link capabilities.Capabilities}, or as a raw hash
  423. * object.
  424. * @param {(http.Executor|remote.DriverService)=} opt_executor Either a
  425. * pre-configured command executor to use for communicating with an
  426. * externally managed remote end (which is assumed to already be running),
  427. * or the `DriverService` to use to start the geckodriver in a child
  428. * process.
  429. *
  430. * If an executor is provided, care should e taken not to use reuse it with
  431. * other clients as its internal command mappings will be updated to support
  432. * Firefox-specific commands.
  433. *
  434. * _This parameter may only be used with Mozilla's GeckoDriver._
  435. *
  436. * @param {promise.ControlFlow=} opt_flow The flow to
  437. * schedule commands through. Defaults to the active flow object.
  438. * @throws {Error} If a custom command executor is provided and the driver is
  439. * configured to use the legacy FirefoxDriver from the Selenium project.
  440. * @return {!Driver} A new driver instance.
  441. */
  442. static createSession(opt_config, opt_executor, opt_flow) {
  443. let caps;
  444. if (opt_config instanceof Options) {
  445. caps = opt_config.toCapabilities();
  446. } else {
  447. caps = new capabilities.Capabilities(opt_config);
  448. }
  449. if (caps.has(capabilities.Capability.PROXY)) {
  450. let proxy =
  451. normalizeProxyConfiguration(caps.get(capabilities.Capability.PROXY));
  452. caps.set(capabilities.Capability.PROXY, proxy);
  453. }
  454. let executor;
  455. let onQuit;
  456. if (opt_executor instanceof http.Executor) {
  457. executor = opt_executor;
  458. configureExecutor(executor);
  459. } else if (opt_executor instanceof remote.DriverService) {
  460. executor = createExecutor(opt_executor.start());
  461. onQuit = () => opt_executor.kill();
  462. } else {
  463. let service = new ServiceBuilder().build();
  464. executor = createExecutor(service.start());
  465. onQuit = () => service.kill();
  466. }
  467. return /** @type {!Driver} */(super.createSession(
  468. executor, caps, opt_flow, onQuit));
  469. }
  470. /**
  471. * This function is a no-op as file detectors are not supported by this
  472. * implementation.
  473. * @override
  474. */
  475. setFileDetector() {
  476. }
  477. /**
  478. * Get the context that is currently in effect.
  479. *
  480. * @return {!promise.Thenable<Context>} Current context.
  481. */
  482. getContext() {
  483. return this.schedule(
  484. new command.Command(ExtensionCommand.GET_CONTEXT),
  485. 'get WebDriver.context');
  486. }
  487. /**
  488. * Changes target context for commands between chrome- and content.
  489. *
  490. * Changing the current context has a stateful impact on all subsequent
  491. * commands. The {@link Context.CONTENT} context has normal web
  492. * platform document permissions, as if you would evaluate arbitrary
  493. * JavaScript. The {@link Context.CHROME} context gets elevated
  494. * permissions that lets you manipulate the browser chrome itself,
  495. * with full access to the XUL toolkit.
  496. *
  497. * Use your powers wisely.
  498. *
  499. * @param {!promise.Thenable<void>} ctx The context to switch to.
  500. */
  501. setContext(ctx) {
  502. return this.schedule(
  503. new command.Command(ExtensionCommand.SET_CONTEXT)
  504. .setParameter("context", ctx),
  505. 'set WebDriver.context');
  506. }
  507. }
  508. // PUBLIC API
  509. exports.Binary = Binary;
  510. exports.Channel = Channel;
  511. exports.Context = Context;
  512. exports.Driver = Driver;
  513. exports.Options = Options;
  514. exports.Profile = Profile;
  515. exports.ServiceBuilder = ServiceBuilder;