opera.js

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