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