Blame view

modules/clientutils.js 34.5 KB
1 2 3
/*!
 * Casper is a navigation utility for PhantomJS.
 *
4
 * Documentation: http://casperjs.org/
5 6
 * Repository:    http://github.com/n1k0/casperjs
 *
7 8 9
 * Copyright (c) 2011-2012 Nicolas Perriault
 *
 * Part of source code is Copyright Joyent, Inc. and other Node contributors.
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */
30

31
/*global console, escape, exports, NodeList, window*/
32

33
(function(exports) {
34 35
    "use strict";

36 37
    exports.create = function create(options) {
        return new this.ClientUtils(options);
38
    };
39

40
    /**
41
     * Casper client-side helpers.
42
     */
43
    exports.ClientUtils = function ClientUtils(options) {
44
        /*jshint maxstatements:40*/
45
        // private members
46 47 48 49 50 51 52 53 54 55 56
        var BASE64_ENCODE_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
        var BASE64_DECODE_CHARS = new Array(
            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
            52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
            -1,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14,
            15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
            -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
            41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1
        );
57
        var SUPPORTED_SELECTOR_TYPES = ['css', 'xpath'];
58

59 60
        // public members
        this.options = options || {};
61
        this.options.scope = this.options.scope || document;
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81

        /**
         * Calls a method part of the current prototype, with arguments.
         *
         * @param  {String} method Method name
         * @param  {Array}  args   arguments
         * @return {Mixed}
         */
        this.__call = function __call(method, args) {
            if (method === "__call") {
                return;
            }
            try {
                return this[method].apply(this, args);
            } catch (err) {
                err.__isCallError = true;
                return err;
            }
        };

82
        /**
83 84
         * Clicks on the DOM element behind the provided selector.
         *
85
         * @param  String  selector  A CSS3 selector to the element to click
86 87
         * @return Boolean
         */
88
        this.click = function click(selector) {
89 90 91 92
            return this.mouseEvent('click', selector);
        };

        /**
93 94 95 96 97
         * Decodes a base64 encoded string. Succeeds where window.atob() fails.
         *
         * @param  String  str  The base64 encoded contents
         * @return string
         */
98
        this.decode = function decode(str) {
99
            /*jshint maxstatements:30, maxcomplexity:30 */
100 101 102 103
            var c1, c2, c3, c4, i = 0, len = str.length, out = "";
            while (i < len) {
                do {
                    c1 = BASE64_DECODE_CHARS[str.charCodeAt(i++) & 0xff];
104 105
                } while (i < len && c1 === -1);
                if (c1 === -1) {
106 107 108 109
                    break;
                }
                do {
                    c2 = BASE64_DECODE_CHARS[str.charCodeAt(i++) & 0xff];
110 111
                } while (i < len && c2 === -1);
                if (c2 === -1) {
112 113 114 115 116
                    break;
                }
                out += String.fromCharCode((c1 << 2) | ((c2 & 0x30) >> 4));
                do {
                    c3 = str.charCodeAt(i++) & 0xff;
117
                    if (c3 === 61)
118
                    return out;
119
                    c3 = BASE64_DECODE_CHARS[c3];
120 121
                } while (i < len && c3 === -1);
                if (c3 === -1) {
122 123 124 125 126
                    break;
                }
                out += String.fromCharCode(((c2 & 0XF) << 4) | ((c3 & 0x3C) >> 2));
                do {
                    c4 = str.charCodeAt(i++) & 0xff;
127
                    if (c4 === 61) {
128 129 130
                        return out;
                    }
                    c4 = BASE64_DECODE_CHARS[c4];
131 132
                } while (i < len && c4 === -1);
                if (c4 === -1) {
133
                    break;
134
                }
135
                out += String.fromCharCode(((c3 & 0x03) << 6) | c4);
136
            }
137 138
            return out;
        };
139

140
        /**
141 142 143 144 145 146 147 148 149 150
         * Echoes something to casper console.
         *
         * @param  String  message
         * @return
         */
        this.echo = function echo(message) {
            console.log("[casper.echo] " + message);
        };

        /**
151 152
         * Checks if a given DOM element is visible in remove page.
         *
153
         * @param  Object   element  DOM element
154 155 156
         * @return Boolean
         */
        this.elementVisible = function elementVisible(elem) {
157
            var style;
158
            try {
159
                style = window.getComputedStyle(elem, null);
160 161 162
            } catch (e) {
                return false;
            }
163 164 165 166 167 168 169 170
            var hidden = style.visibility === 'hidden' || style.display === 'none';
            if (hidden) {
                return false;
            }
            if (style.display === "inline") {
                return true;
            }
            return elem.clientHeight > 0 && elem.clientWidth > 0;
171 172 173
        }

        /**
174 175 176 177 178 179
         * Base64 encodes a string, even binary ones. Succeeds where
         * window.btoa() fails.
         *
         * @param  String  str  The string content to encode
         * @return string
         */
180
        this.encode = function encode(str) {
181
            /*jshint maxstatements:30 */
182 183 184
            var out = "", i = 0, len = str.length, c1, c2, c3;
            while (i < len) {
                c1 = str.charCodeAt(i++) & 0xff;
185
                if (i === len) {
186 187 188 189 190 191
                    out += BASE64_ENCODE_CHARS.charAt(c1 >> 2);
                    out += BASE64_ENCODE_CHARS.charAt((c1 & 0x3) << 4);
                    out += "==";
                    break;
                }
                c2 = str.charCodeAt(i++);
192
                if (i === len) {
193 194 195 196 197 198 199
                    out += BASE64_ENCODE_CHARS.charAt(c1 >> 2);
                    out += BASE64_ENCODE_CHARS.charAt(((c1 & 0x3)<< 4) | ((c2 & 0xF0) >> 4));
                    out += BASE64_ENCODE_CHARS.charAt((c2 & 0xF) << 2);
                    out += "=";
                    break;
                }
                c3 = str.charCodeAt(i++);
200
                out += BASE64_ENCODE_CHARS.charAt(c1 >> 2);
201 202 203
                out += BASE64_ENCODE_CHARS.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4));
                out += BASE64_ENCODE_CHARS.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >> 6));
                out += BASE64_ENCODE_CHARS.charAt(c3 & 0x3F);
204
            }
