testing/marionette/marionette-elements.js

Wed, 31 Dec 2014 06:55:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:55:50 +0100
changeset 2
7e26c7da4463
permissions
-rw-r--r--

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 }

mercurial