// Licensed to the Software Freedom Conservancy (SFC) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The SFC licenses this file // to you 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 Browser atom for injecting JavaScript into the page under * test. There is no point in using this atom directly from JavaScript. * Instead, it is intended to be used in its compiled form when injecting * script from another language (e.g. C++). * * TODO: Add an example */ goog.provide('bot.inject'); goog.provide('bot.inject.cache'); goog.require('bot'); goog.require('bot.Error'); goog.require('bot.ErrorCode'); goog.require('bot.json'); /** * @suppress {extraRequire} Used as a forward declaration which causes * compilation errors if missing. */ goog.require('bot.response.ResponseObject'); goog.require('goog.array'); goog.require('goog.dom.NodeType'); goog.require('goog.object'); goog.require('goog.userAgent'); goog.require('goog.utils'); /** * Type definition for the WebDriver's JSON wire protocol representation * of a DOM element. * @typedef {{ELEMENT: string}} * @see bot.inject.ELEMENT_KEY * @see https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol */ bot.inject.JsonElement; /** * Type definition for a cached Window object that can be referenced in * WebDriver's JSON wire protocol. Note, this is a non-standard * representation. * @typedef {{WINDOW: string}} * @see bot.inject.WINDOW_KEY */ bot.inject.JsonWindow; /** * Key used to identify DOM elements in the WebDriver wire protocol. * @type {string} * @const * @see https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol */ bot.inject.ELEMENT_KEY = 'ELEMENT'; /** * Key used to identify Window objects in the WebDriver wire protocol. * @type {string} * @const */ bot.inject.WINDOW_KEY = 'WINDOW'; /** * Converts an element to a JSON friendly value so that it can be * stringified for transmission to the injector. Values are modified as * follows: *
* Object result = ((JavascriptExecutor) driver).executeScript(
* "return arguments[0] + arguments[1];", 1, 2);
*
*
* Once transmitted to the driver, this command would be injected into the
* page for evaluation as:
*
* bot.inject.executeScript(
* function() {return arguments[0] + arguments[1];},
* [1, 2]);
*
*
* The details of how this actually gets injected for evaluation is left
* as an implementation detail for clients of this library.
*
* @param {!(Function|string)} fn Either the function to execute, or a string
* defining the body of an anonymous function that should be executed. This
* function should only contain references to symbols defined in the context
* of the target window (`opt_window`). Any references to symbols
* defined in this context will likely generate a ReferenceError.
* @param {Array.<*>} args An array of wrapped script arguments, as defined by
* the WebDriver wire protocol.
* @param {boolean=} opt_stringify Whether the result should be returned as a
* serialized JSON string.
* @param {!Window=} opt_window The window in whose context the function should
* be invoked; defaults to the current window.
* @return {!(string|bot.response.ResponseObject)} The response object. If
* opt_stringify is true, the result will be serialized and returned in
* string format.
*/
bot.inject.executeScript = function (fn, args, opt_stringify, opt_window) {
var win = opt_window || bot.getWindow();
var ret;
try {
fn = bot.inject.recompileFunction_(fn, win);
var unwrappedArgs = /**@type {Object}*/ (bot.inject.unwrapValue(args,
win.document));
ret = bot.inject.wrapResponse(fn.apply(null, unwrappedArgs));
} catch (ex) {
ret = bot.inject.wrapError(ex);
}
return opt_stringify ? bot.json.stringify(ret) : ret;
};
/**
* Executes an injected script, which is expected to finish asynchronously
* before the given `timeout`. When the script finishes or an error
* occurs, the given `onDone` callback will be invoked. This callback
* will have a single argument, a {@link bot.response.ResponseObject} object.
*
* The script signals its completion by invoking a supplied callback given
* as its last argument. The callback may be invoked with a single value.
*
* The script timeout event will be scheduled with the provided window,
* ensuring the timeout is synchronized with that window's event queue.
* Furthermore, asynchronous scripts do not work across new page loads; if an
* "unload" event is fired on the window while an asynchronous script is
* pending, the script will be aborted and an error will be returned.
*
* Like `bot.inject.executeScript`, this function should only be called
* from an external source. It handles wrapping and unwrapping of input/output
* values.
*
* @param {(!Function|string)} fn Either the function to execute, or a string
* defining the body of an anonymous function that should be executed. This
* function should only contain references to symbols defined in the context
* of the target window (`opt_window`). Any references to symbols
* defined in this context will likely generate a ReferenceError.
* @param {Array.<*>} args An array of wrapped script arguments, as defined by
* the WebDriver wire protocol.
* @param {number} timeout The amount of time, in milliseconds, the script
* should be permitted to run; must be non-negative.
* @param {function(string)|function(!bot.response.ResponseObject)} onDone
* The function to call when the given `fn` invokes its callback,
* or when an exception or timeout occurs. This will always be called.
* @param {boolean=} opt_stringify Whether the result should be returned as a
* serialized JSON string.
* @param {!Window=} opt_window The window to synchronize the script with;
* defaults to the current window.
*/
bot.inject.executeAsyncScript = function (fn, args, timeout, onDone,
opt_stringify, opt_window) {
var win = opt_window || window;
var timeoutId;
var responseSent = false;
function sendResponse(status, value) {
if (!responseSent) {
if (win.removeEventListener) {
win.removeEventListener('unload', onunload, true);
} else {
win.detachEvent('onunload', onunload);
}
win.clearTimeout(timeoutId);
if (status != bot.ErrorCode.SUCCESS) {
var err = new bot.Error(status, value.message || value + '');
err.stack = value.stack;
value = bot.inject.wrapError(err);
} else {
value = bot.inject.wrapResponse(value);
}
onDone(opt_stringify ? bot.json.stringify(value) : value);
responseSent = true;
}
}
var sendError = goog.utils.partial(sendResponse, bot.ErrorCode.UNKNOWN_ERROR);
if (win.closed) {
sendError('Unable to execute script; the target window is closed.');
return;
}
fn = bot.inject.recompileFunction_(fn, win);
args = /** @type {Array.<*>} */ (bot.inject.unwrapValue(args, win.document));
args.push(goog.utils.partial(sendResponse, bot.ErrorCode.SUCCESS));
if (win.addEventListener) {
win.addEventListener('unload', onunload, true);
} else {
win.attachEvent('onunload', onunload);
}
var startTime = goog.utils.now();
try {
fn.apply(win, args);
// Register our timeout *after* the function has been invoked. This will
// ensure we don't timeout on a function that invokes its callback after
// a 0-based timeout.
timeoutId = win.setTimeout(function () {
sendResponse(bot.ErrorCode.SCRIPT_TIMEOUT,
Error('Timed out waiting for asynchronous script result ' +
'after ' + (goog.utils.now() - startTime) + ' ms'));
}, Math.max(0, timeout));
} catch (ex) {
sendResponse(ex.code || bot.ErrorCode.UNKNOWN_ERROR, ex);
}
function onunload() {
sendResponse(bot.ErrorCode.UNKNOWN_ERROR,
Error('Detected a page unload event; asynchronous script ' +
'execution does not work across page loads.'));
}
};
/**
* Wraps the response to an injected script that executed successfully so it
* can be JSON-ified for transmission to the process that injected this
* script.
* @param {*} value The script result.
* @return {{status:bot.ErrorCode,value:*}} The wrapped value.
* @see https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol#responses
*/
bot.inject.wrapResponse = function (value) {
return {
'status': bot.ErrorCode.SUCCESS,
'value': bot.inject.wrapValue(value)
};
};
/**
* Wraps a JavaScript error in an object-literal so that it can be JSON-ified
* for transmission to the process that injected this script.
* @param {Error} err The error to wrap.
* @return {{status:bot.ErrorCode,value:*}} The wrapped error object.
* @see https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol#failed-commands
*/
bot.inject.wrapError = function (err) {
// TODO: Parse stackTrace
return {
'status': goog.object.containsKey(err, 'code') ?
err['code'] : bot.ErrorCode.UNKNOWN_ERROR,
// TODO: Parse stackTrace
'value': {
'message': err.message
}
};
};
/**
* The property key used to store the element cache on the DOCUMENT node
* when it is injected into the page. Since compiling each browser atom results
* in a different symbol table, we must use this known key to access the cache.
* This ensures the same object is used between injections of different atoms.
* @private {string}
* @const
*/
bot.inject.cache.CACHE_KEY_ = '$wdc_';
/**
* The prefix for each key stored in an cache.
* @type {string}
* @const
*/
bot.inject.cache.ELEMENT_KEY_PREFIX = ':wdc:';
/**
* Retrieves the cache object for the given window. Will initialize the cache
* if it does not yet exist.
* @param {Document=} opt_doc The document whose cache to retrieve. Defaults to
* the current document.
* @return {Object.