tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

MarionetteReftestChild.sys.mjs (8461B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 const lazy = {};
      6 
      7 ChromeUtils.defineESModuleGetters(lazy, {
      8  setTimeout: "resource://gre/modules/Timer.sys.mjs",
      9 
     10  Log: "chrome://remote/content/shared/Log.sys.mjs",
     11 });
     12 
     13 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
     14  lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
     15 );
     16 
     17 /**
     18 * Child JSWindowActor to handle navigation for reftests relying on marionette.
     19 */
     20 export class MarionetteReftestChild extends JSWindowActorChild {
     21  constructor() {
     22    super();
     23 
     24    // This promise will resolve with the URL recorded in the "load" event
     25    // handler. This URL will not be impacted by any hash modification that
     26    // might be performed by the test script.
     27    // The harness should be loaded before loading any test page, so the actors
     28    // should be registered before the "load" event is received for a test page.
     29    this._loadedURLPromise = new Promise(
     30      r => (this._resolveLoadedURLPromise = r)
     31    );
     32  }
     33 
     34  handleEvent(event) {
     35    if (event.type == "load") {
     36      const url = event.target.location.href;
     37      lazy.logger.debug(`Handle load event with URL ${url}`);
     38      this._resolveLoadedURLPromise(url);
     39    }
     40  }
     41 
     42  actorCreated() {
     43    lazy.logger.trace(
     44      `[${this.browsingContext.id}] Reftest actor created ` +
     45        `for window id ${this.manager.innerWindowId}`
     46    );
     47  }
     48 
     49  async receiveMessage(msg) {
     50    const { name, data } = msg;
     51 
     52    let result;
     53    switch (name) {
     54      case "MarionetteReftestParent:flushRendering":
     55        result = await this.flushRendering(data);
     56        break;
     57      case "MarionetteReftestParent:reftestWait":
     58        result = await this.reftestWait(data);
     59        break;
     60    }
     61    return result;
     62  }
     63 
     64  /**
     65   * Wait for a reftest page to be ready for screenshots:
     66   * - wait for the loadedURL to be available (see handleEvent)
     67   * - check if the URL matches the expected URL
     68   * - if present, wait for the "reftest-wait" classname to be removed from the
     69   *   document element
     70   *
     71   * @param {object} options
     72   * @param {string} options.url
     73   *        The expected test page URL
     74   * @param {boolean} options.useRemote
     75   *        True when using e10s
     76   * @param {boolean} options.warnOnOverflow
     77   *        True if we should check the content fits in the viewport.
     78   *        This isn't necessary for print reftests where we will render the full
     79   *        size of the paginated content.
     80   * @returns {boolean}
     81   *         Returns true when the correct page is loaded and ready for
     82   *         screenshots. Returns false if the page loaded bug does not have the
     83   *         expected URL.
     84   */
     85  async reftestWait(options = {}) {
     86    const { url, useRemote } = options;
     87    const loadedURL = await this._loadedURLPromise;
     88    if (loadedURL !== url) {
     89      lazy.logger.debug(
     90        `Window URL does not match the expected URL "${loadedURL}" !== "${url}"`
     91      );
     92      return false;
     93    }
     94 
     95    const documentElement = this.document.documentElement;
     96    const hasReftestWait = documentElement.classList.contains("reftest-wait");
     97 
     98    lazy.logger.debug("Waiting for event loop to spin");
     99    await new Promise(resolve => lazy.setTimeout(resolve, 0));
    100 
    101    await this.paintComplete({
    102      useRemote,
    103      ignoreThrottledAnimations: true,
    104      hasReftestWait,
    105    });
    106 
    107    if (hasReftestWait) {
    108      const event = new this.document.defaultView.Event("TestRendered", {
    109        bubbles: true,
    110      });
    111      documentElement.dispatchEvent(event);
    112      lazy.logger.info("Emitted TestRendered event");
    113      await this.reftestWaitRemoved();
    114      await this.paintComplete({
    115        useRemote,
    116        ignoreThrottledAnimations: false,
    117        hasReftestWait,
    118      });
    119    }
    120    if (
    121      options.warnOnOverflow &&
    122      (this.document.defaultView.innerWidth < documentElement.scrollWidth ||
    123        this.document.defaultView.innerHeight < documentElement.scrollHeight)
    124    ) {
    125      lazy.logger.warn(
    126        `${url} overflows viewport (width: ${documentElement.scrollWidth}, height: ${documentElement.scrollHeight})`
    127      );
    128    }
    129    return true;
    130  }
    131 
    132  paintComplete({ useRemote, ignoreThrottledAnimations, hasReftestWait }) {
    133    lazy.logger.debug("Waiting for rendering");
    134    let win = this.document.defaultView;
    135    let windowUtils = win.windowUtils;
    136    let painted = false;
    137    const documentElement = this.document.documentElement;
    138    return new Promise(resolve => {
    139      let maybeResolve = () => {
    140        this.flushRendering({ ignoreThrottledAnimations });
    141        if (useRemote) {
    142          // Flush display (paint)
    143          lazy.logger.debug("Force update of layer tree");
    144          windowUtils.updateLayerTree();
    145        }
    146 
    147        const once =
    148          hasReftestWait && !documentElement.classList.contains("reftest-wait");
    149        if (windowUtils.isMozAfterPaintPending && (!once || !painted)) {
    150          lazy.logger.debug("isMozAfterPaintPending: true");
    151          win.windowRoot.addEventListener(
    152            "MozAfterPaint",
    153            () => {
    154              lazy.logger.debug("MozAfterPaint fired");
    155              painted = true;
    156              maybeResolve();
    157            },
    158            { once: true }
    159          );
    160        } else {
    161          // resolve at the start of the next frame in case of leftover paints
    162          lazy.logger.debug("isMozAfterPaintPending: false");
    163          win.requestAnimationFrame(() => {
    164            win.requestAnimationFrame(resolve);
    165          });
    166        }
    167      };
    168      maybeResolve();
    169    });
    170  }
    171 
    172  reftestWaitRemoved() {
    173    lazy.logger.debug("Waiting for reftest-wait removal");
    174    return new Promise(resolve => {
    175      const documentElement = this.document.documentElement;
    176      let observer = new this.document.defaultView.MutationObserver(() => {
    177        if (!documentElement.classList.contains("reftest-wait")) {
    178          observer.disconnect();
    179          lazy.logger.debug("reftest-wait removed");
    180          lazy.setTimeout(resolve, 0);
    181        }
    182      });
    183      if (documentElement.classList.contains("reftest-wait")) {
    184        observer.observe(documentElement, { attributes: true });
    185      } else {
    186        lazy.setTimeout(resolve, 0);
    187      }
    188    });
    189  }
    190 
    191  /**
    192   * Ensure layout is flushed in each frame
    193   *
    194   * @param {object} options
    195   * @param {boolean} options.ignoreThrottledAnimations Don't flush
    196   *        the layout of throttled animations. We can end up in a
    197   *        situation where flushing a throttled animation causes
    198   *        mozAfterPaint events even when all rendering we care about
    199   *        should have ceased. See
    200   *        https://searchfox.org/mozilla-central/rev/d58860eb739af613774c942c3bb61754123e449b/layout/tools/reftest/reftest-content.js#723-729
    201   *        for more detail.
    202   */
    203  flushRendering(options = {}) {
    204    let { ignoreThrottledAnimations } = options;
    205    lazy.logger.debug(
    206      `flushRendering ignoreThrottledAnimations:${ignoreThrottledAnimations}`
    207    );
    208    let anyPendingPaintsGeneratedInDescendants = false;
    209 
    210    function flushWindow(win) {
    211      let utils = win.windowUtils;
    212      let afterPaintWasPending = utils.isMozAfterPaintPending;
    213 
    214      let root = win.document.documentElement;
    215      if (root) {
    216        try {
    217          if (ignoreThrottledAnimations) {
    218            utils.flushLayoutWithoutThrottledAnimations();
    219          } else {
    220            root.getBoundingClientRect();
    221          }
    222        } catch (e) {
    223          lazy.logger.error("flushWindow failed", e);
    224        }
    225      }
    226 
    227      if (!afterPaintWasPending && utils.isMozAfterPaintPending) {
    228        anyPendingPaintsGeneratedInDescendants = true;
    229      }
    230 
    231      for (let i = 0; i < win.frames.length; ++i) {
    232        // Skip remote frames, flushRendering will be called on their individual
    233        // MarionetteReftest actor via _recursiveFlushRendering performed from
    234        // the topmost MarionetteReftest actor.
    235        if (!Cu.isRemoteProxy(win.frames[i])) {
    236          flushWindow(win.frames[i]);
    237        }
    238      }
    239    }
    240 
    241    let thisWin = this.document.defaultView;
    242    flushWindow(thisWin);
    243 
    244    if (
    245      anyPendingPaintsGeneratedInDescendants &&
    246      !thisWin.windowUtils.isMozAfterPaintPending
    247    ) {
    248      lazy.logger.error(
    249        "Descendant frame generated a MozAfterPaint event, " +
    250          "but the root document doesn't have one!"
    251      );
    252    }
    253  }
    254 }