205 206
            return out;
        };
207

208 209 210 211 212 213
        /**
         * Checks if a given DOM element exists in remote page.
         *
         * @param  String  selector  CSS3 selector
         * @return Boolean
         */
214
        this.exists = function exists(selector) {
215
            try {
216
                return this.findAll(selector).length > 0;
217 218 219
            } catch (e) {
                return false;
            }
220
        };
221 222 223 224 225 226 227 228

        /**
         * Fetches innerText within the element(s) matching a given CSS3
         * selector.
         *
         * @param  String  selector  A CSS3 selector
         * @return String
         */
229
        this.fetchText = function fetchText(selector) {
230 231
            var text = '', elements = this.findAll(selector);
            if (elements && elements.length) {
232
                Array.prototype.forEach.call(elements, function _forEach(element) {
233
                    text += element.textContent || element.innerText;
234
                });
235
            }
236 237 238 239
            return text;
        };

        /**
240
         * Fills a form with provided field values, and optionally submits it.
241
         *
242 243 244 245
         * @param  HTMLElement|String  form      A form element, or a CSS3 selector to a form element
         * @param  Object              vals      Field values
         * @param  Function            findType  Element finder type (css, names, xpath)
         * @return Object                        An object containing setting result for each field, including file uploads
246
         */
247
        this.fill = function fill(form, vals, findType) {
248
            /*jshint maxcomplexity:8*/
249 250 251 252 253
            var out = {
                errors: [],
                fields: [],
                files:  []
            };
254

255
            if (!(form instanceof HTMLElement) || typeof form === "string") {
256
                this.log("attempting to fetch form element from selector: '" + form + "'", "info");
257
                try {
258
                    form = this.findOne(form);
259 260 261 262 263 264
                } catch (e) {
                    if (e.name === "SYNTAX_ERR") {
                        out.errors.push("invalid form selector provided: '" + form + "'");
                        return out;
                    }
                }
265
            }
266

267 268 269 270
            if (!form) {
                out.errors.push("form not found");
                return out;
            }
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285

            var finders = {
                css: function(inputSelector, formSelector) {
                    return this.findAll(inputSelector, form);
                },
                names: function(elementName, formSelector) {
                    return this.findAll('[name="' + elementName + '"]', form);
                },
                xpath: function(xpath, formSelector) {
                    return this.findAll({type: "xpath", path: xpath}, form);
                }
            };

            for (var fieldSelector in vals) {
                if (!vals.hasOwnProperty(fieldSelector)) {
286 287
                    continue;
                }
288 289
                var field = finders[findType || "names"].call(this, fieldSelector, form),
                    value = vals[fieldSelector];
290
                if (!field || field.length === 0) {
291
                    out.errors.push('no field matching ' + findType + ' selector "' + fieldSelector + '" in form');
292 293 294
                    continue;
                }
                try {
295
                    out.fields[fieldSelector] = this.setField(field, value);
296 297 298
                } catch (err) {
                    if (err.name === "FileUploadError") {
                        out.files.push({
299 300
                            type: findType,
                            selector: fieldSelector,
301 302
                            path: err.path
                        });
303 304
                    } else if (err.name === "FieldNotFound") {
                        out.errors.push('Unable to find field element in form: ' + err.toString());
305
                    } else {
306
                        out.errors.push(err.toString());
307
                    }
308
                }
309
            }
310 311
            return out;
        };
