diff --git a/.gitignore b/.gitignore index b33ca8c7fbe77..8d8c27acb8817 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ common/build cpp/iedriver/IEReturnTypes.h java/client/src/org/openqa/selenium/ie/IeReturnTypes.java javascript/deps.js +javascript/node/selenium-webdriver/node_modules/ .idea/vcs.xml .idea/misc.xml .idea/workspace.xml diff --git a/Rakefile b/Rakefile index f6aa724cb8ae4..3c8b6b2b16c38 100644 --- a/Rakefile +++ b/Rakefile @@ -562,6 +562,39 @@ namespace :docs do end end +namespace :node do + task :deploy => [ + "//javascript/firefox-driver:webdriver", + "//javascript/webdriver:asserts_lib", + "//javascript/webdriver:webdriver_lib", + "//javascript/webdriver:unit_test_lib" + ] do + js = Javascript::BaseJs.new + # Get JS lib deps, excluding those need to build the FirefoxDriver. + deps = js.build_deps("", Rake::Task["//javascript/webdriver:asserts_lib"], []) + deps = js.build_deps("", Rake::Task["//javascript/webdriver:webdriver_lib"], deps) + deps = js.build_deps("", Rake::Task["//javascript/webdriver:unit_test_lib"], deps) + deps.uniq! + + cmd = "node javascript/node/deploy.js" << + " --output=build/javascript/node/selenium-webdriver" << + " --resource=COPYING:/COPYING" << + " --resource=javascript/firefox-driver/webdriver.json:firefox/webdriver.json" << + " --resource=build/javascript/firefox-driver/webdriver.xpi:firefox/webdriver.xpi" << + " --resource=third_party/closure/LICENSE:goog/LICENSE" << + " --resource=common/src/web/:test/data/" << + " --exclude_resource=common/src/web/Bin" << + " --exclude_resource=.gitignore" << + " --root=javascript" << + " --root=third_party/closure" << + " --lib=third_party/closure/goog" << + " --lib=" << deps.join(" --lib=") << + " --src=javascript/node/selenium-webdriver" + + sh cmd + end +end + namespace :safari do desc "Build the SafariDriver extension" task :extension => [ "//javascript/safari-driver:SafariDriver" ] diff --git a/javascript/node/build.desc b/javascript/node/build.desc index 5eec6836ccab2..1a07a0e557b66 100644 --- a/javascript/node/build.desc +++ b/javascript/node/build.desc @@ -1,21 +1,3 @@ -node_module( - name = "selenium-webdriver", - srcdir = "selenium-webdriver", - deps = [ - "//javascript/webdriver:asserts_lib", - "//javascript/webdriver:webdriver_lib", - "//javascript/webdriver:unit_test_lib", - ], - content_roots = [ - "javascript", - "third_party/closure" - ], - resources = [ - { "COPYING" : "/COPYING" }, - { "third_party/closure/LICENSE" : "goog/LICENSE" }, - { "common/src/web/" : "test/data/" }, - ], - exclude_resources = [ - "common/src/web/Bin", - "\.gitignore", - ]) +rake_task(name = "selenium-webdriver", + task_name = "node:deploy", + out = "javascript/node/selenium-webdriver") diff --git a/javascript/node/deploy.js b/javascript/node/deploy.js index 54200ab63d3d2..5dcb9dd12814b 100644 --- a/javascript/node/deploy.js +++ b/javascript/node/deploy.js @@ -172,6 +172,9 @@ function processLibraryFiles(filePaths, contentRoots) { function copySrcs(srcDir, outputDirPath) { var filePaths = fs.readdirSync(srcDir); filePaths.forEach(function(filePath) { + if (filePath === 'node_modules') { + return; + } filePath = path.join(srcDir, filePath); if (fs.statSync(filePath).isDirectory()) { copySrcs(filePath, path.join(outputDirPath, path.basename(filePath))); @@ -377,7 +380,11 @@ function generateDocs(outputDir, callback) { 'readme': path.join(outputDir, 'README.md'), 'language': 'ES5', 'sources': sourceFiles, - 'modules': moduleFiles + 'modules': moduleFiles, + 'excludes': [ + path.join(outputDir, 'docs'), + path.join(outputDir, 'node_modules') + ] }; var configFile = outputDir + '-docs.json'; diff --git a/javascript/node/selenium-webdriver/.npmignore b/javascript/node/selenium-webdriver/.npmignore new file mode 100644 index 0000000000000..d5700888a3a2e --- /dev/null +++ b/javascript/node/selenium-webdriver/.npmignore @@ -0,0 +1,2 @@ +node_modules/ + diff --git a/javascript/node/selenium-webdriver/CHANGES.md b/javascript/node/selenium-webdriver/CHANGES.md index 899703b7095a0..b51460026cb94 100644 --- a/javascript/node/selenium-webdriver/CHANGES.md +++ b/javascript/node/selenium-webdriver/CHANGES.md @@ -1,5 +1,7 @@ ## v2.43.0-dev +* Added native support for Firefox - the Java Selenium server is no longer + required. * Added support for generator functions to `ControlFlow#execute` and `ControlFlow#wait`. For more information, see documentation on `webdriver.promise.consume`. Requires harmony support (run with diff --git a/javascript/node/selenium-webdriver/builder.js b/javascript/node/selenium-webdriver/builder.js index 9fa3850f8947c..48cd9496e2b17 100644 --- a/javascript/node/selenium-webdriver/builder.js +++ b/javascript/node/selenium-webdriver/builder.js @@ -64,6 +64,9 @@ var Builder = function() { /** @private {chrome.Options} */ this.chromeOptions_ = null; + + /** @private {firefox.Options} */ + this.firefoxOptions_ = null; }; @@ -218,6 +221,21 @@ Builder.prototype.setChromeOptions = function(options) { }; +/** + * Sets Firefox-specific options for drivers created by this builder. Any + * logging or proxy settings defined on the given options will take precedence + * over those set through {@link #setLoggingPrefs} and {@link #setProxy}, + * respectively. + * + * @param {!firefox.Options} options The FirefoxDriver options to use. + * @return {!Builder} A self reference. + */ +Builder.prototype.setFirefoxOptions = function(options) { + this.firefoxOptions_ = options; + return this; +}; + + /** * Sets the control flow that created drivers should execute actions in. If * the flow is never set, or is set to {@code null}, it will use the active @@ -264,6 +282,10 @@ Builder.prototype.build = function() { capabilities.merge(this.chromeOptions_.toCapabilities()); } + if (browser === Browser.FIREFOX && this.firefoxOptions_) { + capabilities.merge(this.firefoxOptions_.toCapabilities()); + } + // Check for a remote browser. var url = process.env.SELENIUM_REMOTE_URL || this.url_; if (url) { @@ -279,6 +301,12 @@ Builder.prototype.build = function() { var chrome = require('./chrome'); return new chrome.Driver(capabilities, null, this.flow_); + case Browser.FIREFOX: + // Requiring 'firefox' above would create a cycle: + // index -> builder -> firefox -> index + var firefox = require('./firefox'); + return new firefox.Driver(capabilities, this.flow_); + case Browser.PHANTOM_JS: // Requiring 'phantomjs' would create a cycle: // index -> builder -> phantomjs -> index diff --git a/javascript/node/selenium-webdriver/firefox/binary.js b/javascript/node/selenium-webdriver/firefox/binary.js new file mode 100644 index 0000000000000..9f51200265f5f --- /dev/null +++ b/javascript/node/selenium-webdriver/firefox/binary.js @@ -0,0 +1,260 @@ +// Copyright 2014 Selenium committers +// Copyright 2014 Software Freedom Conservancy +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +var child = require('child_process'), + path = require('path'), + util = require('util'); + +var promise = require('..').promise, + _base = require('../_base'), + io = require('../io'), + exec = require('../io/exec'); + + + +/** @const */ +var NO_FOCUS_LIB_X86 = _base.isDevMode() ? + path.join(__dirname, '../../../../cpp/prebuilt/i386/libnoblur.so') : + path.join(__dirname, 'libnoblur.so') ; + +/** @const */ +var NO_FOCUS_LIB_AMD64 = _base.isDevMode() ? + path.join(__dirname, '../../../../cpp/prebuilt/amd64/libnoblur64.so') : + path.join(__dirname, 'libnoblur64.so') ; + +var X_IGNORE_NO_FOCUS_LIB = 'x_ignore_nofocus.so'; + +var foundBinary = null; + + +/** + * Checks the default Windows Firefox locations in Program Files. + * @return {!promise.Promise.} A promise for the located executable. + * The promise will resolve to {@code null} if Fireox was not found. + */ +function defaultWindowsLocation() { + var files = [ + process.env['PROGRAMFILES'] || 'C:\\Program Files', + process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)' + ].map(function(prefix) { + return path.join(prefix, 'Mozilla Firefox\\firefox.exe'); + }); + return io.exists(files[0]).then(function(exists) { + return exists ? files[0] : io.exists(files[1]).then(function(exists) { + return exists ? files[1] : null; + }); + }); +} + + +/** + * Locates the Firefox binary for the current system. + * @return {!promise.Promise.} A promise for the located binary. The + * promise will be rejected if Firefox cannot be located. + */ +function findFirefox() { + if (foundBinary) { + return foundBinary; + } + + if (process.platform === 'darwin') { + var osxExe = '/Applications/Firefox.app/Contents/MacOS/firefox-bin'; + foundBinary = io.exists(osxExe).then(function(exists) { + return exists ? osxExe : null; + }); + } else if (process.platform === 'win32') { + foundBinary = defaultWindowsLocation(); + } else { + foundBinary = promise.fulfill(io.findInPath('firefox')); + } + + return foundBinary = foundBinary.then(function(found) { + if (found) { + return found; + } + throw Error('Could not locate Firefox on the current system'); + }); +} + + +/** + * Copies the no focus libs into the given profile directory. + * @param {string} profileDir Path to the profile directory to install into. + * @return {!promise.Promise.} The LD_LIBRARY_PATH prefix string to use + * for the installed libs. + */ +function installNoFocusLibs(profileDir) { + var x86 = path.join(profileDir, 'x86'); + var amd64 = path.join(profileDir, 'amd64'); + + return mkdir(x86) + .then(copyLib.bind(null, NO_FOCUS_LIB_X86, x86)) + .then(mkdir.bind(null, amd64)) + .then(copyLib.bind(null, NO_FOCUS_LIB_AMD64, amd64)) + .then(function() { + return x86 + ':' + amd64; + }); + + function mkdir(dir) { + return io.exists(dir).then(function(exists) { + if (!exists) { + return promise.checkedNodeCall(fs.mkdir, dir); + } + }); + } + + function copyLib(src, dir) { + return io.copy(src, path.join(dir, X_IGNORE_NO_FOCUS_LIB)); + } +} + + +/** + * Silently runs Firefox to install a profile directory (which is assumed to be + * defined in the given environment variables). + * @param {string} firefox Path to the Firefox executable. + * @param {!Object.} env The environment variables to use. + * @return {!promise.Promise} A promise for when the profile has been installed. + */ +function installProfile(firefox, env) { + var installed = promise.defer(); + child.exec(firefox + ' -silent', {env: env, timeout: 180 * 1000}, + function(err) { + if (err) { + installed.reject(new Error( + 'Failed to install Firefox profile: ' + err)); + return; + } + installed.fulfill(); + }); + return installed.promise; +} + + +/** + * Manages a Firefox subprocess configured for use with WebDriver. + * @param {string=} opt_exe Path to the Firefox binary to use. If not + * specified, will attempt to locate Firefox on the current system. + * @constructor + */ +var Binary = function(opt_exe) { + /** @private {(string|undefined)} */ + this.exe_ = opt_exe; + + /** @private {!Array.} */ + this.args_ = []; + + /** @private {!Object.} */ + this.env_ = {}; + Object.keys(process.env).forEach(function(key) { + this.env_[key] = process.env[key]; + }.bind(this)); + this.env_['MOZ_CRASHREPORTER_DISABLE'] = '1'; + this.env_['MOZ_NO_REMOTE'] = '1'; + this.env_['NO_EM_RESTART'] = '1'; + + /** @private {promise.Promise.} */ + this.command_ = null; +}; + + +/** + * Add arguments to the command line used to start Firefox. + * @param {...(string|!Array.)} var_args Either the arguments to add as + * varargs, or the arguments as an array. + */ +Binary.prototype.addArguments = function(var_args) { + for (var i = 0; i < arguments.length; i++) { + if (util.isArray(arguments[i])) { + this.args_ = this.args_.concat(arguments[i]); + } else { + this.args_.push(arguments[i]); + } + } +}; + + +/** + * Launches Firefox and eturns a promise that will be fulfilled when the process + * terminates. + * @param {string} profile Path to the profile directory to use. + * @return {!promise.Promise.} A promise for the process result. + * @throws {Error} If this instance has already been started. + */ +Binary.prototype.launch = function(profile) { + if (this.command_) { + throw Error('Firefox is already running'); + } + + var env = {}; + Object.keys(this.env_).forEach(function(key) { + env[key] = this.env_[key]; + }.bind(this)); + env['XRE_PROFILE_PATH'] = profile; + + var args = ['-foreground'].concat(this.args_); + + var self = this; + + this.command_ = promise.when(this.exe_ || findFirefox(), function(firefox) { + if (process.platform === 'win32' || process.platform === 'darwin') { + return firefox; + } + return installNoFocusLibs().then(function(ldLibraryPath) { + env['LD_LIBRARY_PATH'] = ldLibraryPath + ':' + env['LD_LIBRARY_PATH']; + env['LD_PRELOAD'] = X_IGNORE_NO_FOCUS_LIB; + return firefox; + }); + }).then(function(firefox) { + var install = exec(firefox, {args: ['-silent'], env: env}); + return install.result().then(function(result) { + if (result.code !== 0) { + throw Error( + 'Failed to install profile; firefox terminated with ' + result); + } + + return exec(firefox, {args: args, env: env}); + }); + }); + + return this.command_.then(function() { + // Don't return the actual command handle, just a promise to signal it has + // been started. + }); +}; + + +/** + * Kills the managed Firefox process. + * @return {!promise.Promise} A promise for when the process has terminated. + */ +Binary.prototype.kill = function() { + if (!this.command_) { + return promise.defer(); // Not running. + } + return this.command_.then(function(command) { + command.kill(); + return command.result(); + }); +}; + + +// PUBLIC API + + +exports.Binary = Binary; + diff --git a/javascript/node/selenium-webdriver/firefox/extension.js b/javascript/node/selenium-webdriver/firefox/extension.js new file mode 100644 index 0000000000000..c2244216a83db --- /dev/null +++ b/javascript/node/selenium-webdriver/firefox/extension.js @@ -0,0 +1,184 @@ +// Copyright 2014 Selenium committers +// Copyright 2014 Software Freedom Conservancy +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** @fileoverview Utilities for working with Firefox extensions. */ + +'use strict'; + +var AdmZip = require('adm-zip'), + fs = require('fs'), + path = require('path'), + util = require('util'), + xml = require('xml2js'); + +var promise = require('..').promise, + checkedCall = promise.checkedNodeCall, + io = require('../io'); + + +/** + * Thrown when there an add-on is malformed. + * @param {string} msg The error message. + * @constructor + * @extends {Error} + */ +function AddonFormatError(msg) { + Error.call(this); + + Error.captureStackTrace(this, AddonFormatError); + + /** @override */ + this.name = AddonFormatError.name; + + /** @override */ + this.message = msg; +} +util.inherits(AddonFormatError, Error); + + + +/** + * Installs an extension to the given directory. + * @param {string} extension Path to the extension to install, as either a xpi + * file or a directory. + * @param {string} dir Path to the directory to install the extension in. + * @return {!promise.Promise.} A promise for the add-on ID once + * installed. + */ +function install(extension, dir) { + return getDetails(extension).then(function(details) { + function returnId() { return details.id; } + + var dst = path.join(dir, details.id); + if (extension.slice(-4) === '.xpi') { + if (!details.unpack) { + return io.copy(extension, dst + '.xpi').then(returnId); + } else { + return checkedCall(fs.readFile, extension).then(function(buff) { + var zip = new AdmZip(buff); + // TODO: find an async library for inflating a zip archive. + new AdmZip(buff).extractAllTo(dst, true); + }).then(returnId); + } + } else { + return io.copyDir(extension, dst).then(returnId); + } + }); +} + + +/** + * Describes a Firefox add-on. + * @typedef {{id: string, name: string, version: string, unpack: boolean}} + */ +var AddonDetails; + + +/** + * Extracts the details needed to install an add-on. + * @param {string} addonPath Path to the extension directory. + * @return {!promise.Promise.} A promise for the add-on details. + */ +function getDetails(addonPath) { + return readManifest(addonPath).then(function(doc) { + var em = getNamespaceId(doc, 'http://www.mozilla.org/2004/em-rdf#'); + var rdf = getNamespaceId( + doc, 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'); + + var description = doc[rdf + 'RDF'][rdf + 'Description'][0]; + var details = { + id: getNodeText(description, em + 'id'), + name: getNodeText(description, em + 'name'), + version: getNodeText(description, em + 'version'), + unpack: getNodeText(description, em + 'unpack') || false + }; + + if (typeof details.unpack === 'string') { + details.unpack = details.unpack.toLowerCase() === 'true'; + } + + if (!details.id) { + throw new AddonFormatError('Could not find add-on ID for ' + addonPath); + } + + return details; + }); + + function getNodeText(node, name) { + return node[name] && node[name][0] || ''; + } + + function getNamespaceId(doc, url) { + var keys = Object.keys(doc); + if (keys.length !== 1) { + throw new AddonFormatError('Malformed manifest for add-on ' + addonPath); + } + + var namespaces = doc[keys[0]].$; + var id = ''; + Object.keys(namespaces).some(function(ns) { + if (namespaces[ns] !== url) { + return false; + } + + if (ns.indexOf(':') != -1) { + id = ns.split(':')[1] + ':'; + } + return true; + }); + return id; + } +} + + +/** + * Reads the manifest for a Firefox add-on. + * @param {string} addonPath Path to a Firefox add-on as a xpi or an extension. + * @return {!promise.Promise.} A promise for the parsed manifest. + */ +function readManifest(addonPath) { + var manifest; + + if (addonPath.slice(-4) === '.xpi') { + manifest = checkedCall(fs.readFile, addonPath).then(function(buff) { + var zip = new AdmZip(buff); + if (!zip.getEntry('install.rdf')) { + throw new AddonFormatError( + 'Could not find install.rdf in ' + addonPath); + } + var done = promise.defer(); + zip.readAsTextAsync('install.rdf', done.fulfill); + return done.promise; + }); + } else { + manifest = checkedCall(fs.stat, addonPath).then(function(stats) { + if (!stats.isDirectory()) { + throw Error( + 'Add-on path is niether a xpi nor a directory: ' + addonPath); + } + return checkedCall(fs.readFile, path.join(addonPath, 'install.rdf')); + }); + } + + return manifest.then(function(content) { + return checkedCall(xml.parseString, content); + }); +} + + +// PUBLIC API + + +exports.install = install; diff --git a/javascript/node/selenium-webdriver/firefox/index.js b/javascript/node/selenium-webdriver/firefox/index.js new file mode 100644 index 0000000000000..b780b5a146f96 --- /dev/null +++ b/javascript/node/selenium-webdriver/firefox/index.js @@ -0,0 +1,202 @@ +// Copyright 2014 Selenium committers +// Copyright 2014 Software Freedom Conservancy +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +var url = require('url'), + util = require('util'); + +var Binary = require('./binary').Binary, + Profile = require('./profile').Profile, + decodeProfile = require('./profile').decode, + webdriver = require('..'), + executors = require('../executors'), + httpUtil = require('../http/util'), + net = require('../net'), + portprober = require('../net/portprober'); + + +/** + * Configuration options for the FirefoxDriver. + * @constructor + */ +var Options = function() { + /** @private {Profile} */ + this.profile_ = null; + + /** @private {Binary} */ + this.binary_ = null; + + /** @private {webdriver.logging.Preferences} */ + this.logPrefs_ = null; + + /** @private {webdriver.ProxyConfig} */ + this.proxy_ = null; +}; + + +/** + * Sets the profile to use. The profile may be specified as a + * {@link Profile} object or as the path to an existing Firefox profile to use + * as a template. + * + * @param {(string|!Profile)} profile The profile to use. + * @return {!Options} A self reference. + */ +Options.prototype.setProfile = function(profile) { + if (typeof profile === 'string') { + profile = new Profile(profile); + } + this.profile_ = profile; + return this; +}; + + +/** + * Sets the binary to use. The binary may be specified as the path to a Firefox + * executable, or as a {@link Binary} object. + * + * @param {(string|!Binary)} binary The binary to use. + * @return {!Options} A self reference. + */ +Options.prototype.setBinary = function(binary) { + if (typeof binary === 'string') { + binary = new Binary(binary); + } + this.binary_ = binary; + return this; +}; + + +/** + * Sets the logging preferences for the new session. + * @param {webdriver.logging.Preferences} prefs The logging preferences. + * @return {!Options} A self reference. + */ +Options.prototype.setLoggingPreferences = function(prefs) { + this.logPrefs_ = prefs; + return this; +}; + + +/** + * Sets the proxy to use. + * + * @param {webdriver.ProxyConfig} proxy The proxy configuration to use. + * @return {!Options} A self reference. + */ +Options.prototype.setProxy = function(proxy) { + this.proxy_ = proxy; + return this; +}; + + +/** + * Converts these options to a {@link webdriver.Capabilities} instance. + * + * @return {!webdriver.Capabilities} A new capabilities object. + */ +Options.prototype.toCapabilities = function(opt_remote) { + var caps = webdriver.Capabilities.firefox(); + if (this.logPrefs_) { + caps.set(webdriver.Capability.LOGGING_PREFS, this.logPrefs_); + } + if (this.proxy_) { + caps.set(webdriver.Capability.PROXY, this.proxy_); + } + if (this.binary_) { + caps.set('firefox_binary', this.binary_); + } + if (this.profile_) { + caps.set('firefox_profile', this.profile_); + } + return caps; +}; + + +/** + * A WebDriver client for Firefox. + * + * @param {(Options|webdriver.Capabilities|Object)=} opt_config The + * configuration options for this driver, specified as either an + * {@link Options} or {@link webdriver.Capabilities}, or as a raw hash + * object. + * @param {webdriver.promise.ControlFlow=} opt_flow The flow to + * schedule commands through. Defaults to the active flow object. + * @constructor + * @extends {webdriver.WebDriver} + */ +var Driver = function(opt_config, opt_flow) { + var caps; + if (opt_config instanceof Options) { + caps = opt_config.toCapabilities(); + } else { + caps = new webdriver.Capabilities(opt_config); + } + + var binary = caps.get('firefox_binary') || new Binary(); + if (typeof binary === 'string') { + binary = new Binary(binary); + } + + var profile = caps.get('firefox_profile') || new Profile(); + + caps.set('firefox_binary', null); + caps.set('firefox_profile', null); + + var serverUrl = portprober.findFreePort().then(function(port) { + var prepareProfile; + if (typeof profile === 'string') { + prepareProfile = decodeProfile(profile).then(function(dir) { + var profile = new Profile(dir); + profile.setPreference('webdriver_firefox_port', port); + return profile.writeToDisk(); + }); + } else { + profile.setPreference('webdriver_firefox_port', port); + prepareProfile = profile.writeToDisk(); + } + + return prepareProfile.then(function(dir) { + return binary.launch(dir); + }).then(function() { + var serverUrl = url.format({ + protocol: 'http', + hostname: net.getLoopbackAddress(), + port: port, + pathname: '/hub' + }); + + return httpUtil.waitForServer(serverUrl, 45 * 1000).then(function() { + return serverUrl; + }); + }); + }); + + var executor = executors.createExecutor(serverUrl); + var driver = webdriver.WebDriver.createSession(executor, caps, opt_flow); + + webdriver.WebDriver.call(this, driver.getSession(), executor, opt_flow); +}; +util.inherits(Driver, webdriver.WebDriver); + + +// PUBLIC API + + +exports.Binary = Binary; +exports.Driver = Driver; +exports.Options = Options; +exports.Profile = Profile; diff --git a/javascript/node/selenium-webdriver/firefox/profile.js b/javascript/node/selenium-webdriver/firefox/profile.js new file mode 100644 index 0000000000000..32cacb8409a2a --- /dev/null +++ b/javascript/node/selenium-webdriver/firefox/profile.js @@ -0,0 +1,408 @@ +// Copyright 2014 Selenium committers +// Copyright 2014 Software Freedom Conservancy +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +var AdmZip = require('adm-zip'), + fs = require('fs'), + path = require('path'), + vm = require('vm'); + +var promise = require('..').promise, + _base = require('../_base'), + io = require('../io'), + extension = require('./extension'); + + +/** @const */ +var WEBDRIVER_PREFERENCES_PATH = _base.isDevMode() + ? path.join(__dirname, '../../../firefox-driver/webdriver.json') + : path.join(__dirname, '../lib/firefox/webdriver.json'); + +/** @const */ +var WEBDRIVER_EXTENSION_PATH = _base.isDevMode() + ? path.join(__dirname, + '../../../../build/javascript/firefox-driver/webdriver.xpi') + : path.join(__dirname, '../lib/firefox/webdriver.xpi'); + +/** @const */ +var WEBDRIVER_EXTENSION_NAME = 'fxdriver@googlecode.com'; + + + +/** @type {Object} */ +var defaultPreferences = null; + +/** + * Synchronously loads the default preferences used for the FirefoxDriver. + * @return {!Object} The default preferences JSON object. + */ +function getDefaultPreferences() { + if (!defaultPreferences) { + var contents = fs.readFileSync(WEBDRIVER_PREFERENCES_PATH, 'utf8'); + defaultPreferences = JSON.parse(contents); + } + return defaultPreferences; +} + + +/** + * Parses a user.js file in a Firefox profile directory. + * @param {string} f Path to the file to parse. + * @return {!promise.Promise.} A promise for the parsed preferences as + * a JSON object. If the file does not exist, an empty object will be + * returned. + */ +function loadUserPrefs(f) { + var done = promise.defer(); + fs.readFile(f, function(err, contents) { + if (err && err.code === 'ENOENT') { + done.fulfill({}); + return; + } + + if (err) { + done.reject(err); + return; + } + + var prefs = {}; + var context = vm.createContext({ + 'user_pref': function(key, value) { + prefs[key] = value; + } + }); + + vm.runInContext(contents, context, f); + done.fulfill(prefs); + }); + return done.promise; +} + + +/** + * Copies the properties of one object into another. + * @param {!Object} a The destination object. + * @param {!Object} b The source object to apply as a mixin. + */ +function mixin(a, b) { + Object.keys(b).forEach(function(key) { + a[key] = b[key]; + }); +} + + +/** + * @param {!Object} defaults The default preferences to write. Will be + * overridden by user.js preferences in the template directory and the + * frozen preferences required by WebDriver. + * @param {string} dir Path to the directory write the file to. + * @return {!promise.Promise.} A promise for the profile directory, + * to be fulfilled when user preferences have been written. + */ +function writeUserPrefs(prefs, dir) { + var userPrefs = path.join(dir, 'user.js'); + return loadUserPrefs(userPrefs).then(function(overrides) { + mixin(prefs, overrides); + mixin(prefs, getDefaultPreferences()['frozen']); + + var contents = Object.keys(prefs).map(function(key) { + return 'user_pref(' + JSON.stringify(key) + ', ' + + JSON.stringify(prefs[key]) + ');'; + }).join('\n'); + + var done = promise.defer(); + fs.writeFile(userPrefs, contents, function(err) { + err && done.reject(err) || done.fulfill(dir); + }); + return done.promise; + }); +}; + + +/** + * Installs a group of extensions in the given profile directory. If the + * WebDriver extension is not included in this set, the default version + * bundled with this package will be installed. + * @param {!Array.} extensions The extensions to install, as a + * path to an unpacked extension directory or a path to a xpi file. + * @param {string} dir The profile directory to install to. + * @param {boolean=} opt_excludeWebDriverExt Whether to skip installation of + * the default WebDriver extension. + * @return {!promise.Promise.} A promise for the main profile directory + * once all extensions have been installed. + */ +function installExtensions(extensions, dir, opt_excludeWebDriverExt) { + var hasWebDriver = !!opt_excludeWebDriverExt; + var next = 0; + var extensionDir = path.join(dir, 'extensions'); + var done = promise.defer(); + + return io.exists(extensionDir).then(function(exists) { + if (!exists) { + return promise.checkedNodeCall(fs.mkdir, extensionDir); + } + }).then(function() { + installNext(); + return done.promise; + }); + + function installNext() { + if (!done.isPending()) { + return; + } + + if (next >= extensions.length) { + if (hasWebDriver) { + done.fulfill(dir); + } else { + install(WEBDRIVER_EXTENSION_PATH); + } + } else { + install(extensions[next++]); + } + } + + function install(ext) { + extension.install(ext, extensionDir).then(function(id) { + hasWebDriver = hasWebDriver || (id === WEBDRIVER_EXTENSION_NAME); + installNext(); + }, done.reject); + } +} + + +/** + * Decodes a base64 encoded profile. + * @param {string} data The base64 encoded string. + * @return {!promise.Promise.} A promise for the path to the decoded + * profile directory. + */ +function decode(data) { + return io.tmpFile().then(function(file) { + var buf = new Buffer(data, 'base64'); + return promise.checkedNodeCall(fs.writeFile, file, buf).then(function() { + return io.tmpDir(); + }).then(function(dir) { + var zip = new AdmZip(file); + zip.extractAllTo(dir); // Sync only? Why?? :-( + return dir; + }); + }); +} + + + +/** + * Models a Firefox proifle directory for use with the FirefoxDriver. The + * {@code Proifle} directory uses an in-memory model until {@link #writeToDisk} + * is called. + * @param {string=} opt_dir Path to an existing Firefox profile directory to + * use a template for this profile. If not specified, a blank profile will + * be used. + * @constructor + */ +var Profile = function(opt_dir) { + /** @private {!Object} */ + this.preferences_ = {}; + + mixin(this.preferences_, getDefaultPreferences()['mutable']); + mixin(this.preferences_, getDefaultPreferences()['frozen']); + + /** @private {boolean} */ + this.nativeEventsEnabled_ = true; + + /** @private {(string|undefined)} */ + this.template_ = opt_dir; + + /** @private {number} */ + this.port_ = 0; + + /** @private {!Array.} */ + this.extensions_ = []; +}; + + +/** + * Registers an extension to be included with this profile. + * @param {string} extension Path to the extension to include, as either an + * unpacked extension directory or the path to a xpi file. + */ +Profile.prototype.addExtension = function(extension) { + this.extensions_.push(extension); +}; + + +/** + * Sets a desired preference for this profile. + * @param {string} key The preference key. + * @param {(string|number|boolean)} value The preference value. + * @throws {Error} If attempting to set a frozen preference. + */ +Profile.prototype.setPreference = function(key, value) { + var frozen = getDefaultPreferences()['frozen']; + if (frozen.hasOwnProperty(key) && frozen[key] !== value) { + throw Error('You may not set ' + key + '=' + JSON.stringify(value) + + '; value is frozen for proper WebDriver functionality (' + + key + '=' + JSON.stringify(frozen[key]) + ')'); + } + this.preferences_[key] = value; +}; + + +/** + * Returns the currently configured value of a profile preference. This does + * not include any defaults defined in the profile's template directory user.js + * file (if a template were specified on construction). + * @param {string} key The desired preference. + * @return {(string|number|boolean|undefined)} The current value of the + * requested preference. + */ +Profile.prototype.getPreference = function(key) { + return this.preferences_[key]; +}; + + +/** + * @return {number} The port this profile is currently configured to use, or + * 0 if the port will be selected at random when the profile is written + * to disk. + */ +Profile.prototype.getPort = function() { + return this.port_; +}; + + +/** + * Sets the port to use for the WebDriver extension loaded by this profile. + * @param {number} port The desired port, or 0 to use any free port. + */ +Profile.prototype.setPort = function(port) { + this.port_ = port; +}; + + +/** + * @return {boolean} Whether the FirefoxDriver is configured to automatically + * accept untrusted SSL certificates. + */ +Profile.prototype.acceptUntrustedCerts = function() { + return !!this.preferences_['webdriver_accept_untrusted_certs']; +}; + + +/** + * Sets whether the FirefoxDriver should automatically accept untrusted SSL + * certificates. + * @param {boolean} value . + */ +Profile.prototype.setAcceptUntrustedCerts = function(value) { + this.preferences_['webdriver_accept_untrusted_certs'] = !!value; +}; + + +/** + * Sets whether to assume untrusted certificates come from untrusted issuers. + * @param {boolean} value . + */ +Profile.prototype.setAssumeUntrustedCertIssuer = function(value) { + this.preferences_['webdriver_assume_untrusted_issuer'] = !!value; +}; + + +/** + * @return {boolean} Whether to assume untrusted certs come from untrusted + * issuers. + */ +Profile.prototype.assumeUntrustedCertIssuer = function() { + return !!this.preferences_['webdriver_assume_untrusted_issuer']; +}; + + +/** + * Sets whether to use native events with this profile. + * @param {boolean} enabled . + */ +Profile.prototype.setNativeEventsEnabled = function(enabled) { + this.nativeEventsEnabled_ = enabled; +}; + + +/** + * Returns whether native events are enabled in this profile. + * @return {boolean} . + */ +Profile.prototype.nativeEventsEnabled = function() { + return this.nativeEventsEnabled_; +}; + + +/** + * Writes this profile to disk. + * @param {boolean=} opt_excludeWebDriverExt Whether to exclude the WebDriver + * extension from the generated profile. Used to reduce the size of an + * {@link #encode() encoded profile} since the server will always install + * the extension itself. + * @return {!promise.Promise.} A promise for the path to the new + * profile directory. + */ +Profile.prototype.writeToDisk = function(opt_excludeWebDriverExt) { + var profileDir = io.tmpDir(); + if (this.template_) { + profileDir = profileDir.then(function(dir) { + return io.copyDir( + this.template_, dir, /(parent\.lock|lock|\.parentlock)/); + }.bind(this)); + } + + // Freeze preferences for async operations. + var prefs = {}; + mixin(prefs, this.preferences_); + + // Freeze extensions for async operations. + var extensions = this.extensions_.concat(); + + return profileDir.then(function(dir) { + return writeUserPrefs(prefs, dir); + }).then(function(dir) { + return installExtensions(extensions, dir, !!opt_excludeWebDriverExt); + }); +}; + + +/** + * Encodes this profile as a zipped, base64 encoded directory. + * @return {!promise.Promise.} A promise for the encoded profile. + */ +Profile.prototype.encode = function() { + return this.writeToDisk(true).then(function(dir) { + var zip = new AdmZip(); + zip.addLocalFolder(dir, ''); + return io.tmpFile().then(function(file) { + zip.writeZip(file); // Sync! Why oh why :-( + return promise.checkedNodeCall(fs.readFile, file); + }); + }).then(function(data) { + return new Buffer(data).toString('base64'); + }); +}; + + +// PUBLIC API + + +exports.Profile = Profile; +exports.decode = decode; +exports.loadUserPrefs = loadUserPrefs; diff --git a/javascript/node/selenium-webdriver/io/exec.js b/javascript/node/selenium-webdriver/io/exec.js index db8cb18bb527f..df0b189903549 100644 --- a/javascript/node/selenium-webdriver/io/exec.js +++ b/javascript/node/selenium-webdriver/io/exec.js @@ -58,6 +58,13 @@ var Result = function(code, signal) { }; +/** @override */ +Result.prototype.toString = function() { + return 'Result(code=' + this.code + ', signal=' + this.signal + ')'; +}; + + + /** * Represents a command running in a sub-process. * @param {!promise.Promise.} result The command result. diff --git a/javascript/node/selenium-webdriver/io/index.js b/javascript/node/selenium-webdriver/io/index.js index 5d5a3de4e5043..ffe39c844fb04 100644 --- a/javascript/node/selenium-webdriver/io/index.js +++ b/javascript/node/selenium-webdriver/io/index.js @@ -14,15 +14,121 @@ // limitations under the License. var fs = require('fs'), - path = require('path'); + ncp = require('ncp').ncp, + path = require('path'), + tmp = require('tmp'); + +var promise = require('..').promise; var PATH_SEPARATOR = process.platform === 'win32' ? ';' : ':'; +/** + * Creates the specified directory and any necessary parent directories. No + * action is taken if the directory already exists. + * @param {string} dir The directory to create. + * @param {function(Error)} callback Callback function; accepts a single error + * or {@code null}. + */ +function createDirectories(dir, callback) { + fs.mkdir(dir, function(err) { + if (!err || err.code === 'EEXIST') { + callback(); + } else { + createDirectories(path.dirname(dir), function(err) { + err && callback(err) || fs.mkdir(dir, callback); + }); + } + }); +}; + // PUBLIC API + +/** + * Copies one file to another. + * @param {string} src The source file. + * @param {string} dst The destination file. + * @return {!promise.Promise.} A promise for the copied file's path. + */ +exports.copy = function(src, dst) { + var copied = promise.defer(); + + var rs = fs.createReadStream(src); + rs.on('error', copied.reject); + rs.on('end', function() { + copied.fulfill(dst); + }); + + var ws = fs.createWriteStream(dst); + ws.on('error', copied.reject); + + rs.pipe(ws); + + return copied.promise; +}; + + +/** + * Recursively copies the contents of one directory to another. + * @param {string} src The source directory to copy. + * @param {string} dst The directory to copy into. + * @param {(RegEx|function(string): boolean)=} opt_exclude An exclusion filter + * as either a regex or predicate function. All files matching this filter + * will not be copied. + * @return {!promise.Promise.} A promise for the destination + * directory's path once all files have been copied. + */ +exports.copyDir = function(src, dst, opt_exclude) { + var predicate = opt_exclude; + if (opt_exclude && typeof opt_exclude !== 'function') { + predicate = function(p) { + return !opt_exclude.test(p); + }; + } + + var copied = promise.defer(); + ncp(src, dst, {filter: predicate}, function(err) { + err && copied.reject(err) || copied.fulfill(dst); + }); + return copied.promise; +}; + + +/** + * Tests if a file path exists. + * @param {string} path The path to test. + * @return {!promise.Promise.} A promise for whether the file exists. + */ +exports.exists = function(path) { + var result = promise.defer(); + fs.exists(path, result.fulfill); + return result.promise; +}; + + +/** + * @return {!promise.Promise.} A promise for the path to a temporary + * directory. + * @see https://www.npmjs.org/package/tmp + */ +exports.tmpDir = function() { + return promise.checkedNodeCall(tmp.dir); +}; + + +/** + * @return {!promise.Promise.} A promise for the path to a temporary + * file. + * @see https://www.npmjs.org/package/tmp + */ +exports.tmpFile = function() { + return promise.checkedNodeCall(tmp.file); +}; + + /** * Searches the {@code PATH} environment variable for the given file. * @param {string} file The file to locate on the PATH. diff --git a/javascript/node/selenium-webdriver/lib/test/data/firefox/jetpack-sample.xpi b/javascript/node/selenium-webdriver/lib/test/data/firefox/jetpack-sample.xpi new file mode 100644 index 0000000000000..84d6493dd4f56 Binary files /dev/null and b/javascript/node/selenium-webdriver/lib/test/data/firefox/jetpack-sample.xpi differ diff --git a/javascript/node/selenium-webdriver/lib/test/data/firefox/sample.xpi b/javascript/node/selenium-webdriver/lib/test/data/firefox/sample.xpi new file mode 100644 index 0000000000000..062f9a1722450 Binary files /dev/null and b/javascript/node/selenium-webdriver/lib/test/data/firefox/sample.xpi differ diff --git a/javascript/node/selenium-webdriver/lib/test/index.js b/javascript/node/selenium-webdriver/lib/test/index.js index 500c777515682..1a435a0731fc6 100644 --- a/javascript/node/selenium-webdriver/lib/test/index.js +++ b/javascript/node/selenium-webdriver/lib/test/index.js @@ -17,8 +17,10 @@ var assert = require('assert'); -var webdriver = require('../..'), +var build = require('./build'), + webdriver = require('../..'), flow = webdriver.promise.controlFlow(), + _base = require('../../_base'), testing = require('../../testing'), fileserver = require('./fileserver'), seleniumserver = require('./seleniumserver'); @@ -39,6 +41,7 @@ var Browser = { // Browsers that should always be tested via the java Selenium server. REMOTE_CHROME: 'remote.chrome', + REMOTE_FIREFOX: 'remote.firefox', REMOTE_PHANTOMJS: 'remote.phantomjs' }; @@ -49,12 +52,13 @@ var Browser = { */ var NATIVE_BROWSERS = [ Browser.CHROME, + Browser.FIREFOX, Browser.PHANTOMJS ]; var browsersToTest = (function() { - var browsers = process.env['SELENIUM_BROWSERS'] || Browser.CHROME; + var browsers = process.env['SELENIUM_BROWSERS'] || Browser.FIREFOX; browsers = browsers.split(','); browsers.forEach(function(browser) { if (browser === Browser.IOS) { @@ -219,7 +223,11 @@ function suite(fn, opt_options) { if (!serverToUse) { serverToUse = seleniumServer = new seleniumserver.Server(); } - testing.before(seleniumServer.start.bind(seleniumServer, 60 * 1000)); + testing.before(function() { + // Starting the server may require a build, so disable timeouts. + this.timeout(0); + return seleniumServer.start(60 * 1000); + }); } var env = new TestEnvironment(browser, serverToUse); @@ -249,6 +257,12 @@ function suite(fn, opt_options) { testing.before(fileserver.start); testing.after(fileserver.stop); +if (_base.isDevMode() && browsersToTest.indexOf(Browser.FIREFOX) != -1) { + testing.before(function() { + return build.of('//javascript/firefox-driver:webdriver').onlyOnce().go(); + }); +} + // Server is only started if required for a specific config. testing.after(function() { if (seleniumServer) { diff --git a/javascript/node/selenium-webdriver/lib/test/seleniumserver.js b/javascript/node/selenium-webdriver/lib/test/seleniumserver.js index 932a02a9d1d96..79259727157c3 100644 --- a/javascript/node/selenium-webdriver/lib/test/seleniumserver.js +++ b/javascript/node/selenium-webdriver/lib/test/seleniumserver.js @@ -17,6 +17,7 @@ var assert = require('assert'), fs = require('fs'), + path = require('path'), util = require('util'); var promise = require('../..').promise, @@ -25,8 +26,8 @@ var promise = require('../..').promise, build = require('./build'); -var DEV_MODE_JAR_PATH = - 'build/java/server/src/org/openqa/grid/selenium/selenium-standalone.jar'; +var DEV_MODE_JAR_PATH = path.join(__dirname, '../../../../..', + 'build/java/server/src/org/openqa/grid/selenium/selenium-standalone.jar'); var SELENIUM_SERVER_JAR_ENV = 'SELENIUM_SERVER_JAR'; var PROD_MODE_JAR_PATH = process.env[SELENIUM_SERVER_JAR_ENV]; @@ -59,7 +60,8 @@ function getProdModeJarPath() { function Server() { var jarPath = isDevMode ? DEV_MODE_JAR_PATH : getProdModeJarPath(); RemoteServer.call(this, jarPath, { - port: 0 + port: 0, + stdio: 'inherit' }); } util.inherits(Server, RemoteServer); diff --git a/javascript/node/selenium-webdriver/package.json b/javascript/node/selenium-webdriver/package.json index 5adbf6523f00f..b885f1ee691fa 100644 --- a/javascript/node/selenium-webdriver/package.json +++ b/javascript/node/selenium-webdriver/package.json @@ -21,10 +21,16 @@ "engines": { "node": ">= 0.8.x" }, + "dependencies": { + "adm-zip": "0.4.4", + "ncp": "0.6.0", + "tmp": "0.0.24", + "xml2js": "0.4.4" + }, "devDependencies" : { "mocha" : "~1.10.0" }, "scripts": { - "test": "node_modules/mocha/bin/mocha -R list --timeout=10000 --recursive test" + "test": "npm install && node_modules/mocha/bin/mocha -R list -t 120000 --recursive test" } } diff --git a/javascript/node/selenium-webdriver/test/firefox/extension_test.js b/javascript/node/selenium-webdriver/test/firefox/extension_test.js new file mode 100644 index 0000000000000..dbfae96614e05 --- /dev/null +++ b/javascript/node/selenium-webdriver/test/firefox/extension_test.js @@ -0,0 +1,94 @@ +// Copyright 2014 Selenium committers +// Copyright 2014 Software Freedom Conservancy +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +var AdmZip = require('adm-zip'), + assert = require('assert'), + crypto = require('crypto'), + fs = require('fs'), + path = require('path'); + +var extension = require('../../firefox/extension'), + io = require('../../io'), + it = require('../../testing').it; + + +var JETPACK_EXTENSION = path.join(__dirname, + '../../lib/test/data/firefox/jetpack-sample.xpi'); +var NORMAL_EXTENSION = path.join(__dirname, + '../../lib/test/data/firefox/sample.xpi'); + +var JETPACK_EXTENSION_ID = 'jid1-EaXX7k0wwiZR7w@jetpack'; +var NORMAL_EXTENSION_ID = 'sample@seleniumhq.org'; + + +describe('extension', function() { + it('can install a jetpack xpi file', function() { + return io.tmpDir().then(function(dir) { + return extension.install(JETPACK_EXTENSION, dir).then(function(id) { + assert.equal(JETPACK_EXTENSION_ID, id); + var file = path.join(dir, id + '.xpi'); + assert.ok(fs.existsSync(file), 'no such file: ' + file); + assert.ok(!fs.statSync(file).isDirectory()); + + var copiedSha1 = crypto.createHash('sha1') + .update(fs.readFileSync(file)) + .digest('hex'); + + var goldenSha1 = crypto.createHash('sha1') + .update(fs.readFileSync(JETPACK_EXTENSION)) + .digest('hex'); + + assert.equal(copiedSha1, goldenSha1); + }); + }); + }); + + it('can install a normal xpi file', function() { + return io.tmpDir().then(function(dir) { + return extension.install(NORMAL_EXTENSION, dir).then(function(id) { + assert.equal(NORMAL_EXTENSION_ID, id); + + var file = path.join(dir, NORMAL_EXTENSION_ID); + assert.ok(fs.statSync(file).isDirectory()); + + assert.ok(fs.existsSync(path.join(file, 'chrome.manifest'))); + assert.ok(fs.existsSync(path.join(file, 'content/overlay.xul'))); + assert.ok(fs.existsSync(path.join(file, 'content/overlay.js'))); + assert.ok(fs.existsSync(path.join(file, 'install.rdf'))); + }); + }); + }); + + it('can install an extension from a directory', function() { + return io.tmpDir().then(function(srcDir) { + var buf = fs.readFileSync(NORMAL_EXTENSION); + new AdmZip(buf).extractAllTo(srcDir, true); + return io.tmpDir().then(function(dstDir) { + return extension.install(srcDir, dstDir).then(function(id) { + assert.equal(NORMAL_EXTENSION_ID, id); + + var dir = path.join(dstDir, NORMAL_EXTENSION_ID); + + assert.ok(fs.existsSync(path.join(dir, 'chrome.manifest'))); + assert.ok(fs.existsSync(path.join(dir, 'content/overlay.xul'))); + assert.ok(fs.existsSync(path.join(dir, 'content/overlay.js'))); + assert.ok(fs.existsSync(path.join(dir, 'install.rdf'))); + }); + }); + }); + }); +}); diff --git a/javascript/node/selenium-webdriver/test/firefox/firefox_test.js b/javascript/node/selenium-webdriver/test/firefox/firefox_test.js new file mode 100644 index 0000000000000..9172babc0c814 --- /dev/null +++ b/javascript/node/selenium-webdriver/test/firefox/firefox_test.js @@ -0,0 +1,94 @@ +// Copyright 2014 Selenium committers +// Copyright 2014 Software Freedom Conservancy +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +var path = require('path'); + +var firefox = require('../../firefox'), + test = require('../../lib/test'), + assert = require('../../testing/assert'); + + +var JETPACK_EXTENSION = path.join(__dirname, + '../../lib/test/data/firefox/jetpack-sample.xpi'); +var NORMAL_EXTENSION = path.join(__dirname, + '../../lib/test/data/firefox/sample.xpi'); + + +test.suite(function(env) { + env.autoCreateDriver = false; + + describe('firefox', function() { + describe('Options', function() { + test.afterEach(function() { + return env.dispose(); + }); + + test.it('can start Firefox with custom preferences', function() { + var profile = new firefox.Profile(); + profile.setPreference('general.useragent.override', 'foo;bar'); + + var options = new firefox.Options().setProfile(profile); + + var driver = env.driver = new firefox.Driver(options); + driver.get('data:text/html,
content
'); + + var userAgent = driver.executeScript( + 'return window.navigator.userAgent'); + assert(userAgent).equalTo('foo;bar'); + }); + + test.it('can start Firefox with a jetpack extension', function() { + var profile = new firefox.Profile(); + profile.addExtension(JETPACK_EXTENSION); + + var options = new firefox.Options().setProfile(profile); + + var driver = env.driver = new firefox.Driver(options); + driver.get('data:text/html,
content
'); + assert(driver.findElement({id: 'jetpack-sample-banner'}).getText()) + .equalTo('Hello, world!'); + }); + + test.it('can start Firefox with a normal extension', function() { + var profile = new firefox.Profile(); + profile.addExtension(NORMAL_EXTENSION); + + var options = new firefox.Options().setProfile(profile); + + var driver = env.driver = new firefox.Driver(options); + driver.get('data:text/html,
content
'); + assert(driver.findElement({id: 'sample-extension-footer'}).getText()) + .equalTo('Goodbye'); + }); + + test.it('can start Firefox with multiple extensions', function() { + var profile = new firefox.Profile(); + profile.addExtension(JETPACK_EXTENSION); + profile.addExtension(NORMAL_EXTENSION); + + var options = new firefox.Options().setProfile(profile); + + var driver = env.driver = new firefox.Driver(options); + driver.get('data:text/html,
content
'); + assert(driver.findElement({id: 'jetpack-sample-banner'}).getText()) + .equalTo('Hello, world!'); + assert(driver.findElement({id: 'sample-extension-footer'}).getText()) + .equalTo('Goodbye'); + }); + }); + }); +}, {browsers: ['firefox']}); \ No newline at end of file diff --git a/javascript/node/selenium-webdriver/test/firefox/profile_test.js b/javascript/node/selenium-webdriver/test/firefox/profile_test.js new file mode 100644 index 0000000000000..588bdaddc9cdb --- /dev/null +++ b/javascript/node/selenium-webdriver/test/firefox/profile_test.js @@ -0,0 +1,185 @@ +// Copyright 2014 Selenium committers +// Copyright 2014 Software Freedom Conservancy +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +var AdmZip = require('adm-zip'), + assert = require('assert'), + fs = require('fs'), + path = require('path'); + +var promise = require('../..').promise, + Profile = require('../../firefox/profile').Profile, + decode = require('../../firefox/profile').decode, + loadUserPrefs = require('../../firefox/profile').loadUserPrefs, + io = require('../../io'), + it = require('../../testing').it; + + +var JETPACK_EXTENSION = path.join(__dirname, + '../../lib/test/data/firefox/jetpack-sample.xpi'); +var NORMAL_EXTENSION = path.join(__dirname, + '../../lib/test/data/firefox/sample.xpi'); + +var JETPACK_EXTENSION_ID = 'jid1-EaXX7k0wwiZR7w@jetpack.xpi'; +var NORMAL_EXTENSION_ID = 'sample@seleniumhq.org'; +var WEBDRIVER_EXTENSION_ID = 'fxdriver@googlecode.com'; + + + +describe('Profile', function() { + describe('setPreference', function() { + it('allows setting custom properties', function() { + var profile = new Profile(); + assert.equal(undefined, profile.getPreference('foo')); + + profile.setPreference('foo', 'bar'); + assert.equal('bar', profile.getPreference('foo')); + }); + + it('allows overriding mutable properties', function() { + var profile = new Profile(); + assert.equal('about:blank', profile.getPreference('browser.newtab.url')); + + profile.setPreference('browser.newtab.url', 'http://www.example.com'); + assert.equal('http://www.example.com', + profile.getPreference('browser.newtab.url')); + }); + + it('throws if setting a frozen preference', function() { + var profile = new Profile(); + assert.throws(function() { + profile.setPreference('app.update.auto', true); + }); + }); + }); + + describe('writeToDisk', function() { + it('copies template directory recursively', function() { + var templateDir; + return io.tmpDir().then(function(td) { + templateDir = td; + var foo = path.join(templateDir, 'foo'); + fs.writeFileSync(foo, 'Hello, world'); + + var bar = path.join(templateDir, 'subfolder/bar'); + fs.mkdirSync(path.dirname(bar)); + fs.writeFileSync(bar, 'Goodbye, world!'); + + return new Profile(templateDir).writeToDisk(); + }).then(function(profileDir) { + assert.notEqual(profileDir, templateDir); + + assert.equal('Hello, world', + fs.readFileSync(path.join(profileDir, 'foo'))); + assert.equal('Goodbye, world!', + fs.readFileSync(path.join(profileDir, 'subfolder/bar'))); + }); + }); + + it('does not copy lock files', function() { + return io.tmpDir().then(function(dir) { + fs.writeFileSync(path.join(dir, 'parent.lock'), 'lock'); + fs.writeFileSync(path.join(dir, 'lock'), 'lock'); + fs.writeFileSync(path.join(dir, '.parentlock'), 'lock'); + return new Profile(dir).writeToDisk(); + }).then(function(dir) { + assert.ok(fs.existsSync(dir)); + assert.ok(!fs.existsSync(path.join(dir, 'parent.lock'))); + assert.ok(!fs.existsSync(path.join(dir, 'lock'))); + assert.ok(!fs.existsSync(path.join(dir, '.parentlock'))); + }); + }); + + describe('user.js', function() { + + it('writes defaults', function() { + return new Profile().writeToDisk().then(function(dir) { + return loadUserPrefs(path.join(dir, 'user.js')); + }).then(function(prefs) { + // Just check a few. + assert.equal(false, prefs['app.update.auto']); + assert.equal(true, prefs['browser.EULA.override']); + assert.equal(false, prefs['extensions.update.enabled']); + assert.equal('about:blank', prefs['browser.newtab.url']); + assert.equal(30, prefs['dom.max_script_run_time']); + }); + }); + + it('merges template user.js into preferences', function() { + return io.tmpDir().then(function(dir) { + fs.writeFileSync(path.join(dir, 'user.js'), [ + 'user_pref("browser.newtab.url", "http://www.example.com")', + 'user_pref("dom.max_script_run_time", 1234)' + ].join('\n')); + + return new Profile(dir).writeToDisk(); + }).then(function(profile) { + return loadUserPrefs(path.join(profile, 'user.js')); + }).then(function(prefs) { + assert.equal('http://www.example.com', prefs['browser.newtab.url']); + assert.equal(1234, prefs['dom.max_script_run_time']); + }); + }); + + it('ignores frozen preferences when merging template user.js', + function() { + return io.tmpDir().then(function(dir) { + fs.writeFileSync(path.join(dir, 'user.js'), + 'user_pref("app.update.auto", true)'); + return new Profile(dir).writeToDisk(); + }).then(function(profile) { + return loadUserPrefs(path.join(profile, 'user.js')); + }).then(function(prefs) { + assert.equal(false, prefs['app.update.auto']); + }); + }); + }); + + describe('extensions', function() { + it('are copied into new profile directory', function() { + var profile = new Profile(); + profile.addExtension(JETPACK_EXTENSION); + profile.addExtension(NORMAL_EXTENSION); + + return profile.writeToDisk().then(function(dir) { + dir = path.join(dir, 'extensions'); + assert.ok(fs.existsSync(path.join(dir, JETPACK_EXTENSION_ID))); + assert.ok(fs.existsSync(path.join(dir, NORMAL_EXTENSION_ID))); + assert.ok(fs.existsSync(path.join(dir, WEBDRIVER_EXTENSION_ID))); + }); + }); + }); + }); + + describe('encode', function() { + it('excludes the bundled WebDriver extension', function() { + return new Profile().encode().then(function(data) { + return decode(data); + }).then(function(dir) { + assert.ok(fs.existsSync(path.join(dir, 'user.js'))); + assert.ok(fs.existsSync(path.join(dir, 'extensions'))); + return loadUserPrefs(path.join(dir, 'user.js')); + }).then(function(prefs) { + // Just check a few. + assert.equal(false, prefs['app.update.auto']); + assert.equal(true, prefs['browser.EULA.override']); + assert.equal(false, prefs['extensions.update.enabled']); + assert.equal('about:blank', prefs['browser.newtab.url']); + assert.equal(30, prefs['dom.max_script_run_time']); + }); + }); + }); +}); diff --git a/javascript/node/selenium-webdriver/test/io_test.js b/javascript/node/selenium-webdriver/test/io_test.js new file mode 100644 index 0000000000000..0066ffb646d8b --- /dev/null +++ b/javascript/node/selenium-webdriver/test/io_test.js @@ -0,0 +1,177 @@ +// Copyright 2014 Selenium committers +// Copyright 2014 Software Freedom Conservancy +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +var assert = require('assert'), + fs = require('fs'), + path = require('path'), + tmp = require('tmp'); + +var io = require('../io'), + before = require('../testing').before, + beforeEach = require('../testing').beforeEach, + it = require('../testing').it; + + +describe('io', function() { + describe('copy', function() { + var tmpDir; + + before(function() { + return io.tmpDir().then(function(d) { + tmpDir = d; + + fs.writeFileSync(path.join(d, 'foo'), 'Hello, world'); + fs.symlinkSync(path.join(d, 'foo'), path.join(d, 'symlinked-foo')); + }); + }); + + it('can copy one file to another', function() { + return io.tmpFile().then(function(f) { + return io.copy(path.join(tmpDir, 'foo'), f).then(function(p) { + assert.equal(p, f); + assert.equal('Hello, world', fs.readFileSync(p)); + }); + }); + }); + + it('can copy symlink to destination', function() { + return io.tmpFile().then(function(f) { + return io.copy(path.join(tmpDir, 'symlinked-foo'), f).then(function(p) { + assert.equal(p, f); + assert.equal('Hello, world', fs.readFileSync(p)); + }); + }); + }); + + it('fails if given a directory as a source', function() { + return io.tmpFile().then(function(f) { + return io.copy(tmpDir, f); + }).then(function() { + throw Error('Should have failed with a type error'); + }, function() { + // Do nothing; expected. + }); + }); + }); + + describe('copyDir', function() { + it('copies recursively', function() { + return io.tmpDir().then(function(dir) { + fs.writeFileSync(path.join(dir, 'file1'), 'hello'); + fs.mkdirSync(path.join(dir, 'sub')); + fs.mkdirSync(path.join(dir, 'sub/folder')); + fs.writeFileSync(path.join(dir, 'sub/folder/file2'), 'goodbye'); + + return io.tmpDir().then(function(dst) { + return io.copyDir(dir, dst).then(function(ret) { + assert.equal(dst, ret); + + assert.equal('hello', + fs.readFileSync(path.join(dst, 'file1'))); + assert.equal('goodbye', + fs.readFileSync(path.join(dst, 'sub/folder/file2'))); + }); + }); + }); + }); + + it('creates destination dir if necessary', function() { + return io.tmpDir().then(function(srcDir) { + fs.writeFileSync(path.join(srcDir, 'foo'), 'hi'); + return io.tmpDir().then(function(dstDir) { + return io.copyDir(srcDir, path.join(dstDir, 'sub')); + }); + }).then(function(p) { + assert.equal('sub', path.basename(p)); + assert.equal('hi', fs.readFileSync(path.join(p, 'foo'))); + }); + }); + + it('supports regex exclusion filter', function() { + return io.tmpDir().then(function(src) { + fs.writeFileSync(path.join(src, 'foo'), 'a'); + fs.writeFileSync(path.join(src, 'bar'), 'b'); + fs.writeFileSync(path.join(src, 'baz'), 'c'); + fs.mkdirSync(path.join(src, 'sub')); + fs.writeFileSync(path.join(src, 'sub/quux'), 'd'); + fs.writeFileSync(path.join(src, 'sub/quot'), 'e'); + + return io.tmpDir().then(function(dst) { + return io.copyDir(src, dst, /(bar|quux)/); + }); + }).then(function(dir) { + assert.equal('a', fs.readFileSync(path.join(dir, 'foo'))); + assert.equal('c', fs.readFileSync(path.join(dir, 'baz'))); + assert.equal('e', fs.readFileSync(path.join(dir, 'sub/quot'))); + + assert.ok(!fs.existsSync(path.join(dir, 'bar'))); + assert.ok(!fs.existsSync(path.join(dir, 'sub/quux'))); + }); + }); + + it('supports exclusion filter function', function() { + return io.tmpDir().then(function(src) { + fs.writeFileSync(path.join(src, 'foo'), 'a'); + fs.writeFileSync(path.join(src, 'bar'), 'b'); + fs.writeFileSync(path.join(src, 'baz'), 'c'); + fs.mkdirSync(path.join(src, 'sub')); + fs.writeFileSync(path.join(src, 'sub/quux'), 'd'); + fs.writeFileSync(path.join(src, 'sub/quot'), 'e'); + + return io.tmpDir().then(function(dst) { + return io.copyDir(src, dst, function(f) { + return f !== path.join(src, 'foo') + && f !== path.join(src, 'sub/quot'); + }); + }); + }).then(function(dir) { + assert.equal('b', fs.readFileSync(path.join(dir, 'bar'))); + assert.equal('c', fs.readFileSync(path.join(dir, 'baz'))); + assert.equal('d', fs.readFileSync(path.join(dir, 'sub/quux'))); + + assert.ok(!fs.existsSync(path.join(dir, 'foo'))); + assert.ok(!fs.existsSync(path.join(dir, 'sub/quot'))); + }); + }); + }); + + describe('exists', function() { + var dir; + + before(function() { + return io.tmpDir().then(function(d) { + dir = d; + }); + }); + + it('works for directories', function() { + return io.exists(dir).then(assert.ok); + }); + + it('works for files', function() { + var file = path.join(dir, 'foo'); + fs.writeFileSync(file, ''); + return io.exists(file).then(assert.ok); + }); + + it('does not return a rejected promise if file does not exist', function() { + return io.exists(path.join(dir, 'not-there')).then(function(exists) { + assert.ok(!exists); + }); + }); + }); +}); diff --git a/javascript/node/selenium-webdriver/test/page_loading_test.js b/javascript/node/selenium-webdriver/test/page_loading_test.js index 812faae44b65d..cd747a44438a0 100644 --- a/javascript/node/selenium-webdriver/test/page_loading_test.js +++ b/javascript/node/selenium-webdriver/test/page_loading_test.js @@ -137,7 +137,12 @@ test.suite(function(env) { then(function() { throw Error('Should have timed out on page load'); }, function(e) { - assert(e.code).equalTo(ErrorCode.SCRIPT_TIMEOUT); + // The FirefoxDriver returns TIMEOUT directly, where as the + // java server returns SCRIPT_TIMEOUT (bug?). + if (e.code !== ErrorCode.SCRIPT_TIMEOUT && + e.code !== ErrorCode.TIMEOUT) { + throw Error('Unexpected error response: ' + e); + } }); }).then(resetPageLoad, function(err) { resetPageLoad().thenFinally(function() { diff --git a/javascript/webdriver/promise.js b/javascript/webdriver/promise.js index cbbbe1a34bd23..7e9890a562797 100644 --- a/javascript/webdriver/promise.js +++ b/javascript/webdriver/promise.js @@ -696,17 +696,21 @@ webdriver.promise.rejected = function(opt_reason) { * If the call fails, the returned promise will be rejected, otherwise it will * be resolved with the result. * @param {!Function} fn The function to wrap. + * @param {...?} var_args The arguments to apply to the function, excluding the + * final callback. * @return {!webdriver.promise.Promise} A promise that will be resolved with the * result of the provided function's callback. */ -webdriver.promise.checkedNodeCall = function(fn) { +webdriver.promise.checkedNodeCall = function(fn, var_args) { var deferred = new webdriver.promise.Deferred(function() { throw Error('This Deferred may not be cancelled'); }); try { - fn(function(error, value) { + var args = goog.array.slice(arguments, 1); + args.push(function(error, value) { error ? deferred.reject(error) : deferred.fulfill(value); }); + fn.apply(null, args); } catch (ex) { deferred.reject(ex); } @@ -2204,14 +2208,14 @@ webdriver.promise.isGenerator = function(fn) { * console.log(e.toString()); // Error: boom * }); * - * + * * @param {!Function} generatorFn The generator function to execute. * @param {Object=} opt_self The object to use as "this" when invoking the * initial generator. * @param {...*} var_args Any arguments to pass to the initial generator. * @return {!webdriver.promise.Promise.} A promise that will resolve to the * generator's final result. - * @throws {TypeError} If the given function is not a generator. + * @throws {TypeError} If the given function is not a generator. */ webdriver.promise.consume = function(generatorFn, opt_self, var_args) { if (!webdriver.promise.isGenerator(generatorFn)) {