Source code

Revision control

Copy as Markdown

Other Tools

/* 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;
}
}
}