merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Mon, 01 Dec 2014 12:01:08 +0100
changeset 218152 707a34b55e44baf41a7e3480db80e7f0d799e219
parent 218141 af5fc587f98b4cdca6280181fff03a2b8fc16c08 (current diff)
parent 218151 2bb476f9857aa3930c8b60f55ff4653b124f3f2b (diff)
child 218157 08be3008650fbeb88777dea3e6034622196dc949
push id27917
push usercbook@mozilla.com
push dateMon, 01 Dec 2014 11:03:31 +0000
treeherdermozilla-central@707a34b55e44 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone37.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge fx-team to mozilla-central a=merge
--- a/browser/base/content/browser-loop.js
+++ b/browser/base/content/browser-loop.js
@@ -19,58 +19,63 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 
     /**
      * Opens the panel for Loop and sizes it appropriately.
      *
      * @param {event}  event   The event opening the panel, used to anchor
      *                         the panel to the button which triggers it.
      * @param {String} [tabId] Identifier of the tab to select when the panel is
      *                         opened. Example: 'rooms', 'contacts', etc.
+     * @return {Promise}
      */
     openCallPanel: function(event, tabId = null) {
-      let callback = iframe => {
-        // Helper function to show a specific tab view in the panel.
-        function showTab() {
-          if (!tabId) {
+      return new Promise((resolve) => {
+        let callback = iframe => {
+          // Helper function to show a specific tab view in the panel.
+          function showTab() {
+            if (!tabId) {
+              resolve();
+              return;
+            }
+
+            let win = iframe.contentWindow;
+            let ev = new win.CustomEvent("UIAction", Cu.cloneInto({
+              detail: {
+                action: "selectTab",
+                tab: tabId
+              }
+            }, win));
+            win.dispatchEvent(ev);
+            resolve();
+          }
+
+          // If the panel has been opened and initialized before, we can skip waiting
+          // for the content to load - because it's already there.
+          if (("contentWindow" in iframe) && iframe.contentWindow.document.readyState == "complete") {
+            showTab();
             return;
           }
 
-          let win = iframe.contentWindow;
-          let ev = new win.CustomEvent("UIAction", Cu.cloneInto({
-            detail: {
-              action: "selectTab",
-              tab: tabId
-            }
-          }, win));
-          win.dispatchEvent(ev);
-        }
-
-        // If the panel has been opened and initialized before, we can skip waiting
-        // for the content to load - because it's already there.
-        if (("contentWindow" in iframe) && iframe.contentWindow.document.readyState == "complete") {
-          showTab();
-          return;
-        }
+          iframe.addEventListener("DOMContentLoaded", function documentDOMLoaded() {
+            iframe.removeEventListener("DOMContentLoaded", documentDOMLoaded, true);
+            injectLoopAPI(iframe.contentWindow);
+            iframe.contentWindow.addEventListener("loopPanelInitialized", function loopPanelInitialized() {
+              iframe.contentWindow.removeEventListener("loopPanelInitialized",
+                                                       loopPanelInitialized);
+              showTab();
+            });
+          }, true);
+        };
 
-        iframe.addEventListener("DOMContentLoaded", function documentDOMLoaded() {
-          iframe.removeEventListener("DOMContentLoaded", documentDOMLoaded, true);
-          injectLoopAPI(iframe.contentWindow);
-          iframe.contentWindow.addEventListener("loopPanelInitialized", function loopPanelInitialized() {
-            iframe.contentWindow.removeEventListener("loopPanelInitialized",
-              loopPanelInitialized);
-            showTab();
-          });
-        }, true);
-      };
+        // Used to clear the temporary "login" state from the button.
+        Services.obs.notifyObservers(null, "loop-status-changed", null);
 
-      // Used to clear the temporary "login" state from the button.
-      Services.obs.notifyObservers(null, "loop-status-changed", null);
-
-      PanelFrame.showPopup(window, event ? event.target : this.toolbarButton.node,
-                           "loop", null, "about:looppanel", null, callback);
+        PanelFrame.showPopup(window, event ? event.target : this.toolbarButton.node,
+                             "loop", null, "about:looppanel", null, callback);
+      });
     },
 
     /**
      * Triggers the initialization of the loop service.  Called by
      * delayedStartup.
      */
     init: function() {
       // Add observer notifications before the service is initialized
--- a/browser/components/loop/test/mochitest/head.js
+++ b/browser/components/loop/test/mochitest/head.js
@@ -59,17 +59,17 @@ function promiseGetMozLoopAPI() {
     // Remove the iframe after each test. This also avoids mochitest complaining
     // about leaks on shutdown as we intentionally hold the iframe open for the
     // life of the application.
     registerCleanupFunction(function() {
       loopPanel.hidePopup();
       let frameId = btn.getAttribute("notificationFrameId");
       let frame = document.getElementById(frameId);
       if (frame) {
-        loopPanel.removeChild(frame);
+        frame.remove();
       }
     });
   });
 }
 
 /**
  * Loads the loop panel by clicking the button and waits for its open to complete.
  * It also registers
--- a/browser/modules/UITour.jsm
+++ b/browser/modules/UITour.jsm
@@ -106,25 +106,25 @@ this.UITour = {
       query: (aDocument) => {
         let customizeButton = aDocument.getElementById("PanelUI-customize");
         return aDocument.getAnonymousElementByAttribute(customizeButton,
                                                         "class",
                                                         "toolbarbutton-icon");
       },
       widgetName: "PanelUI-customize",
     }],
+    ["devtools",    {query: "#developer-button"}],
     ["help",        {query: "#PanelUI-help"}],
     ["home",        {query: "#home-button"}],
-    ["loop",        {query: "#loop-button"}],
-    ["devtools",    {query: "#developer-button"}],
-    ["webide",      {query: "#webide-button"}],
     ["forget", {
       query: "#panic-button",
       widgetName: "panic-button",
-      allowAdd: true }],
+      allowAdd: true,
+    }],
+    ["loop",        {query: "#loop-button"}],
     ["privateWindow",  {query: "#privatebrowsing-button"}],
     ["quit",        {query: "#PanelUI-quit"}],
     ["search",      {
       query: "#searchbar",
       widgetName: "search-container",
     }],
     ["searchProvider", {
       query: (aDocument) => {
@@ -183,16 +183,17 @@ this.UITour = {
         }
         return element;
       },
     }],
     ["urlbar",      {
       query: "#urlbar",
       widgetName: "urlbar-container",
     }],
+    ["webide",      {query: "#webide-button"}],
   ]),
 
   init: function() {
     log.debug("Initializing UITour");
     // Lazy getter is initialized here so it can be replicated any time
     // in a test.
     delete this.seenPageIDs;
     Object.defineProperty(this, "seenPageIDs", {
@@ -709,36 +710,41 @@ this.UITour = {
     return {
       seenPageIDs: [...this.seenPageIDs.keys()],
     };
   },
 
   teardownTour: function(aWindow, aWindowClosing = false) {
     log.debug("teardownTour: aWindowClosing = " + aWindowClosing);
     aWindow.gBrowser.tabContainer.removeEventListener("TabSelect", this);
-    aWindow.PanelUI.panel.removeEventListener("popuphiding", this.hidePanelAnnotations);
-    aWindow.PanelUI.panel.removeEventListener("ViewShowing", this.hidePanelAnnotations);
     aWindow.removeEventListener("SSWindowClosing", this);
 
     let originTabs = this.originTabs.get(aWindow);
     if (originTabs) {
       for (let tab of originTabs) {
         tab.removeEventListener("TabClose", this);
         tab.removeEventListener("TabBecomingWindow", this);
       }
     }
     this.originTabs.delete(aWindow);
 
     if (!aWindowClosing) {
       this.hideHighlight(aWindow);
       this.hideInfo(aWindow);
       // Ensure the menu panel is hidden before calling recreatePopup so popup events occur.
       this.hideMenu(aWindow, "appMenu");
+      this.hideMenu(aWindow, "loop");
     }
 
+    // Clean up panel listeners after we may have called hideMenu above.
+    aWindow.PanelUI.panel.removeEventListener("popuphiding", this.hidePanelAnnotations);
+    aWindow.PanelUI.panel.removeEventListener("ViewShowing", this.hidePanelAnnotations);
+    let loopPanel = aWindow.document.getElementById("loop-notification-panel");
+    loopPanel.removeEventListener("popuphidden", this.onLoopPanelHidden);
+
     this.endUrlbarCapture(aWindow);
     this.removePinnedTab(aWindow);
     this.resetTheme();
   },
 
   getChromeWindow: function(aContentDocument) {
     return aContentDocument.defaultView
                            .window
@@ -862,17 +868,17 @@ this.UITour = {
   },
 
   /**
    * Called before opening or after closing a highlight or info panel to see if
    * we need to open or close the appMenu to see the annotation's anchor.
    */
   _setAppMenuStateForAnnotation: function(aWindow, aAnnotationType, aShouldOpenForHighlight, aCallback = null) {
     log.debug("_setAppMenuStateForAnnotation:", aAnnotationType);
-    log.debug("_setAppMenuStateForAnnotation: Menu is exptected to be:", aShouldOpenForHighlight ? "open" : "closed");
+    log.debug("_setAppMenuStateForAnnotation: Menu is expected to be:", aShouldOpenForHighlight ? "open" : "closed");
 
     // If the panel is in the desired state, we're done.
     let panelIsOpen = aWindow.PanelUI.panel.state != "closed";
     if (aShouldOpenForHighlight == panelIsOpen) {
       log.debug("_setAppMenuStateForAnnotation: Panel already in expected state");
       if (aCallback)
         aCallback();
       return;
@@ -1233,16 +1239,36 @@ this.UITour = {
       aWindow.PanelUI.panel.addEventListener("ViewShowing", this.hidePanelAnnotations);
       if (aOpenCallback) {
         aWindow.PanelUI.panel.addEventListener("popupshown", onPopupShown);
       }
       aWindow.PanelUI.show();
     } else if (aMenuName == "bookmarks") {
       let menuBtn = aWindow.document.getElementById("bookmarks-menu-button");
       openMenuButton(menuBtn);
+    } else if (aMenuName == "loop") {
+      let toolbarButton = aWindow.LoopUI.toolbarButton;
+      if (!toolbarButton || !toolbarButton.node) {
+        return;
+      }
+
+      let panel = aWindow.document.getElementById("loop-notification-panel");
+      panel.setAttribute("noautohide", true);
+      if (panel.state != "open") {
+        this.recreatePopup(panel);
+      }
+
+      // An event object is expected but we don't want to toggle the panel with a click if the panel
+      // is already open.
+      aWindow.LoopUI.openCallPanel({ target: toolbarButton.node, }).then(() => {
+        if (aOpenCallback) {
+          aOpenCallback();
+        }
+      });
+      panel.addEventListener("popuphidden", this.onLoopPanelHidden);
     } else if (aMenuName == "searchEngines") {
       this.getTarget(aWindow, "searchProvider").then(target => {
         openMenuButton(target.node);
       }).catch(log.error);
     }
   },
 
   hideMenu: function(aWindow, aMenuName) {
@@ -1253,16 +1279,19 @@ this.UITour = {
 
     if (aMenuName == "appMenu") {
       aWindow.PanelUI.panel.removeAttribute("noautohide");
       aWindow.PanelUI.hide();
       this.recreatePopup(aWindow.PanelUI.panel);
     } else if (aMenuName == "bookmarks") {
       let menuBtn = aWindow.document.getElementById("bookmarks-menu-button");
       closeMenuButton(menuBtn);
+    } else if (aMenuName == "loop") {
+      let panel = aWindow.document.getElementById("loop-notification-panel");
+      panel.hidePopup();
     } else if (aMenuName == "searchEngines") {
       let menuBtn = this.targets.get("searchProvider").query(aWindow.document);
       closeMenuButton(menuBtn);
     }
   },
 
   hidePanelAnnotations: function(aEvent) {
     let win = aEvent.target.ownerDocument.defaultView;
@@ -1284,16 +1313,21 @@ this.UITour = {
           }
           hideMethod(win);
         }).catch(log.error);
       }
     });
     UITour.appMenuOpenForAnnotation.clear();
   },
 
