diff options
Diffstat (limited to 'services/sync/tps/extensions/mozmill/resource/driver')
5 files changed, 3184 insertions, 0 deletions
diff --git a/services/sync/tps/extensions/mozmill/resource/driver/controller.js b/services/sync/tps/extensions/mozmill/resource/driver/controller.js new file mode 100644 index 000000000..a378ce51f --- /dev/null +++ b/services/sync/tps/extensions/mozmill/resource/driver/controller.js @@ -0,0 +1,1141 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +var EXPORTED_SYMBOLS = ["MozMillController", "globalEventRegistry", + "sleep", "windowMap"]; + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; + +var EventUtils = {}; Cu.import('resource://mozmill/stdlib/EventUtils.js', EventUtils); + +var assertions = {}; Cu.import('resource://mozmill/modules/assertions.js', assertions); +var broker = {}; Cu.import('resource://mozmill/driver/msgbroker.js', broker); +var elementslib = {}; Cu.import('resource://mozmill/driver/elementslib.js', elementslib); +var errors = {}; Cu.import('resource://mozmill/modules/errors.js', errors); +var mozelement = {}; Cu.import('resource://mozmill/driver/mozelement.js', mozelement); +var utils = {}; Cu.import('resource://mozmill/stdlib/utils.js', utils); +var windows = {}; Cu.import('resource://mozmill/modules/windows.js', windows); + +// Declare most used utils functions in the controller namespace +var assert = new assertions.Assert(); +var waitFor = assert.waitFor; + +var sleep = utils.sleep; + +// For Mozmill 1.5 backward compatibility +var windowMap = windows.map; + +waitForEvents = function () { +} + +waitForEvents.prototype = { + /** + * Initialize list of events for given node + */ + init: function waitForEvents_init(node, events) { + if (node.getNode != undefined) + node = node.getNode(); + + this.events = events; + this.node = node; + node.firedEvents = {}; + this.registry = {}; + + if (!events) { + return; + } + for (var key in events) { + var e = events[key]; + var listener = function (event) { + this.firedEvents[event.type] = true; + } + + this.registry[e] = listener; + this.registry[e].result = false; + this.node.addEventListener(e, this.registry[e], true); + } + }, + + /** + * Wait until all assigned events have been fired + */ + wait: function waitForEvents_wait(timeout, interval) { + for (var e in this.registry) { + assert.waitFor(function () { + return this.node.firedEvents[e] == true; + }, "waitForEvents.wait(): Event '" + ex + "' has been fired.", timeout, interval); + + this.node.removeEventListener(e, this.registry[e], true); + } + } +} + +/** + * Class to handle menus and context menus + * + * @constructor + * @param {MozMillController} controller + * Mozmill controller of the window under test + * @param {string} menuSelector + * jQuery like selector string of the element + * @param {object} document + * Document to use for finding the menu + * [optional - default: aController.window.document] + */ +var Menu = function (controller, menuSelector, document) { + this._controller = controller; + this._menu = null; + + document = document || controller.window.document; + var node = document.querySelector(menuSelector); + if (node) { + // We don't unwrap nodes automatically yet (Bug 573185) + node = node.wrappedJSObject || node; + this._menu = new mozelement.Elem(node); + } else { + throw new Error("Menu element '" + menuSelector + "' not found."); + } +} + +Menu.prototype = { + + /** + * Open and populate the menu + * + * @param {ElemBase} contextElement + * Element whose context menu has to be opened + * @returns {Menu} The Menu instance + */ + open: function Menu_open(contextElement) { + // We have to open the context menu + var menu = this._menu.getNode(); + if ((menu.localName == "popup" || menu.localName == "menupopup") && + contextElement && contextElement.exists()) { + this._controller.rightClick(contextElement); + assert.waitFor(function () { + return menu.state == "open"; + }, "Context menu has been opened."); + } + + // Run through the entire menu and populate with dynamic entries + this._buildMenu(menu); + + return this; + }, + + /** + * Close the menu + * + * @returns {Menu} The Menu instance + */ + close: function Menu_close() { + var menu = this._menu.getNode(); + + this._controller.keypress(this._menu, "VK_ESCAPE", {}); + assert.waitFor(function () { + return menu.state == "closed"; + }, "Context menu has been closed."); + + return this; + }, + + /** + * Retrieve the specified menu entry + * + * @param {string} itemSelector + * jQuery like selector string of the menu item + * @returns {ElemBase} Menu element + * @throws Error If menu element has not been found + */ + getItem: function Menu_getItem(itemSelector) { + // Run through the entire menu and populate with dynamic entries + this._buildMenu(this._menu.getNode()); + + var node = this._menu.getNode().querySelector(itemSelector); + + if (!node) { + throw new Error("Menu entry '" + itemSelector + "' not found."); + } + + return new mozelement.Elem(node); + }, + + /** + * Click the specified menu entry + * + * @param {string} itemSelector + * jQuery like selector string of the menu item + * + * @returns {Menu} The Menu instance + */ + click: function Menu_click(itemSelector) { + this._controller.click(this.getItem(itemSelector)); + + return this; + }, + + /** + * Synthesize a keypress against the menu + * + * @param {string} key + * Key to press + * @param {object} modifier + * Key modifiers + * @see MozMillController#keypress + * + * @returns {Menu} The Menu instance + */ + keypress: function Menu_keypress(key, modifier) { + this._controller.keypress(this._menu, key, modifier); + + return this; + }, + + /** + * Opens the context menu, click the specified entry and + * make sure that the menu has been closed. + * + * @param {string} itemSelector + * jQuery like selector string of the element + * @param {ElemBase} contextElement + * Element whose context menu has to be opened + * + * @returns {Menu} The Menu instance + */ + select: function Menu_select(itemSelector, contextElement) { + this.open(contextElement); + this.click(itemSelector); + this.close(); + }, + + /** + * Recursive function which iterates through all menu elements and + * populates the menus with dynamic menu entries. + * + * @param {node} menu + * Top menu node whose elements have to be populated + */ + _buildMenu: function Menu__buildMenu(menu) { + var items = menu ? menu.childNodes : null; + + Array.forEach(items, function (item) { + // When we have a menu node, fake a click onto it to populate + // the sub menu with dynamic entries + if (item.tagName == "menu") { + var popup = item.querySelector("menupopup"); + + if (popup) { + var popupEvent = this._controller.window.document.createEvent("MouseEvent"); + popupEvent.initMouseEvent("popupshowing", true, true, + this._controller.window, 0, 0, 0, 0, 0, + false, false, false, false, 0, null); + popup.dispatchEvent(popupEvent); + + this._buildMenu(popup); + } + } + }, this); + } +}; + +var MozMillController = function (window) { + this.window = window; + + this.mozmillModule = {}; + Cu.import('resource://mozmill/driver/mozmill.js', this.mozmillModule); + + var self = this; + assert.waitFor(function () { + return window != null && self.isLoaded(); + }, "controller(): Window has been initialized."); + + // Ensure to focus the window which will move it virtually into the foreground + // when focusmanager.testmode is set enabled. + this.window.focus(); + + var windowType = window.document.documentElement.getAttribute('windowtype'); + if (controllerAdditions[windowType] != undefined ) { + this.prototype = new utils.Copy(this.prototype); + controllerAdditions[windowType](this); + this.windowtype = windowType; + } +} + +/** + * Returns the global browser object of the window + * + * @returns {Object} The browser object + */ +MozMillController.prototype.__defineGetter__("browserObject", function () { + return utils.getBrowserObject(this.window); +}); + +// constructs a MozMillElement from the controller's window +MozMillController.prototype.__defineGetter__("rootElement", function () { + if (this._rootElement == undefined) { + let docElement = this.window.document.documentElement; + this._rootElement = new mozelement.MozMillElement("Elem", docElement); + } + + return this._rootElement; +}); + +MozMillController.prototype.sleep = utils.sleep; +MozMillController.prototype.waitFor = assert.waitFor; + +// Open the specified url in the current tab +MozMillController.prototype.open = function (url) { + switch (this.mozmillModule.Application) { + case "Firefox": + // Stop a running page load to not overlap requests + if (this.browserObject.selectedBrowser) { + this.browserObject.selectedBrowser.stop(); + } + + this.browserObject.loadURI(url); + break; + + default: + throw new Error("MozMillController.open not supported."); + } + + broker.pass({'function':'Controller.open()'}); +} + +/** + * Take a screenshot of specified node + * + * @param {Element} node + * The window or DOM element to capture + * @param {String} name + * The name of the screenshot used in reporting and as filename + * @param {Boolean} save + * If true saves the screenshot as 'name.jpg' in tempdir, + * otherwise returns a dataURL + * @param {Element[]} highlights + * A list of DOM elements to highlight by drawing a red rectangle around them + * + * @returns {Object} Object which contains properties like filename, dataURL, + * name and timestamp of the screenshot + */ +MozMillController.prototype.screenshot = function (node, name, save, highlights) { + if (!node) { + throw new Error("node is undefined"); + } + + // Unwrap the node and highlights + if ("getNode" in node) { + node = node.getNode(); + } + + if (highlights) { + for (var i = 0; i < highlights.length; ++i) { + if ("getNode" in highlights[i]) { + highlights[i] = highlights[i].getNode(); + } + } + } + + // If save is false, a dataURL is used + // Include both in the report anyway to avoid confusion and make the report easier to parse + var screenshot = {"filename": undefined, + "dataURL": utils.takeScreenshot(node, highlights), + "name": name, + "timestamp": new Date().toLocaleString()}; + + if (!save) { + return screenshot; + } + + // Save the screenshot to disk + + let {filename, failure} = utils.saveDataURL(screenshot.dataURL, name); + screenshot.filename = filename; + screenshot.failure = failure; + + if (failure) { + broker.log({'function': 'controller.screenshot()', + 'message': 'Error writing to file: ' + screenshot.filename}); + } else { + // Send the screenshot object to python over jsbridge + broker.sendMessage("screenshot", screenshot); + broker.pass({'function': 'controller.screenshot()'}); + } + + return screenshot; +} + +/** + * Checks if the specified window has been loaded + * + * @param {DOMWindow} [aWindow=this.window] Window object to check for loaded state + */ +MozMillController.prototype.isLoaded = function (aWindow) { + var win = aWindow || this.window; + + return windows.map.getValue(utils.getWindowId(win), "loaded") || false; +}; + +MozMillController.prototype.__defineGetter__("waitForEvents", function () { + if (this._waitForEvents == undefined) { + this._waitForEvents = new waitForEvents(); + } + + return this._waitForEvents; +}); + +/** + * Wrapper function to create a new instance of a menu + * @see Menu + */ +MozMillController.prototype.getMenu = function (menuSelector, document) { + return new Menu(this, menuSelector, document); +}; + +MozMillController.prototype.__defineGetter__("mainMenu", function () { + return this.getMenu("menubar"); +}); + +MozMillController.prototype.__defineGetter__("menus", function () { + logDeprecated('controller.menus', 'Use controller.mainMenu instead'); +}); + +MozMillController.prototype.waitForImage = function (aElement, timeout, interval) { + this.waitFor(function () { + return aElement.getNode().complete == true; + }, "timeout exceeded for waitForImage " + aElement.getInfo(), timeout, interval); + + broker.pass({'function':'Controller.waitForImage()'}); +} + +MozMillController.prototype.startUserShutdown = function (timeout, restart, next, resetProfile) { + if (restart && resetProfile) { + throw new Error("You can't have a user-restart and reset the profile; there is a race condition"); + } + + let shutdownObj = { + 'user': true, + 'restart': Boolean(restart), + 'next': next, + 'resetProfile': Boolean(resetProfile), + 'timeout': timeout + }; + + broker.sendMessage('shutdown', shutdownObj); +} + +/** + * Restart the application + * + * @param {string} aNext + * Name of the next test function to run after restart + * @param {boolean} [aFlags=undefined] + * Additional flags how to handle the shutdown or restart. The attributes + * eRestarti386 (0x20) and eRestartx86_64 (0x30) have not been documented yet. + * @see https://developer.mozilla.org/nsIAppStartup#Attributes + */ +MozMillController.prototype.restartApplication = function (aNext, aFlags) { + var flags = Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart; + + if (aFlags) { + flags |= aFlags; + } + + broker.sendMessage('shutdown', {'user': false, + 'restart': true, + 'flags': flags, + 'next': aNext, + 'timeout': 0 }); + + // We have to ensure to stop the test from continuing until the application is + // shutting down. The only way to do that is by throwing an exception. + throw new errors.ApplicationQuitError(); +} + +/** + * Stop the application + * + * @param {boolean} [aResetProfile=false] + * Whether to reset the profile during restart + * @param {boolean} [aFlags=undefined] + * Additional flags how to handle the shutdown or restart. The attributes + * eRestarti386 and eRestartx86_64 have not been documented yet. + * @see https://developer.mozilla.org/nsIAppStartup#Attributes + */ +MozMillController.prototype.stopApplication = function (aResetProfile, aFlags) { + var flags = Ci.nsIAppStartup.eAttemptQuit; + + if (aFlags) { + flags |= aFlags; + } + + broker.sendMessage('shutdown', {'user': false, + 'restart': false, + 'flags': flags, + 'resetProfile': aResetProfile, + 'timeout': 0 }); + + // We have to ensure to stop the test from continuing until the application is + // shutting down. The only way to do that is by throwing an exception. + throw new errors.ApplicationQuitError(); +} + +//Browser navigation functions +MozMillController.prototype.goBack = function () { + this.window.content.history.back(); + broker.pass({'function':'Controller.goBack()'}); + + return true; +} + +MozMillController.prototype.goForward = function () { + this.window.content.history.forward(); + broker.pass({'function':'Controller.goForward()'}); + + return true; +} + +MozMillController.prototype.refresh = function () { + this.window.content.location.reload(true); + broker.pass({'function':'Controller.refresh()'}); + + return true; +} + +function logDeprecated(funcName, message) { + broker.log({'function': funcName + '() - DEPRECATED', + 'message': funcName + '() is deprecated. ' + message}); +} + +function logDeprecatedAssert(funcName) { + logDeprecated('controller.' + funcName, + '. Use the generic `assertion` module instead.'); +} + +MozMillController.prototype.assertText = function (el, text) { + logDeprecatedAssert("assertText"); + + var n = el.getNode(); + + if (n && n.innerHTML == text) { + broker.pass({'function': 'Controller.assertText()'}); + } else { + throw new Error("could not validate element " + el.getInfo() + + " with text "+ text); + } + + return true; +}; + +/** + * Assert that a specified node exists + */ +MozMillController.prototype.assertNode = function (el) { + logDeprecatedAssert("assertNode"); + + //this.window.focus(); + var element = el.getNode(); + if (!element) { + throw new Error("could not find element " + el.getInfo()); + } + + broker.pass({'function': 'Controller.assertNode()'}); + return true; +}; + +/** + * Assert that a specified node doesn't exist + */ +MozMillController.prototype.assertNodeNotExist = function (el) { + logDeprecatedAssert("assertNodeNotExist"); + + try { + var element = el.getNode(); + } catch (e) { + broker.pass({'function': 'Controller.assertNodeNotExist()'}); + } + + if (element) { + throw new Error("Unexpectedly found element " + el.getInfo()); + } else { + broker.pass({'function':'Controller.assertNodeNotExist()'}); + } + + return true; +}; + +/** + * Assert that a form element contains the expected value + */ +MozMillController.prototype.assertValue = function (el, value) { + logDeprecatedAssert("assertValue"); + + var n = el.getNode(); + + if (n && n.value == value) { + broker.pass({'function': 'Controller.assertValue()'}); + } else { + throw new Error("could not validate element " + el.getInfo() + + " with value " + value); + } + + return false; +}; + +/** + * Check if the callback function evaluates to true + */ +MozMillController.prototype.assert = function (callback, message, thisObject) { + logDeprecatedAssert("assert"); + + utils.assert(callback, message, thisObject); + broker.pass({'function': ": controller.assert('" + callback + "')"}); + + return true; +} + +/** + * Assert that a provided value is selected in a select element + */ +MozMillController.prototype.assertSelected = function (el, value) { + logDeprecatedAssert("assertSelected"); + + var n = el.getNode(); + var validator = value; + + if (n && n.options[n.selectedIndex].value == validator) { + broker.pass({'function':'Controller.assertSelected()'}); + } else { + throw new Error("could not assert value for element " + el.getInfo() + + " with value " + value); + } + + return true; +}; + +/** + * Assert that a provided checkbox is checked + */ +MozMillController.prototype.assertChecked = function (el) { + logDeprecatedAssert("assertChecked"); + + var element = el.getNode(); + + if (element && element.checked == true) { + broker.pass({'function':'Controller.assertChecked()'}); + } else { + throw new Error("assert failed for checked element " + el.getInfo()); + } + + return true; +}; + +/** + * Assert that a provided checkbox is not checked + */ +MozMillController.prototype.assertNotChecked = function (el) { + logDeprecatedAssert("assertNotChecked"); + + var element = el.getNode(); + + if (!element) { + throw new Error("Could not find element" + el.getInfo()); + } + + if (!element.hasAttribute("checked") || element.checked != true) { + broker.pass({'function': 'Controller.assertNotChecked()'}); + } else { + throw new Error("assert failed for not checked element " + el.getInfo()); + } + + return true; +}; + +/** + * Assert that an element's javascript property exists or has a particular value + * + * if val is undefined, will return true if the property exists. + * if val is specified, will return true if the property exists and has the correct value + */ +MozMillController.prototype.assertJSProperty = function (el, attrib, val) { + logDeprecatedAssert("assertJSProperty"); + + var element = el.getNode(); + + if (!element){ + throw new Error("could not find element " + el.getInfo()); + } + + var value = element[attrib]; + var res = (value !== undefined && (val === undefined ? true : + String(value) == String(val))); + if (res) { + broker.pass({'function':'Controller.assertJSProperty("' + el.getInfo() + '") : ' + val}); + } else { + throw new Error("Controller.assertJSProperty(" + el.getInfo() + ") : " + + (val === undefined ? "property '" + attrib + + "' doesn't exist" : val + " == " + value)); + } + + return true; +}; + +/** + * Assert that an element's javascript property doesn't exist or doesn't have a particular value + * + * if val is undefined, will return true if the property doesn't exist. + * if val is specified, will return true if the property doesn't exist or doesn't have the specified value + */ +MozMillController.prototype.assertNotJSProperty = function (el, attrib, val) { + logDeprecatedAssert("assertNotJSProperty"); + + var element = el.getNode(); + + if (!element){ + throw new Error("could not find element " + el.getInfo()); + } + + var value = element[attrib]; + var res = (val === undefined ? value === undefined : String(value) != String(val)); + if (res) { + broker.pass({'function':'Controller.assertNotProperty("' + el.getInfo() + '") : ' + val}); + } else { + throw new Error("Controller.assertNotJSProperty(" + el.getInfo() + ") : " + + (val === undefined ? "property '" + attrib + + "' exists" : val + " != " + value)); + } + + return true; +}; + +/** + * Assert that an element's dom property exists or has a particular value + * + * if val is undefined, will return true if the property exists. + * if val is specified, will return true if the property exists and has the correct value + */ +MozMillController.prototype.assertDOMProperty = function (el, attrib, val) { + logDeprecatedAssert("assertDOMProperty"); + + var element = el.getNode(); + + if (!element){ + throw new Error("could not find element " + el.getInfo()); + } + + var value, res = element.hasAttribute(attrib); + if (res && val !== undefined) { + value = element.getAttribute(attrib); + res = (String(value) == String(val)); + } + + if (res) { + broker.pass({'function':'Controller.assertDOMProperty("' + el.getInfo() + '") : ' + val}); + } else { + throw new Error("Controller.assertDOMProperty(" + el.getInfo() + ") : " + + (val === undefined ? "property '" + attrib + + "' doesn't exist" : val + " == " + value)); + } + + return true; +}; + +/** + * Assert that an element's dom property doesn't exist or doesn't have a particular value + * + * if val is undefined, will return true if the property doesn't exist. + * if val is specified, will return true if the property doesn't exist or doesn't have the specified value + */ +MozMillController.prototype.assertNotDOMProperty = function (el, attrib, val) { + logDeprecatedAssert("assertNotDOMProperty"); + + var element = el.getNode(); + + if (!element) { + throw new Error("could not find element " + el.getInfo()); + } + + var value, res = element.hasAttribute(attrib); + if (res && val !== undefined) { + value = element.getAttribute(attrib); + res = (String(value) == String(val)); + } + + if (!res) { + broker.pass({'function':'Controller.assertNotDOMProperty("' + el.getInfo() + '") : ' + val}); + } else { + throw new Error("Controller.assertNotDOMProperty(" + el.getInfo() + ") : " + + (val == undefined ? "property '" + attrib + + "' exists" : val + " == " + value)); + } + + return true; +}; + +/** + * Assert that a specified image has actually loaded. The Safari workaround results + * in additional requests for broken images (in Safari only) but works reliably + */ +MozMillController.prototype.assertImageLoaded = function (el) { + logDeprecatedAssert("assertImageLoaded"); + + var img = el.getNode(); + + if (!img || img.tagName != 'IMG') { + throw new Error('Controller.assertImageLoaded() failed.') + return false; + } + + var comp = img.complete; + var ret = null; // Return value + + // Workaround for Safari -- it only supports the + // complete attrib on script-created images + if (typeof comp == 'undefined') { + test = new Image(); + // If the original image was successfully loaded, + // src for new one should be pulled from cache + test.src = img.src; + comp = test.complete; + } + + // Check the complete attrib. Note the strict + // equality check -- we don't want undefined, null, etc. + // -------------------------- + if (comp === false) { + // False -- Img failed to load in IE/Safari, or is + // still trying to load in FF + ret = false; + } else if (comp === true && img.naturalWidth == 0) { + // True, but image has no size -- image failed to + // load in FF + ret = false; + } else { + // Otherwise all we can do is assume everything's + // hunky-dory + ret = true; + } + + if (ret) { + broker.pass({'function':'Controller.assertImageLoaded'}); + } else { + throw new Error('Controller.assertImageLoaded() failed.') + } + + return true; +}; + +/** + * Drag one element to the top x,y coords of another specified element + */ +MozMillController.prototype.mouseMove = function (doc, start, dest) { + // if one of these elements couldn't be looked up + if (typeof start != 'object'){ + throw new Error("received bad coordinates"); + } + + if (typeof dest != 'object'){ + throw new Error("received bad coordinates"); + } + + var triggerMouseEvent = function (element, clientX, clientY) { + clientX = clientX ? clientX: 0; + clientY = clientY ? clientY: 0; + + // make the mouse understand where it is on the screen + var screenX = element.boxObject.screenX ? element.boxObject.screenX : 0; + var screenY = element.boxObject.screenY ? element.boxObject.screenY : 0; + + var evt = element.ownerDocument.createEvent('MouseEvents'); + if (evt.initMouseEvent) { + evt.initMouseEvent('mousemove', true, true, element.ownerDocument.defaultView, + 1, screenX, screenY, clientX, clientY); + } else { + evt.initEvent('mousemove', true, true); + } + + element.dispatchEvent(evt); + }; + + // Do the initial move to the drag element position + triggerMouseEvent(doc.body, start[0], start[1]); + triggerMouseEvent(doc.body, dest[0], dest[1]); + + broker.pass({'function':'Controller.mouseMove()'}); + return true; +} + +/** + * Drag an element to the specified offset on another element, firing mouse and + * drag events. Adapted from EventUtils.js synthesizeDrop() + * + * @deprecated Use the MozMillElement object + * + * @param {MozElement} aSrc + * Source element to be dragged + * @param {MozElement} aDest + * Destination element over which the drop occurs + * @param {Number} [aOffsetX=element.width/2] + * Relative x offset for dropping on the aDest element + * @param {Number} [aOffsetY=element.height/2] + * Relative y offset for dropping on the aDest element + * @param {DOMWindow} [aSourceWindow=this.element.ownerDocument.defaultView] + * Custom source Window to be used. + * @param {String} [aDropEffect="move"] + * Effect used for the drop event + * @param {Object[]} [aDragData] + * An array holding custom drag data to be used during the drag event + * Format: [{ type: "text/plain", "Text to drag"}, ...] + * + * @returns {String} the captured dropEffect + */ +MozMillController.prototype.dragToElement = function (aSrc, aDest, aOffsetX, + aOffsetY, aSourceWindow, + aDropEffect, aDragData) { + logDeprecated("controller.dragToElement", "Use the MozMillElement object."); + return aSrc.dragToElement(aDest, aOffsetX, aOffsetY, aSourceWindow, null, + aDropEffect, aDragData); +}; + +function Tabs(controller) { + this.controller = controller; +} + +Tabs.prototype.getTab = function (index) { + return this.controller.browserObject.browsers[index].contentDocument; +} + +Tabs.prototype.__defineGetter__("activeTab", function () { + return this.controller.browserObject.selectedBrowser.contentDocument; +}); + +Tabs.prototype.selectTab = function (index) { + // GO in to tab manager and grab the tab by index and call focus. +} + +Tabs.prototype.findWindow = function (doc) { + for (var i = 0; i <= (this.controller.window.frames.length - 1); i++) { + if (this.controller.window.frames[i].document == doc) { + return this.controller.window.frames[i]; + } + } + + throw new Error("Cannot find window for document. Doc title == " + doc.title); +} + +Tabs.prototype.getTabWindow = function (index) { + return this.findWindow(this.getTab(index)); +} + +Tabs.prototype.__defineGetter__("activeTabWindow", function () { + return this.findWindow(this.activeTab); +}); + +Tabs.prototype.__defineGetter__("length", function () { + return this.controller.browserObject.browsers.length; +}); + +Tabs.prototype.__defineGetter__("activeTabIndex", function () { + var browser = this.controller.browserObject; + return browser.tabContainer.selectedIndex; +}); + +Tabs.prototype.selectTabIndex = function (aIndex) { + var browser = this.controller.browserObject; + browser.selectTabAtIndex(aIndex); +} + +function browserAdditions (controller) { + controller.tabs = new Tabs(controller); + + controller.waitForPageLoad = function (aDocument, aTimeout, aInterval) { + var timeout = aTimeout || 30000; + var win = null; + var timed_out = false; + + // If a user tries to do waitForPageLoad(2000), this will assign the + // interval the first arg which is most likely what they were expecting + if (typeof(aDocument) == "number"){ + timeout = aDocument; + } + + // If we have a real document use its default view + if (aDocument && (typeof(aDocument) === "object") && + "defaultView" in aDocument) + win = aDocument.defaultView; + + // If no document has been specified, fallback to the default view of the + // currently selected tab browser + win = win || this.browserObject.selectedBrowser.contentWindow; + + // Wait until the content in the tab has been loaded + try { + this.waitFor(function () { + return windows.map.hasPageLoaded(utils.getWindowId(win)); + }, "Timeout", timeout, aInterval); + } + catch (ex) { + if (!ex instanceof errors.TimeoutError) { + throw ex; + } + timed_out = true; + } + finally { + state = 'URI=' + win.document.location.href + + ', readyState=' + win.document.readyState; + message = "controller.waitForPageLoad(" + state + ")"; + + if (timed_out) { + throw new errors.AssertionError(message); + } + + broker.pass({'function': message}); + } + } +} + +var controllerAdditions = { + 'navigator:browser' :browserAdditions +}; + +/** + * DEPRECATION WARNING + * + * The following methods have all been DEPRECATED as of Mozmill 2.0 + */ +MozMillController.prototype.assertProperty = function (el, attrib, val) { + logDeprecatedAssert("assertProperty"); + + return this.assertJSProperty(el, attrib, val); +}; + +MozMillController.prototype.assertPropertyNotExist = function (el, attrib) { + logDeprecatedAssert("assertPropertyNotExist"); + return this.assertNotJSProperty(el, attrib); +}; + +/** + * DEPRECATION WARNING + * + * The following methods have all been DEPRECATED as of Mozmill 2.0 + * Use the MozMillElement object instead (https://developer.mozilla.org/en/Mozmill/Mozmill_Element_Object) + */ +MozMillController.prototype.select = function (aElement, index, option, value) { + logDeprecated("controller.select", "Use the MozMillElement object."); + + return aElement.select(index, option, value); +}; + +MozMillController.prototype.keypress = function (aElement, aKey, aModifiers, aExpectedEvent) { + logDeprecated("controller.keypress", "Use the MozMillElement object."); + + if (!aElement) { + aElement = new mozelement.MozMillElement("Elem", this.window); + } + + return aElement.keypress(aKey, aModifiers, aExpectedEvent); +} + +MozMillController.prototype.type = function (aElement, aText, aExpectedEvent) { + logDeprecated("controller.type", "Use the MozMillElement object."); + + if (!aElement) { + aElement = new mozelement.MozMillElement("Elem", this.window); + } + + var that = this; + var retval = true; + Array.forEach(aText, function (letter) { + if (!that.keypress(aElement, letter, {}, aExpectedEvent)) { + retval = false; } + }); + + return retval; +} + +MozMillController.prototype.mouseEvent = function (aElement, aOffsetX, aOffsetY, aEvent, aExpectedEvent) { + logDeprecated("controller.mouseEvent", "Use the MozMillElement object."); + + return aElement.mouseEvent(aOffsetX, aOffsetY, aEvent, aExpectedEvent); +} + +MozMillController.prototype.click = function (aElement, left, top, expectedEvent) { + logDeprecated("controller.click", "Use the MozMillElement object."); + + return aElement.click(left, top, expectedEvent); +} + +MozMillController.prototype.doubleClick = function (aElement, left, top, expectedEvent) { + logDeprecated("controller.doubleClick", "Use the MozMillElement object."); + + return aElement.doubleClick(left, top, expectedEvent); +} + +MozMillController.prototype.mouseDown = function (aElement, button, left, top, expectedEvent) { + logDeprecated("controller.mouseDown", "Use the MozMillElement object."); + + return aElement.mouseDown(button, left, top, expectedEvent); +}; + +MozMillController.prototype.mouseOut = function (aElement, button, left, top, expectedEvent) { + logDeprecated("controller.mouseOut", "Use the MozMillElement object."); + + return aElement.mouseOut(button, left, top, expectedEvent); +}; + +MozMillController.prototype.mouseOver = function (aElement, button, left, top, expectedEvent) { + logDeprecated("controller.mouseOver", "Use the MozMillElement object."); + + return aElement.mouseOver(button, left, top, expectedEvent); +}; + +MozMillController.prototype.mouseUp = function (aElement, button, left, top, expectedEvent) { + logDeprecated("controller.mouseUp", "Use the MozMillElement object."); + + return aElement.mouseUp(button, left, top, expectedEvent); +}; + +MozMillController.prototype.middleClick = function (aElement, left, top, expectedEvent) { + logDeprecated("controller.middleClick", "Use the MozMillElement object."); + + return aElement.middleClick(aElement, left, top, expectedEvent); +} + +MozMillController.prototype.rightClick = function (aElement, left, top, expectedEvent) { + logDeprecated("controller.rightClick", "Use the MozMillElement object."); + + return aElement.rightClick(left, top, expectedEvent); +} + +MozMillController.prototype.check = function (aElement, state) { + logDeprecated("controller.check", "Use the MozMillElement object."); + + return aElement.check(state); +} + +MozMillController.prototype.radio = function (aElement) { + logDeprecated("controller.radio", "Use the MozMillElement object."); + + return aElement.select(); +} + +MozMillController.prototype.waitThenClick = function (aElement, timeout, interval) { + logDeprecated("controller.waitThenClick", "Use the MozMillElement object."); + + return aElement.waitThenClick(timeout, interval); +} + +MozMillController.prototype.waitForElement = function (aElement, timeout, interval) { + logDeprecated("controller.waitForElement", "Use the MozMillElement object."); + + return aElement.waitForElement(timeout, interval); +} + +MozMillController.prototype.waitForElementNotPresent = function (aElement, timeout, interval) { + logDeprecated("controller.waitForElementNotPresent", "Use the MozMillElement object."); + + return aElement.waitForElementNotPresent(timeout, interval); +} diff --git a/services/sync/tps/extensions/mozmill/resource/driver/elementslib.js b/services/sync/tps/extensions/mozmill/resource/driver/elementslib.js new file mode 100644 index 000000000..4bf35a384 --- /dev/null +++ b/services/sync/tps/extensions/mozmill/resource/driver/elementslib.js @@ -0,0 +1,537 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +var EXPORTED_SYMBOLS = ["ID", "Link", "XPath", "Selector", "Name", "Anon", "AnonXPath", + "Lookup", "_byID", "_byName", "_byAttrib", "_byAnonAttrib", + ]; + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); + +var utils = {}; Cu.import('resource://mozmill/stdlib/utils.js', utils); +var strings = {}; Cu.import('resource://mozmill/stdlib/strings.js', strings); +var arrays = {}; Cu.import('resource://mozmill/stdlib/arrays.js', arrays); +var json2 = {}; Cu.import('resource://mozmill/stdlib/json2.js', json2); +var withs = {}; Cu.import('resource://mozmill/stdlib/withs.js', withs); +var dom = {}; Cu.import('resource://mozmill/stdlib/dom.js', dom); +var objects = {}; Cu.import('resource://mozmill/stdlib/objects.js', objects); + +var countQuotes = function (str) { + var count = 0; + var i = 0; + + while (i < str.length) { + i = str.indexOf('"', i); + if (i != -1) { + count++; + i++; + } else { + break; + } + } + + return count; +}; + +/** + * smartSplit() + * + * Takes a lookup string as input and returns + * a list of each node in the string + */ +var smartSplit = function (str) { + // Ensure we have an even number of quotes + if (countQuotes(str) % 2 != 0) { + throw new Error ("Invalid Lookup Expression"); + } + + /** + * This regex matches a single "node" in a lookup string. + * In otherwords, it matches the part between the two '/'s + * + * Regex Explanation: + * \/ - start matching at the first forward slash + * ([^\/"]*"[^"]*")* - match as many pairs of quotes as possible until we hit a slash (ignore slashes inside quotes) + * [^\/]* - match the remainder of text outside of last quote but before next slash + */ + var re = /\/([^\/"]*"[^"]*")*[^\/]*/g + var ret = [] + var match = re.exec(str); + + while (match != null) { + ret.push(match[0].replace(/^\//, "")); + match = re.exec(str); + } + + return ret; +}; + +/** + * defaultDocuments() + * + * Returns a list of default documents in which to search for elements + * if no document is provided + */ +function defaultDocuments() { + var win = Services.wm.getMostRecentWindow("navigator:browser"); + + return [ + win.document, + utils.getBrowserObject(win).selectedBrowser.contentWindow.document + ]; +}; + +/** + * nodeSearch() + * + * Takes an optional document, callback and locator string + * Returns a handle to the located element or null + */ +function nodeSearch(doc, func, string) { + if (doc != undefined) { + var documents = [doc]; + } else { + var documents = defaultDocuments(); + } + + var e = null; + var element = null; + + //inline function to recursively find the element in the DOM, cross frame. + var search = function (win, func, string) { + if (win == null) { + return; + } + + //do the lookup in the current window + element = func.call(win, string); + + if (!element || (element.length == 0)) { + var frames = win.frames; + for (var i = 0; i < frames.length; i++) { + search(frames[i], func, string); + } + } else { + e = element; + } + }; + + for (var i = 0; i < documents.length; ++i) { + var win = documents[i].defaultView; + search(win, func, string); + if (e) { + break; + } + } + + return e; +}; + +/** + * Selector() + * + * Finds an element by selector string + */ +function Selector(_document, selector, index) { + if (selector == undefined) { + throw new Error('Selector constructor did not recieve enough arguments.'); + } + + this.selector = selector; + + this.getNodeForDocument = function (s) { + return this.document.querySelectorAll(s); + }; + + var nodes = nodeSearch(_document, this.getNodeForDocument, this.selector); + + return nodes ? nodes[index || 0] : null; +}; + +/** + * ID() + * + * Finds an element by ID + */ +function ID(_document, nodeID) { + if (nodeID == undefined) { + throw new Error('ID constructor did not recieve enough arguments.'); + } + + this.getNodeForDocument = function (nodeID) { + return this.document.getElementById(nodeID); + }; + + return nodeSearch(_document, this.getNodeForDocument, nodeID); +}; + +/** + * Link() + * + * Finds a link by innerHTML + */ +function Link(_document, linkName) { + if (linkName == undefined) { + throw new Error('Link constructor did not recieve enough arguments.'); + } + + this.getNodeForDocument = function (linkName) { + var getText = function (el) { + var text = ""; + + if (el.nodeType == 3) { //textNode + if (el.data != undefined) { + text = el.data; + } else { + text = el.innerHTML; + } + + text = text.replace(/n|r|t/g, " "); + } + else if (el.nodeType == 1) { //elementNode + for (var i = 0; i < el.childNodes.length; i++) { + var child = el.childNodes.item(i); + text += getText(child); + } + + if (el.tagName == "P" || el.tagName == "BR" || + el.tagName == "HR" || el.tagName == "DIV") { + text += "\n"; + } + } + + return text; + }; + + //sometimes the windows won't have this function + try { + var links = this.document.getElementsByTagName('a'); + } catch (e) { + // ADD LOG LINE mresults.write('Error: '+ e, 'lightred'); + } + + for (var i = 0; i < links.length; i++) { + var el = links[i]; + //if (getText(el).indexOf(this.linkName) != -1) { + if (el.innerHTML.indexOf(linkName) != -1) { + return el; + } + } + + return null; + }; + + return nodeSearch(_document, this.getNodeForDocument, linkName); +}; + +/** + * XPath() + * + * Finds an element by XPath + */ +function XPath(_document, expr) { + if (expr == undefined) { + throw new Error('XPath constructor did not recieve enough arguments.'); + } + + this.getNodeForDocument = function (s) { + var aNode = this.document; + var aExpr = s; + var xpe = null; + + if (this.document.defaultView == null) { + xpe = new getMethodInWindows('XPathEvaluator')(); + } else { + xpe = new this.document.defaultView.XPathEvaluator(); + } + + var nsResolver = xpe.createNSResolver(aNode.ownerDocument == null ? aNode.documentElement + : aNode.ownerDocument.documentElement); + var result = xpe.evaluate(aExpr, aNode, nsResolver, 0, null); + var found = []; + var res; + + while (res = result.iterateNext()) { + found.push(res); + } + + return found[0]; + }; + + return nodeSearch(_document, this.getNodeForDocument, expr); +}; + +/** + * Name() + * + * Finds an element by Name + */ +function Name(_document, nName) { + if (nName == undefined) { + throw new Error('Name constructor did not recieve enough arguments.'); + } + + this.getNodeForDocument = function (s) { + try{ + var els = this.document.getElementsByName(s); + if (els.length > 0) { + return els[0]; + } + } catch (e) { + } + + return null; + }; + + return nodeSearch(_document, this.getNodeForDocument, nName); +}; + + +var _returnResult = function (results) { + if (results.length == 0) { + return null + } + else if (results.length == 1) { + return results[0]; + } else { + return results; + } +} + +var _forChildren = function (element, name, value) { + var results = []; + var nodes = Array.from(element.childNodes).filter(e => e); + + for (var i in nodes) { + var n = nodes[i]; + if (n[name] == value) { + results.push(n); + } + } + + return results; +} + +var _forAnonChildren = function (_document, element, name, value) { + var results = []; + var nodes = Array.from(_document.getAnoymousNodes(element)).filter(e => e); + + for (var i in nodes ) { + var n = nodes[i]; + if (n[name] == value) { + results.push(n); + } + } + + return results; +} + +var _byID = function (_document, parent, value) { + return _returnResult(_forChildren(parent, 'id', value)); +} + +var _byName = function (_document, parent, value) { + return _returnResult(_forChildren(parent, 'tagName', value)); +} + +var _byAttrib = function (parent, attributes) { + var results = []; + var nodes = parent.childNodes; + + for (var i in nodes) { + var n = nodes[i]; + requirementPass = 0; + requirementLength = 0; + + for (var a in attributes) { + requirementLength++; + try { + if (n.getAttribute(a) == attributes[a]) { + requirementPass++; + } + } catch (e) { + // Workaround any bugs in custom attribute crap in XUL elements + } + } + + if (requirementPass == requirementLength) { + results.push(n); + } + } + + return _returnResult(results) +} + +var _byAnonAttrib = function (_document, parent, attributes) { + var results = []; + + if (objects.getLength(attributes) == 1) { + for (var i in attributes) { + var k = i; + var v = attributes[i]; + } + + var result = _document.getAnonymousElementByAttribute(parent, k, v); + if (result) { + return result; + } + } + + var nodes = Array.from(_document.getAnonymousNodes(parent)).filter(n => n.getAttribute); + + function resultsForNodes (nodes) { + for (var i in nodes) { + var n = nodes[i]; + requirementPass = 0; + requirementLength = 0; + + for (var a in attributes) { + requirementLength++; + if (n.getAttribute(a) == attributes[a]) { + requirementPass++; + } + } + + if (requirementPass == requirementLength) { + results.push(n); + } + } + } + + resultsForNodes(nodes); + if (results.length == 0) { + resultsForNodes(Array.from(parent.childNodes).filter(n => n != undefined && n.getAttribute)); + } + + return _returnResult(results) +} + +var _byIndex = function (_document, parent, i) { + if (parent instanceof Array) { + return parent[i]; + } + + return parent.childNodes[i]; +} + +var _anonByName = function (_document, parent, value) { + return _returnResult(_forAnonChildren(_document, parent, 'tagName', value)); +} + +var _anonByAttrib = function (_document, parent, value) { + return _byAnonAttrib(_document, parent, value); +} + +var _anonByIndex = function (_document, parent, i) { + return _document.getAnonymousNodes(parent)[i]; +} + +/** + * Lookup() + * + * Finds an element by Lookup expression + */ +function Lookup(_document, expression) { + if (expression == undefined) { + throw new Error('Lookup constructor did not recieve enough arguments.'); + } + + var expSplit = smartSplit(expression).filter(e => e != ''); + expSplit.unshift(_document); + + var nCases = {'id':_byID, 'name':_byName, 'attrib':_byAttrib, 'index':_byIndex}; + var aCases = {'name':_anonByName, 'attrib':_anonByAttrib, 'index':_anonByIndex}; + + /** + * Reduces the lookup expression + * @param {Object} parentNode + * Parent node (previousValue of the formerly executed reduce callback) + * @param {String} exp + * Lookup expression for the parents child node + * + * @returns {Object} Node found by the given expression + */ + var reduceLookup = function (parentNode, exp) { + // Abort in case the parent node was not found + if (!parentNode) { + return false; + } + + // Handle case where only index is provided + var cases = nCases; + + // Handle ending index before any of the expression gets mangled + if (withs.endsWith(exp, ']')) { + var expIndex = json2.JSON.parse(strings.vslice(exp, '[', ']')); + } + + // Handle anon + if (withs.startsWith(exp, 'anon')) { + exp = strings.vslice(exp, '(', ')'); + cases = aCases; + } + + if (withs.startsWith(exp, '[')) { + try { + var obj = json2.JSON.parse(strings.vslice(exp, '[', ']')); + } catch (e) { + throw new SyntaxError(e + '. String to be parsed was || ' + + strings.vslice(exp, '[', ']') + ' ||'); + } + + var r = cases['index'](_document, parentNode, obj); + if (r == null) { + throw new SyntaxError('Expression "' + exp + + '" returned null. Anonymous == ' + (cases == aCases)); + } + + return r; + } + + for (var c in cases) { + if (withs.startsWith(exp, c)) { + try { + var obj = json2.JSON.parse(strings.vslice(exp, '(', ')')) + } catch (e) { + throw new SyntaxError(e + '. String to be parsed was || ' + + strings.vslice(exp, '(', ')') + ' ||'); + } + var result = cases[c](_document, parentNode, obj); + } + } + + if (!result) { + if (withs.startsWith(exp, '{')) { + try { + var obj = json2.JSON.parse(exp); + } catch (e) { + throw new SyntaxError(e + '. String to be parsed was || ' + exp + ' ||'); + } + + if (cases == aCases) { + var result = _anonByAttrib(_document, parentNode, obj); + } else { + var result = _byAttrib(parentNode, obj); + } + } + } + + // Final return + if (expIndex) { + // TODO: Check length and raise error + return result[expIndex]; + } else { + // TODO: Check length and raise error + return result; + } + + // Maybe we should cause an exception here + return false; + }; + + return expSplit.reduce(reduceLookup); +}; diff --git a/services/sync/tps/extensions/mozmill/resource/driver/mozelement.js b/services/sync/tps/extensions/mozmill/resource/driver/mozelement.js new file mode 100644 index 000000000..850c86523 --- /dev/null +++ b/services/sync/tps/extensions/mozmill/resource/driver/mozelement.js @@ -0,0 +1,1163 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +var EXPORTED_SYMBOLS = ["Elem", "Selector", "ID", "Link", "XPath", "Name", "Lookup", + "MozMillElement", "MozMillCheckBox", "MozMillRadio", "MozMillDropList", + "MozMillTextBox", "subclasses" + ]; + +const NAMESPACE_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; + +var EventUtils = {}; Cu.import('resource://mozmill/stdlib/EventUtils.js', EventUtils); + +var assertions = {}; Cu.import('resource://mozmill/modules/assertions.js', assertions); +var broker = {}; Cu.import('resource://mozmill/driver/msgbroker.js', broker); +var elementslib = {}; Cu.import('resource://mozmill/driver/elementslib.js', elementslib); +var utils = {}; Cu.import('resource://mozmill/stdlib/utils.js', utils); + +var assert = new assertions.Assert(); + +// A list of all the subclasses available. Shared modules can push their own subclasses onto this list +var subclasses = [MozMillCheckBox, MozMillRadio, MozMillDropList, MozMillTextBox]; + +/** + * createInstance() + * + * Returns an new instance of a MozMillElement + * The type of the element is automatically determined + */ +function createInstance(locatorType, locator, elem, document) { + var args = { "document": document, "element": elem }; + + // If we already have an element lets determine the best MozMillElement type + if (elem) { + for (var i = 0; i < subclasses.length; ++i) { + if (subclasses[i].isType(elem)) { + return new subclasses[i](locatorType, locator, args); + } + } + } + + // By default we create a base MozMillElement + if (MozMillElement.isType(elem)) { + return new MozMillElement(locatorType, locator, args); + } + + throw new Error("Unsupported element type " + locatorType + ": " + locator); +} + +var Elem = function (node) { + return createInstance("Elem", node, node); +}; + +var Selector = function (document, selector, index) { + return createInstance("Selector", selector, elementslib.Selector(document, selector, index), document); +}; + +var ID = function (document, nodeID) { + return createInstance("ID", nodeID, elementslib.ID(document, nodeID), document); +}; + +var Link = function (document, linkName) { + return createInstance("Link", linkName, elementslib.Link(document, linkName), document); +}; + +var XPath = function (document, expr) { + return createInstance("XPath", expr, elementslib.XPath(document, expr), document); +}; + +var Name = function (document, nName) { + return createInstance("Name", nName, elementslib.Name(document, nName), document); +}; + +var Lookup = function (document, expression) { + var elem = createInstance("Lookup", expression, elementslib.Lookup(document, expression), document); + + // Bug 864268 - Expose the expression property to maintain backwards compatibility + elem.expression = elem._locator; + + return elem; +}; + +/** + * MozMillElement + * The base class for all mozmill elements + */ +function MozMillElement(locatorType, locator, args) { + args = args || {}; + this._locatorType = locatorType; + this._locator = locator; + this._element = args["element"]; + this._owner = args["owner"]; + + this._document = this._element ? this._element.ownerDocument : args["document"]; + this._defaultView = this._document ? this._document.defaultView : null; + + // Used to maintain backwards compatibility with controller.js + this.isElement = true; +} + +// Static method that returns true if node is of this element type +MozMillElement.isType = function (node) { + return true; +}; + +// This getter is the magic behind lazy loading (note distinction between _element and element) +MozMillElement.prototype.__defineGetter__("element", function () { + // If the document is invalid (e.g. reload of the page), invalidate the cached + // element and update the document cache + if (this._defaultView && this._defaultView.document !== this._document) { + this._document = this._defaultView.document; + this._element = undefined; + } + + if (this._element == undefined) { + if (elementslib[this._locatorType]) { + this._element = elementslib[this._locatorType](this._document, this._locator); + } else if (this._locatorType == "Elem") { + this._element = this._locator; + } else { + throw new Error("Unknown locator type: " + this._locatorType); + } + } + + return this._element; +}); + +/** + * Drag an element to the specified offset on another element, firing mouse and + * drag events. Adapted from EventUtils.js synthesizeDrop() + * + * By default it will drag the source element over the destination's element + * center with a "move" dropEffect. + * + * @param {MozElement} aElement + * Destination element over which the drop occurs + * @param {Number} [aOffsetX=aElement.width/2] + * Relative x offset for dropping on aElement + * @param {Number} [aOffsetY=aElement.height/2] + * Relative y offset for dropping on aElement + * @param {DOMWindow} [aSourceWindow=this.element.ownerDocument.defaultView] + * Custom source Window to be used. + * @param {DOMWindow} [aDestWindow=aElement.getNode().ownerDocument.defaultView] + * Custom destination Window to be used. + * @param {String} [aDropEffect="move"] + * Possible values: copy, move, link, none + * @param {Object[]} [aDragData] + * An array holding custom drag data to be used during the drag event + * Format: [{ type: "text/plain", "Text to drag"}, ...] + * + * @returns {String} the captured dropEffect + */ +MozMillElement.prototype.dragToElement = function(aElement, aOffsetX, aOffsetY, + aSourceWindow, aDestWindow, + aDropEffect, aDragData) { + if (!this.element) { + throw new Error("Could not find element " + this.getInfo()); + } + if (!aElement) { + throw new Error("Missing destination element"); + } + + var srcNode = this.element; + var destNode = aElement.getNode(); + var srcWindow = aSourceWindow || + (srcNode.ownerDocument ? srcNode.ownerDocument.defaultView + : srcNode); + var destWindow = aDestWindow || + (destNode.ownerDocument ? destNode.ownerDocument.defaultView + : destNode); + + var srcRect = srcNode.getBoundingClientRect(); + var srcCoords = { + x: srcRect.width / 2, + y: srcRect.height / 2 + }; + var destRect = destNode.getBoundingClientRect(); + var destCoords = { + x: (!aOffsetX || isNaN(aOffsetX)) ? (destRect.width / 2) : aOffsetX, + y: (!aOffsetY || isNaN(aOffsetY)) ? (destRect.height / 2) : aOffsetY + }; + + var windowUtils = destWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + var ds = Cc["@mozilla.org/widget/dragservice;1"].getService(Ci.nsIDragService); + + var dataTransfer; + var trapDrag = function (event) { + srcWindow.removeEventListener("dragstart", trapDrag, true); + dataTransfer = event.dataTransfer; + + if (!aDragData) { + return; + } + + for (var i = 0; i < aDragData.length; i++) { + var item = aDragData[i]; + for (var j = 0; j < item.length; j++) { + dataTransfer.mozSetDataAt(item[j].type, item[j].data, i); + } + } + + dataTransfer.dropEffect = aDropEffect || "move"; + event.preventDefault(); + event.stopPropagation(); + } + + ds.startDragSession(); + + try { + srcWindow.addEventListener("dragstart", trapDrag, true); + EventUtils.synthesizeMouse(srcNode, srcCoords.x, srcCoords.y, + { type: "mousedown" }, srcWindow); + EventUtils.synthesizeMouse(destNode, destCoords.x, destCoords.y, + { type: "mousemove" }, destWindow); + + var event = destWindow.document.createEvent("DragEvent"); + event.initDragEvent("dragenter", true, true, destWindow, 0, 0, 0, 0, 0, + false, false, false, false, 0, null, dataTransfer); + event.initDragEvent("dragover", true, true, destWindow, 0, 0, 0, 0, 0, + false, false, false, false, 0, null, dataTransfer); + event.initDragEvent("drop", true, true, destWindow, 0, 0, 0, 0, 0, + false, false, false, false, 0, null, dataTransfer); + windowUtils.dispatchDOMEventViaPresShell(destNode, event, true); + + EventUtils.synthesizeMouse(destNode, destCoords.x, destCoords.y, + { type: "mouseup" }, destWindow); + + return dataTransfer.dropEffect; + } finally { + ds.endDragSession(true); + } + +}; + +// Returns the actual wrapped DOM node +MozMillElement.prototype.getNode = function () { + return this.element; +}; + +MozMillElement.prototype.getInfo = function () { + return this._locatorType + ": " + this._locator; +}; + +/** + * Sometimes an element which once existed will no longer exist in the DOM + * This function re-searches for the element + */ +MozMillElement.prototype.exists = function () { + this._element = undefined; + if (this.element) { + return true; + } + + return false; +}; + +/** + * Synthesize a keypress event on the given element + * + * @param {string} aKey + * Key to use for synthesizing the keypress event. It can be a simple + * character like "k" or a string like "VK_ESCAPE" for command keys + * @param {object} aModifiers + * Information about the modifier keys to send + * Elements: accelKey - Hold down the accelerator key (ctrl/meta) + * [optional - default: false] + * altKey - Hold down the alt key + * [optional - default: false] + * ctrlKey - Hold down the ctrl key + * [optional - default: false] + * metaKey - Hold down the meta key (command key on Mac) + * [optional - default: false] + * shiftKey - Hold down the shift key + * [optional - default: false] + * @param {object} aExpectedEvent + * Information about the expected event to occur + * Elements: target - Element which should receive the event + * [optional - default: current element] + * type - Type of the expected key event + */ +MozMillElement.prototype.keypress = function (aKey, aModifiers, aExpectedEvent) { + if (!this.element) { + throw new Error("Could not find element " + this.getInfo()); + } + + var win = this.element.ownerDocument ? this.element.ownerDocument.defaultView + : this.element; + this.element.focus(); + + if (aExpectedEvent) { + if (!aExpectedEvent.type) { + throw new Error(arguments.callee.name + ": Expected event type not specified"); + } + + var target = aExpectedEvent.target ? aExpectedEvent.target.getNode() + : this.element; + EventUtils.synthesizeKeyExpectEvent(aKey, aModifiers || {}, target, aExpectedEvent.type, + "MozMillElement.keypress()", win); + } else { + EventUtils.synthesizeKey(aKey, aModifiers || {}, win); + } + + broker.pass({'function':'MozMillElement.keypress()'}); + + return true; +}; + + +/** + * Synthesize a general mouse event on the given element + * + * @param {number} aOffsetX + * Relative x offset in the elements bounds to click on + * @param {number} aOffsetY + * Relative y offset in the elements bounds to click on + * @param {object} aEvent + * Information about the event to send + * Elements: accelKey - Hold down the accelerator key (ctrl/meta) + * [optional - default: false] + * altKey - Hold down the alt key + * [optional - default: false] + * button - Mouse button to use + * [optional - default: 0] + * clickCount - Number of counts to click + * [optional - default: 1] + * ctrlKey - Hold down the ctrl key + * [optional - default: false] + * metaKey - Hold down the meta key (command key on Mac) + * [optional - default: false] + * shiftKey - Hold down the shift key + * [optional - default: false] + * type - Type of the mouse event ('click', 'mousedown', + * 'mouseup', 'mouseover', 'mouseout') + * [optional - default: 'mousedown' + 'mouseup'] + * @param {object} aExpectedEvent + * Information about the expected event to occur + * Elements: target - Element which should receive the event + * [optional - default: current element] + * type - Type of the expected mouse event + */ +MozMillElement.prototype.mouseEvent = function (aOffsetX, aOffsetY, aEvent, aExpectedEvent) { + if (!this.element) { + throw new Error(arguments.callee.name + ": could not find element " + this.getInfo()); + } + + if ("document" in this.element) { + throw new Error("A window cannot be a target for mouse events."); + } + + var rect = this.element.getBoundingClientRect(); + + if (!aOffsetX || isNaN(aOffsetX)) { + aOffsetX = rect.width / 2; + } + + if (!aOffsetY || isNaN(aOffsetY)) { + aOffsetY = rect.height / 2; + } + + // Scroll element into view otherwise the click will fail + if ("scrollIntoView" in this.element) + this.element.scrollIntoView(); + + if (aExpectedEvent) { + // The expected event type has to be set + if (!aExpectedEvent.type) { + throw new Error(arguments.callee.name + ": Expected event type not specified"); + } + + // If no target has been specified use the specified element + var target = aExpectedEvent.target ? aExpectedEvent.target.getNode() + : this.element; + if (!target) { + throw new Error(arguments.callee.name + ": could not find element " + + aExpectedEvent.target.getInfo()); + } + + EventUtils.synthesizeMouseExpectEvent(this.element, aOffsetX, aOffsetY, aEvent, + target, aExpectedEvent.type, + "MozMillElement.mouseEvent()", + this.element.ownerDocument.defaultView); + } else { + EventUtils.synthesizeMouse(this.element, aOffsetX, aOffsetY, aEvent, + this.element.ownerDocument.defaultView); + } + + // Bug 555347 + // We don't know why this sleep is necessary but more investigation is needed + // before it can be removed + utils.sleep(0); + + return true; +}; + +/** + * Synthesize a mouse click event on the given element + */ +MozMillElement.prototype.click = function (aOffsetX, aOffsetY, aExpectedEvent) { + // Handle menu items differently + if (this.element && this.element.tagName == "menuitem") { + this.element.click(); + } else { + this.mouseEvent(aOffsetX, aOffsetY, {}, aExpectedEvent); + } + + broker.pass({'function':'MozMillElement.click()'}); + + return true; +}; + +/** + * Synthesize a double click on the given element + */ +MozMillElement.prototype.doubleClick = function (aOffsetX, aOffsetY, aExpectedEvent) { + this.mouseEvent(aOffsetX, aOffsetY, {clickCount: 2}, aExpectedEvent); + + broker.pass({'function':'MozMillElement.doubleClick()'}); + + return true; +}; + +/** + * Synthesize a mouse down event on the given element + */ +MozMillElement.prototype.mouseDown = function (aButton, aOffsetX, aOffsetY, aExpectedEvent) { + this.mouseEvent(aOffsetX, aOffsetY, {button: aButton, type: "mousedown"}, aExpectedEvent); + + broker.pass({'function':'MozMillElement.mouseDown()'}); + + return true; +}; + +/** + * Synthesize a mouse out event on the given element + */ +MozMillElement.prototype.mouseOut = function (aButton, aOffsetX, aOffsetY, aExpectedEvent) { + this.mouseEvent(aOffsetX, aOffsetY, {button: aButton, type: "mouseout"}, aExpectedEvent); + + broker.pass({'function':'MozMillElement.mouseOut()'}); + + return true; +}; + +/** + * Synthesize a mouse over event on the given element + */ +MozMillElement.prototype.mouseOver = function (aButton, aOffsetX, aOffsetY, aExpectedEvent) { + this.mouseEvent(aOffsetX, aOffsetY, {button: aButton, type: "mouseover"}, aExpectedEvent); + + broker.pass({'function':'MozMillElement.mouseOver()'}); + + return true; +}; + +/** + * Synthesize a mouse up event on the given element + */ +MozMillElement.prototype.mouseUp = function (aButton, aOffsetX, aOffsetY, aExpectedEvent) { + this.mouseEvent(aOffsetX, aOffsetY, {button: aButton, type: "mouseup"}, aExpectedEvent); + + broker.pass({'function':'MozMillElement.mouseUp()'}); + + return true; +}; + +/** + * Synthesize a mouse middle click event on the given element + */ +MozMillElement.prototype.middleClick = function (aOffsetX, aOffsetY, aExpectedEvent) { + this.mouseEvent(aOffsetX, aOffsetY, {button: 1}, aExpectedEvent); + + broker.pass({'function':'MozMillElement.middleClick()'}); + + return true; +}; + +/** + * Synthesize a mouse right click event on the given element + */ +MozMillElement.prototype.rightClick = function (aOffsetX, aOffsetY, aExpectedEvent) { + this.mouseEvent(aOffsetX, aOffsetY, {type : "contextmenu", button: 2 }, aExpectedEvent); + + broker.pass({'function':'MozMillElement.rightClick()'}); + + return true; +}; + +/** + * Synthesize a general touch event on the given element + * + * @param {Number} [aOffsetX=aElement.width / 2] + * Relative x offset in the elements bounds to click on + * @param {Number} [aOffsetY=aElement.height / 2] + * Relative y offset in the elements bounds to click on + * @param {Object} [aEvent] + * Information about the event to send + * @param {Boolean} [aEvent.altKey=false] + * A Boolean value indicating whether or not the alt key was down when + * the touch event was fired + * @param {Number} [aEvent.angle=0] + * The angle (in degrees) that the ellipse described by rx and + * ry must be rotated, clockwise, to most accurately cover the area + * of contact between the user and the surface. + * @param {Touch[]} [aEvent.changedTouches] + * A TouchList of all the Touch objects representing individual points of + * contact whose states changed between the previous touch event and + * this one + * @param {Boolean} [aEvent.ctrlKey] + * A Boolean value indicating whether or not the control key was down + * when the touch event was fired + * @param {Number} [aEvent.force=1] + * The amount of pressure being applied to the surface by the user, as a + * float between 0.0 (no pressure) and 1.0 (maximum pressure) + * @param {Number} [aEvent.id=0] + * A unique identifier for this Touch object. A given touch (say, by a + * finger) will have the same identifier for the duration of its movement + * around the surface. This lets you ensure that you're tracking the same + * touch all the time + * @param {Boolean} [aEvent.metaKey] + * A Boolean value indicating whether or not the meta key was down when + * the touch event was fired. + * @param {Number} [aEvent.rx=1] + * The X radius of the ellipse that most closely circumscribes the area + * of contact with the screen. + * @param {Number} [aEvent.ry=1] + * The Y radius of the ellipse that most closely circumscribes the area + * of contact with the screen. + * @param {Boolean} [aEvent.shiftKey] + * A Boolean value indicating whether or not the shift key was down when + * the touch event was fired + * @param {Touch[]} [aEvent.targetTouches] + * A TouchList of all the Touch objects that are both currently in + * contact with the touch surface and were also started on the same + * element that is the target of the event + * @param {Touch[]} [aEvent.touches] + * A TouchList of all the Touch objects representing all current points + * of contact with the surface, regardless of target or changed status + * @param {Number} [aEvent.type=*|touchstart|touchend|touchmove|touchenter|touchleave|touchcancel] + * The type of touch event that occurred + * @param {Element} [aEvent.target] + * The target of the touches associated with this event. This target + * corresponds to the target of all the touches in the targetTouches + * attribute, but note that other touches in this event may have a + * different target. To be careful, you should use the target associated + * with individual touches + */ +MozMillElement.prototype.touchEvent = function (aOffsetX, aOffsetY, aEvent) { + if (!this.element) { + throw new Error(arguments.callee.name + ": could not find element " + this.getInfo()); + } + + if ("document" in this.element) { + throw new Error("A window cannot be a target for touch events."); + } + + var rect = this.element.getBoundingClientRect(); + + if (!aOffsetX || isNaN(aOffsetX)) { + aOffsetX = rect.width / 2; + } + + if (!aOffsetY || isNaN(aOffsetY)) { + aOffsetY = rect.height / 2; + } + + // Scroll element into view otherwise the click will fail + if ("scrollIntoView" in this.element) { + this.element.scrollIntoView(); + } + + EventUtils.synthesizeTouch(this.element, aOffsetX, aOffsetY, aEvent, + this.element.ownerDocument.defaultView); + + return true; +}; + +/** + * Synthesize a touch tap event on the given element + * + * @param {Number} [aOffsetX=aElement.width / 2] + * Left offset in px where the event is triggered + * @param {Number} [aOffsetY=aElement.height / 2] + * Top offset in px where the event is triggered + * @param {Object} [aExpectedEvent] + * Information about the expected event to occur + * @param {MozMillElement} [aExpectedEvent.target=this.element] + * Element which should receive the event + * @param {MozMillElement} [aExpectedEvent.type] + * Type of the expected mouse event + */ +MozMillElement.prototype.tap = function (aOffsetX, aOffsetY, aExpectedEvent) { + this.mouseEvent(aOffsetX, aOffsetY, { + clickCount: 1, + inputSource: Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH + }, aExpectedEvent); + + broker.pass({'function':'MozMillElement.tap()'}); + + return true; +}; + +/** + * Synthesize a double tap on the given element + * + * @param {Number} [aOffsetX=aElement.width / 2] + * Left offset in px where the event is triggered + * @param {Number} [aOffsetY=aElement.height / 2] + * Top offset in px where the event is triggered + * @param {Object} [aExpectedEvent] + * Information about the expected event to occur + * @param {MozMillElement} [aExpectedEvent.target=this.element] + * Element which should receive the event + * @param {MozMillElement} [aExpectedEvent.type] + * Type of the expected mouse event + */ +MozMillElement.prototype.doubleTap = function (aOffsetX, aOffsetY, aExpectedEvent) { + this.mouseEvent(aOffsetX, aOffsetY, { + clickCount: 2, + inputSource: Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH + }, aExpectedEvent); + + broker.pass({'function':'MozMillElement.doubleTap()'}); + + return true; +}; + +/** + * Synthesize a long press + * + * @param {Number} aOffsetX + * Left offset in px where the event is triggered + * @param {Number} aOffsetY + * Top offset in px where the event is triggered + * @param {Number} [aTime=1000] + * Duration of the "press" event in ms + */ +MozMillElement.prototype.longPress = function (aOffsetX, aOffsetY, aTime) { + var time = aTime || 1000; + + this.touchStart(aOffsetX, aOffsetY); + utils.sleep(time); + this.touchEnd(aOffsetX, aOffsetY); + + broker.pass({'function':'MozMillElement.longPress()'}); + + return true; +}; + +/** + * Synthesize a touch & drag event on the given element + * + * @param {Number} aOffsetX1 + * Left offset of the start position + * @param {Number} aOffsetY1 + * Top offset of the start position + * @param {Number} aOffsetX2 + * Left offset of the end position + * @param {Number} aOffsetY2 + * Top offset of the end position + */ +MozMillElement.prototype.touchDrag = function (aOffsetX1, aOffsetY1, aOffsetX2, aOffsetY2) { + this.touchStart(aOffsetX1, aOffsetY1); + this.touchMove(aOffsetX2, aOffsetY2); + this.touchEnd(aOffsetX2, aOffsetY2); + + broker.pass({'function':'MozMillElement.move()'}); + + return true; +}; + +/** + * Synthesize a press / touchstart event on the given element + * + * @param {Number} aOffsetX + * Left offset where the event is triggered + * @param {Number} aOffsetY + * Top offset where the event is triggered + */ +MozMillElement.prototype.touchStart = function (aOffsetX, aOffsetY) { + this.touchEvent(aOffsetX, aOffsetY, { type: "touchstart" }); + + broker.pass({'function':'MozMillElement.touchStart()'}); + + return true; +}; + +/** + * Synthesize a release / touchend event on the given element + * + * @param {Number} aOffsetX + * Left offset where the event is triggered + * @param {Number} aOffsetY + * Top offset where the event is triggered + */ +MozMillElement.prototype.touchEnd = function (aOffsetX, aOffsetY) { + this.touchEvent(aOffsetX, aOffsetY, { type: "touchend" }); + + broker.pass({'function':'MozMillElement.touchEnd()'}); + + return true; +}; + +/** + * Synthesize a touchMove event on the given element + * + * @param {Number} aOffsetX + * Left offset where the event is triggered + * @param {Number} aOffsetY + * Top offset where the event is triggered + */ +MozMillElement.prototype.touchMove = function (aOffsetX, aOffsetY) { + this.touchEvent(aOffsetX, aOffsetY, { type: "touchmove" }); + + broker.pass({'function':'MozMillElement.touchMove()'}); + + return true; +}; + +MozMillElement.prototype.waitForElement = function (timeout, interval) { + var elem = this; + + assert.waitFor(function () { + return elem.exists(); + }, "Element.waitForElement(): Element '" + this.getInfo() + + "' has been found", timeout, interval); + + broker.pass({'function':'MozMillElement.waitForElement()'}); +}; + +MozMillElement.prototype.waitForElementNotPresent = function (timeout, interval) { + var elem = this; + + assert.waitFor(function () { + return !elem.exists(); + }, "Element.waitForElementNotPresent(): Element '" + this.getInfo() + + "' has not been found", timeout, interval); + + broker.pass({'function':'MozMillElement.waitForElementNotPresent()'}); +}; + +MozMillElement.prototype.waitThenClick = function (timeout, interval, + aOffsetX, aOffsetY, aExpectedEvent) { + this.waitForElement(timeout, interval); + this.click(aOffsetX, aOffsetY, aExpectedEvent); +}; + +/** + * Waits for the element to be available in the DOM, then trigger a tap event + * + * @param {Number} [aTimeout=5000] + * Time to wait for the element to be available + * @param {Number} [aInterval=100] + * Interval to check for availability + * @param {Number} [aOffsetX=aElement.width / 2] + * Left offset where the event is triggered + * @param {Number} [aOffsetY=aElement.height / 2] + * Top offset where the event is triggered + * @param {Object} [aExpectedEvent] + * Information about the expected event to occur + * @param {MozMillElement} [aExpectedEvent.target=this.element] + * Element which should receive the event + * @param {MozMillElement} [aExpectedEvent.type] + * Type of the expected mouse event + */ +MozMillElement.prototype.waitThenTap = function (aTimeout, aInterval, + aOffsetX, aOffsetY, aExpectedEvent) { + this.waitForElement(aTimeout, aInterval); + this.tap(aOffsetX, aOffsetY, aExpectedEvent); +}; + +// Dispatches an HTMLEvent +MozMillElement.prototype.dispatchEvent = function (eventType, canBubble, modifiers) { + canBubble = canBubble || true; + modifiers = modifiers || { }; + + let document = 'ownerDocument' in this.element ? this.element.ownerDocument + : this.element.document; + + let evt = document.createEvent('HTMLEvents'); + evt.shiftKey = modifiers["shift"]; + evt.metaKey = modifiers["meta"]; + evt.altKey = modifiers["alt"]; + evt.ctrlKey = modifiers["ctrl"]; + evt.initEvent(eventType, canBubble, true); + + this.element.dispatchEvent(evt); +}; + + +/** + * MozMillCheckBox, which inherits from MozMillElement + */ +function MozMillCheckBox(locatorType, locator, args) { + MozMillElement.call(this, locatorType, locator, args); +} + + +MozMillCheckBox.prototype = Object.create(MozMillElement.prototype, { + check : { + /** + * Enable/Disable a checkbox depending on the target state + * + * @param {boolean} state State to set + * @return {boolean} Success state + */ + value : function MMCB_check(state) { + var result = false; + + if (!this.element) { + throw new Error("could not find element " + this.getInfo()); + } + + // If we have a XUL element, unwrap its XPCNativeWrapper + if (this.element.namespaceURI == NAMESPACE_XUL) { + this.element = utils.unwrapNode(this.element); + } + + state = (typeof(state) == "boolean") ? state : false; + if (state != this.element.checked) { + this.click(); + var element = this.element; + + assert.waitFor(function () { + return element.checked == state; + }, "CheckBox.check(): Checkbox " + this.getInfo() + " could not be checked/unchecked", 500); + + result = true; + } + + broker.pass({'function':'MozMillCheckBox.check(' + this.getInfo() + + ', state: ' + state + ')'}); + + return result; + } + } +}); + + +/** + * Returns true if node is of type MozMillCheckBox + * + * @static + * @param {DOMNode} node Node to check for its type + * @return {boolean} True if node is of type checkbox + */ +MozMillCheckBox.isType = function MMCB_isType(node) { + return ((node.localName.toLowerCase() == "input" && node.getAttribute("type") == "checkbox") || + (node.localName.toLowerCase() == 'toolbarbutton' && node.getAttribute('type') == 'checkbox') || + (node.localName.toLowerCase() == 'checkbox')); +}; + + +/** + * MozMillRadio, which inherits from MozMillElement + */ +function MozMillRadio(locatorType, locator, args) { + MozMillElement.call(this, locatorType, locator, args); +} + + +MozMillRadio.prototype = Object.create(MozMillElement.prototype, { + select : { + /** + * Select the given radio button + * + * @param {number} [index=0] + * Specifies which radio button in the group to select (only + * applicable to radiogroup elements) + * @return {boolean} Success state + */ + value : function MMR_select(index) { + if (!this.element) { + throw new Error("could not find element " + this.getInfo()); + } + + if (this.element.localName.toLowerCase() == "radiogroup") { + var element = this.element.getElementsByTagName("radio")[index || 0]; + new MozMillRadio("Elem", element).click(); + } else { + var element = this.element; + this.click(); + } + + assert.waitFor(function () { + // If we have a XUL element, unwrap its XPCNativeWrapper + if (element.namespaceURI == NAMESPACE_XUL) { + element = utils.unwrapNode(element); + return element.selected == true; + } + + return element.checked == true; + }, "Radio.select(): Radio button " + this.getInfo() + " has been selected", 500); + + broker.pass({'function':'MozMillRadio.select(' + this.getInfo() + ')'}); + + return true; + } + } +}); + + +/** + * Returns true if node is of type MozMillRadio + * + * @static + * @param {DOMNode} node Node to check for its type + * @return {boolean} True if node is of type radio + */ +MozMillRadio.isType = function MMR_isType(node) { + return ((node.localName.toLowerCase() == 'input' && node.getAttribute('type') == 'radio') || + (node.localName.toLowerCase() == 'toolbarbutton' && node.getAttribute('type') == 'radio') || + (node.localName.toLowerCase() == 'radio') || + (node.localName.toLowerCase() == 'radiogroup')); +}; + + +/** + * MozMillDropList, which inherits from MozMillElement + */ +function MozMillDropList(locatorType, locator, args) { + MozMillElement.call(this, locatorType, locator, args); +} + + +MozMillDropList.prototype = Object.create(MozMillElement.prototype, { + select : { + /** + * Select the specified option and trigger the relevant events of the element + * @return {boolean} + */ + value : function MMDL_select(index, option, value) { + if (!this.element){ + throw new Error("Could not find element " + this.getInfo()); + } + + //if we have a select drop down + if (this.element.localName.toLowerCase() == "select"){ + var item = null; + + // The selected item should be set via its index + if (index != undefined) { + // Resetting a menulist has to be handled separately + if (index == -1) { + this.dispatchEvent('focus', false); + this.element.selectedIndex = index; + this.dispatchEvent('change', true); + + broker.pass({'function':'MozMillDropList.select()'}); + + return true; + } else { + item = this.element.options.item(index); + } + } else { + for (var i = 0; i < this.element.options.length; i++) { + var entry = this.element.options.item(i); + if (option != undefined && entry.innerHTML == option || + value != undefined && entry.value == value) { + item = entry; + break; + } + } + } + + // Click the item + try { + // EventUtils.synthesizeMouse doesn't work. + this.dispatchEvent('focus', false); + item.selected = true; + this.dispatchEvent('change', true); + + var self = this; + var selected = index || option || value; + assert.waitFor(function () { + switch (selected) { + case index: + return selected === self.element.selectedIndex; + break; + case option: + return selected === item.label; + break; + case value: + return selected === item.value; + break; + } + }, "DropList.select(): The correct item has been selected"); + + broker.pass({'function':'MozMillDropList.select()'}); + + return true; + } catch (e) { + throw new Error("No item selected for element " + this.getInfo()); + } + } + //if we have a xul menupopup select accordingly + else if (this.element.namespaceURI.toLowerCase() == NAMESPACE_XUL) { + var ownerDoc = this.element.ownerDocument; + // Unwrap the XUL element's XPCNativeWrapper + this.element = utils.unwrapNode(this.element); + // Get the list of menuitems + var menuitems = this.element. + getElementsByTagNameNS(NAMESPACE_XUL, "menupopup")[0]. + getElementsByTagNameNS(NAMESPACE_XUL, "menuitem"); + + var item = null; + + if (index != undefined) { + if (index == -1) { + this.dispatchEvent('focus', false); + this.element.boxObject.activeChild = null; + this.dispatchEvent('change', true); + + broker.pass({'function':'MozMillDropList.select()'}); + + return true; + } else { + item = menuitems[index]; + } + } else { + for (var i = 0; i < menuitems.length; i++) { + var entry = menuitems[i]; + if (option != undefined && entry.label == option || + value != undefined && entry.value == value) { + item = entry; + break; + } + } + } + + // Click the item + try { + item.click(); + + var self = this; + var selected = index || option || value; + assert.waitFor(function () { + switch (selected) { + case index: + return selected === self.element.selectedIndex; + break; + case option: + return selected === self.element.label; + break; + case value: + return selected === self.element.value; + break; + } + }, "DropList.select(): The correct item has been selected"); + + broker.pass({'function':'MozMillDropList.select()'}); + + return true; + } catch (e) { + throw new Error('No item selected for element ' + this.getInfo()); + } + } + } + } +}); + + +/** + * Returns true if node is of type MozMillDropList + * + * @static + * @param {DOMNode} node Node to check for its type + * @return {boolean} True if node is of type dropdown list + */ +MozMillDropList.isType = function MMR_isType(node) { + return ((node.localName.toLowerCase() == 'toolbarbutton' && + (node.getAttribute('type') == 'menu' || node.getAttribute('type') == 'menu-button')) || + (node.localName.toLowerCase() == 'menu') || + (node.localName.toLowerCase() == 'menulist') || + (node.localName.toLowerCase() == 'select' )); +}; + + +/** + * MozMillTextBox, which inherits from MozMillElement + */ +function MozMillTextBox(locatorType, locator, args) { + MozMillElement.call(this, locatorType, locator, args); +} + + +MozMillTextBox.prototype = Object.create(MozMillElement.prototype, { + sendKeys : { + /** + * Synthesize keypress events for each character on the given element + * + * @param {string} aText + * The text to send as single keypress events + * @param {object} aModifiers + * Information about the modifier keys to send + * Elements: accelKey - Hold down the accelerator key (ctrl/meta) + * [optional - default: false] + * altKey - Hold down the alt key + * [optional - default: false] + * ctrlKey - Hold down the ctrl key + * [optional - default: false] + * metaKey - Hold down the meta key (command key on Mac) + * [optional - default: false] + * shiftKey - Hold down the shift key + * [optional - default: false] + * @param {object} aExpectedEvent + * Information about the expected event to occur + * Elements: target - Element which should receive the event + * [optional - default: current element] + * type - Type of the expected key event + * @return {boolean} Success state + */ + value : function MMTB_sendKeys(aText, aModifiers, aExpectedEvent) { + if (!this.element) { + throw new Error("could not find element " + this.getInfo()); + } + + var element = this.element; + Array.forEach(aText, function (letter) { + var win = element.ownerDocument ? element.ownerDocument.defaultView + : element; + element.focus(); + + if (aExpectedEvent) { + if (!aExpectedEvent.type) { + throw new Error(arguments.callee.name + ": Expected event type not specified"); + } + + var target = aExpectedEvent.target ? aExpectedEvent.target.getNode() + : element; + EventUtils.synthesizeKeyExpectEvent(letter, aModifiers || {}, target, + aExpectedEvent.type, + "MozMillTextBox.sendKeys()", win); + } else { + EventUtils.synthesizeKey(letter, aModifiers || {}, win); + } + }); + + broker.pass({'function':'MozMillTextBox.type()'}); + + return true; + } + } +}); + + +/** + * Returns true if node is of type MozMillTextBox + * + * @static + * @param {DOMNode} node Node to check for its type + * @return {boolean} True if node is of type textbox + */ +MozMillTextBox.isType = function MMR_isType(node) { + return ((node.localName.toLowerCase() == 'input' && + (node.getAttribute('type') == 'text' || node.getAttribute('type') == 'search')) || + (node.localName.toLowerCase() == 'textarea') || + (node.localName.toLowerCase() == 'textbox')); +}; diff --git a/services/sync/tps/extensions/mozmill/resource/driver/mozmill.js b/services/sync/tps/extensions/mozmill/resource/driver/mozmill.js new file mode 100644 index 000000000..1e422591f --- /dev/null +++ b/services/sync/tps/extensions/mozmill/resource/driver/mozmill.js @@ -0,0 +1,285 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +var EXPORTED_SYMBOLS = ["controller", "utils", "elementslib", "os", + "getBrowserController", "newBrowserController", + "getAddonsController", "getPreferencesController", + "newMail3PaneController", "getMail3PaneController", + "wm", "platform", "getAddrbkController", + "getMsgComposeController", "getDownloadsController", + "Application", "findElement", + "getPlacesController", 'isMac', 'isLinux', 'isWindows', + "firePythonCallback", "getAddons" + ]; + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; + + +Cu.import("resource://gre/modules/AddonManager.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +// imports +var assertions = {}; Cu.import('resource://mozmill/modules/assertions.js', assertions); +var broker = {}; Cu.import('resource://mozmill/driver/msgbroker.js', broker); +var controller = {}; Cu.import('resource://mozmill/driver/controller.js', controller); +var elementslib = {}; Cu.import('resource://mozmill/driver/elementslib.js', elementslib); +var findElement = {}; Cu.import('resource://mozmill/driver/mozelement.js', findElement); +var os = {}; Cu.import('resource://mozmill/stdlib/os.js', os); +var utils = {}; Cu.import('resource://mozmill/stdlib/utils.js', utils); +var windows = {}; Cu.import('resource://mozmill/modules/windows.js', windows); + + +const DEBUG = false; + +// This is a useful "check" timer. See utils.js, good for debugging +if (DEBUG) { + utils.startTimer(); +} + +var assert = new assertions.Assert(); + +// platform information +var platform = os.getPlatform(); +var isMac = false; +var isWindows = false; +var isLinux = false; + +if (platform == "darwin"){ + isMac = true; +} + +if (platform == "winnt"){ + isWindows = true; +} + +if (platform == "linux"){ + isLinux = true; +} + +var wm = Services.wm; + +var appInfo = Services.appinfo; +var Application = utils.applicationName; + + +/** + * Retrieves the list with information about installed add-ons. + * + * @returns {String} JSON data of installed add-ons + */ +function getAddons() { + var addons = null; + + AddonManager.getAllAddons(function (addonList) { + var tmp_list = [ ]; + + addonList.forEach(function (addon) { + var tmp = { }; + + // We have to filter out properties of type 'function' of the addon + // object, which will break JSON.stringify() and result in incomplete + // addon information. + for (var key in addon) { + if (typeof(addon[key]) !== "function") { + tmp[key] = addon[key]; + } + } + + tmp_list.push(tmp); + }); + + addons = tmp_list; + }); + + try { + // Sychronize with getAllAddons so we do not return too early + assert.waitFor(function () { + return !!addons; + }) + + return addons; + } catch (e) { + return null; + } +} + +/** + * Retrieves application details for the Mozmill report + * + * @return {String} JSON data of application details + */ +function getApplicationDetails() { + var locale = Cc["@mozilla.org/chrome/chrome-registry;1"] + .getService(Ci.nsIXULChromeRegistry) + .getSelectedLocale("global"); + + // Put all our necessary information into JSON and return it: + // appinfo, startupinfo, and addons + var details = { + application_id: appInfo.ID, + application_name: Application, + application_version: appInfo.version, + application_locale: locale, + platform_buildid: appInfo.platformBuildID, + platform_version: appInfo.platformVersion, + addons: getAddons(), + startupinfo: getStartupInfo(), + paths: { + appdata: Services.dirsvc.get('UAppData', Ci.nsIFile).path, + profile: Services.dirsvc.get('ProfD', Ci.nsIFile).path + } + }; + + return JSON.stringify(details); +} + +// get startup time if available +// see http://blog.mozilla.com/tglek/2011/04/26/measuring-startup-speed-correctly/ +function getStartupInfo() { + var startupInfo = {}; + + try { + var _startupInfo = Services.startup.getStartupInfo(); + for (var time in _startupInfo) { + // convert from Date object to ms since epoch + startupInfo[time] = _startupInfo[time].getTime(); + } + } catch (e) { + startupInfo = null; + } + + return startupInfo; +} + + + +function newBrowserController () { + return new controller.MozMillController(utils.getMethodInWindows('OpenBrowserWindow')()); +} + +function getBrowserController () { + var browserWindow = wm.getMostRecentWindow("navigator:browser"); + + if (browserWindow == null) { + return newBrowserController(); + } else { + return new controller.MozMillController(browserWindow); + } +} + +function getPlacesController () { + utils.getMethodInWindows('PlacesCommandHook').showPlacesOrganizer('AllBookmarks'); + + return new controller.MozMillController(wm.getMostRecentWindow('')); +} + +function getAddonsController () { + if (Application == 'SeaMonkey') { + utils.getMethodInWindows('toEM')(); + } + else if (Application == 'Thunderbird') { + utils.getMethodInWindows('openAddonsMgr')(); + } + else if (Application == 'Sunbird') { + utils.getMethodInWindows('goOpenAddons')(); + } else { + utils.getMethodInWindows('BrowserOpenAddonsMgr')(); + } + + return new controller.MozMillController(wm.getMostRecentWindow('')); +} + +function getDownloadsController() { + utils.getMethodInWindows('BrowserDownloadsUI')(); + + return new controller.MozMillController(wm.getMostRecentWindow('')); +} + +function getPreferencesController() { + if (Application == 'Thunderbird') { + utils.getMethodInWindows('openOptionsDialog')(); + } else { + utils.getMethodInWindows('openPreferences')(); + } + + return new controller.MozMillController(wm.getMostRecentWindow('')); +} + +// Thunderbird functions +function newMail3PaneController () { + return new controller.MozMillController(utils.getMethodInWindows('toMessengerWindow')()); +} + +function getMail3PaneController () { + var mail3PaneWindow = wm.getMostRecentWindow("mail:3pane"); + + if (mail3PaneWindow == null) { + return newMail3PaneController(); + } else { + return new controller.MozMillController(mail3PaneWindow); + } +} + +// Thunderbird - Address book window +function newAddrbkController () { + utils.getMethodInWindows("toAddressBook")(); + utils.sleep(2000); + var addyWin = wm.getMostRecentWindow("mail:addressbook"); + + return new controller.MozMillController(addyWin); +} + +function getAddrbkController () { + var addrbkWindow = wm.getMostRecentWindow("mail:addressbook"); + if (addrbkWindow == null) { + return newAddrbkController(); + } else { + return new controller.MozMillController(addrbkWindow); + } +} + +function firePythonCallback (filename, method, args, kwargs) { + obj = {'filename': filename, 'method': method}; + obj['args'] = args || []; + obj['kwargs'] = kwargs || {}; + + broker.sendMessage("firePythonCallback", obj); +} + +function timer (name) { + this.name = name; + this.timers = {}; + this.actions = []; + + frame.timers.push(this); +} + +timer.prototype.start = function (name) { + this.timers[name].startTime = (new Date).getTime(); +} + +timer.prototype.stop = function (name) { + var t = this.timers[name]; + + t.endTime = (new Date).getTime(); + t.totalTime = (t.endTime - t.startTime); +} + +timer.prototype.end = function () { + frame.events.fireEvent("timer", this); + frame.timers.remove(this); +} + +// Initialization + +/** + * Initialize Mozmill + */ +function initialize() { + windows.init(); +} + +initialize(); diff --git a/services/sync/tps/extensions/mozmill/resource/driver/msgbroker.js b/services/sync/tps/extensions/mozmill/resource/driver/msgbroker.js new file mode 100644 index 000000000..95e431f08 --- /dev/null +++ b/services/sync/tps/extensions/mozmill/resource/driver/msgbroker.js @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +var EXPORTED_SYMBOLS = ['addListener', 'addObject', + 'removeListener', + 'sendMessage', 'log', 'pass', 'fail']; + +var listeners = {}; + +// add a listener for a specific message type +function addListener(msgType, listener) { + if (listeners[msgType] === undefined) { + listeners[msgType] = []; + } + + listeners[msgType].push(listener); +} + +// add each method in an object as a message listener +function addObject(object) { + for (var msgType in object) { + addListener(msgType, object[msgType]); + } +} + +// remove a listener for all message types +function removeListener(listener) { + for (var msgType in listeners) { + for (let i = 0; i < listeners.length; ++i) { + if (listeners[msgType][i] == listener) { + listeners[msgType].splice(i, 1); // remove listener from array + } + } + } +} + +function sendMessage(msgType, obj) { + if (listeners[msgType] === undefined) { + return; + } + + for (let i = 0; i < listeners[msgType].length; ++i) { + listeners[msgType][i](obj); + } +} + +function log(obj) { + sendMessage('log', obj); +} + +function pass(obj) { + sendMessage('pass', obj); +} + +function fail(obj) { + sendMessage('fail', obj); +} |