Bug 498514 -- Deleting a message while it's open in another tab causes us to load multiple URLs through the docshell simultaneously. Add a few event handlers we were missing, and rework post-deletion selection logic. r=bienvenu+asuth, sr=bienvenu, blocking-tb3b3+
authorSiddharth Agarwal <sid.bugzilla@gmail.com>
Wed, 08 Jul 2009 23:41:47 +0530
changeset 3041 5bb1a26308abf4593d846fc67f8ae2d10f9ccd29
parent 3040 e33a0b494e4d8adf68f404ad8c21d50b1254a96b
child 3042 9c9440b5a999871781a145c020db44556d9773d5
push idunknown
push userunknown
push dateunknown
reviewersbienvenu, bienvenu, blocking-tb3b3
bugs498514
Bug 498514 -- Deleting a message while it's open in another tab causes us to load multiple URLs through the docshell simultaneously. Add a few event handlers we were missing, and rework post-deletion selection logic. r=bienvenu+asuth, sr=bienvenu, blocking-tb3b3+
mail/base/content/folderDisplay.js
mail/base/content/messageDisplay.js
mail/base/content/messageWindow.js
mail/test/mozmill/folder-display/test-deletion-with-multiple-displays.js
mail/test/mozmill/shared-modules/test-folder-display-helpers.js
mailnews/base/src/nsMsgDBView.cpp
mailnews/base/util/jsTreeSelection.js
--- a/mail/base/content/folderDisplay.js
+++ b/mail/base/content/folderDisplay.js
@@ -129,24 +129,33 @@ function FolderDisplayWidget(aTabInfo, a
   this._active = false;
   /**
    * A list of methods to call on 'this' object when we are next made active.
    *  This list is populated by calls to |_notifyWhenActive| when we are
    *  not active at the moment.
    */
   this._notificationsPendingActivation = [];
 
-  let dummyDOMNode = document.getElementById('mail-toolbox');
+  // Create a DOM node for the fake tree box below.
+  let domNode = document.createElementNS(
+      "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "vbox");
+
+  // We care about onselect events, so add a listener for that.
+  let self = this;
+  domNode.addEventListener("select", function () {
+    self.view.dbView.selectionChanged();
+  }, false);
+
   /**
    * Create a fake tree box object for if/when this folder is in the background.
-   * We need to give it a bogus DOM object to send events to, so we choose the
-   *  mail-toolbox, who is hopefully unlikely to take offense.
+   * We need to give it a DOM object to send events to, including the onselect
+   * event we care about and for which we added a handler above, and all the
+   * other events we don't care about.
    */
-  this._fakeTreeBox = dummyDOMNode ?
-                        new FakeTreeBoxObject(dummyDOMNode.boxObject) : null;
+  this._fakeTreeBox = new FakeTreeBoxObject(domNode);
 
   /**
    * Create a fake tree selection for cases where we have opened a background
    * tab. We'll get rid of this as soon as we've switched to the tab for the
    * first time, and have a real tree selection.
    */
   this._fakeTreeSelection = new JSTreeSelection(this._fakeTreeBox);
 
@@ -818,22 +827,18 @@ FolderDisplayWidget.prototype = {
       UpdateSortIndicators(this.view.primarySortType,
                            this.view.primarySortOrder);
   },
 
   /**
    * Messages (that may have been displayed) have been removed; this may impact
    *  our message selection.  If we saw this coming, then
    *  this._nextViewIndexAfterDelete should know what view index we should
-   *  select next.  If we didn't see this coming, the cause is likely an
-   *  explicit deletion in another tab/window.
-   * Because the nsMsgDBView is on top of things, it will already have called
-   *  summarizeSelection as a result of the changes to the message display.
-   *  So our job here is really just to try and potentially improve on the
-   *  default selection logic.
+   *  select next.  (We should really always see this coming -- the extra code
+   *  is just in case we didn't.)
    */
   onMessagesRemoved: function FolderDisplayWidget_onMessagesRemoved() {
     // - we saw this coming
     let rowCount = this.view.dbView.rowCount;
     if (!this._massMoveActive && (this._nextViewIndexAfterDelete != null)) {
       // adjust the index if it is after the last row...
       // (this can happen if the "mail.delete_matches_sort_order" pref is not
       //  set and the message is the last message in the view.)
@@ -908,17 +913,20 @@ FolderDisplayWidget.prototype = {
   /*   ==================================   */
 
   /**
    * This gets called when the selection changes AND !suppressCommandUpdating
    *  AND (we're not removing a row OR we are now out of rows).
    * In response, we update the toolbar.
    */
   updateCommandStatus: function FolderDisplayWidget_updateCommandStatus() {
-    UpdateMailToolbar("FolderDisplayWidget command updater notification");
+    // Do this only if we're active. If we aren't, we're going to take care of
+    // this when we switch back to the tab.
+    if (this._active)
+      UpdateMailToolbar("FolderDisplayWidget command updater notification");
   },
 
   /**
    * This gets called by nsMsgDBView::UpdateDisplayMessage following a call
    *  to nsIMessenger.OpenURL to kick off message display OR (UDM gets called)
    *  by nsMsgDBView::SelectionChanged in lieu of loading the message because
    *  mSupressMsgDisplay.
    * In other words, we get notified immediately after the process of displaying
@@ -953,20 +961,21 @@ FolderDisplayWidget.prototype = {
     //  to display (bug 183394).  This is not a problem for us because we hook
     //  our notification when the message load is initiated, rather than when
     //  the message completes loading.
     this._nextViewIndexAfterDelete = null;
   },
 
   /**
    * This gets called as a hint that the currently selected message is junk and
-   *  said junked message is going to be moved out of the current folder.  The
-   *  legacy behaviour is to retrieve the msgToSelectAfterDelete attribute off
-   *  the db view, stashing it for benefit of the code that gets called when a
-   *  message move/deletion is completed so that we can trigger its display.
+   *  said junked message is going to be moved out of the current folder, or
+   *  right before a header is removed from the db view.  The legacy behaviour
+   *  is to retrieve the msgToSelectAfterDelete attribute off the db view,
+   *  stashing it for benefit of the code that gets called when a message
+   *  move/deletion is completed so that we can trigger its display.
    */
   updateNextMessageAfterDelete:
       function FolderDisplayWidget_updateNextMessageAfterDelete() {
     this.hintAboutToDeleteMessages();
   },
 
   /**
    * The most recent currentIndexes on the selection (from the last time
@@ -1990,24 +1999,23 @@ FolderDisplayWidget.prototype = {
 /**
  * Implement a fake nsITreeBoxObject so that we can keep the view
  *  nsITreeSelection selections 'live' when they are in the background.  We need
  *  to do this because nsTreeSelection changes its behaviour (and gets ornery)
  *  if it does not have a box object.
  * This does not need to exist once we abandon multiplexed tabbing.
  *
  * Sometimes, nsTreeSelection tries to turn us into an nsIBoxObject and then in
- *  turn get the associated element, and then create DOM events on that.  We
- *  can't really stop that, but we can use misdirection to tell it about a box
- *  object that we don't care about.  That way it gets the bogus events,
- *  effectively blackholing them.
+ *  turn get the associated element, and then create DOM events on that. The
+ *  only event that we care about is onselect, so we get a DOM node here (with
+ *  an event listener for onselect already attached), and pass its boxObject in
+ *  whenever nsTreeSelection QIs us to nsIBoxObject.
  */
-function FakeTreeBoxObject(aDummyBoxObject) {
-  this.dummyBoxObject = aDummyBoxObject.QueryInterface(
-                          Components.interfaces.nsIBoxObject);
+function FakeTreeBoxObject(aDOMNode) {
+  this.domNode = aDOMNode;
   this.view = null;
 }
 FakeTreeBoxObject.prototype = {
   view: null,
   ensureRowIsVisible: function FakeTreeBoxObject_ensureRowIsVisible() {
     // NOP
   },
   /**
@@ -2024,27 +2032,43 @@ FakeTreeBoxObject.prototype = {
     // NOP
   },
   beginUpdateBatch: function FakeTreeBoxObject_beginUpdateBatch() {
 
   },
   endUpdateBatch: function FakeTreeBoxObject_endUpdateBatch() {
 
   },
-  rowCountChanged: function FakeTreeBoxObject_rowCountChanged() {
+  /**
+   * We're going to make an exception to our NOP rule here, as this is rather
+   * important for us to pass on. The db view calls this if a row's been
+   * inserted or deleted. Without this, the selection's going to be out of sync
+   * with the view.
+   *
+   * @param aIndex the index where the rows have been inserted or deleted
+   * @param aCount the number of rows inserted or deleted (negative for
+   *               deleted)
+   */
+  rowCountChanged: function FakeTreeBoxObject_rowCountChanged(aIndex, aCount) {
+    if (aCount == 0 || !this.view)
+      // Nothing to do
+      return;
 
+    let selection = this.view.selection;
+    if (selection)
+      selection.adjustSelection(aIndex, aCount);
   },
   /**
    * Sleight of hand!  If someone asks us about an nsIBoxObject, we tell them
-   *  about a real box object that is just a dummy and is never used for
-   *  anything.
+   *  about a real box object that only has an onselect event listener attached
+   *  to it. (This violates the QI equivalence requirement, though.)
    */
   QueryInterface: function FakeTreeBoxObject_QueryInterface(aIID) {
     if (aIID.equals(Components.interfaces.nsIBoxObject))
-      return this.dummyBoxObject;
+      return this.domNode.boxObject;
     if (!aIID.equals(Components.interfaces.nsISupports) &&
         !aIID.equals(Components.interfaces.nsITreeBoxObject))
       throw Components.results.NS_ERROR_NO_INTERFACE;
     return this;
   }
 };
 /*
  * Provide attribute and function implementations that complain very loudly if
--- a/mail/base/content/messageDisplay.js
+++ b/mail/base/content/messageDisplay.js
@@ -319,45 +319,54 @@ MessageDisplayWidget.prototype = {
   _clearSummaryTimer: function MessageDisplayWidget__clearSummaryTimer(aThis) {
     aThis._summaryStabilityTimeout = null;
   },
 
   /**
    * Called by the FolderDisplayWidget when it is being made active again and
    *  it's time for us to step up and re-display or clear the message as
    *  demanded by our multiplexed tab implementation.
-   *  
+   *
    *  @param aDontReloadMessage [optional] true if you don't want to make us
    *                            call reloadMessage even if the conditions are
    *                            right for doing so. Use only when you're sure
    *                            that you've already triggered a message load,
    *                            and that a message reload would be harmful.
    */
   makeActive: function MessageDisplayWidget_makeActive(aDontReloadMessage) {
     let wasInactive = !this._active;
     this._active = true;
 
     if (wasInactive) {
+      let dbView = this.folderDisplay.view.dbView;
       // (see our usage below)
-      let preDisplayedMessage = this.displayedMessage;
+      let preDisplayedViewIndex =
+          dbView.currentlyDisplayedMessage;
       // Force a synthetic selection changed event.  This will propagate through
       //  to a call to onSelectedMessagesChanged who will handle making sure the
       //  right message pane is in use, etc.
-      this.folderDisplay.view.dbView.selectionChanged();
+      dbView.selectionChanged();
       // The one potential problem is that the message view only triggers message
       //  streaming if it doesn't think the message is already displayed.  In that
       //  case we need to force a re-display by calling reloadMessage.  We can
-      //  detect that case by seeing if our displayedMessage value changes its
-      //  value during this call (because we will receive a onDisplayingMessage
-      //  notification).  If we should be displaying a single message but the
-      //  value does not change, we need to force a re-display.
+      //  detect that case by seeing if the preDisplayedViewIndex corresponds to
+      //  the current value of displayedMessage, since if it doesn't, the value
+      //  of displayedMessage has changed during this call (because we will
+      //  receive a onDisplayingMessage notification).  If we should be
+      //  displaying a single message but the value does not change, we need to
+      //  force a re-display.
+      // We used to use the value of this.displayedMessage prior to the
+      //  selectionChanged() call here instead of preDisplayedViewIndex, but we
+      //  don't do that any more because this.displayedMessage might be out of
+      //  sync with reality for an inactive tab.
       if (!aDontReloadMessage && this.singleMessageDisplay &&
-          this.displayedMessage && (this.displayedMessage ==
-                                    preDisplayedMessage))
-        this.folderDisplay.view.dbView.reloadMessage();
+          this.displayedMessage &&
+          (preDisplayedViewIndex != nsMsgViewIndex_None) &&
+          (this.displayedMessage == dbView.getMsgHdrAt(preDisplayedViewIndex)))
+        dbView.reloadMessage();
     }
 
     this._updateActiveMessagePane();
   },
 
   /**
    * Called by the FolderDisplayWidget when it is being made inactive or no
    *  longer requires messages to be displayed.
@@ -432,19 +441,39 @@ MessageTabDisplayWidget.prototype = {
   get visible() {
     return true;
   },
   set visible(aIgnored) {
   },
 
   onSelectedMessagesChanged:
       function MessageTabDisplayWidget_onSelectedMessagesChanged() {
-    this.__proto__.__proto__.onSelectedMessagesChanged.apply(this, arguments);
-    if (!this.closing)
-      document.getElementById('tabmail').setTabTitle(this.folderDisplay._tabInfo);
+    // Look at the number of messages left in the db view. If there aren't any,
+    // close the tab.
+    if (this.folderDisplay.view.dbView.rowCount == 0) {
+      if (!this.closing) {
+        this.closing = true;
+        document.getElementById('tabmail').closeTab(
+            this.folderDisplay._tabInfo);
+      }
+      return true;
+    }
+    else {
+      if (!this.closing)
+        document.getElementById('tabmail').setTabTitle(
+            this.folderDisplay._tabInfo);
+
+      // The db view shouldn't do anything if we're inactive or about to close
+      if (!this.active || this.closing)
+        return true;
+
+      // No summaries in a message tab
+      this.singleMessageDisplay = true;
+      return false;
+    }
   },
 
   /**
    * A message tab should never ever be blank.  Close the tab if we become
    *  blank.
    */
   clearDisplay: function MessageTabDisplayWidget_clearDisplay() {
     if (!this.closing) {
--- a/mail/base/content/messageWindow.js
+++ b/mail/base/content/messageWindow.js
@@ -231,20 +231,16 @@ StandaloneMessageDisplayWidget.prototype
     // null out the selection on the view so it operates in stand alone mode
     this.folderDisplay.view.dbView.selection = null;
     this.folderDisplay.view.dbView.loadMessageByUrl(aUri);
   },
 
   clearDisplay: function () {
     this.messageLoading = false;
     this.messageLoaded = false;
-    // XXX we should figure out why we're calling window.close() both from here
-    // and from onSelectedMessagesChanged.
-    if (!this.aboutToLoadMessage)
-      window.close();
   },
   _updateActiveMessagePane: function() {
     // no-op.  the message pane is always visible.
   },
 
   onDisplayingMessage:
       function StandaloneMessageDisplayWidget_onDisplayingMessage(aMsgHdr) {
     this.__proto__.__proto__.onDisplayingMessage.call(this, aMsgHdr);
@@ -270,19 +266,22 @@ StandaloneMessageDisplayWidget.prototype
     if (!this.isDummy)
       this.displayedUri = null;
 
     // We've loaded a message, so this should be set to false
     this.aboutToLoadMessage = false;
   },
 
   onSelectedMessagesChanged: function () {
-    // XXX we should figure out why we're calling window.close() both from here
-    // and from clearDisplay.
-    if (!this.aboutToLoadMessage && this.folderDisplay.treeSelection.count == 0) {
+    // If the message we're displaying is deleted, we won't have any selection
+    // for a while, but we'll soon select a new message. So don't test the
+    // selection count -- instead see if there are any messages in the db view
+    // at all.
+    if (!this.aboutToLoadMessage &&
+        this.folderDisplay.view.dbView.rowCount == 0) {
       window.close();
       return true;
     }
     return false;
   },
 };
 
 var messagepaneObserver = {
--- a/mail/test/mozmill/folder-display/test-deletion-with-multiple-displays.js
+++ b/mail/test/mozmill/folder-display/test-deletion-with-multiple-displays.js
@@ -15,16 +15,17 @@
  *
  * The Initial Developer of the Original Code is
  * Mozilla Messaging, Inc.
  * Portions created by the Initial Developer are Copyright (C) 2009
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
  *   Andrew Sutherland <asutherland@asutherland.org>
+ *   Siddharth Agarwal <sid.bugzilla@gmail.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
@@ -42,50 +43,69 @@
  *  this both for tabs that have ever been opened in the foreground, and tabs
  *  that haven't (and thus might have fake selections).
  */
 var MODULE_NAME = 'test-deletion-with-multiple-displays';
 
 var RELATIVE_ROOT = '../shared-modules';
 var MODULE_REQUIRES = ['folder-display-helpers', 'window-helpers'];
 
-var folder;
+var folder, lastMessageFolder, oneBeforeFolder, oneAfterFolder,
+    multipleDeletionFolder1, multipleDeletionFolder2, multipleDeletionFolder3,
+    multipleDeletionFolder4;
 
 function setupModule(module) {
   let fdh = collector.getModule('folder-display-helpers');
   fdh.installInto(module);
   let wh = collector.getModule('window-helpers');
   wh.installInto(module);
 
   folder = create_folder("DeletionA");
+  lastMessageFolder = create_folder("DeletionB");
+  oneBeforeFolder = create_folder("DeletionC");
+  oneAfterFolder = create_folder("DeletionD");
+  multipleDeletionFolder1 = create_folder("DeletionE");
+  multipleDeletionFolder2 = create_folder("DeletionF");
+  multipleDeletionFolder3 = create_folder("DeletionG");
+  multipleDeletionFolder4 = create_folder("DeletionH");
   // we want exactly as many messages as we plan to delete, so that we can test
   //  that the message window and tabs close when they run out of things to
   //  to display.
   make_new_sets_in_folder(folder, [{count: 4}]);
+
+  // since we don't test window close here, it doesn't really matter how many
+  // messages these have
+  make_new_sets_in_folder(lastMessageFolder, [{count: 4}]);
+  make_new_sets_in_folder(oneBeforeFolder, [{count: 10}]);
+  make_new_sets_in_folder(oneAfterFolder, [{count: 10}]);
+  make_new_sets_in_folder(multipleDeletionFolder1, [{count: 30}]);
+
+  // We're depending on selecting the last message here, so these do matter
+  make_new_sets_in_folder(multipleDeletionFolder2, [{count: 10}]);
+  make_new_sets_in_folder(multipleDeletionFolder3, [{count: 10}]);
+  make_new_sets_in_folder(multipleDeletionFolder4, [{count: 10}]);
 }
 
 
 var tabFolder, tabMessage, tabMessageBackground, curMessage, nextMessage;
 
 /**
  * The message window controller.  Short names because controllers get used a
  *  lot.
  */
 var msgc;
 
 /**
- * Have a message displayed in a folder tab, message tab, and message window.
- *  The idea is that as we delete from the various sources, they should all
- *  advance in lock-step through their messages, simplifying our lives (but
- *  making us explode forevermore the first time any of the tests fail.)
+ * Open up the message at aIndex in all our display mechanisms, and check to see
+ * if the displays are all correct. This also sets up all our globals.
  */
-function test_open_message_in_all_three_display_mechanisms() {
+function _open_message_in_all_four_display_mechanisms_helper(aFolder, aIndex) {
   // - Select the message in this tab.
-  tabFolder = be_in_folder(folder);
-  curMessage = select_click_row(0);
+  tabFolder = be_in_folder(aFolder);
+  curMessage = select_click_row(aIndex);
   assert_selected_and_displayed(curMessage);
 
   // - Open the tab with the message
   tabMessage = open_selected_message_in_new_tab();
   assert_selected_and_displayed(curMessage);
   assert_tab_titled_from(tabMessage, curMessage);
 
   // go back to the folder tab
@@ -97,115 +117,569 @@ function test_open_message_in_all_three_
 
   // - Open the window with the message
   // need to go back to the folder tab.  (well, should.)
   switch_tab(tabFolder);
   msgc = open_selected_message_in_new_window();
   assert_selected_and_displayed(msgc, curMessage);
 }
 
+/// Check whether this message is displayed in the folder tab
+var VERIFY_FOLDER_TAB = 0x1;
+/// Check whether this message is displayed in the foreground message tab
+var VERIFY_MESSAGE_TAB = 0x2;
+/// Check whether this message is displayed in the background message tab
+var VERIFY_BACKGROUND_MESSAGE_TAB = 0x4;
+/// Check whether this message is displayed in the message window
+var VERIFY_MESSAGE_WINDOW = 0x8;
+var VERIFY_ALL = 0xE;
+
+/**
+ * Verify that the message is displayed in the given tabs. The index is
+ * optional.
+ */
+function _verify_message_is_displayed_in(aFlags, aMessage, aIndex) {
+  if (aFlags & VERIFY_FOLDER_TAB) {
+    switch_tab(tabFolder);
+    assert_selected_and_displayed(aMessage);
+    if (aIndex !== undefined)
+      assert_selected_and_displayed(aIndex);
+  }
+  if (aFlags & VERIFY_MESSAGE_TAB) {
+    // Verify the title first
+    assert_tab_titled_from(tabMessage, aMessage);
+    switch_tab(tabMessage);
+    // Verify the title again, just in case
+    assert_tab_titled_from(tabMessage, aMessage);
+    assert_selected_and_displayed(aMessage);
+    if (aIndex !== undefined)
+      assert_selected_and_displayed(aIndex);
+  }
+  if (aFlags & VERIFY_BACKGROUND_MESSAGE_TAB) {
+    // Only verify the title
+    assert_tab_titled_from(tabMessageBackground, aMessage);
+  }
+  if (aFlags & VERIFY_MESSAGE_WINDOW) {
+    assert_selected_and_displayed(msgc, aMessage);
+    if (aIndex !== undefined)
+      assert_selected_and_displayed(msgc, aIndex);
+  }
+}
+
+/**
+ * Have a message displayed in a folder tab, message tab (foreground and
+ * background), and message window. The idea is that as we delete from the
+ * various sources, they should all advance in lock-step through their messages,
+ * simplifying our lives (but making us explode forevermore the first time any
+ * of the tests fail.)
+ */
+function test_open_first_message_in_all_four_display_mechanisms() {
+  _open_message_in_all_four_display_mechanisms_helper(folder, 0);
+}
+
 /**
  * Perform a deletion from the folder tab, verify the others update correctly
  *  (advancing to the next message).
  */
 function test_delete_in_folder_tab() {
   // - plan to end up on the guy who is currently at index 1
   curMessage = mc.dbView.getMsgHdrAt(1);
   // while we're at it, figure out who is at 2 for the next step
   nextMessage = mc.dbView.getMsgHdrAt(2);
   // - delete the message
   press_delete();
-  // - make sure the right guy is selected, and that he is at index 0
-  assert_selected_and_displayed(curMessage);
-  assert_selected_and_displayed(0);
 
-  // - make sure the message tab updated its title even without us switching
-  assert_tab_titled_from(tabMessage, curMessage);
-
-  // - switch to the message tab, make sure he is now on the right guy
-  switch_tab(tabMessage);
-  assert_selected_and_displayed(curMessage);
-
-  // - make sure the background message tab updated its title
-  assert_tab_titled_from(tabMessageBackground, curMessage);
-
-  // - check the window
-  assert_selected_and_displayed(msgc, curMessage);
+  // - verify all displays
+  _verify_message_is_displayed_in(VERIFY_ALL, curMessage, 0);
 }
 
 /**
  * Perform a deletion from the message tab, verify the others update correctly
  *  (advancing to the next message).
  */
 function test_delete_in_message_tab() {
-  // (we're still on the message tab, and nextMessage is the guy we want to see
-  //  once the delete completes.)
+  switch_tab(tabMessage);
+  // nextMessage is the guy we want to see once the delete completes.
   press_delete();
   curMessage = nextMessage;
-  assert_selected_and_displayed(curMessage);
-  assert_tab_titled_from(tabMessage, curMessage);
 
-  // - check the background message tab
-  assert_tab_titled_from(tabMessageBackground, curMessage);
+  // - verify all displays
+  _verify_message_is_displayed_in(VERIFY_ALL, curMessage, 0);
 
-  // - switch to the folder tab and make sure he is on the right guy and at 0
-  switch_tab(tabFolder);
-  assert_selected_and_displayed(curMessage);
-  assert_selected_and_displayed(0);
   // figure out the next guy...
   nextMessage = mc.dbView.getMsgHdrAt(1);
   if (!nextMessage)
     throw new Error("We ran out of messages early?");
-
-  // - check the message window
-  assert_selected_and_displayed(msgc, curMessage);
 }
 
 /**
  * Perform a deletion from the message window, verify the others update
  *  correctly (advancing to the next message).
  */
 function test_delete_in_message_window() {
-  // - delete, verify in the message window
+  // - delete
   press_delete(msgc);
   curMessage = nextMessage;
-  assert_selected_and_displayed(msgc, curMessage);
-
-  // - verify in the folder tab (we're still on this tab)
-  assert_selected_and_displayed(curMessage);
-  assert_selected_and_displayed(0);
-
-  // - verify in the message tab
-  switch_tab(tabMessage);
-  assert_selected_and_displayed(curMessage);
-  assert_tab_titled_from(tabMessage, curMessage);
-
-  // - verify in the background message tab
-  assert_tab_titled_from(tabMessageBackground, curMessage);
+  // - verify all displays
+  _verify_message_is_displayed_in(VERIFY_ALL, curMessage, 0);
 }
 
 /**
  * Delete the last message in that folder, which should close all message
  *  displays.
  */
 function test_delete_last_message_closes_message_displays() {
   // - since we have both foreground and background message tabs, we don't need
   // to open yet another tab to test
 
   // - prep for the message window disappearing
   plan_for_window_close(msgc);
 
   // - let's arbitrarily perform the deletion on this message tab
+  switch_tab(tabMessage);
   press_delete();
 
   // - the message window should have gone away...
   // (this also helps ensure that the 3pane gets enough event loop time to do
   //  all that it needs to accomplish)
   wait_for_window_close(msgc);
   msgc = null;
 
   // - and we should now be on the folder tab and there should be no other tabs
   if (mc.tabmail.tabInfo.length != 1)
     throw new Error("There should only be one tab left!");
   // the below check is implied by the previous check if things are sane-ish
   if (mc.tabmail.currentTabInfo != tabFolder)
     throw new Error("We should be on the folder tab!");
 }
+
+/*
+ * Now we retest everything, but while deleting the last message in our
+ * selection. We need to make sure we select the previously next-to-last message
+ * in that case.
+ */
+
+/**
+ * Have the last message displayed in a folder tab, message tab (foreground and
+ * background), and message window. The idea is that as we delete from the
+ * various sources, they should all advance in lock-step through their messages,
+ * simplifying our lives (but making us explode forevermore the first time any
+ * of the tests fail.)
+ */
+function test_open_last_message_in_all_four_display_mechanisms() {
+  // since we have four messages, index 3 is the last message.
+  _open_message_in_all_four_display_mechanisms_helper(lastMessageFolder, 3);
+}
+
+/**
+ * Perform a deletion from the folder tab, verify the others update correctly
+ * (advancing to the next message).
+ */
+function test_delete_last_message_in_folder_tab() {
+  // - plan to end up on the guy who is currently at index 2
+  curMessage = mc.dbView.getMsgHdrAt(2);
+  // while we're at it, figure out who is at 1 for the next step
+  nextMessage = mc.dbView.getMsgHdrAt(1);
+  // - delete the message
+  press_delete();
+
+  // - verify all displays
+  _verify_message_is_displayed_in(VERIFY_ALL, curMessage, 2);
+}
+
+/**
+ * Perform a deletion from the message tab, verify the others update correctly
+ * (advancing to the next message).
+ */
+function test_delete_last_message_in_message_tab() {
+  // (we're still on the message tab, and nextMessage is the guy we want to see
+  //  once the delete completes.)
+  press_delete();
+  curMessage = nextMessage;
+
+  // - verify all displays
+  _verify_message_is_displayed_in(VERIFY_ALL, curMessage, 1);
+  // figure out the next guy...
+
+  nextMessage = mc.dbView.getMsgHdrAt(0);
+  if (!nextMessage)
+    throw new Error("We ran out of messages early?");
+}
+
+/**
+ * Perform a deletion from the message window, verify the others update
+ * correctly (advancing to the next message).
+ */
+function test_delete_last_message_in_message_window() {
+  // Vary this up. Switch to the folder tab instead of staying on the message
+  // tab
+  switch_tab(tabFolder);
+  // - delete
+  press_delete(msgc);
+  curMessage = nextMessage;
+  // - verify all displays
+  _verify_message_is_displayed_in(VERIFY_ALL, curMessage, 0);
+
+  // - clean up, close the message window and displays
+  close_message_window(msgc);
+  close_tab(tabMessage);
+  close_tab(tabMessageBackground);
+  switch_tab(tabFolder);
+}
+
+/*
+ * Our next job is to open up a message, then delete the message one before it
+ * in another view. The other selections shouldn't be affected.
+ */
+
+/**
+ * Test "one before" deletion in the folder tab.
+ */
+function test_delete_one_before_message_in_folder_tab() {
+  // Open up message 4 in message tabs and a window (we'll delete message 3).
+  _open_message_in_all_four_display_mechanisms_helper(oneBeforeFolder, 4);
+
+  let expectedMessage = mc.dbView.getMsgHdrAt(4);
+  select_click_row(3);
+  press_delete();
+
+  // The message tab, background message tab and window shouldn't have changed
+  _verify_message_is_displayed_in(VERIFY_MESSAGE_TAB |
+                                  VERIFY_BACKGROUND_MESSAGE_TAB |
+                                  VERIFY_MESSAGE_WINDOW, expectedMessage);
+
+  // Clean up, close everything
+  close_message_window(msgc);
+  close_tab(tabMessage);
+  close_tab(tabMessageBackground);
+  switch_tab(tabFolder);
+}
+
+/**
+ * Test "one before" deletion in the message tab.
+ */
+function test_delete_one_before_message_in_message_tab() {
+  // Open up 3 in a message tab, then select and open up 4 in a background tab
+  // and window.
+  select_click_row(3);
+  tabMessage = open_selected_message_in_new_tab(true);
+  let expectedMessage = select_click_row(4);
+  tabMessageBackground = open_selected_message_in_new_tab(true);
+  msgc = open_selected_message_in_new_window(true);
+
+  // Switch to the message tab, and delete.
+  switch_tab(tabMessage);
+  press_delete();
+
+  // The folder tab, background message tab and window shouldn't have changed
+  _verify_message_is_displayed_in(VERIFY_FOLDER_TAB |
+                                  VERIFY_BACKGROUND_MESSAGE_TAB |
+                                  VERIFY_MESSAGE_WINDOW, expectedMessage);
+
+  // Clean up, close everything
+  close_message_window(msgc);
+  close_tab(tabMessage);
+  close_tab(tabMessageBackground);
+  switch_tab(tabFolder);
+}
+
+/**
+ * Test "one before" deletion in the message window.
+ */
+function test_delete_one_before_message_in_message_window() {
+  // Open up 3 in a message window, then select and open up 4 in a background
+  // and a foreground tab.
+  select_click_row(3);
+  msgc = open_selected_message_in_new_window();
+  let expectedMessage = select_click_row(4);
+  tabMessage = open_selected_message_in_new_tab();
+  switch_tab(tabFolder);
+  tabMessageBackground = open_selected_message_in_new_tab(true);
+
+  // Press delete in the message window.
+  press_delete(msgc);
+
+  // The folder tab, message tab and background message tab shouldn't have
+  // changed
+  _verify_message_is_displayed_in(VERIFY_FOLDER_TAB |
+                                  VERIFY_MESSAGE_TAB |
+                                  VERIFY_BACKGROUND_MESSAGE_TAB,
+                                  expectedMessage);
+
+  // Clean up, close everything
+  close_message_window(msgc);
+  close_tab(tabMessage);
+  close_tab(tabMessageBackground);
+  switch_tab(tabFolder);
+}
+
+/*
+ * Now do all of that again, but this time delete the message _after_ the open one.
+ */
+
+/**
+ * Test "one after" deletion in the folder tab.
+ */
+function test_delete_one_after_message_in_folder_tab() {
+  // Open up message 4 in message tabs and a window (we'll delete message 5).
+  _open_message_in_all_four_display_mechanisms_helper(oneAfterFolder, 4);
+
+  let expectedMessage = mc.dbView.getMsgHdrAt(4);
+  select_click_row(5);
+  press_delete();
+
+  // The message tab, background message tab and window shouldn't have changed
+  _verify_message_is_displayed_in(VERIFY_MESSAGE_TAB |
+                                  VERIFY_BACKGROUND_MESSAGE_TAB |
+                                  VERIFY_MESSAGE_WINDOW, expectedMessage);
+
+  // Clean up, close everything
+  close_message_window(msgc);
+  close_tab(tabMessage);
+  close_tab(tabMessageBackground);
+  switch_tab(tabFolder);
+}
+
+/**
+ * Test "one after" deletion in the message tab.
+ */
+function test_delete_one_after_message_in_message_tab() {
+  // Open up 5 in a message tab, then select and open up 4 in a background tab
+  // and window.
+  select_click_row(5);
+  tabMessage = open_selected_message_in_new_tab(true);
+  let expectedMessage = select_click_row(4);
+  tabMessageBackground = open_selected_message_in_new_tab(true);
+  msgc = open_selected_message_in_new_window(true);
+
+  // Switch to the message tab, and delete.
+  switch_tab(tabMessage);
+  press_delete();
+
+  // The folder tab, background message tab and window shouldn't have changed
+  _verify_message_is_displayed_in(VERIFY_FOLDER_TAB |
+                                  VERIFY_BACKGROUND_MESSAGE_TAB |
+                                  VERIFY_MESSAGE_WINDOW, expectedMessage);
+
+  // Clean up, close everything
+  close_message_window(msgc);
+  close_tab(tabMessage);
+  close_tab(tabMessageBackground);
+  switch_tab(tabFolder);
+}
+
+/**
+ * Test "one after" deletion in the message window.
+ */
+function test_delete_one_after_message_in_message_window() {
+  // Open up 5 in a message window, then select and open up 4 in a background
+  // and a foreground tab.
+  select_click_row(5);
+  msgc = open_selected_message_in_new_window();
+  let expectedMessage = select_click_row(4);
+  tabMessage = open_selected_message_in_new_tab();
+  switch_tab(tabFolder);
+  tabMessageBackground = open_selected_message_in_new_tab(true);
+
+  // Press delete in the message window.
+  press_delete(msgc);
+
+  // The folder tab, message tab and background message tab shouldn't have
+  // changed
+  _verify_message_is_displayed_in(VERIFY_FOLDER_TAB |
+                                  VERIFY_MESSAGE_TAB |
+                                  VERIFY_BACKGROUND_MESSAGE_TAB,
+                                  expectedMessage);
+
+  // Clean up, close everything
+  close_message_window(msgc);
+  close_tab(tabMessage);
+  close_tab(tabMessageBackground);
+  switch_tab(tabFolder);
+}
+
+/*
+ * Delete multiple messages in a folder tab. Make sure message displays at the
+ * beginning, middle and end of a selection work out.
+ */
+
+/**
+ * Test deleting multiple messages in a folder tab, with message displays open
+ * to the beginning of a selection.
+ */
+function test_delete_multiple_messages_with_first_selected_message_open() {
+  // Open up 2 in a message tab, background tab, and message window.
+  _open_message_in_all_four_display_mechanisms_helper(multipleDeletionFolder1,
+                                                      2);
+
+  // We'll select 2-5, 8, 9 and 10. We expect 6 to be the next displayed
+  // message.
+  select_click_row(2);
+  select_shift_click_row(5);
+  select_control_click_row(8);
+  select_control_click_row(9);
+  select_control_click_row(10);
+  let expectedMessage = mc.dbView.getMsgHdrAt(6);
+
+  // Delete the selected messages
+  press_delete();
+
+  // All the displays should now be showing the expectedMessage
+  _verify_message_is_displayed_in(VERIFY_ALL, expectedMessage);
+
+  // Clean up, close everything
+  close_message_window(msgc);
+  close_tab(tabMessage);
+  close_tab(tabMessageBackground);
+  switch_tab(tabFolder);
+}
+
+/**
+ * Test deleting multiple messages in a folder tab, with message displays open
+ * to somewhere in the middle of a selection.
+ */
+function test_delete_multiple_messages_with_nth_selected_message_open() {
+  // Open up 9 in a message tab, background tab, and message window.
+  _open_message_in_all_four_display_mechanisms_helper(multipleDeletionFolder1,
+                                                      9);
+
+  // We'll select 2-5, 8, 9 and 10. We expect 11 to be the next displayed
+  // message.
+  select_click_row(2);
+  select_shift_click_row(5);
+  select_control_click_row(8);
+  select_control_click_row(9);
+  select_control_click_row(10);
+  let expectedMessage = mc.dbView.getMsgHdrAt(11);
+
+  // Delete the selected messages
+  press_delete();
+
+  // All the displays should now be showing the expectedMessage
+  _verify_message_is_displayed_in(VERIFY_ALL, expectedMessage);
+
+  // Clean up, close everything
+  close_message_window(msgc);
+  close_tab(tabMessage);
+  close_tab(tabMessageBackground);
+  switch_tab(tabFolder);
+}
+
+/**
+ * Test deleting multiple messages in a folder tab, with message displays open
+ * to the end of a selection.
+ */
+function test_delete_multiple_messages_with_last_selected_message_open() {
+  // Open up 10 in a message tab, background tab, and message window.
+  _open_message_in_all_four_display_mechanisms_helper(multipleDeletionFolder1,
+                                                      9);
+
+  // We'll select 2-5, 8, 9 and 10. We expect 11 to be the next displayed
+  // message.
+  select_click_row(2);
+  select_shift_click_row(5);
+  select_control_click_row(8);
+  select_control_click_row(9);
+  select_control_click_row(10);
+  let expectedMessage = mc.dbView.getMsgHdrAt(11);
+
+  // Delete the selected messages
+  press_delete();
+
+  // All the displays should now be showing the expectedMessage
+  _verify_message_is_displayed_in(VERIFY_ALL, expectedMessage);
+
+  // Clean up, close everything
+  close_message_window(msgc);
+  close_tab(tabMessage);
+  close_tab(tabMessageBackground);
+  switch_tab(tabFolder);
+}
+
+/**
+ * Test deleting multiple messages in a folder tab (including the last one!),
+ * with message displays open to the beginning of a selection.
+ */
+function test_delete_multiple_messages_including_the_last_one_with_first_open() {
+  // 10 messages in this folder. Open up message 1 everywhere.
+  _open_message_in_all_four_display_mechanisms_helper(multipleDeletionFolder2,
+                                                      1);
+
+  // We'll select 1-4, 7, 8 and 9. We expect 5 to be the next displayed message.
+  select_click_row(1);
+  select_shift_click_row(4);
+  select_control_click_row(7);
+  select_control_click_row(8);
+  select_control_click_row(9);
+  let expectedMessage = mc.dbView.getMsgHdrAt(5);
+
+  // Delete the selected messages
+  press_delete();
+
+  // All the displays should now be showing the expectedMessage
+  _verify_message_is_displayed_in(VERIFY_ALL, expectedMessage);
+
+  // Clean up, close everything
+  close_message_window(msgc);
+  close_tab(tabMessage);
+  close_tab(tabMessageBackground);
+  switch_tab(tabFolder);
+}
+
+/**
+ * Test deleting multiple messages in a folder tab (including the last one!),
+ * with message displays open to the middle of a selection.
+ */
+function test_delete_multiple_messages_including_the_last_one_with_nth_open() {
+  // 10 messages in this folder. Open up message 7 everywhere.
+  _open_message_in_all_four_display_mechanisms_helper(multipleDeletionFolder3,
+                                                      7);
+
+  // We'll select 1-4, 7, 8 and 9. We expect 6 to be the next displayed message.
+  select_click_row(1);
+  select_shift_click_row(4);
+  select_control_click_row(7);
+  select_control_click_row(8);
+  select_control_click_row(9);
+  let expectedMessage = mc.dbView.getMsgHdrAt(6);
+
+  // Delete the selected messages
+  press_delete();
+
+  // All the displays should now be showing the expectedMessage
+  _verify_message_is_displayed_in(VERIFY_ALL, expectedMessage);
+
+  // Clean up, close everything
+  close_message_window(msgc);
+  close_tab(tabMessage);
+  close_tab(tabMessageBackground);
+  switch_tab(tabFolder);
+}
+
+/**
+ * Test deleting multiple messages in a folder tab (including the last one!),
+ * with message displays open to the end of a selection.
+ */
+function test_delete_multiple_messages_including_the_last_one_with_last_open() {
+  // 10 messages in this folder. Open up message 9 everywhere.
+  _open_message_in_all_four_display_mechanisms_helper(multipleDeletionFolder4,
+                                                      9);
+
+  // We'll select 1-4, 7, 8 and 9. We expect 6 to be the next displayed message.
+  select_click_row(1);
+  select_shift_click_row(4);
+  select_control_click_row(7);
+  select_control_click_row(8);
+  select_control_click_row(9);
+  let expectedMessage = mc.dbView.getMsgHdrAt(6);
+
+  // Delete the selected messages
+  press_delete();
+
+  // All the displays should now be showing the expectedMessage
+  _verify_message_is_displayed_in(VERIFY_ALL, expectedMessage);
+
+  // Clean up, close everything
+  close_message_window(msgc);
+  close_tab(tabMessage);
+  close_tab(tabMessageBackground);
+  switch_tab(tabFolder);
+}
--- a/mail/test/mozmill/shared-modules/test-folder-display-helpers.js
+++ b/mail/test/mozmill/shared-modules/test-folder-display-helpers.js
@@ -317,16 +317,18 @@ function open_selected_message_in_new_wi
 /**
  * Switch to another tab.  If no tab is specified, we switch to the 'other' tab.
  *  That is the last tab we used, most likely the tab that was current when we
  *  created this tab.
  *
  * @param aNewTab Optional, index of the other tab to switch to.
  */
 function switch_tab(aNewTab) {
+  // If we're still loading a message at this point, wait for that to finish
+  wait_for_message_display_completion();
   let targetTab = (aNewTab != null) ? aNewTab : otherTab;
   // now the current tab will be the 'other' tab after we switch
   otherTab = mc.tabmail.currentTabInfo;
   mc.tabmail.switchToTab(targetTab);
   wait_for_message_display_completion();
 }
 
 /**
--- a/mailnews/base/src/nsMsgDBView.cpp
+++ b/mailnews/base/src/nsMsgDBView.cpp
@@ -5567,17 +5567,29 @@ NS_IMETHODIMP nsMsgDBView::OnHdrFlagsCha
   return NS_OK;
 }
 
 NS_IMETHODIMP nsMsgDBView::OnHdrDeleted(nsIMsgDBHdr *aHdrChanged, nsMsgKey aParentKey, PRInt32 aFlags,
                             nsIDBChangeListener *aInstigator)
 {
   nsMsgViewIndex deletedIndex = FindHdr(aHdrChanged);
   if (deletedIndex != nsMsgViewIndex_None)
+  {
+    // Check if this message is currently selected. If it is, tell the frontend
+    // to be prepared for a delete.
+    if (mTreeSelection && mCommandUpdater)
+    {
+      PRBool isMsgSelected = PR_FALSE;
+      mTreeSelection->IsSelected(deletedIndex, &isMsgSelected);
+      if (isMsgSelected)
+        mCommandUpdater->UpdateNextMessageAfterDelete();
+    }
+
     RemoveByIndex(deletedIndex);
+  }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP nsMsgDBView::OnHdrAdded(nsIMsgDBHdr *aHdrChanged, nsMsgKey aParentKey, PRInt32 aFlags,
                           nsIDBChangeListener *aInstigator)
 {
   return OnNewHeader(aHdrChanged, aParentKey, PR_FALSE);
--- a/mailnews/base/util/jsTreeSelection.js
+++ b/mailnews/base/util/jsTreeSelection.js
@@ -114,17 +114,17 @@ JSTreeSelection.prototype = {
   /**
    * The number of currently selected rows.
    */
   _count: 0,
 
   // In the case of the stand-alone message window, there's no tree, but
   // there's a view.
   _view: null,
-  
+
   get tree JSTreeSelection_get_treeBoxObject() {
     return this._treeBoxObject;
   },
   set tree JSTreeSelection_set_treeBoxObject(aTreeBoxObject) {
     this._treeBoxObject = aTreeBoxObject;
   },
 
   set view JSTreeSelection_set_view(aView) {