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