1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/testing/marionette/marionette-elements.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,484 @@ 1.4 +/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.7 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +/** 1.10 + * The ElementManager manages DOM references and interactions with elements. 1.11 + * According to the WebDriver spec (http://code.google.com/p/selenium/wiki/JsonWireProtocol), the 1.12 + * server sends the client an element reference, and maintains the map of reference to element. 1.13 + * The client uses this reference when querying/interacting with the element, and the 1.14 + * server uses maps this reference to the actual element when it executes the command. 1.15 + */ 1.16 + 1.17 +this.EXPORTED_SYMBOLS = [ 1.18 + "ElementManager", 1.19 + "CLASS_NAME", 1.20 + "SELECTOR", 1.21 + "ID", 1.22 + "NAME", 1.23 + "LINK_TEXT", 1.24 + "PARTIAL_LINK_TEXT", 1.25 + "TAG", 1.26 + "XPATH" 1.27 +]; 1.28 + 1.29 +const DOCUMENT_POSITION_DISCONNECTED = 1; 1.30 + 1.31 +let uuidGen = Components.classes["@mozilla.org/uuid-generator;1"] 1.32 + .getService(Components.interfaces.nsIUUIDGenerator); 1.33 + 1.34 +this.CLASS_NAME = "class name"; 1.35 +this.SELECTOR = "css selector"; 1.36 +this.ID = "id"; 1.37 +this.NAME = "name"; 1.38 +this.LINK_TEXT = "link text"; 1.39 +this.PARTIAL_LINK_TEXT = "partial link text"; 1.40 +this.TAG = "tag name"; 1.41 +this.XPATH = "xpath"; 1.42 + 1.43 +function ElementException(msg, num, stack) { 1.44 + this.message = msg; 1.45 + this.code = num; 1.46 + this.stack = stack; 1.47 +} 1.48 + 1.49 +this.ElementManager = function ElementManager(notSupported) { 1.50 + this.seenItems = {}; 1.51 + this.timer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer); 1.52 + this.elementStrategies = [CLASS_NAME, SELECTOR, ID, NAME, LINK_TEXT, PARTIAL_LINK_TEXT, TAG, XPATH]; 1.53 + for (let i = 0; i < notSupported.length; i++) { 1.54 + this.elementStrategies.splice(this.elementStrategies.indexOf(notSupported[i]), 1); 1.55 + } 1.56 +} 1.57 + 1.58 +ElementManager.prototype = { 1.59 + /** 1.60 + * Reset values 1.61 + */ 1.62 + reset: function EM_clear() { 1.63 + this.seenItems = {}; 1.64 + }, 1.65 + 1.66 + /** 1.67 + * Add element to list of seen elements 1.68 + * 1.69 + * @param nsIDOMElement element 1.70 + * The element to add 1.71 + * 1.72 + * @return string 1.73 + * Returns the server-assigned reference ID 1.74 + */ 1.75 + addToKnownElements: function EM_addToKnownElements(element) { 1.76 + for (let i in this.seenItems) { 1.77 + let foundEl = null; 1.78 + try { 1.79 + foundEl = this.seenItems[i].get(); 1.80 + } 1.81 + catch(e) {} 1.82 + if (foundEl) { 1.83 + if (XPCNativeWrapper(foundEl) == XPCNativeWrapper(element)) { 1.84 + return i; 1.85 + } 1.86 + } 1.87 + else { 1.88 + //cleanup reference to GC'd element 1.89 + delete this.seenItems[i]; 1.90 + } 1.91 + } 1.92 + var id = uuidGen.generateUUID().toString(); 1.93 + this.seenItems[id] = Components.utils.getWeakReference(element); 1.94 + return id; 1.95 + }, 1.96 + 1.97 + /** 1.98 + * Retrieve element from its unique ID 1.99 + * 1.100 + * @param String id 1.101 + * The DOM reference ID 1.102 + * @param nsIDOMWindow win 1.103 + * The window that contains the element 1.104 + * 1.105 + * @returns nsIDOMElement 1.106 + * Returns the element or throws Exception if not found 1.107 + */ 1.108 + getKnownElement: function EM_getKnownElement(id, win) { 1.109 + let el = this.seenItems[id]; 1.110 + if (!el) { 1.111 + throw new ElementException("Element has not been seen before. Id given was " + id, 17, null); 1.112 + } 1.113 + try { 1.114 + el = el.get(); 1.115 + } 1.116 + catch(e) { 1.117 + el = null; 1.118 + delete this.seenItems[id]; 1.119 + } 1.120 + // use XPCNativeWrapper to compare elements; see bug 834266 1.121 + let wrappedWin = XPCNativeWrapper(win); 1.122 + if (!el || 1.123 + !(XPCNativeWrapper(el).ownerDocument == wrappedWin.document) || 1.124 + (XPCNativeWrapper(el).compareDocumentPosition(wrappedWin.document.documentElement) & 1.125 + DOCUMENT_POSITION_DISCONNECTED)) { 1.126 + throw new ElementException("The element reference is stale. Either the element " + 1.127 + "is no longer attached to the DOM or the page has been refreshed.", 10, null); 1.128 + } 1.129 + return el; 1.130 + }, 1.131 + 1.132 + /** 1.133 + * Convert values to primitives that can be transported over the 1.134 + * Marionette protocol. 1.135 + * 1.136 + * This function implements the marshaling algorithm defined in the 1.137 + * WebDriver specification: 1.138 + * 1.139 + * https://dvcs.w3.org/hg/webdriver/raw-file/tip/webdriver-spec.html#synchronous-javascript-execution 1.140 + * 1.141 + * @param object val 1.142 + * object to be marshaled 1.143 + * 1.144 + * @return object 1.145 + * Returns a JSON primitive or Object 1.146 + */ 1.147 + wrapValue: function EM_wrapValue(val) { 1.148 + let result = null; 1.149 + 1.150 + switch (typeof(val)) { 1.151 + case "undefined": 1.152 + result = null; 1.153 + break; 1.154 + 1.155 + case "string": 1.156 + case "number": 1.157 + case "boolean": 1.158 + result = val; 1.159 + break; 1.160 + 1.161 + case "object": 1.162 + let type = Object.prototype.toString.call(val); 1.163 + if (type == "[object Array]" || 1.164 + type == "[object NodeList]") { 1.165 + result = []; 1.166 + for (let i = 0; i < val.length; ++i) { 1.167 + result.push(this.wrapValue(val[i])); 1.168 + } 1.169 + } 1.170 + else if (val == null) { 1.171 + result = null; 1.172 + } 1.173 + else if (val.nodeType == 1) { 1.174 + result = {'ELEMENT': this.addToKnownElements(val)}; 1.175 + } 1.176 + else { 1.177 + result = {}; 1.178 + for (let prop in val) { 1.179 + result[prop] = this.wrapValue(val[prop]); 1.180 + } 1.181 + } 1.182 + break; 1.183 + } 1.184 + 1.185 + return result; 1.186 + }, 1.187 + 1.188 + /** 1.189 + * Convert any ELEMENT references in 'args' to the actual elements 1.190 + * 1.191 + * @param object args 1.192 + * Arguments passed in by client 1.193 + * @param nsIDOMWindow win 1.194 + * The window that contains the elements 1.195 + * 1.196 + * @returns object 1.197 + * Returns the objects passed in by the client, with the 1.198 + * reference IDs replaced by the actual elements. 1.199 + */ 1.200 + convertWrappedArguments: function EM_convertWrappedArguments(args, win) { 1.201 + let converted; 1.202 + switch (typeof(args)) { 1.203 + case 'number': 1.204 + case 'string': 1.205 + case 'boolean': 1.206 + converted = args; 1.207 + break; 1.208 + case 'object': 1.209 + if (args == null) { 1.210 + converted = null; 1.211 + } 1.212 + else if (Object.prototype.toString.call(args) == '[object Array]') { 1.213 + converted = []; 1.214 + for (let i in args) { 1.215 + converted.push(this.convertWrappedArguments(args[i], win)); 1.216 + } 1.217 + } 1.218 + else if (typeof(args['ELEMENT'] === 'string') && 1.219 + args.hasOwnProperty('ELEMENT')) { 1.220 + converted = this.getKnownElement(args['ELEMENT'], win); 1.221 + if (converted == null) 1.222 + throw new ElementException("Unknown element: " + args['ELEMENT'], 500, null); 1.223 + } 1.224 + else { 1.225 + converted = {}; 1.226 + for (let prop in args) { 1.227 + converted[prop] = this.convertWrappedArguments(args[prop], win); 1.228 + } 1.229 + } 1.230 + break; 1.231 + } 1.232 + return converted; 1.233 + }, 1.234 + 1.235 + /* 1.236 + * Execute* helpers 1.237 + */ 1.238 + 1.239 + /** 1.240 + * Return an object with any namedArgs applied to it. Used 1.241 + * to let clients use given names when refering to arguments 1.242 + * in execute calls, instead of using the arguments list. 1.243 + * 1.244 + * @param object args 1.245 + * list of arguments being passed in 1.246 + * 1.247 + * @return object 1.248 + * If '__marionetteArgs' is in args, then 1.249 + * it will return an object with these arguments 1.250 + * as its members. 1.251 + */ 1.252 + applyNamedArgs: function EM_applyNamedArgs(args) { 1.253 + namedArgs = {}; 1.254 + args.forEach(function(arg) { 1.255 + if (typeof(arg['__marionetteArgs']) === 'object') { 1.256 + for (let prop in arg['__marionetteArgs']) { 1.257 + namedArgs[prop] = arg['__marionetteArgs'][prop]; 1.258 + } 1.259 + } 1.260 + }); 1.261 + return namedArgs; 1.262 + }, 1.263 + 1.264 + /** 1.265 + * Find an element or elements starting at the document root or 1.266 + * given node, using the given search strategy. Search 1.267 + * will continue until the search timelimit has been reached. 1.268 + * 1.269 + * @param nsIDOMWindow win 1.270 + * The window to search in 1.271 + * @param object values 1.272 + * The 'using' member of values will tell us which search 1.273 + * method to use. The 'value' member tells us the value we 1.274 + * are looking for. 1.275 + * If this object has an 'element' member, this will be used 1.276 + * as the start node instead of the document root 1.277 + * If this object has a 'time' member, this number will be 1.278 + * used to see if we have hit the search timelimit. 1.279 + * @param function on_success 1.280 + * The notification callback used when we are returning successfully. 1.281 + * @param function on_error 1.282 + The callback to invoke when an error occurs. 1.283 + * @param boolean all 1.284 + * If true, all found elements will be returned. 1.285 + * If false, only the first element will be returned. 1.286 + * 1.287 + * @return nsIDOMElement or list of nsIDOMElements 1.288 + * Returns the element(s) by calling the on_success function. 1.289 + */ 1.290 + find: function EM_find(win, values, searchTimeout, on_success, on_error, all, command_id) { 1.291 + let startTime = values.time ? values.time : new Date().getTime(); 1.292 + let startNode = (values.element != undefined) ? 1.293 + this.getKnownElement(values.element, win) : win.document; 1.294 + if (this.elementStrategies.indexOf(values.using) < 0) { 1.295 + throw new ElementException("No such strategy.", 17, null); 1.296 + } 1.297 + let found = all ? this.findElements(values.using, values.value, win.document, startNode) : 1.298 + this.findElement(values.using, values.value, win.document, startNode); 1.299 + if (found) { 1.300 + let type = Object.prototype.toString.call(found); 1.301 + if ((type == '[object Array]') || (type == '[object HTMLCollection]') || (type == '[object NodeList]')) { 1.302 + let ids = [] 1.303 + for (let i = 0 ; i < found.length ; i++) { 1.304 + ids.push(this.addToKnownElements(found[i])); 1.305 + } 1.306 + on_success(ids, command_id); 1.307 + } 1.308 + else { 1.309 + let id = this.addToKnownElements(found); 1.310 + on_success({'ELEMENT':id}, command_id); 1.311 + } 1.312 + return; 1.313 + } else { 1.314 + if (!searchTimeout || new Date().getTime() - startTime > searchTimeout) { 1.315 + on_error("Unable to locate element: " + values.value, 7, null, command_id); 1.316 + } else { 1.317 + values.time = startTime; 1.318 + this.timer.initWithCallback(this.find.bind(this, win, values, 1.319 + searchTimeout, 1.320 + on_success, on_error, all, 1.321 + command_id), 1.322 + 100, 1.323 + Components.interfaces.nsITimer.TYPE_ONE_SHOT); 1.324 + } 1.325 + } 1.326 + }, 1.327 + 1.328 + /** 1.329 + * Find a value by XPATH 1.330 + * 1.331 + * @param nsIDOMElement root 1.332 + * Document root 1.333 + * @param string value 1.334 + * XPATH search string 1.335 + * @param nsIDOMElement node 1.336 + * start node 1.337 + * 1.338 + * @return nsIDOMElement 1.339 + * returns the found element 1.340 + */ 1.341 + findByXPath: function EM_findByXPath(root, value, node) { 1.342 + return root.evaluate(value, node, null, 1.343 + Components.interfaces.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; 1.344 + }, 1.345 + 1.346 + /** 1.347 + * Find values by XPATH 1.348 + * 1.349 + * @param nsIDOMElement root 1.350 + * Document root 1.351 + * @param string value 1.352 + * XPATH search string 1.353 + * @param nsIDOMElement node 1.354 + * start node 1.355 + * 1.356 + * @return object 1.357 + * returns a list of found nsIDOMElements 1.358 + */ 1.359 + findByXPathAll: function EM_findByXPathAll(root, value, node) { 1.360 + let values = root.evaluate(value, node, null, 1.361 + Components.interfaces.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, null); 1.362 + let elements = []; 1.363 + let element = values.iterateNext(); 1.364 + while (element) { 1.365 + elements.push(element); 1.366 + element = values.iterateNext(); 1.367 + } 1.368 + return elements; 1.369 + }, 1.370 + 1.371 + /** 1.372 + * Helper method to find. Finds one element using find's criteria 1.373 + * 1.374 + * @param string using 1.375 + * String identifying which search method to use 1.376 + * @param string value 1.377 + * Value to look for 1.378 + * @param nsIDOMElement rootNode 1.379 + * Document root 1.380 + * @param nsIDOMElement startNode 1.381 + * Node from which we start searching 1.382 + * 1.383 + * @return nsIDOMElement 1.384 + * Returns found element or throws Exception if not found 1.385 + */ 1.386 + findElement: function EM_findElement(using, value, rootNode, startNode) { 1.387 + let element; 1.388 + switch (using) { 1.389 + case ID: 1.390 + element = startNode.getElementById ? 1.391 + startNode.getElementById(value) : 1.392 + this.findByXPath(rootNode, './/*[@id="' + value + '"]', startNode); 1.393 + break; 1.394 + case NAME: 1.395 + element = startNode.getElementsByName ? 1.396 + startNode.getElementsByName(value)[0] : 1.397 + this.findByXPath(rootNode, './/*[@name="' + value + '"]', startNode); 1.398 + break; 1.399 + case CLASS_NAME: 1.400 + element = startNode.getElementsByClassName(value)[0]; //works for >=FF3 1.401 + break; 1.402 + case TAG: 1.403 + element = startNode.getElementsByTagName(value)[0]; //works for all elements 1.404 + break; 1.405 + case XPATH: 1.406 + element = this.findByXPath(rootNode, value, startNode); 1.407 + break; 1.408 + case LINK_TEXT: 1.409 + case PARTIAL_LINK_TEXT: 1.410 + let allLinks = startNode.getElementsByTagName('A'); 1.411 + for (let i = 0; i < allLinks.length && !element; i++) { 1.412 + let text = allLinks[i].text; 1.413 + if (PARTIAL_LINK_TEXT == using) { 1.414 + if (text.indexOf(value) != -1) { 1.415 + element = allLinks[i]; 1.416 + } 1.417 + } else if (text == value) { 1.418 + element = allLinks[i]; 1.419 + } 1.420 + } 1.421 + break; 1.422 + case SELECTOR: 1.423 + element = startNode.querySelector(value); 1.424 + break; 1.425 + default: 1.426 + throw new ElementException("No such strategy", 500, null); 1.427 + } 1.428 + return element; 1.429 + }, 1.430 + 1.431 + /** 1.432 + * Helper method to find. Finds all element using find's criteria 1.433 + * 1.434 + * @param string using 1.435 + * String identifying which search method to use 1.436 + * @param string value 1.437 + * Value to look for 1.438 + * @param nsIDOMElement rootNode 1.439 + * Document root 1.440 + * @param nsIDOMElement startNode 1.441 + * Node from which we start searching 1.442 + * 1.443 + * @return nsIDOMElement 1.444 + * Returns found elements or throws Exception if not found 1.445 + */ 1.446 + findElements: function EM_findElements(using, value, rootNode, startNode) { 1.447 + let elements = []; 1.448 + switch (using) { 1.449 + case ID: 1.450 + value = './/*[@id="' + value + '"]'; 1.451 + case XPATH: 1.452 + elements = this.findByXPathAll(rootNode, value, startNode); 1.453 + break; 1.454 + case NAME: 1.455 + elements = startNode.getElementsByName ? 1.456 + startNode.getElementsByName(value) : 1.457 + this.findByXPathAll(rootNode, './/*[@name="' + value + '"]', startNode); 1.458 + break; 1.459 + case CLASS_NAME: 1.460 + elements = startNode.getElementsByClassName(value); 1.461 + break; 1.462 + case TAG: 1.463 + elements = startNode.getElementsByTagName(value); 1.464 + break; 1.465 + case LINK_TEXT: 1.466 + case PARTIAL_LINK_TEXT: 1.467 + let allLinks = rootNode.getElementsByTagName('A'); 1.468 + for (let i = 0; i < allLinks.length; i++) { 1.469 + let text = allLinks[i].text; 1.470 + if (PARTIAL_LINK_TEXT == using) { 1.471 + if (text.indexOf(value) != -1) { 1.472 + elements.push(allLinks[i]); 1.473 + } 1.474 + } else if (text == value) { 1.475 + elements.push(allLinks[i]); 1.476 + } 1.477 + } 1.478 + break; 1.479 + case SELECTOR: 1.480 + elements = Array.slice(startNode.querySelectorAll(value)); 1.481 + break; 1.482 + default: 1.483 + throw new ElementException("No such strategy", 500, null); 1.484 + } 1.485 + return elements; 1.486 + }, 1.487 +}