browser/metro/base/tests/mochitest/head.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/browser/metro/base/tests/mochitest/head.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1070 @@
     1.4 +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     1.5 +/* vim: set ts=2 et sw=2 tw=80: */
     1.6 +/* Any copyright is dedicated to the Public Domain.
     1.7 +   http://creativecommons.org/publicdomain/zero/1.0/ */
     1.8 +
     1.9 +/*=============================================================================
    1.10 +  Globals
    1.11 +=============================================================================*/
    1.12 +XPCOMUtils.defineLazyModuleGetter(this, "Promise", "resource://gre/modules/Promise.jsm");
    1.13 +XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
    1.14 +
    1.15 +/*=============================================================================
    1.16 +  Useful constants
    1.17 +=============================================================================*/
    1.18 +const serverRoot = "http://example.com/browser/metro/";
    1.19 +const baseURI = "http://mochi.test:8888/browser/metro/";
    1.20 +const chromeRoot = getRootDirectory(gTestPath);
    1.21 +const kDefaultWait = 2000;
    1.22 +const kDefaultInterval = 50;
    1.23 +
    1.24 +/*=============================================================================
    1.25 +  Load Helpers
    1.26 +=============================================================================*/
    1.27 +
    1.28 +let splitPath = chromeRoot.split('/');
    1.29 +if (!splitPath[splitPath.length-1]) {
    1.30 +  splitPath.pop();
    1.31 +}
    1.32 +// ../mochitest to make sure we're looking for the libs on the right path
    1.33 +// even for mochiperf tests.
    1.34 +splitPath.pop();
    1.35 +splitPath.push('mochitest');
    1.36 +
    1.37 +const mochitestPath = splitPath.join('/') + '/';
    1.38 +
    1.39 +[
    1.40 +  "helpers/BookmarksHelper.js",
    1.41 +  "helpers/HistoryHelper.js",
    1.42 +  "helpers/ViewStateHelper.js"
    1.43 +].forEach(function(lib) {
    1.44 +  Services.scriptloader.loadSubScript(mochitestPath + lib, this);
    1.45 +}, this);
    1.46 +
    1.47 +/*=============================================================================
    1.48 +  Metro ui helpers
    1.49 +=============================================================================*/
    1.50 +
    1.51 +function isLandscapeMode()
    1.52 +{
    1.53 +  return Elements.windowState.getAttribute("viewstate") == "landscape";
    1.54 +}
    1.55 +
    1.56 +function setDevPixelEqualToPx()
    1.57 +{
    1.58 +  todo(false, "test depends on devPixelsPerPx set to 1.0 - see bugs 886624 and 859742");
    1.59 +  SpecialPowers.setCharPref("layout.css.devPixelsPerPx", "1.0");
    1.60 +  registerCleanupFunction(function () {
    1.61 +    SpecialPowers.clearUserPref("layout.css.devPixelsPerPx");
    1.62 +  });
    1.63 +}
    1.64 +
    1.65 +function checkContextUIMenuItemCount(aCount)
    1.66 +{
    1.67 +  let visibleCount = 0;
    1.68 +  for (let idx = 0; idx < ContextMenuUI.commands.childNodes.length; idx++) {
    1.69 +    if (!ContextMenuUI.commands.childNodes[idx].hidden)
    1.70 +      visibleCount++;
    1.71 +  }
    1.72 +  is(visibleCount, aCount, "command list count");
    1.73 +}
    1.74 +
    1.75 +function checkContextUIMenuItemVisibility(aVisibleList)
    1.76 +{
    1.77 +  let errors = 0;
    1.78 +  for (let idx = 0; idx < ContextMenuUI.commands.childNodes.length; idx++) {
    1.79 +    let item = ContextMenuUI.commands.childNodes[idx];
    1.80 +    if (aVisibleList.indexOf(item.id) != -1 && item.hidden) {
    1.81 +      // item should be visible
    1.82 +      errors++;
    1.83 +      info("should be visible:" + item.id);
    1.84 +    } else if (aVisibleList.indexOf(item.id) == -1 && !item.hidden) {
    1.85 +      // item should be hidden
    1.86 +      errors++;
    1.87 +      info("should be hidden:" + item.id);
    1.88 +    }
    1.89 +  }
    1.90 +  is(errors, 0, "context menu item list visibility");
    1.91 +}
    1.92 +
    1.93 +function checkMonoclePositionRange(aMonocle, aMinX, aMaxX, aMinY, aMaxY)
    1.94 +{
    1.95 +  let monocle = null;
    1.96 +  if (aMonocle == "start")
    1.97 +    monocle = SelectionHelperUI._startMark;
    1.98 +  else if (aMonocle == "end")
    1.99 +    monocle = SelectionHelperUI._endMark;
   1.100 +  else if (aMonocle == "caret")
   1.101 +    monocle = SelectionHelperUI._caretMark;
   1.102 +  else
   1.103 +    ok(false, "bad monocle id");
   1.104 +
   1.105 +  ok(monocle.xPos > aMinX && monocle.xPos < aMaxX,
   1.106 +    "X position is " + monocle.xPos + ", expected between " + aMinX + " and " + aMaxX);
   1.107 +  ok(monocle.yPos > aMinY && monocle.yPos < aMaxY,
   1.108 +    "Y position is " + monocle.yPos + ", expected between " + aMinY + " and " + aMaxY);
   1.109 +}
   1.110 +
   1.111 +/*
   1.112 + * showNotification - displays a test notification with the current
   1.113 + * browser and waits for the noticiation to be fully displayed.
   1.114 + *
   1.115 + * Usage: yield showNotification();
   1.116 + */
   1.117 +function showNotification()
   1.118 +{
   1.119 +  return Task.spawn(function() {
   1.120 +    let strings = Strings.browser;
   1.121 +    var buttons = [
   1.122 +      {
   1.123 +        isDefault: false,
   1.124 +        label: strings.GetStringFromName("popupButtonAllowOnce2"),
   1.125 +        accessKey: "",
   1.126 +        callback: function() { }
   1.127 +      },
   1.128 +      {
   1.129 +        label: strings.GetStringFromName("popupButtonAlwaysAllow3"),
   1.130 +        accessKey: "",
   1.131 +        callback: function() { }
   1.132 +      },
   1.133 +      {
   1.134 +        label: strings.GetStringFromName("popupButtonNeverWarn3"),
   1.135 +        accessKey: "",
   1.136 +        callback: function() { }
   1.137 +      }
   1.138 +    ];
   1.139 +    let notificationBox = Browser.getNotificationBox();
   1.140 +    const priority = notificationBox.PRIORITY_WARNING_MEDIUM;
   1.141 +    let note = notificationBox.appendNotification("test notification", "popup-blocked",
   1.142 +                                                  "chrome://browser/skin/images/infobar-popup.png",
   1.143 +                                                  priority, buttons);
   1.144 +    yield waitForEvent(notificationBox, "transitionend");
   1.145 +    throw new Task.Result(note);
   1.146 +  });
   1.147 +}
   1.148 +
   1.149 +function removeNotifications() {
   1.150 +  Browser.getNotificationBox().removeAllNotifications(true);
   1.151 +}
   1.152 +
   1.153 +function getSelection(aElement) {
   1.154 +  if (!aElement)
   1.155 +    return null;
   1.156 +
   1.157 +  // chrome text edit
   1.158 +  if (aElement instanceof Ci.nsIDOMXULTextBoxElement) {
   1.159 +    return aElement.QueryInterface(Components.interfaces.nsIDOMXULTextBoxElement)
   1.160 +                   .editor.selection;
   1.161 +  }
   1.162 +
   1.163 +  // editable content element
   1.164 +  if (aElement instanceof Ci.nsIDOMNSEditableElement) {
   1.165 +    return aElement.QueryInterface(Ci.nsIDOMNSEditableElement)
   1.166 +                   .editor.selection;
   1.167 +  }
   1.168 +
   1.169 +  // document or window
   1.170 +  if (aElement instanceof HTMLDocument || aElement instanceof Window) {
   1.171 +    return aElement.getSelection();
   1.172 +  }
   1.173 +
   1.174 +  // browser
   1.175 +  return aElement.contentWindow.getSelection();
   1.176 +}
   1.177 +
   1.178 +function getTrimmedSelection(aElement) {
   1.179 +  let sel = getSelection(aElement);
   1.180 +  if (!sel)
   1.181 +    return "";
   1.182 +  return sel.toString().trim();
   1.183 +}
   1.184 +
   1.185 +/*
   1.186 + * clearSelection(aTarget) - clears the current selection in
   1.187 + * aTarget, shuts down the selection manager and purges all
   1.188 + * message manager events to insure a reset state for the ui.
   1.189 + */
   1.190 +function clearSelection(aTarget) {
   1.191 +  SelectionHelperUI.closeEditSession(true);
   1.192 +  getSelection(aTarget).removeAllRanges();
   1.193 +  purgeEventQueue();
   1.194 +}
   1.195 +
   1.196 +// Hides the tab and context app bar if they are visible
   1.197 +function hideContextUI()
   1.198 +{
   1.199 +  purgeEventQueue();
   1.200 +
   1.201 +  return Task.spawn(function() {
   1.202 +    if (ContextUI.tabbarVisible) {
   1.203 +      let promise = waitForEvent(Elements.tray, "transitionend", null, Elements.tray);
   1.204 +      if (ContextUI.dismiss()) {
   1.205 +        yield promise;
   1.206 +      }
   1.207 +    }
   1.208 +
   1.209 +    if (ContextUI.contextAppbarVisible) {
   1.210 +      let promise = waitForEvent(Elements.contextappbar, "transitionend", null, Elements.contextappbar);
   1.211 +      ContextUI.dismissContextAppbar();
   1.212 +      yield promise;
   1.213 +    }
   1.214 +  });
   1.215 +}
   1.216 +
   1.217 +function showNavBar()
   1.218 +{
   1.219 +  if (!ContextUI.navbarVisible) {
   1.220 +    let promise = waitForEvent(Elements.navbar, "transitionend");
   1.221 +    ContextUI.displayNavbar();
   1.222 +    return promise;
   1.223 +  }
   1.224 +  return Promise.resolve(null);
   1.225 +}
   1.226 +
   1.227 +function hideNavBar()
   1.228 +{
   1.229 +  if (ContextUI.navbarVisible) {
   1.230 +    let promise = waitForEvent(Elements.navbar, "transitionend");
   1.231 +    ContextUI.dismissNavbar();
   1.232 +    return promise;
   1.233 +  }
   1.234 +  return Promise.resolve(null);
   1.235 +}
   1.236 +
   1.237 +function fireAppBarDisplayEvent()
   1.238 +{
   1.239 +  let promise = waitForEvent(Elements.tray, "transitionend");
   1.240 +  let event = document.createEvent("Events");
   1.241 +  event.initEvent("MozEdgeUICompleted", true, false);
   1.242 +  gWindow.dispatchEvent(event);
   1.243 +  purgeEventQueue();
   1.244 +  return promise;
   1.245 +}
   1.246 +
   1.247 +/*=============================================================================
   1.248 +  General test helpers
   1.249 +=============================================================================*/
   1.250 +let gOpenedTabs = [];
   1.251 +
   1.252 +function loadUriInActiveTab(aUri)
   1.253 +{
   1.254 +  return Task.spawn(function() {
   1.255 +    let promise = waitForEvent(getBrowser(), "pageshow");
   1.256 +    BrowserUI.goToURI(aUri);
   1.257 +    yield waitForCondition(function () {
   1.258 +      return getBrowser().currentURI.spec == aUri
   1.259 +    }, "getBrowser().currentURI.spec == " + aUri);
   1.260 +    yield promise;
   1.261 +  });
   1.262 +}
   1.263 +
   1.264 +function navForward() {
   1.265 +  return Task.spawn(function() {
   1.266 +    let promise = waitForEvent(getBrowser(), "pageshow");
   1.267 +    EventUtils.synthesizeKey("VK_RIGHT", { altKey: true }, window);
   1.268 +    yield promise;
   1.269 +  });
   1.270 +}
   1.271 +
   1.272 +function navBackViaNavButton() {
   1.273 +  return Task.spawn(function() {
   1.274 +    let promise = waitForEvent(getBrowser(), "pageshow");
   1.275 +    let backButton = document.getElementById("overlay-back");
   1.276 +    sendElementTap(window, backButton);
   1.277 +    yield promise;
   1.278 +  });
   1.279 +}
   1.280 +
   1.281 +/**
   1.282 + *  Loads a URL in a new tab asynchronously.
   1.283 + *
   1.284 + * Usage:
   1.285 + *    Task.spawn(function() {
   1.286 + *      let tab = yield addTab("http://example.com/");
   1.287 + *      ok(Browser.selectedTab == tab, "the new tab is selected");
   1.288 + *    });
   1.289 + *
   1.290 + * @param aUrl the URL to load
   1.291 + * @returns a task that resolves to the new tab object after the URL is loaded.
   1.292 + */
   1.293 +function addTab(aUrl) {
   1.294 +  return Task.spawn(function() {
   1.295 +    info("Opening "+aUrl+" in a new tab");
   1.296 +    let tab = Browser.addTab(aUrl, true);
   1.297 +    yield tab.pageShowPromise;
   1.298 +
   1.299 +    is(tab.browser.currentURI.spec, aUrl, aUrl + " is loaded");
   1.300 +
   1.301 +    yield hideContextUI();
   1.302 +
   1.303 +    gOpenedTabs.push(tab);
   1.304 +
   1.305 +    throw new Task.Result(tab);
   1.306 +  });
   1.307 +}
   1.308 +
   1.309 +/**
   1.310 + * Cleans up tabs left open by addTab().
   1.311 + * This is being called at runTests() after the test loop.
   1.312 + */
   1.313 +function cleanUpOpenedTabs() {
   1.314 +  let tab;
   1.315 +  while(tab = gOpenedTabs.shift()) {
   1.316 +    cleanupNotificationsForBrowser(tab.browser);
   1.317 +    Browser.closeTab(Browser.getTabFromChrome(tab.chromeTab), { forceClose: true })
   1.318 +  }
   1.319 +}
   1.320 +
   1.321 +function cleanupNotificationsForBrowser(aBrowser) {
   1.322 +  let notificationBox = Browser.getNotificationBox(aBrowser);
   1.323 +  notificationBox && notificationBox.removeAllNotifications(true);
   1.324 +}
   1.325 +
   1.326 +/**
   1.327 + * Waits a specified number of miliseconds for a specified event to be
   1.328 + * fired on a specified element.
   1.329 + *
   1.330 + * Usage:
   1.331 + *    let receivedEvent = waitForEvent(element, "eventName");
   1.332 + *    // Do some processing here that will cause the event to be fired
   1.333 + *    // ...
   1.334 + *    // Now yield until the Promise is fulfilled
   1.335 + *    yield receivedEvent;
   1.336 + *    if (receivedEvent && !(receivedEvent instanceof Error)) {
   1.337 + *      receivedEvent.msg == "eventName";
   1.338 + *      // ...
   1.339 + *    }
   1.340 + *
   1.341 + * @param aSubject the element that should receive the event
   1.342 + * @param aEventName the event to wait for
   1.343 + * @param aTimeoutMs the number of miliseconds to wait before giving up
   1.344 + * @returns a Promise that resolves to the received event, or to an Error
   1.345 + */
   1.346 +function waitForEvent(aSubject, aEventName, aTimeoutMs, aTarget) {
   1.347 +  let eventDeferred = Promise.defer();
   1.348 +  let timeoutMs = aTimeoutMs || kDefaultWait;
   1.349 +  let stack = new Error().stack;
   1.350 +  let timerID = setTimeout(function wfe_canceller() {
   1.351 +    aSubject.removeEventListener(aEventName, listener);
   1.352 +    eventDeferred.reject( new Error(aEventName+" event timeout at " + stack) );
   1.353 +  }, timeoutMs);
   1.354 +
   1.355 +  var listener = function (aEvent) {
   1.356 +    if (aTarget && aTarget !== aEvent.target)
   1.357 +        return;
   1.358 +
   1.359 +    // stop the timeout clock and resume
   1.360 +    clearTimeout(timerID);
   1.361 +    eventDeferred.resolve(aEvent);
   1.362 +  }
   1.363 +
   1.364 +  function cleanup(aEventOrError) {
   1.365 +    // unhook listener in case of success or failure
   1.366 +    aSubject.removeEventListener(aEventName, listener);
   1.367 +    return aEventOrError;
   1.368 +  }
   1.369 +  aSubject.addEventListener(aEventName, listener, false);
   1.370 +  return eventDeferred.promise.then(cleanup, cleanup);
   1.371 +}
   1.372 +
   1.373 +/**
   1.374 + * Wait for an nsIMessageManager IPC message.
   1.375 + */
   1.376 +function waitForMessage(aName, aMessageManager) {
   1.377 +  let deferred = Promise.defer();
   1.378 +  let manager = aMessageManager || messageManager;
   1.379 +  function listener(aMessage) {
   1.380 +    deferred.resolve(aMessage);
   1.381 +  }
   1.382 +  manager.addMessageListener(aName, listener);
   1.383 +  function cleanup(aEventOrError) {
   1.384 +    manager.removeMessageListener(aName, listener);
   1.385 +  }
   1.386 +  return deferred.promise.then(cleanup, cleanup);
   1.387 +}
   1.388 +
   1.389 +/**
   1.390 + * Waits a specified number of miliseconds.
   1.391 + *
   1.392 + * Usage:
   1.393 + *    let wait = yield waitForMs(2000);
   1.394 + *    ok(wait, "2 seconds should now have elapsed");
   1.395 + *
   1.396 + * @param aMs the number of miliseconds to wait for
   1.397 + * @returns a Promise that resolves to true after the time has elapsed
   1.398 + */
   1.399 +function waitForMs(aMs) {
   1.400 +  info("Wating for " + aMs + "ms");
   1.401 +  let deferred = Promise.defer();
   1.402 +  let startTime = Date.now();
   1.403 +  setTimeout(done, aMs);
   1.404 +
   1.405 +  function done() {
   1.406 +    deferred.resolve(true);
   1.407 +    info("waitForMs finished waiting, waited for "
   1.408 +       + (Date.now() - startTime)
   1.409 +       + "ms");
   1.410 +  }
   1.411 +
   1.412 +  return deferred.promise;
   1.413 +}
   1.414 +
   1.415 +/**
   1.416 + * Waits a specified number of miliseconds for a supplied callback to
   1.417 + * return a truthy value.
   1.418 + *
   1.419 + * Usage:
   1.420 + *     let success = yield waitForCondition(myTestFunction);
   1.421 + *     if (success && !(success instanceof Error)) {
   1.422 + *       // ...
   1.423 + *     }
   1.424 + *
   1.425 + * @param aCondition the callback that must return a truthy value
   1.426 + * @param aTimeoutMs the number of miliseconds to wait before giving up
   1.427 + * @param aIntervalMs the number of miliseconds between calls to aCondition
   1.428 + * @returns a Promise that resolves to true, or to an Error
   1.429 + */
   1.430 +function waitForCondition(aCondition, aTimeoutMs, aIntervalMs) {
   1.431 +  let deferred = Promise.defer();
   1.432 +  let timeoutMs = aTimeoutMs || kDefaultWait;
   1.433 +  let intervalMs = aIntervalMs || kDefaultInterval;
   1.434 +  let startTime = Date.now();
   1.435 +  let stack = new Error().stack;
   1.436 +
   1.437 +  function testCondition() {
   1.438 +    let now = Date.now();
   1.439 +    if((now - startTime) > timeoutMs) {
   1.440 +      deferred.reject( new Error("Timed out waiting for condition to be true at " + stack) );
   1.441 +      return;
   1.442 +    }
   1.443 +
   1.444 +    let condition;
   1.445 +    try {
   1.446 +      condition = aCondition();
   1.447 +    } catch (e) {
   1.448 +      deferred.reject( new Error("Got exception while attempting to test condition: " + e) );
   1.449 +      return;
   1.450 +    }
   1.451 +
   1.452 +    if (condition) {
   1.453 +      deferred.resolve(true);
   1.454 +    } else {
   1.455 +      setTimeout(testCondition, intervalMs);
   1.456 +    }
   1.457 +  }
   1.458 +
   1.459 +  setTimeout(testCondition, 0);
   1.460 +  return deferred.promise;
   1.461 +}
   1.462 +
   1.463 +/**
   1.464 + * same as waitForCondition but with better test output.
   1.465 + *
   1.466 + * @param aCondition the callback that must return a truthy value
   1.467 + * @param aTestMsg test condition message printed when the test succeeds or
   1.468 + * fails. Defaults to the stringified version of aCondition.
   1.469 + * @param aTimeoutMs the number of miliseconds to wait before giving up
   1.470 + * @param aIntervalMs the number of miliseconds between calls to aCondition
   1.471 + * @returns a Promise that resolves to true, or to an Error
   1.472 + */
   1.473 +function waitForCondition2(aCondition, aTestMsg, aTimeoutMs, aIntervalMs) {
   1.474 +  let deferred = Promise.defer();
   1.475 +  let msg = aTestMsg || aCondition;
   1.476 +  let timeoutMs = aTimeoutMs || kDefaultWait;
   1.477 +  let intervalMs = aIntervalMs || kDefaultInterval;
   1.478 +  let startTime = Date.now();
   1.479 +
   1.480 +  function testCondition() {
   1.481 +    let now = Date.now();
   1.482 +    if((now - startTime) > timeoutMs) {
   1.483 +      deferred.reject( new Error("Timed out waiting for " + msg) );
   1.484 +      return;
   1.485 +    }
   1.486 +
   1.487 +    let condition;
   1.488 +    try {
   1.489 +      condition = aCondition();
   1.490 +    } catch (e) {
   1.491 +      deferred.reject( new Error("Got exception while attempting to test '" + msg + "': " + e) );
   1.492 +      return;
   1.493 +    }
   1.494 +
   1.495 +    if (condition) {
   1.496 +      ok(true, msg);
   1.497 +      deferred.resolve(true);
   1.498 +    } else {
   1.499 +      setTimeout(testCondition, intervalMs);
   1.500 +    }
   1.501 +  }
   1.502 +
   1.503 +  setTimeout(testCondition, 0);
   1.504 +  return deferred.promise;
   1.505 +}
   1.506 +
   1.507 +/*
   1.508 + * Waits for an image in a page to load. Wrapper around waitForCondition.
   1.509 + *
   1.510 + * @param aWindow the tab or window that contains the image.
   1.511 + * @param aImageId the id of the image in the page.
   1.512 + * @returns a Promise that resolves to true, or to an Error
   1.513 + */
   1.514 +function waitForImageLoad(aWindow, aImageId) {
   1.515 +  let elem = aWindow.document.getElementById(aImageId);
   1.516 +  return waitForCondition(function () {
   1.517 +    let request = elem.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
   1.518 +    if (request && (request.imageStatus & request.STATUS_SIZE_AVAILABLE))
   1.519 +      return true;
   1.520 +    return false;
   1.521 +  }, 5000, 100);
   1.522 +}
   1.523 +
   1.524 +/**
   1.525 + * Waits a specified number of miliseconds for an observer event.
   1.526 + *
   1.527 + * @param aObsEvent the observer event to wait for
   1.528 + * @param aTimeoutMs the number of miliseconds to wait before giving up
   1.529 + * @returns a Promise that resolves to true, or to an Error
   1.530 + */
   1.531 +function waitForObserver(aObsEvent, aTimeoutMs, aObsData) {
   1.532 +  try {
   1.533 +
   1.534 +  let deferred = Promise.defer();
   1.535 +  let timeoutMs = aTimeoutMs || kDefaultWait;
   1.536 +  let timerID = 0;
   1.537 +
   1.538 +  var observeWatcher = {
   1.539 +    onEvent: function () {
   1.540 +      clearTimeout(timerID);
   1.541 +      Services.obs.removeObserver(this, aObsEvent);
   1.542 +      deferred.resolve();
   1.543 +    },
   1.544 +
   1.545 +    onError: function () {
   1.546 +      clearTimeout(timerID);
   1.547 +      Services.obs.removeObserver(this, aObsEvent);
   1.548 +      deferred.reject(new Error(aObsEvent + " event timeout"));
   1.549 +    },
   1.550 +
   1.551 +    observe: function (aSubject, aTopic, aData) {
   1.552 +      if (aTopic == aObsEvent &&
   1.553 +        (!aObsData || (aObsData == aData))) {
   1.554 +        this.onEvent();
   1.555 +      }
   1.556 +    },
   1.557 +
   1.558 +    QueryInterface: function (aIID) {
   1.559 +      if (!aIID.equals(Ci.nsIObserver) &&
   1.560 +          !aIID.equals(Ci.nsISupportsWeakReference) &&
   1.561 +          !aIID.equals(Ci.nsISupports)) {
   1.562 +        throw Components.results.NS_ERROR_NO_INTERFACE;
   1.563 +      }
   1.564 +      return this;
   1.565 +    },
   1.566 +  }
   1.567 +
   1.568 +  timerID = setTimeout(function wfo_canceller() {
   1.569 +    observeWatcher.onError();
   1.570 +  }, timeoutMs);
   1.571 +
   1.572 +  Services.obs.addObserver(observeWatcher, aObsEvent, true);
   1.573 +  return deferred.promise;
   1.574 +
   1.575 +  } catch (ex) {
   1.576 +    info(ex.message);
   1.577 +  }
   1.578 +}
   1.579 +
   1.580 +
   1.581 +/*=============================================================================
   1.582 + * Input mode helpers - these helpers notify observers to metro_precise_input
   1.583 + * and metro_imprecise_input respectively, triggering the same behaviour as user touch or mouse input
   1.584 + *
   1.585 + * Usage: let promise = waitForObservers("metro_imprecise_input");
   1.586 + *        notifyImprecise();
   1.587 + *        yield promise; // you are now in imprecise mode
   1.588 + *===========================================================================*/
   1.589 +function notifyPrecise()
   1.590 +{
   1.591 +  Services.obs.notifyObservers(null, "metro_precise_input", null);
   1.592 +}
   1.593 +
   1.594 +function notifyImprecise()
   1.595 +{
   1.596 +  Services.obs.notifyObservers(null, "metro_imprecise_input", null);
   1.597 +}
   1.598 +
   1.599 +/*=============================================================================
   1.600 + * Native input helpers - these helpers send input directly to the os
   1.601 + * generating os level input events that get processed by widget and
   1.602 + * apzc logic.
   1.603 + *===========================================================================*/
   1.604 +function synthesizeNativeMouse(aElement, aOffsetX, aOffsetY, aMsg) {
   1.605 +  let x = aOffsetX;
   1.606 +  let y = aOffsetY;
   1.607 +  if (aElement) {
   1.608 +    if (aElement.getBoundingClientRect) {
   1.609 +      let rect = aElement.getBoundingClientRect();
   1.610 +      x += rect.left;
   1.611 +      y += rect.top;
   1.612 +    } else if(aElement.left && aElement.top) {
   1.613 +      x += aElement.left;
   1.614 +      y += aElement.top;
   1.615 +    }
   1.616 +  }
   1.617 +  Browser.windowUtils.sendNativeMouseEvent(x, y, aMsg, 0, null);
   1.618 +}
   1.619 +
   1.620 +function synthesizeNativeMouseMove(aElement, aOffsetX, aOffsetY) {
   1.621 +  synthesizeNativeMouse(aElement,
   1.622 +                        aOffsetX,
   1.623 +                        aOffsetY,
   1.624 +                        0x0001);  // MOUSEEVENTF_MOVE
   1.625 +}
   1.626 +
   1.627 +function synthesizeNativeMouseLDown(aElement, aOffsetX, aOffsetY) {
   1.628 +  synthesizeNativeMouse(aElement,
   1.629 +                        aOffsetX,
   1.630 +                        aOffsetY,
   1.631 +                        0x0002);  // MOUSEEVENTF_LEFTDOWN
   1.632 +}
   1.633 +
   1.634 +function synthesizeNativeMouseLUp(aElement, aOffsetX, aOffsetY) {
   1.635 +  synthesizeNativeMouse(aElement,
   1.636 +                        aOffsetX,
   1.637 +                        aOffsetY,
   1.638 +                        0x0004);  // MOUSEEVENTF_LEFTUP
   1.639 +}
   1.640 +
   1.641 +function synthesizeNativeMouseRDown(aElement, aOffsetX, aOffsetY) {
   1.642 +  synthesizeNativeMouse(aElement,
   1.643 +                        aOffsetX,
   1.644 +                        aOffsetY,
   1.645 +                        0x0008);  // MOUSEEVENTF_RIGHTDOWN
   1.646 +}
   1.647 +
   1.648 +function synthesizeNativeMouseRUp(aElement, aOffsetX, aOffsetY) {
   1.649 +  synthesizeNativeMouse(aElement,
   1.650 +                        aOffsetX,
   1.651 +                        aOffsetY,
   1.652 +                        0x0010);  // MOUSEEVENTF_RIGHTUP
   1.653 +}
   1.654 +
   1.655 +function synthesizeNativeMouseMDown(aElement, aOffsetX, aOffsetY) {
   1.656 +  synthesizeNativeMouse(aElement,
   1.657 +                        aOffsetX,
   1.658 +                        aOffsetY,
   1.659 +                        0x0020);  // MOUSEEVENTF_MIDDLEDOWN
   1.660 +}
   1.661 +
   1.662 +function synthesizeNativeMouseMUp(aElement, aOffsetX, aOffsetY) {
   1.663 +  synthesizeNativeMouse(aElement,
   1.664 +                        aOffsetX,
   1.665 +                        aOffsetY,
   1.666 +                        0x0040);  // MOUSEEVENTF_MIDDLEUP
   1.667 +}
   1.668 +
   1.669 +// WARNING: these calls can trigger the soft keyboard on tablets, but not
   1.670 +// on test slaves (bug 947428).
   1.671 +// WARNING: When testing the apzc, be careful of bug 933990. Events sent
   1.672 +// shortly after loading a page may get ignored.
   1.673 +
   1.674 +function sendNativeLongTap(aElement, aX, aY) {
   1.675 +  let coords = logicalCoordsForElement(aElement, aX, aY);
   1.676 +  Browser.windowUtils.sendNativeTouchTap(coords.x, coords.y, true);
   1.677 +}
   1.678 +
   1.679 +function sendNativeTap(aElement, aX, aY) {
   1.680 +  let coords = logicalCoordsForElement(aElement, aX, aY);
   1.681 +  Browser.windowUtils.sendNativeTouchTap(coords.x, coords.y, false);
   1.682 +}
   1.683 +
   1.684 +function sendNativeDoubleTap(aElement, aX, aY) {
   1.685 +  let coords = logicalCoordsForElement(aElement, aX, aY);
   1.686 +  Browser.windowUtils.sendNativeTouchTap(coords.x, coords.y, false);
   1.687 +  Browser.windowUtils.sendNativeTouchTap(coords.x, coords.y, false);
   1.688 +}
   1.689 +
   1.690 +function clearNativeTouchSequence() {
   1.691 +  Browser.windowUtils.clearNativeTouchSequence();
   1.692 +}
   1.693 +
   1.694 +/*=============================================================================
   1.695 + * Synthesized event helpers - these helpers synthesize input events that get
   1.696 + * dispatched directly to the dom. As such widget and apzc logic is bypassed.
   1.697 + *===========================================================================*/
   1.698 +
   1.699 +/*
   1.700 + * logicalCoordsForElement - given coordinates relative to top-left of
   1.701 + * given element, returns logical coordinates for window. If a non-numeric
   1.702 + * X or Y value is given, a value for the center of the element in that
   1.703 + * dimension is used.
   1.704 + *
   1.705 + * @param aElement element coordinates are relative to.
   1.706 + * @param aX, aY relative coordinates.
   1.707 + */
   1.708 +function logicalCoordsForElement (aElement, aX, aY) {
   1.709 +  let coords = { x: null, y: null };
   1.710 +  let rect = aElement.getBoundingClientRect();
   1.711 +
   1.712 +  coords.x = isNaN(aX) ? rect.left + (rect.width / 2) : rect.left + aX;
   1.713 +  coords.y = isNaN(aY) ? rect.top + (rect.height / 2) : rect.top + aY;
   1.714 +
   1.715 +  return coords;
   1.716 +}
   1.717 +
   1.718 +function sendContextMenuMouseClickToElement(aWindow, aElement, aX, aY) {
   1.719 +  let utils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
   1.720 +                     .getInterface(Ci.nsIDOMWindowUtils);
   1.721 +  let coords = logicalCoordsForElement(aElement, aX, aY);
   1.722 +
   1.723 +  utils.sendMouseEventToWindow("mousedown", coords.x, coords.y, 2, 1, 0);
   1.724 +  utils.sendMouseEventToWindow("mouseup", coords.x, coords.y, 2, 1, 0);
   1.725 +  utils.sendMouseEventToWindow("contextmenu", coords.x, coords.y, 2, 1, 0);
   1.726 +}
   1.727 +
   1.728 +function sendMouseClick(aWindow, aX, aY) {
   1.729 +  EventUtils.synthesizeMouseAtPoint(aX, aY, {}, aWindow);
   1.730 +}
   1.731 +
   1.732 +/*
   1.733 + * sendContextMenuClick - simulates a press-hold touch input event. Event
   1.734 + * is delivered to the main window of the application through the top-level
   1.735 + * widget.
   1.736 + *
   1.737 + * @param aX, aY logical coordinates of the event.
   1.738 + */
   1.739 +function sendContextMenuClick(aX, aY) {
   1.740 +  let mediator = Components.classes["@mozilla.org/appshell/window-mediator;1"]
   1.741 +                            .getService(Components.interfaces.nsIWindowMediator);
   1.742 +  let mainwin = mediator.getMostRecentWindow("navigator:browser");
   1.743 +  let utils = mainwin.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
   1.744 +                      .getInterface(Components.interfaces.nsIDOMWindowUtils);
   1.745 +  utils.sendMouseEvent("contextmenu", aX, aY, 2, 1, 0, true,
   1.746 +                        1, Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH);
   1.747 +}
   1.748 +
   1.749 +/*
   1.750 + * sendContextMenuClickToSelection - simulates a press-hold touch input event
   1.751 + * selected text in a window.
   1.752 + */
   1.753 +function sendContextMenuClickToSelection(aWindow) {
   1.754 +  let selection = aWindow.getSelection();
   1.755 +  if (!selection || !selection.rangeCount) {
   1.756 +    ok(false, "no selection to tap!");
   1.757 +    return;
   1.758 +  }
   1.759 +  let range = selection.getRangeAt(0);
   1.760 +  let rect = range.getBoundingClientRect();
   1.761 +  let x = rect.left + (rect.width / 2);
   1.762 +  let y = rect.top + (rect.height / 2);
   1.763 +  let utils = aWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
   1.764 +                      .getInterface(Components.interfaces.nsIDOMWindowUtils);
   1.765 +  utils.sendMouseEventToWindow("contextmenu", x, y, 2, 1, 0, true,
   1.766 +                                1, Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH);
   1.767 +}
   1.768 +
   1.769 +/*
   1.770 + * sendContextMenuClickToWindow - simulates a press-hold touch input event.
   1.771 + *
   1.772 + * @param aWindow window used to retrieve dom window utils, and the
   1.773 + * target window for the event.
   1.774 + * @param aX, aY logical coordinates of the event relative to aWindow.
   1.775 + */
   1.776 +function sendContextMenuClickToWindow(aWindow, aX, aY) {
   1.777 +  let utils = aWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
   1.778 +                      .getInterface(Components.interfaces.nsIDOMWindowUtils);
   1.779 +
   1.780 +  utils.sendMouseEventToWindow("contextmenu", aX, aY, 2, 1, 0, true,
   1.781 +                                1, Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH);
   1.782 +}
   1.783 +
   1.784 +function sendContextMenuClickToElement(aWindow, aElement, aX, aY) {
   1.785 +  let utils = aWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
   1.786 +                      .getInterface(Components.interfaces.nsIDOMWindowUtils);
   1.787 +  let coords = logicalCoordsForElement(aElement, aX, aY);
   1.788 +  utils.sendMouseEventToWindow("contextmenu", coords.x, coords.y, 2, 1, 0, true,
   1.789 +                                1, Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH);
   1.790 +}
   1.791 +
   1.792 +/*
   1.793 + * sendDoubleTap - simulates a double click or double tap.
   1.794 + */
   1.795 +function sendDoubleTap(aWindow, aX, aY) {
   1.796 +  EventUtils.synthesizeMouseAtPoint(aX, aY, {
   1.797 +      clickCount: 1,
   1.798 +      inputSource: Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH
   1.799 +    }, aWindow);
   1.800 +
   1.801 +  EventUtils.synthesizeMouseAtPoint(aX, aY, {
   1.802 +      clickCount: 2,
   1.803 +      inputSource: Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH
   1.804 +    }, aWindow);
   1.805 +}
   1.806 +
   1.807 +function sendTap(aWindow, aX, aY) {
   1.808 +  EventUtils.synthesizeMouseAtPoint(aX, aY, {
   1.809 +      clickCount: 1,
   1.810 +      inputSource: Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH
   1.811 +    }, aWindow);
   1.812 +}
   1.813 +
   1.814 +function sendElementTap(aWindow, aElement, aX, aY) {
   1.815 +  let coords = logicalCoordsForElement(aElement, aX, aY);
   1.816 +  EventUtils.synthesizeMouseAtPoint(coords.x, coords.y, {
   1.817 +      clickCount: 1,
   1.818 +      inputSource: Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH
   1.819 +    }, aWindow);
   1.820 +}
   1.821 +
   1.822 +/*
   1.823 + * sendTouchDrag - sends a touch series composed of a touchstart,
   1.824 + * touchmove, and touchend w3c event.
   1.825 + */
   1.826 +function sendTouchDrag(aWindow, aStartX, aStartY, aEndX, aEndY) {
   1.827 +  EventUtils.synthesizeTouchAtPoint(aStartX, aStartY, { type: "touchstart" }, aWindow);
   1.828 +  EventUtils.synthesizeTouchAtPoint(aEndX, aEndY, { type: "touchmove" }, aWindow);
   1.829 +  EventUtils.synthesizeTouchAtPoint(aEndX, aEndY, { type: "touchend" }, aWindow);
   1.830 +}
   1.831 +
   1.832 +/*
   1.833 + * TouchDragAndHold - simulates a drag and hold sequence of events.
   1.834 + */
   1.835 +function TouchDragAndHold() {
   1.836 +}
   1.837 +
   1.838 +TouchDragAndHold.prototype = {
   1.839 +  _timeoutStep: 2,
   1.840 +  _numSteps: 50,
   1.841 +  _debug: false,
   1.842 +  _win: null,
   1.843 +  _native: false,
   1.844 +  _pointerId: 1,
   1.845 +  _dui: Components.interfaces.nsIDOMWindowUtils,
   1.846 +
   1.847 +  set useNativeEvents(aValue) {
   1.848 +    this._native = aValue;
   1.849 +  },
   1.850 +
   1.851 +  set stepTimeout(aValue) {
   1.852 +    this._timeoutStep = aValue;
   1.853 +  },
   1.854 +
   1.855 +  set numSteps(aValue) {
   1.856 +    this._numSteps = aValue;
   1.857 +  },
   1.858 +
   1.859 +  set nativePointerId(aValue) {
   1.860 +    this._pointerId = aValue;
   1.861 +  },
   1.862 +
   1.863 +  callback: function callback() {
   1.864 +    if (this._win == null)
   1.865 +      return;
   1.866 +
   1.867 +    if (this._debug) {
   1.868 +      SelectionHelperUI.debugDisplayDebugPoint(this._currentPoint.xPos,
   1.869 +        this._currentPoint.yPos, 5, "#FF0000", true);
   1.870 +    }
   1.871 +
   1.872 +    if (++this._step.steps >= this._numSteps) {
   1.873 +      if (this._native) {
   1.874 +        this._utils.sendNativeTouchPoint(this._pointerId, this._dui.TOUCH_CONTACT,
   1.875 +                                         this._endPoint.xPos, this._endPoint.yPos,
   1.876 +                                         1, 90);
   1.877 +      } else {
   1.878 +        EventUtils.synthesizeTouchAtPoint(this._endPoint.xPos, this._endPoint.yPos,
   1.879 +                                          { type: "touchmove" }, this._win);
   1.880 +      }
   1.881 +      this._defer.resolve();
   1.882 +      return;
   1.883 +    }
   1.884 +    this._currentPoint.xPos += this._step.x;
   1.885 +    this._currentPoint.yPos += this._step.y;
   1.886 +    if (this._debug) {
   1.887 +      info("[" + this._step.steps + "] touchmove " + this._currentPoint.xPos + " x " + this._currentPoint.yPos);
   1.888 +    }
   1.889 +
   1.890 +    if (this._native) {
   1.891 +      this._utils.sendNativeTouchPoint(this._pointerId, this._dui.TOUCH_CONTACT,
   1.892 +                                       this._currentPoint.xPos, this._currentPoint.yPos,
   1.893 +                                       1, 90);
   1.894 +    } else {
   1.895 +      EventUtils.synthesizeTouchAtPoint(this._currentPoint.xPos, this._currentPoint.yPos,
   1.896 +                                        { type: "touchmove" }, this._win);
   1.897 +    }
   1.898 +
   1.899 +    let self = this;
   1.900 +    setTimeout(function () { self.callback(); }, this._timeoutStep);
   1.901 +  },
   1.902 +
   1.903 +  start: function start(aWindow, aStartX, aStartY, aEndX, aEndY) {
   1.904 +    this._defer = Promise.defer();
   1.905 +    this._win = aWindow;
   1.906 +    this._utils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
   1.907 +                         .getInterface(Ci.nsIDOMWindowUtils);
   1.908 +    this._endPoint = { xPos: aEndX, yPos: aEndY };
   1.909 +    this._currentPoint = { xPos: aStartX, yPos: aStartY };
   1.910 +    this._step = { steps: 0, x: (aEndX - aStartX) / this._numSteps, y: (aEndY - aStartY) / this._numSteps };
   1.911 +    if (this._debug) {
   1.912 +      info("[0] touchstart " + aStartX + " x " + aStartY);
   1.913 +    }
   1.914 +    // flush layout, bug 914847
   1.915 +    this._utils.elementFromPoint(aStartX, aStartY, false, true);
   1.916 +    if (this._native) {
   1.917 +      this._utils.sendNativeTouchPoint(this._pointerId, this._dui.TOUCH_CONTACT,
   1.918 +                                       aStartX, aStartY, 1, 90);
   1.919 +    } else {
   1.920 +      EventUtils.synthesizeTouchAtPoint(aStartX, aStartY, { type: "touchstart" }, aWindow);
   1.921 +    }
   1.922 +    let self = this;
   1.923 +    setTimeout(function () { self.callback(); }, this._timeoutStep);
   1.924 +    return this._defer.promise;
   1.925 +  },
   1.926 +
   1.927 +  move: function move(aEndX, aEndY) {
   1.928 +    if (this._win == null)
   1.929 +      return;
   1.930 +    if (this._debug) {
   1.931 +      info("[0] continuation to " + aEndX + " x " + aEndY);
   1.932 +    }
   1.933 +    this._defer = Promise.defer();
   1.934 +    this._step = { steps: 0,
   1.935 +                   x: (aEndX - this._endPoint.xPos) / this._numSteps,
   1.936 +                   y: (aEndY - this._endPoint.yPos) / this._numSteps };
   1.937 +    this._endPoint = { xPos: aEndX, yPos: aEndY };
   1.938 +    let self = this;
   1.939 +    setTimeout(function () { self.callback(); }, this._timeoutStep);
   1.940 +    return this._defer.promise;
   1.941 +  },
   1.942 +
   1.943 +  end: function end() {
   1.944 +    if (this._debug) {
   1.945 +      info("[" + this._step.steps + "] touchend " + this._endPoint.xPos + " x " + this._endPoint.yPos);
   1.946 +      SelectionHelperUI.debugClearDebugPoints();
   1.947 +    }
   1.948 +    if (this._native) {
   1.949 +      this._utils.sendNativeTouchPoint(this._pointerId, this._dui.TOUCH_REMOVE,
   1.950 +                                       this._endPoint.xPos, this._endPoint.yPos,
   1.951 +                                       1, 90);
   1.952 +    } else {
   1.953 +      EventUtils.synthesizeTouchAtPoint(this._endPoint.xPos, this._endPoint.yPos,
   1.954 +                                        { type: "touchend" }, this._win);
   1.955 +    }
   1.956 +    this._win = null;
   1.957 +  },
   1.958 +};
   1.959 +
   1.960 +/*=============================================================================
   1.961 +  System utilities
   1.962 +=============================================================================*/
   1.963 +
   1.964 +/*
   1.965 + * emptyClipboard - clear the windows clipboard.
   1.966 + */
   1.967 +function emptyClipboard() {
   1.968 +  Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard)
   1.969 +                                       .emptyClipboard(Ci.nsIClipboard.kGlobalClipboard);
   1.970 +}
   1.971 +
   1.972 +/*
   1.973 + * purgeEventQueue - purges the event queue on the calling thread.
   1.974 + * Pumps latent in-process message manager events awaiting delivery.
   1.975 + */
   1.976 +function purgeEventQueue() {
   1.977 +  let thread = Services.tm.currentThread;
   1.978 +  while (thread.hasPendingEvents()) {
   1.979 +    if (!thread.processNextEvent(true))
   1.980 +      break;
   1.981 +  }
   1.982 +}
   1.983 +
   1.984 +/*=============================================================================
   1.985 +  Test-running helpers
   1.986 +=============================================================================*/
   1.987 +let gCurrentTest = null;
   1.988 +let gTests = [];
   1.989 +
   1.990 +function runTests() {
   1.991 +  waitForExplicitFinish();
   1.992 +
   1.993 +  Task.spawn(function() {
   1.994 +    while((gCurrentTest = gTests.shift())){
   1.995 +      try {
   1.996 +        if ('function' == typeof gCurrentTest.setUp) {
   1.997 +          info("SETUP " + gCurrentTest.desc);
   1.998 +          yield Task.spawn(gCurrentTest.setUp.bind(gCurrentTest));
   1.999 +        }
  1.1000 +        try {
  1.1001 +          info("RUN " + gCurrentTest.desc);
  1.1002 +          yield Task.spawn(gCurrentTest.run.bind(gCurrentTest));
  1.1003 +        } finally {
  1.1004 +          if ('function' == typeof gCurrentTest.tearDown) {
  1.1005 +            info("TEARDOWN " + gCurrentTest.desc);
  1.1006 +            yield Task.spawn(gCurrentTest.tearDown.bind(gCurrentTest));
  1.1007 +          }
  1.1008 +        }
  1.1009 +      } catch (ex) {
  1.1010 +        ok(false, "runTests: Task failed - " + ex + ' at ' + ex.stack);
  1.1011 +      } finally {
  1.1012 +        info("END " + gCurrentTest.desc);
  1.1013 +      }
  1.1014 +    }
  1.1015 +
  1.1016 +    try {
  1.1017 +      cleanUpOpenedTabs();
  1.1018 +
  1.1019 +      let badTabs = [];
  1.1020 +      Browser.tabs.forEach(function(item, index, array) {
  1.1021 +        let location = item.browser.currentURI.spec;
  1.1022 +        if (index == 0 && location == "about:blank" || location == "about:start") {
  1.1023 +          return;
  1.1024 +        }
  1.1025 +        ok(false, "Left over tab after test: '" + location + "'");
  1.1026 +        badTabs.push(item);
  1.1027 +      });
  1.1028 +
  1.1029 +      badTabs.forEach(function(item, index, array) {
  1.1030 +        Browser.closeTab(item, { forceClose: true });
  1.1031 +      });
  1.1032 +    } catch (ex) {
  1.1033 +      ok(false, "Cleanup tabs failed - " + ex);
  1.1034 +    }
  1.1035 +
  1.1036 +    finish();
  1.1037 +  });
  1.1038 +}
  1.1039 +
  1.1040 +// wrap a method with a spy that records how and how many times it gets called
  1.1041 +// the spy is returned; use spy.restore() to put the original back
  1.1042 +function spyOnMethod(aObj, aMethod) {
  1.1043 +  let origFunc = aObj[aMethod];
  1.1044 +  let spy = function() {
  1.1045 +    let callArguments = Array.slice(arguments);
  1.1046 +    spy.callCount++;
  1.1047 +    spy.calledWith = callArguments;
  1.1048 +    spy.argsForCall.push(callArguments);
  1.1049 +    return (spy.returnValue = origFunc.apply(aObj, arguments));
  1.1050 +  };
  1.1051 +  spy.callCount = 0;
  1.1052 +  spy.argsForCall = [];
  1.1053 +  spy.restore = function() {
  1.1054 +    return (aObj[aMethod] = origFunc);
  1.1055 +  };
  1.1056 +  return (aObj[aMethod] = spy);
  1.1057 +}
  1.1058 +
  1.1059 +// replace a method with a stub that records how and how many times it gets called
  1.1060 +// the stub is returned; use stub.restore() to put the original back
  1.1061 +function stubMethod(aObj, aMethod) {
  1.1062 +  let origFunc = aObj[aMethod];
  1.1063 +  let func = function() {
  1.1064 +    func.calledWith = Array.slice(arguments);
  1.1065 +    func.callCount++;
  1.1066 +  };
  1.1067 +  func.callCount = 0;
  1.1068 +  func.restore = function() {
  1.1069 +    return (aObj[aMethod] = origFunc);
  1.1070 +  };
  1.1071 +  aObj[aMethod] = func;
  1.1072 +  return func;
  1.1073 +}

mercurial