+  onLoopPanelHidden: function(aEvent) {
+    aEvent.target.removeAttribute("noautohide");
+    UITour.recreatePopup(aEvent.target);
+  },
+
   recreatePopup: function(aPanel) {
     // After changing popup attributes that relate to how the native widget is created
     // (e.g. @noautohide) we need to re-create the frame/widget for it to take effect.
     if (aPanel.hidden) {
       // If the panel is already hidden, we don't need to recreate it but flush
       // in case someone just hid it.
       aPanel.clientWidth; // flush
       return;
--- a/browser/modules/test/browser.ini
+++ b/browser/modules/test/browser.ini
@@ -23,16 +23,18 @@ skip-if = e10s # Bug 941428 - UITour.jsm
 [browser_UITour3.js]
 skip-if = os == "linux" || e10s # Linux: Bug 986760, Bug 989101; e10s: Bug 941428 - UITour.jsm not e10s friendly
 [browser_UITour_availableTargets.js]
 skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly
 [browser_UITour_detach_tab.js]
 skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly
 [browser_UITour_annotation_size_attributes.js]
 skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly.
+[browser_UITour_loop.js]
+skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly.
 [browser_UITour_modalDialog.js]
 run-if = os == "mac" # modal dialog disabling only working on OS X
 skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly
 [browser_UITour_panel_close_annotation.js]
 skip-if = true # Disabled due to frequent failures, bugs 1026310 and 1032137
 [browser_UITour_registerPageID.js]
 skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly
 [browser_UITour_sync.js]
--- a/browser/modules/test/browser_UITour2.js
+++ b/browser/modules/test/browser_UITour2.js
@@ -84,17 +84,17 @@ let tests = [
     tabInfo = UITour.pinnedTabs.get(window);
     is(tabInfo, null, "Should not have any data about the removed pinned tab after removePinnedTab()");
 
     yield addPinnedTabPromise();
     yield addPinnedTabPromise();
     yield addPinnedTabPromise();
     is(gBrowser.tabs[1].pinned, false, "After multiple calls of addPinnedTab, should still only have one pinned tab");
   }),
-  taskify(function* test_menu() {
+  taskify(function* test_bookmarks_menu() {
     let bookmarksMenuButton = document.getElementById("bookmarks-menu-button");
 
     ise(bookmarksMenuButton.open, false, "Menu should initially be closed");
     gContentAPI.showMenu("bookmarks");
 
     yield waitForConditionPromise(() => {
       return bookmarksMenuButton.open;
     }, "Menu should be visible after showMenu()");
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/browser_UITour_loop.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let gTestTab;
+let gContentAPI;
+let gContentWindow;
+let loopButton;
+let loopPanel = document.getElementById("loop-notification-panel");
+
+Components.utils.import("resource:///modules/UITour.jsm");
+
+function test() {
+  UITourTest();
+}
+
+let tests = [
+  taskify(function* test_menu_show_hide() {
+    ise(loopButton.open, false, "Menu should initially be closed");
+    gContentAPI.showMenu("loop");
+
+    yield waitForConditionPromise(() => {
+      return loopButton.open;
+    }, "Menu should be visible after showMenu()");
+
+    ok(loopPanel.hasAttribute("noautohide"), "@noautohide should be on the loop panel");
+    ok(loopPanel.hasAttribute("panelopen"), "The panel should have @panelopen");
+    is(loopPanel.state, "open", "The panel should be open");
+    ok(loopButton.hasAttribute("open"), "Loop button should know that the menu is open");
+
+    gContentAPI.hideMenu("loop");
+    yield waitForConditionPromise(() => {
+        return !loopButton.open;
+    }, "Menu should be hidden after hideMenu()");
+
+    checkLoopPanelIsHidden();
+  }),
+  // Test the menu was cleaned up in teardown.
+  taskify(function* setup_menu_cleanup() {
+    gContentAPI.showMenu("loop");
+
+    yield waitForConditionPromise(() => {
+      return loopButton.open;
+    }, "Menu should be visible after showMenu()");
+
+    // Leave it open so it gets torn down and we can test below that teardown was succesful.
+  }),
+  taskify(function* test_menu_cleanup() {
+    // Test that the open menu from above was torn down fully.
+    checkLoopPanelIsHidden();
+  }),
+];
+
+function checkLoopPanelIsHidden() {
+  ok(!loopPanel.hasAttribute("noautohide"), "@noautohide on the loop panel should have been cleaned up");
+  ok(!loopPanel.hasAttribute("panelopen"), "The panel shouldn't have @panelopen");
+  isnot(loopPanel.state, "open", "The panel shouldn't be open");
+  is(loopButton.hasAttribute("open"), false, "Loop button should know that the panel is closed");
+}
+
+if (Services.prefs.getBoolPref("loop.enabled")) {
+  loopButton = window.LoopUI.toolbarButton.node;
+  registerCleanupFunction(() => {
+    // Copied from browser/components/loop/test/mochitest/head.js
+    // Remove the iframe after each test. This also avoids mochitest complaining
+    // about leaks on shutdown as we intentionally hold the iframe open for the
+    // life of the application.
+    let frameId = loopButton.getAttribute("notificationFrameId");
+    let frame = document.getElementById(frameId);
+    if (frame) {
+      frame.remove();
+    }
+  });
+} else {
+  ok(true, "Loop is disabled so skip the UITour Loop tests");
+  tests = [];
+}
--- a/browser/themes/shared/incontentprefs/preferences.inc.css
+++ b/browser/themes/shared/incontentprefs/preferences.inc.css
@@ -29,17 +29,17 @@ treecol {
   list-style-image: url("chrome://browser/skin/preferences/in-content/icons.png");
 }
 
 #category-general > .category-icon {
   -moz-image-region: rect(0, 24px, 24px, 0);
 }
 
 #category-search > .category-icon {
-  -moz-image-region: rect(0, 194px, 24px, 168px);
+  -moz-image-region: rect(0, 192px, 24px, 168px);
 }
 
 #category-content > .category-icon {
   -moz-image-region: rect(0, 48px, 24px, 24px);
 }
 
 #category-application > .category-icon {
   -moz-image-region: rect(0, 72px, 24px, 48px);
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -742,16 +742,17 @@ browser.jar:
         skin/classic/aero/browser/devtools/breadcrumbs-divider@2x.png      (../shared/devtools/images/breadcrumbs-divider@2x.png)
         skin/classic/aero/browser/devtools/breadcrumbs-scrollbutton.png    (../shared/devtools/images/breadcrumbs-scrollbutton.png)
         skin/classic/aero/browser/devtools/breadcrumbs-scrollbutton@2x.png (../shared/devtools/images/breadcrumbs-scrollbutton@2x.png)
 *       skin/classic/aero/browser/devtools/canvasdebugger.css        (devtools/canvasdebugger.css)
 *       skin/classic/aero/browser/devtools/debugger.css              (devtools/debugger.css)
         skin/classic/aero/browser/devtools/eyedropper.css            (../shared/devtools/eyedropper.css)
 *       skin/classic/aero/browser/devtools/netmonitor.css            (devtools/netmonitor.css)
 *       skin/classic/aero/browser/devtools/profiler.css              (devtools/profiler.css)
+*       skin/classic/aero/browser/devtools/performance.css           (devtools/performance.css)
 *       skin/classic/aero/browser/devtools/timeline.css              (devtools/timeline.css)
 *       skin/classic/aero/browser/devtools/scratchpad.css            (devtools/scratchpad.css)
 *       skin/classic/aero/browser/devtools/shadereditor.css          (devtools/shadereditor.css)
 *       skin/classic/aero/browser/devtools/splitview.css             (../shared/devtools/splitview.css)
         skin/classic/aero/browser/devtools/styleeditor.css           (../shared/devtools/styleeditor.css)
         skin/classic/aero/browser/devtools/storage.css               (../shared/devtools/storage.css)
 *       skin/classic/aero/browser/devtools/webaudioeditor.css        (devtools/webaudioeditor.css)
         skin/classic/aero/browser/devtools/magnifying-glass.png      (../shared/devtools/images/magnifying-glass.png)
--- a/services/sync/modules/engines/tabs.js
+++ b/services/sync/modules/engines/tabs.js
@@ -1,19 +1,18 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-this.EXPORTED_SYMBOLS = ['TabEngine', 'TabSetRecord'];
+this.EXPORTED_SYMBOLS = ["TabEngine", "TabSetRecord"];
 
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cu = Components.utils;
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
-const TABS_TTL = 604800; // 7 days
+const TABS_TTL = 604800;           // 7 days.
+const TAB_ENTRIES_LIMIT = 25;      // How many URLs to include in tab history.
 
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/engines/clients.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/constants.js");
@@ -22,60 +21,60 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 this.TabSetRecord = function TabSetRecord(collection, id) {
   CryptoWrapper.call(this, collection, id);
 }
 TabSetRecord.prototype = {
   __proto__: CryptoWrapper.prototype,
   _logName: "Sync.Record.Tabs",
-  ttl: TABS_TTL
+  ttl: TABS_TTL,
 };
 
 Utils.deferGetSet(TabSetRecord, "cleartext", ["clientName", "tabs"]);
 
 
 this.TabEngine = function TabEngine(service) {
   SyncEngine.call(this, "Tabs", service);
 
-  // Reset the client on every startup so that we fetch recent tabs
+  // Reset the client on every startup so that we fetch recent tabs.
   this._resetClient();
 }
 TabEngine.prototype = {
   __proto__: SyncEngine.prototype,
   _storeObj: TabStore,
   _trackerObj: TabTracker,
   _recordObj: TabSetRecord,
 
   syncPriority: 3,
 
-  getChangedIDs: function getChangedIDs() {
+  getChangedIDs: function () {
     // No need for a proper timestamp (no conflict resolution needed).
     let changedIDs = {};
     if (this._tracker.modified)
       changedIDs[this.service.clientsEngine.localID] = 0;
     return changedIDs;
   },
 
-  // API for use by Weave UI code to give user choices of tabs to open:
-  getAllClients: function TabEngine_getAllClients() {
+  // API for use by Sync UI code to give user choices of tabs to open.
+  getAllClients: function () {
     return this._store._remoteClients;
   },
 
-  getClientById: function TabEngine_getClientById(id) {
+  getClientById: function (id) {
     return this._store._remoteClients[id];
   },
 
-  _resetClient: function TabEngine__resetClient() {
+  _resetClient: function () {
     SyncEngine.prototype._resetClient.call(this);
     this._store.wipe();
     this._tracker.modified = true;
   },
 
-  removeClientData: function removeClientData() {
+  removeClientData: function () {
     let url = this.engineURL + "/" + this.service.clientsEngine.localID;
     this.service.resource(url).delete();
   },
 
   /**
    * Return a Set of open URLs.
    */
   getOpenURLs: function () {
@@ -89,17 +88,17 @@ TabEngine.prototype = {
 
 
 function TabStore(name, engine) {
   Store.call(this, name, engine);
 }
 TabStore.prototype = {
   __proto__: Store.prototype,
 
-  itemExists: function TabStore_itemExists(id) {
+  itemExists: function (id) {
     return id == this.engine.service.clientsEngine.localID;
   },
 
   getWindowEnumerator: function () {
     return Services.wm.getEnumerator("navigator:browser");
   },
 
   shouldSkipWindow: function (win) {
@@ -126,41 +125,58 @@ TabStore.prototype = {
       for (let tab of win.gBrowser.tabs) {
         tabState = this.getTabState(tab);
 
         // Make sure there are history entries to look at.
         if (!tabState || !tabState.entries.length) {
           continue;
         }
 
-        // Until we store full or partial history, just grab the current entry.
-        // index is 1 based, so make sure we adjust.
-        let entry = tabState.entries[tabState.index - 1];
+        let acceptable = !filter ? (url) => url :
+                                   (url) => url && !filteredUrls.test(url);
 
-        // Filter out some urls if necessary. SessionStore can return empty
-        // tabs in some cases - easiest thing is to just ignore them for now.
-        if (!entry.url || filter && filteredUrls.test(entry.url)) {
+        let entries = tabState.entries;
+        let index = tabState.index;
+        let current = entries[index - 1];
+
+        // We ignore the tab completely if the current entry url is
+        // not acceptable (we need something accurate to open).
+        if (!acceptable(current.url)) {
           continue;
         }
 
-        // I think it's also possible that attributes[.image] might not be set
-        // so handle that as well.
+        // The element at `index` is the current page. Previous URLs were
+        // previously visited URLs; subsequent URLs are in the 'forward' stack,
+        // which we can't represent in Sync, so we truncate here.
+        let candidates = (entries.length == index) ?
+                         entries :
+                         entries.slice(0, index);
+
+        let urls = candidates.map((entry) => entry.url)
+                             .filter(acceptable)
+                             .reverse();                       // Because Sync puts current at index 0, and history after.
+
+        // Truncate if necessary.
+        if (urls.length > TAB_ENTRIES_LIMIT) {
+          urls.length = TAB_ENTRIES_LIMIT;
+        }
+
         allTabs.push({
-          title: entry.title || "",
-          urlHistory: [entry.url],
+          title: current.title || "",
+          urlHistory: urls,
           icon: tabState.attributes && tabState.attributes.image || "",
-          lastUsed: Math.floor((tabState.lastAccessed || 0) / 1000)
+          lastUsed: Math.floor((tabState.lastAccessed || 0) / 1000),
         });
       }
     }
 
     return allTabs;
   },
 
-  createRecord: function createRecord(id, collection) {
+  createRecord: function (id, collection) {
     let record = new TabSetRecord(collection, id);
     record.clientName = this.engine.service.clientsEngine.localName;
 
     // Sort tabs in descending-used order to grab the most recently used
     let tabs = this.getAllTabs(true).sort(function (a, b) {
       return b.lastUsed - a.lastUsed;
     });
 
@@ -183,17 +199,17 @@ TabStore.prototype = {
     tabs.forEach(function (tab) {
       this._log.trace("Wrapping tab: " + JSON.stringify(tab));
     }, this);
 
     record.tabs = tabs;
     return record;
   },
 
-  getAllIDs: function TabStore_getAllIds() {
+  getAllIDs: function () {
     // Don't report any tabs if all windows are in private browsing for
     // first syncs.
     let ids = {};
     let allWindowsArePrivate = false;
     let wins = Services.wm.getEnumerator("navigator:browser");
     while (wins.hasMoreElements()) {
       if (PrivateBrowsingUtils.isWindowPrivate(wins.getNext())) {
         // Ensure that at least there is a private window.
@@ -209,80 +225,88 @@ TabStore.prototype = {
         !PrivateBrowsingUtils.permanentPrivateBrowsing) {
       return ids;
     }
 
     ids[this.engine.service.clientsEngine.localID] = true;
     return ids;
   },
 
-  wipe: function TabStore_wipe() {
+  wipe: function () {
     this._remoteClients = {};
   },
 
-  create: function TabStore_create(record) {
+  create: function (record) {
     this._log.debug("Adding remote tabs from " + record.clientName);
     this._remoteClients[record.id] = record.cleartext;
 
-    // Lose some precision, but that's good enough (seconds)
+    // Lose some precision, but that's good enough (seconds).
     let roundModify = Math.floor(record.modified / 1000);
     let notifyState = Svc.Prefs.get("notifyTabState");
-    // If there's no existing pref, save this first modified time
-    if (notifyState == null)
+
+    // If there's no existing pref, save this first modified time.
+    if (notifyState == null) {
       Svc.Prefs.set("notifyTabState", roundModify);
-    // Don't change notifyState if it's already 0 (don't notify)
-    else if (notifyState == 0)
+      return;
+    }
+
+    // Don't change notifyState if it's already 0 (don't notify).
+    if (notifyState == 0) {
       return;
-    // We must have gotten a new tab that isn't the same as last time
-    else if (notifyState != roundModify)
+    }
+
+    // We must have gotten a new tab that isn't the same as last time.
+    if (notifyState != roundModify) {
       Svc.Prefs.set("notifyTabState", 0);
+    }
   },
 
-  update: function update(record) {
+  update: function (record) {
     this._log.trace("Ignoring tab updates as local ones win");
-  }
+  },
 };
 
 
 function TabTracker(name, engine) {
   Tracker.call(this, name, engine);
   Svc.Obs.add("weave:engine:start-tracking", this);
   Svc.Obs.add("weave:engine:stop-tracking", this);
 
-  // Make sure "this" pointer is always set correctly for event listeners
+  // Make sure "this" pointer is always set correctly for event listeners.
   this.onTab = Utils.bind2(this, this.onTab);
   this._unregisterListeners = Utils.bind2(this, this._unregisterListeners);
 }
 TabTracker.prototype = {
   __proto__: Tracker.prototype,
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
 
-  loadChangedIDs: function loadChangedIDs() {
+  loadChangedIDs: function () {
     // Don't read changed IDs from disk at start up.
   },
 
-  clearChangedIDs: function clearChangedIDs() {
+  clearChangedIDs: function () {
     this.modified = false;
   },
 
   _topics: ["pageshow", "TabOpen", "TabClose", "TabSelect"],
-  _registerListenersForWindow: function registerListenersFW(window) {
+
+  _registerListenersForWindow: function (window) {
     this._log.trace("Registering tab listeners in window");
     for each (let topic in this._topics) {
       window.addEventListener(topic, this.onTab, false);
     }
     window.addEventListener("unload", this._unregisterListeners, false);
   },
 
-  _unregisterListeners: function unregisterListeners(event) {
+  _unregisterListeners: function (event) {
     this._unregisterListenersForWindow(event.target);
   },
 
-  _unregisterListenersForWindow: function unregisterListenersFW(window) {
+  _unregisterListenersForWindow: function (window) {
     this._log.trace("Removing tab listeners in window");
     window.removeEventListener("unload", this._unregisterListeners, false);
     for each (let topic in this._topics) {
       window.removeEventListener(topic, this.onTab, false);
     }
   },
 
   startTracking: function () {
@@ -313,28 +337,29 @@ TabTracker.prototype = {
         };
 
         // Add tab listeners now that a window has opened.
         subject.addEventListener("load", onLoad, false);
         break;
     }
   },
 
-  onTab: function onTab(event) {
+  onTab: function (event) {
     if (event.originalTarget.linkedBrowser) {
       let browser = event.originalTarget.linkedBrowser;
       if (PrivateBrowsingUtils.isBrowserPrivate(browser) &&
           !PrivateBrowsingUtils.permanentPrivateBrowsing) {
         this._log.trace("Ignoring tab event from private browsing.");
         return;
       }
     }
 
     this._log.trace("onTab event: " + event.type);
     this.modified = true;
 
     // For page shows, bump the score 10% of the time, emulating a partial
     // score. We don't want to sync too frequently. For all other page
     // events, always bump the score.
-    if (event.type != "pageshow" || Math.random() < .1)
+    if (event.type != "pageshow" || Math.random() < .1) {
       this.score += SCORE_INCREMENT_SMALL;
+    }
   },
-}
+};
--- a/services/sync/tests/unit/head_helpers.js
+++ b/services/sync/tests/unit/head_helpers.js
@@ -133,36 +133,41 @@ function mockShouldSkipWindow (win) {
   return win.closed ||
          win.mockIsPrivate;
 }
 
 function mockGetTabState (tab) {
   return tab;
 }
 
-function mockGetWindowEnumerator(url, numWindows, numTabs) {
+function mockGetWindowEnumerator(url, numWindows, numTabs, indexes, moreURLs) {
   let elements = [];
+
+  function url2entry(url) {
+    return {
+      url: ((typeof url == "function") ? url() : url),
+      title: "title"
+    };
+  }
+
   for (let w = 0; w < numWindows; ++w) {
     let tabs = [];
     let win = {
       closed: false,
       mockIsPrivate: false,
       gBrowser: {
         tabs: tabs,
       },
     };
     elements.push(win);
 
     for (let t = 0; t < numTabs; ++t) {
       tabs.push(TestingUtils.deepCopy({
-        index: 1,
-        entries: [{
-          url: ((typeof url == "string") ? url : url()),
-          title: "title"
-        }],
+        index: indexes ? indexes() : 1,
+        entries: (moreURLs ? [url].concat(moreURLs()) : [url]).map(url2entry),
         attributes: {
           image: "image"
         },
         lastAccessed: 1499
       }));
     }
   }
 
--- a/services/sync/tests/unit/test_tab_store.js
+++ b/services/sync/tests/unit/test_tab_store.js
@@ -47,33 +47,53 @@ function test_create() {
   // reset the notifyTabState
   Svc.Prefs.reset("notifyTabState");
 }
 
 function test_getAllTabs() {
   let store = getMockStore();
   let tabs;
 
-  store.getWindowEnumerator = mockGetWindowEnumerator.bind(this, "http://foo.com", 1, 1);
+  let threeUrls = ["http://foo.com", "http://fuubar.com", "http://barbar.com"];
+
+  store.getWindowEnumerator = mockGetWindowEnumerator.bind(this, "http://bar.com", 1, 1, () => 2, () => threeUrls);
 
   _("Get all tabs.");
   tabs = store.getAllTabs();
   _("Tabs: " + JSON.stringify(tabs));
   do_check_eq(tabs.length, 1);
   do_check_eq(tabs[0].title, "title");
-  do_check_eq(tabs[0].urlHistory.length, 1);
-  do_check_eq(tabs[0].urlHistory[0], ["http://foo.com"]);
+  do_check_eq(tabs[0].urlHistory.length, 2);
+  do_check_eq(tabs[0].urlHistory[0], "http://foo.com");
+  do_check_eq(tabs[0].urlHistory[1], "http://bar.com");
   do_check_eq(tabs[0].icon, "image");
   do_check_eq(tabs[0].lastUsed, 1);
 
   _("Get all tabs, and check that filtering works.");
-  store.getWindowEnumerator = mockGetWindowEnumerator.bind(this, "about:foo", 1, 1);
+  let twoUrls = ["about:foo", "http://fuubar.com"];
+  store.getWindowEnumerator = mockGetWindowEnumerator.bind(this, "http://foo.com", 1, 1, () => 2, () => twoUrls);
   tabs = store.getAllTabs(true);
   _("Filtered: " + JSON.stringify(tabs));
   do_check_eq(tabs.length, 0);
+
+  _("Get all tabs, and check that the entries safety limit works.");
+  let allURLs = [];
+  for (let i = 0; i < 50; i++) {
+    allURLs.push("http://foo" + i + ".bar");
+  }
+  allURLs.splice(35, 0, "about:foo", "about:bar", "about:foobar");
+
+  store.getWindowEnumerator = mockGetWindowEnumerator.bind(this, "http://bar.com", 1, 1, () => 45, () => allURLs);
+  tabs = store.getAllTabs((url) => url.startsWith("about"));
+
+  _("Sliced: " + JSON.stringify(tabs));
+  do_check_eq(tabs.length, 1);
+  do_check_eq(tabs[0].urlHistory.length, 25);
+  do_check_eq(tabs[0].urlHistory[0], "http://foo40.bar");
+  do_check_eq(tabs[0].urlHistory[24], "http://foo16.bar");
 }
 
 function test_createRecord() {
   let store = getMockStore();
   let record;
 
   store.getTabState = mockGetTabState;
   store.shouldSkipWindow = mockShouldSkipWindow;