Wed, 31 Dec 2014 06:55:50 +0100
Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2
1 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
4 * You can obtain one at http://mozilla.org/MPL/2.0/. */
6 /**
7 * The ElementManager manages DOM references and interactions with elements.
8 * According to the WebDriver spec (http://code.google.com/p/selenium/wiki/JsonWireProtocol), the
9 * server sends the client an element reference, and maintains the map of reference to element.
10 * The client uses this reference when querying/interacting with the element, and the
11 * server uses maps this reference to the actual element when it executes the command.
12 */
14 this.EXPORTED_SYMBOLS = [
15 "ElementManager",
16 "CLASS_NAME",
17 "SELECTOR",
18 "ID",
19 "NAME",
20 "LINK_TEXT",
21 "PARTIAL_LINK_TEXT",
22 "TAG",
23 "XPATH"
24 ];
26 const DOCUMENT_POSITION_DISCONNECTED = 1;
28 let uuidGen = Components.classes["@mozilla.org/uuid-generator;1"]
29 .getService(Components.interfaces.nsIUUIDGenerator);
31 this.CLASS_NAME = "class name";
32 this.SELECTOR = "css selector";
33 this.ID = "id";
34 this.NAME = "name";
35 this.LINK_TEXT = "link text";
36 this.PARTIAL_LINK_TEXT = "partial link text";
37 this.TAG = "tag name";
38 this.XPATH = "xpath";
40 function ElementException(msg, num, stack) {
41 this.message = msg;
42 this.code = num;
43 this.stack = stack;
44 }
46 this.ElementManager = function ElementManager(notSupported) {
47 this.seenItems = {};
48 this.timer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer);
49 this.elementStrategies = [CLASS_NAME, SELECTOR, ID, NAME, LINK_TEXT, PARTIAL_LINK_TEXT, TAG, XPATH];
50 for (let i = 0; i < notSupported.length; i++) {
51 this.elementStrategies.splice(this.elementStrategies.indexOf(notSupported[i]), 1);
52 }
53 }
55 ElementManager.prototype = {
56 /**
57 * Reset values
58 */
59 reset: function EM_clear() {
60 this.seenItems = {};
61 },
63 /**
64 * Add element to list of seen elements
65 *
66 * @param nsIDOMElement element
67 * The element to add
68 *
69 * @return string
70 * Returns the server-assigned reference ID
71 */
72 addToKnownElements: function EM_addToKnownElements(element) {
73 for (let i in this.seenItems) {
74 let foundEl = null;
75 try {
76 foundEl = this.seenItems[i].get();
77 }
78 catch(e) {}
79 if (foundEl) {
80 if (XPCNativeWrapper(foundEl) == XPCNativeWrapper(element)) {
81 return i;
82 }
83 }
84 else {
85 //cleanup reference to GC'd element
86 delete this.seenItems[i];
87 }
88 }
89 var id = uuidGen.generateUUID().toString();
90 this.seenItems[id] = Components.utils.getWeakReference(element);
91 return id;
92 },
94 /**
95 * Retrieve element from its unique ID
96 *
97 * @param String id
98 * The DOM reference ID
99 * @param nsIDOMWindow win
100 * The window that contains the element
101 *
102 * @returns nsIDOMElement
103 * Returns the element or throws Exception if not found
104 */
105 getKnownElement: function EM_getKnownElement(id, win) {
106 let el = this.seenItems[id];
107 if (!el) {
108 throw new ElementException("Element has not been seen before. Id given was " + id, 17, null);
109 }
110 try {
111 el = el.get();
112 }
113 catch(e) {
114 el = null;
115 delete this.seenItems[id];
116 }
117 // use XPCNativeWrapper to compare elements; see bug 834266
118 let wrappedWin = XPCNativeWrapper(win);
119 if (!el ||
120 !(XPCNativeWrapper(el).ownerDocument == wrappedWin.document) ||
121 (XPCNativeWrapper(el).compareDocumentPosition(wrappedWin.document.documentElement) &
122 DOCUMENT_POSITION_DISCONNECTED)) {
123 throw new ElementException("The element reference is stale. Either the element " +
124 "is no longer attached to the DOM or the page has been refreshed.", 10, null);
125 }
126 return el;
127 },
129 /**
130 * Convert values to primitives that can be transported over the
131 * Marionette protocol.
132 *
133 * This function implements the marshaling algorithm defined in the
134 * WebDriver specification:
135 *
136 * https://dvcs.w3.org/hg/webdriver/raw-file/tip/webdriver-spec.html#synchronous-javascript-execution
137 *
138 * @param object val
139 * object to be marshaled
140 *
141 * @return object
142 * Returns a JSON primitive or Object
143 */
144 wrapValue: function EM_wrapValue(val) {
145 let result = null;
147 switch (typeof(val)) {
148 case "undefined":
149 result = null;
150 break;
152 case "string":
153 case "number":
154 case "boolean":
155 result = val;
156 break;
158 case "object":
159 let type = Object.prototype.toString.call(val);
160 if (type == "[object Array]" ||
161 type == "[object NodeList]") {
162 result = [];
163 for (let i = 0; i < val.length; ++i) {
164 result.push(this.wrapValue(val[i]));
165 }
166 }
167 else if (val == null) {
168 result = null;
169 }
170 else if (val.nodeType == 1) {
171 result = {'ELEMENT': this.addToKnownElements(val)};
172 }
173 else {
174 result = {};
175 for (let prop in val) {
176 result[prop] = this.wrapValue(val[prop]);
177 }
178 }
179 break;
180 }
182 return result;
183 },
185 /**
186 * Convert any ELEMENT references in 'args' to the actual elements
187 *
188 * @param object args
189 * Arguments passed in by client
190 * @param nsIDOMWindow win
191 * The window that contains the elements
192 *
193 * @returns object
194 * Returns the objects passed in by the client, with the
195 * reference IDs replaced by the actual elements.
196 */
197 convertWrappedArguments: function EM_convertWrappedArguments(args, win) {
198 let converted;
199 switch (typeof(args)) {
200 case 'number':
201 case 'string':
202 case 'boolean':
203 converted = args;
204 break;
205 case 'object':
206 if (args == null) {
207 converted = null;
208 }
209 else if (Object.prototype.toString.call(args) == '[object Array]') {
210 converted = [];
211 for (let i in args) {
212 converted.push(this.convertWrappedArguments(args[i], win));
213 }
214 }
215 else if (typeof(args['ELEMENT'] === 'string') &&
216 args.hasOwnProperty('ELEMENT')) {
217 converted = this.getKnownElement(args['ELEMENT'], win);
218 if (converted == null)
219 throw new ElementException("Unknown element: " + args['ELEMENT'], 500, null);
220 }
221 else {
222 converted = {};
223 for (let prop in args) {
224 converted[prop] = this.convertWrappedArguments(args[prop], win);
225 }
226 }
227 break;
228 }
229 return converted;
230 },
232 /*
233 * Execute* helpers
234 */
236 /**
237 * Return an object with any namedArgs applied to it. Used
238 * to let clients use given names when refering to arguments
239 * in execute calls, instead of using the arguments list.
240 *
241 * @param object args
242 * list of arguments being passed in
243 *
244 * @return object
245 * If '__marionetteArgs' is in args, then
246 * it will return an object with these arguments
247 * as its members.
248 */
249 applyNamedArgs: function EM_applyNamedArgs(args) {
250 namedArgs = {};
251 args.forEach(function(arg) {
252 if (typeof(arg['__marionetteArgs']) === 'object') {
253 for (let prop in arg['__marionetteArgs']) {
254 namedArgs[prop] = arg['__marionetteArgs'][prop];
255 }
256 }
257 });
258 return namedArgs;
259 },
261 /**
262 * Find an element or elements starting at the document root or
263 * given node, using the given search strategy. Search
264 * will continue until the search timelimit has been reached.
265 *
266 * @param nsIDOMWindow win
267 * The window to search in
268 * @param object values
269 * The 'using' member of values will tell us which search
270 * method to use. The 'value' member tells us the value we
271 * are looking for.
272 * If this object has an 'element' member, this will be used
273 * as the start node instead of the document root
274 * If this object has a 'time' member, this number will be
275 * used to see if we have hit the search timelimit.
276 * @param function on_success
277 * The notification callback used when we are returning successfully.
278 * @param function on_error
279 The callback to invoke when an error occurs.
280 * @param boolean all
281 * If true, all found elements will be returned.
282 * If false, only the first element will be returned.
283 *
284 * @return nsIDOMElement or list of nsIDOMElements
285 * Returns the element(s) by calling the on_success function.
286 */
287 find: function EM_find(win, values, searchTimeout, on_success, on_error, all, command_id) {
288 let startTime = values.time ? values.time : new Date().getTime();
289 let startNode = (values.element != undefined) ?
290 this.getKnownElement(values.element, win) : win.document;
291 if (this.elementStrategies.indexOf(values.using) < 0) {
292 throw new ElementException("No such strategy.", 17, null);
293 }
294 let found = all ? this.findElements(values.using, values.value, win.document, startNode) :
295 this.findElement(values.using, values.value, win.document, startNode);
296 if (found) {
297 let type = Object.prototype.toString.call(found);
298 if ((type == '[object Array]') || (type == '[object HTMLCollection]') || (type == '[object NodeList]')) {
299 let ids = []
300 for (let i = 0 ; i < found.length ; i++) {
301 ids.push(this.addToKnownElements(found[i]));
302 }
303 on_success(ids, command_id);
304 }
305 else {
306 let id = this.addToKnownElements(found);
307 on_success({'ELEMENT':id}, command_id);
308 }
309 return;
310 } else {
311 if (!searchTimeout || new Date().getTime() - startTime > searchTimeout) {
312 on_error("Unable to locate element: " + values.value, 7, null, command_id);
313 } else {
314 values.time = startTime;
315 this.timer.initWithCallback(this.find.bind(this, win, values,
316 searchTimeout,
317 on_success, on_error, all,
318 command_id),
319 100,
320 Components.interfaces.nsITimer.TYPE_ONE_SHOT);
321 }
322 }
323 },
325 /**
326 * Find a value by XPATH
327 *
328 * @param nsIDOMElement root
329 * Document root
330 * @param string value
331 * XPATH search string
332 * @param nsIDOMElement node
333 * start node
334 *
335 * @return nsIDOMElement
336 * returns the found element
337 */
338 findByXPath: function EM_findByXPath(root, value, node) {
339 return root.evaluate(value, node, null,
340 Components.interfaces.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
341 },
343 /**
344 * Find values by XPATH
345 *
346 * @param nsIDOMElement root
347 * Document root
348 * @param string value
349 * XPATH search string
350 * @param nsIDOMElement node
351 * start node
352 *
353 * @return object
354 * returns a list of found nsIDOMElements
355 */
356 findByXPathAll: function EM_findByXPathAll(root, value, node) {
357 let values = root.evaluate(value, node, null,
358 Components.interfaces.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
359 let elements = [];
360 let element = values.iterateNext();
361 while (element) {
362 elements.push(element);
363 element = values.iterateNext();
364 }
365 return elements;
366 },
368 /**
369 * Helper method to find. Finds one element using find's criteria
370 *
371 * @param string using
372 * String identifying which search method to use
373 * @param string value
374 * Value to look for
375 * @param nsIDOMElement rootNode
376 * Document root
377 * @param nsIDOMElement startNode
378 * Node from which we start searching
379 *
380 * @return nsIDOMElement
381 * Returns found element or throws Exception if not found
382 */
383 findElement: function EM_findElement(using, value, rootNode, startNode) {
384 let element;
385 switch (using) {
386 case ID:
387 element = startNode.getElementById ?
388 startNode.getElementById(value) :
389 this.findByXPath(rootNode, './/*[@id="' + value + '"]', startNode);
390 break;
391 case NAME:
392 element = startNode.getElementsByName ?
393 startNode.getElementsByName(value)[0] :
394 this.findByXPath(rootNode, './/*[@name="' + value + '"]', startNode);
395 break;
396 case CLASS_NAME:
397 element = startNode.getElementsByClassName(value)[0]; //works for >=FF3
398 break;
399 case TAG:
400 element = startNode.getElementsByTagName(value)[0]; //works for all elements
401 break;
402 case XPATH:
403 element = this.findByXPath(rootNode, value, startNode);
404 break;
405 case LINK_TEXT:
406 case PARTIAL_LINK_TEXT:
407 let allLinks = startNode.getElementsByTagName('A');
408 for (let i = 0; i < allLinks.length && !element; i++) {
409 let text = allLinks[i].text;
410 if (PARTIAL_LINK_TEXT == using) {
411 if (text.indexOf(value) != -1) {
412 element = allLinks[i];
413 }
414 } else if (text == value) {
415 element = allLinks[i];
416 }
417 }
418 break;
419 case SELECTOR:
420 element = startNode.querySelector(value);
421 break;
422 default:
423 throw new ElementException("No such strategy", 500, null);
424 }
425 return element;
426 },
428 /**
429 * Helper method to find. Finds all element using find's criteria
430 *
431 * @param string using
432 * String identifying which search method to use
433 * @param string value
434 * Value to look for
435 * @param nsIDOMElement rootNode
436 * Document root
437 * @param nsIDOMElement startNode
438 * Node from which we start searching
439 *
440 * @return nsIDOMElement
441 * Returns found elements or throws Exception if not found
442 */
443 findElements: function EM_findElements(using, value, rootNode, startNode) {
444 let elements = [];
445 switch (using) {
446 case ID:
447 value = './/*[@id="' + value + '"]';
448 case XPATH:
449 elements = this.findByXPathAll(rootNode, value, startNode);
450 break;
451 case NAME:
452 elements = startNode.getElementsByName ?
453 startNode.getElementsByName(value) :
454 this.findByXPathAll(rootNode, './/*[@name="' + value + '"]', startNode);
455 break;
456 case CLASS_NAME:
457 elements = startNode.getElementsByClassName(value);
458 break;
459 case TAG:
460 elements = startNode.getElementsByTagName(value);
461 break;
462 case LINK_TEXT:
463 case PARTIAL_LINK_TEXT:
464 let allLinks = rootNode.getElementsByTagName('A');
465 for (let i = 0; i < allLinks.length; i++) {
466 let text = allLinks[i].text;
467 if (PARTIAL_LINK_TEXT == using) {
468 if (text.indexOf(value) != -1) {
469 elements.push(allLinks[i]);
470 }
471 } else if (text == value) {
472 elements.push(allLinks[i]);
473 }
474 }
475 break;
476 case SELECTOR:
477 elements = Array.slice(startNode.querySelectorAll(value));
478 break;
479 default:
480 throw new ElementException("No such strategy", 500, null);
481 }
482 return elements;
483 },
484 }