Bug 826851: Unit tests for Plugin Hang UI. r=bsmedberg
authorAaron Klotz <aklotz@mozilla.com>
Sat, 19 Apr 2014 12:06:35 -0600
changeset 197856 443dcdf8eed26aa7a2134d970c003d2bd7b85903
parent 197855 65aa67280bfb399219ab971ac1bd6856ff19e8f3
child 197857 83c8cecf82977f58aa38dbcf0764d4b208d31989
push id3624
push userasasaki@mozilla.com
push dateMon, 09 Jun 2014 21:49:01 +0000
treeherdermozilla-beta@b1a5da15899a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbsmedberg
bugs826851
milestone31.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 826851: Unit tests for Plugin Hang UI. r=bsmedberg
dom/plugins/ipc/hangui/PluginHangUIChild.cpp
dom/plugins/test/mochitest/chrome.ini
dom/plugins/test/mochitest/dialog_watcher.js
dom/plugins/test/mochitest/hangui_common.js
dom/plugins/test/mochitest/hangui_iface.js
dom/plugins/test/mochitest/hangui_subpage.html
dom/plugins/test/mochitest/test_hangui.xul
dom/plugins/test/testplugin/nptest.cpp
--- a/dom/plugins/ipc/hangui/PluginHangUIChild.cpp
+++ b/dom/plugins/ipc/hangui/PluginHangUIChild.cpp
@@ -304,16 +304,17 @@ PluginHangUIChild::HangUIDlgProc(HWND aD
           break;
         default:
           break;
       }
       break;
     }
     case WM_DESTROY: {
       EnableWindow(mParentWindow, TRUE);
+      SetForegroundWindow(mParentWindow);
       break;
     }
     default:
       break;
   }
   return FALSE;
 }
 
--- a/dom/plugins/test/mochitest/chrome.ini
+++ b/dom/plugins/test/mochitest/chrome.ini
@@ -14,16 +14,19 @@ skip-if = toolkit != "cocoa"
 [test_crash_notify.xul]
 skip-if = !crashreporter
 [test_crash_notify_no_report.xul]
 skip-if = !crashreporter
 [test_crash_submit.xul]
 skip-if = !crashreporter
 [test_hang_submit.xul]
 skip-if = !crashreporter
+[test_hangui.xul]
+skip-if = (!crashreporter) || (os != "win")
+support-files = hangui_subpage.html hangui_common.js hangui_iface.js dialog_watcher.js
 [test_idle_hang.xul]
 skip-if = (!crashreporter) || (os != "win")
 [test_npruntime.xul]
 [test_plugin_tag_clicktoplay.html]
 [test_privatemode_perwindowpb.xul]
 [test_refresh_navigator_plugins.html]
 [test_xulbrowser_plugin_visibility.xul]
 skip-if = (toolkit == "cocoa") || (os == "win")
