Bug 604699 - Store thumbnails in the browser's image cache rather than in sessionstore [r=ian,sdwilsh, a=blocking2.0:final+]
authorTim Taubert <tim.taubert@gmx.de>
Sat, 19 Feb 2011 19:22:49 +0100
changeset 62866 4ae4b97f392bafd1fb8e7d6651f1c00da902db4d
parent 62865 97625b507157c24adef2f2ac31692c0a2a5bb718
child 62867 61c6c43d9630a4496d65e03d0f83f24c231b70c5
push idunknown
push userunknown
push dateunknown
reviewersian, sdwilsh, blocking2.0
bugs604699
milestone2.0b12pre
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
Bug 604699 - Store thumbnails in the browser's image cache rather than in sessionstore [r=ian,sdwilsh, a=blocking2.0:final+] (Based on patch from Sean Dunn <seanedunn@yahoo.com>.)
browser/base/content/tabview/storage.js
browser/base/content/tabview/tabitems.js
browser/base/content/tabview/tabview.js
browser/base/content/tabview/ui.js
browser/base/content/test/tabview/Makefile.in
browser/base/content/test/tabview/browser_tabview_bug597248.js
browser/base/content/test/tabview/browser_tabview_bug604699.js
browser/base/content/test/tabview/browser_tabview_bug624265.js
browser/base/content/test/tabview/browser_tabview_bug627288.js
--- a/browser/base/content/tabview/storage.js
+++ b/browser/base/content/tabview/storage.js
@@ -42,30 +42,48 @@
 // ##########
 // Class: Storage
 // Singleton for permanent storage of TabView data.
 let Storage = {
   GROUP_DATA_IDENTIFIER: "tabview-group",
   GROUPS_DATA_IDENTIFIER: "tabview-groups",
   TAB_DATA_IDENTIFIER: "tabview-tab",
   UI_DATA_IDENTIFIER: "tabview-ui",
+  CACHE_CLIENT_IDENTIFIER: "tabview-cache",
+  CACHE_PREFIX: "moz-panorama:",
 
   // ----------
   // Function: init
   // Sets up the object.
   init: function Storage_init() {
     this._sessionStore =
       Cc["@mozilla.org/browser/sessionstore;1"].
         getService(Ci.nsISessionStore);
+    
+    // Create stream-based cache session for tabview
+    let cacheService = 
+      Cc["@mozilla.org/network/cache-service;1"].
+        getService(Ci.nsICacheService);
+    this._cacheSession = cacheService.createSession(
+      this.CACHE_CLIENT_IDENTIFIER, Ci.nsICache.STORE_ON_DISK, true);
+    this.StringInputStream = Components.Constructor(
+      "@mozilla.org/io/string-input-stream;1", "nsIStringInputStream",
+      "setData");
+    this.StorageStream = Components.Constructor(
+      "@mozilla.org/storagestream;1", "nsIStorageStream", 
+      "init");
   },
 
   // ----------
   // Function: uninit
   uninit: function Storage_uninit () {
     this._sessionStore = null;
+    this._cacheSession = null;
+    this.StringInputStream = null;
+    this.StorageStream = null;
   },
 
   // ----------
   // Function: wipe
   // Cleans out all the stored data, leaving empty objects.
   wipe: function Storage_wipe() {
     try {
       var self = this;
@@ -85,46 +103,205 @@ let Storage = {
       this._sessionStore.setWindowValue(gWindow, this.GROUP_DATA_IDENTIFIER,
         JSON.stringify({}));
     } catch (e) {
       Utils.log("Error in wipe: "+e);
     }
   },
 
   // ----------
+  // Function: _openCacheEntry
+  // Opens a cache entry for the given <url> and requests access <access>.
+  // Calls <successCallback>(entry) when the entry was successfully opened with
+  // requested access rights. Otherwise calls <errorCallback>().
+  _openCacheEntry: function Storage__openCacheEntry(url, access, successCallback, errorCallback) {
+    let onCacheEntryAvailable = function (entry, accessGranted, status) {
+      if (entry && access == accessGranted && Components.isSuccessCode(status)) {
+        successCallback(entry);
+      } else {
+        entry && entry.close();
+        errorCallback();
+      }
+    }
+
+    let key = this.CACHE_PREFIX + url;
+
+    // switch to synchronous mode if parent window is about to close
+    if (UI.isDOMWindowClosing) {
+      let entry = this._cacheSession.openCacheEntry(key, access, true);
+      let status = Components.results.NS_OK;
+      onCacheEntryAvailable(entry, entry.accessGranted, status);
+    } else {
+      let listener = new CacheListener(onCacheEntryAvailable);
+      this._cacheSession.asyncOpenCacheEntry(key, access, listener);
+    }
+  },
+
+  // ----------
+  // Function: saveThumbnail
+  // Saves the <imageData> to the cache using the given <url> as key.
+  // Calls <callback>(status) when finished (passing true or false indicating
+  // whether the operation succeeded).
+  saveThumbnail: function Storage_saveThumbnail(url, imageData, callback) {
+    Utils.assert(url, "url");
+    Utils.assert(imageData, "imageData");
+    Utils.assert(typeof callback == "function", "callback arg must be a function");
+
+    let self = this;
+    let StringInputStream = this.StringInputStream;
+
+    let onCacheEntryAvailable = function (entry) {
+      let outputStream = entry.openOutputStream(0);
+
+      let cleanup = function () {
+        outputStream.close();
+        entry.close();
+      }
+
+      // switch to synchronous mode if parent window is about to close
+      if (UI.isDOMWindowClosing) {
+        outputStream.write(imageData, imageData.length);
+        cleanup();
+        callback(true);
+        return;
+      }
+
+      // asynchronous mode
+      let inputStream = new StringInputStream(imageData, imageData.length);
+      gNetUtil.asyncCopy(inputStream, outputStream, function (result) {
+        cleanup();
+        inputStream.close();
+        callback(Components.isSuccessCode(result));
+      });
+    }
+
+    let onCacheEntryUnavailable = function () {
+      callback(false);
+    }
+
+    this._openCacheEntry(url, Ci.nsICache.ACCESS_WRITE,
+        onCacheEntryAvailable, onCacheEntryUnavailable);
+  },
+
+  // ----------
+  // Function: loadThumbnail
+  // Asynchrously loads image data from the cache using the given <url> as key.
+  // Calls <callback>(status, data) when finished, passing true or false
+  // (indicating whether the operation succeeded) and the retrieved image data.
+  loadThumbnail: function Storage_loadThumbnail(url, callback) {
+    Utils.assert(url, "url");
+    Utils.assert(typeof callback == "function", "callback arg must be a function");
+
+    let self = this;
+
+    let onCacheEntryAvailable = function (entry) {
+      let imageChunks = [];
+      let nativeInputStream = entry.openInputStream(0);
+
+      const CHUNK_SIZE = 0x10000; // 65k
+      const PR_UINT32_MAX = 0xFFFFFFFF;
+      let storageStream = new self.StorageStream(CHUNK_SIZE, PR_UINT32_MAX, null);
+      let storageOutStream = storageStream.getOutputStream(0);
+
+      let cleanup = function () {
+        nativeInputStream.close();
+        storageStream.close();
+        storageOutStream.close();
+        entry.close();
+      }
+
+      gNetUtil.asyncCopy(nativeInputStream, storageOutStream, function (result) {
+        // cancel if parent window has already been closed
+        if (typeof UI == "undefined") {
+          cleanup();
+          return;
+        }
+
+        let imageData = null;
+        let isSuccess = Components.isSuccessCode(result);
+
+        if (isSuccess) {
+          let storageInStream = storageStream.newInputStream(0);
+          imageData = gNetUtil.readInputStreamToString(storageInStream,
+            storageInStream.available());
+          storageInStream.close();
+        }
+
+        cleanup();
+        callback(isSuccess, imageData);
+      });
+    }
+
+    let onCacheEntryUnavailable = function () {
+      callback(false);
+    }
+
+    this._openCacheEntry(url, Ci.nsICache.ACCESS_READ,
+        onCacheEntryAvailable, onCacheEntryUnavailable);
+  },
+
+  // ----------
   // Function: saveTab
   // Saves the data for a single tab.
   saveTab: function Storage_saveTab(tab, data) {
     Utils.assert(tab, "tab");
 
+    if (data != null) {
+      let imageData = data.imageData;
+      // Remove imageData from payload
+      delete data.imageData;
+      if (imageData != null) {
+        this.saveThumbnail(data.url, imageData, function (status) {
+          if (status) {
+            // Notify subscribers
+            tab._tabViewTabItem._sendToSubscribers("savedCachedImageData");
+          } else {
+            Utils.log("Error while saving thumbnail: " + e);
+          }
+        });
+      }
+    }
+
     this._sessionStore.setTabValue(tab, this.TAB_DATA_IDENTIFIER,
       JSON.stringify(data));
-
-    // Notify subscribers
-    if (data && data.imageData && tab._tabViewTabItem)
-      tab._tabViewTabItem._sendToSubscribers("savedImageData");
   },
 
   // ----------
   // Function: getTabData
-  // Returns the data object associated with a single tab.
-  getTabData: function Storage_getTabData(tab) {
+  // Load tab data from session store and return it. Asynchrously loads the tab's
+  // thumbnail from the cache and calls <callback>(imageData) when done.
+  getTabData: function Storage_getTabData(tab, callback) {
     Utils.assert(tab, "tab");
+    Utils.assert(typeof callback == "function", "callback arg must be a function");
 
-    var existingData = null;
+    let existingData = null;
+
     try {
-      var tabData = this._sessionStore.getTabValue(tab, this.TAB_DATA_IDENTIFIER);
+      let tabData = this._sessionStore.getTabValue(tab, this.TAB_DATA_IDENTIFIER);
       if (tabData != "") {
         existingData = JSON.parse(tabData);
       }
     } catch (e) {
-      // getWindowValue will fail if the property doesn't exist
+      // getTabValue will fail if the property doesn't exist.
       Utils.log(e);
     }
 
+    if (existingData) {
+      this.loadThumbnail(existingData.url, function (status, imageData) {
+        if (status) {
+          callback(imageData);
+
+          // Notify subscribers
+          tab._tabViewTabItem._sendToSubscribers("loadedCachedImageData");
+        } else {
+          Utils.log("Error while loading thumbnail: " + e);
+        }
+      });
+    }
+
     return existingData;
   },
 
   // ----------
   // Function: saveGroupItem
   // Saves the data for a single groupItem, associated with a specific window.
   saveGroupItem: function Storage_saveGroupItem(win, data) {
     var id = data.id;
@@ -219,8 +396,25 @@ let Storage = {
         existingData = JSON.parse(data);
     } catch (e) {
       Utils.log("Error in readData: "+e);
     }
 
     return existingData;
   }
 };
+
+// ##########
+// Class: CacheListener
+// Generic CacheListener for feeding to asynchronous cache calls.
+// Calls <callback>(entry, access, status) when the requested cache entry
+// is available.
+function CacheListener(callback) {
+  Utils.assert(typeof callback == "function", "callback arg must be a function");
+  this.callback = callback;
+};
+
+CacheListener.prototype = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsICacheListener]),
+  onCacheEntryAvailable: function (entry, access, status) {
+    this.callback(entry, access, status);
+  }
+};
--- a/browser/base/content/tabview/tabitems.js
+++ b/browser/base/content/tabview/tabitems.js
@@ -323,56 +323,65 @@ TabItem.prototype = Utils.extend(new Ite
   // ----------
   // Function: _reconnect
   // Load the reciever's persistent data from storage. If there is none, 
   // treats it as a new tab. 
   _reconnect: function TabItem__reconnect() {
     Utils.assertThrow(!this._reconnected, "shouldn't already be reconnected");
     Utils.assertThrow(this.tab, "should have a xul:tab");
 
-    let tabData = Storage.getTabData(this.tab);
+    let tabData = null;
+    let self = this;
+    let imageDataCb = function(imageData) {
+      Utils.assertThrow(tabData, "tabData");
+      
+      tabData.imageData = imageData;
+
+      let currentUrl = self.tab.linkedBrowser.currentURI.spec;
+      // If we have a cached image, then show it if the loaded URL matches
+      // what the cache is from, OR the loaded URL is blank, which means
+      // that the page hasn't loaded yet.
+      if (tabData.imageData &&
+          (tabData.url == currentUrl || currentUrl == 'about:blank')) {
+        self.showCachedData(tabData);
+      }
+    };
+    // getTabData returns the sessionstore contents, but passes
+    // a callback to run when the thumbnail is finally loaded.
+    tabData = Storage.getTabData(this.tab, imageDataCb);
     if (tabData && TabItems.storageSanity(tabData)) {
-      if (this.parent)
-        this.parent.remove(this, {immediately: true});
+      if (self.parent)
+        self.parent.remove(self, {immediately: true});
 
-      this.setBounds(tabData.bounds, true);
+      self.setBounds(tabData.bounds, true);
 
       if (Utils.isPoint(tabData.userSize))
-        this.userSize = new Point(tabData.userSize);
+        self.userSize = new Point(tabData.userSize);
 
       if (tabData.groupID) {
         var groupItem = GroupItems.groupItem(tabData.groupID);
         if (groupItem) {
-          groupItem.add(this, {immediately: true});
+          groupItem.add(self, {immediately: true});
 
-          // if it matches the selected tab or no active tab and the browser 
+          // if it matches the selected tab or no active tab and the browser
           // tab is hidden, the active group item would be set.
-          if (this.tab == gBrowser.selectedTab || 
-              (!GroupItems.getActiveGroupItem() && !this.tab.hidden))
-            GroupItems.setActiveGroupItem(this.parent);
+          if (self.tab == gBrowser.selectedTab ||
+              (!GroupItems.getActiveGroupItem() && !self.tab.hidden))
+            GroupItems.setActiveGroupItem(self.parent);
         }
       }
-
-      let currentUrl = this.tab.linkedBrowser.currentURI.spec;
-
-      // If we have a cached image, then show it if the loaded URL matches
-      // what the cache is from, OR the loaded URL is blank, which means
-      // that the page hasn't loaded yet.
-      if (tabData.imageData && (tabData.url == currentUrl ||
-        currentUrl == 'about:blank'))
-        this.showCachedData(tabData);
     } else {
       // create tab by double click is handled in UI_init().
       if (!TabItems.creatingNewOrphanTab)
-        GroupItems.newTab(this, {immediately: true});
+        GroupItems.newTab(self, {immediately: true});
     }
 
-    this._reconnected = true;  
-    this.save();
-    this._sendToSubscribers("reconnected");
+    self._reconnected = true;
+    self.save();
+    self._sendToSubscribers("reconnected");
   },
   
   // ----------
   // Function: setHidden
   // Hide/unhide this item
   setHidden: function TabItem_setHidden(val) {
     if (val)
       this.addClass("tabHidden");
--- a/browser/base/content/tabview/tabview.js
+++ b/browser/base/content/tabview/tabview.js
@@ -18,16 +18,22 @@ XPCOMUtils.defineLazyGetter(this, "gPref
   return Services.prefs.getBranch("browser.panorama.");
 });
 
 XPCOMUtils.defineLazyGetter(this, "gPrivateBrowsing", function() {
   return Cc["@mozilla.org/privatebrowsing;1"].
            getService(Ci.nsIPrivateBrowsingService);
 });
 
+XPCOMUtils.defineLazyGetter(this, "gNetUtil", function() {
+  var obj = {};
+  Cu.import("resource://gre/modules/NetUtil.jsm", obj);
+  return obj.NetUtil;
+});
+
 var gWindow = window.parent;
 var gBrowser = gWindow.gBrowser;
 var gTabView = gWindow.TabView;
 var gTabViewDeck = gWindow.document.getElementById("tab-view-deck");
 var gTabViewFrame = gWindow.document.getElementById("tab-view");
 
 # NB: Certain files need to evaluate before others
 
--- a/browser/base/content/tabview/ui.js
+++ b/browser/base/content/tabview/ui.js
@@ -122,16 +122,21 @@ let UI = {
     transitionMode: "",
     wasInTabView: false 
   },
   
   // Variable: _storageBusyCount
   // Used to keep track of how many calls to storageBusy vs storageReady.
   _storageBusyCount: 0,
 
+  // Variable: isDOMWindowClosing
+  // Tells wether we already received the "domwindowclosed" event and the parent
+  // windows is about to close.
+  isDOMWindowClosing: false,
+
   // ----------
   // Function: init
   // Must be called after the object is created.
   init: function UI_init() {
     try {
       let self = this;
 
       // initialize the direction of the page
@@ -236,16 +241,17 @@ let UI = {
 
       iQ(window).resize(function() {
         self._resize();
       });
 
       // ___ setup observer to save canvas images
       function domWinClosedObserver(subject, topic, data) {
         if (topic == "domwindowclosed" && subject == gWindow) {
+          self.isDOMWindowClosing = true;
           if (self.isTabViewVisible())
             GroupItems.removeHiddenGroups();
           TabItems.saveAll(true);
           self._save();
         }
       }
       Services.obs.addObserver(
         domWinClosedObserver, "domwindowclosed", false);
--- a/browser/base/content/test/tabview/Makefile.in
+++ b/browser/base/content/test/tabview/Makefile.in
@@ -73,16 +73,17 @@ include $(topsrcdir)/config/rules.mk
                  browser_tabview_bug597399.js \
                  browser_tabview_bug597980.js \
                  browser_tabview_bug598600.js \
                  browser_tabview_bug599626.js \
                  browser_tabview_bug600645.js \
                  browser_tabview_bug600812.js \
                  browser_tabview_bug602432.js \
                  browser_tabview_bug604098.js \
+                 browser_tabview_bug604699.js \
                  browser_tabview_bug606657.js \
                  browser_tabview_bug606905.js \
                  browser_tabview_bug608037.js \
                  browser_tabview_bug608184.js \
                  browser_tabview_bug608158.js \
                  browser_tabview_bug608405.js \
                  browser_tabview_bug610242.js \
                  browser_tabview_bug612470.js \
--- a/browser/base/content/test/tabview/browser_tabview_bug597248.js
+++ b/browser/base/content/test/tabview/browser_tabview_bug597248.js
@@ -1,110 +1,59 @@
-/* ***** BEGIN LICENSE BLOCK *****
- * Version: MPL 1.1/GPL 2.0/LGPL 2.1
- *
- * The contents of this file are subject to the Mozilla Public License Version
- * 1.1 (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- * http://www.mozilla.org/MPL/
- *
- * Software distributed under the License is distributed on an "AS IS" basis,
- * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- * for the specific language governing rights and limitations under the
- * License.
- *
- * The Original Code is tabview bug 597248 test.
- *
- * The Initial Developer of the Original Code is
- * Mozilla Foundation.
- * Portions created by the Initial Developer are Copyright (C) 2010
- * the Initial Developer. All Rights Reserved.
- *
- * Contributor(s):
- * Raymond Lee <raymond@appcoast.com>
- *
- * Alternatively, the contents of this file may be used under the terms of
- * either the GNU General Public License Version 2 or later (the "GPL"), or
- * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
- * in which case the provisions of the GPL or the LGPL are applicable instead
- * of those above. If you wish to allow use of your version of this file only
- * under the terms of either the GPL or the LGPL, and not to allow others to
- * use your version of this file under the terms of the MPL, indicate your
- * decision by deleting the provisions above and replace them with the notice
- * and other provisions required by the GPL or the LGPL. If you do not delete
- * the provisions above, a recipient may use your version of this file under
- * the terms of any one of the MPL, the GPL or the LGPL.
- *
- * ***** END LICENSE BLOCK ***** */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
 
-let newWin;
-let restoredWin;
 let newTabOne;
 let newTabTwo;
 let restoredNewTabOneLoaded = false;
 let restoredNewTabTwoLoaded = false;
 let frameInitialized = false;
 
 function test() {
   waitForExplicitFinish();
-
-  // open a new window 
-  newWin = openDialog(getBrowserURL(), "_blank", "chrome,all,dialog=no");
-  newWin.addEventListener("load", function(event) {
-    newWin.removeEventListener("load", arguments.callee, false);
-    setupOne();
-  }, false);
+  newWindowWithTabView(setupOne);
 }
 
-function setupOne() {
-  let loadedCount = 0;
-  let allLoaded = function() {
-    if (++loadedCount == 2) {
-      newWin.addEventListener("tabviewshown", setupTwo, false);
-      newWin.TabView.toggle();
-    }
-  }
-  
-  newTabOne = newWin.gBrowser.tabs[0];
-  newTabTwo = newWin.gBrowser.addTab();
-  load(newTabOne, "http://mochi.test:8888/browser/browser/base/content/test/tabview/search1.html", allLoaded);
-  load(newTabTwo, "http://mochi.test:8888/browser/browser/base/content/test/tabview/dummy_page.html", allLoaded);
+function setupOne(win) {
+  win.TabView.firstUseExperienced = true;
+
+  win.gBrowser.addTab("http://mochi.test:8888/browser/browser/base/content/test/tabview/search1.html");
+  win.gBrowser.addTab("http://mochi.test:8888/browser/browser/base/content/test/tabview/dummy_page.html");
+
+  afterAllTabsLoaded(function () setupTwo(win), win);
 }
 
-function setupTwo() {
-  newWin.removeEventListener("tabviewshown", setupTwo, false);
-
-  let contentWindow = newWin.document.getElementById("tab-view").contentWindow;
+function setupTwo(win) {
+  let contentWindow = win.TabView.getContentWindow();
 
   let tabItems = contentWindow.TabItems.getItems();
-  is(tabItems.length, 2, "There should be 2 tab items before closing");
+  is(tabItems.length, 3, "There should be 3 tab items before closing");
 
   let numTabsToSave = tabItems.length;
 
   // force all canvases to update, and hook in imageData save detection
   tabItems.forEach(function(tabItem) {
     contentWindow.TabItems._update(tabItem.tab);
-    tabItem.addSubscriber(tabItem, "savedImageData", function(item) {
-      item.removeSubscriber(item, "savedImageData");
+    tabItem.addSubscriber(tabItem, "savedCachedImageData", function(item) {
+      item.removeSubscriber(item, "savedCachedImageData");
       --numTabsToSave;
     });
   });
 
   // after the window is closed, restore it.
   let xulWindowDestory = function() {
     Services.obs.removeObserver(
        xulWindowDestory, "xul-window-destroyed", false);
 
-    newWin = null;
     // "xul-window-destroyed" is just fired just before a XUL window is
     // destroyed so restore window and test it after a delay
     executeSoon(function() {
-      restoredWin = undoCloseWindow();
-      restoredWin.addEventListener("load", function(event) {
-        restoredWin.removeEventListener("load", arguments.callee, false);
+      let restoredWin = undoCloseWindow();
+      restoredWin.addEventListener("load", function onLoad(event) {
+        restoredWin.removeEventListener("load", onLoad, false);
 
         // ensure that closed tabs have been saved
         is(numTabsToSave, 0, "All tabs were saved when window was closed.");
 
         // execute code when the frame is initialized.
         let onTabViewFrameInitialized = function() {
           restoredWin.removeEventListener(
             "tabviewframeinitialized", onTabViewFrameInitialized, false);
@@ -117,17 +66,17 @@ function setupTwo() {
           restoredContentWindow.TabItems._pauseUpdateForTest = true;
           */
           restoredWin.close();
           finish();
         }
         restoredWin.addEventListener(
           "tabviewframeinitialized", onTabViewFrameInitialized, false);
 
-        is(restoredWin.gBrowser.tabs.length, 2, "The total number of tabs is 2");
+        is(restoredWin.gBrowser.tabs.length, 3, "The total number of tabs is 3");
 
         /*
         // bug 615954 happens too often so we disable this until we have a fix
         restoredWin.addEventListener("tabviewshown", onTabViewShown, false);
 
         // setup tab variables and listen to the load progress.
         newTabOne = restoredWin.gBrowser.tabs[0];
         newTabTwo = restoredWin.gBrowser.tabs[1];
@@ -135,20 +84,20 @@ function setupTwo() {
         */
       }, false);
     });
   };
 
   Services.obs.addObserver(
     xulWindowDestory, "xul-window-destroyed", false);
 
-  newWin.close();
+  win.close();
 }
 
-let gTabsProgressListener = {
+/*let gTabsProgressListener = {
   onStateChange: function(browser, webProgress, request, stateFlags, status) {
     // ensure about:blank doesn't trigger the code
     if ((stateFlags & Ci.nsIWebProgressListener.STATE_STOP) &&
         (stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) &&
          browser.currentURI.spec != "about:blank") {
       if (newTabOne.linkedBrowser == browser)
         restoredNewTabOneLoaded = true;
       else if (newTabTwo.linkedBrowser == browser)
@@ -226,18 +175,9 @@ function updateAndCheck() {
     ok(!tabItem.isShowingCachedData(), 
       "Tab item is not showing cached data anymore. " +
       tabItem.tab.linkedBrowser.currentURI.spec);
   });
 
   // clean up and finish
   restoredWin.close();
   finish();
-}
-
-function load(tab, url, callback) {
-  tab.linkedBrowser.addEventListener("load", function (event) {
-    tab.linkedBrowser.removeEventListener("load", arguments.callee, true);
-    callback();
-  }, true);
-  tab.linkedBrowser.loadURI(url);
-}
-
+}*/
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/tabview/browser_tabview_bug604699.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+  let url = "http://non.existant/url";
+  let cw;
+
+  let finishTest = function () {
+    is(1, gBrowser.tabs.length, "there is one tab, only");
+    ok(!TabView.isVisible(), "tabview is not visible");
+    finish();
+  }
+
+  waitForExplicitFinish();
+
+  let testErroneousLoading = function () {
+    cw.Storage.loadThumbnail(url, function (status, data) {
+      ok(!status, "thumbnail entry failed to load");
+      is(null, data, "no thumbnail data received");
+      next();
+    });
+  }
+
+  let testAsynchronousSaving = function () {
+    let saved = false;
+    let data = "thumbnail-data-asynchronous";
+
+    cw.Storage.saveThumbnail(url, data, function (status) {
+      ok(status, "thumbnail entry was saved");
+      ok(saved, "thumbnail was saved asynchronously");
+
+      cw.Storage.loadThumbnail(url, function (status, imageData) {
+        ok(status, "thumbnail entry was loaded");
+        is(imageData, data, "valid thumbnail data received");
+        next();
+      });
+    });
+
+    saved = true;
+  }
+
+  let testSynchronousSaving = function () {
+    let saved = false;
+    let data = "thumbnail-data-synchronous";
+
+    cw.UI.isDOMWindowClosing = true;
+    registerCleanupFunction(function () cw.UI.isDOMWindowClosing = false);
+
+    cw.Storage.saveThumbnail(url, data, function (status) {
+      ok(status, "thumbnail entry was saved");
+      ok(!saved, "thumbnail was saved synchronously");
+
+      cw.Storage.loadThumbnail(url, function (status, imageData) {
+        ok(status, "thumbnail entry was loaded");
+        is(imageData, data, "valid thumbnail data received");
+
+        cw.UI.isDOMWindowClosing = false;
+        next();
+      });
+    });
+
+    saved = true;
+  }
+
+  let tests = [testErroneousLoading, testAsynchronousSaving, testSynchronousSaving];
+
+  let next = function () {
+    let test = tests.shift();
+    if (test)
+      test();
+    else
+      hideTabView(finishTest);
+  }
+
+  showTabView(function () {
+    registerCleanupFunction(function () TabView.hide());
+    cw = TabView.getContentWindow();
+
+    next();
+  });
+}
--- a/browser/base/content/test/tabview/browser_tabview_bug624265.js
+++ b/browser/base/content/test/tabview/browser_tabview_bug624265.js
@@ -97,17 +97,18 @@ function test() {
   }
 
   // ----------
   // [623792] duplicating tab via middle click on reload button
   let testDuplicateTab = function () {
     gBrowser.loadOneTab('http://mochi.test:8888/', {inBackground: true});
 
     afterAllTabsLoaded(function () {
-      duplicateTabIn(gBrowser.selectedTab, 'current');
+      // Valid choices for 'where' are window|tabshifted|tab
+      duplicateTabIn(gBrowser.selectedTab, 'tab');
 
       afterAllTabsLoaded(function () {
         assertNumberOfVisibleTabs(3);
         assertOneSingleGroupItem();
         next();
       });
     });
   }
--- a/browser/base/content/test/tabview/browser_tabview_bug627288.js
+++ b/browser/base/content/test/tabview/browser_tabview_bug627288.js
@@ -14,20 +14,28 @@ function test() {
       gBrowser.removeTab(tab);
 
       cw.TabItems.pauseReconnecting();
       tab = gBrowser.loadOneTab('http://mochi.test:8888/', {inBackground: true});
       cw.Storage.saveTab(tab, data);
 
       whenTabAttrModified(tab, function () {
         tabItem = tab._tabViewTabItem;
+
+        // Hook into loadedCachedImageData since loading cached thumbnails 
+        // is asynchronous.
+        tabItem.addSubscriber(tabItem, "loadedCachedImageData", function(item) {
+          item.removeSubscriber(item, "loadedCachedImageData");
+
+          ok(tabItem.isShowingCachedData(), 'tabItem shows cached data');
+
+          testChangeUrlAfterReconnect();
+        });
+
         cw.TabItems.resumeReconnecting();
-        ok(tabItem.isShowingCachedData(), 'tabItem shows cached data');
-
-        testChangeUrlAfterReconnect();
       });
     });
   }
 
   let testChangeUrlAfterReconnect = function () {
     tab.linkedBrowser.loadURI('http://mochi.test:8888/browser/');
 
     whenTabAttrModified(tab, function () {