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 }