 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,
      hasReftestWait,
    });

    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,
        hasReftestWait,
      });
    }
    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, hasReftestWait }) {
    lazy.logger.debug("Waiting for rendering");
    let win = this.document.defaultView;
    let windowUtils = win.windowUtils;
    let painted = false;
    const documentElement = this.document.documentElement;
    return new Promise(resolve => {
      let maybeResolve = () => {
        this.flushRendering({ ignoreThrottledAnimations });
        if (useRemote) {
          // Flush display (paint)
          lazy.logger.debug("Force update of layer tree");
          windowUtils.updateLayerTree();
        }

        const once =
          hasReftestWait && !documentElement.classList.contains("reftest-wait");
        if (windowUtils.isMozAfterPaintPending && (!once || !painted)) {
          lazy.logger.debug("isMozAfterPaintPending: true");
          win.windowRoot.addEventListener(
            "MozAfterPaint",
            () => {
              lazy.logger.debug("MozAfterPaint fired");
              painted = true;
              maybeResolve();
            },
            { once: true }
          );
        } else {
          // resolve at the start of the next frame in case of leftover paints
          lazy.logger.debug("isMozAfterPaintPending: false");
          win.requestAnimationFrame(() => {
            win.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;

    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]);
        }
      }
    }

    let thisWin = this.document.defaultView;
    flushWindow(thisWin);

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