summaryrefslogtreecommitdiffstats
path: root/testing/mochitest/BrowserTestUtils/ContentEventListenerChild.sys.mjs
blob: 937a9d35ccbaa9d69eb09a9526bbc7481117f986 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
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;
    }
  }
}