summaryrefslogtreecommitdiffstats
path: root/testing/mochitest/BrowserTestUtils/ContentEventListenerChild.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'testing/mochitest/BrowserTestUtils/ContentEventListenerChild.sys.mjs')
-rw-r--r--testing/mochitest/BrowserTestUtils/ContentEventListenerChild.sys.mjs178
1 files changed, 178 insertions, 0 deletions
diff --git a/testing/mochitest/BrowserTestUtils/ContentEventListenerChild.sys.mjs b/testing/mochitest/BrowserTestUtils/ContentEventListenerChild.sys.mjs
new file mode 100644
index 0000000000..937a9d35cc
--- /dev/null
+++ b/testing/mochitest/BrowserTestUtils/ContentEventListenerChild.sys.mjs
@@ -0,0 +1,178 @@
+/* vim: set ts=2 sw=2 sts=2 et tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+export class ContentEventListenerChild extends JSWindowActorChild {
+ actorCreated() {
+ this._contentEvents = new Map();
+ this._shutdown = false;
+ this._chromeEventHandler = null;
+ Services.cpmm.sharedData.addEventListener("change", this);
+ }
+
+ didDestroy() {
+ this._shutdown = true;
+ Services.cpmm.sharedData.removeEventListener("change", this);
+ this._updateContentEventListeners(/* clearListeners = */ true);
+ if (this._contentEvents.size != 0) {
+ throw new Error(`Didn't expect content events after willDestroy`);
+ }
+ }
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "DOMWindowCreated": {
+ this._updateContentEventListeners();
+ break;
+ }
+
+ case "change": {
+ if (
+ !event.changedKeys.includes("BrowserTestUtils:ContentEventListener")
+ ) {
+ return;
+ }
+ this._updateContentEventListeners();
+ break;
+ }
+ }
+ }
+
+ /**
+ * This method first determines the desired set of content event listeners
+ * for the window. This is either the empty set, if clearListeners is true,
+ * or is retrieved from the message manager's shared data. It then compares
+ * this event listener data to the existing set of listeners that we have
+ * registered, as recorded in this._contentEvents. Each content event listener
+ * has been assigned a unique id by the parent process. If a listener was
+ * added, but is not in the new event data, it is removed. If a listener was
+ * not present, but is in the new event data, it is added. If it is in both,
+ * then a basic check is done to see if they are the same.
+ *
+ * @param {bool} clearListeners [optional]
+ * If this is true, then instead of checking shared data to decide
+ * what the desired set of listeners is, just use the empty set. This
+ * will result in any existing listeners being cleared, and is used
+ * when the window is going away.
+ */
+ _updateContentEventListeners(clearListeners = false) {
+ // If we've already begun the destruction process, any new event
+ // listeners for our bc id can't possibly really be for us, so ignore them.
+ if (this._shutdown && !clearListeners) {
+ throw new Error(
+ "Tried to update after we shut down content event listening"
+ );
+ }
+
+ let newEventData;
+ if (!clearListeners) {
+ newEventData = Services.cpmm.sharedData.get(
+ "BrowserTestUtils:ContentEventListener"
+ );
+ }
+ if (!newEventData) {
+ newEventData = new Map();
+ }
+
+ // Check that entries that continue to exist are the same and remove entries
+ // that no longer exist.
+ for (let [
+ listenerId,
+ { eventName, listener, listenerOptions },
+ ] of this._contentEvents.entries()) {
+ let newData = newEventData.get(listenerId);
+ if (newData) {
+ if (newData.eventName !== eventName) {
+ // Could potentially check if listenerOptions are the same, but
+ // checkFnSource can't be checked unless we store it, and this is
+ // just a smoke test anyways, so don't bother.
+ throw new Error(
+ "Got new content event listener that disagreed with existing data"
+ );
+ }
+ continue;
+ }
+ if (!this._chromeEventHandler) {
+ throw new Error(
+ "Trying to remove an event listener for waitForContentEvent without a cached event handler"
+ );
+ }
+ this._chromeEventHandler.removeEventListener(
+ eventName,
+ listener,
+ listenerOptions
+ );
+ this._contentEvents.delete(listenerId);
+ }
+
+ let actorChild = this;
+
+ // Add in new entries.
+ for (let [
+ listenerId,
+ { eventName, listenerOptions, checkFnSource },
+ ] of newEventData.entries()) {
+ let oldData = this._contentEvents.get(listenerId);
+ if (oldData) {
+ // We checked that the data is the same in the previous loop.
+ continue;
+ }
+
+ /* eslint-disable no-eval */
+ let checkFn;
+ if (checkFnSource) {
+ checkFn = eval(`(() => (${unescape(checkFnSource)}))()`);
+ }
+ /* eslint-enable no-eval */
+
+ function listener(event) {
+ if (checkFn && !checkFn(event)) {
+ return;
+ }
+ actorChild.sendAsyncMessage("ContentEventListener:Run", {
+ listenerId,
+ });
+ }
+
+ // Cache the chrome event handler because this.docShell won't be
+ // available during shut down.
+ if (!this._chromeEventHandler) {
+ try {
+ this._chromeEventHandler = this.docShell.chromeEventHandler;
+ } catch (error) {
+ if (error.name === "InvalidStateError") {
+ // We'll arrive here if we no longer have our manager, so we can
+ // just swallow this error.
+ continue;
+ }
+ throw error;
+ }
+ }
+
+ // Some windows, like top-level browser windows, maybe not have a chrome
+ // event handler set up as this point, but we don't actually care about
+ // events on those windows, so ignore them.
+ if (!this._chromeEventHandler) {
+ continue;
+ }
+
+ this._chromeEventHandler.addEventListener(
+ eventName,
+ listener,
+ listenerOptions
+ );
+ this._contentEvents.set(listenerId, {
+ eventName,
+ listener,
+ listenerOptions,
+ });
+ }
+
+ // If there are no active content events, clear our reference to the chrome
+ // event handler to prevent leaks.
+ if (this._contentEvents.size == 0) {
+ this._chromeEventHandler = null;
+ }
+ }
+}