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 */
9var StatusCode = {
10  STALE_ELEMENT_REFERENCE: 10,
11  UNKNOWN_ERROR: 13,
12};
13
14/**
15 * Enum for node types.
16 * @enum {number}
17 */
18var 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 */
28var ELEMENT_KEY = 'ELEMENT';
29
30/**
31 * True if shadow dom is enabled.
32 * @const
33 * @type {boolean}
34 */
35var SHADOW_DOM_ENABLED = typeof ShadowRoot === 'function';
36
37/**
38 * A cache which maps IDs <-> cached objects for the purpose of identifying
39 * a script object remotely.
40 * @constructor
41 */
42function Cache() {
43  this.cache_ = {};
44  this.nextId_ = 1;
45  this.idPrefix_ = Math.random().toString();
46}
47
48Cache.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 ShadowRoot)
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 */
116function 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 */
129function 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 */
144function 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 ShadowRoot)) {
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 */
171function 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 */
204function 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