browser/base/content/test/newtab/head.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* Any copyright is dedicated to the Public Domain.
     2    http://creativecommons.org/publicdomain/zero/1.0/ */
     4 const PREF_NEWTAB_ENABLED = "browser.newtabpage.enabled";
     5 const PREF_NEWTAB_DIRECTORYSOURCE = "browser.newtabpage.directorySource";
     7 Services.prefs.setBoolPref(PREF_NEWTAB_ENABLED, true);
     8 // start with no directory links by default
     9 Services.prefs.setCharPref(PREF_NEWTAB_DIRECTORYSOURCE, "data:application/json,{}");
    11 let tmp = {};
    12 Cu.import("resource://gre/modules/Promise.jsm", tmp);
    13 Cu.import("resource://gre/modules/NewTabUtils.jsm", tmp);
    14 Cc["@mozilla.org/moz/jssubscript-loader;1"]
    15   .getService(Ci.mozIJSSubScriptLoader)
    16   .loadSubScript("chrome://browser/content/sanitize.js", tmp);
    17 let {Promise, NewTabUtils, Sanitizer} = tmp;
    19 let uri = Services.io.newURI("about:newtab", null, null);
    20 let principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(uri);
    22 let isMac = ("nsILocalFileMac" in Ci);
    23 let isLinux = ("@mozilla.org/gnome-gconf-service;1" in Cc);
    24 let isWindows = ("@mozilla.org/windows-registry-key;1" in Cc);
    25 let gWindow = window;
    27 // The tests assume all three rows of sites are shown, but the window may be too
    28 // short to actually show three rows.  Resize it if necessary.
    29 let requiredInnerHeight =
    30   40 + 32 + // undo container + bottom margin
    31   44 + 32 + // search bar + bottom margin
    32   (3 * (150 + 32)) + // 3 rows * (tile height + title and bottom margin)
    33   100; // breathing room
    35 let oldInnerHeight = null;
    36 if (gBrowser.contentWindow.innerHeight < requiredInnerHeight) {
    37   oldInnerHeight = gBrowser.contentWindow.innerHeight;
    38   info("Changing browser inner height from " + oldInnerHeight + " to " +
    39        requiredInnerHeight);
    40   gBrowser.contentWindow.innerHeight = requiredInnerHeight;
    41   let screenHeight = {};
    42   Cc["@mozilla.org/gfx/screenmanager;1"].
    43     getService(Ci.nsIScreenManager).
    44     primaryScreen.
    45     GetAvailRectDisplayPix({}, {}, {}, screenHeight);
    46   screenHeight = screenHeight.value;
    47   if (screenHeight < gBrowser.contentWindow.outerHeight) {
    48     info("Warning: Browser outer height is now " +
    49          gBrowser.contentWindow.outerHeight + ", which is larger than the " +
    50          "available screen height, " + screenHeight +
    51          ". That may cause problems.");
    52   }
    53 }
    55 registerCleanupFunction(function () {
    56   while (gWindow.gBrowser.tabs.length > 1)
    57     gWindow.gBrowser.removeTab(gWindow.gBrowser.tabs[1]);
    59   if (oldInnerHeight)
    60     gBrowser.contentWindow.innerHeight = oldInnerHeight;
    62   Services.prefs.clearUserPref(PREF_NEWTAB_ENABLED);
    63   Services.prefs.clearUserPref(PREF_NEWTAB_DIRECTORYSOURCE);
    64 });
    66 /**
    67  * Provide the default test function to start our test runner.
    68  */
    69 function test() {
    70   TestRunner.run();
    71 }
    73 /**
    74  * The test runner that controls the execution flow of our tests.
    75  */
    76 let TestRunner = {
    77   /**
    78    * Starts the test runner.
    79    */
    80   run: function () {
    81     waitForExplicitFinish();
    83     this._iter = runTests();
    84     this.next();
    85   },
    87   /**
    88    * Runs the next available test or finishes if there's no test left.
    89    */
    90   next: function () {
    91     try {
    92       TestRunner._iter.next();
    93     } catch (e if e instanceof StopIteration) {
    94       TestRunner.finish();
    95     }
    96   },
    98   /**
    99    * Finishes all tests and cleans up.
   100    */
   101   finish: function () {
   102     function cleanupAndFinish() {
   103       clearHistory(function () {
   104         whenPagesUpdated(finish);
   105         NewTabUtils.restore();
   106       });
   107     }
   109     let callbacks = NewTabUtils.links._populateCallbacks;
   110     let numCallbacks = callbacks.length;
   112     if (numCallbacks)
   113       callbacks.splice(0, numCallbacks, cleanupAndFinish);
   114     else
   115       cleanupAndFinish();
   116   }
   117 };
   119 /**
   120  * Returns the selected tab's content window.
   121  * @return The content window.
   122  */
   123 function getContentWindow() {
   124   return gWindow.gBrowser.selectedBrowser.contentWindow;
   125 }
   127 /**
   128  * Returns the selected tab's content document.
   129  * @return The content document.
   130  */
   131 function getContentDocument() {
   132   return gWindow.gBrowser.selectedBrowser.contentDocument;
   133 }
   135 /**
   136  * Returns the newtab grid of the selected tab.
   137  * @return The newtab grid.
   138  */
   139 function getGrid() {
   140   return getContentWindow().gGrid;
   141 }
   143 /**
   144  * Returns the cell at the given index of the selected tab's newtab grid.
   145  * @param aIndex The cell index.
   146  * @return The newtab cell.
   147  */
   148 function getCell(aIndex) {
   149   return getGrid().cells[aIndex];
   150 }
   152 /**
   153  * Allows to provide a list of links that is used to construct the grid.
   154  * @param aLinksPattern the pattern (see below)
   155  *
   156  * Example: setLinks("1,2,3")
   157  * Result: [{url: "http://example.com/#1", title: "site#1"},
   158  *          {url: "http://example.com/#2", title: "site#2"}
   159  *          {url: "http://example.com/#3", title: "site#3"}]
   160  */
   161 function setLinks(aLinks) {
   162   let links = aLinks;
   164   if (typeof links == "string") {
   165     links = aLinks.split(/\s*,\s*/).map(function (id) {
   166       return {url: "http://example.com/#" + id, title: "site#" + id};
   167     });
   168   }
   170   // Call populateCache() once to make sure that all link fetching that is
   171   // currently in progress has ended. We clear the history, fill it with the
   172   // given entries and call populateCache() now again to make sure the cache
   173   // has the desired contents.
   174   NewTabUtils.links.populateCache(function () {
   175     clearHistory(function () {
   176       fillHistory(links, function () {
   177         NewTabUtils.links.populateCache(function () {
   178           NewTabUtils.allPages.update();
   179           TestRunner.next();
   180         }, true);
   181       });
   182     });
   183   });
   184 }
   186 function clearHistory(aCallback) {
   187   Services.obs.addObserver(function observe(aSubject, aTopic, aData) {
   188     Services.obs.removeObserver(observe, aTopic);
   189     executeSoon(aCallback);
   190   }, PlacesUtils.TOPIC_EXPIRATION_FINISHED, false);
   192   PlacesUtils.history.removeAllPages();
   193 }
   195 function fillHistory(aLinks, aCallback) {
   196   let numLinks = aLinks.length;
   197   if (!numLinks) {
   198     if (aCallback)
   199       executeSoon(aCallback);
   200     return;
   201   }
   203   let transitionLink = Ci.nsINavHistoryService.TRANSITION_LINK;
   205   // Important: To avoid test failures due to clock jitter on Windows XP, call
   206   // Date.now() once here, not each time through the loop.
   207   let now = Date.now() * 1000;
   209   for (let i = 0; i < aLinks.length; i++) {
   210     let link = aLinks[i];
   211     let place = {
   212       uri: makeURI(link.url),
   213       title: link.title,
   214       // Links are secondarily sorted by visit date descending, so decrease the
   215       // visit date as we progress through the array so that links appear in the
   216       // grid in the order they're present in the array.
   217       visits: [{visitDate: now - i, transitionType: transitionLink}]
   218     };
   220     PlacesUtils.asyncHistory.updatePlaces(place, {
   221       handleError: function () ok(false, "couldn't add visit to history"),
   222       handleResult: function () {},
   223       handleCompletion: function () {
   224         if (--numLinks == 0 && aCallback)
   225           aCallback();
   226       }
   227     });
   228   }
   229 }
   231 /**
   232  * Allows to specify the list of pinned links (that have a fixed position in
   233  * the grid.
   234  * @param aLinksPattern the pattern (see below)
   235  *
   236  * Example: setPinnedLinks("3,,1")
   237  * Result: 'http://example.com/#3' is pinned in the first cell. 'http://example.com/#1' is
   238  *         pinned in the third cell.
   239  */
   240 function setPinnedLinks(aLinks) {
   241   let links = aLinks;
   243   if (typeof links == "string") {
   244     links = aLinks.split(/\s*,\s*/).map(function (id) {
   245       if (id)
   246         return {url: "http://example.com/#" + id, title: "site#" + id};
   247     });
   248   }
   250   let string = Cc["@mozilla.org/supports-string;1"]
   251                  .createInstance(Ci.nsISupportsString);
   252   string.data = JSON.stringify(links);
   253   Services.prefs.setComplexValue("browser.newtabpage.pinned",
   254                                  Ci.nsISupportsString, string);
   256   NewTabUtils.pinnedLinks.resetCache();
   257   NewTabUtils.allPages.update();
   258 }
   260 /**
   261  * Restore the grid state.
   262  */
   263 function restore() {
   264   whenPagesUpdated();
   265   NewTabUtils.restore();
   266 }
   268 /**
   269  * Creates a new tab containing 'about:newtab'.
   270  */
   271 function addNewTabPageTab() {
   272   let tab = gWindow.gBrowser.selectedTab = gWindow.gBrowser.addTab("about:newtab");
   273   let browser = tab.linkedBrowser;
   275   function whenNewTabLoaded() {
   276     if (NewTabUtils.allPages.enabled) {
   277       // Continue when the link cache has been populated.
   278       NewTabUtils.links.populateCache(function () {
   279         executeSoon(TestRunner.next);
   280       });
   281     } else {
   282       // It's important that we call next() asynchronously.
   283       // 'yield addNewTabPageTab()' would fail if next() is called
   284       // synchronously because the iterator is already executing.
   285       executeSoon(TestRunner.next);
   286     }
   287   }
   289   // The new tab page might have been preloaded in the background.
   290   if (browser.contentDocument.readyState == "complete") {
   291     whenNewTabLoaded();
   292     return;
   293   }
   295   // Wait for the new tab page to be loaded.
   296   browser.addEventListener("load", function onLoad() {
   297     browser.removeEventListener("load", onLoad, true);
   298     whenNewTabLoaded();
   299   }, true);
   300 }
   302 /**
   303  * Compares the current grid arrangement with the given pattern.
   304  * @param the pattern (see below)
   305  * @param the array of sites to compare with (optional)
   306  *
   307  * Example: checkGrid("3p,2,,1p")
   308  * Result: We expect the first cell to contain the pinned site 'http://example.com/#3'.
   309  *         The second cell contains 'http://example.com/#2'. The third cell is empty.
   310  *         The fourth cell contains the pinned site 'http://example.com/#4'.
   311  */
   312 function checkGrid(aSitesPattern, aSites) {
   313   let length = aSitesPattern.split(",").length;
   314   let sites = (aSites || getGrid().sites).slice(0, length);
   315   let current = sites.map(function (aSite) {
   316     if (!aSite)
   317       return "";
   319     let pinned = aSite.isPinned();
   320     let pinButton = aSite.node.querySelector(".newtab-control-pin");
   321     let hasPinnedAttr = pinButton.hasAttribute("pinned");
   323     if (pinned != hasPinnedAttr)
   324       ok(false, "invalid state (site.isPinned() != site[pinned])");
   326     return aSite.url.replace(/^http:\/\/example\.com\/#(\d+)$/, "$1") + (pinned ? "p" : "");
   327   });
   329   is(current, aSitesPattern, "grid status = " + aSitesPattern);
   330 }
   332 /**
   333  * Blocks a site from the grid.
   334  * @param aIndex The cell index.
   335  */
   336 function blockCell(aIndex) {
   337   whenPagesUpdated();
   338   getCell(aIndex).site.block();
   339 }
   341 /**
   342  * Pins a site on a given position.
   343  * @param aIndex The cell index.
   344  * @param aPinIndex The index the defines where the site should be pinned.
   345  */
   346 function pinCell(aIndex, aPinIndex) {
   347   getCell(aIndex).site.pin(aPinIndex);
   348 }
   350 /**
   351  * Unpins the given cell's site.
   352  * @param aIndex The cell index.
   353  */
   354 function unpinCell(aIndex) {
   355   whenPagesUpdated();
   356   getCell(aIndex).site.unpin();
   357 }
   359 /**
   360  * Simulates a drag and drop operation.
   361  * @param aSourceIndex The cell index containing the dragged site.
   362  * @param aDestIndex The cell index of the drop target.
   363  */
   364 function simulateDrop(aSourceIndex, aDestIndex) {
   365   let src = getCell(aSourceIndex).site.node;
   366   let dest = getCell(aDestIndex).node;
   368   // Drop 'src' onto 'dest' and continue testing when all newtab
   369   // pages have been updated (i.e. the drop operation is completed).
   370   startAndCompleteDragOperation(src, dest, whenPagesUpdated);
   371 }
   373 /**
   374  * Simulates a drag and drop operation. Instead of rearranging a site that is
   375  * is already contained in the newtab grid, this is used to simulate dragging
   376  * an external link onto the grid e.g. the text from the URL bar.
   377  * @param aDestIndex The cell index of the drop target.
   378  */
   379 function simulateExternalDrop(aDestIndex) {
   380   let dest = getCell(aDestIndex).node;
   382   // Create an iframe that contains the external link we'll drag.
   383   createExternalDropIframe().then(iframe => {
   384     let link = iframe.contentDocument.getElementById("link");
   386     // Drop 'link' onto 'dest'.
   387     startAndCompleteDragOperation(link, dest, () => {
   388       // Wait until the drop operation is complete
   389       // and all newtab pages have been updated.
   390       whenPagesUpdated(() => {
   391         // Clean up and remove the iframe.
   392         iframe.remove();
   393         // Continue testing.
   394         TestRunner.next();
   395       });
   396     });
   397   });
   398 }
   400 /**
   401  * Starts and complete a drag-and-drop operation.
   402  * @param aSource The node that is being dragged.
   403  * @param aDest The node we're dragging aSource onto.
   404  * @param aCallback The function that is called when we're done.
   405  */
   406 function startAndCompleteDragOperation(aSource, aDest, aCallback) {
   407   // Start by pressing the left mouse button.
   408   synthesizeNativeMouseLDown(aSource);
   410   // Move the mouse in 5px steps until the drag operation starts.
   411   let offset = 0;
   412   let interval = setInterval(() => {
   413     synthesizeNativeMouseDrag(aSource, offset += 5);
   414   }, 10);
   416   // When the drag operation has started we'll move
   417   // the dragged element to its target position.
   418   aSource.addEventListener("dragstart", function onDragStart() {
   419     aSource.removeEventListener("dragstart", onDragStart);
   420     clearInterval(interval);
   422     // Place the cursor above the drag target.
   423     synthesizeNativeMouseMove(aDest);
   424   });
   426   // As soon as the dragged element hovers the target, we'll drop it.
   427   aDest.addEventListener("dragenter", function onDragEnter() {
   428     aDest.removeEventListener("dragenter", onDragEnter);
   430     // Finish the drop operation.
   431     synthesizeNativeMouseLUp(aDest);
   432     aCallback();
   433   });
   434 }
   436 /**
   437  * Helper function that creates a temporary iframe in the about:newtab
   438  * document. This will contain a link we can drag to the test the dropping
   439  * of links from external documents.
   440  */
   441 function createExternalDropIframe() {
   442   const url = "data:text/html;charset=utf-8," +
   443               "<a id='link' href='http://example.com/%2399'>link</a>";
   445   let deferred = Promise.defer();
   446   let doc = getContentDocument();
   447   let iframe = doc.createElement("iframe");
   448   iframe.setAttribute("src", url);
   449   iframe.style.width = "50px";
   450   iframe.style.height = "50px";
   452   let margin = doc.getElementById("newtab-margin-top");
   453   margin.appendChild(iframe);
   455   iframe.addEventListener("load", function onLoad() {
   456     iframe.removeEventListener("load", onLoad);
   457     executeSoon(() => deferred.resolve(iframe));
   458   });
   460   return deferred.promise;
   461 }
   463 /**
   464  * Fires a synthetic 'mousedown' event on the current about:newtab page.
   465  * @param aElement The element used to determine the cursor position.
   466  */
   467 function synthesizeNativeMouseLDown(aElement) {
   468   if (isLinux) {
   469     let win = aElement.ownerDocument.defaultView;
   470     EventUtils.synthesizeMouseAtCenter(aElement, {type: "mousedown"}, win);
   471   } else {
   472     let msg = isWindows ? 2 : 1;
   473     synthesizeNativeMouseEvent(aElement, msg);
   474   }
   475 }
   477 /**
   478  * Fires a synthetic 'mouseup' event on the current about:newtab page.
   479  * @param aElement The element used to determine the cursor position.
   480  */
   481 function synthesizeNativeMouseLUp(aElement) {
   482   let msg = isWindows ? 4 : (isMac ? 2 : 7);
   483   synthesizeNativeMouseEvent(aElement, msg);
   484 }
   486 /**
   487  * Fires a synthetic mouse drag event on the current about:newtab page.
   488  * @param aElement The element used to determine the cursor position.
   489  * @param aOffsetX The left offset that is added to the position.
   490  */
   491 function synthesizeNativeMouseDrag(aElement, aOffsetX) {
   492   let msg = isMac ? 6 : 1;
   493   synthesizeNativeMouseEvent(aElement, msg, aOffsetX);
   494 }
   496 /**
   497  * Fires a synthetic 'mousemove' event on the current about:newtab page.
   498  * @param aElement The element used to determine the cursor position.
   499  */
   500 function synthesizeNativeMouseMove(aElement) {
   501   let msg = isMac ? 5 : 1;
   502   synthesizeNativeMouseEvent(aElement, msg);
   503 }
   505 /**
   506  * Fires a synthetic mouse event on the current about:newtab page.
   507  * @param aElement The element used to determine the cursor position.
   508  * @param aOffsetX The left offset that is added to the position (optional).
   509  * @param aOffsetY The top offset that is added to the position (optional).
   510  */
   511 function synthesizeNativeMouseEvent(aElement, aMsg, aOffsetX = 0, aOffsetY = 0) {
   512   let rect = aElement.getBoundingClientRect();
   513   let win = aElement.ownerDocument.defaultView;
   514   let x = aOffsetX + win.mozInnerScreenX + rect.left + rect.width / 2;
   515   let y = aOffsetY + win.mozInnerScreenY + rect.top + rect.height / 2;
   517   let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
   518                  .getInterface(Ci.nsIDOMWindowUtils);
   520   let scale = utils.screenPixelsPerCSSPixel;
   521   utils.sendNativeMouseEvent(x * scale, y * scale, aMsg, 0, null);
   522 }
   524 /**
   525  * Sends a custom drag event to a given DOM element.
   526  * @param aEventType The drag event's type.
   527  * @param aTarget The DOM element that the event is dispatched to.
   528  * @param aData The event's drag data (optional).
   529  */
   530 function sendDragEvent(aEventType, aTarget, aData) {
   531   let event = createDragEvent(aEventType, aData);
   532   let ifaceReq = getContentWindow().QueryInterface(Ci.nsIInterfaceRequestor);
   533   let windowUtils = ifaceReq.getInterface(Ci.nsIDOMWindowUtils);
   534   windowUtils.dispatchDOMEventViaPresShell(aTarget, event, true);
   535 }
   537 /**
   538  * Creates a custom drag event.
   539  * @param aEventType The drag event's type.
   540  * @param aData The event's drag data (optional).
   541  * @return The drag event.
   542  */
   543 function createDragEvent(aEventType, aData) {
   544   let dataTransfer = new (getContentWindow()).DataTransfer("dragstart", false);
   545   dataTransfer.mozSetDataAt("text/x-moz-url", aData, 0);
   546   let event = getContentDocument().createEvent("DragEvents");
   547   event.initDragEvent(aEventType, true, true, getContentWindow(), 0, 0, 0, 0, 0,
   548                       false, false, false, false, 0, null, dataTransfer);
   550   return event;
   551 }
   553 /**
   554  * Resumes testing when all pages have been updated.
   555  * @param aCallback Called when done. If not specified, TestRunner.next is used.
   556  * @param aOnlyIfHidden If true, this resumes testing only when an update that
   557  *                      applies to pre-loaded, hidden pages is observed.  If
   558  *                      false, this resumes testing when any update is observed.
   559  */
   560 function whenPagesUpdated(aCallback, aOnlyIfHidden=false) {
   561   let page = {
   562     update: function (onlyIfHidden=false) {
   563       if (onlyIfHidden == aOnlyIfHidden) {
   564         NewTabUtils.allPages.unregister(this);
   565         executeSoon(aCallback || TestRunner.next);
   566       }
   567     }
   568   };
   570   NewTabUtils.allPages.register(page);
   571   registerCleanupFunction(function () {
   572     NewTabUtils.allPages.unregister(page);
   573   });
   574 }
   576 /**
   577  * Waits a small amount of time for search events to stop occurring in the
   578  * newtab page.
   579  *
   580  * newtab pages receive some search events around load time that are difficult
   581  * to predict.  There are two categories of such events: (1) "State" events
   582  * triggered by engine notifications like engine-changed, due to the search
   583  * service initializing itself on app startup.  This can happen when a test is
   584  * the first test to run.  (2) "State" events triggered by the newtab page
   585  * itself when gSearch first sets itself up.  newtab preloading makes these a
   586  * pain to predict.
   587  */
   588 function whenSearchInitDone() {
   589   info("Waiting for initial search events...");
   590   let numTicks = 0;
   591   function reset(event) {
   592     info("Got initial search event " + event.detail.type +
   593          ", waiting for more...");
   594     numTicks = 0;
   595   }
   596   let eventName = "ContentSearchService";
   597   getContentWindow().addEventListener(eventName, reset);
   598   let interval = window.setInterval(() => {
   599     if (++numTicks >= 100) {
   600       info("Done waiting for initial search events");
   601       window.clearInterval(interval);
   602       getContentWindow().removeEventListener(eventName, reset);
   603       TestRunner.next();
   604     }
   605   }, 0);
   606 }

mercurial