Bug 478536 Crash by removing a scroll target in MozMouseScrollFailed event handler r+sr=roc
authorMasayuki Nakano <masayuki@d-toybox.com>
Wed, 18 Feb 2009 00:55:53 +0900
changeset 25065 cc31eaf33c787e21dcbabaa38f22bcca3fd82dea
parent 25064 d41e94d437f9fd7b7203db7100c0f40ef8a1e1dd
child 25066 b7cc3170f52b159c3a9e182dbb57276043cd8450
push idunknown
push userunknown
push dateunknown
bugs478536
milestone1.9.2a1pre
Bug 478536 Crash by removing a scroll target in MozMouseScrollFailed event handler r+sr=roc
content/events/src/nsEventStateManager.cpp
widget/tests/Makefile.in
widget/tests/test_bug478536.xul
widget/tests/window_bug478536.xul
--- a/content/events/src/nsEventStateManager.cpp
+++ b/content/events/src/nsEventStateManager.cpp
@@ -372,16 +372,18 @@ GetBasePrefKeyForMouseWheel(nsMouseScrol
 }
 
 class nsMouseWheelTransaction {
 public:
   static nsIFrame* GetTargetFrame() { return sTargetFrame; }
   static void BeginTransaction(nsIFrame* aTargetFrame,
                                PRInt32 aNumLines,
                                PRBool aScrollHorizontal);
+  // Be careful, UpdateTransaction may fire a DOM event, therefore, the target
+  // frame might be destroyed in the event handler.
   static PRBool UpdateTransaction(PRInt32 aNumLines,
                                   PRBool aScrollHorizontal);
   static void EndTransaction();
   static void OnEvent(nsEvent* aEvent);
   static void Shutdown();
 protected:
   static nsIntPoint GetScreenPoint(nsGUIEvent* aEvent);
   static void OnFailToScrollTarget();
@@ -538,16 +540,20 @@ nsMouseWheelTransaction::OnFailToScrollT
 {
   NS_PRECONDITION(sTargetFrame, "We don't have mouse scrolling transaction");
   // This event is used for automated tests, see bug 442774.
   nsContentUtils::DispatchTrustedEvent(
                     sTargetFrame->GetContent()->GetOwnerDoc(),
                     sTargetFrame->GetContent(),
                     NS_LITERAL_STRING("MozMouseScrollFailed"),
                     PR_TRUE, PR_TRUE);
+  // The target frame might be destroyed in the event handler, at that time,
+  // we need to finish the current transaction
+  if (!sTargetFrame)
+    EndTransaction();
 }
 
 void
 nsMouseWheelTransaction::OnTimeout(nsITimer* aTimer, void* aClosure)
 {
   if (!sTargetFrame) {
     // The transaction target was destroyed already
     EndTransaction();
@@ -2729,16 +2735,22 @@ nsEventStateManager::DoScrollText(nsPres
   // operation, or any time the mouse moves out of the frame, or when more than
   // "mousewheel.transaction.timeout" milliseconds have passed after the last
   // operation, even if the mouse hasn't moved.
   nsIFrame* lastScrollFrame = nsMouseWheelTransaction::GetTargetFrame();
   if (lastScrollFrame) {
     nsIScrollableViewProvider* svp = do_QueryFrame(lastScrollFrame);
     if (svp && (scrollView = svp->GetScrollableView())) {
       nsMouseWheelTransaction::UpdateTransaction(aNumLines, aScrollHorizontal);
+      // When the scroll event will not scroll any views, UpdateTransaction
+      // fired MozMouseScrollFailed event which is for automated testing.
+      // In the event handler, the target frame might be destroyed.  Then,
+      // we should not keep handling this scroll event.
+      if (!nsMouseWheelTransaction::GetTargetFrame())
+        return NS_OK;
     } else {
       nsMouseWheelTransaction::EndTransaction();
       lastScrollFrame = nsnull;
     }
   }
   PRBool passToParent = lastScrollFrame ? PR_FALSE : PR_TRUE;
 
   for (; scrollFrame && passToParent;
--- a/widget/tests/Makefile.in
+++ b/widget/tests/Makefile.in
@@ -55,16 +55,18 @@ REQUIRES += appshell content docshell \
 endif
 endif
 
 include $(topsrcdir)/config/rules.mk
 
 _TEST_FILES =	test_bug343416.xul \
 		test_bug444800.xul \
 		test_bug462106.xul \
+		test_bug478536.xul \
+		window_bug478536.xul \
 		test_keycodes.xul \
 		test_wheeltransaction.xul \
 		window_wheeltransaction.xul \
 		$(NULL)
 
 ifeq ($(MOZ_WIDGET_TOOLKIT),cocoa)
 _TEST_FILES += native_menus_window.xul \
                test_native_menus.xul \
new file mode 100644
--- /dev/null
+++ b/widget/tests/test_bug478536.xul
@@ -0,0 +1,36 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+                 type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=478536
+-->
+<window title="Mozilla Bug 478536"
+  xmlns:html="http://www.w3.org/1999/xhtml"
+  xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+  <title>Test for Bug 478536</title>
+  <script type="application/javascript" 
+   src="chrome://mochikit/content/MochiKit/packed.js"></script>
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+<body  xmlns="http://www.w3.org/1999/xhtml">
+<div id="content" style="display: none">
+  
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+SimpleTest.waitForExplicitFinish();
+window.open("window_bug478536.xul", "_blank", 
+            "chrome,width=600,height=600");
+
+]]>
+</script>
+
+</window>
new file mode 100644
--- /dev/null
+++ b/widget/tests/window_bug478536.xul
@@ -0,0 +1,204 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<window title="Mozilla Bug 478536"
+  width="600" height="600"
+  onload="onload();"
+  onunload="onunload();"
+  xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" />
+
+<body xmlns="http://www.w3.org/1999/xhtml" id="body">
+<style type="text/css">
+  #view {
+    overflow: auto;
+    width: 100px;
+    height: 100px;
+    border: 1px solid;
+    margin: 0;
+  }
+</style>
+<pre id="view" onscroll="onScrollView(event);">
+Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text.
+Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text.
+Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text.
+Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text.
+Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text.
+Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text.
+Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text.
+Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text.
+Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text.
+Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text.
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+function ok(aCondition, aMessage)
+{
+  window.opener.wrappedJSObject.SimpleTest.ok(aCondition, aMessage);
+}
+
+function is(aLeft, aRight, aMessage)
+{
+  window.opener.wrappedJSObject.SimpleTest.is(aLeft, aRight, aMessage);
+}
+
+function isnot(aLeft, aRight, aMessage)
+{
+  window.opener.wrappedJSObject.SimpleTest.isnot(aLeft, aRight, aMessage);
+}
+
+var gBody = document.getElementById("body");
+var gView = document.getElementById("view");
+
+/**
+ * Description:
+ *
+ *   First, lock the wheel scrolling target to "view" at first step.
+ *   Next, scroll back to top most of the "view" at second step.
+ *   Finally, scroll back again at third step.  This fails to scroll the "view",
+ *   then, |onMouseScrollFailed| event should be fired.  And at that time, we
+ *   can remove the "view".  So, in post processing of the event firere, the
+ *   "view" should not be referred.
+ *
+ *   For suppressing random test failure, all tests will be retried if we handle
+ *   unexpected timeout event.
+ */
+
+var gTests = [
+ { scrollToForward: true,  shouldScroll: true },
+ { scrollToForward: false, shouldScroll: true },
+ { scrollToForward: false, shouldScroll: false }
+];
+var gCurrentTestIndex = -1;
+var gIgnoreScrollEvent = true;
+
+var gPrefSvc = Components.classes["@mozilla.org/preferences-service;1"].
+               getService(Components.interfaces.nsIPrefBranch2);
+const kPrefNameTimeout = "mousewheel.transaction.timeout";
+const kDefaultTimeout = gPrefSvc.getIntPref(kPrefNameTimeout);
+
+var gTimeout = kDefaultTimeout;
+
+gBody.addEventListener("MozMouseScrollFailed", onMouseScrollFailed, false);
+gBody.addEventListener("MozMouseScrollTransactionTimeout",
+                       onTransactionTimeout, false);
+
+function setTimeoutPrefs(aTimeout)
+{
+  gPrefSvc.setIntPref(kPrefNameTimeout, aTimeout);
+  gTimeout = aTimeout;
+}
+
+function resetTimeoutPrefs()
+{
+  if (gTimeout == kDefaultTimeout)
+    return;
+  setTimeoutPrefs(kDefaultTimeout);
+}
+
+function growUpTimeoutPrefs()
+{
+  if (gTimeout != kDefaultTimeout)
+    return;
+  setTimeoutPrefs(5000);
+}
+
+function onload()
+{
+  disableNonTestMouseEvents(true);
+  setTimeout(runNextTest, 0);
+}
+
+function onunload()
+{
+  resetTimeoutPrefs();
+  disableNonTestMouseEvents(false);
+  window.opener.wrappedJSObject.SimpleTest.finish();
+}
+
+function finish()
+{
+  window.close();
+}
+
+// testing code
+
+var gTimer;
+function clearTimer()
+{
+  clearTimeout(gTimer);
+  gTimer = 0;
+}
+
+function runNextTest()
+{
+  clearTimer();
+  if (++gCurrentTestIndex >= gTests.length) {
+    ok(true, "didn't crash, succeeded");
+    finish();
+    return;
+  }
+  fireWheelScrollEvent(gTests[gCurrentTestIndex].scrollToForward);
+}
+
+var gRetryCount = 5;
+function retryAllTests()
+{
+  clearTimer();
+  if (--gRetryCount >= 0) {
+    gView.scrollTop = 0;
+    gView.scrollLeft = 0;
+    gCurrentTestIndex = -1;
+    growUpTimeoutPrefs();
+    ok(true, "WARNING: retry current test-list...");
+    gTimer = setTimeout(runNextTest, 0);
+  } else {
+    ok(false, "Failed by unexpected timeout");
+    finish();
+  }
+}
+
+function fireWheelScrollEvent(aForward)
+{
+  gIgnoreScrollEvent = false;
+  var event = { axis: "vertical", delta: aForward ? 4 : -4,
+                type: "DOMMouseScroll" };
+  synthesizeMouseScroll(gView, 5, 5, event, window);
+}
+
+function onScrollView(aEvent)
+{
+  if (gIgnoreScrollEvent)
+    return;
+  gIgnoreScrollEvent = true;
+  clearTimer();
+  ok(gTests[gCurrentTestIndex].shouldScroll, "The view is scrolled");
+  gTimer = setTimeout(runNextTest, 0);
+}
+
+function onMouseScrollFailed(aEvent)
+{
+  clearTimer();
+  gIgnoreScrollEvent = true;
+  ok(!gTests[gCurrentTestIndex].shouldScroll, "The view is not scrolled");
+  if (!gTests[gCurrentTestIndex].shouldScroll)
+    gBody.removeChild(gView);
+  runNextTest();
+}
+
+function onTransactionTimeout(aEvent)
+{
+  if (!gTimer)
+    return;
+  gIgnoreScrollEvent = true;
+  retryAllTests();
+}
+
+]]>
+</script>
+
+</window>