diff options
Diffstat (limited to 'testing/mochitest/BrowserTestUtils/ContentEventListenerChild.sys.mjs')
-rw-r--r-- | testing/mochitest/BrowserTestUtils/ContentEventListenerChild.sys.mjs | 178 |
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; + } + } +} |