Home | History | Annotate | Download | only in js
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 /**
      6  * Enum for WebDriver status codes.
      7  * @enum {number}
      8  */
      9 var StatusCode = {
     10   STALE_ELEMENT_REFERENCE: 10,
     11   UNKNOWN_ERROR: 13,
     12 };
     13 
     14 /**
     15  * Enum for node types.
     16  * @enum {number}
     17  */
     18 var NodeType = {
     19   ELEMENT: 1,
     20   DOCUMENT: 9,
     21 };
     22 
     23 /**
     24  * Dictionary key to use for holding an element ID.
     25  * @const
     26  * @type {string}
     27  */
     28 var ELEMENT_KEY = 'ELEMENT';
     29 
     30 /**
     31  * True if shadow dom is enabled.
     32  * @const
     33  * @type {boolean}
     34  */
     35 var SHADOW_DOM_ENABLED = typeof WebKitShadowRoot === 'function';
     36 
     37 /**
     38  * A cache which maps IDs <-> cached objects for the purpose of identifying
     39  * a script object remotely.
     40  * @constructor
     41  */
     42 function Cache() {
     43   this.cache_ = {};
     44   this.nextId_ = 1;
     45   this.idPrefix_ = Math.random().toString();
     46 }
     47 
     48 Cache.prototype = {
     49 
     50   /**
     51    * Stores a given item in the cache and returns a unique ID.
     52    *
     53    * @param {!Object} item The item to store in the cache.
     54    * @return {number} The ID for the cached item.
     55    */
     56   storeItem: function(item) {
     57     for (var i in this.cache_) {
     58       if (item == this.cache_[i])
     59         return i;
     60     }
     61     var id = this.idPrefix_  + '-' + this.nextId_;
     62     this.cache_[id] = item;
     63     this.nextId_++;
     64     return id;
     65   },
     66 
     67   /**
     68    * Retrieves the cached object for the given ID.
     69    *
     70    * @param {number} id The ID for the cached item to retrieve.
     71    * @return {!Object} The retrieved item.
     72    */
     73   retrieveItem: function(id) {
     74     var item = this.cache_[id];
     75     if (item)
     76       return item;
     77     var error = new Error('not in cache');
     78     error.code = StatusCode.STALE_ELEMENT_REFERENCE;
     79     error.message = 'element is not attached to the page document';
     80     throw error;
     81   },
     82 
     83   /**
     84    * Clears stale items from the cache.
     85    */
     86   clearStale: function() {
     87     for (var id in this.cache_) {
     88       var node = this.cache_[id];
     89       if (!this.isNodeReachable_(node))
     90         delete this.cache_[id];
     91     }
     92   },
     93 
     94   /**
     95     * @private
     96     * @param {!Node} node The node to check.
     97     * @return {boolean} If the nodes is reachable.
     98     */
     99   isNodeReachable_: function(node) {
    100     var nodeRoot = getNodeRoot(node);
    101     if (nodeRoot == document)
    102       return true;
    103     else if (SHADOW_DOM_ENABLED && nodeRoot instanceof WebKitShadowRoot)
    104       return true;
    105 
    106     return false;
    107   }
    108 };
    109 
    110 /**
    111  * Returns the root element of the node.  Found by traversing parentNodes until
    112  * a node with no parent is found.  This node is considered the root.
    113  * @param {!Node} node The node to find the root element for.
    114  * @return {!Node} The root node.
    115  */
    116 function getNodeRoot(node) {
    117   while (node.parentNode) {
    118     node = node.parentNode;
    119   }
    120   return node;
    121 }
    122 
    123 /**
    124  * Returns the global object cache for the page.
    125  * @param {Document=} opt_doc The document whose cache to retrieve. Defaults to
    126  *     the current document.
    127  * @return {!Cache} The page's object cache.
    128  */
    129 function getPageCache(opt_doc) {
    130   var doc = opt_doc || document;
    131   var key = '$cdc_asdjflasutopfhvcZLmcfl_';
    132   if (!(key in doc))
    133     doc[key] = new Cache();
    134   return doc[key];
    135 }
    136 
    137 /**
    138  * Wraps the given value to be transmitted remotely by converting
    139  * appropriate objects to cached object IDs.
    140  *
    141  * @param {*} value The value to wrap.
    142  * @return {*} The wrapped value.
    143  */
    144 function wrap(value) {
    145   if (typeof(value) == 'object' && value != null) {
    146     var nodeType = value['nodeType'];
    147     if (nodeType == NodeType.ELEMENT || nodeType == NodeType.DOCUMENT
    148         || (SHADOW_DOM_ENABLED && value instanceof WebKitShadowRoot)) {
    149       var wrapped = {};
    150       var root = getNodeRoot(value);
    151       wrapped[ELEMENT_KEY] = getPageCache(root).storeItem(value);
    152       return wrapped;
    153     }
    154 
    155     var obj = (typeof(value.length) == 'number') ? [] : {};
    156     for (var prop in value)
    157       obj[prop] = wrap(value[prop]);
    158     return obj;
    159   }
    160   return value;
    161 }
    162 
    163 /**
    164  * Unwraps the given value by converting from object IDs to the cached
    165  * objects.
    166  *
    167  * @param {*} value The value to unwrap.
    168  * @param {Cache} cache The cache to retrieve wrapped elements from.
    169  * @return {*} The unwrapped value.
    170  */
    171 function unwrap(value, cache) {
    172   if (typeof(value) == 'object' && value != null) {
    173     if (ELEMENT_KEY in value)
    174       return cache.retrieveItem(value[ELEMENT_KEY]);
    175 
    176     var obj = (typeof(value.length) == 'number') ? [] : {};
    177     for (var prop in value)
    178       obj[prop] = unwrap(value[prop], cache);
    179     return obj;
    180   }
    181   return value;
    182 }
    183 
    184 /**
    185  * Calls a given function and returns its value.
    186  *
    187  * The inputs to and outputs of the function will be unwrapped and wrapped
    188  * respectively, unless otherwise specified. This wrapping involves converting
    189  * between cached object reference IDs and actual JS objects. The cache will
    190  * automatically be pruned each call to remove stale references.
    191  *
    192  * @param  {Array.<string>} shadowHostIds The host ids of the nested shadow
    193  *     DOMs the function should be executed in the context of.
    194  * @param {function(...[*]) : *} func The function to invoke.
    195  * @param {!Array.<*>} args The array of arguments to supply to the function,
    196  *     which will be unwrapped before invoking the function.
    197  * @param {boolean=} opt_unwrappedReturn Whether the function's return value
    198  *     should be left unwrapped.
    199  * @return {*} An object containing a status and value property, where status
    200  *     is a WebDriver status code and value is the wrapped value. If an
    201  *     unwrapped return was specified, this will be the function's pure return
    202  *     value.
    203  */
    204 function callFunction(shadowHostIds, func, args, opt_unwrappedReturn) {
    205   var cache = getPageCache();
    206   cache.clearStale();
    207   if (shadowHostIds && SHADOW_DOM_ENABLED) {
    208     for (var i = 0; i < shadowHostIds.length; i++) {
    209       var host = cache.retrieveItem(shadowHostIds[i]);
    210       // TODO(zachconrad): Use the olderShadowRoot API when available to check
    211       // all of the shadow roots.
    212       cache = getPageCache(host.webkitShadowRoot);
    213       cache.clearStale();
    214     }
    215   }
    216 
    217   if (opt_unwrappedReturn)
    218     return func.apply(null, unwrap(args, cache));
    219 
    220   var status = 0;
    221   try {
    222     var returnValue = wrap(func.apply(null, unwrap(args, cache)));
    223   } catch (error) {
    224     status = error.code || StatusCode.UNKNOWN_ERROR;
    225     var returnValue = error.message;
    226   }
    227   return {
    228       status: status,
    229       value: returnValue
    230   }
    231 }
    232