// 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 } }