michael@0: /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ts=2 et sw=2 tw=80: */ michael@0: /* Any copyright is dedicated to the Public Domain. michael@0: http://creativecommons.org/publicdomain/zero/1.0/ */ michael@0: michael@0: /*============================================================================= michael@0: Globals michael@0: =============================================================================*/ michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Promise", "resource://gre/modules/Promise.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm"); michael@0: michael@0: /*============================================================================= michael@0: Useful constants michael@0: =============================================================================*/ michael@0: const serverRoot = "http://example.com/browser/metro/"; michael@0: const baseURI = "http://mochi.test:8888/browser/metro/"; michael@0: const chromeRoot = getRootDirectory(gTestPath); michael@0: const kDefaultWait = 2000; michael@0: const kDefaultInterval = 50; michael@0: michael@0: /*============================================================================= michael@0: Load Helpers michael@0: =============================================================================*/ michael@0: michael@0: let splitPath = chromeRoot.split('/'); michael@0: if (!splitPath[splitPath.length-1]) { michael@0: splitPath.pop(); michael@0: } michael@0: // ../mochitest to make sure we're looking for the libs on the right path michael@0: // even for mochiperf tests. michael@0: splitPath.pop(); michael@0: splitPath.push('mochitest'); michael@0: michael@0: const mochitestPath = splitPath.join('/') + '/'; michael@0: michael@0: [ michael@0: "helpers/BookmarksHelper.js", michael@0: "helpers/HistoryHelper.js", michael@0: "helpers/ViewStateHelper.js" michael@0: ].forEach(function(lib) { michael@0: Services.scriptloader.loadSubScript(mochitestPath + lib, this); michael@0: }, this); michael@0: michael@0: /*============================================================================= michael@0: Metro ui helpers michael@0: =============================================================================*/ michael@0: michael@0: function isLandscapeMode() michael@0: { michael@0: return Elements.windowState.getAttribute("viewstate") == "landscape"; michael@0: } michael@0: michael@0: function setDevPixelEqualToPx() michael@0: { michael@0: todo(false, "test depends on devPixelsPerPx set to 1.0 - see bugs 886624 and 859742"); michael@0: SpecialPowers.setCharPref("layout.css.devPixelsPerPx", "1.0"); michael@0: registerCleanupFunction(function () { michael@0: SpecialPowers.clearUserPref("layout.css.devPixelsPerPx"); michael@0: }); michael@0: } michael@0: michael@0: function checkContextUIMenuItemCount(aCount) michael@0: { michael@0: let visibleCount = 0; michael@0: for (let idx = 0; idx < ContextMenuUI.commands.childNodes.length; idx++) { michael@0: if (!ContextMenuUI.commands.childNodes[idx].hidden) michael@0: visibleCount++; michael@0: } michael@0: is(visibleCount, aCount, "command list count"); michael@0: } michael@0: michael@0: function checkContextUIMenuItemVisibility(aVisibleList) michael@0: { michael@0: let errors = 0; michael@0: for (let idx = 0; idx < ContextMenuUI.commands.childNodes.length; idx++) { michael@0: let item = ContextMenuUI.commands.childNodes[idx]; michael@0: if (aVisibleList.indexOf(item.id) != -1 && item.hidden) { michael@0: // item should be visible michael@0: errors++; michael@0: info("should be visible:" + item.id); michael@0: } else if (aVisibleList.indexOf(item.id) == -1 && !item.hidden) { michael@0: // item should be hidden michael@0: errors++; michael@0: info("should be hidden:" + item.id); michael@0: } michael@0: } michael@0: is(errors, 0, "context menu item list visibility"); michael@0: } michael@0: michael@0: function checkMonoclePositionRange(aMonocle, aMinX, aMaxX, aMinY, aMaxY) michael@0: { michael@0: let monocle = null; michael@0: if (aMonocle == "start") michael@0: monocle = SelectionHelperUI._startMark; michael@0: else if (aMonocle == "end") michael@0: monocle = SelectionHelperUI._endMark; michael@0: else if (aMonocle == "caret") michael@0: monocle = SelectionHelperUI._caretMark; michael@0: else michael@0: ok(false, "bad monocle id"); michael@0: michael@0: ok(monocle.xPos > aMinX && monocle.xPos < aMaxX, michael@0: "X position is " + monocle.xPos + ", expected between " + aMinX + " and " + aMaxX); michael@0: ok(monocle.yPos > aMinY && monocle.yPos < aMaxY, michael@0: "Y position is " + monocle.yPos + ", expected between " + aMinY + " and " + aMaxY); michael@0: } michael@0: michael@0: /* michael@0: * showNotification - displays a test notification with the current michael@0: * browser and waits for the noticiation to be fully displayed. michael@0: * michael@0: * Usage: yield showNotification(); michael@0: */ michael@0: function showNotification() michael@0: { michael@0: return Task.spawn(function() { michael@0: let strings = Strings.browser; michael@0: var buttons = [ michael@0: { michael@0: isDefault: false, michael@0: label: strings.GetStringFromName("popupButtonAllowOnce2"), michael@0: accessKey: "", michael@0: callback: function() { } michael@0: }, michael@0: { michael@0: label: strings.GetStringFromName("popupButtonAlwaysAllow3"), michael@0: accessKey: "", michael@0: callback: function() { } michael@0: }, michael@0: { michael@0: label: strings.GetStringFromName("popupButtonNeverWarn3"), michael@0: accessKey: "", michael@0: callback: function() { } michael@0: } michael@0: ]; michael@0: let notificationBox = Browser.getNotificationBox(); michael@0: const priority = notificationBox.PRIORITY_WARNING_MEDIUM; michael@0: let note = notificationBox.appendNotification("test notification", "popup-blocked", michael@0: "chrome://browser/skin/images/infobar-popup.png", michael@0: priority, buttons); michael@0: yield waitForEvent(notificationBox, "transitionend"); michael@0: throw new Task.Result(note); michael@0: }); michael@0: } michael@0: michael@0: function removeNotifications() { michael@0: Browser.getNotificationBox().removeAllNotifications(true); michael@0: } michael@0: michael@0: function getSelection(aElement) { michael@0: if (!aElement) michael@0: return null; michael@0: michael@0: // chrome text edit michael@0: if (aElement instanceof Ci.nsIDOMXULTextBoxElement) { michael@0: return aElement.QueryInterface(Components.interfaces.nsIDOMXULTextBoxElement) michael@0: .editor.selection; michael@0: } michael@0: michael@0: // editable content element michael@0: if (aElement instanceof Ci.nsIDOMNSEditableElement) { michael@0: return aElement.QueryInterface(Ci.nsIDOMNSEditableElement) michael@0: .editor.selection; michael@0: } michael@0: michael@0: // document or window michael@0: if (aElement instanceof HTMLDocument || aElement instanceof Window) { michael@0: return aElement.getSelection(); michael@0: } michael@0: michael@0: // browser michael@0: return aElement.contentWindow.getSelection(); michael@0: } michael@0: michael@0: function getTrimmedSelection(aElement) { michael@0: let sel = getSelection(aElement); michael@0: if (!sel) michael@0: return ""; michael@0: return sel.toString().trim(); michael@0: } michael@0: michael@0: /* michael@0: * clearSelection(aTarget) - clears the current selection in michael@0: * aTarget, shuts down the selection manager and purges all michael@0: * message manager events to insure a reset state for the ui. michael@0: */ michael@0: function clearSelection(aTarget) { michael@0: SelectionHelperUI.closeEditSession(true); michael@0: getSelection(aTarget).removeAllRanges(); michael@0: purgeEventQueue(); michael@0: } michael@0: michael@0: // Hides the tab and context app bar if they are visible michael@0: function hideContextUI() michael@0: { michael@0: purgeEventQueue(); michael@0: michael@0: return Task.spawn(function() { michael@0: if (ContextUI.tabbarVisible) { michael@0: let promise = waitForEvent(Elements.tray, "transitionend", null, Elements.tray); michael@0: if (ContextUI.dismiss()) { michael@0: yield promise; michael@0: } michael@0: } michael@0: michael@0: if (ContextUI.contextAppbarVisible) { michael@0: let promise = waitForEvent(Elements.contextappbar, "transitionend", null, Elements.contextappbar); michael@0: ContextUI.dismissContextAppbar(); michael@0: yield promise; michael@0: } michael@0: }); michael@0: } michael@0: michael@0: function showNavBar() michael@0: { michael@0: if (!ContextUI.navbarVisible) { michael@0: let promise = waitForEvent(Elements.navbar, "transitionend"); michael@0: ContextUI.displayNavbar(); michael@0: return promise; michael@0: } michael@0: return Promise.resolve(null); michael@0: } michael@0: michael@0: function hideNavBar() michael@0: { michael@0: if (ContextUI.navbarVisible) { michael@0: let promise = waitForEvent(Elements.navbar, "transitionend"); michael@0: ContextUI.dismissNavbar(); michael@0: return promise; michael@0: } michael@0: return Promise.resolve(null); michael@0: } michael@0: michael@0: function fireAppBarDisplayEvent() michael@0: { michael@0: let promise = waitForEvent(Elements.tray, "transitionend"); michael@0: let event = document.createEvent("Events"); michael@0: event.initEvent("MozEdgeUICompleted", true, false); michael@0: gWindow.dispatchEvent(event); michael@0: purgeEventQueue(); michael@0: return promise; michael@0: } michael@0: michael@0: /*============================================================================= michael@0: General test helpers michael@0: =============================================================================*/ michael@0: let gOpenedTabs = []; michael@0: michael@0: function loadUriInActiveTab(aUri) michael@0: { michael@0: return Task.spawn(function() { michael@0: let promise = waitForEvent(getBrowser(), "pageshow"); michael@0: BrowserUI.goToURI(aUri); michael@0: yield waitForCondition(function () { michael@0: return getBrowser().currentURI.spec == aUri michael@0: }, "getBrowser().currentURI.spec == " + aUri); michael@0: yield promise; michael@0: }); michael@0: } michael@0: michael@0: function navForward() { michael@0: return Task.spawn(function() { michael@0: let promise = waitForEvent(getBrowser(), "pageshow"); michael@0: EventUtils.synthesizeKey("VK_RIGHT", { altKey: true }, window); michael@0: yield promise; michael@0: }); michael@0: } michael@0: michael@0: function navBackViaNavButton() { michael@0: return Task.spawn(function() { michael@0: let promise = waitForEvent(getBrowser(), "pageshow"); michael@0: let backButton = document.getElementById("overlay-back"); michael@0: sendElementTap(window, backButton); michael@0: yield promise; michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Loads a URL in a new tab asynchronously. michael@0: * michael@0: * Usage: michael@0: * Task.spawn(function() { michael@0: * let tab = yield addTab("http://example.com/"); michael@0: * ok(Browser.selectedTab == tab, "the new tab is selected"); michael@0: * }); michael@0: * michael@0: * @param aUrl the URL to load michael@0: * @returns a task that resolves to the new tab object after the URL is loaded. michael@0: */ michael@0: function addTab(aUrl) { michael@0: return Task.spawn(function() { michael@0: info("Opening "+aUrl+" in a new tab"); michael@0: let tab = Browser.addTab(aUrl, true); michael@0: yield tab.pageShowPromise; michael@0: michael@0: is(tab.browser.currentURI.spec, aUrl, aUrl + " is loaded"); michael@0: michael@0: yield hideContextUI(); michael@0: michael@0: gOpenedTabs.push(tab); michael@0: michael@0: throw new Task.Result(tab); michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Cleans up tabs left open by addTab(). michael@0: * This is being called at runTests() after the test loop. michael@0: */ michael@0: function cleanUpOpenedTabs() { michael@0: let tab; michael@0: while(tab = gOpenedTabs.shift()) { michael@0: cleanupNotificationsForBrowser(tab.browser); michael@0: Browser.closeTab(Browser.getTabFromChrome(tab.chromeTab), { forceClose: true }) michael@0: } michael@0: } michael@0: michael@0: function cleanupNotificationsForBrowser(aBrowser) { michael@0: let notificationBox = Browser.getNotificationBox(aBrowser); michael@0: notificationBox && notificationBox.removeAllNotifications(true); michael@0: } michael@0: michael@0: /** michael@0: * Waits a specified number of miliseconds for a specified event to be michael@0: * fired on a specified element. michael@0: * michael@0: * Usage: michael@0: * let receivedEvent = waitForEvent(element, "eventName"); michael@0: * // Do some processing here that will cause the event to be fired michael@0: * // ... michael@0: * // Now yield until the Promise is fulfilled michael@0: * yield receivedEvent; michael@0: * if (receivedEvent && !(receivedEvent instanceof Error)) { michael@0: * receivedEvent.msg == "eventName"; michael@0: * // ... michael@0: * } michael@0: * michael@0: * @param aSubject the element that should receive the event michael@0: * @param aEventName the event to wait for michael@0: * @param aTimeoutMs the number of miliseconds to wait before giving up michael@0: * @returns a Promise that resolves to the received event, or to an Error michael@0: */ michael@0: function waitForEvent(aSubject, aEventName, aTimeoutMs, aTarget) { michael@0: let eventDeferred = Promise.defer(); michael@0: let timeoutMs = aTimeoutMs || kDefaultWait; michael@0: let stack = new Error().stack; michael@0: let timerID = setTimeout(function wfe_canceller() { michael@0: aSubject.removeEventListener(aEventName, listener); michael@0: eventDeferred.reject( new Error(aEventName+" event timeout at " + stack) ); michael@0: }, timeoutMs); michael@0: michael@0: var listener = function (aEvent) { michael@0: if (aTarget && aTarget !== aEvent.target) michael@0: return; michael@0: michael@0: // stop the timeout clock and resume michael@0: clearTimeout(timerID); michael@0: eventDeferred.resolve(aEvent); michael@0: } michael@0: michael@0: function cleanup(aEventOrError) { michael@0: // unhook listener in case of success or failure michael@0: aSubject.removeEventListener(aEventName, listener); michael@0: return aEventOrError; michael@0: } michael@0: aSubject.addEventListener(aEventName, listener, false); michael@0: return eventDeferred.promise.then(cleanup, cleanup); michael@0: } michael@0: michael@0: /** michael@0: * Wait for an nsIMessageManager IPC message. michael@0: */ michael@0: function waitForMessage(aName, aMessageManager) { michael@0: let deferred = Promise.defer(); michael@0: let manager = aMessageManager || messageManager; michael@0: function listener(aMessage) { michael@0: deferred.resolve(aMessage); michael@0: } michael@0: manager.addMessageListener(aName, listener); michael@0: function cleanup(aEventOrError) { michael@0: manager.removeMessageListener(aName, listener); michael@0: } michael@0: return deferred.promise.then(cleanup, cleanup); michael@0: } michael@0: michael@0: /** michael@0: * Waits a specified number of miliseconds. michael@0: * michael@0: * Usage: michael@0: * let wait = yield waitForMs(2000); michael@0: * ok(wait, "2 seconds should now have elapsed"); michael@0: * michael@0: * @param aMs the number of miliseconds to wait for michael@0: * @returns a Promise that resolves to true after the time has elapsed michael@0: */ michael@0: function waitForMs(aMs) { michael@0: info("Wating for " + aMs + "ms"); michael@0: let deferred = Promise.defer(); michael@0: let startTime = Date.now(); michael@0: setTimeout(done, aMs); michael@0: michael@0: function done() { michael@0: deferred.resolve(true); michael@0: info("waitForMs finished waiting, waited for " michael@0: + (Date.now() - startTime) michael@0: + "ms"); michael@0: } michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Waits a specified number of miliseconds for a supplied callback to michael@0: * return a truthy value. michael@0: * michael@0: * Usage: michael@0: * let success = yield waitForCondition(myTestFunction); michael@0: * if (success && !(success instanceof Error)) { michael@0: * // ... michael@0: * } michael@0: * michael@0: * @param aCondition the callback that must return a truthy value michael@0: * @param aTimeoutMs the number of miliseconds to wait before giving up michael@0: * @param aIntervalMs the number of miliseconds between calls to aCondition michael@0: * @returns a Promise that resolves to true, or to an Error michael@0: */ michael@0: function waitForCondition(aCondition, aTimeoutMs, aIntervalMs) { michael@0: let deferred = Promise.defer(); michael@0: let timeoutMs = aTimeoutMs || kDefaultWait; michael@0: let intervalMs = aIntervalMs || kDefaultInterval; michael@0: let startTime = Date.now(); michael@0: let stack = new Error().stack; michael@0: michael@0: function testCondition() { michael@0: let now = Date.now(); michael@0: if((now - startTime) > timeoutMs) { michael@0: deferred.reject( new Error("Timed out waiting for condition to be true at " + stack) ); michael@0: return; michael@0: } michael@0: michael@0: let condition; michael@0: try { michael@0: condition = aCondition(); michael@0: } catch (e) { michael@0: deferred.reject( new Error("Got exception while attempting to test condition: " + e) ); michael@0: return; michael@0: } michael@0: michael@0: if (condition) { michael@0: deferred.resolve(true); michael@0: } else { michael@0: setTimeout(testCondition, intervalMs); michael@0: } michael@0: } michael@0: michael@0: setTimeout(testCondition, 0); michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * same as waitForCondition but with better test output. michael@0: * michael@0: * @param aCondition the callback that must return a truthy value michael@0: * @param aTestMsg test condition message printed when the test succeeds or michael@0: * fails. Defaults to the stringified version of aCondition. michael@0: * @param aTimeoutMs the number of miliseconds to wait before giving up michael@0: * @param aIntervalMs the number of miliseconds between calls to aCondition michael@0: * @returns a Promise that resolves to true, or to an Error michael@0: */ michael@0: function waitForCondition2(aCondition, aTestMsg, aTimeoutMs, aIntervalMs) { michael@0: let deferred = Promise.defer(); michael@0: let msg = aTestMsg || aCondition; michael@0: let timeoutMs = aTimeoutMs || kDefaultWait; michael@0: let intervalMs = aIntervalMs || kDefaultInterval; michael@0: let startTime = Date.now(); michael@0: michael@0: function testCondition() { michael@0: let now = Date.now(); michael@0: if((now - startTime) > timeoutMs) { michael@0: deferred.reject( new Error("Timed out waiting for " + msg) ); michael@0: return; michael@0: } michael@0: michael@0: let condition; michael@0: try { michael@0: condition = aCondition(); michael@0: } catch (e) { michael@0: deferred.reject( new Error("Got exception while attempting to test '" + msg + "': " + e) ); michael@0: return; michael@0: } michael@0: michael@0: if (condition) { michael@0: ok(true, msg); michael@0: deferred.resolve(true); michael@0: } else { michael@0: setTimeout(testCondition, intervalMs); michael@0: } michael@0: } michael@0: michael@0: setTimeout(testCondition, 0); michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /* michael@0: * Waits for an image in a page to load. Wrapper around waitForCondition. michael@0: * michael@0: * @param aWindow the tab or window that contains the image. michael@0: * @param aImageId the id of the image in the page. michael@0: * @returns a Promise that resolves to true, or to an Error michael@0: */ michael@0: function waitForImageLoad(aWindow, aImageId) { michael@0: let elem = aWindow.document.getElementById(aImageId); michael@0: return waitForCondition(function () { michael@0: let request = elem.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST); michael@0: if (request && (request.imageStatus & request.STATUS_SIZE_AVAILABLE)) michael@0: return true; michael@0: return false; michael@0: }, 5000, 100); michael@0: } michael@0: michael@0: /** michael@0: * Waits a specified number of miliseconds for an observer event. michael@0: * michael@0: * @param aObsEvent the observer event to wait for michael@0: * @param aTimeoutMs the number of miliseconds to wait before giving up michael@0: * @returns a Promise that resolves to true, or to an Error michael@0: */ michael@0: function waitForObserver(aObsEvent, aTimeoutMs, aObsData) { michael@0: try { michael@0: michael@0: let deferred = Promise.defer(); michael@0: let timeoutMs = aTimeoutMs || kDefaultWait; michael@0: let timerID = 0; michael@0: michael@0: var observeWatcher = { michael@0: onEvent: function () { michael@0: clearTimeout(timerID); michael@0: Services.obs.removeObserver(this, aObsEvent); michael@0: deferred.resolve(); michael@0: }, michael@0: michael@0: onError: function () { michael@0: clearTimeout(timerID); michael@0: Services.obs.removeObserver(this, aObsEvent); michael@0: deferred.reject(new Error(aObsEvent + " event timeout")); michael@0: }, michael@0: michael@0: observe: function (aSubject, aTopic, aData) { michael@0: if (aTopic == aObsEvent && michael@0: (!aObsData || (aObsData == aData))) { michael@0: this.onEvent(); michael@0: } michael@0: }, michael@0: michael@0: QueryInterface: function (aIID) { michael@0: if (!aIID.equals(Ci.nsIObserver) && michael@0: !aIID.equals(Ci.nsISupportsWeakReference) && michael@0: !aIID.equals(Ci.nsISupports)) { michael@0: throw Components.results.NS_ERROR_NO_INTERFACE; michael@0: } michael@0: return this; michael@0: }, michael@0: } michael@0: michael@0: timerID = setTimeout(function wfo_canceller() { michael@0: observeWatcher.onError(); michael@0: }, timeoutMs); michael@0: michael@0: Services.obs.addObserver(observeWatcher, aObsEvent, true); michael@0: return deferred.promise; michael@0: michael@0: } catch (ex) { michael@0: info(ex.message); michael@0: } michael@0: } michael@0: michael@0: michael@0: /*============================================================================= michael@0: * Input mode helpers - these helpers notify observers to metro_precise_input michael@0: * and metro_imprecise_input respectively, triggering the same behaviour as user touch or mouse input michael@0: * michael@0: * Usage: let promise = waitForObservers("metro_imprecise_input"); michael@0: * notifyImprecise(); michael@0: * yield promise; // you are now in imprecise mode michael@0: *===========================================================================*/ michael@0: function notifyPrecise() michael@0: { michael@0: Services.obs.notifyObservers(null, "metro_precise_input", null); michael@0: } michael@0: michael@0: function notifyImprecise() michael@0: { michael@0: Services.obs.notifyObservers(null, "metro_imprecise_input", null); michael@0: } michael@0: michael@0: /*============================================================================= michael@0: * Native input helpers - these helpers send input directly to the os michael@0: * generating os level input events that get processed by widget and michael@0: * apzc logic. michael@0: *===========================================================================*/ michael@0: function synthesizeNativeMouse(aElement, aOffsetX, aOffsetY, aMsg) { michael@0: let x = aOffsetX; michael@0: let y = aOffsetY; michael@0: if (aElement) { michael@0: if (aElement.getBoundingClientRect) { michael@0: let rect = aElement.getBoundingClientRect(); michael@0: x += rect.left; michael@0: y += rect.top; michael@0: } else if(aElement.left && aElement.top) { michael@0: x += aElement.left; michael@0: y += aElement.top; michael@0: } michael@0: } michael@0: Browser.windowUtils.sendNativeMouseEvent(x, y, aMsg, 0, null); michael@0: } michael@0: michael@0: function synthesizeNativeMouseMove(aElement, aOffsetX, aOffsetY) { michael@0: synthesizeNativeMouse(aElement, michael@0: aOffsetX, michael@0: aOffsetY, michael@0: 0x0001); // MOUSEEVENTF_MOVE michael@0: } michael@0: michael@0: function synthesizeNativeMouseLDown(aElement, aOffsetX, aOffsetY) { michael@0: synthesizeNativeMouse(aElement, michael@0: aOffsetX, michael@0: aOffsetY, michael@0: 0x0002); // MOUSEEVENTF_LEFTDOWN michael@0: } michael@0: michael@0: function synthesizeNativeMouseLUp(aElement, aOffsetX, aOffsetY) { michael@0: synthesizeNativeMouse(aElement, michael@0: aOffsetX, michael@0: aOffsetY, michael@0: 0x0004); // MOUSEEVENTF_LEFTUP michael@0: } michael@0: michael@0: function synthesizeNativeMouseRDown(aElement, aOffsetX, aOffsetY) { michael@0: synthesizeNativeMouse(aElement, michael@0: aOffsetX, michael@0: aOffsetY, michael@0: 0x0008); // MOUSEEVENTF_RIGHTDOWN michael@0: } michael@0: michael@0: function synthesizeNativeMouseRUp(aElement, aOffsetX, aOffsetY) { michael@0: synthesizeNativeMouse(aElement, michael@0: aOffsetX, michael@0: aOffsetY, michael@0: 0x0010); // MOUSEEVENTF_RIGHTUP michael@0: } michael@0: michael@0: function synthesizeNativeMouseMDown(aElement, aOffsetX, aOffsetY) { michael@0: synthesizeNativeMouse(aElement, michael@0: aOffsetX, michael@0: aOffsetY, michael@0: 0x0020); // MOUSEEVENTF_MIDDLEDOWN michael@0: } michael@0: michael@0: function synthesizeNativeMouseMUp(aElement, aOffsetX, aOffsetY) { michael@0: synthesizeNativeMouse(aElement, michael@0: aOffsetX, michael@0: aOffsetY, michael@0: 0x0040); // MOUSEEVENTF_MIDDLEUP michael@0: } michael@0: michael@0: // WARNING: these calls can trigger the soft keyboard on tablets, but not michael@0: // on test slaves (bug 947428). michael@0: // WARNING: When testing the apzc, be careful of bug 933990. Events sent michael@0: // shortly after loading a page may get ignored. michael@0: michael@0: function sendNativeLongTap(aElement, aX, aY) { michael@0: let coords = logicalCoordsForElement(aElement, aX, aY); michael@0: Browser.windowUtils.sendNativeTouchTap(coords.x, coords.y, true); michael@0: } michael@0: michael@0: function sendNativeTap(aElement, aX, aY) { michael@0: let coords = logicalCoordsForElement(aElement, aX, aY); michael@0: Browser.windowUtils.sendNativeTouchTap(coords.x, coords.y, false); michael@0: } michael@0: michael@0: function sendNativeDoubleTap(aElement, aX, aY) { michael@0: let coords = logicalCoordsForElement(aElement, aX, aY); michael@0: Browser.windowUtils.sendNativeTouchTap(coords.x, coords.y, false); michael@0: Browser.windowUtils.sendNativeTouchTap(coords.x, coords.y, false); michael@0: } michael@0: michael@0: function clearNativeTouchSequence() { michael@0: Browser.windowUtils.clearNativeTouchSequence(); michael@0: } michael@0: michael@0: /*============================================================================= michael@0: * Synthesized event helpers - these helpers synthesize input events that get michael@0: * dispatched directly to the dom. As such widget and apzc logic is bypassed. michael@0: *===========================================================================*/ michael@0: michael@0: /* michael@0: * logicalCoordsForElement - given coordinates relative to top-left of michael@0: * given element, returns logical coordinates for window. If a non-numeric michael@0: * X or Y value is given, a value for the center of the element in that michael@0: * dimension is used. michael@0: * michael@0: * @param aElement element coordinates are relative to. michael@0: * @param aX, aY relative coordinates. michael@0: */ michael@0: function logicalCoordsForElement (aElement, aX, aY) { michael@0: let coords = { x: null, y: null }; michael@0: let rect = aElement.getBoundingClientRect(); michael@0: michael@0: coords.x = isNaN(aX) ? rect.left + (rect.width / 2) : rect.left + aX; michael@0: coords.y = isNaN(aY) ? rect.top + (rect.height / 2) : rect.top + aY; michael@0: michael@0: return coords; michael@0: } michael@0: michael@0: function sendContextMenuMouseClickToElement(aWindow, aElement, aX, aY) { michael@0: let utils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindowUtils); michael@0: let coords = logicalCoordsForElement(aElement, aX, aY); michael@0: michael@0: utils.sendMouseEventToWindow("mousedown", coords.x, coords.y, 2, 1, 0); michael@0: utils.sendMouseEventToWindow("mouseup", coords.x, coords.y, 2, 1, 0); michael@0: utils.sendMouseEventToWindow("contextmenu", coords.x, coords.y, 2, 1, 0); michael@0: } michael@0: michael@0: function sendMouseClick(aWindow, aX, aY) { michael@0: EventUtils.synthesizeMouseAtPoint(aX, aY, {}, aWindow); michael@0: } michael@0: michael@0: /* michael@0: * sendContextMenuClick - simulates a press-hold touch input event. Event michael@0: * is delivered to the main window of the application through the top-level michael@0: * widget. michael@0: * michael@0: * @param aX, aY logical coordinates of the event. michael@0: */ michael@0: function sendContextMenuClick(aX, aY) { michael@0: let mediator = Components.classes["@mozilla.org/appshell/window-mediator;1"] michael@0: .getService(Components.interfaces.nsIWindowMediator); michael@0: let mainwin = mediator.getMostRecentWindow("navigator:browser"); michael@0: let utils = mainwin.QueryInterface(Components.interfaces.nsIInterfaceRequestor) michael@0: .getInterface(Components.interfaces.nsIDOMWindowUtils); michael@0: utils.sendMouseEvent("contextmenu", aX, aY, 2, 1, 0, true, michael@0: 1, Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH); michael@0: } michael@0: michael@0: /* michael@0: * sendContextMenuClickToSelection - simulates a press-hold touch input event michael@0: * selected text in a window. michael@0: */ michael@0: function sendContextMenuClickToSelection(aWindow) { michael@0: let selection = aWindow.getSelection(); michael@0: if (!selection || !selection.rangeCount) { michael@0: ok(false, "no selection to tap!"); michael@0: return; michael@0: } michael@0: let range = selection.getRangeAt(0); michael@0: let rect = range.getBoundingClientRect(); michael@0: let x = rect.left + (rect.width / 2); michael@0: let y = rect.top + (rect.height / 2); michael@0: let utils = aWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor) michael@0: .getInterface(Components.interfaces.nsIDOMWindowUtils); michael@0: utils.sendMouseEventToWindow("contextmenu", x, y, 2, 1, 0, true, michael@0: 1, Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH); michael@0: } michael@0: michael@0: /* michael@0: * sendContextMenuClickToWindow - simulates a press-hold touch input event. michael@0: * michael@0: * @param aWindow window used to retrieve dom window utils, and the michael@0: * target window for the event. michael@0: * @param aX, aY logical coordinates of the event relative to aWindow. michael@0: */ michael@0: function sendContextMenuClickToWindow(aWindow, aX, aY) { michael@0: let utils = aWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor) michael@0: .getInterface(Components.interfaces.nsIDOMWindowUtils); michael@0: michael@0: utils.sendMouseEventToWindow("contextmenu", aX, aY, 2, 1, 0, true, michael@0: 1, Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH); michael@0: } michael@0: michael@0: function sendContextMenuClickToElement(aWindow, aElement, aX, aY) { michael@0: let utils = aWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor) michael@0: .getInterface(Components.interfaces.nsIDOMWindowUtils); michael@0: let coords = logicalCoordsForElement(aElement, aX, aY); michael@0: utils.sendMouseEventToWindow("contextmenu", coords.x, coords.y, 2, 1, 0, true, michael@0: 1, Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH); michael@0: } michael@0: michael@0: /* michael@0: * sendDoubleTap - simulates a double click or double tap. michael@0: */ michael@0: function sendDoubleTap(aWindow, aX, aY) { michael@0: EventUtils.synthesizeMouseAtPoint(aX, aY, { michael@0: clickCount: 1, michael@0: inputSource: Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH michael@0: }, aWindow); michael@0: michael@0: EventUtils.synthesizeMouseAtPoint(aX, aY, { michael@0: clickCount: 2, michael@0: inputSource: Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH michael@0: }, aWindow); michael@0: } michael@0: michael@0: function sendTap(aWindow, aX, aY) { michael@0: EventUtils.synthesizeMouseAtPoint(aX, aY, { michael@0: clickCount: 1, michael@0: inputSource: Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH michael@0: }, aWindow); michael@0: } michael@0: michael@0: function sendElementTap(aWindow, aElement, aX, aY) { michael@0: let coords = logicalCoordsForElement(aElement, aX, aY); michael@0: EventUtils.synthesizeMouseAtPoint(coords.x, coords.y, { michael@0: clickCount: 1, michael@0: inputSource: Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH michael@0: }, aWindow); michael@0: } michael@0: michael@0: /* michael@0: * sendTouchDrag - sends a touch series composed of a touchstart, michael@0: * touchmove, and touchend w3c event. michael@0: */ michael@0: function sendTouchDrag(aWindow, aStartX, aStartY, aEndX, aEndY) { michael@0: EventUtils.synthesizeTouchAtPoint(aStartX, aStartY, { type: "touchstart" }, aWindow); michael@0: EventUtils.synthesizeTouchAtPoint(aEndX, aEndY, { type: "touchmove" }, aWindow); michael@0: EventUtils.synthesizeTouchAtPoint(aEndX, aEndY, { type: "touchend" }, aWindow); michael@0: } michael@0: michael@0: /* michael@0: * TouchDragAndHold - simulates a drag and hold sequence of events. michael@0: */ michael@0: function TouchDragAndHold() { michael@0: } michael@0: michael@0: TouchDragAndHold.prototype = { michael@0: _timeoutStep: 2, michael@0: _numSteps: 50, michael@0: _debug: false, michael@0: _win: null, michael@0: _native: false, michael@0: _pointerId: 1, michael@0: _dui: Components.interfaces.nsIDOMWindowUtils, michael@0: michael@0: set useNativeEvents(aValue) { michael@0: this._native = aValue; michael@0: }, michael@0: michael@0: set stepTimeout(aValue) { michael@0: this._timeoutStep = aValue; michael@0: }, michael@0: michael@0: set numSteps(aValue) { michael@0: this._numSteps = aValue; michael@0: }, michael@0: michael@0: set nativePointerId(aValue) { michael@0: this._pointerId = aValue; michael@0: }, michael@0: michael@0: callback: function callback() { michael@0: if (this._win == null) michael@0: return; michael@0: michael@0: if (this._debug) { michael@0: SelectionHelperUI.debugDisplayDebugPoint(this._currentPoint.xPos, michael@0: this._currentPoint.yPos, 5, "#FF0000", true); michael@0: } michael@0: michael@0: if (++this._step.steps >= this._numSteps) { michael@0: if (this._native) { michael@0: this._utils.sendNativeTouchPoint(this._pointerId, this._dui.TOUCH_CONTACT, michael@0: this._endPoint.xPos, this._endPoint.yPos, michael@0: 1, 90); michael@0: } else { michael@0: EventUtils.synthesizeTouchAtPoint(this._endPoint.xPos, this._endPoint.yPos, michael@0: { type: "touchmove" }, this._win); michael@0: } michael@0: this._defer.resolve(); michael@0: return; michael@0: } michael@0: this._currentPoint.xPos += this._step.x; michael@0: this._currentPoint.yPos += this._step.y; michael@0: if (this._debug) { michael@0: info("[" + this._step.steps + "] touchmove " + this._currentPoint.xPos + " x " + this._currentPoint.yPos); michael@0: } michael@0: michael@0: if (this._native) { michael@0: this._utils.sendNativeTouchPoint(this._pointerId, this._dui.TOUCH_CONTACT, michael@0: this._currentPoint.xPos, this._currentPoint.yPos, michael@0: 1, 90); michael@0: } else { michael@0: EventUtils.synthesizeTouchAtPoint(this._currentPoint.xPos, this._currentPoint.yPos, michael@0: { type: "touchmove" }, this._win); michael@0: } michael@0: michael@0: let self = this; michael@0: setTimeout(function () { self.callback(); }, this._timeoutStep); michael@0: }, michael@0: michael@0: start: function start(aWindow, aStartX, aStartY, aEndX, aEndY) { michael@0: this._defer = Promise.defer(); michael@0: this._win = aWindow; michael@0: this._utils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindowUtils); michael@0: this._endPoint = { xPos: aEndX, yPos: aEndY }; michael@0: this._currentPoint = { xPos: aStartX, yPos: aStartY }; michael@0: this._step = { steps: 0, x: (aEndX - aStartX) / this._numSteps, y: (aEndY - aStartY) / this._numSteps }; michael@0: if (this._debug) { michael@0: info("[0] touchstart " + aStartX + " x " + aStartY); michael@0: } michael@0: // flush layout, bug 914847 michael@0: this._utils.elementFromPoint(aStartX, aStartY, false, true); michael@0: if (this._native) { michael@0: this._utils.sendNativeTouchPoint(this._pointerId, this._dui.TOUCH_CONTACT, michael@0: aStartX, aStartY, 1, 90); michael@0: } else { michael@0: EventUtils.synthesizeTouchAtPoint(aStartX, aStartY, { type: "touchstart" }, aWindow); michael@0: } michael@0: let self = this; michael@0: setTimeout(function () { self.callback(); }, this._timeoutStep); michael@0: return this._defer.promise; michael@0: }, michael@0: michael@0: move: function move(aEndX, aEndY) { michael@0: if (this._win == null) michael@0: return; michael@0: if (this._debug) { michael@0: info("[0] continuation to " + aEndX + " x " + aEndY); michael@0: } michael@0: this._defer = Promise.defer(); michael@0: this._step = { steps: 0, michael@0: x: (aEndX - this._endPoint.xPos) / this._numSteps, michael@0: y: (aEndY - this._endPoint.yPos) / this._numSteps }; michael@0: this._endPoint = { xPos: aEndX, yPos: aEndY }; michael@0: let self = this; michael@0: setTimeout(function () { self.callback(); }, this._timeoutStep); michael@0: return this._defer.promise; michael@0: }, michael@0: michael@0: end: function end() { michael@0: if (this._debug) { michael@0: info("[" + this._step.steps + "] touchend " + this._endPoint.xPos + " x " + this._endPoint.yPos); michael@0: SelectionHelperUI.debugClearDebugPoints(); michael@0: } michael@0: if (this._native) { michael@0: this._utils.sendNativeTouchPoint(this._pointerId, this._dui.TOUCH_REMOVE, michael@0: this._endPoint.xPos, this._endPoint.yPos, michael@0: 1, 90); michael@0: } else { michael@0: EventUtils.synthesizeTouchAtPoint(this._endPoint.xPos, this._endPoint.yPos, michael@0: { type: "touchend" }, this._win); michael@0: } michael@0: this._win = null; michael@0: }, michael@0: }; michael@0: michael@0: /*============================================================================= michael@0: System utilities michael@0: =============================================================================*/ michael@0: michael@0: /* michael@0: * emptyClipboard - clear the windows clipboard. michael@0: */ michael@0: function emptyClipboard() { michael@0: Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard) michael@0: .emptyClipboard(Ci.nsIClipboard.kGlobalClipboard); michael@0: } michael@0: michael@0: /* michael@0: * purgeEventQueue - purges the event queue on the calling thread. michael@0: * Pumps latent in-process message manager events awaiting delivery. michael@0: */ michael@0: function purgeEventQueue() { michael@0: let thread = Services.tm.currentThread; michael@0: while (thread.hasPendingEvents()) { michael@0: if (!thread.processNextEvent(true)) michael@0: break; michael@0: } michael@0: } michael@0: michael@0: /*============================================================================= michael@0: Test-running helpers michael@0: =============================================================================*/ michael@0: let gCurrentTest = null; michael@0: let gTests = []; michael@0: michael@0: function runTests() { michael@0: waitForExplicitFinish(); michael@0: michael@0: Task.spawn(function() { michael@0: while((gCurrentTest = gTests.shift())){ michael@0: try { michael@0: if ('function' == typeof gCurrentTest.setUp) { michael@0: info("SETUP " + gCurrentTest.desc); michael@0: yield Task.spawn(gCurrentTest.setUp.bind(gCurrentTest)); michael@0: } michael@0: try { michael@0: info("RUN " + gCurrentTest.desc); michael@0: yield Task.spawn(gCurrentTest.run.bind(gCurrentTest)); michael@0: } finally { michael@0: if ('function' == typeof gCurrentTest.tearDown) { michael@0: info("TEARDOWN " + gCurrentTest.desc); michael@0: yield Task.spawn(gCurrentTest.tearDown.bind(gCurrentTest)); michael@0: } michael@0: } michael@0: } catch (ex) { michael@0: ok(false, "runTests: Task failed - " + ex + ' at ' + ex.stack); michael@0: } finally { michael@0: info("END " + gCurrentTest.desc); michael@0: } michael@0: } michael@0: michael@0: try { michael@0: cleanUpOpenedTabs(); michael@0: michael@0: let badTabs = []; michael@0: Browser.tabs.forEach(function(item, index, array) { michael@0: let location = item.browser.currentURI.spec; michael@0: if (index == 0 && location == "about:blank" || location == "about:start") { michael@0: return; michael@0: } michael@0: ok(false, "Left over tab after test: '" + location + "'"); michael@0: badTabs.push(item); michael@0: }); michael@0: michael@0: badTabs.forEach(function(item, index, array) { michael@0: Browser.closeTab(item, { forceClose: true }); michael@0: }); michael@0: } catch (ex) { michael@0: ok(false, "Cleanup tabs failed - " + ex); michael@0: } michael@0: michael@0: finish(); michael@0: }); michael@0: } michael@0: michael@0: // wrap a method with a spy that records how and how many times it gets called michael@0: // the spy is returned; use spy.restore() to put the original back michael@0: function spyOnMethod(aObj, aMethod) { michael@0: let origFunc = aObj[aMethod]; michael@0: let spy = function() { michael@0: let callArguments = Array.slice(arguments); michael@0: spy.callCount++; michael@0: spy.calledWith = callArguments; michael@0: spy.argsForCall.push(callArguments); michael@0: return (spy.returnValue = origFunc.apply(aObj, arguments)); michael@0: }; michael@0: spy.callCount = 0; michael@0: spy.argsForCall = []; michael@0: spy.restore = function() { michael@0: return (aObj[aMethod] = origFunc); michael@0: }; michael@0: return (aObj[aMethod] = spy); michael@0: } michael@0: michael@0: // replace a method with a stub that records how and how many times it gets called michael@0: // the stub is returned; use stub.restore() to put the original back michael@0: function stubMethod(aObj, aMethod) { michael@0: let origFunc = aObj[aMethod]; michael@0: let func = function() { michael@0: func.calledWith = Array.slice(arguments); michael@0: func.callCount++; michael@0: }; michael@0: func.callCount = 0; michael@0: func.restore = function() { michael@0: return (aObj[aMethod] = origFunc); michael@0: }; michael@0: aObj[aMethod] = func; michael@0: return func; michael@0: }