/** @license Copyright 2007-2009 WebDriver committers Copyright 2007-2009 Google Inc. 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 The heart of the WebDriver JavaScript API. * @author jmleyba@gmail.com (Jason Leyba) */ goog.provide('webdriver.WebDriver'); goog.provide('webdriver.WebDriver.EventType'); goog.provide('webdriver.WebDriver.Speed'); goog.require('goog.debug.Logger'); goog.require('goog.events'); goog.require('goog.events.EventTarget'); goog.require('webdriver.By.Locator'); goog.require('webdriver.Command'); goog.require('webdriver.CommandName'); goog.require('webdriver.Response'); goog.require('webdriver.WebElement'); goog.require('webdriver.timing'); /** * The main interface for controlling a web browser. How the browser is * controlled is dictated by the injected {@code commandProcessor}. The command * processor may control the browser either through an extension or plugin, or * by sending commands to a RemoteWebDriver server. * * Any WebDriver command that is expected to produce a return value will return * a {@code webdriver.Future}. This Future can passed as an argument to another * command, or an assertion function in the {@code webdriver.asserts} namespace. * For example: * driver.get('http://www.google.com'); * var futureTitle = driver.getTitle(); * assertThat(futureTitle, equals('Google Search')); * * The WebDriver will dispatch the following events: *
* driver.get('http://www.google.com');
* var element = driver.findElement({name: 'q'});
* driver.wait(element.isDisplayed, 3000, element);
*
* @param {function} conditionFn The function to evaluate.
* @param {number} timeout The maximum amount of time to wait, in milliseconds.
* @param {Object} opt_self (Optional) The object in whose context to execute
* the {@code conditionFn}.
* @param {boolean} opt_waitNot (Optional) Whether to wait for the inverse of
* the {@code conditionFn}.
*/
webdriver.WebDriver.prototype.wait = function(conditionFn, timeout, opt_self,
opt_waitNot) {
conditionFn = goog.bind(conditionFn, opt_self);
var waitOnInverse = !!opt_waitNot;
var callFunction = goog.bind(this.callFunction, this);
function pollFunction(opt_startTime, opt_future) {
var startTime = opt_startTime || goog.now();
function checkValue(value) {
var pendingFuture = null;
if (value instanceof webdriver.Future) {
if (value.isSet()) {
value = value.getValue();
} else {
pendingFuture = value;
value = null;
}
}
var done = !pendingFuture && (waitOnInverse != !!value);
if (!done) {
var ellapsed = goog.now() - startTime;
if (ellapsed > timeout) {
throw Error('Wait timed out after ' + ellapsed + 'ms');
}
// If we pass the pending future in as is, the AbstractCommandProcessor
// will try to resolve it to its value. However, if we're scheduling
// this function, it's because the future has not been set yet, which
// will lead to an error. To avoid this, wrap up the pollFunction in an
// anonymous function so the AbstractCommandProcessor does not
// interfere.
callFunction(goog.bind(pollFunction, null, startTime, pendingFuture));
}
}
var result = opt_future || conditionFn();
checkValue(result);
}
this.addCommand(webdriver.CommandName.WAIT).
setParameter('function', pollFunction).
setParameter('args', [0, null]);
};
/**
* Waits for the inverse of a condition to be true before executing the next
* command. If the condition does not hold after the given {@code timeout}, an
* error will be raised. Example:
*
* driver.get('http://www.google.com');
* var element = driver.findElement({name: 'q'});
* driver.waitNot(element.isDisplayed, 3000, element);
*
* @param {function} conditionFn The function to evaluate.
* @param {number} timeout The maximum amount of time to wait, in milliseconds.
* @param {Object} opt_self (Optional) The object in whose context to execute
* the {@code conditionFn}.
*/
webdriver.WebDriver.prototype.waitNot = function(conditionFn, timeout,
opt_self) {
this.wait(conditionFn, timeout, opt_self, true);
};
/**
* Request a new session ID.
*/
webdriver.WebDriver.prototype.newSession = function() {
this.callFunction(function() {
this.addCommand(webdriver.CommandName.NEW_SESSION);
this.callFunction(function(value) {
this.sessionId_ = value;
}, this);
}, this);
};
/**
* Switch the focus of future commands for this driver to the window with the
* given name.
* @param {string|webdriver.Future} name The name of the window to transfer
* control to. Alternatively, the UUID of a window handle, returned by
* {@code #getWindowHandle()} or {@code #getAllWindowHandles()}.
*/
webdriver.WebDriver.prototype.switchToWindow = function(name) {
this.callFunction(function() {
this.addCommand(webdriver.CommandName.SWITCH_TO_WINDOW).
setParameter('name', name);
}, this);
};
/**
* Switch the focus of future commands for this driver to the frame with the
* given name or ID. To select sub-frames, simply separate the frame names/IDs
* by dots. As an example, {@code 'main.child'} will select the frame with the
* name 'main' and hten its child 'child'. If a frame name is a number, then it
* will be treated as an index into the {@code window.frames} array of the
* current window.
* @param {string|number|webdriver.WebElement} frame Identifier for the frame
* to transfer control to.
*/
webdriver.WebDriver.prototype.switchToFrame = function(frame) {
this.callFunction(function() {
this.addCommand(webdriver.CommandName.SWITCH_TO_FRAME).
setParameter('id', frame);
}, this);
};
/**
* Selects either the first frame on the page, or the main document when a page
* contains iframes.
*/
webdriver.WebDriver.prototype.switchToDefaultContent = function() {
return this.switchToFrame(null);
};
/**
* Retrieves the internal UUID handle for the current window.
* @return {webdriver.Future} The current handle wrapped in a Future.
*/
webdriver.WebDriver.prototype.getWindowHandle = function() {
return this.addCommand(webdriver.CommandName.GET_CURRENT_WINDOW_HANDLE).
getFutureResult();
};
/**
* Retrieves the handles for all known windows.
*/
webdriver.WebDriver.prototype.getAllWindowHandles = function() {
this.addCommand(webdriver.CommandName.GET_WINDOW_HANDLES);
};
/**
* Retrieves the HTML source of the current page.
* @return {webdriver.Future} The page source wrapped in a Future.
*/
webdriver.WebDriver.prototype.getPageSource = function() {
return this.addCommand(webdriver.CommandName.GET_PAGE_SOURCE).
getFutureResult();
};
/**
* Closes the current window.
* WARNING: This command provides no protection against closing the
* script window (e.g. the window sending commands to the driver)
*/
webdriver.WebDriver.prototype.close = function() {
this.addCommand(webdriver.CommandName.CLOSE);
};
/**
* Helper function for converting an argument to a script into a parameter
* object to send with the {@code webdriver.Command}.
* @param {*} arg The value to convert.
* @return {*} The converted value.
* @see {webdriver.WebDriver.prototype.executeScript}
* @private
*/
webdriver.WebDriver.wrapScriptArgument_ = function(arg) {
if (arg instanceof webdriver.WebElement) {
return {'ELEMENT': arg.getId()};
} else if (arg == null || !goog.isDef(arg)) {
return null;
} else if (goog.isBoolean(arg) ||
goog.isNumber(arg) ||
goog.isString(arg)) {
return arg;
} else if (goog.isArray(arg)) {
return goog.array.map(arg, webdriver.WebDriver.wrapScriptArgument_);
} else if (goog.isFunction(arg)) {
throw new Error('Invalid script argument type: ' + goog.typeOf(arg));
} else {
return goog.object.map(arg, webdriver.WebDriver.wrapScriptArgument_);
}
};
/**
* Helper function for unwrapping an executeScript result.
* @param {{type:string,value:*}|Array.<{type:string,value:*}>} result The
* result to unwrap.
* @return {*} The unwrapped result.
* @private
*/
webdriver.WebDriver.prototype.unwrapScriptResult_ = function(result) {
if (goog.isArray(result)) {
return goog.array.map(result, goog.bind(this.unwrapScriptResult_, this));
}
if (result != null && goog.isObject(result) && 'ELEMENT' in result) {
var element = new webdriver.WebElement(this);
element.getId().setValue(result['ELEMENT']);
return element;
}
return result;
};
/**
* Adds a command to execute a JavaScript snippet in the window of the page
* currently under test.
* @param {string} script The JavaScript snippet to execute.
* @param {*} var_args The arguments to pass to the script.
* @return {webdriver.Future} The result of the executed script, wrapped in a
* {@code webdriver.Future} instance.
*/
webdriver.WebDriver.prototype.executeScript = function(script, var_args) {
var args = goog.array.map(
goog.array.slice(arguments, 1),
webdriver.WebDriver.wrapScriptArgument_);
return this.callFunction(function() {
this.addCommand(webdriver.CommandName.EXECUTE_SCRIPT).
setParameter("script", script).
setParameter("args", args);
return this.callFunction(function(prevResult) {
return this.unwrapScriptResult_(prevResult);
}, this);
}, this);
};
/**
* Adds a command to fetch the given URL.
* @param {goog.Uri|string} url The URL to fetch.
*/
webdriver.WebDriver.prototype.get = function(url) {
this.callFunction(function() {
this.addCommand(webdriver.CommandName.GET).
setParameter('url', url.toString());
}, this);
};
/**
* Navigate backwards in the current browser window's history.
*/
webdriver.WebDriver.prototype.back = function() {
this.addCommand(webdriver.CommandName.GO_BACK);
};
/**
* Navigate forwards in the current browser window's history.
*/
webdriver.WebDriver.prototype.forward = function() {
this.addCommand(webdriver.CommandName.GO_FORWARD);
};
/**
* Refresh the current page.
*/
webdriver.WebDriver.prototype.refresh = function() {
this.addCommand(webdriver.CommandName.REFRESH);
};
/**
* Retrieves the current window URL.
* @return {webdriver.Future} The current URL in a webdriver.Future.
*/
webdriver.WebDriver.prototype.getCurrentUrl = function() {
return this.addCommand(webdriver.CommandName.GET_CURRENT_URL).
getFutureResult();
};
/**
* Retrieves the current page's title.
* @return {webdriver.Future} The current page title.
*/
webdriver.WebDriver.prototype.getTitle = function() {
return this.addCommand(webdriver.CommandName.GET_TITLE).
getFutureResult();
};
/**
* Find an element on the current page. If the element cannot be found, an
* {@code webdriver.WebDriver.EventType.ERROR} event will be dispatched.
* @param {webdriver.By.Locator|object} by An object describing the locator
* strategy to use.
* @return {webdriver.WebElement} A WebElement wrapper that can be used to
* issue commands against the located element.
*/
webdriver.WebDriver.prototype.findElement = function(by) {
var webElement = new webdriver.WebElement(this);
var locator = webdriver.By.Locator.checkLocator(by);
this.callFunction(function() {
var command = this.addCommand(webdriver.CommandName.FIND_ELEMENT).
setParameter("using", locator.type).
setParameter("value", locator.target);
this.callFunction(function(id) {
webElement.getId().setValue(id['ELEMENT']);
});
}, this);
return webElement;
};
/**
* Determine if an element is present on the page.
* @param {webdriver.By.Locator|{*: string}} by The locator to use for finding
* the element, or a short-hand object that can be converted into a locator.
* @return {webdriver.Future} Whether the element was present on the page. The
* return value is wrapped in a Future that will be defined when the driver
* completes the command.
* @see webdriver.By.Locator.createFromObj
*/
webdriver.WebDriver.prototype.isElementPresent = function(by) {
var locator = webdriver.By.Locator.checkLocator(by);
return this.callFunction(function() {
var findCommand = this.addCommand(webdriver.CommandName.FIND_ELEMENT).
setParameter("using", locator.type).
setParameter("value", locator.target);
var commandFailed = false;
var key = goog.events.listenOnce(findCommand,
webdriver.Command.ERROR_EVENT, function(e) {
commandFailed = true;
this.abortCommand(e.currentTarget);
e.preventDefault();
e.stopPropagation();
return false;
}, /*capture phase*/true, this);
return this.callFunction(function() {
goog.events.unlistenByKey(key);
return !commandFailed;
});
}, this);
};
/**
* Search for multiple elements on the current page. The result of this
* operation can be accessed from the last saved {@code webdriver.Response}
* object:
* driver.findElements({xpath: '//div'});
* driver.callFunction(function(value) {
* value[0].click();
* value[1].click();
* // etc.
* });
* @param {webdriver.By.Locator|{*: string}} by The locator to use for finding
* the element, or a short-hand object that can be converted into a locator.
* @see webdriver.By.Locator.createFromObj
*/
webdriver.WebDriver.prototype.findElements = function(by) {
var locator = webdriver.By.Locator.checkLocator(by);
return this.callFunction(function() {
this.addCommand(webdriver.CommandName.FIND_ELEMENTS).
setParameter("using", locator.type).
setParameter("value", locator.target);
return this.callFunction(function(ids) {
var elements = [];
for (var i = 0; i < ids.length; i++) {
if (ids[i]) {
var element = new webdriver.WebElement(this);
element.getId().setValue(ids[i]['ELEMENT']);
elements.push(element);
}
}
return elements;
}, this);
}, this);
};
/**
* Adjust the speed of user input.
* @param {webdriver.WebDriver.Speed} speed The new speed setting.
*/
webdriver.WebDriver.prototype.setSpeed = function(speed) {
this.addCommand(webdriver.CommandName.SET_SPEED).
setParameter("speed", speed);
};
/**
* Fetch the current user input speed.
* @return {webdriver.Future} A Future whose value will be set by this driver
* when the query command completes.
*/
webdriver.WebDriver.prototype.getSpeed = function() {
return this.addCommand(webdriver.CommandName.GET_SPEED).
getFutureResult();
};