summaryrefslogtreecommitdiffstats
path: root/remote/marionette/actors/MarionetteReftestChild.sys.mjs
blob: fd73f88a883e34dfc918b7a17d08d89386224565 (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
/* 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/. */

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  setTimeout: "resource://gre/modules/Timer.sys.mjs",

  Log: "chrome://remote/content/shared/Log.sys.mjs",
});

ChromeUtils.defineLazyGetter(lazy, "logger", () =>
  lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
);

/**
 * Child JSWindowActor to handle navigation for reftests relying on marionette.
 */
export class MarionetteReftestChild extends JSWindowActorChild {
  constructor() {
    super();

    // This promise will resolve with the URL recorded in the "load" event
    // handler. This URL will not be impacted by any hash modification that
    // might be performed by the test script.
    // The harness should be loaded before loading any test page, so the actors
    // should be registered before the "load" event is received for a test page.
    this._loadedURLPromise = new Promise(
      r => (this._resolveLoadedURLPromise = r)
    );
  }

  handleEvent(event) {
    if (event.type == "load") {
      const url = event.target.location.href;
      lazy.logger.debug(`Handle load event with URL ${url}`);
      this._resolveLoadedURLPromise(url);
    }
  }

  actorCreated() {
    lazy.logger.trace(
      `[${this.browsingContext.id}] Reftest actor created ` +
        `for window id ${this.manager.innerWindowId}`
    );
  }

  async receiveMessage(msg) {
    const { name, data } = msg;

    let result;
    switch (name) {
      case "MarionetteReftestParent:flushRendering":
        result = await this.flushRendering(data);
        break;
      case "MarionetteReftestParent:reftestWait":
        result = await this.reftestWait(data);
        break;
    }
    return result;
  }

  /**
   * Wait for a reftest page to be ready for screenshots:
   * - wait for the loadedURL to be available (see handleEvent)
   * - check if the URL matches the expected URL
   * - if present, wait for the "reftest-wait" classname to be removed from the
   *   document element
   *
   * @param {object} options
   * @param {string} options.url
   *        The expected test page URL
   * @param {boolean} options.useRemote
   *        True when using e10s
   * @param {boolean} options.warnOnOverflow
   *        True if we should check the content fits in the viewport.
   *        This isn't necessary for print reftests where we will render the full
   *        size of the paginated content.
   * @returns {boolean}
   *         Returns true when the correct page is loaded and ready for
   *         screenshots. Returns false if the page loaded bug does not have the
   *         expected URL.
   */
  async reftestWait(options = {}) {
    const { url, useRemote } = options;
    const loadedURL = await this._loadedURLPromise;
    if (loadedURL !== url) {
      lazy.logger.debug(
        `Window URL does not match the expected URL "${loadedURL}" !== "${url}"`
      );
      return false;
    }

    const documentElement = this.document.documentElement;
    const hasReftestWait = documentElement.classList.contains("reftest-wait");

    lazy.logger.debug("Waiting for event loop to spin");
    await new Promise(resolve => lazy.setTimeout(resolve, 0));

    await this.paintComplete({ useRemote, ignoreThrottledAnimations: true });

    if (hasReftestWait) {
      const event = new this.document.defaultView.Event("TestRendered", {
        bubbles: true,
      });
      documentElement.dispatchEvent(event);
      lazy.logger.info("Emitted TestRendered event");
      await this.reftestWaitRemoved();
      await this.paintComplete({ useRemote, ignoreThrottledAnimations: false });
    }
    if (
      options.warnOnOverflow &&
      (this.document.defaultView.innerWidth < documentElement.scrollWidth ||
        this.document.defaultView.innerHeight < documentElement.scrollHeight)
    ) {
      lazy.logger.warn(
        `${url} overflows viewport (width: ${documentElement.scrollWidth}, height: ${documentElement.scrollHeight})`
      );
    }
    return true;
  }

  paintComplete({ useRemote, ignoreThrottledAnimations }) {
    lazy.logger.debug("Waiting for rendering");
    let windowUtils = this.document.defaultView.windowUtils;
    return new Promise(resolve => {
      let maybeResolve = () => {
        this.flushRendering({ ignoreThrottledAnimations });
        if (useRemote) {
          // Flush display (paint)
          lazy.logger.debug("Force update of layer tree");
          windowUtils.updateLayerTree();
        }

        if (windowUtils.isMozAfterPaintPending) {
          lazy.logger.debug("isMozAfterPaintPending: true");
          this.document.defaultView.addEventListener(
            "MozAfterPaint",
            maybeResolve,
            {
              once: true,
            }
          );
        } else {
          // resolve at the start of the next frame in case of leftover paints
          lazy.logger.debug("isMozAfterPaintPending: false");
          this.document.defaultView.requestAnimationFrame(() => {
            this.document.defaultView.requestAnimationFrame(resolve);
          });
        }
      };
      maybeResolve();
    });
  }

  reftestWaitRemoved() {
    lazy.logger.debug("Waiting for reftest-wait removal");
    return new Promise(resolve => {
      const documentElement = this.document.documentElement;
      let observer = new this.document.defaultView.MutationObserver(() => {
        if (!documentElement.classList.contains("reftest-wait")) {
          observer.disconnect();
          lazy.logger.debug("reftest-wait removed");
          lazy.setTimeout(resolve, 0);
        }
      });
      if (documentElement.classList.contains("reftest-wait")) {
        observer.observe(documentElement, { attributes: true });
      } else {
        lazy.setTimeout(resolve, 0);
      }
    });
  }

  /**
   * Ensure layout is flushed in each frame
   *
   * @param {object} options
   * @param {boolean} options.ignoreThrottledAnimations Don't flush
   *        the layout of throttled animations. We can end up in a
   *        situation where flushing a throttled animation causes
   *        mozAfterPaint events even when all rendering we care about
   *        should have ceased. See
   *        https://searchfox.org/mozilla-central/rev/d58860eb739af613774c942c3bb61754123e449b/layout/tools/reftest/reftest-content.js#723-729
   *        for more detail.
   */
  flushRendering(options = {}) {
    let { ignoreThrottledAnimations } = options;
    lazy.logger.debug(
      `flushRendering ignoreThrottledAnimations:${ignoreThrottledAnimations}`
    );
    let anyPendingPaintsGeneratedInDescendants = false;

    let windowUtils = this.document.defaultView.windowUtils;

    function flushWindow(win) {
      let utils = win.windowUtils;
      let afterPaintWasPending = utils.isMozAfterPaintPending;

      let root = win.document.documentElement;
      if (root) {
        try {
          if (ignoreThrottledAnimations) {
            utils.flushLayoutWithoutThrottledAnimations();
          } else {
            root.getBoundingClientRect();
          }
        } catch (e) {
          lazy.logger.error("flushWindow failed", e);
        }
      }

      if (!afterPaintWasPending && utils.isMozAfterPaintPending) {
        anyPendingPaintsGeneratedInDescendants = true;
      }

      for (let i = 0; i < win.frames.length; ++i) {
        // Skip remote frames, flushRendering will be called on their individual
        // MarionetteReftest actor via _recursiveFlushRendering performed from
        // the topmost MarionetteReftest actor.
        if (!Cu.isRemoteProxy(win.frames[i])) {
          flushWindow(win.frames[i]);
        }
      }
    }
    flushWindow(this.document.defaultView);

    if (
      anyPendingPaintsGeneratedInDescendants &&
      !windowUtils.isMozAfterPaintPending
    ) {
      lazy.logger.error(
        "Descendant frame generated a MozAfterPaint event, " +
          "but the root document doesn't have one!"
      );
    }
  }
}