Bug 831916 - Hide and delete tiles in Bookmarks and History tile groups r=mbrubeck
authorRodrigo Silveira <rsilveira@mozilla.com>
Tue, 16 Apr 2013 23:16:34 -0700
changeset 141131 9d3d0552005c2c4a89186d1c89f9d21a2157fea4
parent 141107 9d8977cbbfc6822dae559f314fc3822fe5bc2bee
child 141132 cf9e342c1e7cd23e184ae0c27f9b5e534f5a62fd
push id2579
push userakeybl@mozilla.com
push dateMon, 24 Jun 2013 18:52:47 +0000
treeherdermozilla-beta@b69b7de8a05a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmbrubeck
bugs831916
milestone23.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
Bug 831916 - Hide and delete tiles in Bookmarks and History tile groups r=mbrubeck
browser/metro/base/content/TopSites.js
browser/metro/base/content/appbar.js
browser/metro/base/content/bookmarks.js
browser/metro/base/content/browser-scripts.js
browser/metro/base/content/browser.xul
browser/metro/base/content/helperui/ItemPinHelper.js
browser/metro/base/content/history.js
browser/metro/base/jar.mn
browser/metro/base/tests/mochitest/Makefile.in
browser/metro/base/tests/mochitest/browser_bookmarks.js
browser/metro/base/tests/mochitest/browser_history.js
browser/metro/base/tests/mochitest/head.js
--- a/browser/metro/base/content/TopSites.js
+++ b/browser/metro/base/content/TopSites.js
@@ -391,17 +391,17 @@ TopSitesView.prototype = {
 
   onClearHistory: function() {
     this._set.clearAll();
   },
 
   onPageChanged: function(aURI, aWhat, aValue) {
   },
 
-  onPageExpired: function(aURI, aVisitTime, aWholeEntry) {
+  onDeleteVisits: function (aURI, aVisitTime, aGUID, aReason, aTransitionType) {
   },
 
   QueryInterface: function(iid) {
     if (iid.equals(Components.interfaces.nsINavHistoryObserver) ||
         iid.equals(Components.interfaces.nsISupports)) {
       return this;
     }
     throw Cr.NS_ERROR_NO_INTERFACE;
--- a/browser/metro/base/content/appbar.js
+++ b/browser/metro/base/content/appbar.js
@@ -13,28 +13,32 @@ var Appbar = {
 
   init: function Appbar_init() {
     window.addEventListener('MozAppbarShowing', this, false);
     window.addEventListener('MozPrecisePointer', this, false);
     window.addEventListener('MozImprecisePointer', this, false);
     window.addEventListener('MozContextActionsChange', this, false);
     Elements.browsers.addEventListener('URLChanged', this, true);
     Elements.tabList.addEventListener('TabSelect', this, true);
+    Elements.panelUI.addEventListener('ToolPanelShown', this, false);
+    Elements.panelUI.addEventListener('ToolPanelHidden', this, false);
 
     this._updateDebugButtons();
     this._updateZoomButtons();
 
     // tilegroup selection events for all modules get bubbled up
     window.addEventListener("selectionchange", this, false);
   },
 
   handleEvent: function Appbar_handleEvent(aEvent) {
     switch (aEvent.type) {
       case 'URLChanged':
       case 'TabSelect':
+      case 'ToolPanelShown':
+      case 'ToolPanelHidden':
         this.appbar.dismiss();
         break;
       case 'MozAppbarShowing':
         this._updatePinButton();
         this._updateStarButton();
         break;
       case 'MozPrecisePointer':
       case 'MozImprecisePointer':
--- a/browser/metro/base/content/bookmarks.js
+++ b/browser/metro/base/content/bookmarks.js
@@ -1,12 +1,14 @@
 /* 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/. */
 
+'use strict';
+
 /**
  * Utility singleton for manipulating bookmarks.
  */
 var Bookmarks = {
   get metroRoot() {
     return PlacesUtils.annotations.getItemsWithAnnotation('metro/bookmarksRoot', {})[0];
   },
 
@@ -67,189 +69,321 @@ var Bookmarks = {
 /**
  * Wraps a list/grid control implementing nsIDOMXULSelectControlElement and
  * fills it with the user's bookmarks.
  * 
  * @param           aSet    Control implementing nsIDOMXULSelectControlElement.
  * @param {Number}  aLimit  Maximum number of items to show in the view.
  * @param           aRoot   Bookmark root to show in the view.
  */
-function BookmarksView(aSet, aLimit, aRoot) {
+function BookmarksView(aSet, aLimit, aRoot, aFilterUnpinned) {
   this._set = aSet;
   this._set.controller = this;
 
   this._limit = aLimit;
+  this._filterUnpinned = aFilterUnpinned;
+  this._bookmarkService = PlacesUtils.bookmarks;
+  this._navHistoryService = gHistSvc;
 
   this._changes = new BookmarkChangeListener(this);
-  PlacesUtils.bookmarks.addObserver(this._changes, false);
+  this._pinHelper = new ItemPinHelper("metro.bookmarks.unpinned");
+  this._bookmarkService.addObserver(this._changes, false);
+  window.addEventListener('MozAppbarDismissing', this, false);
+  window.addEventListener('BookmarksNeedsRefresh', this, false);
 
-  // This also implicitly calls `getBookmarks`
   this.root = aRoot;
 }
 
 BookmarksView.prototype = {
   _limit: null,
   _set: null,
   _changes: null,
   _root: null,
   _sort: 0, // Natural bookmark order.
+  _toRemove: null,
 
   get sort() {
     return this._sort;
   },
 
   set sort(aSort) {
     this._sort = aSort;
+    this.clearBookmarks();
     this.getBookmarks();
   },
 
   get root() {
     return this._root;
   },
 
   set root(aRoot) {
     this._root = aRoot;
-    this.getBookmarks();
   },
 
   handleItemClick: function bv_handleItemClick(aItem) {
     let url = aItem.getAttribute("value");
     BrowserUI.goToURI(url);
   },
 
   _getItemForBookmarkId: function bv__getItemForBookmark(aBookmarkId) {
     return this._set.querySelector("richgriditem[bookmarkId='" + aBookmarkId + "']");
   },
 
   _getBookmarkIdForItem: function bv__getBookmarkForItem(aItem) {
-    return aItem.getAttribute("bookmarkId");
+    return +aItem.getAttribute("bookmarkId");
   },
 
   _updateItemWithAttrs: function dv__updateItemWithAttrs(anItem, aAttrs) {
     for (let name in aAttrs)
       anItem.setAttribute(name, aAttrs[name]);
   },
 
-  getBookmarks: function bv_getBookmarks() {
-    let options = gHistSvc.getNewQueryOptions();
+  getBookmarks: function bv_getBookmarks(aRefresh) {
+    let options = this._navHistoryService.getNewQueryOptions();
     options.queryType = options.QUERY_TYPE_BOOKMARKS;
     options.excludeQueries = true; // Don't include "smart folders"
-    options.maxResults = this._limit;
     options.sortingMode = this._sort;
 
-    let query = gHistSvc.getNewQuery();
+    let limit = this._limit || Infinity;
+
+    let query = this._navHistoryService.getNewQuery();
     query.setFolders([Bookmarks.metroRoot], 1);
 
-    let result = gHistSvc.executeQuery(query, options);
+    let result = this._navHistoryService.executeQuery(query, options);
     let rootNode = result.root;
     rootNode.containerOpen = true;
     let childCount = rootNode.childCount;
 
-    for (let i = 0; i < childCount; i++) {
+    for (let i = 0, addedCount = 0; i < childCount && addedCount < limit; i++) {
       let node = rootNode.getChild(i);
 
       // Ignore folders, separators, undefined item types, etc.
       if (node.type != node.RESULT_TYPE_URI)
         continue;
 
-      this.addBookmark(node.itemId);
+      // If item is marked for deletion, skip it.
+      if (this._toRemove && this._toRemove.indexOf(node.itemId) !== -1)
+        continue;
+
+      let item = this._getItemForBookmarkId(node.itemId);
+
+      // Item has been unpinned.
+      if (this._filterUnpinned && !this._pinHelper.isPinned(node.itemId)) {
+        if (item)
+          this.removeBookmark(node.itemId);
+
+        continue;
+      }
+
+      if (!aRefresh || !item) {
+        // If we're not refreshing or the item is not in the grid, add it.
+        this.addBookmark(node.itemId, addedCount);
+      } else if (aRefresh && item) {
+        // Update context action in case it changed in another view.
+        this._setContextActions(item);
+      }
+
+      addedCount++;
+    }
+
+    // Remove extra items in case a refresh added more than the limit.
+    // This can happen when undoing a delete.
+    if (aRefresh) {
+      while (this._set.itemCount > limit)
+        this._set.removeItemAt(this._set.itemCount - 1);
     }
 
     rootNode.containerOpen = false;
   },
 
-  inCurrentView: function bv_inCurrentView(aParentId, aIndex, aItemType) {
+  inCurrentView: function bv_inCurrentView(aParentId, aItemId) {
     if (this._root && aParentId != this._root)
       return false;
 
-    if (this._limit && aIndex >= this._limit)
-      return false;
-
-    if (aItemType != PlacesUtils.bookmarks.TYPE_BOOKMARK)
-      return false;
-
-    return true;
+    return !!this._getItemForBookmarkId(aItemId);
   },
 
   clearBookmarks: function bv_clearBookmarks() {
     while (this._set.itemCount > 0)
       this._set.removeItemAt(0);
   },
 
-  addBookmark: function bv_addBookmark(aBookmarkId) {
-    let bookmarks = PlacesUtils.bookmarks;
-
-    let index = bookmarks.getItemIndex(aBookmarkId);
-    let uri = bookmarks.getBookmarkURI(aBookmarkId);
-    let title = bookmarks.getItemTitle(aBookmarkId) || uri.spec;
-    let item = this._set.insertItemAt(index, title, uri.spec);
+  addBookmark: function bv_addBookmark(aBookmarkId, aPos) {
+    let index = this._bookmarkService.getItemIndex(aBookmarkId);
+    let uri = this._bookmarkService.getBookmarkURI(aBookmarkId);
+    let title = this._bookmarkService.getItemTitle(aBookmarkId) || uri.spec;
+    let item = this._set.insertItemAt(aPos || index, title, uri.spec);
     item.setAttribute("bookmarkId", aBookmarkId);
+    this._setContextActions(item);
     this._updateFavicon(aBookmarkId, item, uri);
   },
 
-  _updateFavicon: function _updateFavicon(aBookmarkId, aItem, aUri) {
+  _setContextActions: function bv__setContextActions(aItem) {
+    let itemId = this._getBookmarkIdForItem(aItem);
+    aItem.setAttribute("data-contextactions", "delete," + (this._pinHelper.isPinned(itemId) ? "unpin" : "pin"));
+    if (aItem.refresh) aItem.refresh();
+  },
+
+  _updateFavicon: function bv__updateFavicon(aBookmarkId, aItem, aUri) {
     PlacesUtils.favicons.getFaviconURLForPage(aUri, this._gotIcon.bind(this, aBookmarkId, aItem));
   },
 
-  _gotIcon: function _gotIcon(aBookmarkId, aItem, aIconUri) {
+  _gotIcon: function bv__gotIcon(aBookmarkId, aItem, aIconUri) {
     aItem.setAttribute("iconURI", aIconUri ? aIconUri.spec : "");
     if (!aIconUri) {
       return;
     }
     ColorUtils.getForegroundAndBackgroundIconColors(aIconUri, function(foregroundColor, backgroundColor) {
       aItem.style.color = foregroundColor; //color text
       aItem.setAttribute("customColor", backgroundColor); //set background
       if (aItem.refresh) {
         aItem.refresh();
       }
     });
   },
 
+  _sendNeedsRefresh: function bv__sendNeedsRefresh(){
+    // Event sent when all view instances need to refresh.
+    let event = document.createEvent("Events");
+    event.initEvent("BookmarksNeedsRefresh", true, false);
+    window.dispatchEvent(event);
+  },
+
   updateBookmark: function bv_updateBookmark(aBookmarkId) {
     let item = this._getItemForBookmarkId(aBookmarkId);
 
     if (!item)
       return;
     
-    let bookmarks = PlacesUtils.bookmarks;
     let oldIndex = this._set.getIndexOfItem(item);
-    let index = bookmarks.getItemIndex(aBookmarkId);
+    let index = this._bookmarkService.getItemIndex(aBookmarkId);
 
     if (oldIndex != index) {
       this.removeBookmark(aBookmarkId);
       this.addBookmark(aBookmarkId);
       return;
     }
 
-    let uri = bookmarks.getBookmarkURI(aBookmarkId);
-    let title = bookmarks.getItemTitle(aBookmarkId) || uri.spec;
+    let uri = this._bookmarkService.getBookmarkURI(aBookmarkId);
+    let title = this._bookmarkService.getItemTitle(aBookmarkId) || uri.spec;
 
     item.setAttribute("value", uri.spec);
     item.setAttribute("label", title);
 
     this._updateFavicon(aBookmarkId, item, uri);
   },
 
   removeBookmark: function bv_removeBookmark(aBookmarkId) {
     let item = this._getItemForBookmarkId(aBookmarkId);
     let index = this._set.getIndexOfItem(item);
     this._set.removeItemAt(index);
   },
 
   destruct: function bv_destruct() {
-    PlacesUtils.bookmarks.removeObserver(this._changes);
+    this._bookmarkService.removeObserver(this._changes);
+    window.removeEventListener('MozAppbarDismissing', this, false);
+    window.removeEventListener('BookmarksNeedsRefresh', this, false);
+  },
+
+  doActionOnSelectedTiles: function bv_doActionOnSelectedTiles(aActionName, aEvent) {
+    let tileGroup = this._set;
+    let selectedTiles = tileGroup.selectedItems;
+
+    switch (aActionName){
+      case "delete":
+        Array.forEach(selectedTiles, function(aNode) {
+          if (!this._toRemove) {
+            this._toRemove = [];
+          }
+
+          let itemId = this._getBookmarkIdForItem(aNode);
+
+          this._toRemove.push(itemId);
+          this.removeBookmark(itemId);
+        }, this);
+
+        // stop the appbar from dismissing
+        aEvent.preventDefault();
+
+        // at next tick, re-populate the context appbar.
+        setTimeout(function(){
+          // fire a MozContextActionsChange event to update the context appbar
+          let event = document.createEvent("Events");
+          // we need the restore button to show (the tile node will go away though)
+          event.actions = ["restore"];
+          event.initEvent("MozContextActionsChange", true, false);
+          tileGroup.dispatchEvent(event);
+        }, 0);
+        break;
+
+      case "restore":
+        // clear toRemove and let _sendNeedsRefresh update the items.
+        this._toRemove = null;
+        break;
+
+      case "unpin":
+        Array.forEach(selectedTiles, function(aNode) {
+          let itemId = this._getBookmarkIdForItem(aNode);
+
+          if (this._filterUnpinned)
+            this.removeBookmark(itemId);
+
+          this._pinHelper.setUnpinned(itemId);
+        }, this);
+        break;
+
+      case "pin":
+        Array.forEach(selectedTiles, function(aNode) {
+          let itemId = this._getBookmarkIdForItem(aNode);
+
+          this._pinHelper.setPinned(itemId);
+        }, this);
+        break;
+
+      default:
+        return;
+    }
+
+    // Send refresh event so all view are in sync.
+    this._sendNeedsRefresh();
+  },
+
+  handleEvent: function bv_handleEvent(aEvent) {
+    switch (aEvent.type){
+      case "MozAppbarDismissing":
+        // If undo wasn't pressed, time to do definitive actions.
+        if (this._toRemove) {
+          for (let bookmarkId of this._toRemove) {
+            this._bookmarkService.removeItem(bookmarkId);
+          }
+
+          this._toRemove = null;
+          this._set.clearSelection();
+
+          // Clear context app bar
+          let event = document.createEvent("Events");
+          event.initEvent("MozContextActionsChange", true, false);
+          this._set.dispatchEvent(event);
+        }
+        break;
+
+      case "BookmarksNeedsRefresh":
+        this.getBookmarks(true);
+        break;
+    }
   }
 };
 
 var BookmarksStartView = {
   _view: null,
   get _grid() { return document.getElementById("start-bookmarks-grid"); },
 
   init: function init() {
-    this._view = new BookmarksView(this._grid, StartUI.maxResultsPerSection, Bookmarks.metroRoot);
+    this._view = new BookmarksView(this._grid, StartUI.maxResultsPerSection, Bookmarks.metroRoot, true);
+    this._view.getBookmarks();
   },
 
   uninit: function uninit() {
     this._view.destruct();
   },
 
   show: function show() {
     this._grid.arrangeItems();
@@ -262,71 +396,72 @@ var BookmarksPanelView = {
   get _grid() { return document.getElementById("bookmarks-list"); },
   get visible() { return PanelUI.isPaneVisible("bookmarks-container"); },
 
   init: function init() {
     this._view = new BookmarksView(this._grid, null, Bookmarks.metroRoot);
   },
 
   show: function show() {
+    this._view.getBookmarks(true);
     this._grid.arrangeItems();
   },
 
   uninit: function uninit() {
     this._view.destruct();
   }
 };
 
 /**
  * Observes bookmark changes and keeps a linked BookmarksView updated.
  *
  * @param aView An instance of BookmarksView.
  */
 function BookmarkChangeListener(aView) {
   this._view = aView;
-};
+}
 
 BookmarkChangeListener.prototype = {
   //////////////////////////////////////////////////////////////////////////////
   //// nsINavBookmarkObserver
   onBeginUpdateBatch: function () { },
   onEndUpdateBatch: function () { },
 
   onItemAdded: function bCL_onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle, aDateAdded, aGUID, aParentGUID) {
-    if (!this._view.inCurrentView(aParentId, aIndex, aItemType))
-      return;
-
-    this._view.addBookmark(aItemId);
+    this._view.getBookmarks(true);
   },
 
   onItemChanged: function bCL_onItemChanged(aItemId, aProperty, aIsAnnotationProperty, aNewValue, aLastModified, aItemType, aParentId, aGUID, aParentGUID) {
     let itemIndex = PlacesUtils.bookmarks.getItemIndex(aItemId);
-    if (!this._view.inCurrentView(aParentId, itemIndex, aItemType))
+    if (!this._view.inCurrentView(aParentId, aItemId))
       return;
     
     this._view.updateBookmark(aItemId);
   },
 
   onItemMoved: function bCL_onItemMoved(aItemId, aOldParentId, aOldIndex, aNewParentId, aNewIndex, aItemType, aGUID, aOldParentGUID, aNewParentGUID) {
-    let wasInView = this._view.inCurrentView(aOldParentId, aOldIndex, aItemType);
-    let nowInView = this._view.inCurrentView(aNewParentId, aNewIndex, aItemType);
+    let wasInView = this._view.inCurrentView(aOldParentId, aItemId);
+    let nowInView = this._view.inCurrentView(aNewParentId, aItemId);
 
     if (!wasInView && nowInView)
-      this._view.addBookmark(aItemId, aParentId, aIndex, aItemType, aURI, aTitle, aDateAdded);
+      this._view.addBookmark(aItemId);
 
     if (wasInView && !nowInView)
       this._view.removeBookmark(aItemId);
+
+    this._view.getBookmarks(true);
   },
 
   onBeforeItemRemoved: function (aItemId, aItemType, aParentId, aGUID, aParentGUID) { },
   onItemRemoved: function bCL_onItemRemoved(aItemId, aParentId, aIndex, aItemType, aURI, aGUID, aParentGUID) {
-    if (!this._view.inCurrentView(aParentId, aIndex, aItemType))
+    if (!this._view.inCurrentView(aParentId, aItemId))
       return;
 
     this._view.removeBookmark(aItemId);
+    this._view.getBookmarks(true);
   },
 
   onItemVisited: function(aItemId, aVisitId, aTime, aTransitionType, aURI, aParentId, aGUID, aParentGUID) { },
 
   //////////////////////////////////////////////////////////////////////////////
   //// nsISupports
   QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver])
 };
--- a/browser/metro/base/content/browser-scripts.js
+++ b/browser/metro/base/content/browser-scripts.js
@@ -127,16 +127,17 @@ let ScriptContexts = {};
   ["Site", "chrome://browser/content/Site.js"],
   ["TopSites", "chrome://browser/content/TopSites.js"],
   ["TopSitesView", "chrome://browser/content/TopSites.js"],
   ["TopSitesSnappedView", "chrome://browser/content/TopSites.js"],
   ["TopSitesStartView", "chrome://browser/content/TopSites.js"],
   ["Sanitizer", "chrome://browser/content/sanitize.js"],
   ["SanitizeUI", "chrome://browser/content/sanitizeUI.js"],
   ["SSLExceptions", "chrome://browser/content/exceptions.js"],
+  ["ItemPinHelper", "chrome://browser/content/helperui/ItemPinHelper.js"],
 #ifdef MOZ_SERVICES_SYNC
   ["WeaveGlue", "chrome://browser/content/sync.js"],
   ["SyncPairDevice", "chrome://browser/content/sync.js"],
   ["RemoteTabsView", "chrome://browser/content/RemoteTabs.js"],
   ["RemoteTabsPanelView", "chrome://browser/content/RemoteTabs.js"],
   ["RemoteTabsStartView", "chrome://browser/content/RemoteTabs.js"],
 #endif
 ].forEach(function (aScript) {
--- a/browser/metro/base/content/browser.xul
+++ b/browser/metro/base/content/browser.xul
@@ -263,17 +263,18 @@
               <richgrid id="start-topsites-grid" rows="3" columns="3" seltype="multiple" flex="1"/>
             </vbox>
             <vbox id="start-bookmarks" class="meta-section">
               <label class="meta-section-title" value="&startBookmarksHeader.label;"
                 onclick="PanelUI.show('bookmarks-container');"/>
               <richgrid id="start-bookmarks-grid" seltype="multiple" flex="1"/>
             </vbox>
             <vbox id="start-history" class="meta-section">
-              <label class="meta-section-title" value="&startHistoryHeader.label;"/>
+              <label class="meta-section-title" value="&startHistoryHeader.label;"
+                onclick="PanelUI.show('history-container');"/>
               <richgrid id="start-history-grid" seltype="multiple" flex="1"/>
             </vbox>
             <vbox id="start-remotetabs" class="meta-section">
               <label class="meta-section-title" value="&startRemoteTabsHeader.label;"
                 onclick="PanelUI.show('remotetabs-container');"/>
               <richgrid id="start-remotetabs-grid" seltype="multiple" flex="1"/>
             </vbox>
             </scrollbox>
@@ -306,40 +307,16 @@
       </stack>
     </vbox>
 
     <!-- popup for content navigator helper -->
     <vbox id="content-navigator" top="0">
       <textbox id="find-helper-textbox" class="search-bar content-navigator-item" oncommand="FindHelperUI.search(this.value)" oninput="FindHelperUI.updateCommands(this.value);" type="search"/>
     </vbox>
 
-    <!-- Windows 8 Appbar -->
-    <appbar id="appbar" mousethrough="never" observes="bcast_windowState">
-      <!-- contextual actions temporarily hidden, pending #800996 -->
-      <hbox id="contextualactions-tray" flex="1">
-        <toolbarbutton id="delete-selected-button" hidden="true" fade="true" oncommand="Appbar.dispatchContextualAction('delete')"/>
-        <toolbarbutton id="restore-selected-button" hidden="true" fade="true" oncommand="Appbar.dispatchContextualAction('restore')"/>
-        <toolbarbutton id="pin-selected-button" hidden="true" fade="true" oncommand="Appbar.dispatchContextualAction('pin')"/>
-        <toolbarbutton id="unpin-selected-button" hidden="true" fade="true" oncommand="Appbar.dispatchContextualAction('unpin')"/>
-        <toolbarbutton id="clear-selected-button" hidden="true" fade="true" oncommand="Appbar.dispatchContextualAction('clear')"/>
-      </hbox>
-      <hbox flex="1">
-        <toolbarbutton id="download-button" oncommand="Appbar.onDownloadButton()"/>
-        <toolbarbutton id="console-button" oncommand="Appbar.onConsoleButton()"/>
-        <toolbarbutton id="jsshell-button" oncommand="Appbar.onJSShellButton()"/>
-      </hbox>
-      <hbox>
-        <toolbarbutton id="more-button" onclick="Appbar.onMoreButton(event)" />
-        <toolbarbutton id="zoomout-button" oncommand="Appbar.onZoomOutButton()"/>
-        <toolbarbutton id="zoomin-button" oncommand="Appbar.onZoomInButton()"/>
-        <toolbarbutton id="star-button" type="checkbox" oncommand="Appbar.onStarButton()"/>
-        <toolbarbutton id="pin-button" type="checkbox" oncommand="Appbar.onPinButton()"/>
-      </hbox>
-    </appbar>
-
     <vbox id="panel-container" hidden="true" class="window-width window-height meta" observes="bcast_windowState">
       <hbox id="panel-header">
         <toolbarbutton id="panel-close-button" command="cmd_panel"/>
 
         <menulist id="panel-view-switcher" oncommand="PanelUI.switchPane(this.value);">
           <menupopup>
             <menuitem label="&bookmarksHeader.label;" value="bookmarks-container" id="menuitem-bookmarks"/>
             <menuitem label="&startHistoryHeader.label;" value="history-container" id="menuitem-history"/>
@@ -347,20 +324,20 @@
             <menuitem label="&downloadsHeader.label;" value="downloads-container" id="menuitem-downloads"/>
             <menuitem label="&consoleHeader.label;" value="console-container" id="menuitem-console"/>
           </menupopup>
         </menulist>
       </hbox>
 
       <deck id="panel-items" selectedIndex="0" flex="1" >
         <scrollbox id="bookmarks-container" flex="1">
-          <richgrid id="bookmarks-list" class="canSnapTiles" seltype="single" flex="1"/>
+          <richgrid id="bookmarks-list" class="canSnapTiles" seltype="multiple" flex="1"/>
         </scrollbox>
         <scrollbox id="history-container" flex="1">
-          <richgrid id="history-list" class="canSnapTiles" seltype="single" flex="1"/>
+          <richgrid id="history-list" class="canSnapTiles" seltype="multiple" flex="1"/>
         </scrollbox>
         <scrollbox id="downloads-container" flex="1">
           <richgrid id="downloads-list" class="canSnapTiles" seltype="single" flex="1"/>
         </scrollbox>
         <scrollbox id="remotetabs-container" flex="1">
           <richgrid id="remotetabs-list" class="canSnapTiles" seltype="single" flex="1"/>
         </scrollbox>
         <vbox id="console-container" flex="1">
@@ -382,16 +359,39 @@
             </hbox>
           </vbox>
 
           <richlistbox id="console-box" class="panel-list console-box" flex="1" onkeypress="ConsolePanelView.onConsoleBoxKeyPress(event)" oncontextmenu="ConsolePanelView.onContextMenu(event);"/>
         </vbox>
       </deck>
     </vbox>
 
+    <!-- Windows 8 Appbar -->
+    <appbar id="appbar" mousethrough="never" observes="bcast_windowState">
+      <hbox id="contextualactions-tray" flex="1">
+        <toolbarbutton id="delete-selected-button" hidden="true" fade="true" oncommand="Appbar.dispatchContextualAction('delete')"/>
+        <toolbarbutton id="restore-selected-button" hidden="true" fade="true" oncommand="Appbar.dispatchContextualAction('restore')"/>
+        <toolbarbutton id="pin-selected-button" hidden="true" fade="true" oncommand="Appbar.dispatchContextualAction('pin')"/>
+        <toolbarbutton id="unpin-selected-button" hidden="true" fade="true" oncommand="Appbar.dispatchContextualAction('unpin')"/>
+        <toolbarbutton id="clear-selected-button" hidden="true" fade="true" oncommand="Appbar.dispatchContextualAction('clear')"/>
+      </hbox>
+      <hbox flex="1">
+        <toolbarbutton id="download-button" oncommand="Appbar.onDownloadButton()"/>
+        <toolbarbutton id="console-button" oncommand="Appbar.onConsoleButton()"/>
+        <toolbarbutton id="jsshell-button" oncommand="Appbar.onJSShellButton()"/>
+      </hbox>
+      <hbox>
+        <toolbarbutton id="more-button" onclick="Appbar.onMoreButton(event)" />
+        <toolbarbutton id="zoomout-button" oncommand="Appbar.onZoomOutButton()"/>
+        <toolbarbutton id="zoomin-button" oncommand="Appbar.onZoomInButton()"/>
+        <toolbarbutton id="star-button" type="checkbox" oncommand="Appbar.onStarButton()"/>
+        <toolbarbutton id="pin-button" type="checkbox" oncommand="Appbar.onPinButton()"/>
+      </hbox>
+    </appbar>
+
   <!-- Selection overlay - this should be below any content that can have selectable text -->
   <!-- onclick addresses dom bug 835175, str in bug 832957 -->
   <box onclick="false" class="selection-overlay-hidden" id="selection-overlay"/>
 
   <flyoutpanel id="about-flyoutpanel" headertext="&aboutHeader.title;">
         <label id="about-product-label" value="&aboutHeader.product.label;"/>
         <label value="&aboutHeader.company.label;"/>
 #expand <label id="about-version-label">__MOZ_APP_VERSION__</label>
new file mode 100644
--- /dev/null
+++ b/browser/metro/base/content/helperui/ItemPinHelper.js
@@ -0,0 +1,59 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* 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/. */
+
+"use strict";
+
+function ItemPinHelper(aUnpinnedPrefName) {
+  this._prefKey = aUnpinnedPrefName;
+}
+
+// Cache preferences on a static variable shared
+// by all instances registered to the same pref key.
+ItemPinHelper._prefValue = {};
+
+ItemPinHelper.prototype = {
+  _getPrefValue: function _getPrefValue() {
+    if (ItemPinHelper._prefValue[this._prefKey])
+      return ItemPinHelper._prefValue[this._prefKey];
+
+    try {
+      // getComplexValue throws if pref never set. Really.
+      let prefValue = Services.prefs.getComplexValue(this._prefKey, Ci.nsISupportsString);
+      ItemPinHelper._prefValue[this._prefKey] = JSON.parse(prefValue.data);
+    } catch(e) {
+      ItemPinHelper._prefValue[this._prefKey] = [];
+    }
+
+    return ItemPinHelper._prefValue[this._prefKey];
+  },
+
+  _setPrefValue: function _setPrefValue(aNewValue) {
+    let stringified = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
+    stringified.data = JSON.stringify(aNewValue);
+
+    Services.prefs.setComplexValue(this._prefKey, Ci.nsISupportsString, stringified);
+    ItemPinHelper._prefValue[this._prefKey] = aNewValue;
+  },
+
+  isPinned: function isPinned(aItemId) {
+    // Bookmarks are visible on StartUI (pinned) by default
+    return this._getPrefValue().indexOf(aItemId) === -1;
+  },
+
+  setUnpinned: function setPinned(aItemId) {
+    let unpinned = this._getPrefValue();
+    unpinned.push(aItemId);
+    this._setPrefValue(unpinned);
+  },
+
+  setPinned: function unsetPinned(aItemId) {
+    let unpinned = this._getPrefValue();
+
+    let index = unpinned.indexOf(aItemId);
+    unpinned.splice(index, 1);
+
+    this._setPrefValue(unpinned);
+  },
+}
--- a/browser/metro/base/content/history.js
+++ b/browser/metro/base/content/history.js
@@ -1,103 +1,242 @@
 // -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
 /* 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/. */
 'use strict';
 
-
-function HistoryView(aSet) {
+function HistoryView(aSet, aLimit, aFilterUnpinned) {
   this._set = aSet;
   this._set.controller = this;
   this._inBatch = false;
 
-  let history = Cc["@mozilla.org/browser/nav-history-service;1"].
-                getService(Ci.nsINavHistoryService);
-  history.addObserver(this, false);
+  this._limit = aLimit;
+  this._filterUnpinned = aFilterUnpinned;
+  this._historyService = PlacesUtils.history;
+  this._navHistoryService = gHistSvc;
+
+  this._pinHelper = new ItemPinHelper("metro.history.unpinned");
+  this._historyService.addObserver(this, false);
+  window.addEventListener('MozAppbarDismissing', this, false);
+  window.addEventListener('HistoryNeedsRefresh', this, false);
 }
 
 HistoryView.prototype = {
-  _set:null,
+  _set: null,
+  _toRemove: null,
 
   handleItemClick: function tabview_handleItemClick(aItem) {
     let url = aItem.getAttribute("value");
     BrowserUI.goToURI(url);
   },
 
-  populateGrid: function populateGrid() {
-    let query = gHistSvc.getNewQuery();
-    let options = gHistSvc.getNewQueryOptions();
+  populateGrid: function populateGrid(aRefresh) {
+    let query = this._navHistoryService.getNewQuery();
+    let options = this._navHistoryService.getNewQueryOptions();
     options.excludeQueries = true;
     options.queryType = options.QUERY_TYPE_HISTORY;
-    options.maxResults = StartUI.maxResultsPerSection;
     options.resultType = options.RESULTS_AS_URI;
     options.sortingMode = options.SORT_BY_DATE_DESCENDING;
 
-    let result = gHistSvc.executeQuery(query, options);
+    let limit = this._limit || Infinity;
+    let result = this._navHistoryService.executeQuery(query, options);
     let rootNode = result.root;
     rootNode.containerOpen = true;
     let childCount = rootNode.childCount;
 
-    for (let i = 0; i < childCount; i++) {
+    for (let i = 0, addedCount = 0; i < childCount && addedCount < limit; i++) {
       let node = rootNode.getChild(i);
       let uri = node.uri;
       let title = node.title || uri;
 
-      this.addItemToSet(uri, title, node.icon);
+      // If item is marked for deletion, skip it.
+      if (this._toRemove && this._toRemove.indexOf(uri) !== -1)
+        continue;
+
+      let items = this._set.getItemsByUrl(uri);
+
+      // Item has been unpinned, skip if filterUnpinned set.
+      if (this._filterUnpinned && !this._pinHelper.isPinned(uri)) {
+        if (items.length > 0)
+          this.removeHistory(uri);
+
+        continue;
+      }
+
+      if (!aRefresh || items.length === 0) {
+        // If we're not refreshing or the item is not in the grid, add it.
+        this.addItemToSet(uri, title, node.icon, addedCount);
+      } else if (aRefresh && items.length > 0) {
+        // Update context action in case it changed in another view.
+        for (let item of items) {
+          this._setContextActions(item);
+        }
+      }
+
+      addedCount++;
+    }
+
+    // Remove extra items in case a refresh added more than the limit.
+    // This can happen when undoing a delete.
+    if (aRefresh) {
+      while (this._set.itemCount > limit)
+        this._set.removeItemAt(this._set.itemCount - 1);
     }
 
     rootNode.containerOpen = false;
   },
 
   destruct: function destruct() {
+    this._historyService.removeObserver(this);
+    window.removeEventListener('MozAppbarDismissing', this, false);
+    window.removeEventListener('HistoryNeedsRefresh', this, false);
+  },
+
+  addItemToSet: function addItemToSet(aURI, aTitle, aIcon, aPos) {
+    let item = this._set.insertItemAt(aPos || 0, aTitle, aURI, this._inBatch);
+    item.setAttribute("iconURI", aIcon);
+    this._setContextActions(item);
+  },
+
+  _setContextActions: function bv__setContextActions(aItem) {
+    let uri = aItem.getAttribute("value");
+    aItem.setAttribute("data-contextactions", "delete," + (this._pinHelper.isPinned(uri) ? "unpin" : "pin"));
+    if (aItem.refresh) aItem.refresh();
+  },
+
+  _sendNeedsRefresh: function bv__sendNeedsRefresh(){
+    // Event sent when all views need to refresh.
+    let event = document.createEvent("Events");
+    event.initEvent("HistoryNeedsRefresh", true, false);
+    window.dispatchEvent(event);
+  },
+
+  removeHistory: function (aUri) {
+    let items = this._set.getItemsByUrl(aUri);
+    for (let item of items)
+      this._set.removeItem(item, this._inBatch);
+  },
+
+  doActionOnSelectedTiles: function bv_doActionOnSelectedTiles(aActionName, aEvent) {
+    let tileGroup = this._set;
+    let selectedTiles = tileGroup.selectedItems;
+
+    switch (aActionName){
+      case "delete":
+        Array.forEach(selectedTiles, function(aNode) {
+          if (!this._toRemove) {
+            this._toRemove = [];
+          }
+
+          let uri = aNode.getAttribute("value");
+
+          this._toRemove.push(uri);
+          this.removeHistory(uri);
+        }, this);
+
+        // stop the appbar from dismissing
+        aEvent.preventDefault();
+
+        // at next tick, re-populate the context appbar.
+        setTimeout(function(){
+          // fire a MozContextActionsChange event to update the context appbar
+          let event = document.createEvent("Events");
+          // we need the restore button to show (the tile node will go away though)
+          event.actions = ["restore"];
+          event.initEvent("MozContextActionsChange", true, false);
+          tileGroup.dispatchEvent(event);
+        }, 0);
+        break;
+
+      case "restore":
+        // clear toRemove and let _sendNeedsRefresh update the items.
+        this._toRemove = null;
+        break;
+
+      case "unpin":
+        Array.forEach(selectedTiles, function(aNode) {
+          let uri = aNode.getAttribute("value");
+
+          if (this._filterUnpinned)
+            this.removeHistory(uri);
+
+          this._pinHelper.setUnpinned(uri);
+        }, this);
+        break;
+
+      case "pin":
+        Array.forEach(selectedTiles, function(aNode) {
+          let uri = aNode.getAttribute("value");
+
+          this._pinHelper.setPinned(uri);
+        }, this);
+        break;
+
+      default:
+        return;
+    }
+
+    // Send refresh event so all view are in sync.
+    this._sendNeedsRefresh();
+  },
+
+  handleEvent: function bv_handleEvent(aEvent) {
+    switch (aEvent.type){
+      case "MozAppbarDismissing":
+        // If undo wasn't pressed, time to do definitive actions.
+        if (this._toRemove) {
+          for (let uri of this._toRemove) {
+            this._historyService.removePage(NetUtil.newURI(uri));
+          }
+
+          // Clear context app bar
+          let event = document.createEvent("Events");
+          event.initEvent("MozContextActionsChange", true, false);
+          this._set.dispatchEvent(event);
+
+          this._toRemove = null;
+          this._set.clearSelection();
+        }
+        break;
+
+      case "HistoryNeedsRefresh":
+        this.populateGrid(true);
+        break;
+    }
   },
 
   // nsINavHistoryObserver & helpers
 
-  addItemToSet: function addItemToSet(uri, title, icon) {
-    let item = this._set.appendItem(title, uri, this._inBatch);
-    item.setAttribute("iconURI", icon);
-  },
-
-  // TODO rebase/merge Alert: bug 831916 's patch merges in,
-  // this can be replaced with the updated calls to populateGrid()
-  refreshAndRepopulate: function() {
-    this._set.clearAll();
-    this.populateGrid();
-  },
-
   onBeginUpdateBatch: function() {
     // Avoid heavy grid redraws while a batch is in process
     this._inBatch = true;
   },
 
   onEndUpdateBatch: function() {
     this._inBatch = false;
-    this.refreshAndRepopulate();
+    this.populateGrid(true);
   },
 
   onVisit: function(aURI, aVisitID, aTime, aSessionID,
                     aReferringID, aTransitionType) {
     if (!this._inBatch) {
-      this.refreshAndRepopulate();
+      this.populateGrid(true);
     }
   },
 
   onTitleChanged: function(aURI, aPageTitle) {
     let changedItems = this._set.getItemsByUrl(aURI.spec);
     for (let item of changedItems) {
       item.setAttribute("label", aPageTitle);
     }
   },
 
   onDeleteURI: function(aURI) {
-    for (let item of this._set.getItemsByUrl(aURI.spec)) {
-      this._set.removeItem(item, this._inBatch);
-    }
+    this.removeHistory(aURI.spec);
   },
 
   onClearHistory: function() {
     this._set.clearAll();
   },
 
   onPageChanged: function(aURI, aWhat, aValue) {
     if (aWhat ==  Ci.nsINavHistoryObserver.ATTRIBUTE_FAVICON) {
@@ -108,17 +247,17 @@ HistoryView.prototype = {
           item.setAttribute("iconURI", aValue);
         }
       }
     }
   },
 
   onDeleteVisits: function (aURI, aVisitTime, aGUID, aReason, aTransitionType) {
     if ((aReason ==  Ci.nsINavHistoryObserver.REASON_DELETED) && !this._inBatch) {
-      this.refreshAndRepopulate();
+      this.populateGrid(true);
     }
   },
 
   QueryInterface: function(iid) {
     if (iid.equals(Components.interfaces.nsINavHistoryObserver) ||
         iid.equals(Components.interfaces.nsISupports)) {
       return this;
     }
@@ -130,35 +269,35 @@ let HistoryStartView = {
   _view: null,
   get _grid() { return document.getElementById("start-history-grid"); },
 
   show: function show() {
     this._grid.arrangeItems();
   },
 
   init: function init() {
-    this._view = new HistoryView(this._grid);
+    this._view = new HistoryView(this._grid, StartUI.maxResultsPerSection, true);
     this._view.populateGrid();
   },
 
   uninit: function uninit() {
     this._view.destruct();
   }
 };
 
 let HistoryPanelView = {
   _view: null,
   get _grid() { return document.getElementById("history-list"); },
   get visible() { return PanelUI.isPaneVisible("history-container"); },
 
   show: function show() {
+    this._view.populateGrid(true);
     this._grid.arrangeItems();
   },
 
   init: function init() {
-    this._view = new HistoryView(this._grid);
-    this._view.populateGrid();
+    this._view = new HistoryView(this._grid, StartUI.maxResultsPerSection, false);
   },
 
   uninit: function uninit() {
     this._view.destruct();
   }
 };
--- a/browser/metro/base/jar.mn
+++ b/browser/metro/base/jar.mn
@@ -37,16 +37,17 @@ chrome.jar:
   content/helperui/AlertsHelper.js             (content/helperui/AlertsHelper.js)
   content/helperui/IndexedDB.js                (content/helperui/IndexedDB.js)
   content/helperui/MenuUI.js                   (content/helperui/MenuUI.js)
   content/helperui/OfflineApps.js              (content/helperui/OfflineApps.js)
   content/helperui/SelectHelperUI.js           (content/helperui/SelectHelperUI.js)
   content/helperui/SelectionHelperUI.js        (content/helperui/SelectionHelperUI.js)
   content/helperui/FormHelperUI.js             (content/helperui/FormHelperUI.js)
   content/helperui/FindHelperUI.js             (content/helperui/FindHelperUI.js)
+  content/helperui/ItemPinHelper.js            (content/helperui/ItemPinHelper.js)
 
   content/contenthandlers/ContextMenuHandler.js (content/contenthandlers/ContextMenuHandler.js)
   content/contenthandlers/PluginCTPHandler.js  (content/contenthandlers/PluginCTPHandler.js)
   content/contenthandlers/SelectionHandler.js  (content/contenthandlers/SelectionHandler.js)
   content/contenthandlers/FormHelper.js        (content/contenthandlers/FormHelper.js)
   content/contenthandlers/FindHandler.js       (content/contenthandlers/FindHandler.js)
   content/contenthandlers/ViewportHandler.js   (content/contenthandlers/ViewportHandler.js)
   content/contenthandlers/ConsoleAPIObserver.js (content/contenthandlers/ConsoleAPIObserver.js)
--- a/browser/metro/base/tests/mochitest/Makefile.in
+++ b/browser/metro/base/tests/mochitest/Makefile.in
@@ -23,16 +23,18 @@ BROWSER_TESTS = \
   browser_downloads.js \
   browser_context_menu_tests.js \
   browser_context_menu_tests_01.html \
   browser_context_menu_tests_02.html \
   browser_context_menu_tests_03.html \
   browser_prefs_ui.js \
   browser_topsites.js \
   browser_tabs.js \
+  browser_bookmarks.js \
+  browser_history.js \
   $(NULL)
 
 # disabled due to timeouts and lack of plugin support.
 #  browser_plugin_input.html \
 #  browser_plugin_input_mouse.js \
 #  browser_plugin_input_keyboard.js \
 
 ifndef MOZ_DEBUG
new file mode 100644
--- /dev/null
+++ b/browser/metro/base/tests/mochitest/browser_bookmarks.js
@@ -0,0 +1,560 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* 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/. */
+
+"use strict";
+
+let gStartView = BookmarksStartView._view;
+let gPanelView = BookmarksPanelView._view;
+
+function test() {
+  runTests();
+}
+
+function setup() {
+  PanelUI.hide();
+  BookmarksTestHelper.setup();
+
+  if (StartUI.isStartPageVisible)
+    return;
+
+  yield addTab("about:start");
+
+  yield waitForCondition(() => StartUI.isStartPageVisible);
+
+  yield hideContextUI();
+}
+
+function tearDown() {
+  PanelUI.hide();
+  BookmarksTestHelper.restore();
+}
+
+var BookmarksTestHelper = {
+  _originalNavHistoryService: null,
+  MockNavHistoryService: {
+    getNewQueryOptions: function () {
+      return {};
+    },
+    getNewQuery: function () {
+      return {
+        setFolders: function(){}
+      };
+    },
+    executeQuery: function () {
+      return {
+        root: {
+          get childCount() {
+            return Object.keys(BookmarksTestHelper._nodes).length;
+          },
+
+          getChild: function (aIndex) BookmarksTestHelper._nodes[Object.keys(BookmarksTestHelper._nodes)[aIndex]]
+        }
+      }
+    }
+  },
+
+  _originalBookmarkService: null,
+  MockBookmarkService: {
+    getItemIndex: function (aIndex) aIndex,
+    getBookmarkURI: function (aId) BookmarksTestHelper._nodes[aId].uri,
+    getItemTitle: function (aId) BookmarksTestHelper._nodes[aId].title,
+    removeItem: function (aId) {
+      delete BookmarksTestHelper._nodes[aId];
+
+      // Simulate observer notification
+      gStartView._changes.onItemRemoved(aId, gStartView._root);
+      gPanelView._changes.onItemRemoved(aId, gPanelView._root);
+    },
+  },
+
+  Node: function (aTitle, aId) {
+    this.type = this.RESULT_TYPE_URI = 0;
+    this.title = aTitle;
+    this.itemId = aId;
+    this.uri = "http://" + aTitle + ".com.br";
+    this.pinned = true
+  },
+
+  _nodes: null,
+  createNodes: function (aMany) {
+    this._nodes = {};
+    for (let i=0; i<aMany; i++) {
+      this._nodes[i] = new this.Node("Mock-Bookmark" + i, i);
+    }
+  },
+
+  _originalPinHelper: null,
+  MockPinHelper: {
+    isPinned: function (aItem) BookmarksTestHelper._nodes[aItem].pinned,
+    setUnpinned: function (aItem) BookmarksTestHelper._nodes[aItem].pinned = false,
+    setPinned: function (aItem) BookmarksTestHelper._nodes[aItem].pinned = true,
+  },
+
+  _originalUpdateFavicon: null,
+  setup: function setup() {
+    // Just enough items so that there will be one less then the limit
+    // after removing 4 items.
+    this.createNodes(gStartView._limit + 3);
+
+    this._originalNavHistoryService = gStartView._navHistoryService;
+    gStartView._navHistoryService = this.MockNavHistoryService;
+    gPanelView._navHistoryService = this.MockNavHistoryService;
+
+    this._originalBookmarkService = gStartView._bookmarkService;
+    gStartView._bookmarkService= this.MockBookmarkService;
+    gPanelView._bookmarkService= this.MockBookmarkService;
+
+    this._originalPinHelper = gStartView._pinHelper;
+    gStartView._pinHelper = this.MockPinHelper;
+    gPanelView._pinHelper = this.MockPinHelper;
+
+    this._originalUpdateFavicon = gStartView._updateFavicon;
+    gStartView._updateFavicon = function () {};
+    gPanelView._updateFavicon = function () {};
+
+    gStartView.clearBookmarks();
+    gStartView.getBookmarks();
+    gPanelView.clearBookmarks();
+    gPanelView.getBookmarks();
+  },
+
+  restore: function () {
+    gStartView._navHistoryService = this._originalNavHistoryService;
+    gStartView._bookmarkService= this._originalBookmarkService;
+    gStartView._pinHelper = this._originalPinHelper;
+    gStartView._updateFavicon = this._originalUpdateFavicon;
+
+    gPanelView._navHistoryService = this._originalNavHistoryService;
+    gPanelView._bookmarkService= this._originalBookmarkService;
+    gPanelView._pinHelper = this._originalPinHelper;
+    gPanelView._updateFavicon = this._originalUpdateFavicon;
+
+    gStartView.clearBookmarks();
+    gStartView.getBookmarks();
+    gPanelView.clearBookmarks();
+    gPanelView.getBookmarks();
+  }
+};
+
+gTests.push({
+  desc: "Test bookmarks StartUI unpin",
+  setUp: setup,
+  tearDown: tearDown,
+  run: function testBookmarksStartUnpin() {
+    let unpinButton = document.getElementById("unpin-selected-button");
+
+    // --------- unpin item 2
+
+    let item = gStartView._getItemForBookmarkId(2);
+
+    let promise = waitForEvent(Elements.appbar, "transitionend");
+    sendContextMenuClickToElement(window, item, 10, 10);
+    yield promise;
+
+    ok(!unpinButton.hidden, "Unpin button is visible.");
+
+    let promise = waitForEvent(Elements.appbar, "transitionend");
+    EventUtils.synthesizeMouse(unpinButton, 10, 10, {}, window);
+    yield promise;
+
+    item = gStartView._getItemForBookmarkId(2);
+
+    ok(!item, "Item not in grid");
+    ok(!gStartView._pinHelper.isPinned(2), "Item unpinned");
+    ok(gStartView._set.itemCount === gStartView._limit, "Grid repopulated");
+
+    // --------- unpin multiple items
+
+    let item1 = gStartView._getItemForBookmarkId(0);
+    let item2 = gStartView._getItemForBookmarkId(5);
+    let item3 = gStartView._getItemForBookmarkId(12);
+
+    let promise = waitForEvent(Elements.appbar, "transitionend");
+    sendContextMenuClickToElement(window, item1, 10, 10);
+    sendContextMenuClickToElement(window, item2, 10, 10);
+    sendContextMenuClickToElement(window, item3, 10, 10);
+    yield promise;
+
+    ok(!unpinButton.hidden, "Unpin button is visible.");
+
+    let promise = waitForEvent(Elements.appbar, "transitionend");
+    EventUtils.synthesizeMouse(unpinButton, 10, 10, {}, window);
+    yield promise;
+
+    item1 = gStartView._getItemForBookmarkId(0);
+    item2 = gStartView._getItemForBookmarkId(5);
+    item3 = gStartView._getItemForBookmarkId(12);
+
+    ok(!item1 && !item2 && !item3, "Items are not in grid");
+    ok(!gStartView._pinHelper.isPinned(0) && !gStartView._pinHelper.isPinned(5) && !gStartView._pinHelper.isPinned(12) , "Items unpinned");
+    ok(gStartView._set.itemCount === gStartView._limit - 1, "Grid repopulated");
+  }
+});
+
+gTests.push({
+  desc: "Test bookmarks StartUI delete",
+  setUp: setup,
+  tearDown: tearDown,
+  run: function testBookmarksStartDelete() {
+    let restoreButton = document.getElementById("restore-selected-button");
+    let deleteButton = document.getElementById("delete-selected-button");
+
+    // --------- delete item 2 and restore
+
+    let item = gStartView._getItemForBookmarkId(2);
+    let initialLocation = gStartView._set.getIndexOfItem(item);
+
+    let promise = waitForEvent(Elements.appbar, "transitionend");
+    sendContextMenuClickToElement(window, item, 10, 10);
+    yield promise;
+
+    ok(!deleteButton.hidden, "Delete button is visible.");
+
+    let promise = waitForCondition(() => !restoreButton.hidden);
+    EventUtils.synthesizeMouse(deleteButton, 10, 10, {}, window);
+    yield promise;
+
+    item = gStartView._getItemForBookmarkId(2);
+
+    ok(!item, "Item not in grid");
+    ok(BookmarksTestHelper._nodes[2], "Item not deleted yet");
+    ok(!restoreButton.hidden, "Restore button is visible.");
+    ok(gStartView._set.itemCount === gStartView._limit, "Grid repopulated");
+
+    let promise = waitForEvent(Elements.appbar, "transitionend");
+    EventUtils.synthesizeMouse(restoreButton, 10, 10, {}, window);
+    yield promise;
+
+    item = gStartView._getItemForBookmarkId(2);
+    ok(item, "Item back in grid");
+    ok(gStartView._set.getIndexOfItem(item) === initialLocation, "Back in same position.");
+
+    // --------- delete item 2 for realz
+
+    let item = gStartView._getItemForBookmarkId(2);
+
+    let promise = waitForEvent(Elements.appbar, "transitionend");
+    sendContextMenuClickToElement(window, item, 10, 10);
+    yield promise;
+
+    ok(!deleteButton.hidden, "Delete button is visible.");
+
+    let promise = waitForCondition(() => !restoreButton.hidden);
+    EventUtils.synthesizeMouse(deleteButton, 10, 10, {}, window);
+    yield promise;
+
+    item = gStartView._getItemForBookmarkId(2);
+
+    ok(!item, "Item not in grid");
+    ok(BookmarksTestHelper._nodes[2], "Item not deleted yet");
+    ok(!restoreButton.hidden, "Restore button is visible.");
+
+    let promise = waitForEvent(Elements.appbar, "transitionend");
+    Elements.appbar.dismiss();
+    yield promise;
+
+    item = gStartView._getItemForBookmarkId(2);
+
+    ok(!item, "Item not in grid");
+    ok(!BookmarksTestHelper._nodes[2], "Item RIP");
+    ok(gStartView._set.itemCount === gStartView._limit, "Grid repopulated");
+
+    // --------- delete multiple items and restore
+
+    let item1 = gStartView._getItemForBookmarkId(0);
+    let item2 = gStartView._getItemForBookmarkId(5);
+    let item3 = gStartView._getItemForBookmarkId(12);
+
+    let initialLocation1 = gStartView._set.getIndexOfItem(item1);
+    let initialLocation2 = gStartView._set.getIndexOfItem(item2);
+    let initialLocation3 = gStartView._set.getIndexOfItem(item3);
+
+    let promise = waitForEvent(Elements.appbar, "transitionend");
+    sendContextMenuClickToElement(window, item1, 10, 10);
+    sendContextMenuClickToElement(window, item2, 10, 10);
+    sendContextMenuClickToElement(window, item3, 10, 10);
+    yield promise;
+
+    ok(!deleteButton.hidden, "Delete button is visible.");
+
+    let promise = waitForCondition(() => !restoreButton.hidden);
+    EventUtils.synthesizeMouse(deleteButton, 10, 10, {}, window);
+    yield promise;
+
+    item1 = gStartView._getItemForBookmarkId(0);
+    item2 = gStartView._getItemForBookmarkId(5);
+    item3 = gStartView._getItemForBookmarkId(12);
+
+    ok(!item1 && !item2 && !item3, "Items are not in grid");
+    ok(BookmarksTestHelper._nodes[0] && BookmarksTestHelper._nodes[5] && BookmarksTestHelper._nodes[12],
+      "Items not deleted yet");
+    ok(!restoreButton.hidden, "Restore button is visible.");
+    ok(gStartView._set.itemCount === gStartView._limit - 1, "Grid repopulated");
+
+    let promise = waitForEvent(Elements.appbar, "transitionend");
+    EventUtils.synthesizeMouse(restoreButton, 10, 10, {}, window);
+    yield promise;
+
+    item1 = gStartView._getItemForBookmarkId(0);
+    item2 = gStartView._getItemForBookmarkId(5);
+    item3 = gStartView._getItemForBookmarkId(12);
+
+    ok(item1 && item2 && item3, "Items are back in grid");
+    ok(gStartView._set.getIndexOfItem(item1) === initialLocation1 &&
+      gStartView._set.getIndexOfItem(item2) === initialLocation2 &&
+      gStartView._set.getIndexOfItem(item3) === initialLocation3, "Items back in the same position.");
+    ok(gStartView._set.itemCount === gStartView._limit, "Grid repopulated");
+
+    // --------- delete multiple items for good
+
+    let item1 = gStartView._getItemForBookmarkId(0);
+    let item2 = gStartView._getItemForBookmarkId(5);
+    let item3 = gStartView._getItemForBookmarkId(12);
+
+    let promise = waitForEvent(Elements.appbar, "transitionend");
+    sendContextMenuClickToElement(window, item1, 10, 10);
+    sendContextMenuClickToElement(window, item2, 10, 10);
+    sendContextMenuClickToElement(window, item3, 10, 10);
+    yield promise;
+
+    ok(!deleteButton.hidden, "Delete button is visible.");
+
+    let promise = waitForCondition(() => !restoreButton.hidden);
+    EventUtils.synthesizeMouse(deleteButton, 10, 10, {}, window);
+    yield promise;
+
+    item1 = gStartView._getItemForBookmarkId(0);
+    item2 = gStartView._getItemForBookmarkId(5);
+    item3 = gStartView._getItemForBookmarkId(12);
+
+    ok(!item1 && !item2 && !item3, "Items are not in grid");
+    ok(BookmarksTestHelper._nodes[0] && BookmarksTestHelper._nodes[5] && BookmarksTestHelper._nodes[12],
+      "Items not deleted yet");
+    ok(!restoreButton.hidden, "Restore button is visible.");
+
+    let promise = waitForEvent(Elements.appbar, "transitionend");
+    Elements.appbar.dismiss();
+    yield promise;
+
+    item1 = gStartView._getItemForBookmarkId(0);
+    item2 = gStartView._getItemForBookmarkId(5);
+    item3 = gStartView._getItemForBookmarkId(12);
+
+    ok(!item1 && !item2 && !item3, "Items are not in grid");
+    ok(!BookmarksTestHelper._nodes[0] && !BookmarksTestHelper._nodes[5] && !BookmarksTestHelper._nodes[12],
+      "Items are gone");
+    ok(gStartView._set.itemCount === gStartView._limit - 1, "Grid repopulated");
+  }
+});
+
+gTests.push({
+  desc: "Test bookmarks PanelUI unpin",
+  setUp: setup,
+  tearDown: tearDown,
+  run: function testBookmarksPanelUnpin() {
+    PanelUI.show('bookmarks-container');
+
+    let pinButton = document.getElementById("pin-selected-button");
+    let unpinButton = document.getElementById("unpin-selected-button");
+
+    // --------- unpin item 2
+
+    let item = gPanelView._getItemForBookmarkId(2);
+
+    let promise = waitForEvent(Elements.appbar, "transitionend");
+    sendContextMenuClickToElement(window, item, 10, 10);
+    yield promise;
+
+    ok(!unpinButton.hidden, "Unpin button is visible.");
+
+    let promise = waitForEvent(Elements.appbar, "transitionend");
+    EventUtils.synthesizeMouse(unpinButton, 10, 10, {}, window);
+    yield promise;
+
+    item = gPanelView._getItemForBookmarkId(2);
+    let startItem = gStartView._getItemForBookmarkId(2);
+
+    ok(item, "Item is in grid");
+    ok(!startItem, "Item not in start grid");
+    ok(!gPanelView._pinHelper.isPinned(2), "Item unpinned");
+
+    // --------- unpin multiple items
+
+    let item1 = gPanelView._getItemForBookmarkId(0);
+    let item2 = gPanelView._getItemForBookmarkId(5);
+    let item3 = gPanelView._getItemForBookmarkId(12);
+
+    let promise = waitForEvent(Elements.appbar, "transitionend");
+    sendContextMenuClickToElement(window, item1, 10, 10);
+    sendContextMenuClickToElement(window, item2, 10, 10);
+    sendContextMenuClickToElement(window, item3, 10, 10);
+    yield promise;
+
+    ok(!unpinButton.hidden, "Unpin button is visible.");
+
+    let promise = waitForEvent(Elements.appbar, "transitionend");
+    EventUtils.synthesizeMouse(unpinButton, 10, 10, {}, window);
+    yield promise;
+
+    item1 = gPanelView._getItemForBookmarkId(0);
+    item2 = gPanelView._getItemForBookmarkId(5);
+    item3 = gPanelView._getItemForBookmarkId(12);
+    let startItem1 = gStartView._getItemForBookmarkId(0);
+    let startItem2 = gStartView._getItemForBookmarkId(5);
+    let startItem3 = gStartView._getItemForBookmarkId(12);
+
+    ok(item1 && item2 && item3, "Items are in grid");
+    ok(!startItem1 && !startItem2 && !startItem3, "Items are not in start grid");
+    ok(!gPanelView._pinHelper.isPinned(0) && !gPanelView._pinHelper.isPinned(5) && !gPanelView._pinHelper.isPinned(12) , "Items unpinned");
+
+    // --------- pin item 2
+
+    let item = gPanelView._getItemForBookmarkId(2);
+
+    let promise = waitForEvent(Elements.appbar, "transitionend");
+    sendContextMenuClickToElement(window, item, 10, 10);
+    yield promise;
+
+    // Make sure app bar is updated
+    yield waitForCondition(() => !pinButton.hidden);
+
+    ok(!pinButton.hidden, "Pin button is visible.");
+
+    let promise = waitForEvent(Elements.appbar, "transitionend");
+    EventUtils.synthesizeMouse(pinButton, 10, 10, {}, window);
+    yield promise;
+
+    item = gPanelView._getItemForBookmarkId(2);
+    let startItem = gStartView._getItemForBookmarkId(2);
+
+    ok(item, "Item is in grid");
+    ok(startItem, "Item is back in start grid");
+    ok(gPanelView._pinHelper.isPinned(2), "Item pinned");
+
+    // --------- pin multiple items
+
+    let item1 = gPanelView._getItemForBookmarkId(0);
+    let item2 = gPanelView._getItemForBookmarkId(5);
+    let item3 = gPanelView._getItemForBookmarkId(12);
+
+    let promise = waitForEvent(Elements.appbar, "transitionend");
+    sendContextMenuClickToElement(window, item1, 10, 10);
+    sendContextMenuClickToElement(window, item2, 10, 10);
+    sendContextMenuClickToElement(window, item3, 10, 10);
+    yield promise;
+
+    // Make sure app bar is updated
+    yield waitForCondition(() => !pinButton.hidden);
+
+    ok(!pinButton.hidden, "pin button is visible.");
+
+    let promise = waitForEvent(Elements.appbar, "transitionend");
+    EventUtils.synthesizeMouse(pinButton, 10, 10, {}, window);
+    yield promise;
+
+    item1 = gPanelView._getItemForBookmarkId(0);
+    item2 = gPanelView._getItemForBookmarkId(5);
+    item3 = gPanelView._getItemForBookmarkId(12);
+    let startItem1 = gStartView._getItemForBookmarkId(0);
+    let startItem2 = gStartView._getItemForBookmarkId(5);
+    let startItem3 = gStartView._getItemForBookmarkId(12);
+
+    ok(item1 && item2 && item3, "Items are in grid");
+    ok(startItem1 && startItem2 && startItem3, "Items are back in start grid");
+    ok(gPanelView._pinHelper.isPinned(0) && gPanelView._pinHelper.isPinned(5) && gPanelView._pinHelper.isPinned(12) , "Items pinned");
+  }
+});
+
+gTests.push({
+  desc: "Test bookmarks PanelUI delete",
+  setUp: setup,
+  tearDown: tearDown,
+  run: function testBookmarksPanelDelete() {
+    PanelUI.show('bookmarks-container');
+
+    let restoreButton = document.getElementById("restore-selected-button");
+    let deleteButton = document.getElementById("delete-selected-button");
+
+    // --------- delete item 2
+
+    let item = gPanelView._getItemForBookmarkId(2);
+
+    let promise = waitForEvent(Elements.appbar, "transitionend");
+    sendContextMenuClickToElement(window, item, 10, 10);
+    yield promise;
+
+    ok(!deleteButton.hidden, "Delete button is visible.");
+
+    let promise = waitForCondition(() => !restoreButton.hidden);
+    EventUtils.synthesizeMouse(deleteButton, 10, 10, {}, window);
+    yield promise;
+
+    item = gPanelView._getItemForBookmarkId(2);
+    let startItem = gStartView._getItemForBookmarkId(2);
+
+    ok(!item, "Item is not in grid");
+    ok(startItem, "Item is not deleted from start grid yet");
+    ok(BookmarksTestHelper._nodes[2], "Item exists");
+    ok(!restoreButton.hidden, "Restore button is visible.");
+
+    let promise = waitForEvent(Elements.appbar, "transitionend");
+    Elements.appbar.dismiss();
+    yield promise;
+
+    item = gPanelView._getItemForBookmarkId(2);
+    startItem = gStartView._getItemForBookmarkId(2);
+
+    ok(!item, "Item gone from grid");
+    ok(!startItem, "Item gone from start grid");
+    ok(!BookmarksTestHelper._nodes[2], "Item RIP");
+
+    // --------- delete multiple items
+
+    let item1 = gPanelView._getItemForBookmarkId(0);
+    let item2 = gPanelView._getItemForBookmarkId(5);
+    let item3 = gPanelView._getItemForBookmarkId(12);
+
+    let promise = waitForEvent(Elements.appbar, "transitionend");
+    sendContextMenuClickToElement(window, item1, 10, 10);
+    sendContextMenuClickToElement(window, item2, 10, 10);
+    sendContextMenuClickToElement(window, item3, 10, 10);
+    yield promise;
+
+    ok(!deleteButton.hidden, "Delete button is visible.");
+
+    let promise = waitForCondition(() => !restoreButton.hidden);
+    EventUtils.synthesizeMouse(deleteButton, 10, 10, {}, window);
+    yield promise;
+
+    item1 = gPanelView._getItemForBookmarkId(0);
+    item2 = gPanelView._getItemForBookmarkId(5);
+    item3 = gPanelView._getItemForBookmarkId(12);
+    let startItem1 = gStartView._getItemForBookmarkId(0);
+    let startItem2 = gStartView._getItemForBookmarkId(5);
+    let startItem3 = gStartView._getItemForBookmarkId(12);
+
+    ok(!restoreButton.hidden, "Restore button is visible.");
+    ok(!item1 && !item2 && !item3, "Items are not in grid");
+    ok(startItem1 && startItem2 && startItem3, "Items are still in start grid");
+    ok(BookmarksTestHelper._nodes[0] && BookmarksTestHelper._nodes[5] && BookmarksTestHelper._nodes[12],
+      "Items not deleted yet");
+
+    let promise = waitForEvent(Elements.appbar, "transitionend");
+    Elements.appbar.dismiss();
+    yield promise;
+
+    item1 = gPanelView._getItemForBookmarkId(0);
+    item2 = gPanelView._getItemForBookmarkId(5);
+    item3 = gPanelView._getItemForBookmarkId(12);
+    let startItem1 = gStartView._getItemForBookmarkId(0);
+    let startItem2 = gStartView._getItemForBookmarkId(5);
+    let startItem3 = gStartView._getItemForBookmarkId(12);
+
+    ok(!item1 && !item2 && !item3, "Items are gone from grid");
+    ok(!startItem1 && !startItem2 && !startItem3, "Items are gone from start grid");
+    ok(!BookmarksTestHelper._nodes[0] && !BookmarksTestHelper._nodes[5] && !BookmarksTestHelper._nodes[12],
+      "Items are gone for good");
+  }
+});
new file mode 100644
--- /dev/null
+++ b/browser/metro/base/tests/mochitest/browser_history.js
@@ -0,0 +1,561 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* 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/. */
+
+"use strict";
+
+let gStartView = HistoryStartView._view;
+let gPanelView = HistoryPanelView._view;
+
+function test() {
+  runTests();
+}
+
+function setup() {
+  PanelUI.hide();
+  HistoryTestHelper.setup();
+
+  if (StartUI.isStartPageVisible)
+    return;
+
+  yield addTab("about:start");
+
+  yield waitForCondition(() => StartUI.isStartPageVisible);
+
+  yield hideContextUI();
+}
+
+function tearDown() {
+  PanelUI.hide();
+  HistoryTestHelper.restore();
+}
+
+var HistoryTestHelper = {
+  _originalNavHistoryService: null,
+  MockNavHistoryService: {
+    getNewQueryOptions: function () {
+      return {};
+    },
+    getNewQuery: function () {
+      return {
+        setFolders: function(){}
+      };
+    },
+    executeQuery: function () {
+      return {
+        root: {
+          get childCount() {
+            return Object.keys(HistoryTestHelper._nodes).length;
+          },
+
+          getChild: function (aIndex) HistoryTestHelper._nodes[Object.keys(HistoryTestHelper._nodes)[aIndex]]
+        }
+      }
+    }
+  },
+
+  _originalHistoryService: null,
+  MockHistoryService: {
+    removePage: function (aURI) {
+      delete HistoryTestHelper._nodes[aURI.spec];
+
+      // Simulate observer notification
+      gStartView.onDeleteURI(aURI);
+      gPanelView.onDeleteURI(aURI);
+    },
+  },
+
+  Node: function (aTitle, aURISpec) {
+    this.title = aTitle;
+    this.uri = aURISpec;
+    this.pinned = true
+  },
+
+  _nodes: null,
+  createNodes: function (aMany) {
+    this._nodes = {};
+    for (let i=0; i<aMany; i++) {
+      let title = "mock-history-" + i;
+      let uri = "http://" + title + ".com.br/";
+
+      this._nodes[uri] = new this.Node(title, uri);
+    }
+  },
+
+  _originalPinHelper: null,
+  MockPinHelper: {
+    isPinned: function (aItem) HistoryTestHelper._nodes[aItem].pinned,
+    setUnpinned: function (aItem) HistoryTestHelper._nodes[aItem].pinned = false,
+    setPinned: function (aItem) HistoryTestHelper._nodes[aItem].pinned = true,
+  },
+
+  setup: function setup() {
+    // Just enough items so that there will be one less then the limit
+    // after removing 4 items.
+    this.createNodes(gStartView._limit + 3);
+
+    this._originalNavHistoryService = gStartView._navHistoryService;
+    gStartView._navHistoryService = this.MockNavHistoryService;
+    gPanelView._navHistoryService = this.MockNavHistoryService;
+
+    this._originalHistoryService = gStartView._historyService;
+    gStartView._historyService= this.MockHistoryService;
+    gPanelView._historyService= this.MockHistoryService;
+
+    this._originalPinHelper = gStartView._pinHelper;
+    gStartView._pinHelper = this.MockPinHelper;
+    gPanelView._pinHelper = this.MockPinHelper;
+
+    gStartView._set.clearAll();
+    gStartView.populateGrid();
+    gPanelView._set.clearAll();
+    gPanelView.populateGrid();
+  },
+
+  restore: function () {
+    gStartView._navHistoryService = this._originalNavHistoryService;
+    gStartView._historyService= this._originalHistoryService;
+    gStartView._pinHelper = this._originalPinHelper;
+
+    gPanelView._navHistoryService = this._originalNavHistoryService;
+    gPanelView._historyService= this._originalHistoryService;
+    gPanelView._pinHelper = this._originalPinHelper;
+
+    gStartView._set.clearAll();
+    gStartView.populateGrid();
+    gPanelView._set.clearAll();
+    gPanelView.populateGrid();
+  }
+};
+
+function uriFromIndex(aIndex) {
+  return "http://mock-history-" + aIndex + ".com.br/"
+}
+
+gTests.push({
+  desc: "Test history StartUI unpin",
+  setUp: setup,
+  tearDown: tearDown,
+  run: function testHistoryStartUnpin() {
+    let unpinButton = document.getElementById("unpin-selected-button");
+
+    // --------- unpin item 2
+
+    let item = gStartView._set.getItemsByUrl(uriFromIndex(2))[0];
+
+    let promise = waitForEvent(Elements.appbar, "transitionend", null, Elements.appbar);
+    sendContextMenuClickToElement(window, item, 10, 10);
+    yield promise;
+
+    ok(!unpinButton.hidden, "Unpin button is visible.");
+
+    let promise = waitForEvent(Elements.appbar, "transitionend", null, Elements.appbar);
+    unpinButton.click();
+    yield promise;
+
+    item = gStartView._set.getItemsByUrl(uriFromIndex(2))[0];
+
+    ok(!item, "Item not in grid");
+    ok(!gStartView._pinHelper.isPinned(uriFromIndex(2)), "Item unpinned");
+    ok(gStartView._set.itemCount === gStartView._limit, "Grid repopulated");
+
+    // --------- unpin multiple items
+
+    let item1 = gStartView._set.getItemsByUrl(uriFromIndex(0))[0];
+    let item2 = gStartView._set.getItemsByUrl(uriFromIndex(5))[0];
+    let item3 = gStartView._set.getItemsByUrl(uriFromIndex(12))[0];
+
+    let promise = waitForEvent(Elements.appbar, "transitionend", null, Elements.appbar);
+    sendContextMenuClickToElement(window, item1, 10, 10);
+    sendContextMenuClickToElement(window, item2, 10, 10);
+    sendContextMenuClickToElement(window, item3, 10, 10);
+    yield promise;
+
+    ok(!unpinButton.hidden, "Unpin button is visible.");
+
+    let promise = waitForEvent(Elements.appbar, "transitionend", null, Elements.appbar);
+    EventUtils.synthesizeMouse(unpinButton, 10, 10, {}, window);
+    yield promise;
+
+    item1 = gStartView._set.getItemsByUrl(uriFromIndex(0))[0];
+    item2 = gStartView._set.getItemsByUrl(uriFromIndex(5))[0];
+    item3 = gStartView._set.getItemsByUrl(uriFromIndex(12))[0];
+
+    ok(!item1 && !item2 && !item3, "Items are not in grid");
+    ok(!gStartView._pinHelper.isPinned(uriFromIndex(0)) && !gStartView._pinHelper.isPinned(uriFromIndex(5)) && !gStartView._pinHelper.isPinned(uriFromIndex(12)) , "Items unpinned");
+    ok(gStartView._set.itemCount === gStartView._limit - 1, "Grid repopulated");
+  }
+});
+
+gTests.push({
+  desc: "Test history StartUI delete",
+  setUp: setup,
+  tearDown: tearDown,
+  run: function testHistoryStartDelete() {
+    let restoreButton = document.getElementById("restore-selected-button");
+    let deleteButton = document.getElementById("delete-selected-button");
+
+    // --------- delete item 2 and restore
+
+    let item = gStartView._set.getItemsByUrl(uriFromIndex(2))[0];
+    let initialLocation = gStartView._set.getIndexOfItem(item);
+
+    let promise = waitForEvent(Elements.appbar, "transitionend", null, Elements.appbar);
+    sendContextMenuClickToElement(window, item, 10, 10);
+    yield promise;
+
+    ok(!deleteButton.hidden, "Delete button is visible.");
+
+    let promise = waitForCondition(() => !restoreButton.hidden);
+    EventUtils.synthesizeMouse(deleteButton, 10, 10, {}, window);
+    yield promise;
+
+    item = gStartView._set.getItemsByUrl(uriFromIndex(2))[0];
+
+    ok(!item, "Item not in grid");
+    ok(HistoryTestHelper._nodes[uriFromIndex(2)], "Item not deleted yet");
+    ok(!restoreButton.hidden, "Restore button is visible.");
+    ok(gStartView._set.itemCount === gStartView._limit, "Grid repopulated");
+
+    let promise = waitForEvent(Elements.appbar, "transitionend", null, Elements.appbar);
+    EventUtils.synthesizeMouse(restoreButton, 10, 10, {}, window);
+    yield promise;
+
+    item = gStartView._set.getItemsByUrl(uriFromIndex(2))[0];
+    ok(item, "Item back in grid");
+    ok(gStartView._set.getIndexOfItem(item) === initialLocation, "Back in same position.");
+    ok(gStartView._set.itemCount === gStartView._limit, "Grid repopulated");
+
+    // --------- delete item 2 for realz
+
+    let item = gStartView._set.getItemsByUrl(uriFromIndex(2))[0];
+
+    let promise = waitForEvent(Elements.appbar, "transitionend", null, Elements.appbar);
+    sendContextMenuClickToElement(window, item, 10, 10);
+    yield promise;
+
+    ok(!deleteButton.hidden, "Delete button is visible.");
+
+    let promise = waitForCondition(() => !restoreButton.hidden);
+    EventUtils.synthesizeMouse(deleteButton, 10, 10, {}, window);
+    yield promise;
+
+    item = gStartView._set.getItemsByUrl(uriFromIndex(2))[0];
+
+    ok(!item, "Item not in grid");
+    ok(HistoryTestHelper._nodes[uriFromIndex(2)], "Item not deleted yet");
+    ok(!restoreButton.hidden, "Restore button is visible.");
+
+    let promise = waitForEvent(Elements.appbar, "transitionend", null, Elements.appbar);
+    Elements.appbar.dismiss();
+    yield promise;
+
+    item = gStartView._set.getItemsByUrl(uriFromIndex(2))[0];
+
+    ok(!item, "Item not in grid");
+    ok(!HistoryTestHelper._nodes[uriFromIndex(2)], "Item RIP");
+    ok(gStartView._set.itemCount === gStartView._limit, "Grid repopulated");
+
+    // --------- delete multiple items and restore
+
+    let item1 = gStartView._set.getItemsByUrl(uriFromIndex(0))[0];
+    let item2 = gStartView._set.getItemsByUrl(uriFromIndex(5))[0];
+    let item3 = gStartView._set.getItemsByUrl(uriFromIndex(12))[0];
+
+    let initialLocation1 = gStartView._set.getIndexOfItem(item1);
+    let initialLocation2 = gStartView._set.getIndexOfItem(item2);
+    let initialLocation3 = gStartView._set.getIndexOfItem(item3);
+
+    let promise = waitForEvent(Elements.appbar, "transitionend", null, Elements.appbar);
+    sendContextMenuClickToElement(window, item1, 10, 10);
+    sendContextMenuClickToElement(window, item2, 10, 10);
+    sendContextMenuClickToElement(window, item3, 10, 10);
+    yield promise;
+
+    yield waitForCondition(() => !deleteButton.hidden);
+
+    ok(!deleteButton.hidden, "Delete button is visible.");
+
+    let promise = waitForCondition(() => !restoreButton.hidden);
+    EventUtils.synthesizeMouse(deleteButton, 10, 10, {}, window);
+    yield promise;
+
+    item1 = gStartView._set.getItemsByUrl(uriFromIndex(0))[0];
+    item2 = gStartView._set.getItemsByUrl(uriFromIndex(5))[0];
+    item3 = gStartView._set.getItemsByUrl(uriFromIndex(12))[0];
+
+    ok(!item1 && !item2 && !item3, "Items are not in grid");
+    ok(HistoryTestHelper._nodes[uriFromIndex(0)] && HistoryTestHelper._nodes[uriFromIndex(5)] && HistoryTestHelper._nodes[uriFromIndex(12)],
+      "Items not deleted yet");
+    ok(!restoreButton.hidden, "Restore button is visible.");
+    ok(gStartView._set.itemCount === gStartView._limit - 1, "Grid repopulated");
+
+    let promise = waitForEvent(Elements.appbar, "transitionend", null, Elements.appbar);
+    EventUtils.synthesizeMouse(restoreButton, 10, 10, {}, window);
+    yield promise;
+
+    item1 = gStartView._set.getItemsByUrl(uriFromIndex(0))[0];
+    item2 = gStartView._set.getItemsByUrl(uriFromIndex(5))[0];
+    item3 = gStartView._set.getItemsByUrl(uriFromIndex(12))[0];
+
+    ok(item1 && item2 && item3, "Items are back in grid");
+    ok(gStartView._set.getIndexOfItem(item1) === initialLocation1 &&
+      gStartView._set.getIndexOfItem(item2) === initialLocation2 &&
+      gStartView._set.getIndexOfItem(item3) === initialLocation3, "Items back in the same position.");
+    ok(gStartView._set.itemCount === gStartView._limit, "Grid repopulated");
+
+    // --------- delete multiple items for good
+
+    let item1 = gStartView._set.getItemsByUrl(uriFromIndex(0))[0];
+    let item2 = gStartView._set.getItemsByUrl(uriFromIndex(5))[0];
+    let item3 = gStartView._set.getItemsByUrl(uriFromIndex(12))[0];
+
+    let promise = waitForEvent(Elements.appbar, "transitionend", null, Elements.appbar);
+    sendContextMenuClickToElement(window, item1, 10, 10);
+    sendContextMenuClickToElement(window, item2, 10, 10);
+    sendContextMenuClickToElement(window, item3, 10, 10);
+    yield promise;
+
+    ok(!deleteButton.hidden, "Delete button is visible.");
+
+    let promise = waitForCondition(() => !restoreButton.hidden);
+    EventUtils.synthesizeMouse(deleteButton, 10, 10, {}, window);
+    yield promise;
+
+    item1 = gStartView._set.getItemsByUrl(uriFromIndex(0))[0];
+    item2 = gStartView._set.getItemsByUrl(uriFromIndex(5))[0];
+    item3 = gStartView._set.getItemsByUrl(uriFromIndex(12))[0];
+
+    ok(!item1 && !item2 && !item3, "Items are not in grid");
+    ok(HistoryTestHelper._nodes[uriFromIndex(0)] && HistoryTestHelper._nodes[uriFromIndex(5)] && HistoryTestHelper._nodes[uriFromIndex(12)],
+      "Items not deleted yet");
+    ok(!restoreButton.hidden, "Restore button is visible.");
+    ok(gStartView._set.itemCount === gStartView._limit - 1, "Grid repopulated");
+
+    let promise = waitForEvent(Elements.appbar, "transitionend", null, Elements.appbar);
+    Elements.appbar.dismiss();
+    yield promise;
+
+    item1 = gStartView._set.getItemsByUrl(uriFromIndex(0))[0];
+    item2 = gStartView._set.getItemsByUrl(uriFromIndex(5))[0];
+    item3 = gStartView._set.getItemsByUrl(uriFromIndex(12))[0];
+
+    ok(!item1 && !item2 && !item3, "Items are not in grid");
+    ok(!HistoryTestHelper._nodes[uriFromIndex(0)] && !HistoryTestHelper._nodes[uriFromIndex(5)] && !HistoryTestHelper._nodes[uriFromIndex(12)],
+      "Items are gone");
+    ok(gStartView._set.itemCount === gStartView._limit - 1, "Grid repopulated");
+  }
+});
+
+gTests.push({
+  desc: "Test history PanelUI unpin",
+  setUp: setup,
+  tearDown: tearDown,
+  run: function testHistoryPanelUnpin() {
+    PanelUI.show('history-container');
+
+    let pinButton = document.getElementById("pin-selected-button");
+    let unpinButton = document.getElementById("unpin-selected-button");
+
+    // --------- unpin item 2
+
+    let item = gPanelView._set.getItemsByUrl(uriFromIndex(2))[0];
+
+    let promise = waitForEvent(Elements.appbar, "transitionend", null, Elements.appbar);
+    sendContextMenuClickToElement(window, item, 10, 10);
+    yield promise;
+
+    yield waitForCondition(() => !unpinButton.hidden);
+
+    ok(!unpinButton.hidden, "Unpin button is visible.");
+
+    let promise = waitForEvent(Elements.appbar, "transitionend", null, Elements.appbar);
+    EventUtils.synthesizeMouse(unpinButton, 10, 10, {}, window);
+    yield promise;
+
+    item = gPanelView._set.getItemsByUrl(uriFromIndex(2))[0];
+    let startItem = gStartView._set.getItemsByUrl(uriFromIndex(2))[0];
+
+    ok(item, "Item is in grid");
+    ok(!startItem, "Item not in start grid");
+    ok(!gPanelView._pinHelper.isPinned(uriFromIndex(2)), "Item unpinned");
+
+    // --------- unpin multiple items
+
+    let item1 = gPanelView._set.getItemsByUrl(uriFromIndex(0))[0];
+    let item2 = gPanelView._set.getItemsByUrl(uriFromIndex(5))[0];
+    let item3 = gPanelView._set.getItemsByUrl(uriFromIndex(12))[0];
+
+    let promise = waitForEvent(Elements.appbar, "transitionend", null, Elements.appbar);
+    sendContextMenuClickToElement(window, item1, 10, 10);
+    sendContextMenuClickToElement(window, item2, 10, 10);
+    sendContextMenuClickToElement(window, item3, 10, 10);
+    yield promise;
+
+    ok(!unpinButton.hidden, "Unpin button is visible.");
+
+    let promise = waitForEvent(Elements.appbar, "transitionend", null, Elements.appbar);
+    EventUtils.synthesizeMouse(unpinButton, 10, 10, {}, window);
+    yield promise;
+
+    item1 = gPanelView._set.getItemsByUrl(uriFromIndex(0))[0];
+    item2 = gPanelView._set.getItemsByUrl(uriFromIndex(5))[0];
+    item3 = gPanelView._set.getItemsByUrl(uriFromIndex(12))[0];
+    let startItem1 = gStartView._set.getItemsByUrl(uriFromIndex(0))[0];
+    let startItem2 = gStartView._set.getItemsByUrl(uriFromIndex(5))[0];
+    let startItem3 = gStartView._set.getItemsByUrl(uriFromIndex(12))[0];
+
+    ok(item1 && item2 && item3, "Items are in grid");
+    ok(!startItem1 && !startItem2 && !startItem3, "Items are not in start grid");
+    ok(!gPanelView._pinHelper.isPinned(uriFromIndex(0)) && !gPanelView._pinHelper.isPinned(uriFromIndex(5)) && !gPanelView._pinHelper.isPinned(uriFromIndex(12)) , "Items unpinned");
+
+    // --------- pin item 2
+
+    let item = gPanelView._set.getItemsByUrl(uriFromIndex(2))[0];
+
+    let promise = waitForEvent(Elements.appbar, "transitionend", null, Elements.appbar);
+    sendContextMenuClickToElement(window, item, 10, 10);
+    yield promise;
+
+    // Make sure app bar is updated
+    yield waitForCondition(() => !pinButton.hidden);
+
+    ok(!pinButton.hidden, "Pin button is visible.");
+
+    let promise = waitForEvent(Elements.appbar, "transitionend", null, Elements.appbar);
+    EventUtils.synthesizeMouse(pinButton, 10, 10, {}, window);
+    yield promise;
+
+    item = gPanelView._set.getItemsByUrl(uriFromIndex(2))[0];
+    let startItem = gStartView._set.getItemsByUrl(uriFromIndex(2))[0];
+
+    ok(item, "Item is in grid");
+    ok(startItem, "Item is back in start grid");
+    ok(gPanelView._pinHelper.isPinned(uriFromIndex(2)), "Item pinned");
+
+    // --------- pin multiple items
+
+    let item1 = gPanelView._set.getItemsByUrl(uriFromIndex(0))[0];
+    let item2 = gPanelView._set.getItemsByUrl(uriFromIndex(5))[0];
+    let item3 = gPanelView._set.getItemsByUrl(uriFromIndex(12))[0];
+
+    let promise = waitForEvent(Elements.appbar, "transitionend", null, Elements.appbar);
+    sendContextMenuClickToElement(window, item1, 10, 10);
+    sendContextMenuClickToElement(window, item2, 10, 10);
+    sendContextMenuClickToElement(window, item3, 10, 10);
+    yield promise;
+
+    // Make sure app bar is updated
+    yield waitForCondition(() => !pinButton.hidden);
+
+    ok(!pinButton.hidden, "pin button is visible.");
+
+    let promise = waitForEvent(Elements.appbar, "transitionend", null, Elements.appbar);
+    EventUtils.synthesizeMouse(pinButton, 10, 10, {}, window);
+    yield promise;
+
+    item1 = gPanelView._set.getItemsByUrl(uriFromIndex(0))[0];
+    item2 = gPanelView._set.getItemsByUrl(uriFromIndex(5))[0];
+    item3 = gPanelView._set.getItemsByUrl(uriFromIndex(12))[0];
+    let startItem1 = gStartView._set.getItemsByUrl(uriFromIndex(0))[0];
+    let startItem2 = gStartView._set.getItemsByUrl(uriFromIndex(5))[0];
+    let startItem3 = gStartView._set.getItemsByUrl(uriFromIndex(12))[0];
+
+    ok(item1 && item2 && item3, "Items are in grid");
+    ok(startItem1 && startItem2 && startItem3, "Items are back in start grid");
+    ok(gPanelView._pinHelper.isPinned(uriFromIndex(0)) && gPanelView._pinHelper.isPinned(uriFromIndex(5)) && gPanelView._pinHelper.isPinned(uriFromIndex(12)) , "Items pinned");
+  }
+});
+
+gTests.push({
+  desc: "Test history PanelUI delete",
+  setUp: setup,
+  tearDown: tearDown,
+  run: function testHistoryPanelDelete() {
+    PanelUI.show('history-container');
+
+    let restoreButton = document.getElementById("restore-selected-button");
+    let deleteButton = document.getElementById("delete-selected-button");
+
+    // --------- delete item 2
+
+    let item = gPanelView._set.getItemsByUrl(uriFromIndex(2))[0];
+
+    let promise = waitForEvent(Elements.appbar, "transitionend", null, Elements.appbar);
+    sendContextMenuClickToElement(window, item, 10, 10);
+    yield promise;
+
+    ok(!deleteButton.hidden, "Delete button is visible.");
+
+    let promise = waitForCondition(() => !restoreButton.hidden);
+    EventUtils.synthesizeMouse(deleteButton, 10, 10, {}, window);
+    yield promise;
+
+    item = gPanelView._set.getItemsByUrl(uriFromIndex(2))[0];
+    let startItem = gStartView._set.getItemsByUrl(uriFromIndex(2))[0];
+
+    ok(!item, "Item is not in grid");
+    ok(startItem, "Item is not deleted from start grid yet");
+    ok(HistoryTestHelper._nodes[uriFromIndex(2)], "Item exists");
+    ok(!restoreButton.hidden, "Restore button is visible.");
+
+    let promise = waitForEvent(Elements.appbar, "transitionend", null, Elements.appbar);
+    Elements.appbar.dismiss();
+    yield promise;
+
+    item = gPanelView._set.getItemsByUrl(uriFromIndex(2))[0];
+    startItem = gStartView._set.getItemsByUrl(uriFromIndex(2))[0];
+
+    ok(!item, "Item gone from grid");
+    ok(!startItem, "Item gone from start grid");
+    ok(!HistoryTestHelper._nodes[uriFromIndex(2)], "Item RIP");
+
+    // --------- delete multiple items
+
+    let item1 = gPanelView._set.getItemsByUrl(uriFromIndex(0))[0];
+    let item2 = gPanelView._set.getItemsByUrl(uriFromIndex(5))[0];
+    let item3 = gPanelView._set.getItemsByUrl(uriFromIndex(12))[0];
+
+    let promise = waitForEvent(Elements.appbar, "transitionend", null, Elements.appbar);
+    sendContextMenuClickToElement(window, item1, 10, 10);
+    sendContextMenuClickToElement(window, item2, 10, 10);
+    sendContextMenuClickToElement(window, item3, 10, 10);
+    yield promise;
+
+    ok(!deleteButton.hidden, "Delete button is visible.");
+
+    let promise = waitForCondition(() => !restoreButton.hidden);
+    EventUtils.synthesizeMouse(deleteButton, 10, 10, {}, window);
+    yield promise;
+
+    item1 = gPanelView._set.getItemsByUrl(uriFromIndex(0))[0];
+    item2 = gPanelView._set.getItemsByUrl(uriFromIndex(5))[0];
+    item3 = gPanelView._set.getItemsByUrl(uriFromIndex(12))[0];
+    let startItem1 = gStartView._set.getItemsByUrl(uriFromIndex(0))[0];
+    let startItem2 = gStartView._set.getItemsByUrl(uriFromIndex(5))[0];
+    let startItem3 = gStartView._set.getItemsByUrl(uriFromIndex(12))[0];
+
+    ok(!restoreButton.hidden, "Restore button is visible.");
+    ok(!item1 && !item2 && !item3, "Items are not in grid");
+    ok(startItem1 && startItem2 && startItem3, "Items are still in start grid");
+    ok(HistoryTestHelper._nodes[uriFromIndex(0)] && HistoryTestHelper._nodes[uriFromIndex(5)] && HistoryTestHelper._nodes[uriFromIndex(12)],
+      "Items not deleted yet");
+
+    let promise = waitForEvent(Elements.appbar, "transitionend", null, Elements.appbar);
+    Elements.appbar.dismiss();
+    yield promise;
+
+    item1 = gPanelView._set.getItemsByUrl(uriFromIndex(0))[0];
+    item2 = gPanelView._set.getItemsByUrl(uriFromIndex(5))[0];
+    item3 = gPanelView._set.getItemsByUrl(uriFromIndex(12))[0];
+    let startItem1 = gStartView._set.getItemsByUrl(uriFromIndex(0))[0];
+    let startItem2 = gStartView._set.getItemsByUrl(uriFromIndex(5))[0];
+    let startItem3 = gStartView._set.getItemsByUrl(uriFromIndex(12))[0];
+
+    ok(!item1 && !item2 && !item3, "Items are gone from grid");
+    ok(!startItem1 && !startItem2 && !startItem3, "Items are gone from start grid");
+    ok(!HistoryTestHelper._nodes[uriFromIndex(0)] && !HistoryTestHelper._nodes[uriFromIndex(5)] && !HistoryTestHelper._nodes[uriFromIndex(12)],
+      "Items are gone for good");
+  }
+});
--- a/browser/metro/base/tests/mochitest/head.js
+++ b/browser/metro/base/tests/mochitest/head.js
@@ -153,19 +153,22 @@ function clearSelection(aTarget) {
   Asynchronous Metro ui helpers
 =============================================================================*/
 
 function hideContextUI()
 {
   purgeEventQueue();
   if (ContextUI.isVisible) {
     info("is visible, waiting...");
-    let promise = waitForEvent(Elements.tray, "transitionend");
-    ContextUI.dismiss();
-    return promise;
+    let promise = waitForEvent(Elements.tray, "transitionend", null, Elements.tray);
+    if (ContextUI.dismiss())
+    {
+      return promise;
+    }
+    return true;
   }
 }
 
 function showNavBar()
 {
   let promise = waitForEvent(Elements.tray, "transitionend");
   if (!ContextUI.isVisible) {
     ContextUI.displayNavbar();
@@ -225,25 +228,28 @@ function addTab(aUrl) {
  *      // ...
  *    }
  *
  * @param aSubject the element that should receive the event
  * @param aEventName the event to wait for
  * @param aTimeoutMs the number of miliseconds to wait before giving up
  * @returns a Promise that resolves to the received event, or to an Error
  */
-function waitForEvent(aSubject, aEventName, aTimeoutMs) {
+function waitForEvent(aSubject, aEventName, aTimeoutMs, aTarget) {
   let eventDeferred = Promise.defer();
   let timeoutMs = aTimeoutMs || kDefaultWait;
   let timerID = setTimeout(function wfe_canceller() {
     aSubject.removeEventListener(aEventName, onEvent);
     eventDeferred.reject( new Error(aEventName+" event timeout") );
   }, timeoutMs);
 
   function onEvent(aEvent) {
+    if (aTarget && aTarget !== aEvent.target)
+        return;
+
     // stop the timeout clock and resume
     clearTimeout(timerID);
     eventDeferred.resolve(aEvent);
   }
 
   function cleanup() {
     // unhook listener in case of success or failure
     aSubject.removeEventListener(aEventName, onEvent);
@@ -641,29 +647,32 @@ function purgeEventQueue() {
 =============================================================================*/
 let gCurrentTest = null;
 let gTests = [];
 
 function runTests() {
   waitForExplicitFinish();
   Task.spawn(function() {
     while((gCurrentTest = gTests.shift())){
-      info("START " + gCurrentTest.desc);
       try {
         if ('function' == typeof gCurrentTest.setUp) {
           info("SETUP " + gCurrentTest.desc);
           yield Task.spawn(gCurrentTest.setUp.bind(gCurrentTest));
         }
-        yield Task.spawn(gCurrentTest.run.bind(gCurrentTest));
-        if ('function' == typeof gCurrentTest.tearDown) {
-          info("TEARDOWN " + gCurrentTest.desc);
-          yield Task.spawn(gCurrentTest.tearDown.bind(gCurrentTest));
+        try {
+          info("RUN " + gCurrentTest.desc);
+          yield Task.spawn(gCurrentTest.run.bind(gCurrentTest));
+        } finally {
+          if ('function' == typeof gCurrentTest.tearDown) {
+            info("TEARDOWN " + gCurrentTest.desc);
+            yield Task.spawn(gCurrentTest.tearDown.bind(gCurrentTest));
+          }
         }
       } catch (ex) {
-        ok(false, "runTests: Task failed - " + ex);
+        ok(false, "runTests: Task failed - " + ex + ' at ' + ex.stack);
       } finally {
         info("END " + gCurrentTest.desc);
       }
     }
     finish();
   });
 }