3 PoltergeistAgent = (function () {
4 function PoltergeistAgent() {
10 * Executes an external call done from the web page class
15 PoltergeistAgent.prototype.externalCall = function (name, args) {
19 value: this[name].apply(this, args)
25 message: error.toString(),
33 * Object stringifycation
37 PoltergeistAgent.stringify = function (object) {
40 return JSON.stringify(object, function (key, value) {
41 if (Array.isArray(this[key])) {
49 if (error instanceof TypeError) {
50 return '"(cyclic structure)"';
58 * Name speaks for itself
61 PoltergeistAgent.prototype.currentUrl = function () {
62 return encodeURI(decodeURI(window.location.href));
66 * Given a method of selection (xpath or css), a selector and a possible element to search
67 * tries to find the elements that matches such selection
73 PoltergeistAgent.prototype.find = function (method, selector, within) {
74 var elementForXpath, error, i, results, xpath, _i, _len, _results;
79 if (method === "xpath") {
80 xpath = document.evaluate(selector, within, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
81 results = (function () {
82 var _i, _ref, _results;
84 for (i = _i = 0, _ref = xpath.snapshotLength; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) {
85 _results.push(xpath.snapshotItem(i));
90 results = within.querySelectorAll(selector);
93 for (_i = 0, _len = results.length; _i < _len; _i++) {
94 elementForXpath = results[_i];
95 _results.push(this.register(elementForXpath));
100 if (error.code === DOMException.SYNTAX_ERR || error.code === 51) {
101 throw new PoltergeistAgent.InvalidSelector;
109 * Register the element in the agent
113 PoltergeistAgent.prototype.register = function (element) {
114 this.elements.push(element);
115 return this.elements.length - 1;
119 * Gets the size of the document
120 * @return {{height: number, width: number}}
122 PoltergeistAgent.prototype.documentSize = function () {
124 height: document.documentElement.scrollHeight || document.documentElement.clientHeight,
125 width: document.documentElement.scrollWidth || document.documentElement.clientWidth
130 * Gets a Node by a given id
132 * @return {PoltergeistAgent.Node}
134 PoltergeistAgent.prototype.get = function (id) {
135 if (typeof this.nodes[id] == "undefined" || this.nodes[id] === null) {
136 //Let's try now the elements approach
137 if (typeof this.elements[id] == "undefined" || this.elements[id] === null) {
138 throw new PoltergeistAgent.ObsoleteNode;
140 return new PoltergeistAgent.Node(this, this.elements[id]);
143 return this.nodes[id];
147 * Calls a Node agent function from the Node caller via delegates
153 PoltergeistAgent.prototype.nodeCall = function (id, name, args) {
157 if (node.isObsolete()) {
158 throw new PoltergeistAgent.ObsoleteNode;
160 //TODO: add some error control here, we might not be able to call name function
161 return node[name].apply(node, args);
164 PoltergeistAgent.prototype.beforeUpload = function (id) {
165 return this.get(id).setAttribute('_poltergeist_selected', '');
168 PoltergeistAgent.prototype.afterUpload = function (id) {
169 return this.get(id).removeAttribute('_poltergeist_selected');
172 PoltergeistAgent.prototype.clearLocalStorage = function () {
173 //TODO: WTF where is variable...
174 return localStorage.clear();
177 return PoltergeistAgent;
181 PoltergeistAgent.ObsoleteNode = (function () {
182 function ObsoleteNode() {
185 ObsoleteNode.prototype.toString = function () {
186 return "PoltergeistAgent.ObsoleteNode";
193 PoltergeistAgent.InvalidSelector = (function () {
194 function InvalidSelector() {
197 InvalidSelector.prototype.toString = function () {
198 return "PoltergeistAgent.InvalidSelector";
201 return InvalidSelector;
205 PoltergeistAgent.Node = (function () {
208 FOCUS: ['blur', 'focus', 'focusin', 'focusout'],
209 MOUSE: ['click', 'dblclick', 'mousedown', 'mouseenter', 'mouseleave', 'mousemove', 'mouseover', 'mouseout', 'mouseup', 'contextmenu'],
213 function Node(agent, element) {
215 this.element = element;
219 * Give me the node id of the parent of this node
222 Node.prototype.parentId = function () {
223 return this.agent.register(this.element.parentNode);
227 * Returns all the node parents ids up to first child of the dom
230 Node.prototype.parentIds = function () {
233 parent = this.element.parentNode;
234 while (parent !== document) {
235 ids.push(this.agent.register(parent));
236 parent = parent.parentNode;
242 * Finds and returns the node ids that matches the selector within this node
247 Node.prototype.find = function (method, selector) {
248 return this.agent.find(method, selector, this.element);
252 * Checks whether the node is obsolete or not
255 Node.prototype.isObsolete = function () {
258 obsolete = function (element) {
259 if (element.parentNode != null) {
260 if (element.parentNode === document) {
263 return obsolete(element.parentNode);
270 return obsolete(this.element);
273 Node.prototype.changed = function () {
275 event = document.createEvent('HTMLEvents');
276 event.initEvent('change', true, false);
277 return this.element.dispatchEvent(event);
280 Node.prototype.input = function () {
282 event = document.createEvent('HTMLEvents');
283 event.initEvent('input', true, false);
284 return this.element.dispatchEvent(event);
287 Node.prototype.keyupdowned = function (eventName, keyCode) {
289 event = document.createEvent('UIEvents');
290 event.initEvent(eventName, true, true);
291 event.keyCode = keyCode;
292 event.which = keyCode;
294 return this.element.dispatchEvent(event);
297 Node.prototype.keypressed = function (altKey, ctrlKey, shiftKey, metaKey, keyCode, charCode) {
299 event = document.createEvent('UIEvents');
300 event.initEvent('keypress', true, true);
301 event.window = this.agent.window;
302 event.altKey = altKey;
303 event.ctrlKey = ctrlKey;
304 event.shiftKey = shiftKey;
305 event.metaKey = metaKey;
306 event.keyCode = keyCode;
307 event.charCode = charCode;
308 event.which = keyCode;
309 return this.element.dispatchEvent(event);
313 * Tells if the node is inside the body of the document and not somewhere else
316 Node.prototype.insideBody = function () {
317 return this.element === document.body || document.evaluate('ancestor::body', this.element, null, XPathResult.BOOLEAN_TYPE, null).booleanValue;
321 * Returns all text visible or not of the node
324 Node.prototype.allText = function () {
325 return this.element.textContent;
329 * Returns the inner html our outer
332 Node.prototype.allHTML = function (type) {
333 var returnType = type || 'inner';
335 if (returnType === "inner") {
336 return this.element.innerHTML;
339 if (returnType === "outer") {
340 if (this.element.outerHTML) {
341 return this.element.outerHTML;
344 var wrapper = document.createElement('div');
345 wrapper.appendChild(this.element.cloneNode(true));
346 return wrapper.innerHTML;
353 * If the element is visible then we return the text
356 Node.prototype.visibleText = function () {
357 if (!this.isVisible(null)) {
361 if (this.element.nodeName === "TEXTAREA") {
362 return this.element.textContent;
365 return this.element.innerText;
369 * Deletes the actual text being represented by a selection object from the node's element DOM.
372 Node.prototype.deleteText = function () {
374 range = document.createRange();
375 range.selectNodeContents(this.element);
376 window.getSelection().removeAllRanges();
377 window.getSelection().addRange(range);
378 return window.getSelection().deleteFromDocument();
382 * Returns all the attributes {name:value} in the element
385 Node.prototype.getAttributes = function () {
386 var attributes, i, elementAttributes;
388 elementAttributes = this.element.attributes;
390 for (i = 0; i < elementAttributes.length; i++) {
391 attributes[elementAttributes[i].name] = elementAttributes[i].value.replace("\n", "\\n");
398 * Name speaks for it self, returns the value of a given attribute by name
402 Node.prototype.getAttribute = function (name) {
403 if (name === 'checked' || name === 'selected' || name === 'multiple') {
404 return this.element[name];
406 return this.element.getAttribute(name);
410 * Scrolls the current element into the visible area of the browser window
413 Node.prototype.scrollIntoView = function () {
414 return this.element.scrollIntoViewIfNeeded();
418 * Returns the element.value property with special treatment if the element is a select
421 Node.prototype.value = function () {
422 var options, i, values;
424 if (this.element.tagName.toLowerCase() === 'select' && this.element.multiple) {
426 options = this.element.children;
427 for (i = 0; i < options.length; i++) {
428 if (options[i].selected) {
429 values.push(options[i].value);
435 return this.element.value;
439 * Sets a given value in the element value property by simulation key interaction
443 Node.prototype.set = function (value) {
444 var char, keyCode, i, len;
446 if (this.element.readOnly) {
450 //respect the maxLength property if present
451 if (this.element.maxLength >= 0) {
452 value = value.substr(0, this.element.maxLength);
455 this.element.value = '';
456 this.trigger('focus');
458 if (this.element.type === 'number') {
459 this.element.value = value;
461 for (i = 0, len = value.length; i < len; i++) {
463 keyCode = this.characterToKeyCode(char);
464 this.keyupdowned('keydown', keyCode);
465 this.element.value += char;
466 this.keypressed(false, false, false, false, char.charCodeAt(0), char.charCodeAt(0));
467 this.keyupdowned('keyup', keyCode);
474 return this.trigger('blur');
478 * Is the node multiple
481 Node.prototype.isMultiple = function () {
482 return this.element.multiple;
486 * Sets the value of an attribute given by name
491 Node.prototype.setAttribute = function (name, value) {
492 if (value === null) {
493 return this.removeAttribute(name);
496 this.element.setAttribute(name, value);
501 * Removes and attribute by name
505 Node.prototype.removeAttribute = function (name) {
506 this.element.removeAttribute(name);
511 * Selects the current node
515 Node.prototype.select = function (value) {
516 if (value === false && !this.element.parentNode.multiple) {
520 this.element.selected = value;
526 * Selects the radio button that has the defined value
530 Node.prototype.selectRadioValue = function (value) {
531 if (this.element.value == value) {
532 this.element.checked = true;
533 this.trigger('focus');
534 this.trigger('click');
539 var formElements = this.element.form.elements;
540 var name = this.element.getAttribute('name');
543 var deselectAllRadios = function (elements, radioName) {
544 var inputRadioElement;
546 for (i = 0; i < elements.length; i++) {
547 inputRadioElement = elements[i];
548 if (inputRadioElement.tagName.toLowerCase() == 'input' && inputRadioElement.type.toLowerCase() == 'radio' && inputRadioElement.name == radioName) {
549 inputRadioElement.checked = false;
554 var radioChange = function (radioElement) {
556 radioEvent = document.createEvent('HTMLEvents');
557 radioEvent.initEvent('change', true, false);
558 return radioElement.dispatchEvent(radioEvent);
561 var radioClickEvent = function (radioElement, name) {
563 radioEvent = document.createEvent('MouseEvent');
564 radioEvent.initMouseEvent(name, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
565 return radioElement.dispatchEvent(radioEvent);
569 throw new Poltergeist.BrowserError('The radio button does not have the value "' + value + '"');
572 for (i = 0; i < formElements.length; i++) {
573 element = formElements[i];
574 if (element.tagName.toLowerCase() == 'input' && element.type.toLowerCase() == 'radio' && element.name === name) {
575 if (value === element.value) {
576 deselectAllRadios(formElements, name);
577 element.checked = true;
578 radioClickEvent(element, 'click');
579 radioChange(element);
585 throw new Poltergeist.BrowserError('The radio group "' + name + '" does not have an option "' + value + '"');
589 * Checks or uncheck a radio option
593 Node.prototype.checked = function (value) {
594 //TODO: add error control for the checked stuff
595 this.element.checked = value;
600 * Returns the element tag name as is, no transformations done
603 Node.prototype.tagName = function () {
604 return this.element.tagName;
608 * Checks if the element is visible either by itself of because the parents are visible
612 Node.prototype.isVisible = function (element) {
613 var nodeElement = element || this.element;
615 if (window.getComputedStyle(nodeElement).display === 'none') {
617 } else if (nodeElement.parentElement) {
618 return this.isVisible(nodeElement.parentElement);
625 * Is the node disabled for operations with it?
628 Node.prototype.isDisabled = function () {
629 return this.element.disabled || this.element.tagName === 'OPTION' && this.element.parentNode.disabled;
633 * Does the node contains the selections
636 Node.prototype.containsSelection = function () {
639 selectedNode = document.getSelection().focusNode;
643 //this magic number is NODE.TEXT_NODE
644 if (selectedNode.nodeType === 3) {
645 selectedNode = selectedNode.parentNode;
648 return this.element.contains(selectedNode);
652 * Returns the offset of the node in relation to the current frame
653 * @return {{top: number, left: number}}
655 Node.prototype.frameOffset = function () {
656 var offset, rect, style, win;
662 while (win.frameElement) {
663 rect = win.frameElement.getClientRects()[0];
664 style = win.getComputedStyle(win.frameElement);
666 offset.top += rect.top + parseInt(style.getPropertyValue("padding-top"), 10);
667 offset.left += rect.left + parseInt(style.getPropertyValue("padding-left"), 10);
673 * Returns the object position in relation to the window
674 * @return {{top: *, right: *, left: *, bottom: *, width: *, height: *}}
676 Node.prototype.position = function () {
677 var frameOffset, pos, rect;
679 rect = this.element.getClientRects()[0];
681 throw new PoltergeistAgent.ObsoleteNode;
684 frameOffset = this.frameOffset();
686 top: rect.top + frameOffset.top,
687 right: rect.right + frameOffset.left,
688 left: rect.left + frameOffset.left,
689 bottom: rect.bottom + frameOffset.top,
698 * Triggers a DOM event related to the node element
702 Node.prototype.trigger = function (name) {
704 if (Node.EVENTS.MOUSE.indexOf(name) !== -1) {
705 event = document.createEvent('MouseEvent');
706 event.initMouseEvent(name, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
707 } else if (Node.EVENTS.FOCUS.indexOf(name) !== -1) {
708 event = this.obtainEvent(name);
709 } else if (Node.EVENTS.FORM.indexOf(name) !== -1) {
710 event = this.obtainEvent(name);
712 throw "Unknown event";
714 return this.element.dispatchEvent(event);
718 * Creates a generic HTMLEvent to be use in the node element
722 Node.prototype.obtainEvent = function (name) {
724 event = document.createEvent('HTMLEvents');
725 event.initEvent(name, true, true);
730 * Does a check to see if the coordinates given
731 * match the node element or some of the parents chain
736 Node.prototype.mouseEventTest = function (x, y) {
737 var elementForXpath, frameOffset, origEl;
739 frameOffset = this.frameOffset();
740 x -= frameOffset.left;
741 y -= frameOffset.top;
743 elementForXpath = origEl = document.elementFromPoint(x, y);
744 while (elementForXpath) {
745 if (elementForXpath === this.element) {
750 elementForXpath = elementForXpath.parentNode;
756 selector: origEl && this.getSelector(origEl)
761 * Returns the node selector in CSS style (NO xpath)
762 * @param elementForXpath
765 Node.prototype.getSelector = function (elementForXpath) {
766 var className, selector, i, len, classNames;
768 selector = elementForXpath.tagName !== 'HTML' ? this.getSelector(elementForXpath.parentNode) + ' ' : '';
769 selector += elementForXpath.tagName.toLowerCase();
771 if (elementForXpath.id) {
772 selector += "#" + elementForXpath.id;
775 classNames = elementForXpath.classList;
776 for (i = 0, len = classNames.length; i < len; i++) {
777 className = classNames[i];
778 selector += "." + className;
785 * Returns the key code that represents the character
789 Node.prototype.characterToKeyCode = function (character) {
790 var code, specialKeys;
791 code = character.toUpperCase().charCodeAt(0);
827 return specialKeys[code] || code;
831 * Checks if one element is equal to other given by its node id
835 Node.prototype.isDOMEqual = function (other_id) {
836 return this.element === this.agent.get(other_id).element;
840 * The following function allows one to pass an element and an XML document to find a unique string XPath expression leading back to that element.
844 Node.prototype.getXPathForElement = function (element) {
845 var elementForXpath = element || this.element;
849 while (elementForXpath !== document.documentElement) {
851 tempitem2 = elementForXpath;
853 if (tempitem2.nodeType === 1 && tempitem2.nodeName === elementForXpath.nodeName) { // If it is ELEMENT_NODE of the same name
856 tempitem2 = tempitem2.previousSibling;
859 xpath = "*[name()='" + elementForXpath.nodeName + "' and namespace-uri()='" + (elementForXpath.namespaceURI === null ? '' : elementForXpath.namespaceURI) + "'][" + pos + ']' + '/' + xpath;
861 elementForXpath = elementForXpath.parentNode;
864 xpath = '/*' + "[name()='" + document.documentElement.nodeName + "' and namespace-uri()='" + (elementForXpath.namespaceURI === null ? '' : elementForXpath.namespaceURI) + "']" + '/' + xpath;
865 xpath = xpath.replace(/\/$/, '');
870 * Deselect all the options for this element
872 Node.prototype.deselectAllOptions = function () {
873 //TODO: error control when the node is not a select node
874 var i, l = this.element.options.length;
875 for (i = 0; i < l; i++) {
876 this.element.options[i].selected = false;
884 window.__poltergeist = new PoltergeistAgent;
886 document.addEventListener('DOMContentLoaded', function () {
887 return console.log('__DOMContentLoaded');
890 window.confirm = function (message) {
894 window.prompt = function (message, _default) {
895 return _default || null;