new file mode 100644
--- /dev/null
+++ b/dom/plugins/test/mochitest/dialog_watcher.js
@@ -0,0 +1,172 @@
+const EVENT_OBJECT_SHOW = 0x8002;
+const EVENT_OBJECT_HIDE = 0x8003;
+const WINEVENT_OUTOFCONTEXT = 0;
+const WINEVENT_SKIPOWNPROCESS = 2;
+const QS_ALLINPUT = 0x04FF;
+const INFINITE = 0xFFFFFFFF;
+const WAIT_OBJECT_0 = 0;
+const WAIT_TIMEOUT = 258;
+const PM_NOREMOVE = 0;
+
+function DialogWatcher(titleText, onDialogStart, onDialogEnd) {
+  this.titleText = titleText;
+  this.onDialogStart = onDialogStart;
+  this.onDialogEnd = onDialogEnd;
+}
+
+DialogWatcher.prototype.init = function() {
+  this.hwnd = undefined;
+  if (!this.user32) {
+    this.user32 = ctypes.open("user32.dll");
+  }
+  if (!this.findWindow) {
+    this.findWindow = user32.declare("FindWindowW",
+                                     ctypes.winapi_abi,
+                                     ctypes.uintptr_t,
+                                     ctypes.jschar.ptr,
+                                     ctypes.jschar.ptr);
+  }
+  if (!this.winEventProcType) {
+    this.winEventProcType = ctypes.FunctionType(ctypes.stdcall_abi,
+                                                ctypes.void_t,
+                                                [ctypes.uintptr_t,
+                                                ctypes.uint32_t,
+                                                ctypes.uintptr_t,
+                                                ctypes.long,
+                                                ctypes.long,
+                                                ctypes.uint32_t,
+                                                ctypes.uint32_t]).ptr;
+  }
+  if (!this.setWinEventHook) {
+    this.setWinEventHook = user32.declare("SetWinEventHook",
+                                          ctypes.winapi_abi,
+                                          ctypes.uintptr_t,
+                                          ctypes.uint32_t,
+                                          ctypes.uint32_t,
+                                          ctypes.uintptr_t,
+                                          this.winEventProcType,
+                                          ctypes.uint32_t,
+                                          ctypes.uint32_t,
+                                          ctypes.uint32_t);
+  }
+  if (!this.unhookWinEvent) {
+    this.unhookWinEvent = user32.declare("UnhookWinEvent",
+                                         ctypes.winapi_abi,
+                                         ctypes.int,
+                                         ctypes.uintptr_t);
+  }
+  if (!this.pointType) {
+    this.pointType = ctypes.StructType("tagPOINT",
+                                       [ { "x": ctypes.long },
+                                         { "y": ctypes.long } ] );
+  }
+  if (!this.msgType) {
+    this.msgType = ctypes.StructType("tagMSG",
+                                     [ { "hwnd": ctypes.uintptr_t },
+                                       { "message": ctypes.uint32_t },
+                                       { "wParam": ctypes.uintptr_t },
+                                       { "lParam": ctypes.intptr_t },
+                                       { "time": ctypes.uint32_t },
+                                       { "pt": this.pointType } ] );
+  }
+  if (!this.peekMessage) {
+    this.peekMessage = user32.declare("PeekMessageW",
+                                      ctypes.winapi_abi,
+                                      ctypes.int,
+                                      this.msgType.ptr,
+                                      ctypes.uintptr_t,
+                                      ctypes.uint32_t,
+                                      ctypes.uint32_t,
+                                      ctypes.uint32_t);
+  }
+  if (!this.msgWaitForMultipleObjects) {
+    this.msgWaitForMultipleObjects = user32.declare("MsgWaitForMultipleObjects",
+                                                    ctypes.winapi_abi,
+                                                    ctypes.uint32_t,
+                                                    ctypes.uint32_t,
+                                                    ctypes.uintptr_t.ptr,
+                                                    ctypes.int,
+                                                    ctypes.uint32_t,
+                                                    ctypes.uint32_t);
+  }
+  if (!this.getWindowTextW) {
+    this.getWindowTextW = user32.declare("GetWindowTextW",
+                                         ctypes.winapi_abi,
+                                         ctypes.int,
+                                         ctypes.uintptr_t,
+                                         ctypes.jschar.ptr,
+                                         ctypes.int);
+  }
+};
+
+DialogWatcher.prototype.getWindowText = function(hwnd) {
+  var bufType = ctypes.ArrayType(ctypes.jschar);
+  var buffer = new bufType(256);
+  
+  if (this.getWindowTextW(hwnd, buffer, buffer.length)) {
+    return buffer.readString();
+  }
+};
+
+DialogWatcher.prototype.processWindowEvents = function(timeout) {
+  var onWinEvent = function(self, hook, event, hwnd, idObject, idChild, dwEventThread, dwmsEventTime) {
+    var nhwnd = Number(hwnd)
+    if (event == EVENT_OBJECT_SHOW) {
+      if (nhwnd == self.hwnd) {
+        // We've already picked up this event via FindWindow
+        return;
+      }
+      var windowText = self.getWindowText(hwnd);
+      if (windowText == self.titleText && self.onDialogStart) {
+        self.hwnd = nhwnd;
+        self.onDialogStart(nhwnd);
+      }
+    } else if (event == EVENT_OBJECT_HIDE && nhwnd == self.hwnd && self.onDialogEnd) {
+      self.onDialogEnd();
+      self.hwnd = null;
+    }
+  };
+  var self = this;
+  var callback = this.winEventProcType(function(hook, event, hwnd, idObject,
+                                                idChild, dwEventThread,
+                                                dwmsEventTime) {
+      onWinEvent(self, hook, event, hwnd, idObject, idChild, dwEventThread,
+                 dwmsEventTime);
+    } );
+  var hook = this.setWinEventHook(EVENT_OBJECT_SHOW, EVENT_OBJECT_HIDE,
+                                  0, callback, 0, 0,
+                                  WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS);
+  if (!hook) {
+    return;
+  }
+  // Check if the window is already showing
+  var hwnd = this.findWindow(null, this.titleText);
+  if (hwnd && hwnd > 0) {
+    this.hwnd = Number(hwnd);
+    if (this.onDialogStart) {
+      this.onDialogStart(this.hwnd);
+    }
+  }
+
+  if (!timeout) {
+    timeout = INFINITE;
+  }
+
+  var waitStatus = WAIT_OBJECT_0;
+  var expectingStart = this.onDialogStart && this.hwnd === undefined;
+  while (this.hwnd === undefined || this.onDialogEnd && this.hwnd) {
+    waitStatus = this.msgWaitForMultipleObjects(0, null, 0, expectingStart ?
+                                                            INFINITE : timeout, 0);
+    if (waitStatus == WAIT_OBJECT_0) {
+      var msg = new this.msgType;
+      this.peekMessage(msg.address(), 0, 0, 0, PM_NOREMOVE);
+    } else if (waitStatus == WAIT_TIMEOUT) {
+      break;
+    }
+  }
+
+  this.unhookWinEvent(hook);
+  // Returns true if the hook was successful, something was found, and we never timed out
+  return this.hwnd !== undefined && waitStatus == WAIT_OBJECT_0;
+};
+
new file mode 100644
--- /dev/null
+++ b/dom/plugins/test/mochitest/hangui_common.js
@@ -0,0 +1,20 @@
+// Plugin Hang UI constants
+const HANGUIOP_NOTHING = 0;
+const HANGUIOP_CANCEL = 1;
+const HANGUIOP_COMMAND = 2;
+const IDC_CONTINUE = 1001;
+const IDC_STOP = 1002;
+const IDC_NOFUTURE = 1003;
+
+// Windows constants
+const WM_CLOSE = 0x0010;
+const WM_COMMAND = 0x0111;
+const BM_GETCHECK = 0x00F0;
+const BM_SETCHECK = 0x00F1;
+const BN_CLICKED = 0;
+const BST_CHECKED = 1;
+
+// Test-specific constants
+const EPSILON_MS = 1000;
+const STALL_DURATION = 2;
+
new file mode 100644
--- /dev/null
+++ b/dom/plugins/test/mochitest/hangui_iface.js
@@ -0,0 +1,121 @@
+var user32;
+var sendMessage;
+var getDlgItem;
+var messageBox;
+var watcher;
+
+importScripts("hangui_common.js");
+importScripts("dialog_watcher.js");
+
+function initCTypes() {
+  if (!user32) {
+    user32 = ctypes.open("user32.dll");
+  }
+  if (!getDlgItem) {
+    getDlgItem = user32.declare("GetDlgItem",
+                                ctypes.winapi_abi,
+                                ctypes.uintptr_t,
+                                ctypes.uintptr_t,
+                                ctypes.int);
+  }
+  if (!sendMessage) {
+    sendMessage = user32.declare("SendMessageW",
+                                 ctypes.winapi_abi,
+                                 ctypes.intptr_t,
+                                 ctypes.uintptr_t,
+                                 ctypes.uint32_t,
+                                 ctypes.uintptr_t,
+                                 ctypes.intptr_t);
+  }
+  if (!messageBox) {
+    // Handy for debugging the test itself
+    messageBox = user32.declare("MessageBoxW",
+                                ctypes.winapi_abi,
+                                ctypes.int,
+                                ctypes.uintptr_t,
+                                ctypes.jschar.ptr,
+                                ctypes.jschar.ptr,
+                                ctypes.uint32_t);
+  }
+  if (!watcher) {
+    watcher = new DialogWatcher("Warning: Unresponsive plugin");
+  }
+}
+
+function postSuccess(params) {
+  self.postMessage({"status": true, "params": params});
+}
+
+function postFail(params, msg) {
+  self.postMessage({"status": false, "params": params, "msg": msg});
+}
+
+function onDialogStart(inparams, hwnd) {
+  var params = Object.create(inparams);
+  params.testName += " (Start)";
+  params.callback = null;
+  if (!params.expectToFind) {
+    postFail(params, "Dialog showed when we weren't expecting it to!");
+    return;
+  }
+  if (params.opCode == HANGUIOP_CANCEL) {
+    sendMessage(hwnd, WM_CLOSE, 0, 0);
+  } else if (params.opCode == HANGUIOP_COMMAND) {
+    if (params.check) {
+      var checkbox = getDlgItem(hwnd, IDC_NOFUTURE);
+      if (!checkbox) {
+        postFail(params, "Couldn't find checkbox");
+        return;
+      }
+      sendMessage(checkbox, BM_SETCHECK, BST_CHECKED, 0);
+      sendMessage(hwnd, WM_COMMAND, (BN_CLICKED << 16) | IDC_NOFUTURE, checkbox);
+    }
+    var button = getDlgItem(hwnd, params.commandId);
+    if (!button) {
+      postFail(params,
+               "GetDlgItem failed to find button with ID " + params.commandId);
+      return;
+    }
+    sendMessage(hwnd, WM_COMMAND, (BN_CLICKED << 16) | params.commandId, button);
+  }
+  postSuccess(params);
+}
+
+function onDialogEnd(inparams) {
+  var params = Object.create(inparams);
+  params.testName += " (End)";
+  params.callback = inparams.callback;
+  postSuccess(params);
+}
+
+self.onmessage = function(event) {
+  initCTypes();
+  watcher.init();
+  var params = event.data;
+  var timeout = params.timeoutMs;
+  if (params.expectToFind) {
+    watcher.onDialogStart = function(hwnd) { onDialogStart(params, hwnd); };
+    if (params.expectToClose) {
+      watcher.onDialogEnd = function() { onDialogEnd(params); };
+    }
+  } else {
+    watcher.onDialogStart = null;
+    watcher.onDialogEnd = null;
+  }
+  var result = watcher.processWindowEvents(timeout);
+  if (result === null) {
+    postFail(params, "Hook failed");
+  } else if (!result) {
+    if (params.expectToFind) {
+      postFail(params, "The dialog didn't show but we were expecting it to");
+    } else {
+      postSuccess(params);
+    }
+  }
+}
+
+self.onerror = function(event) {
+  var msg = "Error: " + event.message + " at " + event.filename + ":" + event.lineno;
+  postFail(null, msg);
+};
+
new file mode 100644
--- /dev/null
+++ b/dom/plugins/test/mochitest/hangui_subpage.html
@@ -0,0 +1,4 @@
+<html>
+<body onload="window.parent.frameLoaded()">
+  <embed id="plugin1" type="application/x-test" width="400" height="400"></embed>
+
new file mode 100644
--- /dev/null
+++ b/dom/plugins/test/mochitest/test_hangui.xul
@@ -0,0 +1,263 @@
+<?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"?>
+<window title="Basic Plugin Tests"
+  xmlns:html="http://www.w3.org/1999/xhtml"
+  xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <title>Plugin Hang UI Test</title>
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+  <script type="application/javascript"
+          src="http://mochi.test:8888/chrome/dom/plugins/test/mochitest/hang_test.js" />
+  <script type="application/javascript"
+          src="http://mochi.test:8888/chrome/dom/plugins/test/mochitest/hangui_common.js" />
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+  <iframe id="iframe1" src="hangui_subpage.html" width="400" height="400"></iframe>
+</body>
+<script class="testbody" type="application/javascript">
+<![CDATA[
+SimpleTest.waitForExplicitFinish();
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+const hangUITimeoutPref = "dom.ipc.plugins.hangUITimeoutSecs";
+const hangUIMinDisplayPref = "dom.ipc.plugins.hangUIMinDisplaySecs";
+const timeoutPref = "dom.ipc.plugins.timeoutSecs";
+
+var worker = new ChromeWorker("hangui_iface.js");
+worker.onmessage = function(event) {
+  var result = event.data;
+  var params = result.params;
+  var output = params.testName;
+  if (result.msg) {
+    output += ": " + result.msg;
+  }
+  ok(result.status, output);
+  if (params.callback) {
+    var cb = eval(params.callback);
+    var timeout = setTimeout(function() { clearTimeout(timeout); cb(); }, 100);
+  }
+};
+worker.onerror = function(event) {
+  var output = "Error: " + event.message + " at " + event.filename + ":" + event.lineno;
+  ok(false, output);
+};
+
+var iframe;
+var p;
+var os = Components.classes["@mozilla.org/observer-service;1"].getService(Components.interfaces.nsIObserverService);
+
+function hanguiOperation(testName, timeoutSec, expectFind, expectClose, opCode,
+                         commandId, check, cb) {
+  var timeoutMs = timeoutSec * 1000 + EPSILON_MS;
+  worker.postMessage({ "timeoutMs": timeoutMs, "expectToFind": expectFind,
+                       "expectToClose": expectClose, "opCode": opCode,
+                       "commandId": commandId, "check": check,
+                       "testName": testName, "callback": cb });
+}
+
+function hanguiExpect(testName, shouldBeShowing, shouldClose, cb) {
+  var timeoutSec = Services.prefs.getIntPref(hangUITimeoutPref);
+  if (!shouldBeShowing && !timeoutSec) {
+    timeoutSec = Services.prefs.getIntPref(timeoutPref);
+  }
+  hanguiOperation(testName, timeoutSec, shouldBeShowing, shouldClose, HANGUIOP_NOTHING, 0, false, cb);
+}
+
+function hanguiContinue(testName, check, cb) {
+  var timeoutSec = Services.prefs.getIntPref(hangUITimeoutPref);
+  hanguiOperation(testName, timeoutSec, true, true, HANGUIOP_COMMAND, IDC_CONTINUE, check, cb);
+}
+
+function hanguiStop(testName, check, cb) {
+  var timeoutSec = Services.prefs.getIntPref(hangUITimeoutPref);
+  hanguiOperation(testName, timeoutSec, true, true, HANGUIOP_COMMAND, IDC_STOP, check, cb);
+}
+
+function hanguiCancel(testName, cb) {
+  var timeoutSec = Services.prefs.getIntPref(hangUITimeoutPref);
+  hanguiOperation(testName, timeoutSec, true, true, HANGUIOP_CANCEL, 0, false, cb);
+}
+
+function finishTest() {
+  if (obsCount > 0) {
+    os.removeObserver(testObserver, "plugin-crashed");
+    --obsCount;
+  }
+  SpecialPowers.clearUserPref(hangUITimeoutPref);
+  SpecialPowers.clearUserPref(hangUIMinDisplayPref);
+  SpecialPowers.clearUserPref(timeoutPref);
+  SimpleTest.finish();
+}
+
+function runTests() {
+  if (!SimpleTest.testPluginIsOOP()) {
+    ok(true, "Skipping this test when test plugin is not OOP.");
+    SimpleTest.finish();
+  }
+
+  resetVars();
+
+  hanguiExpect("Prime ChromeWorker", false, false, "test1");
+}
+
+window.frameLoaded = runTests;
+
+var obsCount = 0;
+
+function onPluginCrashedHangUI(aEvent) {
+  ok(true, "Plugin crashed notification received");
+  is(aEvent.type, "PluginCrashed", "event is correct type");
+
+  is(p, aEvent.target, "Plugin crashed event target is plugin element");
+
+  ok(aEvent instanceof Components.interfaces.nsIDOMCustomEvent,
+     "plugin crashed event has the right interface");
+
+  var propBag = aEvent.detail.QueryInterface(Components.interfaces.nsIPropertyBag2);
+  var pluginDumpID = propBag.getPropertyAsAString("pluginDumpID");
+  isnot(pluginDumpID, "", "got a non-empty dump ID");
+  var pluginName = propBag.getPropertyAsAString("pluginName");
+  is(pluginName, "Test Plug-in", "got correct plugin name");
+  var pluginFilename = propBag.getPropertyAsAString("pluginFilename");
+  isnot(pluginFilename, "", "got a non-empty filename");
+  var didReport = propBag.getPropertyAsBool("submittedCrashReport");
+  // The app itself may or may not have decided to submit the report, so
+  // allow either true or false here.
+  ok((didReport == true || didReport == false), "event said crash report was submitted");
+  os.removeObserver(testObserver, "plugin-crashed");
+  --obsCount;
+}
+
+function resetVars() {
+  iframe = document.getElementById('iframe1');
+  p = iframe.contentDocument.getElementById("plugin1");
+  if (obsCount == 0) {
+    os.addObserver(testObserver, "plugin-crashed", true);
+    ++obsCount;
+  }
+  iframe.contentDocument.addEventListener("PluginCrashed",
+                                          onPluginCrashedHangUI,
+                                          false);
+}
+
+function test9b() {
+  hanguiExpect("test9b: Plugin Hang UI is not showing (checkbox)", false);
+  p.stall(STALL_DURATION);
+  hanguiExpect("test9b: Plugin Hang UI is still not showing (checkbox)", false, false, "finishTest");
+  p.stall(STALL_DURATION);
+}
+
+function test9a() {
+  resetVars();
+  SpecialPowers.setIntPref(hangUITimeoutPref, 1);
+  SpecialPowers.setIntPref(hangUIMinDisplayPref, 1);
+  SpecialPowers.setIntPref(timeoutPref, 45);
+  hanguiContinue("test9a: Continue button works with checkbox", true, "test9b");
+  p.stall(STALL_DURATION);
+}
+
+function test9() {
+  window.frameLoaded = test9a;
+  iframe.contentWindow.location.reload();
+}
+
+function test8a() {
+  resetVars();
+  SpecialPowers.setIntPref(hangUITimeoutPref, 1);
+  SpecialPowers.setIntPref(hangUIMinDisplayPref, 4);
+  hanguiExpect("test8a: Plugin Hang UI is not showing (disabled due to hangUIMinDisplaySecs)", false, false, "test9");
+  var exceptionThrown = false;
+  try {
+    p.hang();
+  } catch(e) {
+    exceptionThrown = true;
+  }
+  ok(exceptionThrown, "test8a: Exception thrown from hang() when plugin was terminated");
+}
+
+function test8() {
+  window.frameLoaded = test8a;
+  iframe.contentWindow.location.reload();
+}
+
+function test7a() {
+  resetVars();
+  SpecialPowers.setIntPref(hangUITimeoutPref, 0);
+  hanguiExpect("test7a: Plugin Hang UI is not showing (disabled)", false, false, "test8");
+  var exceptionThrown = false;
+  try {
+    p.hang();
+  } catch(e) {
+    exceptionThrown = true;
+  }
+  ok(exceptionThrown, "test7a: Exception thrown from hang() when plugin was terminated");
+}
+
+function test7() {
+  window.frameLoaded = test7a;
+  iframe.contentWindow.location.reload();
+}
+
+function test6() {
+  SpecialPowers.setIntPref(hangUITimeoutPref, 1);
+  SpecialPowers.setIntPref(hangUIMinDisplayPref, 1);
+  SpecialPowers.setIntPref(timeoutPref, 3);
+  hanguiExpect("test6: Plugin Hang UI is showing", true, true, "test7");
+  var exceptionThrown = false;
+  try {
+    p.hang();
+  } catch(e) {
+    exceptionThrown = true;
+  }
+  ok(exceptionThrown, "test6: Exception thrown from hang() when plugin was terminated (child timeout)");
+}
+
+function test5a() {
+  resetVars();
+  hanguiCancel("test5a: Close button works", "test6");
+  p.stall(STALL_DURATION);
+}
+
+function test5() {
+  window.frameLoaded = test5a;
+  iframe.contentWindow.location.reload();
+}
+
+function test4() {
+  hanguiStop("test4: Stop button works", false, "test5");
+  // We'll get an exception here because the plugin was terminated
+  var exceptionThrown = false;
+  try {
+    p.hang();
+  } catch(e) {
+    exceptionThrown = true;
+  }
+  ok(exceptionThrown, "test4: Exception thrown from hang() when plugin was terminated");
+}
+
+function test3() {
+  hanguiContinue("test3: Continue button works", false, "test4");
+  p.stall(STALL_DURATION);
+}
+
+function test2() {
+  // This test is identical to test1 because there were some bugs where the 
+  // Hang UI would show on the first hang but not on subsequent hangs
+  hanguiExpect("test2: Plugin Hang UI is showing", true, true, "test3");
+  p.stall(STALL_DURATION);
+}
+
+function test1() {
+  SpecialPowers.setIntPref(hangUITimeoutPref, 1);
+  SpecialPowers.setIntPref(hangUIMinDisplayPref, 1);
+  SpecialPowers.setIntPref(timeoutPref, 45);
+  hanguiExpect("test1: Plugin Hang UI is showing", true, true, "test2");
+  p.stall(STALL_DURATION);
+}
+
+]]>
+</script>
+</window>
--- a/dom/plugins/test/testplugin/nptest.cpp
+++ b/dom/plugins/test/testplugin/nptest.cpp
@@ -144,16 +144,17 @@ static bool getJavaCodebase(NPObject* np
 static bool checkObjectValue(NPObject* npobj, const NPVariant* args, uint32_t argCount, NPVariant* result);
 static bool enableFPExceptions(NPObject* npobj, const NPVariant* args, uint32_t argCount, NPVariant* result);
 static bool setCookie(NPObject* npobj, const NPVariant* args, uint32_t argCount, NPVariant* result);
 static bool getCookie(NPObject* npobj, const NPVariant* args, uint32_t argCount, NPVariant* result);
 static bool getAuthInfo(NPObject* npobj, const NPVariant* args, uint32_t argCount, NPVariant* result);
 static bool asyncCallbackTest(NPObject* npobj, const NPVariant* args, uint32_t argCount, NPVariant* result);
 static bool checkGCRace(NPObject* npobj, const NPVariant* args, uint32_t argCount, NPVariant* result);
 static bool hangPlugin(NPObject* npobj, const NPVariant* args, uint32_t argCount, NPVariant* result);
+static bool stallPlugin(NPObject* npobj, const NPVariant* args, uint32_t argCount, NPVariant* result);
 static bool getClipboardText(NPObject* npobj, const NPVariant* args, uint32_t argCount, NPVariant* result);
 static bool callOnDestroy(NPObject* npobj, const NPVariant* args, uint32_t argCount, NPVariant* result);
 static bool reinitWidget(NPObject* npobj, const NPVariant* args, uint32_t argCount, NPVariant* result);
 static bool crashPluginInNestedLoop(NPObject* npobj, const NPVariant* args, uint32_t argCount, NPVariant* result);
 static bool destroySharedGfxStuff(NPObject* npobj, const NPVariant* args, uint32_t argCount, NPVariant* result);
 static bool propertyAndMethod(NPObject* npobj, const NPVariant* args, uint32_t argCount, NPVariant* result);
 static bool getTopLevelWindowActivationState(NPObject* npobj, const NPVariant* args, uint32_t argCount, NPVariant* result);
 static bool getTopLevelWindowActivationEventCount(NPObject* npobj, const NPVariant* args, uint32_t argCount, NPVariant* result);
@@ -208,16 +209,17 @@ static const NPUTF8* sPluginMethodIdenti
   "checkObjectValue",
   "enableFPExceptions",
   "setCookie",
   "getCookie",
   "getAuthInfo",
   "asyncCallbackTest",
   "checkGCRace",
   "hang",
+  "stall",
   "getClipboardText",
   "callOnDestroy",
   "reinitWidget",
   "crashInNestedLoop",
   "destroySharedGfxStuff",
   "propertyAndMethod",
   "getTopLevelWindowActivationState",
   "getTopLevelWindowActivationEventCount",
@@ -273,16 +275,17 @@ static const ScriptableFunction sPluginM
   checkObjectValue,
   enableFPExceptions,
   setCookie,
   getCookie,
   getAuthInfo,
   asyncCallbackTest,
   checkGCRace,
   hangPlugin,
+  stallPlugin,
   getClipboardText,
   callOnDestroy,
   reinitWidget,
   crashPluginInNestedLoop,
   destroySharedGfxStuff,
   propertyAndMethod,
   getTopLevelWindowActivationState,
   getTopLevelWindowActivationEventCount,
@@ -3350,16 +3353,34 @@ hangPlugin(NPObject* npobj, const NPVari
 
   // NB: returning true here means that we weren't terminated, and
   // thus the hang detection/handling didn't work correctly.  The
   // test harness will succeed in calling this function, and the
   // test will fail.
   return true;
 }
 
+bool
+stallPlugin(NPObject* npobj, const NPVariant* args, uint32_t argCount,
+           NPVariant* result)
+{
+  uint32_t stallTimeSeconds = 0;
+  if ((argCount == 1) && NPVARIANT_IS_INT32(args[0])) {
+    stallTimeSeconds = (uint32_t) NPVARIANT_TO_INT32(args[0]);
+  }
+
+#ifdef XP_WIN
+  Sleep(stallTimeSeconds * 1000U);
+#else
+  sleep(stallTimeSeconds);
+#endif
+
+  return true;
+}
+
 #if defined(MOZ_WIDGET_GTK)
 bool
 getClipboardText(NPObject* npobj, const NPVariant* args, uint32_t argCount,
                  NPVariant* result)
 {
   NPP npp = static_cast<TestNPObject*>(npobj)->npp;
   InstanceData* id = static_cast<InstanceData*>(npp->pdata);
   string sel = pluginGetClipboardText(id);