# 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 |
75 | var fs = require('fs'),
76 | util = require('util');
77 |
78 | var 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 | */
90 | var 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 | */
106 | var 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} */
128 | ServiceBuilder.prototype.port_ = 0;
129 |
130 |
131 | /** @private {(string|!Array.<string|number|!Stream|null|undefined>)} */
132 | ServiceBuilder.prototype.stdio_ = 'ignore';
133 |
134 |
135 | /** @private {Object.<string, string>} */
136 | ServiceBuilder.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 | */
145 | ServiceBuilder.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 | */
160 | ServiceBuilder.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 | */
170 | ServiceBuilder.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 | */
180 | ServiceBuilder.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 | */
193 | ServiceBuilder.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 | */
205 | ServiceBuilder.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 | */
218 | ServiceBuilder.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} */
235 | var 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 | */
243 | function 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 | */
259 | function getDefaultService() {
260 | if (!defaultService) {
261 | defaultService = new ServiceBuilder().build();
262 | }
263 | return defaultService;
264 | }
265 |
266 |
267 | /**
268 | * @type {string}
269 | * @const
270 | */
271 | var OPTIONS_CAPABILITY_KEY = 'chromeOptions';
272 |
273 |
274 | /**
275 | * Class for managing {@link Driver OperaDriver} specific options.
276 | * @constructor
277 | * @extends {webdriver.Serializable}
278 | */
279 | var 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 | };
288 | util.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 | */
297 | Options.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 | */
332 | Options.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 | */
346 | Options.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 | */
362 | Options.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 | */
373 | Options.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 | */
384 | Options.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 | */
396 | Options.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 | */
419 | Options.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 | */
456 | var 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 | };
480 | util.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 | */
488 | Driver.prototype.setFileDetector = function() {
489 | };
490 |
491 |
492 | // PUBLIC API
493 |
494 |
495 | exports.Driver = Driver;
496 | exports.Options = Options;
497 | exports.ServiceBuilder = ServiceBuilder;
498 | exports.getDefaultService = getDefaultService;
499 | exports.setDefaultService = setDefaultService;