312

313 314 315
        /**
         * Finds all DOM elements matching by the provided selector.
         *
316 317
         * @param  String            selector  CSS3 selector
         * @param  HTMLElement|null  scope     Element to search child elements within
318
         * @return Array|undefined
319
         */
320
        this.findAll = function findAll(selector, scope) {
321
            scope = scope || this.options.scope;
322
            try {
323
                var pSelector = this.processSelector(selector);
324
                if (pSelector.type === 'xpath') {
325
                    return this.getElementsByXPath(pSelector.path, scope);
326
                } else {
327
                    return Array.prototype.slice.call(scope.querySelectorAll(pSelector.path));
328
                }
329
            } catch (e) {
330
                this.log('findAll(): invalid selector provided "' + selector + '":' + e, "error");
331 332
            }
        };
333

334 335 336
        /**
         * Finds a DOM element by the provided selector.
         *
337 338
         * @param  String            selector  CSS3 selector
         * @param  HTMLElement|null  scope     Element to search child elements within
339 340
         * @return HTMLElement|undefined
         */
341
        this.findOne = function findOne(selector, scope) {
342
            scope = scope || this.options.scope;
343
            try {
344
                var pSelector = this.processSelector(selector);
345
                if (pSelector.type === 'xpath') {
346
                    return this.getElementByXPath(pSelector.path, scope);
347
                } else {
348
                    return scope.querySelector(pSelector.path);
349
                }
350
            } catch (e) {
351
                this.log('findOne(): invalid selector provided "' + selector + '":' + e, "error");
352 353
            }
        };
354

355 356 357 358 359 360 361 362 363
        /**
         * Downloads a resource behind an url and returns its base64-encoded
         * contents.
         *
         * @param  String  url     The resource url
         * @param  String  method  The request method, optional (default: GET)
         * @param  Object  data    The request data, optional
         * @return String          Base64 contents string
         */
364
        this.getBase64 = function getBase64(url, method, data) {
365 366
            return this.encode(this.getBinary(url, method, data));
        };
367

368 369 370 371
        /**
         * Retrieves string contents from a binary file behind an url. Silently
         * fails but log errors.
         *
372 373 374 375
         * @param   String   url     Url.
         * @param   String   method  HTTP method.
         * @param   Object   data    Request parameters.
         * @return  String
376
         */
377
        this.getBinary = function getBinary(url, method, data) {
378
            try {
379
                return this.sendAJAX(url, method, data, false);
380 381 382 383 384 385
            } catch (e) {
                if (e.name === "NETWORK_ERR" && e.code === 101) {
                    this.log("getBinary(): Unfortunately, casperjs cannot make cross domain ajax requests", "warning");
                }
                this.log("getBinary(): Error while fetching " + url + ": " + e, "error");
                return "";
386
            }
387
        };
388

