1/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ 2/* vim: set sts=2 sw=2 et tw=80: */ 3/* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6"use strict"; 7 8var EXPORTED_SYMBOLS = ["ExtensionTestUtils"]; 9 10const { ActorManagerParent } = ChromeUtils.import( 11 "resource://gre/modules/ActorManagerParent.jsm" 12); 13const { ExtensionUtils } = ChromeUtils.import( 14 "resource://gre/modules/ExtensionUtils.jsm" 15); 16const { XPCOMUtils } = ChromeUtils.import( 17 "resource://gre/modules/XPCOMUtils.jsm" 18); 19const { AppConstants } = ChromeUtils.import( 20 "resource://gre/modules/AppConstants.jsm" 21); 22 23// Windowless browsers can create documents that rely on XUL Custom Elements: 24ChromeUtils.import("resource://gre/modules/CustomElementsListener.jsm", null); 25 26ChromeUtils.defineModuleGetter( 27 this, 28 "AddonManager", 29 "resource://gre/modules/AddonManager.jsm" 30); 31ChromeUtils.defineModuleGetter( 32 this, 33 "AddonTestUtils", 34 "resource://testing-common/AddonTestUtils.jsm" 35); 36ChromeUtils.defineModuleGetter( 37 this, 38 "ContentTask", 39 "resource://testing-common/ContentTask.jsm" 40); 41ChromeUtils.defineModuleGetter( 42 this, 43 "ExtensionTestCommon", 44 "resource://testing-common/ExtensionTestCommon.jsm" 45); 46ChromeUtils.defineModuleGetter( 47 this, 48 "FileUtils", 49 "resource://gre/modules/FileUtils.jsm" 50); 51ChromeUtils.defineModuleGetter( 52 this, 53 "MessageChannel", 54 "resource://gre/modules/MessageChannel.jsm" 55); 56ChromeUtils.defineModuleGetter( 57 this, 58 "Schemas", 59 "resource://gre/modules/Schemas.jsm" 60); 61ChromeUtils.defineModuleGetter( 62 this, 63 "Services", 64 "resource://gre/modules/Services.jsm" 65); 66ChromeUtils.defineModuleGetter( 67 this, 68 "TestUtils", 69 "resource://testing-common/TestUtils.jsm" 70); 71 72XPCOMUtils.defineLazyGetter(this, "Management", () => { 73 const { Management } = ChromeUtils.import( 74 "resource://gre/modules/Extension.jsm", 75 null 76 ); 77 return Management; 78}); 79 80Services.mm.loadFrameScript( 81 "chrome://global/content/browser-content.js", 82 true, 83 true 84); 85 86ActorManagerParent.flush(); 87 88/* exported ExtensionTestUtils */ 89 90const { promiseDocumentLoaded, promiseEvent, promiseObserved } = ExtensionUtils; 91 92var REMOTE_CONTENT_SCRIPTS = Services.prefs.getBoolPref( 93 "browser.tabs.remote.autostart", 94 false 95); 96 97let BASE_MANIFEST = Object.freeze({ 98 applications: Object.freeze({ 99 gecko: Object.freeze({ 100 id: "test@web.ext", 101 }), 102 }), 103 104 manifest_version: 2, 105 106 name: "name", 107 version: "0", 108}); 109 110function frameScript() { 111 const { MessageChannel } = ChromeUtils.import( 112 "resource://gre/modules/MessageChannel.jsm" 113 ); 114 const { Services } = ChromeUtils.import( 115 "resource://gre/modules/Services.jsm" 116 ); 117 118 Services.obs.notifyObservers(this, "tab-content-frameloader-created"); 119 120 const messageListener = { 121 async receiveMessage({ target, messageName, recipient, data, name }) { 122 /* globals content */ 123 let resp = await content.fetch(data.url, data.options); 124 return resp.text(); 125 }, 126 }; 127 MessageChannel.addListener(this, "Test:Fetch", messageListener); 128 129 // eslint-disable-next-line mozilla/balanced-listeners, no-undef 130 addEventListener( 131 "MozHeapMinimize", 132 () => { 133 Services.obs.notifyObservers(null, "memory-pressure", "heap-minimize"); 134 }, 135 true, 136 true 137 ); 138} 139 140let kungFuDeathGrip = new Set(); 141function promiseBrowserLoaded(browser, url, redirectUrl) { 142 url = url && Services.io.newURI(url); 143 redirectUrl = redirectUrl && Services.io.newURI(redirectUrl); 144 145 return new Promise(resolve => { 146 const listener = { 147 QueryInterface: ChromeUtils.generateQI([ 148 Ci.nsISupportsWeakReference, 149 Ci.nsIWebProgressListener, 150 ]), 151 152 onStateChange(webProgress, request, stateFlags, statusCode) { 153 request.QueryInterface(Ci.nsIChannel); 154 155 let requestURI = 156 request.originalURI || 157 webProgress.DOMWindow.document.documentURIObject; 158 if ( 159 webProgress.isTopLevel && 160 (url?.equals(requestURI) || redirectUrl?.equals(requestURI)) && 161 stateFlags & Ci.nsIWebProgressListener.STATE_STOP 162 ) { 163 resolve(); 164 kungFuDeathGrip.delete(listener); 165 browser.removeProgressListener(listener); 166 } 167 }, 168 }; 169 170 // addProgressListener only supports weak references, so we need to 171 // use one. But we also need to make sure it stays alive until we're 172 // done with it, so thunk away a strong reference to keep it alive. 173 kungFuDeathGrip.add(listener); 174 browser.addProgressListener( 175 listener, 176 Ci.nsIWebProgress.NOTIFY_STATE_WINDOW 177 ); 178 }); 179} 180 181class ContentPage { 182 constructor( 183 remote = REMOTE_CONTENT_SCRIPTS, 184 extension = null, 185 privateBrowsing = false, 186 userContextId = undefined 187 ) { 188 this.remote = remote; 189 this.extension = extension; 190 this.privateBrowsing = privateBrowsing; 191 this.userContextId = userContextId; 192 193 this.browserReady = this._initBrowser(); 194 } 195 196 async _initBrowser() { 197 this.windowlessBrowser = Services.appShell.createWindowlessBrowser(true); 198 199 if (this.privateBrowsing) { 200 let loadContext = this.windowlessBrowser.docShell.QueryInterface( 201 Ci.nsILoadContext 202 ); 203 loadContext.usePrivateBrowsing = true; 204 } 205 206 let system = Services.scriptSecurityManager.getSystemPrincipal(); 207 208 let chromeShell = this.windowlessBrowser.docShell.QueryInterface( 209 Ci.nsIWebNavigation 210 ); 211 212 chromeShell.createAboutBlankContentViewer(system, system); 213 this.windowlessBrowser.browsingContext.useGlobalHistory = false; 214 let loadURIOptions = { 215 triggeringPrincipal: system, 216 }; 217 chromeShell.loadURI( 218 "chrome://extensions/content/dummy.xhtml", 219 loadURIOptions 220 ); 221 222 await promiseObserved( 223 "chrome-document-global-created", 224 win => win.document == chromeShell.document 225 ); 226 227 let chromeDoc = await promiseDocumentLoaded(chromeShell.document); 228 229 let browser = chromeDoc.createXULElement("browser"); 230 browser.setAttribute("type", "content"); 231 browser.setAttribute("disableglobalhistory", "true"); 232 if (this.userContextId) { 233 browser.setAttribute("usercontextid", this.userContextId); 234 } 235 236 if (this.extension && this.extension.remote) { 237 this.remote = true; 238 browser.setAttribute("remote", "true"); 239 browser.setAttribute("remoteType", "extension"); 240 browser.sameProcessAsFrameLoader = this.extension.groupFrameLoader; 241 } 242 243 let awaitFrameLoader = Promise.resolve(); 244 if (this.remote) { 245 awaitFrameLoader = promiseEvent(browser, "XULFrameLoaderCreated"); 246 browser.setAttribute("remote", "true"); 247 } 248 249 chromeDoc.documentElement.appendChild(browser); 250 251 await awaitFrameLoader; 252 this.browser = browser; 253 254 this.loadFrameScript(frameScript); 255 256 return browser; 257 } 258 259 sendMessage(msg, data) { 260 return MessageChannel.sendMessage(this.browser.messageManager, msg, data); 261 } 262 263 loadFrameScript(func) { 264 let frameScript = `data:text/javascript,(${encodeURI(func)}).call(this)`; 265 this.browser.messageManager.loadFrameScript(frameScript, true, true); 266 } 267 268 addFrameScriptHelper(func) { 269 let frameScript = `data:text/javascript,${encodeURI(func)}`; 270 this.browser.messageManager.loadFrameScript(frameScript, false, true); 271 } 272 273 async loadURL(url, redirectUrl = undefined) { 274 await this.browserReady; 275 276 this.browser.loadURI(url, { 277 triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), 278 }); 279 return promiseBrowserLoaded(this.browser, url, redirectUrl); 280 } 281 282 async fetch(url, options) { 283 return this.sendMessage("Test:Fetch", { url, options }); 284 } 285 286 spawn(params, task) { 287 return ContentTask.spawn(this.browser, params, task); 288 } 289 290 async close() { 291 await this.browserReady; 292 293 let { messageManager } = this.browser; 294 295 this.browser = null; 296 297 this.windowlessBrowser.close(); 298 this.windowlessBrowser = null; 299 300 await TestUtils.topicObserved( 301 "message-manager-disconnect", 302 subject => subject === messageManager 303 ); 304 } 305} 306 307class ExtensionWrapper { 308 constructor(testScope, extension = null) { 309 this.testScope = testScope; 310 311 this.extension = null; 312 313 this.handleResult = this.handleResult.bind(this); 314 this.handleMessage = this.handleMessage.bind(this); 315 316 this.state = "uninitialized"; 317 318 this.testResolve = null; 319 this.testDone = new Promise(resolve => { 320 this.testResolve = resolve; 321 }); 322 323 this.messageHandler = new Map(); 324 this.messageAwaiter = new Map(); 325 326 this.messageQueue = new Set(); 327 328 this.testScope.registerCleanupFunction(() => { 329 this.clearMessageQueues(); 330 331 if (this.state == "pending" || this.state == "running") { 332 this.testScope.equal( 333 this.state, 334 "unloaded", 335 "Extension left running at test shutdown" 336 ); 337 return this.unload(); 338 } else if (this.state == "unloading") { 339 this.testScope.equal( 340 this.state, 341 "unloaded", 342 "Extension not fully unloaded at test shutdown" 343 ); 344 } 345 this.destroy(); 346 }); 347 348 if (extension) { 349 this.id = extension.id; 350 this.attachExtension(extension); 351 } 352 } 353 354 destroy() { 355 // This method should be implemented in subclasses which need to 356 // perform cleanup when destroyed. 357 } 358 359 attachExtension(extension) { 360 if (extension === this.extension) { 361 return; 362 } 363 364 if (this.extension) { 365 this.extension.off("test-eq", this.handleResult); 366 this.extension.off("test-log", this.handleResult); 367 this.extension.off("test-result", this.handleResult); 368 this.extension.off("test-done", this.handleResult); 369 this.extension.off("test-message", this.handleMessage); 370 this.clearMessageQueues(); 371 } 372 this.uuid = extension.uuid; 373 this.extension = extension; 374 375 extension.on("test-eq", this.handleResult); 376 extension.on("test-log", this.handleResult); 377 extension.on("test-result", this.handleResult); 378 extension.on("test-done", this.handleResult); 379 extension.on("test-message", this.handleMessage); 380 381 this.testScope.info(`Extension attached`); 382 } 383 384 clearMessageQueues() { 385 if (this.messageQueue.size) { 386 let names = Array.from(this.messageQueue, ([msg]) => msg); 387 this.testScope.equal( 388 JSON.stringify(names), 389 "[]", 390 "message queue is empty" 391 ); 392 this.messageQueue.clear(); 393 } 394 if (this.messageAwaiter.size) { 395 let names = Array.from(this.messageAwaiter.keys()); 396 this.testScope.equal( 397 JSON.stringify(names), 398 "[]", 399 "no tasks awaiting on messages" 400 ); 401 for (let promise of this.messageAwaiter.values()) { 402 promise.reject(); 403 } 404 this.messageAwaiter.clear(); 405 } 406 } 407 408 handleResult(kind, pass, msg, expected, actual) { 409 switch (kind) { 410 case "test-eq": 411 this.testScope.ok( 412 pass, 413 `${msg} - Expected: ${expected}, Actual: ${actual}` 414 ); 415 break; 416 417 case "test-log": 418 this.testScope.info(msg); 419 break; 420 421 case "test-result": 422 this.testScope.ok(pass, msg); 423 break; 424 425 case "test-done": 426 this.testScope.ok(pass, msg); 427 this.testResolve(msg); 428 break; 429 } 430 } 431 432 handleMessage(kind, msg, ...args) { 433 let handler = this.messageHandler.get(msg); 434 if (handler) { 435 handler(...args); 436 } else { 437 this.messageQueue.add([msg, ...args]); 438 this.checkMessages(); 439 } 440 } 441 442 awaitStartup() { 443 return this.startupPromise; 444 } 445 446 async startup() { 447 if (this.state != "uninitialized") { 448 throw new Error("Extension already started"); 449 } 450 this.state = "pending"; 451 452 await ExtensionTestCommon.setIncognitoOverride(this.extension); 453 454 this.startupPromise = this.extension.startup().then( 455 result => { 456 this.state = "running"; 457 458 return result; 459 }, 460 error => { 461 this.state = "failed"; 462 463 return Promise.reject(error); 464 } 465 ); 466 467 return this.startupPromise; 468 } 469 470 async unload() { 471 if (this.state != "running") { 472 throw new Error("Extension not running"); 473 } 474 this.state = "unloading"; 475 476 if (this.addonPromise) { 477 // If addonPromise is still pending resolution, wait for it to make sure 478 // that add-ons that are installed through the AddonManager are properly 479 // uninstalled. 480 await this.addonPromise; 481 } 482 483 if (this.addon) { 484 await this.addon.uninstall(); 485 } else { 486 await this.extension.shutdown(); 487 } 488 489 if (AppConstants.platform === "android") { 490 // We need a way to notify the embedding layer that an extension has been 491 // uninstalled, so that the java layer can be updated too. 492 Services.obs.notifyObservers( 493 null, 494 "testing-uninstalled-addon", 495 this.addon ? this.addon.id : this.extension.id 496 ); 497 } 498 499 this.state = "unloaded"; 500 } 501 502 /* 503 * This method marks the extension unloading without actually calling 504 * shutdown, since shutting down a MockExtension causes it to be uninstalled. 505 * 506 * Normally you shouldn't need to use this unless you need to test something 507 * that requires a restart, such as updates. 508 */ 509 markUnloaded() { 510 if (this.state != "running") { 511 throw new Error("Extension not running"); 512 } 513 this.state = "unloaded"; 514 515 return Promise.resolve(); 516 } 517 518 sendMessage(...args) { 519 this.extension.testMessage(...args); 520 } 521 522 awaitFinish(msg) { 523 return this.testDone.then(actual => { 524 if (msg) { 525 this.testScope.equal(actual, msg, "test result correct"); 526 } 527 return actual; 528 }); 529 } 530 531 checkMessages() { 532 for (let message of this.messageQueue) { 533 let [msg, ...args] = message; 534 535 let listener = this.messageAwaiter.get(msg); 536 if (listener) { 537 this.messageQueue.delete(message); 538 this.messageAwaiter.delete(msg); 539 540 listener.resolve(...args); 541 return; 542 } 543 } 544 } 545 546 checkDuplicateListeners(msg) { 547 if (this.messageHandler.has(msg) || this.messageAwaiter.has(msg)) { 548 throw new Error("only one message handler allowed"); 549 } 550 } 551 552 awaitMessage(msg) { 553 return new Promise((resolve, reject) => { 554 this.checkDuplicateListeners(msg); 555 556 this.messageAwaiter.set(msg, { resolve, reject }); 557 this.checkMessages(); 558 }); 559 } 560 561 onMessage(msg, callback) { 562 this.checkDuplicateListeners(msg); 563 this.messageHandler.set(msg, callback); 564 } 565} 566 567class AOMExtensionWrapper extends ExtensionWrapper { 568 constructor(testScope) { 569 super(testScope); 570 571 this.onEvent = this.onEvent.bind(this); 572 573 Management.on("ready", this.onEvent); 574 Management.on("shutdown", this.onEvent); 575 Management.on("startup", this.onEvent); 576 577 AddonTestUtils.on("addon-manager-shutdown", this.onEvent); 578 AddonTestUtils.on("addon-manager-started", this.onEvent); 579 580 AddonManager.addAddonListener(this); 581 } 582 583 destroy() { 584 this.id = null; 585 this.addon = null; 586 587 Management.off("ready", this.onEvent); 588 Management.off("shutdown", this.onEvent); 589 Management.off("startup", this.onEvent); 590 591 AddonTestUtils.off("addon-manager-shutdown", this.onEvent); 592 AddonTestUtils.off("addon-manager-started", this.onEvent); 593 594 AddonManager.removeAddonListener(this); 595 } 596 597 setRestarting() { 598 if (this.state !== "restarting") { 599 this.startupPromise = new Promise(resolve => { 600 this.resolveStartup = resolve; 601 }).then(async result => { 602 await this.addonPromise; 603 return result; 604 }); 605 } 606 this.state = "restarting"; 607 } 608 609 onEnabling(addon) { 610 if (addon.id === this.id) { 611 this.setRestarting(); 612 } 613 } 614 615 onInstalling(addon) { 616 if (addon.id === this.id) { 617 this.setRestarting(); 618 } 619 } 620 621 onInstalled(addon) { 622 if (addon.id === this.id) { 623 this.addon = addon; 624 } 625 } 626 627 onUninstalled(addon) { 628 if (addon.id === this.id) { 629 this.destroy(); 630 } 631 } 632 633 onEvent(kind, ...args) { 634 switch (kind) { 635 case "addon-manager-started": 636 if (this.state === "uninitialized") { 637 // startup() not called yet, ignore AddonManager startup notification. 638 return; 639 } 640 this.addonPromise = AddonManager.getAddonByID(this.id).then(addon => { 641 this.addon = addon; 642 this.addonPromise = null; 643 }); 644 // FALLTHROUGH 645 case "addon-manager-shutdown": 646 if (this.state === "uninitialized") { 647 return; 648 } 649 this.addon = null; 650 651 this.setRestarting(); 652 break; 653 654 case "startup": { 655 let [extension] = args; 656 657 this.maybeSetID(extension.rootURI, extension.id); 658 659 if (extension.id === this.id) { 660 this.attachExtension(extension); 661 this.state = "pending"; 662 } 663 break; 664 } 665 666 case "shutdown": { 667 let [extension] = args; 668 if (extension.id === this.id && this.state !== "restarting") { 669 this.state = "unloaded"; 670 } 671 break; 672 } 673 674 case "ready": { 675 let [extension] = args; 676 if (extension.id === this.id) { 677 this.state = "running"; 678 if (AppConstants.platform === "android") { 679 // We need a way to notify the embedding layer that a new extension 680 // has been installed, so that the java layer can be updated too. 681 Services.obs.notifyObservers( 682 null, 683 "testing-installed-addon", 684 extension.id 685 ); 686 } 687 this.resolveStartup(extension); 688 } 689 break; 690 } 691 } 692 } 693 694 async _flushCache() { 695 if (this.extension && this.extension.rootURI instanceof Ci.nsIJARURI) { 696 let file = this.extension.rootURI.JARFile.QueryInterface(Ci.nsIFileURL) 697 .file; 698 await Services.ppmm.broadcastAsyncMessage("Extension:FlushJarCache", { 699 path: file.path, 700 }); 701 } 702 } 703 704 get version() { 705 return this.addon && this.addon.version; 706 } 707 708 async unload() { 709 await this._flushCache(); 710 return super.unload(); 711 } 712 713 async upgrade(data) { 714 this.startupPromise = new Promise(resolve => { 715 this.resolveStartup = resolve; 716 }); 717 this.state = "restarting"; 718 719 await this._flushCache(); 720 721 let xpiFile = ExtensionTestCommon.generateXPI(data); 722 723 this.cleanupFiles.push(xpiFile); 724 725 return this._install(xpiFile); 726 } 727} 728 729class InstallableWrapper extends AOMExtensionWrapper { 730 constructor(testScope, xpiFile, addonData = {}) { 731 super(testScope); 732 733 this.file = xpiFile; 734 this.addonData = addonData; 735 this.installType = addonData.useAddonManager || "temporary"; 736 this.installTelemetryInfo = addonData.amInstallTelemetryInfo; 737 738 this.cleanupFiles = [xpiFile]; 739 } 740 741 destroy() { 742 super.destroy(); 743 744 for (let file of this.cleanupFiles.splice(0)) { 745 try { 746 Services.obs.notifyObservers(file, "flush-cache-entry"); 747 file.remove(false); 748 } catch (e) { 749 Cu.reportError(e); 750 } 751 } 752 } 753 754 maybeSetID(uri, id) { 755 if ( 756 !this.id && 757 uri instanceof Ci.nsIJARURI && 758 uri.JARFile.QueryInterface(Ci.nsIFileURL).file.equals(this.file) 759 ) { 760 this.id = id; 761 } 762 } 763 764 _setIncognitoOverride() { 765 // this.id is not set yet so grab it from the manifest data to set 766 // the incognito permission. 767 let { addonData } = this; 768 if (addonData && addonData.incognitoOverride) { 769 try { 770 let { id } = addonData.manifest.applications.gecko; 771 if (id) { 772 return ExtensionTestCommon.setIncognitoOverride({ id, addonData }); 773 } 774 } catch (e) {} 775 throw new Error( 776 "Extension ID is required for setting incognito permission." 777 ); 778 } 779 } 780 781 async _install(xpiFile) { 782 // Timing here is different than in MockExtension so we need to handle 783 // incognitoOverride early. 784 await this._setIncognitoOverride(); 785 786 if (this.installType === "temporary") { 787 return AddonManager.installTemporaryAddon(xpiFile) 788 .then(addon => { 789 this.id = addon.id; 790 this.addon = addon; 791 792 return this.startupPromise; 793 }) 794 .catch(e => { 795 this.state = "unloaded"; 796 return Promise.reject(e); 797 }); 798 } else if (this.installType === "permanent") { 799 return AddonManager.getInstallForFile( 800 xpiFile, 801 null, 802 this.installTelemetryInfo 803 ).then(install => { 804 let listener = { 805 onInstallFailed: () => { 806 this.state = "unloaded"; 807 this.resolveStartup(Promise.reject(new Error("Install failed"))); 808 }, 809 onInstallEnded: (install, newAddon) => { 810 this.id = newAddon.id; 811 this.addon = newAddon; 812 }, 813 }; 814 815 install.addListener(listener); 816 install.install(); 817 818 return this.startupPromise; 819 }); 820 } 821 } 822 823 startup() { 824 if (this.state != "uninitialized") { 825 throw new Error("Extension already started"); 826 } 827 828 this.state = "pending"; 829 this.startupPromise = new Promise(resolve => { 830 this.resolveStartup = resolve; 831 }); 832 833 return this._install(this.file); 834 } 835} 836 837class ExternallyInstalledWrapper extends AOMExtensionWrapper { 838 constructor(testScope, id) { 839 super(testScope); 840 841 this.id = id; 842 this.startupPromise = new Promise(resolve => { 843 this.resolveStartup = resolve; 844 }); 845 846 this.state = "restarting"; 847 } 848 849 maybeSetID(uri, id) {} 850} 851 852var ExtensionTestUtils = { 853 BASE_MANIFEST, 854 855 async normalizeManifest( 856 manifest, 857 manifestType = "manifest.WebExtensionManifest", 858 baseManifest = BASE_MANIFEST 859 ) { 860 await Management.lazyInit(); 861 862 let errors = []; 863 let context = { 864 url: null, 865 866 logError: error => { 867 errors.push(error); 868 }, 869 870 preprocessors: {}, 871 }; 872 873 manifest = Object.assign({}, baseManifest, manifest); 874 875 let normalized = Schemas.normalize(manifest, manifestType, context); 876 normalized.errors = errors; 877 878 return normalized; 879 }, 880 881 currentScope: null, 882 883 profileDir: null, 884 885 init(scope) { 886 this.currentScope = scope; 887 888 this.profileDir = scope.do_get_profile(); 889 890 this.fetchScopes = new Map(); 891 892 // We need to load at least one frame script into every message 893 // manager to ensure that the scriptable wrapper for its global gets 894 // created before we try to access it externally. If we don't, we 895 // fail sanity checks on debug builds the first time we try to 896 // create a wrapper, because we should never have a global without a 897 // cached wrapper. 898 Services.mm.loadFrameScript("data:text/javascript,//", true, true); 899 900 let tmpD = this.profileDir.clone(); 901 tmpD.append("tmp"); 902 tmpD.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); 903 904 let dirProvider = { 905 getFile(prop, persistent) { 906 persistent.value = false; 907 if (prop == "TmpD") { 908 return tmpD.clone(); 909 } 910 return null; 911 }, 912 913 QueryInterface: ChromeUtils.generateQI([Ci.nsIDirectoryServiceProvider]), 914 }; 915 Services.dirsvc.registerProvider(dirProvider); 916 917 scope.registerCleanupFunction(() => { 918 try { 919 tmpD.remove(true); 920 } catch (e) { 921 Cu.reportError(e); 922 } 923 Services.dirsvc.unregisterProvider(dirProvider); 924 925 this.currentScope = null; 926 927 return Promise.all( 928 Array.from(this.fetchScopes.values(), promise => 929 promise.then(scope => scope.close()) 930 ) 931 ); 932 }); 933 }, 934 935 addonManagerStarted: false, 936 937 mockAppInfo() { 938 AddonTestUtils.createAppInfo( 939 "xpcshell@tests.mozilla.org", 940 "XPCShell", 941 "48", 942 "48" 943 ); 944 }, 945 946 startAddonManager() { 947 if (this.addonManagerStarted) { 948 return; 949 } 950 this.addonManagerStarted = true; 951 this.mockAppInfo(); 952 953 return AddonTestUtils.promiseStartupManager(); 954 }, 955 956 loadExtension(data) { 957 if (data.useAddonManager) { 958 // If we're using incognitoOverride, we'll need to ensure 959 // an ID is available before generating the XPI. 960 if (data.incognitoOverride) { 961 ExtensionTestCommon.setExtensionID(data); 962 } 963 let xpiFile = ExtensionTestCommon.generateXPI(data); 964 965 return this.loadExtensionXPI(xpiFile, data); 966 } 967 968 let extension = ExtensionTestCommon.generate(data); 969 970 return new ExtensionWrapper(this.currentScope, extension); 971 }, 972 973 loadExtensionXPI(xpiFile, data) { 974 return new InstallableWrapper(this.currentScope, xpiFile, data); 975 }, 976 977 // Create a wrapper for a webextension that will be installed 978 // by some external process (e.g., Normandy) 979 expectExtension(id) { 980 return new ExternallyInstalledWrapper(this.currentScope, id); 981 }, 982 983 failOnSchemaWarnings(warningsAsErrors = true) { 984 let prefName = "extensions.webextensions.warnings-as-errors"; 985 Services.prefs.setBoolPref(prefName, warningsAsErrors); 986 if (!warningsAsErrors) { 987 this.currentScope.registerCleanupFunction(() => { 988 Services.prefs.setBoolPref(prefName, true); 989 }); 990 } 991 }, 992 993 get remoteContentScripts() { 994 return REMOTE_CONTENT_SCRIPTS; 995 }, 996 997 set remoteContentScripts(val) { 998 REMOTE_CONTENT_SCRIPTS = !!val; 999 }, 1000 1001 async fetch(origin, url, options) { 1002 let fetchScopePromise = this.fetchScopes.get(origin); 1003 if (!fetchScopePromise) { 1004 fetchScopePromise = this.loadContentPage(origin); 1005 this.fetchScopes.set(origin, fetchScopePromise); 1006 } 1007 1008 let fetchScope = await fetchScopePromise; 1009 return fetchScope.sendMessage("Test:Fetch", { url, options }); 1010 }, 1011 1012 /** 1013 * Loads a content page into a hidden docShell. 1014 * 1015 * @param {string} url 1016 * The URL to load. 1017 * @param {object} [options = {}] 1018 * @param {ExtensionWrapper} [options.extension] 1019 * If passed, load the URL as an extension page for the given 1020 * extension. 1021 * @param {boolean} [options.remote] 1022 * If true, load the URL in a content process. If false, load 1023 * it in the parent process. 1024 * @param {string} [options.redirectUrl] 1025 * An optional URL that the initial page is expected to 1026 * redirect to. 1027 * 1028 * @returns {ContentPage} 1029 */ 1030 loadContentPage( 1031 url, 1032 { 1033 extension = undefined, 1034 remote = undefined, 1035 redirectUrl = undefined, 1036 privateBrowsing = false, 1037 userContextId = undefined, 1038 } = {} 1039 ) { 1040 ContentTask.setTestScope(this.currentScope); 1041 1042 let contentPage = new ContentPage( 1043 remote, 1044 extension && extension.extension, 1045 privateBrowsing, 1046 userContextId 1047 ); 1048 1049 return contentPage.loadURL(url, redirectUrl).then(() => { 1050 return contentPage; 1051 }); 1052 }, 1053}; 1054