summaryrefslogtreecommitdiffstats
path: root/services/sync/tps/extensions/mozmill/resource/driver
diff options
context:
space:
mode:
Diffstat (limited to 'services/sync/tps/extensions/mozmill/resource/driver')
-rw-r--r--services/sync/tps/extensions/mozmill/resource/driver/controller.js1141
-rw-r--r--services/sync/tps/extensions/mozmill/resource/driver/elementslib.js537
-rw-r--r--services/sync/tps/extensions/mozmill/resource/driver/mozelement.js1163
-rw-r--r--services/sync/tps/extensions/mozmill/resource/driver/mozmill.js285
-rw-r--r--services/sync/tps/extensions/mozmill/resource/driver/msgbroker.js58
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);
+}