tor-browser

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

reftest.sys.mjs (27926B)


      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 file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 const lazy = {};
      6 
      7 ChromeUtils.defineESModuleGetters(lazy, {
      8  clearTimeout: "resource://gre/modules/Timer.sys.mjs",
      9  E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
     10  setTimeout: "resource://gre/modules/Timer.sys.mjs",
     11 
     12  AppInfo: "chrome://remote/content/shared/AppInfo.sys.mjs",
     13  assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
     14  capture: "chrome://remote/content/shared/Capture.sys.mjs",
     15  Log: "chrome://remote/content/shared/Log.sys.mjs",
     16  navigate: "chrome://remote/content/marionette/navigate.sys.mjs",
     17  print: "chrome://remote/content/shared/PDF.sys.mjs",
     18 });
     19 
     20 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
     21  lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
     22 );
     23 
     24 const XHTML_NS = "http://www.w3.org/1999/xhtml";
     25 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
     26 
     27 const SCREENSHOT_MODE = {
     28  unexpected: 0,
     29  fail: 1,
     30  always: 2,
     31 };
     32 
     33 const STATUS = {
     34  PASS: "PASS",
     35  FAIL: "FAIL",
     36  ERROR: "ERROR",
     37  TIMEOUT: "TIMEOUT",
     38 };
     39 
     40 const DEFAULT_REFTEST_WIDTH = 600;
     41 const DEFAULT_REFTEST_HEIGHT = 600;
     42 
     43 // reftest-print page dimensions in cm
     44 const CM_PER_INCH = 2.54;
     45 const DEFAULT_PAGE_WIDTH = 5 * CM_PER_INCH;
     46 const DEFAULT_PAGE_HEIGHT = 3 * CM_PER_INCH;
     47 const DEFAULT_PAGE_MARGIN = 0.5 * CM_PER_INCH;
     48 
     49 // CSS 96 pixels per inch, compared to pdf.js default 72 pixels per inch
     50 const DEFAULT_PDF_RESOLUTION = 96 / 72;
     51 
     52 /**
     53 * Implements an fast runner for web-platform-tests format reftests
     54 * c.f. http://web-platform-tests.org/writing-tests/reftests.html.
     55 *
     56 * @namespace
     57 */
     58 export const reftest = {};
     59 
     60 /**
     61 * @memberof reftest
     62 * @class Runner
     63 */
     64 reftest.Runner = class {
     65  constructor(driver) {
     66    this.driver = driver;
     67    this.canvasCache = new DefaultMap(undefined, () => new Map([[null, []]]));
     68    this.isPrint = null;
     69    this.windowUtils = null;
     70    this.lastURL = null;
     71    this.useRemoteTabs = lazy.AppInfo.browserTabsRemoteAutostart;
     72    this.useRemoteSubframes = lazy.AppInfo.fissionAutostart;
     73    this.cacheScreenshots = true;
     74    this.useDrawSnapshot = Services.prefs.getBoolPref(
     75      "reftest.use-draw-snapshot",
     76      false
     77    );
     78  }
     79 
     80  /**
     81   * Setup the required environment for running reftests.
     82   *
     83   * This will open a non-browser window in which the tests will
     84   * be loaded, and set up various caches for the reftest run.
     85   *
     86   * @param {Record<string, number>} urlCount
     87   *     Object holding a map of URL: number of times the URL
     88   *     will be opened during the reftest run, where that's
     89   *     greater than 1.
     90   * @param {string} screenshotMode
     91   *     String enum representing when screenshots should be taken
     92   */
     93  setup(urlCount, screenshotMode, isPrint = false, cacheScreenshots = true) {
     94    this.isPrint = isPrint;
     95 
     96    lazy.assert.open(this.driver.getBrowsingContext({ top: true }));
     97    this.parentWindow = this.driver.getCurrentWindow();
     98 
     99    this.screenshotMode =
    100      SCREENSHOT_MODE[screenshotMode] || SCREENSHOT_MODE.unexpected;
    101 
    102    this.urlCount = Object.keys(urlCount || {}).reduce(
    103      (map, key) => map.set(key, urlCount[key]),
    104      new Map()
    105    );
    106 
    107    if (isPrint) {
    108      this.loadPdfJs();
    109    }
    110 
    111    this.cacheScreenshots = cacheScreenshots;
    112 
    113    ChromeUtils.registerWindowActor("MarionetteReftest", {
    114      kind: "JSWindowActor",
    115      parent: {
    116        esModuleURI:
    117          "chrome://remote/content/marionette/actors/MarionetteReftestParent.sys.mjs",
    118      },
    119      child: {
    120        esModuleURI:
    121          "chrome://remote/content/marionette/actors/MarionetteReftestChild.sys.mjs",
    122        events: {
    123          load: { mozSystemGroup: true, capture: true },
    124        },
    125      },
    126      allFrames: true,
    127    });
    128  }
    129 
    130  /**
    131   * Cleanup the environment once the reftest is finished.
    132   */
    133  teardown() {
    134    // Abort the current test if any.
    135    this.abort();
    136 
    137    // Unregister the JSWindowActors.
    138    ChromeUtils.unregisterWindowActor("MarionetteReftest");
    139  }
    140 
    141  async ensureWindow(timeout, width, height) {
    142    lazy.logger.debug(`ensuring we have a window ${width}x${height}`);
    143 
    144    if (this.reftestWin && !this.reftestWin.closed) {
    145      let browserRect = this.reftestWin.gBrowser.getBoundingClientRect();
    146      if (browserRect.width === width && browserRect.height === height) {
    147        return this.reftestWin;
    148      }
    149      lazy.logger.debug(`current: ${browserRect.width}x${browserRect.height}`);
    150    }
    151 
    152    let reftestWin;
    153    if (lazy.AppInfo.isAndroid) {
    154      lazy.logger.debug("Using current window");
    155      reftestWin = this.parentWindow;
    156      await lazy.navigate.waitForNavigationCompleted(this.driver, () => {
    157        const browsingContext = this.driver.getBrowsingContext();
    158        lazy.navigate.navigateTo(browsingContext, "about:blank");
    159      });
    160    } else {
    161      lazy.logger.debug("Using separate window");
    162      if (this.reftestWin && !this.reftestWin.closed) {
    163        this.reftestWin.close();
    164      }
    165      reftestWin = await this.openWindow(width, height);
    166    }
    167 
    168    this.setupWindow(reftestWin, width, height);
    169    this.windowUtils = reftestWin.windowUtils;
    170    this.reftestWin = reftestWin;
    171 
    172    let windowHandle = this.driver.getWindowProperties(reftestWin);
    173    await this.driver.setWindowHandle(windowHandle, true);
    174 
    175    const url = await this.driver._getCurrentURL();
    176    this.lastURL = url.href;
    177    lazy.logger.debug(`loaded initial URL: ${this.lastURL}`);
    178 
    179    let browserRect = reftestWin.gBrowser.getBoundingClientRect();
    180    lazy.logger.debug(`new: ${browserRect.width}x${browserRect.height}`);
    181 
    182    return reftestWin;
    183  }
    184 
    185  async openWindow(width, height) {
    186    lazy.assert.positiveInteger(width);
    187    lazy.assert.positiveInteger(height);
    188 
    189    let reftestWin = this.parentWindow.open(
    190      "chrome://remote/content/marionette/reftest-chrome/reftest.xhtml",
    191      "reftest",
    192      `chrome,height=${height},width=${width}`
    193    );
    194 
    195    await new Promise(resolve => {
    196      reftestWin.addEventListener("load", resolve, { once: true });
    197    });
    198    return reftestWin;
    199  }
    200 
    201  setupWindow(reftestWin, width, height) {
    202    let browser;
    203    if (lazy.AppInfo.isAndroid) {
    204      browser = reftestWin.document.getElementsByTagName("browser")[0];
    205      browser.setAttribute("remote", "false");
    206    } else {
    207      browser = reftestWin.document.createElementNS(XUL_NS, "xul:browser");
    208      browser.permanentKey = {};
    209      browser.setAttribute("id", "browser");
    210      browser.setAttribute("type", "content");
    211      browser.setAttribute("primary", "true");
    212      browser.setAttribute("remote", this.useRemoteTabs ? "true" : "false");
    213    }
    214    // Make sure the browser element is exactly the right size, no matter
    215    // what size our window is
    216    browser.style.setProperty("padding", "0px");
    217    browser.style.setProperty("margin", "0px");
    218    browser.style.setProperty("border", "none");
    219    browser.style.setProperty("min-width", `${width}px`);
    220    browser.style.setProperty("min-height", `${height}px`);
    221    browser.style.setProperty("max-width", `${width}px`);
    222    browser.style.setProperty("max-height", `${height}px`);
    223    browser.style.setProperty(
    224      "color-scheme",
    225      "env(-moz-content-preferred-color-scheme)"
    226    );
    227 
    228    if (!lazy.AppInfo.isAndroid) {
    229      let doc = reftestWin.document.documentElement;
    230      while (doc.firstChild) {
    231        doc.firstChild.remove();
    232      }
    233      doc.appendChild(browser);
    234    }
    235    if (reftestWin.BrowserApp) {
    236      reftestWin.BrowserApp = browser;
    237    }
    238    reftestWin.gBrowser = browser;
    239    return reftestWin;
    240  }
    241 
    242  async abort() {
    243    if (this.reftestWin && this.reftestWin != this.parentWindow) {
    244      await this.driver.closeChromeWindow();
    245      let parentHandle = this.driver.getWindowProperties(this.parentWindow);
    246      await this.driver.setWindowHandle(parentHandle);
    247    }
    248    this.reftestWin = null;
    249  }
    250 
    251  /**
    252   * Run a specific reftest.
    253   *
    254   * The assumed semantics are those of web-platform-tests where
    255   * references form a tree and each test must meet all the conditions
    256   * to reach one leaf node of the tree in order for the overall test
    257   * to pass.
    258   *
    259   * @param {string} testUrl
    260   *     URL of the test itself.
    261   * @param {Array.<Array>} references
    262   *     Array representing a tree of references to try.
    263   *
    264   *     Each item in the array represents a single reference node and
    265   *     has the form <code>[referenceUrl, references, relation]</code>,
    266   *     where <var>referenceUrl</var> is a string to the URL, relation
    267   *     is either <code>==</code> or <code>!=</code> depending on the
    268   *     type of reftest, and references is another array containing
    269   *     items of the same form, representing further comparisons treated
    270   *     as AND with the current item. Sibling entries are treated as OR.
    271   *
    272   *     For example with testUrl of T:
    273   *
    274   *     <pre><code>
    275   *       references = [[A, [[B, [], ==]], ==]]
    276   *       Must have T == A AND A == B to pass
    277   *
    278   *       references = [[A, [], ==], [B, [], !=]
    279   *       Must have T == A OR T != B
    280   *
    281   *       references = [[A, [[B, [], ==], [C, [], ==]], ==], [D, [], ]]
    282   *       Must have (T == A AND A == B) OR (T == A AND A == C) OR (T == D)
    283   *     </code></pre>
    284   *
    285   * @param {string} expected
    286   *     Expected test outcome (e.g. <tt>PASS</tt>, <tt>FAIL</tt>).
    287   * @param {number} timeout
    288   *     Test timeout in milliseconds.
    289   *
    290   * @returns {object}
    291   *     Result object with fields status, message and extra.
    292   */
    293  async run(
    294    testUrl,
    295    references,
    296    expected,
    297    timeout,
    298    pageRanges = {},
    299    width = DEFAULT_REFTEST_WIDTH,
    300    height = DEFAULT_REFTEST_HEIGHT
    301  ) {
    302    let timerId;
    303 
    304    let timeoutPromise = new Promise(resolve => {
    305      timerId = lazy.setTimeout(() => {
    306        resolve({ status: STATUS.TIMEOUT, message: null, extra: {} });
    307      }, timeout);
    308    });
    309 
    310    let testRunner = (async () => {
    311      let result;
    312      try {
    313        result = await this.runTest(
    314          testUrl,
    315          references,
    316          expected,
    317          timeout,
    318          pageRanges,
    319          width,
    320          height
    321        );
    322      } catch (e) {
    323        result = {
    324          status: STATUS.ERROR,
    325          message: String(e),
    326          stack: e.stack,
    327          extra: {},
    328        };
    329      }
    330      return result;
    331    })();
    332 
    333    let result = await Promise.race([testRunner, timeoutPromise]);
    334    lazy.clearTimeout(timerId);
    335    if (result.status === STATUS.TIMEOUT) {
    336      await this.abort();
    337    }
    338 
    339    return result;
    340  }
    341 
    342  async runTest(
    343    testUrl,
    344    references,
    345    expected,
    346    timeout,
    347    pageRanges,
    348    width,
    349    height
    350  ) {
    351    let win = await this.ensureWindow(timeout, width, height);
    352 
    353    function toBase64(screenshot) {
    354      let dataURL = screenshot.canvas.toDataURL();
    355      return dataURL.split(",")[1];
    356    }
    357 
    358    let result = {
    359      status: STATUS.FAIL,
    360      message: "",
    361      stack: null,
    362      extra: {},
    363    };
    364 
    365    let screenshotData = [];
    366 
    367    let stack = [];
    368    for (let i = references.length - 1; i >= 0; i--) {
    369      let item = references[i];
    370      stack.push([testUrl, ...item]);
    371    }
    372 
    373    let done = false;
    374 
    375    while (stack.length && !done) {
    376      let [lhsUrl, rhsUrl, stackframeReferences, relation, extras = {}] =
    377        stack.pop();
    378      result.message += `Testing ${lhsUrl} ${relation} ${rhsUrl}\n`;
    379 
    380      let comparison;
    381      try {
    382        comparison = await this.compareUrls(
    383          win,
    384          lhsUrl,
    385          rhsUrl,
    386          relation,
    387          timeout,
    388          pageRanges,
    389          extras
    390        );
    391      } catch (e) {
    392        comparison = {
    393          lhs: null,
    394          rhs: null,
    395          passed: false,
    396          error: e,
    397          msg: null,
    398        };
    399      }
    400      if (comparison.msg) {
    401        result.message += `${comparison.msg}\n`;
    402      }
    403      if (comparison.error !== null) {
    404        result.status = STATUS.ERROR;
    405        result.message += String(comparison.error);
    406        result.stack = comparison.error.stack;
    407      }
    408 
    409      function recordScreenshot() {
    410        let encodedLHS = comparison.lhs ? toBase64(comparison.lhs) : "";
    411        let encodedRHS = comparison.rhs ? toBase64(comparison.rhs) : "";
    412        screenshotData.push([
    413          { url: lhsUrl, screenshot: encodedLHS },
    414          relation,
    415          { url: rhsUrl, screenshot: encodedRHS },
    416        ]);
    417      }
    418 
    419      if (this.screenshotMode === SCREENSHOT_MODE.always) {
    420        recordScreenshot();
    421      }
    422 
    423      if (comparison.passed) {
    424        if (stackframeReferences.length) {
    425          for (let i = stackframeReferences.length - 1; i >= 0; i--) {
    426            let item = stackframeReferences[i];
    427            stack.push([rhsUrl, ...item]);
    428          }
    429        } else {
    430          // Reached a leaf node so all of one reference chain passed
    431          result.status = STATUS.PASS;
    432          if (
    433            this.screenshotMode <= SCREENSHOT_MODE.fail &&
    434            expected != result.status
    435          ) {
    436            recordScreenshot();
    437          }
    438          done = true;
    439        }
    440      } else if (!stack.length || result.status == STATUS.ERROR) {
    441        // If we don't have any alternatives to try then this will be
    442        // the last iteration, so save the failing screenshots if required.
    443        let isFail = this.screenshotMode === SCREENSHOT_MODE.fail;
    444        let isUnexpected = this.screenshotMode === SCREENSHOT_MODE.unexpected;
    445        if (isFail || (isUnexpected && expected != result.status)) {
    446          recordScreenshot();
    447        }
    448      }
    449 
    450      // Return any reusable canvases to the pool
    451      let cacheKey = width + "x" + height;
    452      let canvasPool = this.canvasCache.get(cacheKey).get(null);
    453      [comparison.lhs, comparison.rhs].map(screenshot => {
    454        if (screenshot !== null && screenshot.reuseCanvas) {
    455          canvasPool.push(screenshot.canvas);
    456        }
    457      });
    458      lazy.logger.debug(
    459        `Canvas pool (${cacheKey}) is of length ${canvasPool.length}`
    460      );
    461    }
    462 
    463    if (screenshotData.length) {
    464      // For now the tbpl formatter only accepts one screenshot, so just
    465      // return the last one we took.
    466      let lastScreenshot = screenshotData[screenshotData.length - 1];
    467      // eslint-disable-next-line camelcase
    468      result.extra.reftest_screenshots = lastScreenshot;
    469    }
    470 
    471    return result;
    472  }
    473 
    474  async compareUrls(
    475    win,
    476    lhsUrl,
    477    rhsUrl,
    478    relation,
    479    timeout,
    480    pageRanges,
    481    extras
    482  ) {
    483    lazy.logger.info(`Testing ${lhsUrl} ${relation} ${rhsUrl}`);
    484 
    485    if (relation !== "==" && relation != "!=") {
    486      throw new error.InvalidArgumentError(
    487        "Reftest operator should be '==' or '!='"
    488      );
    489    }
    490 
    491    let lhsIter, lhsCount, rhsIter, rhsCount;
    492    if (!this.isPrint) {
    493      // Take the reference screenshot first so that if we pause
    494      // we see the test rendering
    495      rhsIter = [await this.screenshot(win, rhsUrl, timeout)].values();
    496      lhsIter = [await this.screenshot(win, lhsUrl, timeout)].values();
    497      lhsCount = rhsCount = 1;
    498    } else {
    499      [rhsIter, rhsCount] = await this.screenshotPaginated(
    500        win,
    501        rhsUrl,
    502        timeout,
    503        pageRanges
    504      );
    505      [lhsIter, lhsCount] = await this.screenshotPaginated(
    506        win,
    507        lhsUrl,
    508        timeout,
    509        pageRanges
    510      );
    511    }
    512 
    513    let passed = null;
    514    let error = null;
    515    let pixelsDifferent = null;
    516    let maxDifferences = {};
    517    let msg = null;
    518 
    519    if (lhsCount != rhsCount) {
    520      passed = relation == "!=";
    521      if (!passed) {
    522        msg = `Got different numbers of pages; test has ${lhsCount}, ref has ${rhsCount}`;
    523      }
    524    }
    525 
    526    let lhs = null;
    527    let rhs = null;
    528    lazy.logger.debug(`Comparing ${lhsCount} pages`);
    529    if (passed === null) {
    530      for (let i = 0; i < lhsCount; i++) {
    531        lhs = (await lhsIter.next()).value;
    532        rhs = (await rhsIter.next()).value;
    533        lazy.logger.debug(
    534          `lhs canvas size ${lhs.canvas.width}x${lhs.canvas.height}`
    535        );
    536        lazy.logger.debug(
    537          `rhs canvas size ${rhs.canvas.width}x${rhs.canvas.height}`
    538        );
    539        if (
    540          lhs.canvas.width != rhs.canvas.width ||
    541          lhs.canvas.height != rhs.canvas.height
    542        ) {
    543          msg =
    544            `Got different page sizes; test is ` +
    545            `${lhs.canvas.width}x${lhs.canvas.height}px, ref is ` +
    546            `${rhs.canvas.width}x${rhs.canvas.height}px`;
    547          passed = false;
    548          break;
    549        }
    550        try {
    551          pixelsDifferent = this.windowUtils.compareCanvases(
    552            lhs.canvas,
    553            rhs.canvas,
    554            maxDifferences
    555          );
    556        } catch (e) {
    557          error = e;
    558          passed = false;
    559          break;
    560        }
    561 
    562        let areEqual = this.isAcceptableDifference(
    563          maxDifferences.value,
    564          pixelsDifferent,
    565          extras.fuzzy
    566        );
    567        lazy.logger.debug(
    568          `Page ${i + 1} maxDifferences: ${maxDifferences.value} ` +
    569            `pixelsDifferent: ${pixelsDifferent}`
    570        );
    571        lazy.logger.debug(
    572          `Page ${i + 1} ${areEqual ? "compare equal" : "compare unequal"}`
    573        );
    574        if (!areEqual) {
    575          if (relation == "==") {
    576            passed = false;
    577            msg =
    578              `Found ${pixelsDifferent} pixels different, ` +
    579              `maximum difference per channel ${maxDifferences.value}`;
    580            if (this.isPrint) {
    581              msg += ` on page ${i + 1}`;
    582            }
    583          } else {
    584            passed = true;
    585          }
    586          break;
    587        }
    588      }
    589    }
    590 
    591    // If passed isn't set we got to the end without finding differences
    592    if (passed === null) {
    593      if (relation == "==") {
    594        passed = true;
    595      } else {
    596        msg = `mismatch reftest has no differences`;
    597        passed = false;
    598      }
    599    }
    600    return { lhs, rhs, passed, error, msg };
    601  }
    602 
    603  isAcceptableDifference(maxDifference, pixelsDifferent, allowed) {
    604    if (!allowed) {
    605      lazy.logger.info(`No differences allowed`);
    606      return pixelsDifferent === 0;
    607    }
    608    let [allowedDiff, allowedPixels] = allowed;
    609    lazy.logger.info(
    610      `Allowed ${allowedPixels.join("-")} pixels different, ` +
    611        `maximum difference per channel ${allowedDiff.join("-")}`
    612    );
    613    return (
    614      (pixelsDifferent === 0 && allowedPixels[0] == 0) ||
    615      (maxDifference === 0 && allowedDiff[0] == 0) ||
    616      (maxDifference >= allowedDiff[0] &&
    617        maxDifference <= allowedDiff[1] &&
    618        pixelsDifferent >= allowedPixels[0] &&
    619        pixelsDifferent <= allowedPixels[1])
    620    );
    621  }
    622 
    623  ensureFocus(win) {
    624    const focusManager = Services.focus;
    625    if (focusManager.activeWindow != win) {
    626      win.focus();
    627    }
    628    this.driver.curBrowser.contentBrowser.focus();
    629  }
    630 
    631  updateBrowserRemotenessByURL(browser, url) {
    632    // We don't use remote tabs on Android.
    633    if (lazy.AppInfo.isAndroid) {
    634      return;
    635    }
    636    let oa = lazy.E10SUtils.predictOriginAttributes({ browser });
    637    let remoteType = lazy.E10SUtils.getRemoteTypeForURI(
    638      url,
    639      this.useRemoteTabs,
    640      this.useRemoteSubframes,
    641      lazy.E10SUtils.DEFAULT_REMOTE_TYPE,
    642      null,
    643      oa
    644    );
    645 
    646    // Only re-construct the browser if its remote type needs to change.
    647    if (browser.remoteType !== remoteType) {
    648      if (remoteType === lazy.E10SUtils.NOT_REMOTE) {
    649        browser.removeAttribute("remote");
    650        browser.removeAttribute("remoteType");
    651      } else {
    652        browser.setAttribute("remote", "true");
    653        browser.setAttribute("remoteType", remoteType);
    654      }
    655 
    656      browser.changeRemoteness({ remoteType });
    657      browser.construct();
    658    }
    659  }
    660 
    661  async loadTestUrl(win, url, timeout, warnOnOverflow = true) {
    662    const browsingContext = this.driver.getBrowsingContext({ top: true });
    663    const webProgress = browsingContext.webProgress;
    664 
    665    lazy.logger.debug(`Starting load of ${url}`);
    666    if (this.lastURL === url) {
    667      lazy.logger.debug(`Refreshing page`);
    668      await lazy.navigate.waitForNavigationCompleted(this.driver, () => {
    669        lazy.navigate.refresh(browsingContext);
    670      });
    671    } else {
    672      // HACK: DocumentLoadListener currently doesn't know how to
    673      // process-switch loads in a non-tabbed <browser>. We need to manually
    674      // set the browser's remote type in order to ensure that the load
    675      // happens in the correct process.
    676      //
    677      // See bug 1636169.
    678      this.updateBrowserRemotenessByURL(win.gBrowser, url);
    679      lazy.navigate.navigateTo(browsingContext, url);
    680 
    681      this.lastURL = url;
    682    }
    683 
    684    this.ensureFocus(win);
    685 
    686    // TODO: Move all the wait logic into the parent process (bug 1669787)
    687    let isReftestReady = false;
    688    while (!isReftestReady) {
    689      // Note: We cannot compare the URL here. Before the navigation is complete
    690      // currentWindowGlobal.documentURI.spec will still point to the old URL.
    691      const actor =
    692        webProgress.browsingContext.currentWindowGlobal.getActor(
    693          "MarionetteReftest"
    694        );
    695      isReftestReady = await actor.reftestWait(
    696        url,
    697        this.useRemoteTabs,
    698        warnOnOverflow
    699      );
    700    }
    701  }
    702 
    703  async screenshot(win, url, timeout) {
    704    // On windows the above doesn't *actually* set the window to be the
    705    // reftest size; but *does* set the content area to be the right size;
    706    // the window is given some extra borders that aren't explicable from CSS
    707    let browserRect = win.gBrowser.getBoundingClientRect();
    708    let canvas = null;
    709    let remainingCount = this.urlCount.get(url) || 1;
    710    let cache = this.cacheScreenshots && remainingCount > 1;
    711    let cacheKey = browserRect.width + "x" + browserRect.height;
    712    lazy.logger.debug(
    713      `screenshot ${url} remainingCount: ` +
    714        `${remainingCount} cache: ${cache} cacheKey: ${cacheKey}`
    715    );
    716    let reuseCanvas = false;
    717    let sizedCache = this.canvasCache.get(cacheKey);
    718    if (sizedCache.has(url)) {
    719      lazy.logger.debug(`screenshot ${url} taken from cache`);
    720      canvas = sizedCache.get(url);
    721      if (!cache) {
    722        sizedCache.delete(url);
    723      }
    724    } else {
    725      let canvasPool = sizedCache.get(null);
    726      if (canvasPool.length) {
    727        lazy.logger.debug("reusing canvas from canvas pool");
    728        canvas = canvasPool.pop();
    729      } else {
    730        lazy.logger.debug("using new canvas");
    731        canvas = null;
    732      }
    733      reuseCanvas = !cache;
    734 
    735      let ctxInterface = win.CanvasRenderingContext2D;
    736      let flags =
    737        ctxInterface.DRAWWINDOW_DRAW_CARET |
    738        ctxInterface.DRAWWINDOW_DRAW_VIEW |
    739        ctxInterface.DRAWWINDOW_USE_WIDGET_LAYERS;
    740 
    741      if (
    742        !(
    743          0 <= browserRect.left &&
    744          0 <= browserRect.top &&
    745          win.innerWidth >= browserRect.width &&
    746          win.innerHeight >= browserRect.height
    747        )
    748      ) {
    749        lazy.logger.error(`Invalid window dimensions:
    750 browserRect.left: ${browserRect.left}
    751 browserRect.top: ${browserRect.top}
    752 win.innerWidth: ${win.innerWidth}
    753 browserRect.width: ${browserRect.width}
    754 win.innerHeight: ${win.innerHeight}
    755 browserRect.height: ${browserRect.height}`);
    756        throw new Error("Window has incorrect dimensions");
    757      }
    758 
    759      url = new URL(url).href; // normalize the URL
    760 
    761      await this.loadTestUrl(win, url, timeout);
    762 
    763      canvas = await lazy.capture.canvas(
    764        win,
    765        win.docShell.browsingContext,
    766        0, // left
    767        0, // top
    768        browserRect.width,
    769        browserRect.height,
    770        { canvas, flags, readback: !this.useDrawSnapshot }
    771      );
    772    }
    773    if (
    774      canvas.width !== browserRect.width ||
    775      canvas.height !== browserRect.height
    776    ) {
    777      lazy.logger.warn(
    778        `Canvas dimensions changed to ${canvas.width}x${canvas.height}`
    779      );
    780      reuseCanvas = false;
    781      cache = false;
    782    }
    783    if (cache) {
    784      sizedCache.set(url, canvas);
    785    }
    786    this.urlCount.set(url, remainingCount - 1);
    787    return { canvas, reuseCanvas };
    788  }
    789 
    790  async screenshotPaginated(win, url, timeout, pageRanges) {
    791    url = new URL(url).href; // normalize the URL
    792    await this.loadTestUrl(win, url, timeout, false);
    793 
    794    const [width, height] = [DEFAULT_PAGE_WIDTH, DEFAULT_PAGE_HEIGHT];
    795    const margin = DEFAULT_PAGE_MARGIN;
    796    const settings = lazy.print.addDefaultSettings({
    797      page: {
    798        width,
    799        height,
    800      },
    801      margin: {
    802        left: margin,
    803        right: margin,
    804        top: margin,
    805        bottom: margin,
    806      },
    807      shrinkToFit: false,
    808      background: true,
    809    });
    810    const printSettings = lazy.print.getPrintSettings(settings);
    811 
    812    const binaryString = await lazy.print.printToBinaryString(
    813      win.gBrowser.browsingContext,
    814      printSettings
    815    );
    816 
    817    try {
    818      const pdf = await this.loadPdf(binaryString);
    819      let pages = this.getPages(pageRanges, url, pdf.numPages);
    820      return [this.renderPages(pdf, pages), pages.size];
    821    } catch (e) {
    822      lazy.logger.warn(`Loading of pdf failed`);
    823      throw e;
    824    }
    825  }
    826 
    827  async loadPdfJs() {
    828    // Ensure pdf.js is loaded in the opener window
    829    await new Promise((resolve, reject) => {
    830      const doc = this.parentWindow.document;
    831      const script = doc.createElement("script");
    832      script.type = "module";
    833      script.src = "resource://pdf.js/build/pdf.mjs";
    834      script.onload = resolve;
    835      script.onerror = () => reject(new Error("pdfjs load failed"));
    836      doc.documentElement.appendChild(script);
    837    });
    838    this.parentWindow.pdfjsLib.GlobalWorkerOptions.workerSrc =
    839      "resource://pdf.js/build/pdf.worker.mjs";
    840  }
    841 
    842  async loadPdf(data) {
    843    return this.parentWindow.pdfjsLib.getDocument({ data }).promise;
    844  }
    845 
    846  async *renderPages(pdf, pages) {
    847    let canvas = null;
    848    for (let pageNumber = 1; pageNumber <= pdf.numPages; pageNumber++) {
    849      if (!pages.has(pageNumber)) {
    850        lazy.logger.info(`Skipping page ${pageNumber}/${pdf.numPages}`);
    851        continue;
    852      }
    853      lazy.logger.info(`Rendering page ${pageNumber}/${pdf.numPages}`);
    854      let page = await pdf.getPage(pageNumber);
    855      let viewport = page.getViewport({ scale: DEFAULT_PDF_RESOLUTION });
    856      // Prepare canvas using PDF page dimensions
    857      if (canvas === null) {
    858        canvas = this.parentWindow.document.createElementNS(XHTML_NS, "canvas");
    859        canvas.height = viewport.height;
    860        canvas.width = viewport.width;
    861      }
    862 
    863      // Render PDF page into canvas context
    864      let context = canvas.getContext("2d");
    865      let renderContext = {
    866        canvasContext: context,
    867        viewport,
    868      };
    869      await page.render(renderContext).promise;
    870      yield { canvas, reuseCanvas: false };
    871    }
    872  }
    873 
    874  getPages(pageRanges, url, totalPages) {
    875    // Extract test id from URL without parsing
    876    let afterHost = url.slice(url.indexOf(":") + 3);
    877    afterHost = afterHost.slice(afterHost.indexOf("/"));
    878    const ranges = pageRanges[afterHost];
    879    let rv = new Set();
    880 
    881    if (!ranges) {
    882      for (let i = 1; i <= totalPages; i++) {
    883        rv.add(i);
    884      }
    885      return rv;
    886    }
    887 
    888    for (let rangePart of ranges) {
    889      if (rangePart.length === 1) {
    890        rv.add(rangePart[0]);
    891      } else {
    892        if (rangePart.length !== 2) {
    893          throw new Error(
    894            `Page ranges must be <int> or <int> '-' <int>, got ${rangePart}`
    895          );
    896        }
    897        let [lower, upper] = rangePart;
    898        if (lower === null) {
    899          lower = 1;
    900        }
    901        if (upper === null) {
    902          upper = totalPages;
    903        }
    904        for (let i = lower; i <= upper; i++) {
    905          rv.add(i);
    906        }
    907      }
    908    }
    909    return rv;
    910  }
    911 };
    912 
    913 class DefaultMap extends Map {
    914  constructor(iterable, defaultFactory) {
    915    super(iterable);
    916    this.defaultFactory = defaultFactory;
    917  }
    918 
    919  get(key) {
    920    if (this.has(key)) {
    921      return super.get(key);
    922    }
    923 
    924    let v = this.defaultFactory();
    925    this.set(key, v);
    926    return v;
    927  }
    928 }