389
        /**
390 391 392 393 394 395 396 397 398 399 400 401 402 403
         * Retrieves total document height.
         * http://james.padolsey.com/javascript/get-document-height-cross-browser/
         *
         * @return {Number}
         */
        this.getDocumentHeight = function getDocumentHeight() {
            return Math.max(
                Math.max(document.body.scrollHeight, document.documentElement.scrollHeight),
                Math.max(document.body.offsetHeight, document.documentElement.offsetHeight),
                Math.max(document.body.clientHeight, document.documentElement.clientHeight)
            );
        };

        /**
404
         * Retrieves bounding rect coordinates of the HTML element matching the
405 406 407
         * provided CSS3 selector in the following form:
         *
         * {top: y, left: x, width: w, height:, h}
408 409 410 411
         *
         * @param  String  selector
         * @return Object or null
         */
412
        this.getElementBounds = function getElementBounds(selector) {
413
            try {
414
                var clipRect = this.findOne(selector).getBoundingClientRect();
415 416 417 418 419 420 421 422 423 424 425
                return {
                    top:    clipRect.top,
                    left:   clipRect.left,
                    width:  clipRect.width,
                    height: clipRect.height
                };
            } catch (e) {
                this.log("Unable to fetch bounds for element " + selector, "warning");
            }
        };

426
        /**
427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455
         * Retrieves the list of bounding rect coordinates for all the HTML elements matching the
         * provided CSS3 selector, in the following form:
         *
         * [{top: y, left: x, width: w, height:, h},
         *  {top: y, left: x, width: w, height:, h},
         *  ...]
         *
         * @param  String  selector
         * @return Array
         */
        this.getElementsBounds = function getElementsBounds(selector) {
            var elements = this.findAll(selector);
            var self = this;
            try {
                return Array.prototype.map.call(elements, function(element) {
                    var clipRect = element.getBoundingClientRect();
                    return {
                        top:    clipRect.top,
                        left:   clipRect.left,
                        width:  clipRect.width,
                        height: clipRect.height
                    };
                });
            } catch (e) {
                this.log("Unable to fetch bounds for elements matching " + selector, "warning");
            }
        };

        /**
456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472
         * Retrieves information about the node matching the provided selector.
         *
         * @param  String|Object  selector  CSS3/XPath selector
         * @return Object
         */
        this.getElementInfo = function getElementInfo(selector) {
            var element = this.findOne(selector);
            var bounds = this.getElementBounds(selector);
            var attributes = {};
            [].forEach.call(element.attributes, function(attr) {
                attributes[attr.name.toLowerCase()] = attr.value;
            });
            return {
                nodeName: element.nodeName.toLowerCase(),
                attributes: attributes,
                tag: element.outerHTML,
                html: element.innerHTML,
473
                text: element.textContent || element.innerText,
474 475 476 477 478 479 480 481 482
                x: bounds.left,
                y: bounds.top,
                width: bounds.width,
                height: bounds.height,
                visible: this.visible(selector)
            };
        };

        /**
483 484 485 486 487 488 489
         * Retrieves information about the nodes matching the provided selector.
         *
         * @param  String|Object  selector  CSS3/XPath selector
         * @return Array
         */
        this.getElementsInfo = function getElementsInfo(selector) {
            var bounds = this.getElementsBounds(selector);
490 491
            var eleVisible = this.elementVisible;
            return [].map.call(this.findAll(selector), function(element, index) {
492 493 494 495 496 497 498 499 500
                var attributes = {};
                [].forEach.call(element.attributes, function(attr) {
                    attributes[attr.name.toLowerCase()] = attr.value;
                });
                return {
                    nodeName: element.nodeName.toLowerCase(),
                    attributes: attributes,
                    tag: element.outerHTML,
                    html: element.innerHTML,
501
                    text: element.textContent || element.innerText,
502 503 504 505
                    x: bounds[index].left,
                    y: bounds[index].top,
                    width: bounds[index].width,
                    height: bounds[index].height,
506
                    visible: eleVisible(element)
507 508 509 510 511
                };
            });
        };

        /**
512
         * Retrieves a single DOM element matching a given XPath expression.
513
         *
514 515
         * @param  String            expression  The XPath expression
         * @param  HTMLElement|null  scope       Element to search child elements within
516 517
         * @return HTMLElement or null
         */
