Bug 1248846: [webext] Test that event callbacks and promises do not fire later than expected. r?aswan draft
authorKris Maglione <maglione.k@gmail.com>
Tue, 05 Apr 2016 08:59:47 -0700
changeset 352800 58e7d5d26bee68002d53ce4f617a2f6d404e0768
parent 352242 2a95760bd9cca8d1912a14158380ce1f0d84766d
child 518754 0209bf6c839dcae0ddf0bac3b82641faa8386876
push id15807
push usermaglione.k@gmail.com
push dateMon, 18 Apr 2016 21:28:21 +0000
reviewersaswan
bugs1248846
milestone48.0a1
Bug 1248846: [webext] Test that event callbacks and promises do not fire later than expected. r?aswan MozReview-Commit-ID: 4fpHc22txy
toolkit/components/extensions/ExtensionUtils.jsm
toolkit/components/extensions/test/xpcshell/test_ext_contexts.js
toolkit/components/extensions/test/xpcshell/xpcshell.ini
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -611,17 +611,17 @@ EventManager.prototype = {
   hasListener(callback) {
     return this.callbacks.has(callback);
   },
 
   fire(...args) {
     for (let callback of this.callbacks) {
       Promise.resolve(callback).then(callback => {
         if (this.context.unloaded) {
-          dump(`${this.name} event fired after context unloaded.`);
+          dump(`${this.name} event fired after context unloaded.\n`);
         } else if (this.callbacks.has(callback)) {
           this.context.runSafe(callback, ...args);
         }
       });
     }
   },
 
   fireWithoutClone(...args) {
@@ -629,17 +629,17 @@ EventManager.prototype = {
       this.context.runSafeWithoutClone(callback, ...args);
     }
   },
 
   close() {
     if (this.callbacks.size) {
       this.unregister();
     }
-    this.callbacks = null;
+    this.callbacks = Object.freeze([]);
   },
 
   api() {
     return {
       addListener: callback => this.addListener(callback),
       removeListener: callback => this.removeListener(callback),
       hasListener: callback => this.hasListener(callback),
     };
@@ -656,17 +656,17 @@ function SingletonEventManager(context, 
   this.unregister = new Map();
   context.callOnClose(this);
 }
 
 SingletonEventManager.prototype = {
   addListener(callback, ...args) {
     let wrappedCallback = (...args) => {
       if (this.context.unloaded) {
-        dump(`${this.name} event fired after context unloaded.`);
+        dump(`${this.name} event fired after context unloaded.\n`);
       } else if (this.unregister.has(callback)) {
         return callback(...args);
       }
     };
 
     let unregister = this.register(wrappedCallback, ...args);
     this.unregister.set(callback, unregister);
   },
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_contexts.js
@@ -0,0 +1,129 @@
+"use strict";
+
+const global = this;
+
+Cu.import("resource://gre/modules/Timer.jsm");
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+var {
+  BaseContext,
+  EventManager,
+  SingletonEventManager,
+} = ExtensionUtils;
+
+class StubContext extends BaseContext {
+  constructor() {
+    super();
+    this.sandbox = new Cu.Sandbox(global);
+  }
+
+  get cloneScope() {
+    return this. sandbox;
+  }
+
+  get extension() {
+    return {id: "test@web.extension"};
+  }
+}
+
+
+add_task(function* test_post_unload_promises() {
+  let context = new StubContext();
+
+  let fail = result => {
+    ok(false, `Unexpected callback: ${result}`);
+  };
+
+  // Make sure promises resolve normally prior to unload.
+  let promises = [
+    context.wrapPromise(Promise.resolve()),
+    context.wrapPromise(Promise.reject({message: ""})).catch(() => {}),
+  ];
+
+  yield Promise.all(promises);
+
+  // Make sure promises that resolve after unload do not trigger
+  // resolution handlers.
+
+  context.wrapPromise(Promise.resolve("resolved"))
+         .then(fail);
+
+  context.wrapPromise(Promise.reject({message: "rejected"}))
+         .then(fail, fail);
+
+  context.unload();
+
+  // The `setTimeout` ensures that we return to the event loop after
+  // promise resolution, which means we're guaranteed to return after
+  // any micro-tasks that get enqueued by the resolution handlers above.
+  yield new Promise(resolve => setTimeout(resolve, 0));
+});
+
+
+add_task(function* test_post_unload_listeners() {
+  let context = new StubContext();
+
+  let fireEvent;
+  let onEvent = new EventManager(context, "onEvent", fire => {
+    fireEvent = fire;
+    return () => {};
+  });
+
+  let fireSingleton;
+  let onSingleton = new SingletonEventManager(context, "onSingleton", callback => {
+    fireSingleton = () => {
+      Promise.resolve().then(callback);
+    };
+    return () => {};
+  });
+
+  let fail = event => {
+    ok(false, `Unexpected event: ${event}`);
+  };
+
+  // Check that event listeners aren't called after they've been removed.
+  onEvent.addListener(fail);
+  onSingleton.addListener(fail);
+
+  let promises = [
+    new Promise(resolve => onEvent.addListener(resolve)),
+    new Promise(resolve => onSingleton.addListener(resolve)),
+  ];
+
+  fireEvent("onEvent");
+  fireSingleton("onSingleton");
+
+  // Both `fireEvent` calls are dispatched asynchronously, so they won't
+  // have fired by this point. The `fail` listeners that we remove now
+  // should not be called, even though the events have already been
+  // enqueued.
+  onEvent.removeListener(fail);
+  onSingleton.removeListener(fail);
+
+  // Wait for the remaining listeners to be called, which should always
+  // happen after the `fail` listeners would normally be called.
+  yield Promise.all(promises);
+
+  // Check that event listeners aren't called after the context has
+  // unloaded.
+  onEvent.addListener(fail);
+  onSingleton.addListener(fail);
+
+  // The EventManager `fire` callback always dispatches events
+  // asynchronously, so we need to test that any pending event callbacks
+  // aren't fired after the context unloads. We also need to test that
+  // any `fire` calls that happen *after* the context is unloaded also
+  // do not trigger callbacks.
+  fireEvent("onEvent");
+  Promise.resolve("onEvent").then(fireEvent);
+
+  fireSingleton("onSingleton");
+  Promise.resolve("onSingleton").then(fireSingleton);
+
+  context.unload();
+
+  // The `setTimeout` ensures that we return to the event loop after
+  // promise resolution, which means we're guaranteed to return after
+  // any micro-tasks that get enqueued by the resolution handlers above.
+  yield new Promise(resolve => setTimeout(resolve, 0));
+});
--- a/toolkit/components/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
@@ -1,10 +1,11 @@
 [DEFAULT]
 head = head.js
 tail =
 firefox-appdir = browser
 skip-if = toolkit == 'gonk'
 
 [test_locale_data.js]
 [test_locale_converter.js]
+[test_ext_contexts.js]
 [test_ext_schemas.js]
 [test_getAPILevelForWindow.js]
\ No newline at end of file