opera.js

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 a {@linkplain Driver WebDriver} client for the
20 * Opera web browser (v26+). Before using this module, you must download the
21 * latest OperaDriver
22 * [release](https://github.com/operasoftware/operachromiumdriver/releases) and
23 * ensure it can be found on your system
24 * [PATH](http://en.wikipedia.org/wiki/PATH_%28variable%29).
25 *
26 * There are three primary classes exported by this module:
27 *
28 * 1. {@linkplain ServiceBuilder}: configures the
29 * {@link selenium-webdriver/remote.DriverService remote.DriverService}
30 * that manages the
31 * [OperaDriver](https://github.com/operasoftware/operachromiumdriver)
32 * child process.
33 *
34 * 2. {@linkplain Options}: defines configuration options for each new Opera
35 * session, such as which {@linkplain Options#setProxy proxy} to use,
36 * what {@linkplain Options#addExtensions extensions} to install, or
37 * what {@linkplain Options#addArguments command-line switches} to use when
38 * starting the browser.
39 *
40 * 3. {@linkplain Driver}: the WebDriver client; each new instance will control
41 * a unique browser session with a clean user profile (unless otherwise
42 * configured through the {@link Options} class).
43 *
44 * By default, every Opera session will use a single driver service, which is
45 * started the first time a {@link Driver} instance is created and terminated
46 * when this process exits. The default service will inherit its environment
47 * from the current process and direct all output to /dev/null. You may obtain
48 * a handle to this default service using
49 * {@link #getDefaultService getDefaultService()} and change its configuration
50 * with {@link #setDefaultService setDefaultService()}.
51 *
52 * You may also create a {@link Driver} with its own driver service. This is
53 * useful if you need to capture the server's log output for a specific session:
54 *
55 * var opera = require('selenium-webdriver/opera');
56 *
57 * var service = new opera.ServiceBuilder()
58 * .loggingTo('/my/log/file.txt')
59 * .enableVerboseLogging()
60 * .build();
61 *
62 * var options = new opera.Options();
63 * // configure browser options ...
64 *
65 * var driver = new opera.Driver(options, service);
66 *
67 * Users should only instantiate the {@link Driver} class directly when they
68 * need a custom driver service configuration (as shown above). For normal
69 * operation, users should start Opera using the
70 * {@link selenium-webdriver.Builder}.
71 */
72
73'use strict';
74
75var fs = require('fs'),
76 util = require('util');
77
78var webdriver = require('./index'),
79 executors = require('./executors'),
80 io = require('./io'),
81 portprober = require('./net/portprober'),
82 remote = require('./remote');
83
84
85/**
86 * Name of the OperaDriver executable.
87 * @type {string}
88 * @const
89 */
90var OPERADRIVER_EXE =
91 process.platform === 'win32' ? 'operadriver.exe' : 'operadriver';
92
93
94/**
95 * Creates {@link remote.DriverService} instances that manages an
96 * [OperaDriver](https://github.com/operasoftware/operachromiumdriver)
97 * server in a child process.
98 *
99 * @param {string=} opt_exe Path to the server executable to use. If omitted,
100 * the builder will attempt to locate the operadriver on the current
101 * PATH.
102 * @throws {Error} If provided executable does not exist, or the operadriver
103 * cannot be found on the PATH.
104 * @constructor
105 */
106var ServiceBuilder = function(opt_exe) {
107 /** @private {string} */
108 this.exe_ = opt_exe || io.findInPath(OPERADRIVER_EXE, true);
109 if (!this.exe_) {
110 throw Error(
111 'The OperaDriver could not be found on the current PATH. Please ' +
112 'download the latest version of the OperaDriver from ' +
113 'https://github.com/operasoftware/operachromiumdriver/releases and ' +
114 'ensure it can be found on your PATH.');
115 }
116
117 if (!fs.existsSync(this.exe_)) {
118 throw Error('File does not exist: ' + this.exe_);
119 }
120
121 /** @private {!Array.<string>} */
122 this.args_ = [];
123 this.stdio_ = 'ignore';
124};
125
126
127/** @private {number} */
128ServiceBuilder.prototype.port_ = 0;
129
130
131/** @private {(string|!Array.<string|number|!Stream|null|undefined>)} */
132ServiceBuilder.prototype.stdio_ = 'ignore';
133
134
135/** @private {Object.<string, string>} */
136ServiceBuilder.prototype.env_ = null;
137
138
139/**
140 * Sets the port to start the OperaDriver on.
141 * @param {number} port The port to use, or 0 for any free port.
142 * @return {!ServiceBuilder} A self reference.
143 * @throws {Error} If the port is invalid.
144 */
145ServiceBuilder.prototype.usingPort = function(port) {
146 if (port < 0) {
147 throw Error('port must be >= 0: ' + port);
148 }
149 this.port_ = port;
150 return this;
151};
152
153
154/**
155 * Sets the path of the log file the driver should log to. If a log file is
156 * not specified, the driver will log to stderr.
157 * @param {string} path Path of the log file to use.
158 * @return {!ServiceBuilder} A self reference.
159 */
160ServiceBuilder.prototype.loggingTo = function(path) {
161 this.args_.push('--log-path=' + path);
162 return this;
163};
164
165
166/**
167 * Enables verbose logging.
168 * @return {!ServiceBuilder} A self reference.
169 */
170ServiceBuilder.prototype.enableVerboseLogging = function() {
171 this.args_.push('--verbose');
172 return this;
173};
174
175
176/**
177 * Silence sthe drivers output.
178 * @return {!ServiceBuilder} A self reference.
179 */
180ServiceBuilder.prototype.silent = function() {
181 this.args_.push('--silent');
182 return this;
183};
184
185
186/**
187 * Defines the stdio configuration for the driver service. See
188 * {@code child_process.spawn} for more information.
189 * @param {(string|!Array.<string|number|!Stream|null|undefined>)} config The
190 * configuration to use.
191 * @return {!ServiceBuilder} A self reference.
192 */
193ServiceBuilder.prototype.setStdio = function(config) {
194 this.stdio_ = config;
195 return this;
196};
197
198
199/**
200 * Defines the environment to start the server under. This settings will be
201 * inherited by every browser session started by the server.
202 * @param {!Object.<string, string>} env The environment to use.
203 * @return {!ServiceBuilder} A self reference.
204 */
205ServiceBuilder.prototype.withEnvironment = function(env) {
206 this.env_ = env;
207 return this;
208};
209
210
211/**
212 * Creates a new DriverService using this instance's current configuration.
213 * @return {remote.DriverService} A new driver service using this instance's
214 * current configuration.
215 * @throws {Error} If the driver exectuable was not specified and a default
216 * could not be found on the current PATH.
217 */
218ServiceBuilder.prototype.build = function() {
219 var port = this.port_ || portprober.findFreePort();
220 var args = this.args_.concat(); // Defensive copy.
221
222 return new remote.DriverService(this.exe_, {
223 loopback: true,
224 port: port,
225 args: webdriver.promise.when(port, function(port) {
226 return args.concat('--port=' + port);
227 }),
228 env: this.env_,
229 stdio: this.stdio_
230 });
231};
232
233
234/** @type {remote.DriverService} */
235var defaultService = null;
236
237
238/**
239 * Sets the default service to use for new OperaDriver instances.
240 * @param {!remote.DriverService} service The service to use.
241 * @throws {Error} If the default service is currently running.
242 */
243function setDefaultService(service) {
244 if (defaultService && defaultService.isRunning()) {
245 throw Error(
246 'The previously configured OperaDriver service is still running. ' +
247 'You must shut it down before you may adjust its configuration.');
248 }
249 defaultService = service;
250}
251
252
253/**
254 * Returns the default OperaDriver service. If such a service has not been
255 * configured, one will be constructed using the default configuration for
256 * a OperaDriver executable found on the system PATH.
257 * @return {!remote.DriverService} The default OperaDriver service.
258 */
259function getDefaultService() {
260 if (!defaultService) {
261 defaultService = new ServiceBuilder().build();
262 }
263 return defaultService;
264}
265
266
267/**
268 * @type {string}
269 * @const
270 */
271var OPTIONS_CAPABILITY_KEY = 'chromeOptions';
272
273
274/**
275 * Class for managing {@link Driver OperaDriver} specific options.
276 * @constructor
277 * @extends {webdriver.Serializable}
278 */
279var Options = function() {
280 webdriver.Serializable.call(this);
281
282 /** @private {!Array.<string>} */
283 this.args_ = [];
284
285 /** @private {!Array.<(string|!Buffer)>} */
286 this.extensions_ = [];
287};
288util.inherits(Options, webdriver.Serializable);
289
290
291/**
292 * Extracts the OperaDriver specific options from the given capabilities
293 * object.
294 * @param {!webdriver.Capabilities} capabilities The capabilities object.
295 * @return {!Options} The OperaDriver options.
296 */
297Options.fromCapabilities = function(capabilities) {
298 var options;
299 var o = capabilities.get(OPTIONS_CAPABILITY_KEY);
300 if (o instanceof Options) {
301 options = o;
302 } else if (o) {
303 options.
304 addArguments(o.args || []).
305 addExtensions(o.extensions || []).
306 setOperaBinaryPath(o.binary);
307 } else {
308 options = new Options;
309 }
310
311 if (capabilities.has(webdriver.Capability.PROXY)) {
312 options.setProxy(capabilities.get(webdriver.Capability.PROXY));
313 }
314
315 if (capabilities.has(webdriver.Capability.LOGGING_PREFS)) {
316 options.setLoggingPrefs(
317 capabilities.get(webdriver.Capability.LOGGING_PREFS));
318 }
319
320 return options;
321};
322
323
324/**
325 * Add additional command line arguments to use when launching the Opera
326 * browser. Each argument may be specified with or without the "--" prefix
327 * (e.g. "--foo" and "foo"). Arguments with an associated value should be
328 * delimited by an "=": "foo=bar".
329 * @param {...(string|!Array.<string>)} var_args The arguments to add.
330 * @return {!Options} A self reference.
331 */
332Options.prototype.addArguments = function(var_args) {
333 this.args_ = this.args_.concat.apply(this.args_, arguments);
334 return this;
335};
336
337
338/**
339 * Add additional extensions to install when launching Opera. Each extension
340 * should be specified as the path to the packed CRX file, or a Buffer for an
341 * extension.
342 * @param {...(string|!Buffer|!Array.<(string|!Buffer)>)} var_args The
343 * extensions to add.
344 * @return {!Options} A self reference.
345 */
346Options.prototype.addExtensions = function(var_args) {
347 this.extensions_ = this.extensions_.concat.apply(
348 this.extensions_, arguments);
349 return this;
350};
351
352
353/**
354 * Sets the path to the Opera binary to use. On Mac OS X, this path should
355 * reference the actual Opera executable, not just the application binary. The
356 * binary path be absolute or relative to the operadriver server executable, but
357 * it must exist on the machine that will launch Opera.
358 *
359 * @param {string} path The path to the Opera binary to use.
360 * @return {!Options} A self reference.
361 */
362Options.prototype.setOperaBinaryPath = function(path) {
363 this.binary_ = path;
364 return this;
365};
366
367
368/**
369 * Sets the logging preferences for the new session.
370 * @param {!webdriver.logging.Preferences} prefs The logging preferences.
371 * @return {!Options} A self reference.
372 */
373Options.prototype.setLoggingPrefs = function(prefs) {
374 this.logPrefs_ = prefs;
375 return this;
376};
377
378
379/**
380 * Sets the proxy settings for the new session.
381 * @param {webdriver.ProxyConfig} proxy The proxy configuration to use.
382 * @return {!Options} A self reference.
383 */
384Options.prototype.setProxy = function(proxy) {
385 this.proxy_ = proxy;
386 return this;
387};
388
389
390/**
391 * Converts this options instance to a {@link webdriver.Capabilities} object.
392 * @param {webdriver.Capabilities=} opt_capabilities The capabilities to merge
393 * these options into, if any.
394 * @return {!webdriver.Capabilities} The capabilities.
395 */
396Options.prototype.toCapabilities = function(opt_capabilities) {
397 var capabilities = opt_capabilities || webdriver.Capabilities.opera();
398 capabilities.
399 set(webdriver.Capability.PROXY, this.proxy_).
400 set(webdriver.Capability.LOGGING_PREFS, this.logPrefs_).
401 set(OPTIONS_CAPABILITY_KEY, this);
402 return capabilities;
403};
404
405
406/**
407 * Converts this instance to its JSON wire protocol representation. Note this
408 * function is an implementation not intended for general use.
409 * @return {{args: !Array.<string>,
410 * binary: (string|undefined),
411 * detach: boolean,
412 * extensions: !Array.<(string|!webdriver.promise.Promise.<string>))>,
413 * localState: (Object|undefined),
414 * logPath: (string|undefined),
415 * prefs: (Object|undefined)}} The JSON wire protocol representation
416 * of this instance.
417 * @override
418 */
419Options.prototype.serialize = function() {
420 var json = {
421 args: this.args_,
422 extensions: this.extensions_.map(function(extension) {
423 if (Buffer.isBuffer(extension)) {
424 return extension.toString('base64');
425 }
426 return webdriver.promise.checkedNodeCall(
427 fs.readFile, extension, 'base64');
428 })
429 };
430 if (this.binary_) {
431 json.binary = this.binary_;
432 }
433 if (this.logFile_) {
434 json.logPath = this.logFile_;
435 }
436 if (this.prefs_) {
437 json.prefs = this.prefs_;
438 }
439
440 return json;
441};
442
443
444/**
445 * Creates a new WebDriver client for Opera.
446 *
447 * @param {(webdriver.Capabilities|Options)=} opt_config The configuration
448 * options.
449 * @param {remote.DriverService=} opt_service The session to use; will use
450 * the {@link getDefaultService default service} by default.
451 * @param {webdriver.promise.ControlFlow=} opt_flow The control flow to use, or
452 * {@code null} to use the currently active flow.
453 * @constructor
454 * @extends {webdriver.WebDriver}
455 */
456var Driver = function(opt_config, opt_service, opt_flow) {
457 var service = opt_service || getDefaultService();
458 var executor = executors.createExecutor(service.start());
459
460 var capabilities =
461 opt_config instanceof Options ? opt_config.toCapabilities() :
462 (opt_config || webdriver.Capabilities.opera());
463
464 // On Linux, the OperaDriver does not look for Opera on the PATH, so we
465 // must explicitly find it. See: operachromiumdriver #9.
466 if (process.platform === 'linux') {
467 var options = Options.fromCapabilities(capabilities);
468 if (!options.binary_) {
469 options.setOperaBinaryPath(io.findInPath('opera', true));
470 }
471 capabilities = options.toCapabilities(capabilities);
472 }
473
474 var driver = webdriver.WebDriver.createSession(
475 executor, capabilities, opt_flow);
476
477 webdriver.WebDriver.call(
478 this, driver.getSession(), executor, driver.controlFlow());
479};
480util.inherits(Driver, webdriver.WebDriver);
481
482
483/**
484 * This function is a no-op as file detectors are not supported by this
485 * implementation.
486 * @override
487 */
488Driver.prototype.setFileDetector = function() {
489};
490
491
492// PUBLIC API
493
494
495exports.Driver = Driver;
496exports.Options = Options;
497exports.ServiceBuilder = ServiceBuilder;
498exports.getDefaultService = getDefaultService;
499exports.setDefaultService = setDefaultService;