Bug 1248846: [webext] Test that event callbacks and promises do not fire later than expected. r?aswan
MozReview-Commit-ID: 4fpHc22txy
--- 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