// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* Enum for WebDriver status codes.
* @enum {number}
*/
var StatusCode = {
STALE_ELEMENT_REFERENCE: 10,
UNKNOWN_ERROR: 13,
};
/**
* Enum for node types.
* @enum {number}
*/
var NodeType = {
ELEMENT: 1,
DOCUMENT: 9,
};
/**
* Dictionary key to use for holding an element ID.
* @const
* @type {string}
*/
var ELEMENT_KEY = 'ELEMENT';
/**
* True if shadow dom is enabled.
* @const
* @type {boolean}
*/
var SHADOW_DOM_ENABLED = typeof ShadowRoot === 'function';
/**
* A cache which maps IDs <-> cached objects for the purpose of identifying
* a script object remotely.
* @constructor
*/
function Cache() {
this.cache_ = {};
this.nextId_ = 1;
this.idPrefix_ = Math.random().toString();
}
Cache.prototype = {
/**
* Stores a given item in the cache and returns a unique ID.
*
* @param {!Object} item The item to store in the cache.
* @return {number} The ID for the cached item.
*/
storeItem: function(item) {
for (var i in this.cache_) {
if (item == this.cache_[i])
return i;
}
var id = this.idPrefix_ + '-' + this.nextId_;
this.cache_[id] = item;
this.nextId_++;
return id;
},
/**
* Retrieves the cached object for the given ID.
*
* @param {number} id The ID for the cached item to retrieve.
* @return {!Object} The retrieved item.
*/
retrieveItem: function(id) {
var item = this.cache_[id];
if (item)
return item;
var error = new Error('not in cache');
error.code = StatusCode.STALE_ELEMENT_REFERENCE;
error.message = 'element is not attached to the page document';
throw error;
},
/**
* Clears stale items from the cache.
*/
clearStale: function() {
for (var id in this.cache_) {
var node = this.cache_[id];
if (!this.isNodeReachable_(node))
delete this.cache_[id];
}
},
/**
* @private
* @param {!Node} node The node to check.
* @return {boolean} If the nodes is reachable.
*/
isNodeReachable_: function(node) {
var nodeRoot = getNodeRoot(node);
if (nodeRoot == document)
return true;
else if (SHADOW_DOM_ENABLED && nodeRoot instanceof ShadowRoot)
return true;
return false;
}
};
/**
* Returns the root element of the node. Found by traversing parentNodes until
* a node with no parent is found. This node is considered the root.
* @param {!Node} node The node to find the root element for.
* @return {!Node} The root node.
*/
function getNodeRoot(node) {
while (node.parentNode) {
node = node.parentNode;
}
return node;
}
/**
* Returns the global object cache for the page.
* @param {Document=} opt_doc The document whose cache to retrieve. Defaults to
* the current document.
* @return {!Cache} The page's object cache.
*/
function getPageCache(opt_doc) {
var doc = opt_doc || document;
var key = '$cdc_asdjflasutopfhvcZLmcfl_';
if (!(key in doc))
doc[key] = new Cache();
return doc[key];
}
/**
* Wraps the given value to be transmitted remotely by converting
* appropriate objects to cached object IDs.
*
* @param {*} value The value to wrap.
* @return {*} The wrapped value.
*/
function wrap(value) {
if (typeof(value) == 'object' && value != null) {
var nodeType = value['nodeType'];
if (nodeType == NodeType.ELEMENT || nodeType == NodeType.DOCUMENT
|| (SHADOW_DOM_ENABLED && value instanceof ShadowRoot)) {
var wrapped = {};
var root = getNodeRoot(value);
wrapped[ELEMENT_KEY] = getPageCache(root).storeItem(value);
return wrapped;
}
var obj = (typeof(value.length) == 'number') ? [] : {};
for (var prop in value)
obj[prop] = wrap(value[prop]);
return obj;
}
return value;
}
/**
* Unwraps the given value by converting from object IDs to the cached
* objects.
*
* @param {*} value The value to unwrap.
* @param {Cache} cache The cache to retrieve wrapped elements from.
* @return {*} The unwrapped value.
*/
function unwrap(value, cache) {
if (typeof(value) == 'object' && value != null) {
if (ELEMENT_KEY in value)
return cache.retrieveItem(value[ELEMENT_KEY]);
var obj = (typeof(value.length) == 'number') ? [] : {};
for (var prop in value)
obj[prop] = unwrap(value[prop], cache);
return obj;
}
return value;
}
/**
* Calls a given function and returns its value.
*
* The inputs to and outputs of the function will be unwrapped and wrapped
* respectively, unless otherwise specified. This wrapping involves converting
* between cached object reference IDs and actual JS objects. The cache will
* automatically be pruned each call to remove stale references.
*
* @param {Array.<string>} shadowHostIds The host ids of the nested shadow
* DOMs the function should be executed in the context of.
* @param {function(...[*]) : *} func The function to invoke.
* @param {!Array.<*>} args The array of arguments to supply to the function,
* which will be unwrapped before invoking the function.
* @param {boolean=} opt_unwrappedReturn Whether the function's return value
* should be left unwrapped.
* @return {*} An object containing a status and value property, where status
* is a WebDriver status code and value is the wrapped value. If an
* unwrapped return was specified, this will be the function's pure return
* value.
*/
function callFunction(shadowHostIds, func, args, opt_unwrappedReturn) {
var cache = getPageCache();
cache.clearStale();
if (shadowHostIds && SHADOW_DOM_ENABLED) {
for (var i = 0; i < shadowHostIds.length; i++) {
var host = cache.retrieveItem(shadowHostIds[i]);
// TODO(zachconrad): Use the olderShadowRoot API when available to check
// all of the shadow roots.
cache = getPageCache(host.webkitShadowRoot);
cache.clearStale();
}
}
if (opt_unwrappedReturn)
return func.apply(null, unwrap(args, cache));
var status = 0;
try {
var returnValue = wrap(func.apply(null, unwrap(args, cache)));
} catch (error) {
status = error.code || StatusCode.UNKNOWN_ERROR;
var returnValue = error.message;
}
return {
status: status,
value: returnValue
}
}