518 519 520
        this.getElementByXPath = function getElementByXPath(expression, scope) {
            scope = scope || this.options.scope;
            var a = document.evaluate(expression, scope, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
521 522 523 524 525
            if (a.snapshotLength > 0) {
                return a.snapshotItem(0);
            }
        };

526 527 528
        /**
         * Retrieves all DOM elements matching a given XPath expression.
         *
529 530
         * @param  String            expression  The XPath expression
         * @param  HTMLElement|null  scope       Element to search child elements within
531 532
         * @return Array
         */
533 534
        this.getElementsByXPath = function getElementsByXPath(expression, scope) {
            scope = scope || this.options.scope;
535
            var nodes = [];
536
            var a = document.evaluate(expression, scope, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
537 538 539 540 541 542
            for (var i = 0; i < a.snapshotLength; i++) {
                nodes.push(a.snapshotItem(i));
            }
            return nodes;
        };

543 544 545 546
        /**
         * Retrieves the value of a form field.
         *
         * @param  String  inputName  The for input name attr value
547
         * @param  Object  options    Object with formSelector, optional
548 549
         * @return Mixed
         */
550
        this.getFieldValue = function getFieldValue(inputName, options) {
551
            options = options || {};
552
            function getSingleValue(input) {
553
                var type;
554 555 556 557 558 559 560 561 562 563 564 565 566 567 568
                try {
                    type = input.getAttribute('type').toLowerCase();
                } catch (e) {
                    type = 'other';
                }
                if (['checkbox', 'radio'].indexOf(type) === -1) {
                    return input.value;
                }
                // single checkbox or… radio button (weird, I know)
                if (input.hasAttribute('value')) {
                    return input.checked ? input.getAttribute('value') : undefined;
                }
                return input.checked;
            }
            function getMultipleValues(inputs) {
569
                var type;
570 571 572 573
                type = inputs[0].getAttribute('type').toLowerCase();
                if (type === 'radio') {
                    var value;
                    [].forEach.call(inputs, function(radio) {
574
                        value = radio.checked ? radio.value : value;
575 576 577 578 579 580 581 582 583 584 585 586
                    });
                    return value;
                } else if (type === 'checkbox') {
                    var values = [];
                    [].forEach.call(inputs, function(checkbox) {
                        if (checkbox.checked) {
                            values.push(checkbox.value);
                        }
                    });
                    return values;
                }
            }
587
            var formSelector = '';
588
            if (options.formSelector) {
589
                formSelector = options.formSelector + ' ';
590 591
            }
            var inputs = this.findAll(formSelector + '[name="' + inputName + '"]');
592 593 594 595 596 597 598 599 600

            if (options.inputSelector) {
                inputs = inputs.concat(this.findAll(options.inputSelector));
            }

            if (options.inputXPath) {
                inputs = inputs.concat(this.getElementsByXPath(options.inputXPath));
            }

601
            switch (inputs.length) {
602
                case 0:  return undefined;
603 604
                case 1:  return getSingleValue(inputs[0]);
                default: return getMultipleValues(inputs);
605 606 607
            }
        };

608
        /**
609 610 611 612 613 614
         * Retrieves a given form all of its field values.
         *
         * @param  String  selector  A DOM CSS3/XPath selector
         * @return Object
         */
        this.getFormValues = function getFormValues(selector) {
Nicolas Perriault authored
615
            var form = this.findOne(selector);
616 617 618 619
            var values = {};
            var self = this;
            [].forEach.call(form.elements, function(element) {
                var name = element.getAttribute('name');
620
                if (name && !values[name]) {
621
                    values[name] = self.getFieldValue(name, {formSelector: selector});
622 623 624 625 626 627
                }
            });
            return values;
        };

        /**
628 629
         * Logs a message. Will format the message a way CasperJS will be able
         * to log phantomjs side.
630
         *
631 632
         * @param  String  message  The message to log
         * @param  String  level    The log level
633
         */
634
        this.log = function log(message, level) {
635 636
            console.log("[casper:" + (level || "debug") + "] " + message);
        };
637

638
        /**
639 640 641 642 643 644 645 646 647 648 649 650
         * Dispatches a mouse event to the DOM element behind the provided selector.
         *
         * @param  String   type     Type of event to dispatch
         * @param  String  selector  A CSS3 selector to the element to click
         * @return Boolean
         */
        this.mouseEvent = function mouseEvent(type, selector) {
            var elem = this.findOne(selector);
            if (!elem) {
                this.log("mouseEvent(): Couldn't find any element matching '" + selector + "' selector", "error");
                return false;
            }
651 652
            try {
                var evt = document.createEvent("MouseEvents");
653 654 655 656 657 658
                var center_x = 1, center_y = 1;
                try {
                    var pos = elem.getBoundingClientRect();
                    center_x = Math.floor((pos.left + pos.right) / 2),
                    center_y = Math.floor((pos.top + pos.bottom) / 2);
                } catch(e) {}
659
                evt.initMouseEvent(type, true, true, window, 1, 1, 1, center_x, center_y, false, false, false, false, 0, elem);
660 661 662 663 664 665 666 667 668 669 670
                // dispatchEvent return value is false if at least one of the event
                // handlers which handled this event called preventDefault;
                // so we cannot returns this results as it cannot accurately informs on the status
                // of the operation
                // let's assume the event has been sent ok it didn't raise any error
                elem.dispatchEvent(evt);
                return true;
            } catch (e) {
                this.log("Failed dispatching " + type + "mouse event on " + selector + ": " + e, "error");
                return false;
            }
671 672 673
        };

        /**
674 675 676 677 678 679 680 681 682 683 684 685 686
         * Processes a selector input, either as a string or an object.
         *
         * If passed an object, if must be of the form:
         *
         *     selectorObject = {
         *         type: <'css' or 'xpath'>,
         *         path: <a string>
         *     }
         *
         * @param  String|Object  selector  The selector string or object
         *
         * @return an object containing 'type' and 'path' keys
         */
687 688 689
        this.processSelector = function processSelector(selector) {
            var selectorObject = {
                toString: function toString() {
690
                    return this.type + ' selector: ' + this.path;
691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712
                }
            };
            if (typeof selector === "string") {
                // defaults to CSS selector
                selectorObject.type = "css";
                selectorObject.path = selector;
                return selectorObject;
            } else if (typeof selector === "object") {
                // validation
                if (!selector.hasOwnProperty('type') || !selector.hasOwnProperty('path')) {
                    throw new Error("Incomplete selector object");
                } else if (SUPPORTED_SELECTOR_TYPES.indexOf(selector.type) === -1) {
                    throw new Error("Unsupported selector type: " + selector.type);
                }
                if (!selector.hasOwnProperty('toString')) {
                    selector.toString = selectorObject.toString;
                }
                return selector;
            }
            throw new Error("Unsupported selector type: " + typeof selector);
        };

713
        /**
714 715 716 717 718 719 720 721 722 723 724 725 726
         * Removes all DOM elements matching a given XPath expression.
         *
         * @param  String  expression  The XPath expression
         * @return Array
         */
        this.removeElementsByXPath = function removeElementsByXPath(expression) {
            var a = document.evaluate(expression, document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
            for (var i = 0; i < a.snapshotLength; i++) {
                a.snapshotItem(i).parentNode.removeChild(a.snapshotItem(i));
            }
        };

        /**
727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743
         * Scrolls current document to x, y coordinates.
         *
         * @param  {Number} x X position
         * @param  {Number} y Y position
         */
        this.scrollTo = function scrollTo(x, y) {
            window.scrollTo(parseInt(x || 0, 10), parseInt(y || 0, 10));
        };

        /**
         * Scrolls current document up to its bottom.
         */
        this.scrollToBottom = function scrollToBottom() {
            this.scrollTo(0, this.getDocumentHeight());
        },

        /**
744 745
         * Performs an AJAX request.
         *
746 747 748 749 750 751
         * @param   String   url      Url.
         * @param   String   method   HTTP method (default: GET).
         * @param   Object   data     Request parameters.
         * @param   Boolean  async    Asynchroneous request? (default: false)
         * @param   Object   settings Other settings when perform the ajax request
         * @return  String            Response text.
752
         */
753
        this.sendAJAX = function sendAJAX(url, method, data, async, settings) {
754 755 756 757
            var xhr = new XMLHttpRequest(),
                dataString = "",
                dataList = [];
            method = method && method.toUpperCase() || "GET";
758
            var contentType = settings && settings.contentType || "application/x-www-form-urlencoded";
759
            xhr.open(method, url, !!async);
760
            this.log("sendAJAX(): Using HTTP method: '" + method + "'", "debug");
761 762 763 764 765 766 767 768 769 770 771
            xhr.overrideMimeType("text/plain; charset=x-user-defined");
            if (method === "POST") {
                if (typeof data === "object") {
                    for (var k in data) {
                        dataList.push(encodeURIComponent(k) + "=" + encodeURIComponent(data[k].toString()));
                    }
                    dataString = dataList.join('&');
                    this.log("sendAJAX(): Using request data: '" + dataString + "'", "debug");
                } else if (typeof data === "string") {
                    dataString = data;
                }
772
                xhr.setRequestHeader("Content-Type", contentType);
773 774 775 776 777 778
            }
            xhr.send(method === "POST" ? dataString : null);
            return xhr.responseText;
        };

        /**
779 780 781 782 783 784
         * Sets a field (or a set of fields) value. Fails silently, but log
         * error messages.
         *
         * @param  HTMLElement|NodeList  field  One or more element defining a field
         * @param  mixed                 value  The field value to set
         */
785
        this.setField = function setField(field, value) {
786
            /*jshint maxcomplexity:99 */
787
            var logValue, fields, out;
788
            value = logValue = (value || "");
789 790

            if (field instanceof NodeList || field instanceof Array) {
791 792 793
                fields = field;
                field = fields[0];
            }
794

795 796 797 798
            if (!(field instanceof HTMLElement)) {
                var error = new Error('Invalid field type; only HTMLElement and NodeList are supported');
                error.name = 'FieldNotFound';
                throw error;
799
            }
800

801 802
            if (this.options && this.options.safeLogs && field.getAttribute('type') === "password") {
                // obfuscate password value
803
                logValue = new Array(value.length + 1).join("*");
804
            }
805

806
            this.log('Set "' + field.getAttribute('name') + '" field value to ' + logValue, "debug");
807

808 809 810
            try {
                field.focus();
            } catch (e) {
811
                this.log("Unable to focus() input field " + field.getAttribute('name') + ": " + e, "warning");
812
            }
813

814
            var nodeName = field.nodeName.toLowerCase();
815

816 817 818 819 820 821 822 823 824 825
            switch (nodeName) {
                case "input":
                    var type = field.getAttribute('type') || "text";
                    switch (type.toLowerCase()) {
                        case "checkbox":
                            if (fields.length > 1) {
                                var values = value;
                                if (!Array.isArray(values)) {
                                    values = [values];
                                }
826
                                Array.prototype.forEach.call(fields, function _forEach(f) {
827 828 829 830
                                    f.checked = values.indexOf(f.value) !== -1 ? true : false;
                                });
                            } else {
                                field.checked = value ? true : false;
831
                            }
832 833 834 835
                            break;
                        case "file":
                            throw {
                                name:    "FileUploadError",
836
                                message: "File field must be filled using page.uploadFile",
837 838 839 840
                                path:    value
                            };
                        case "radio":
                            if (fields) {
841
                                Array.prototype.forEach.call(fields, function _forEach(e) {
842 843 844
                                    e.checked = (e.value === value);
                                });
                            } else {
pborreli authored
845
                                out = 'Provided radio elements are empty';
846 847 848
                            }
                            break;
                        default:
849
                            field.value = value;
850 851 852 853 854 855 856 857
                            break;
                    }
                    break;
                case "select":
                case "textarea":
                    field.value = value;
                    break;
                default:
858
                    out = 'Unsupported field type: ' + nodeName;
859 860
                    break;
            }
861

Lee Byrd authored
862 863 864 865 866 867
            // firing the `change` and `input` events
            ['change', 'input'].forEach(function(name) {
                var event = document.createEvent("HTMLEvents");
                event.initEvent(name, true, true);
                field.dispatchEvent(event);
            });
868

869
            // blur the field
870 871 872
            try {
                field.blur();
            } catch (err) {
873
                this.log("Unable to blur() input field " + field.getAttribute('name') + ": " + err, "warning");
874 875 876
            }
            return out;
        };
877 878

        /**
879
         * Checks if any element matching a given selector is visible in remote page.
880 881 882 883
         *
         * @param  String  selector  CSS3 selector
         * @return Boolean
         */
884
        this.visible = function visible(selector) {
885
            return [].some.call(this.findAll(selector), this.elementVisible);
886
        };
887
    };
888
})(typeof exports === "object" ? exports : window);