/*! * Casper is a navigation utility for PhantomJS. * * Documentation: http://casperjs.org/ * Repository: http://github.com/casperjs/casperjs * * Copyright (c) 2011-2012 Nicolas Perriault * * Part of source code is Copyright Joyent, Inc. and other Node contributors. * * 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. * */ /*global escape, NodeList*/ (function(exports) { "use strict"; exports.create = function create(options) { return new this.ClientUtils(options); }; /** * Casper client-side helpers. */ exports.ClientUtils = function ClientUtils(options) { /*eslint max-statements:0, no-multi-spaces:0*/ // private members var BASE64_ENCODE_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; var BASE64_DECODE_CHARS = [ -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 ]; var SUPPORTED_SELECTOR_TYPES = ['css', 'xpath']; var XPATH_NAMESPACE = { svg: 'http://www.w3.org/2000/svg', mathml: 'http://www.w3.org/1998/Math/MathML' }; function form_urlencoded(str) { return encodeURIComponent(str) .replace(/%20/g, '+') .replace(/[!'()*]/g, function(c) { return '%' + c.charCodeAt(0).toString(16); }); } // public members this.options = options || {}; this.options.scope = this.options.scope || document; /** * 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; } }; /** * Clicks on the DOM element behind the provided selector. * * @param String selector A CSS3 selector to the element to click * @param {Number} x X position * @param {Number} y Y position * @return Boolean */ this.click = function click(selector, x, y) { return this.mouseEvent('click', selector, x, y); }; /** * Decodes a base64 encoded string. Succeeds where window.atob() fails. * * @param String str The base64 encoded contents * @return string */ this.decode = function decode(str) { /*eslint max-statements:0, complexity:0 */ var c1, c2, c3, c4, i = 0, len = str.length, out = ""; while (i < len) { do { c1 = BASE64_DECODE_CHARS[str.charCodeAt(i++) & 0xff]; } while (i < len && c1 === -1); if (c1 === -1) { break; } do { c2 = BASE64_DECODE_CHARS[str.charCodeAt(i++) & 0xff]; } while (i < len && c2 === -1); if (c2 === -1) { break; } out += String.fromCharCode(c1 << 2 | (c2 & 0x30) >> 4); do { c3 = str.charCodeAt(i++) & 0xff; if (c3 === 61) { return out; } c3 = BASE64_DECODE_CHARS[c3]; } while (i < len && c3 === -1); if (c3 === -1) { break; } out += String.fromCharCode((c2 & 0XF) << 4 | (c3 & 0x3C) >> 2); do { c4 = str.charCodeAt(i++) & 0xff; if (c4 === 61) { return out; } c4 = BASE64_DECODE_CHARS[c4]; } while (i < len && c4 === -1); if (c4 === -1) { break; } out += String.fromCharCode((c3 & 0x03) << 6 | c4); } return out; }; /** * Echoes something to casper console. * * @param String message * @return */ this.echo = function echo(message) { console.log("[casper.echo] " + message); }; /** * Checks if a given DOM element is visible in remote page. * * @param Object element DOM element * @return Boolean */ this.elementVisible = function elementVisible(elem) { var style; try { style = window.getComputedStyle(elem, null); } catch (e) { return false; } if(style.visibility === 'hidden' || style.display === 'none') return false; var cr = elem.getBoundingClientRect(); return cr.width > 0 && cr.height > 0; }; /** * Base64 encodes a string, even binary ones. Succeeds where * window.btoa() fails. * * @param String str The string content to encode * @return string */ this.encode = function encode(str) { /*eslint max-statements:0 */ var out = "", i = 0, len = str.length, c1, c2, c3; while (i < len) { c1 = str.charCodeAt(i++) & 0xff; if (i === len) { out += BASE64_ENCODE_CHARS.charAt(c1 >> 2); out += BASE64_ENCODE_CHARS.charAt((c1 & 0x3) << 4); out += "=="; break; } c2 = str.charCodeAt(i++); if (i === len) { 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++); 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 | (c3 & 0xC0) >> 6); out += BASE64_ENCODE_CHARS.charAt(c3 & 0x3F); } return out; }; /** * Checks if a given DOM element exists in remote page. * * @param String selector CSS3 selector * @return Boolean */ this.exists = function exists(selector) { try { return this.findAll(selector).length > 0; } catch (e) { return false; } }; /** * Fetches innerText within the element(s) matching a given CSS3 * selector. * * @param String selector A CSS3 selector * @return String */ this.fetchText = function fetchText(selector) { var text = '', elements = this.findAll(selector); if (elements && elements.length) { Array.prototype.forEach.call(elements, function _forEach(element) { text += element.textContent || element.innerText || element.value || ''; }); } return text; }; /** * Fills a form with provided field values, and optionally submits it. * * @param HTMLElement|String form A form element, or a CSS3 selector to a form element * @param Object vals Field values * @param String findType Element finder type (css, names, xpath, labels) * @return Object An object containing setting result for each field, including file uploads */ this.fill = function fill(form, vals, findType) { findType = findType || "names"; /*eslint complexity:0*/ var out = { errors: [], fields: [], files: [] }; if (!(form instanceof HTMLElement) || typeof form === "string") { this.log("attempting to fetch form element from selector: '" + form + "'", "info"); try { form = this.findOne(form); } catch (e) { if (e.name === "SYNTAX_ERR") { out.errors.push("invalid form selector provided: '" + form + "'"); return out; } } } if (!form) { out.errors.push("form not found"); return out; } for (var fieldSelector in vals) { if (!vals.hasOwnProperty(fieldSelector)) { continue; } try { out.fields[fieldSelector] = this.setFieldValue(this.makeSelector(fieldSelector, findType), vals[fieldSelector], form); } catch (err) { switch (err.name) { case "FieldNotFound": out.errors.push('Unable to find field element in form: ' + err.toString()); break; case "FileUploadError": out.files.push({ type: findType, selector: findType === "labels" ? '#' + err.id : fieldSelector, path: err.path }); break; default: out.errors.push(err.toString()); } } } return out; }; /** * Finds all DOM elements matching by the provided selector. * * @param String | Object selector CSS3 selector (String only) or XPath object * @param HTMLElement|null scope Element to search child elements within * @return Array|undefined */ this.findAll = function findAll(selector, scope) { scope = scope instanceof HTMLElement ? scope : scope && this.findOne(scope) || this.options.scope; try { var pSelector = this.processSelector(selector); if (pSelector.type === 'xpath') { return this.getElementsByXPath(pSelector.path, scope); } else { return Array.prototype.slice.call(scope.querySelectorAll(pSelector.path)); } } catch (e) { this.log('findAll(): invalid selector provided "' + selector + '":' + e, "error"); } }; /** * Finds a DOM element by the provided selector. * * @param String | Object selector CSS3 selector (String only) or XPath object * @param HTMLElement|null scope Element to search child elements within * @return HTMLElement|undefined */ this.findOne = function findOne(selector, scope) { scope = scope instanceof HTMLElement ? scope : scope && this.findOne(scope) || this.options.scope; try { var pSelector = this.processSelector(selector); if (pSelector.type === 'xpath') { return this.getElementByXPath(pSelector.path, scope); } else { return scope.querySelector(pSelector.path); } } catch (e) { this.log('findOne(): invalid selector provided "' + selector + '":' + e, "error"); } }; /** * Force target on
and tag. * * @param String selector CSS3 selector * @param String A HTML target '_blank','_self','_parent','_top','framename' * @return Boolean */ this.forceTarget = function forceTarget(selector, target) { var elem = this.findOne(selector); while (!!elem && elem.tagName !== 'A' && elem.tagName !== 'FORM' && elem.tagName !== 'BODY'){ elem = elem.parentNode; } if (elem === 'A' || elem === 'FORM') { elem.setAttribute('target', target); return true; } return false; }; /** * 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 */ this.getBase64 = function getBase64(url, method, data) { return this.encode(this.getBinary(url, method, data)); }; /** * Retrieves string contents from a binary file behind an url. Silently * fails but log errors. * * @param String url Url. * @param String method HTTP method. * @param Object data Request parameters. * @return String */ this.getBinary = function getBinary(url, method, data) { try { return this.sendAJAX(url, method, data, false, { overrideMimeType: "text/plain; charset=x-user-defined" }); } 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 ""; } }; /** * Convert a Xpath or a css Selector into absolute css3 selector * * @param String|Object selector CSS3/XPath selector * @param HTMLElement|null scope Element to search child elements within * @param String limit Parent limit NodeName * @return String */ this.getCssSelector = function getCssSelector(selector, scope, limit) { scope = scope || this.options.scope; limit = limit || 'BODY'; var elem = (selector instanceof Node) ? selector : this.findOne(selector, scope); if (!!elem && elem.nodeName !== "#document") { var str = ""; while (elem.nodeName.toUpperCase() !== limit.toUpperCase()) { str = "> " + elem.nodeName + ':nth-child(' + ([].indexOf.call(elem.parentNode.children, elem) + 1) + ') ' + str; elem = elem.parentNode; } return str.substring(2); } return ""; }; /** * 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) ); }; /** * Retrieves total document width. * http://james.padolsey.com/javascript/get-document-width-cross-browser/ * * @return {Number} */ this.getDocumentWidth = function getDocumentWidth() { return Math.max( Math.max(document.body.scrollWidth, document.documentElement.scrollWidth), Math.max(document.body.offsetWidth, document.documentElement.offsetWidth), Math.max(document.body.clientWidth, document.documentElement.clientWidth) ); }; /** * Retrieves bounding rect coordinates of the HTML element matching the * provided CSS3 selector in the following form: * * {top: y, left: x, width: w, height:, h} * * @param String selector * @return Object or null */ this.getElementBounds = function getElementBounds(selector) { try { var clipRect = this.findOne(selector).getBoundingClientRect(); 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"); } }; /** * 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); 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"); } }; /** * 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, text: element.textContent || element.innerText, x: bounds.left, y: bounds.top, width: bounds.width, height: bounds.height, visible: this.visible(selector) }; }; /** * 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); var eleVisible = this.elementVisible; return [].map.call(this.findAll(selector), function(element, index) { 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, text: element.textContent || element.innerText, x: bounds[index].left, y: bounds[index].top, width: bounds[index].width, height: bounds[index].height, visible: eleVisible(element) }; }); }; /** * Retrieves a single DOM element matching a given XPath expression. * * @param String expression The XPath expression * @param HTMLElement|null scope Element to search child elements within * @return HTMLElement or null */ this.getElementByXPath = function getElementByXPath(expression, scope) { scope = scope || this.options.scope; var a = document.evaluate(expression, scope, this.xpathNamespaceResolver, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); if (a.snapshotLength > 0) { return a.snapshotItem(0); } }; /** * Retrieves all DOM elements matching a given XPath expression. * * @param String expression The XPath expression * @param HTMLElement|null scope Element to search child elements within * @return Array */ this.getElementsByXPath = function getElementsByXPath(expression, scope) { scope = scope || this.options.scope; var nodes = []; var a = document.evaluate(expression, scope, this.xpathNamespaceResolver, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); for (var i = 0; i < a.snapshotLength; i++) { nodes.push(a.snapshotItem(i)); } return nodes; }; /** * Build the xpath namespace resolver to evaluate on document * * @param String prefix The namespace prefix * @return the resolve namespace or null */ this.xpathNamespaceResolver = function xpathNamespaceResolver(prefix) { return XPATH_NAMESPACE[prefix] || null; }; /** * Retrieves the value of an element * * @param String inputName The for input name attr value * @param Object options Object with formSelector, optional * @return Mixed */ this.getFieldValue = function getFieldValue(selector, scope) { var self = this; var fields = this.findAll(selector, scope); var type; // for Backward Compatibility if (!(fields instanceof NodeList || fields instanceof Array)) { this.log("attempting to fetch field element from selector: '" + selector + "'", "info"); fields = this.findAll('[name="' + selector + '"]'); } if (fields && fields.length > 1) { type = fields[0].hasAttribute('type') ? fields[0].getAttribute('type') : "other"; fields = [].filter.call(fields, function(elm){ if (elm.nodeName.toLowerCase() === 'input' && ['checkbox', 'radio'].indexOf(elm.getAttribute('type')) !== -1) { return elm.checked; } return true; }); } if (fields.length === 0 ) { return type !== "radio" ? [] : undefined; } if (fields.length > 1 ) { return [].map.call(fields, function(elm) { var ret = self.getField(elm); return ret && type === 'checkbox' ? elm.value : ret; }); } return this.getField(fields[0]); }; /** * Retrieves the value of a form field. * * @param HTMLElement An html element * @return Mixed */ this.getField = function getField(field) { var nodeName, type; if (!(field instanceof HTMLElement)) { var error = new Error('getFieldValue: Invalid field ; only HTMLElement is supported'); error.name = 'FieldNotFound'; throw error; } nodeName = field.nodeName.toLowerCase(); type = field.hasAttribute('type') ? field.getAttribute('type').toLowerCase() : 'text'; if (nodeName === "select" && field.multiple) { return [].filter.call(field.options, function(option){ return !!option.selected; }).map(function(option){ return option.value || option.text; }); } if (type === 'radio') { return field.checked ? field.value : null; } if (type === 'checkbox') { return field.checked; } return field.value || ''; }; /** * Retrieves a given form all of its field values. * * @param HTMLElement|String form A form element, or a CSS3 selector to a form element * @return Object */ this.getFormValues = function getFormValues(form) { var self = this; var values = {}, checked = {}; if (!(form instanceof HTMLElement) || typeof form === "string") { this.log("attempting to fetch form element from selector: '" + form + "'", "info"); try { form = this.findOne(form); } catch (e) { this.log("invalid form selector provided: '" + form + "'"); return {}; } } [].forEach.call(form.elements, function(elm) { var name = elm.getAttribute('name'); var value = self.getField(elm); var multi = !!value && elm.hasAttribute('type') && elm.type === 'checkbox' ? elm.value : value; if (!!name && value !== null && !(elm.type === 'checkbox' && value === false)) { if (typeof values[name] === "undefined") { values[name] = value; checked[name] = multi; } else { if (!Array.isArray(values[name])) { values[name] = [checked[name]]; } values[name].push(multi); } } }); return values; }; /** * Logs a message. Will format the message a way CasperJS will be able * to log phantomjs side. * * @param String message The message to log * @param String level The log level */ this.log = function log(message, level) { console.log("[casper:" + (level || "debug") + "] " + message); }; /** * Makes selector by defined type XPath, Name or Label. Function has same result as selectXPath in Casper module for * XPath type - it makes XPath object. * Function also accepts name attribut of the form filed or can select element by its label text. * * @param String selector Selector of defined type * @param String|null type Type of selector, it can have these values: * css - CSS3 selector - selector is returned trasparently * xpath - XPath selector - return XPath object * name|names - select input of specific name, internally covert to CSS3 selector * label|labels - select input of specific label, internally covert to XPath selector. As selector is label's text used. * @return String|Object */ this.makeSelector = function makeSelector(selector, type){ type = type || 'xpath'; // default type var ret; if (typeof selector === "object") { // selector object (CSS3 | XPath) could by passed selector = selector.path; } switch (type) { case 'css': // do nothing ret = selector; break; case 'name': // convert to css case 'names': ret = '[name="' + selector + '"]'; break; case 'label': // covert to xpath object case 'labels': ret = {type: 'xpath', path: '//*[@id=string(//label[text()="' + selector + '"]/@for)]'}; break; case 'xpath': // covert to xpath object ret = {type: 'xpath', path: selector}; break; default: throw new Error("Unsupported selector type: " + type); } return ret; }; /** * 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 * @param {Number} x X position * @param {Number} y Y position * @return Boolean */ this.mouseEvent = function mouseEvent(type, selector, x, y) { var elem = this.findOne(selector); if (!elem) { this.log("mouseEvent(): Couldn't find any element matching '" + selector + "' selector", "error"); return false; } var convertNumberToIntAndPercentToFloat = function (a, def){ return !!a && !isNaN(a) && parseInt(a, 10) || !!a && !isNaN(parseFloat(a)) && parseFloat(a) >= 0 && parseFloat(a) <= 100 && parseFloat(a) / 100 || def; }; try { var evt = document.createEvent("MouseEvents"); var px = convertNumberToIntAndPercentToFloat(x, 0.5), py = convertNumberToIntAndPercentToFloat(y, 0.5); try { var bounds = elem.getBoundingClientRect(); px = Math.floor(bounds.width * (px - (px ^ 0)).toFixed(10)) + (px ^ 0) + bounds.left; py = Math.floor(bounds.height * (py - (py ^ 0)).toFixed(10)) + (py ^ 0) + bounds.top; } catch (e) { px = 1; py = 1; } evt.initMouseEvent(type, true, true, window, 1, 1, 1, px, py, false, false, false, false, type !== "contextmenu" ? 0 : 2, elem); // 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; } }; /** * 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: * } * * @param String|Object selector The selector string or object * * @return an object containing 'type' and 'path' keys */ this.processSelector = function processSelector(selector) { var selectorObject = { toString: function toString() { return this.type + ' selector: ' + this.path; } }; 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); }; /** * 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)); } }; /** * 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()); }; /** * Performs an AJAX request. * * @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 like some undocumented * Request Headers. * WARNING: an invalid header here may make the request fail silently. * @return String Response text. */ this.sendAJAX = function sendAJAX(url, method, data, async, settings) { var xhr = new XMLHttpRequest(), dataString = "", dataList = []; var CONTENT_TYPE_HEADER = "Content-Type"; method = method && method.toUpperCase() || "GET"; var contentTypeValue = settings && settings.contentType || "application/x-www-form-urlencoded"; xhr.open(method, url, !!async); this.log("sendAJAX(): Using HTTP method: '" + method + "'", "debug"); if (settings && settings.overrideMimeType) { xhr.overrideMimeType(settings.overrideMimeType); } if (settings && settings.headers) { for (var header in settings.headers) { if (header === CONTENT_TYPE_HEADER) { // this way Content-Type is correctly overriden, // otherwise it is concatenated by xhr.setRequestHeader() // see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader // If the header was already set, the value will be augmented. contentTypeValue = settings.headers[header]; } else { xhr.setRequestHeader(header, settings.headers[header]); } } } if (method === "POST") { if (typeof data === "object") { for (var k in data) { if (data.hasOwnProperty(k)) { dataList.push(form_urlencoded(k) + "=" + form_urlencoded(data[k].toString())); } } dataString = dataList.join('&'); this.log("sendAJAX(): Using request data: '" + dataString + "'", "debug"); } else if (typeof data === "string") { dataString = data; } xhr.setRequestHeader(CONTENT_TYPE_HEADER, contentTypeValue); } xhr.send(method === "POST" ? dataString : null); return xhr.responseText; }; /** * Sets a value to form element by CSS3 or XPath selector. * * With makeSelector() helper can by easily used with name or label selector * @exemple setFieldValue(this.makeSelector('email', 'name'), 'value') * * @param String|Object CSS3|XPath selector * @param Mixed Input value * @param HTMLElement|String|null scope Element to search child elements within * @return bool */ this.setFieldValue = function setFieldValue(selector, value, scope) { var self = this; var fields = this.findAll(selector, scope); var values = value; if (!Array.isArray(value)) { values = [value]; } if (fields && fields.length > 1) { fields = [].filter.call(fields, function(elm){ if (elm.nodeName.toLowerCase() === 'input' && ['checkbox', 'radio'].indexOf(elm.getAttribute('type')) !== -1) { return values.indexOf(elm.getAttribute('value')) !== -1; } return true; }); [].forEach.call(fields, function(elm) { self.setField(elm, value); }); } else { this.setField(fields[0], value); } return true; }; /** * Sets a field value. Fails silently, but log * error messages. * * @param HTMLElement field One element defining a field * @param mixed value The field value to set */ this.setField = function setField(field, value) { /*eslint complexity:0*/ var logValue, out, filter; value = logValue = value || ""; if (!(field instanceof HTMLElement)) { var error = new Error('setField: Invalid field ; only HTMLElement is supported'); error.name = 'FieldNotFound'; throw error; } if (this.options && this.options.safeLogs && field.getAttribute('type') === "password") { // obfuscate password value logValue = new Array(('' + value).length + 1).join("*"); } this.log('Set "' + field.getAttribute('name') + '" field value to ' + logValue, "debug"); try { field.focus(); } catch (e) { this.log("Unable to focus() input field " + field.getAttribute('name') + ": " + e, "warning"); } if (field.getAttribute('contenteditable')) { field.textContent = value; } else { filter = String(field.getAttribute('type') ? field.getAttribute('type') : field.nodeName).toLowerCase(); switch (filter) { case "checkbox": case "radio": field.checked = value ? true : false; break; case "file": throw { name: "FileUploadError", message: "File field must be filled using page.uploadFile", path: value, id: field.id || null }; break; case "select": if (field.multiple) { [].forEach.call(field.options, function(option) { option.selected = value.indexOf(option.value) !== -1; }); // If the values can't be found, try search options text if (field.value === "") { [].forEach.call(field.options, function(option) { option.selected = value.indexOf(option.text) !== -1; }); } } else { // PhantomJS 1.x.x can't handle setting value to '' if (value === "") { field.selectedIndex = -1; } else { field.value = value; } // If the value can't be found, try search options text if (field.value !== value) { [].some.call(field.options, function(option) { option.selected = value === option.text; return value === option.text; }); } } break; default: field.value = value; } } ['change', 'input'].forEach(function(name) { var event = document.createEvent("HTMLEvents"); event.initEvent(name, true, true); field.dispatchEvent(event); }); // blur the field try { field.blur(); } catch (err) { this.log("Unable to blur() input field " + field.getAttribute('name') + ": " + err, "warning"); } return out; }; /** * set the default scope selector * * @param String selector CSS3 selector * @return String */ this.setScope = function setScope(selector) { var scope = !(this.options.scope instanceof HTMLElement) ? this.getCssSelector(this.options.scope) : ""; this.options.scope = (selector !== "") ? this.findOne(selector) : document; return scope; }; /** * Checks if any element matching a given selector is visible in remote page. * * @param String selector CSS3 selector * @return Boolean */ this.visible = function visible(selector) { return [].some.call(this.findAll(selector), this.elementVisible); }; /** * Checks if all elements matching a given selector are visible in remote page. * * @param String selector CSS3 selector * @return Boolean */ this.allVisible = function allVisible(selector) { return [].every.call(this.findAll(selector), this.elementVisible); }; }; })(typeof exports === "object" && !(exports instanceof Element) ? exports : window);