summaryrefslogtreecommitdiffstats
path: root/testing/specialpowers/content/ContentTaskUtils.sys.mjs
blob: 0d026d1e3b39dd9e243e30851a79a9c0d563f52d (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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
/* 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/. */

/*
 * This module implements a number of utility functions that can be loaded
 * into content scope.
 *
 * All asynchronous helper methods should return promises, rather than being
 * callback based.
 */

// Disable ownerGlobal use since that's not available on content-privileged elements.

/* eslint-disable mozilla/use-ownerGlobal */

import { setTimeout } from "resource://gre/modules/Timer.sys.mjs";

export var ContentTaskUtils = {
  /**
   * Checks if a DOM element is hidden.
   *
   * @param {Element} element
   *        The element which is to be checked.
   *
   * @return {boolean}
   */
  isHidden(element) {
    let style = element.ownerDocument.defaultView.getComputedStyle(element);
    if (style.display == "none") {
      return true;
    }
    if (style.visibility != "visible") {
      return true;
    }

    // Hiding a parent element will hide all its children
    if (
      element.parentNode != element.ownerDocument &&
      element.parentNode.nodeType != Node.DOCUMENT_FRAGMENT_NODE
    ) {
      return ContentTaskUtils.isHidden(element.parentNode);
    }

    // Walk up the shadow DOM if we've reached the top of the shadow root
    if (element.parentNode.host) {
      return ContentTaskUtils.isHidden(element.parentNode.host);
    }

    return false;
  },

  /**
   * Checks if a DOM element is visible.
   *
   * @param {Element} element
   *        The element which is to be checked.
   *
   * @return {boolean}
   */
  isVisible(element) {
    return !this.isHidden(element);
  },

  /**
   * Will poll a condition function until it returns true.
   *
   * @param condition
   *        A condition function that must return true or false. If the
   *        condition ever throws, this is also treated as a false.
   * @param msg
   *        The message to use when the returned promise is rejected.
   *        This message will be extended with additional information
   *        about the number of tries or the thrown exception.
   * @param interval
   *        The time interval to poll the condition function. Defaults
   *        to 100ms.
   * @param maxTries
   *        The number of times to poll before giving up and rejecting
   *        if the condition has not yet returned true. Defaults to 50
   *        (~5 seconds for 100ms intervals)
   * @return Promise
   *        Resolves when condition is true.
   *        Rejects if timeout is exceeded or condition ever throws.
   */
  async waitForCondition(condition, msg, interval = 100, maxTries = 50) {
    let startTime = Cu.now();
    for (let tries = 0; tries < maxTries; ++tries) {
      await new Promise(resolve => setTimeout(resolve, interval));

      let conditionPassed = false;
      try {
        conditionPassed = await condition();
      } catch (e) {
        msg += ` - threw exception: ${e}`;
        ChromeUtils.addProfilerMarker(
          "ContentTaskUtils",
          { startTime, category: "Test" },
          `waitForCondition - ${msg}`
        );
        throw msg;
      }
      if (conditionPassed) {
        ChromeUtils.addProfilerMarker(
          "ContentTaskUtils",
          { startTime, category: "Test" },
          `waitForCondition succeeded after ${tries} retries - ${msg}`
        );
        return conditionPassed;
      }
    }

    msg += ` - timed out after ${maxTries} tries.`;
    ChromeUtils.addProfilerMarker(
      "ContentTaskUtils",
      { startTime, category: "Test" },
      `waitForCondition - ${msg}`
    );
    throw msg;
  },

  /**
   * Waits for an event to be fired on a specified element.
   *
   * Usage:
   *    let promiseEvent = ContentTasKUtils.waitForEvent(element, "eventName");
   *    // Do some processing here that will cause the event to be fired
   *    // ...
   *    // Now yield until the Promise is fulfilled
   *    let receivedEvent = yield promiseEvent;
   *
   * @param {Element} subject
   *        The element that should receive the event.
   * @param {string} eventName
   *        Name of the event to listen to.
   * @param {bool} capture [optional]
   *        True to use a capturing listener.
   * @param {function} checkFn [optional]
   *        Called with the Event object as argument, should return true if the
   *        event is the expected one, or false if it should be ignored and
   *        listening should continue. If not specified, the first event with
   *        the specified name resolves the returned promise.
   *
   * @note Because this function is intended for testing, any error in checkFn
   *       will cause the returned promise to be rejected instead of waiting for
   *       the next event, since this is probably a bug in the test.
   *
   * @returns {Promise}
   * @resolves The Event object.
   */
  waitForEvent(subject, eventName, capture, checkFn, wantsUntrusted = false) {
    return new Promise((resolve, reject) => {
      let startTime = Cu.now();
      subject.addEventListener(
        eventName,
        function listener(event) {
          try {
            if (checkFn && !checkFn(event)) {
              return;
            }
            subject.removeEventListener(eventName, listener, capture);
            setTimeout(() => {
              ChromeUtils.addProfilerMarker(
                "ContentTaskUtils",
                { category: "Test", startTime },
                "waitForEvent - " + eventName
              );
              resolve(event);
            }, 0);
          } catch (ex) {
            try {
              subject.removeEventListener(eventName, listener, capture);
            } catch (ex2) {
              // Maybe the provided object does not support removeEventListener.
            }
            setTimeout(() => reject(ex), 0);
          }
        },
        capture,
        wantsUntrusted
      );
    });
  },

  /**
   * Wait until DOM mutations cause the condition expressed in checkFn to pass.
   * Intended as an easy-to-use alternative to waitForCondition.
   *
   * @param {Element} subject
   *        The element on which to observe mutations.
   * @param {Object} options
   *        The options to pass to MutationObserver.observe();
   * @param {function} checkFn [optional]
   *        Function that returns true when it wants the promise to be resolved.
   *        If not specified, the first mutation will resolve the promise.
   *
   * @returns {Promise<void>}
   */
  waitForMutationCondition(subject, options, checkFn) {
    if (checkFn?.()) {
      return Promise.resolve();
    }
    return new Promise(resolve => {
      let obs = new subject.ownerGlobal.MutationObserver(function () {
        if (checkFn && !checkFn()) {
          return;
        }
        obs.disconnect();
        resolve();
      });
      obs.observe(subject, options);
    });
  },

  /**
   * Gets an instance of the `EventUtils` helper module for usage in
   * content tasks. See https://searchfox.org/mozilla-central/source/testing/mochitest/tests/SimpleTest/EventUtils.js
   *
   * @param content
   *        The `content` global object from your content task.
   *
   * @returns an EventUtils instance.
   */
  getEventUtils(content) {
    if (content._EventUtils) {
      return content._EventUtils;
    }

    let EventUtils = (content._EventUtils = {});

    EventUtils.window = {};
    EventUtils.setTimeout = setTimeout;
    EventUtils.parent = EventUtils.window;
    /* eslint-disable camelcase */
    EventUtils._EU_Ci = Ci;
    EventUtils._EU_Cc = Cc;
    /* eslint-enable camelcase */
    // EventUtils' `sendChar` function relies on the navigator to synthetize events.
    EventUtils.navigator = content.navigator;
    EventUtils.KeyboardEvent = content.KeyboardEvent;

    Services.scriptloader.loadSubScript(
      "chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
      EventUtils
    );

    return EventUtils;
  },
};