--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -5040,16 +5040,18 @@ pref("extensions.webextensions.remote",
// Whether or not the moz-extension resource loads are remoted. For debugging
// purposes only. Setting this to false will break moz-extension URI loading
// unless other process sandboxing and extension remoting prefs are changed.
pref("extensions.webextensions.protocol.remote", true);
// Disable tab hiding API by default.
pref("extensions.webextensions.tabhide.enabled", false);
+pref("extensions.webextensions.background-delayed-startup", false);
+
// Report Site Issue button
pref("extensions.webcompat-reporter.newIssueEndpoint", "https://webcompat.com/issues/new");
#if defined(MOZ_DEV_EDITION) || defined(NIGHTLY_BUILD)
pref("extensions.webcompat-reporter.enabled", true);
#else
pref("extensions.webcompat-reporter.enabled", false);
#endif
--- a/toolkit/components/extensions/ExtensionCommon.jsm
+++ b/toolkit/components/extensions/ExtensionCommon.jsm
@@ -16,16 +16,17 @@
var EXPORTED_SYMBOLS = ["ExtensionCommon"];
Cu.importGlobalProperties(["fetch"]);
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
+ AppConstants: "resource://gre/modules/AppConstants.jsm",
ConsoleAPI: "resource://gre/modules/Console.jsm",
MessageChannel: "resource://gre/modules/MessageChannel.jsm",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
Schemas: "resource://gre/modules/Schemas.jsm",
SchemaRoot: "resource://gre/modules/Schemas.jsm",
});
XPCOMUtils.defineLazyServiceGetter(this, "styleSheetService",
@@ -1768,32 +1769,196 @@ class EventManager {
* An object representing the extension instance using this event.
* @param {string} params.name
* A name used only for debugging.
* @param {functon} params.register
* A function called whenever a new listener is added.
* @param {boolean} [params.inputHandling=false]
* If true, the "handling user input" flag is set while handlers
* for this event are executing.
+ * @param {object} [params.persistent]
+ * Details for persistent event listeners
+ * @param {string} params.persistent.module
+ * The name of the module in which this event is defined.
+ * @param {string} params.persistent.event
+ * The name of this event.
*/
constructor(params) {
// Maintain compatibility with the old EventManager API in which
// the constructor took parameters (contest, name, register).
// Remove this in bug 1451212.
if (arguments.length > 1) {
[this.context, this.name, this.register] = arguments;
this.inputHandling = false;
+ this.persistent = null;
} else {
- let {context, name, register, inputHandling = false} = params;
+ let {context, name, register, inputHandling = false, persistent = null} = params;
this.context = context;
this.name = name;
this.register = register;
this.inputHandling = inputHandling;
+ this.persistent = persistent;
}
+
this.unregister = new Map();
+ this.remove = new Map();
+
+ if (this.persistent) {
+ if (this.context.viewType !== "background") {
+ this.persistent = null;
+ }
+ if (AppConstants.DEBUG) {
+ if (this.context.envType !== "addon_parent") {
+ throw new Error("Persistent event managers can only be created for addon_parent");
+ }
+ if (!this.persistent.module || !this.persistent.event) {
+ throw new Error("Persistent event manager must specify module and event");
+ }
+ }
+ }
+ }
+
+ /*
+ * Information about listeners to persistent events is associated with
+ * the extension to which they belong. Any extension thas has such
+ * listeners has a property called `persistentListeners` that is a
+ * 3-level Map. The first 2 keys are the module name (e.g., webRequest)
+ * and the name of the event within the module (e.g., onBeforeRequest).
+ * The third level of the map is used to track multiple listeners for
+ * the same event, these listeners are distinguished by the extra arguments
+ * passed to addListener(). For quick lookups, the key to the third Map
+ * is the result of calling uneval() on the array of extra arguments.
+ *
+ * The value stored in the Map is a plain object with a property called
+ * `params` that is the original (ie, not uneval()ed) extra arguments to
+ * addListener(). For a primed listener (i.e., the stub listener created
+ * during browser startup before the extension background page is started,
+ * the object also has a `primed` property that holds the things needed
+ * to handle events during startup and eventually connect the listener
+ * with a callback registered from the extension.
+ */
+ static _initPersistentListeners(extension) {
+ if (extension.persistentListeners) {
+ return;
+ }
+
+ let listeners = new DefaultMap(() => new DefaultMap(() => new Map()));
+ extension.persistentListeners = listeners;
+
+ let {persistentListeners} = extension.startupData;
+ if (!persistentListeners) {
+ return;
+ }
+
+ for (let [module, entry] of Object.entries(persistentListeners)) {
+ for (let [event, paramlists] of Object.entries(entry)) {
+ for (let paramlist of paramlists) {
+ let key = uneval(paramlist);
+ listeners.get(module).get(event).set(key, {params: paramlist});
+ }
+ }
+ }
+ }
+
+ // Extract just the information needed at startup for all persistent
+ // listeners, and arrange for it to be saved. This should be called
+ // whenever the set of persistent listeners for an extension changes.
+ static _writePersistentListeners(extension) {
+ let startupListeners = {};
+ for (let [module, moduleEntry] of extension.persistentListeners) {
+ startupListeners[module] = {};
+ for (let [event, eventEntry] of moduleEntry) {
+ startupListeners[module][event] = Array.from(eventEntry.values(),
+ listener => listener.params);
+ }
+ }
+
+ extension.startupData.persistentListeners = startupListeners;
+ extension.saveStartupData();
+ }
+
+ // Set up "primed" event listeners for any saved event listeners
+ // in an extension's startup data.
+ // This function is only called during browser startup, it stores details
+ // about all primed listeners in the extension's persistentListeners Map.
+ static primeListeners(extension) {
+ EventManager._initPersistentListeners(extension);
+
+ for (let [module, moduleEntry] of extension.persistentListeners) {
+ let api = extension.apiManager.getAPI(module, extension, "addon_parent");
+ for (let [event, eventEntry] of moduleEntry) {
+ for (let listener of eventEntry.values()) {
+ let primed = {pendingEvents: []};
+ listener.primed = primed;
+
+ let wakeup = (...args) => new Promise((resolve, reject) => {
+ primed.pendingEvents.push({args, resolve, reject});
+ extension.emit("background-page-event");
+ });
+
+ let fire = {
+ sync: wakeup,
+ async: wakeup,
+ };
+
+ let {unregister, convert} = api.primeListener(extension, event, fire, listener.params);
+ Object.assign(primed, {unregister, convert});
+ }
+ }
+ }
+ }
+
+ // Remove any primed listeners that were not re-registered.
+ // This function is called after the background page has started.
+ static clearPrimedListeners(extension) {
+ for (let [module, moduleEntry] of extension.persistentListeners) {
+ for (let [event, listeners] of moduleEntry) {
+ for (let [key, listener] of listeners) {
+ let {primed} = listener;
+ if (!primed) {
+ continue;
+ }
+
+ for (let evt of primed.pendingEvents) {
+ evt.reject(new Error("listener not re-registered"));
+ }
+
+ EventManager.clearPersistentListener(extension, module, event, key);
+ primed.unregister();
+ }
+ }
+ }
+ }
+
+ // Record the fact that there is a listener for the given event in
+ // the given extension. `args` is an Array containing any extra
+ // arguments that were passed to addListener().
+ static savePersistentListener(extension, module, event, args) {
+ EventManager._initPersistentListeners(extension);
+ let key = uneval(args);
+ extension.persistentListeners.get(module).get(event).set(key, {params: args});
+ EventManager._writePersistentListeners(extension);
+ }
+
+ // Remove the record for the given event listener from the extension's
+ // startup data. `key` must be a string, the result of calling uneval()
+ // on the array of extra arguments originally passed to addListener().
+ static clearPersistentListener(extension, module, event, key) {
+ let listeners = extension.persistentListeners.get(module).get(event);
+ listeners.delete(key);
+
+ if (listeners.size == 0) {
+ let moduleEntry = extension.persistentListeners.get(module);
+ moduleEntry.delete(event);
+ if (moduleEntry.size == 0) {
+ extension.persistentListeners.delete(module);
+ }
+ }
+
+ EventManager._writePersistentListeners(extension);
}
addListener(callback, ...args) {
if (this.unregister.has(callback)) {
return;
}
let shouldFire = () => {
@@ -1830,45 +1995,99 @@ class EventManager {
return Promise.resolve().then(() => {
if (shouldFire()) {
return this.context.applySafeWithoutClone(callback, args);
}
});
},
};
- let unregister = this.register(fire, ...args);
+ let {extension} = this.context;
+
+ let unregister = null;
+ let recordStartupData = false;
+
+ // If this is a persistent event, check for a listener that was already
+ // created during startup. If there is one, use it and don't create a
+ // new one.
+ if (this.persistent) {
+ recordStartupData = true;
+ let {module, event} = this.persistent;
+
+ let key = uneval(args);
+ EventManager._initPersistentListeners(extension);
+ let listener = extension.persistentListeners
+ .get(module).get(event).get(key);
+ if (listener) {
+ let {primed} = listener;
+ listener.primed = null;
+
+ primed.convert(fire);
+ unregister = primed.unregister;
+
+ for (let evt of primed.pendingEvents) {
+ evt.resolve(fire.async(...evt.args));
+ }
+
+ recordStartupData = false;
+ this.remove.set(callback, () => {
+ EventManager.clearPersistentListener(extension, module, event, uneval(args));
+ });
+ }
+ }
+
+ if (!unregister) {
+ unregister = this.register(fire, ...args);
+ }
+
this.unregister.set(callback, unregister);
this.context.callOnClose(this);
+
+ // If this is a new listener for a persistent event, record
+ // the details for subsequent startups.
+ if (recordStartupData) {
+ let {module, event} = this.persistent;
+ EventManager.savePersistentListener(extension, module, event, args);
+ this.remove.set(callback, () => {
+ EventManager.clearPersistentListener(extension, module, event, uneval(args));
+ });
+ }
}
- removeListener(callback) {
+ removeListener(callback, clearPersistentListener = true) {
if (!this.unregister.has(callback)) {
return;
}
let unregister = this.unregister.get(callback);
this.unregister.delete(callback);
try {
unregister();
} catch (e) {
Cu.reportError(e);
}
+
+ if (clearPersistentListener && this.remove.has(callback)) {
+ let cleanup = this.remove.get(callback);
+ this.remove.delete(callback);
+ cleanup();
+ }
+
if (this.unregister.size == 0) {
this.context.forgetOnClose(this);
}
}
hasListener(callback) {
return this.unregister.has(callback);
}
revoke() {
for (let callback of this.unregister.keys()) {
- this.removeListener(callback);
+ this.removeListener(callback, false);
}
}
close() {
this.revoke();
}
api() {
--- a/toolkit/components/extensions/parent/.eslintrc.js
+++ b/toolkit/components/extensions/parent/.eslintrc.js
@@ -9,16 +9,18 @@ module.exports = {
"InputEventManager": true,
"PRIVATE_STORE": true,
"TabBase": true,
"TabManagerBase": true,
"TabTrackerBase": true,
"WindowBase": true,
"WindowManagerBase": true,
"WindowTrackerBase": true,
+ "browserPaintedPromise": true,
+ "browserStartupPromise": true,
"getContainerForCookieStoreId": true,
"getCookieStoreIdForContainer": true,
"getCookieStoreIdForTab": true,
"isContainerCookieStoreId": true,
"isDefaultCookieStoreId": true,
"isPrivateCookieStoreId": true,
"isValidCookieStoreId": true,
},
--- a/toolkit/components/extensions/parent/ext-backgroundPage.js
+++ b/toolkit/components/extensions/parent/ext-backgroundPage.js
@@ -4,16 +4,19 @@ ChromeUtils.defineModuleGetter(this, "Te
"resource://gre/modules/TelemetryStopwatch.jsm");
ChromeUtils.import("resource://gre/modules/ExtensionParent.jsm");
var {
HiddenExtensionPage,
promiseExtensionViewLoaded,
} = ExtensionParent;
+XPCOMUtils.defineLazyPreferenceGetter(this, "DELAYED_STARTUP",
+ "extensions.webextensions.background-delayed-startup");
+
// Responsible for the background_page section of the manifest.
class BackgroundPage extends HiddenExtensionPage {
constructor(extension, options) {
super(extension, "background");
this.page = options.page || null;
this.isGenerated = !!options.scripts;
@@ -49,19 +52,45 @@ class BackgroundPage extends HiddenExten
shutdown() {
this.extension._backgroundPageFrameLoader = null;
super.shutdown();
}
}
this.backgroundPage = class extends ExtensionAPI {
onManifestEntry(entryName) {
- let {manifest} = this.extension;
+ let {extension} = this;
+ let {manifest} = extension;
+
+ this.bgPage = new BackgroundPage(extension, manifest.background);
+ if (extension.startupReason !== "APP_STARTUP" || !DELAYED_STARTUP) {
+ return this.bgPage.build();
+ }
+
+ EventManager.primeListeners(extension);
+
+ extension.once("start-background-page", async () => {
+ await this.bgPage.build();
+ EventManager.clearPrimedListeners(extension);
+ });
- this.bgPage = new BackgroundPage(this.extension, manifest.background);
+ // There are two ways to start the background page:
+ // 1. If a primed event fires, then start the background page as
+ // soon as we have painted a browser window. Note that we have
+ // to touch browserPaintedPromise here to initialize the listener
+ // or else we can miss it if the event occurs after the first
+ // window is painted but before #2
+ // 2. After all windows have been restored.
+ void browserPaintedPromise;
+ extension.once("background-page-event", async () => {
+ await browserPaintedPromise;
+ extension.emit("start-background-page");
+ });
- return this.bgPage.build();
+ browserStartupPromise.then(() => {
+ extension.emit("start-background-page");
+ });
}
onShutdown() {
this.bgPage.shutdown();
}
};
--- a/toolkit/components/extensions/parent/ext-toolkit.js
+++ b/toolkit/components/extensions/parent/ext-toolkit.js
@@ -69,8 +69,29 @@ global.getContainerForCookieStoreId = fu
return null;
};
global.isValidCookieStoreId = function(storeId) {
return isDefaultCookieStoreId(storeId) ||
isPrivateCookieStoreId(storeId) ||
isContainerCookieStoreId(storeId);
};
+
+function makeEventPromise(name, event) {
+ Object.defineProperty(global, name, {
+ get() {
+ let promise = ExtensionUtils.promiseObserved(event);
+ Object.defineProperty(global, name, {value: promise});
+ return promise;
+ },
+ configurable: true,
+ enumerable: true,
+ });
+}
+
+// browserPaintedPromise and browserStartupPromise are promises that
+// resolve after the first browser window is painted and after browser
+// windows have been restored, respectively.
+// These promises must be referenced during startup to be valid -- if the
+// first reference happens after the corresponding event has occurred,
+// the Promise will never resolve.
+makeEventPromise("browserPaintedPromise", "browser-delayed-startup-finished");
+makeEventPromise("browserStartupPromise", "sessionstore-windows-restored");
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_persistent_events.js
@@ -0,0 +1,355 @@
+"use strict";
+
+PromiseTestUtils.whitelistRejectionsGlobally(/Message manager disconnected/);
+
+Cu.importGlobalProperties(["Blob", "URL"]);
+
+ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
+const {ExtensionAPI} = ExtensionCommon;
+
+const SCHEMA = [
+ {
+ namespace: "eventtest",
+ events: [
+ {
+ name: "onEvent1",
+ type: "function",
+ extraParameters: [{type: "any"}],
+ },
+ {
+ name: "onEvent2",
+ type: "function",
+ extraParameters: [{type: "any"}],
+ },
+ ],
+ },
+];
+
+// The code in this class does not actually run in this test scope, it is
+// serialized into a string which is later loaded by the WebExtensions
+// framework in the same context as other extension APIs. By writing it
+// this way rather than as a big string constant we get lint coverage.
+// But eslint doesn't understand that this code runs in a different context
+// where the EventManager class is available so just tell it here:
+/* global EventManager */
+const API = class extends ExtensionAPI {
+ primeListener(extension, event, fire, params) {
+ let data = {wrappedJSObject: {event, params}};
+ Services.obs.notifyObservers(data, "prime-event-listener");
+
+ const FIRE_TOPIC = `fire-${event}`;
+
+ async function listener(subject, topic, _data) {
+ try {
+ await fire.async(subject.wrappedJSObject);
+ } catch (err) {
+ Services.obs.notifyObservers(data, "listener-callback-exception");
+ }
+ }
+ Services.obs.addObserver(listener, FIRE_TOPIC);
+
+ return {
+ unregister() {
+ Services.obs.notifyObservers(data, "unregister-primed-listener");
+ Services.obs.removeObserver(listener, FIRE_TOPIC);
+ },
+ convert(_fire) {
+ Services.obs.notifyObservers(data, "convert-event-listener");
+ fire = _fire;
+ },
+ };
+ }
+
+ getAPI(context) {
+ return {
+ eventtest: {
+ onEvent1: new EventManager({
+ context,
+ name: "test.event1",
+ persistent: {
+ module: "eventtest",
+ event: "onEvent1",
+ },
+ register: (fire, ...params) => {
+ let data = {wrappedJSObject: {event: "onEvent1", params}};
+ Services.obs.notifyObservers(data, "register-event-listener");
+ return () => {
+ Services.obs.notifyObservers(data, "unregister-event-listener");
+ };
+ },
+ }).api(),
+
+ onEvent2: new EventManager({
+ context,
+ name: "test.event1",
+ persistent: {
+ module: "eventtest",
+ event: "onEvent2",
+ },
+ register: (fire, ...params) => {
+ let data = {wrappedJSObject: {event: "onEvent2", params}};
+ Services.obs.notifyObservers(data, "register-event-listener");
+ return () => {
+ Services.obs.notifyObservers(data, "unregister-event-listener");
+ };
+ },
+ }).api(),
+ },
+ };
+ }
+};
+
+const API_SCRIPT = `this.eventtest = ${API.toString()}`;
+
+const MODULE_INFO = {
+ eventtest: {
+ schema: `data:,${JSON.stringify(SCHEMA)}`,
+ scopes: ["addon_parent"],
+ paths: [["eventtest"]],
+ url: URL.createObjectURL(new Blob([API_SCRIPT])),
+ },
+};
+
+const global = this;
+
+// Wait for the given event (topic) to occur a specific number of times
+// (count). If fn is not supplied, the Promise returned from this function
+// resolves as soon as that many instances of the event have been observed.
+// If fn is supplied, this function also waits for the Promise that fn()
+// returns to complete and ensures that the given event does not occur more
+// than `count` times before then. On success, resolves with an array
+// of the subjects from each of the observed events.
+async function promiseObservable(topic, count, fn = null) {
+ let _countResolve;
+ let results = [];
+ function listener(subject, _topic, data) {
+ results.push(subject.wrappedJSObject);
+ if (results.length > count) {
+ ok(false, `Got unexpected ${topic} event`);
+ } else if (results.length == count) {
+ _countResolve();
+ }
+ }
+ Services.obs.addObserver(listener, topic);
+
+ try {
+ await Promise.all([
+ new Promise(resolve => { _countResolve = resolve; }),
+ fn && fn(),
+ ]);
+ } finally {
+ Services.obs.removeObserver(listener, topic);
+ }
+
+ return results;
+}
+
+add_task(async function() {
+ Services.prefs.setBoolPref("extensions.webextensions.background-delayed-startup", true);
+
+ AddonTestUtils.init(global);
+ AddonTestUtils.overrideCertDB();
+ AddonTestUtils.createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "43");
+
+ await AddonTestUtils.promiseStartupManager();
+
+ ExtensionParent.apiManager.registerModules(MODULE_INFO);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ background() {
+ let register1 = true, register2 = true;
+ if (localStorage.getItem("skip1")) {
+ register1 = false;
+ }
+ if (localStorage.getItem("skip2")) {
+ register2 = false;
+ }
+
+ let listener1 = arg => browser.test.sendMessage("listener1", arg);
+ let listener2 = arg => browser.test.sendMessage("listener2", arg);
+ let listener3 = arg => browser.test.sendMessage("listener3", arg);
+
+ if (register1) {
+ browser.eventtest.onEvent1.addListener(listener1, "listener1");
+ }
+ if (register2) {
+ browser.eventtest.onEvent1.addListener(listener2, "listener2");
+ browser.eventtest.onEvent2.addListener(listener3, "listener3");
+ }
+
+ browser.test.onMessage.addListener(msg => {
+ if (msg == "unregister2") {
+ browser.eventtest.onEvent2.removeListener(listener3);
+ localStorage.setItem("skip2", true);
+ } else if (msg == "unregister1") {
+ localStorage.setItem("skip1", true);
+ browser.test.sendMessage("unregistered");
+ }
+ });
+
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ function check(info, what, {listener1 = true, listener2 = true, listener3 = true} = {}) {
+ let count = (listener1 ? 1 : 0) + (listener2 ? 1 : 0) + (listener3 ? 1 : 0);
+ equal(info.length, count, `Got ${count} ${what} events`);
+
+ let i = 0;
+ if (listener1) {
+ equal(info[i].event, "onEvent1", `Got ${what} on event1 for listener 1`);
+ deepEqual(info[i].params, ["listener1"], `Got event1 ${what} args for listener 1`);
+ ++i;
+ }
+
+ if (listener2) {
+ equal(info[i].event, "onEvent1", `Got ${what} on event1 for listener 2`);
+ deepEqual(info[i].params, ["listener2"], `Got event1 ${what} args for listener 2`);
+ ++i;
+ }
+
+ if (listener3) {
+ equal(info[i].event, "onEvent2", `Got ${what} on event2 for listener 3`);
+ deepEqual(info[i].params, ["listener3"], `Got event2 ${what} args for listener 3`);
+ ++i;
+ }
+ }
+
+ // Check that the regular event registration process occurs when
+ // the extension is installed.
+ let [info] = await Promise.all([
+ promiseObservable("register-event-listener", 3),
+ extension.startup(),
+ ]);
+ check(info, "register");
+
+ await extension.awaitMessage("ready");
+
+ // Check that the regular unregister process occurs when
+ // the browser shuts down.
+ [info] = await Promise.all([
+ promiseObservable("unregister-event-listener", 3),
+ new Promise(resolve => extension.extension.once("shutdown", resolve)),
+ AddonTestUtils.promiseShutdownManager(),
+ ]);
+ check(info, "unregister");
+
+ // Check that listeners are primed at the next browser startup.
+ [info] = await Promise.all([
+ promiseObservable("prime-event-listener", 3),
+ AddonTestUtils.promiseStartupManager(false),
+ ]);
+ check(info, "prime");
+
+ // Check that primed listeners are converted to regular listeners
+ // when the background page is started after browser startup.
+ let p = promiseObservable("convert-event-listener", 3);
+ Services.obs.notifyObservers(null, "sessionstore-windows-restored");
+ info = await p;
+
+ check(info, "convert");
+
+ await extension.awaitMessage("ready");
+
+ // Check that when the event is triggered, all the plumbing worked
+ // correctly for the primed-then-converted listener.
+ let eventDetails = {test: "kaboom"};
+ let eventSubject = {wrappedJSObject: eventDetails};
+ Services.obs.notifyObservers(eventSubject, "fire-onEvent1");
+
+ let details = await extension.awaitMessage("listener1");
+ deepEqual(details, eventDetails, "Listener 1 fired");
+ details = await extension.awaitMessage("listener2");
+ deepEqual(details, eventDetails, "Listener 2 fired");
+
+ // Check that the converted listener is properly unregistered at
+ // browser shutdown.
+ [info] = await Promise.all([
+ promiseObservable("unregister-primed-listener", 3),
+ AddonTestUtils.promiseShutdownManager(),
+ ]);
+ check(info, "unregister");
+
+ // Start up again, listener should be primed
+ [info] = await Promise.all([
+ promiseObservable("prime-event-listener", 3),
+ AddonTestUtils.promiseStartupManager(false),
+ ]);
+ check(info, "prime");
+
+ // Check that triggering the event before the listener has been converted
+ // causes the background page to be loaded and the listener to be converted,
+ // and the listener is invoked.
+ p = promiseObservable("convert-event-listener", 3);
+ eventDetails.test = "startup event";
+ Services.obs.notifyObservers(eventSubject, "fire-onEvent2");
+ info = await p;
+
+ check(info, "convert");
+
+ details = await extension.awaitMessage("listener3");
+ deepEqual(details, eventDetails, "Listener 3 fired for event during startup");
+
+ await extension.awaitMessage("ready");
+
+ // Check that the unregister process works when we manually remove
+ // a listener.
+ p = promiseObservable("unregister-primed-listener", 1);
+ extension.sendMessage("unregister2");
+ info = await p;
+ check(info, "unregister", {listener1: false, listener2: false});
+
+ // Check that we only get unregisters for the remaining events after
+ // one listener has been removed.
+ info = await promiseObservable("unregister-primed-listener", 2,
+ () => AddonTestUtils.promiseShutdownManager());
+ check(info, "unregister", {listener3: false});
+
+ // Check that after restart, only listeners that were present at
+ // the end of the last session are primed.
+ info = await promiseObservable("prime-event-listener", 2,
+ () => AddonTestUtils.promiseStartupManager(false));
+ check(info, "prime", {listener3: false});
+
+ // Check that if the background script does not re-register listeners,
+ // the primed listeners are unregistered after the background page
+ // starts up.
+ p = promiseObservable("unregister-primed-listener", 1,
+ () => extension.awaitMessage("ready"));
+ Services.obs.notifyObservers(null, "sessionstore-windows-restored");
+ info = await p;
+ check(info, "unregister", {listener1: false, listener3: false});
+
+ // Just listener1 should be registered now, fire event1 to confirm.
+ eventDetails.test = "third time";
+ Services.obs.notifyObservers(eventSubject, "fire-onEvent1");
+ details = await extension.awaitMessage("listener1");
+ deepEqual(details, eventDetails, "Listener 1 fired");
+
+ // Tell the extension not to re-register listener1 on the next startup
+ extension.sendMessage("unregister1");
+ await extension.awaitMessage("unregistered");
+
+ // Shut down, start up
+ info = await promiseObservable("unregister-primed-listener", 1,
+ () => AddonTestUtils.promiseShutdownManager());
+ check(info, "unregister", {listener2: false, listener3: false});
+
+ info = await promiseObservable("prime-event-listener", 1,
+ () => AddonTestUtils.promiseStartupManager(false));
+ check(info, "register", {listener2: false, listener3: false});
+
+ // Check that firing event1 causes the listener fire callback to
+ // reject.
+ p = promiseObservable("listener-callback-exception", 1);
+ Services.obs.notifyObservers(eventSubject, "fire-onEvent1");
+ await p;
+ ok(true, "Primed listener that was not re-registered received an error when event was triggered during startup");
+
+ await extension.awaitMessage("ready");
+
+ await extension.unload();
+
+ await AddonTestUtils.promiseShutdownManager();
+});
--- a/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
@@ -56,16 +56,17 @@ skip-if = os == "android" # checking for
[test_ext_legacy_extension_embedding.js]
[test_ext_localStorage.js]
[test_ext_management.js]
skip-if = (os == "win" && !debug) #Bug 1419183 disable on Windows
[test_ext_management_uninstall_self.js]
[test_ext_onmessage_removelistener.js]
skip-if = true # This test no longer tests what it is meant to test.
[test_ext_permission_xhr.js]
+[test_ext_persistent_events.js]
[test_ext_privacy.js]
[test_ext_privacy_disable.js]
[test_ext_privacy_update.js]
[test_ext_proxy_auth.js]
[test_ext_proxy_onauthrequired.js]
[test_ext_proxy_socks.js]
[test_ext_proxy_speculative.js]
[test_ext_redirects.js]