1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
9 ChromeUtils.defineESModuleGetters(lazy, {
10 ExtensionData: "resource://gre/modules/Extension.sys.mjs",
11 ExtensionTestCommon: "resource://testing-common/ExtensionTestCommon.sys.mjs",
12 HiddenFrame: "resource://gre/modules/HiddenFrame.sys.mjs",
14 "resource://testing-common/PerTestCoverageUtils.sys.mjs",
15 ServiceWorkerCleanUp: "resource://gre/modules/ServiceWorkerCleanUp.sys.mjs",
17 "resource://testing-common/SpecialPowersSandbox.sys.mjs",
20 class SpecialPowersError extends Error {
22 return "SpecialPowersError";
27 [Ci.nsIPrefBranch.PREF_INVALID]: "INVALID",
28 [Ci.nsIPrefBranch.PREF_INT]: "INT",
29 [Ci.nsIPrefBranch.PREF_BOOL]: "BOOL",
30 [Ci.nsIPrefBranch.PREF_STRING]: "STRING",
36 // We share a single preference environment stack between all
37 // SpecialPowers instances, across all processes.
38 let prefUndoStack = [];
39 let inPrefEnvOp = false;
41 let permissionUndoStack = [];
43 function doPrefEnvOp(fn) {
46 "Reentrant preference environment operations not supported"
57 async function createWindowlessBrowser({ isPrivate = false } = {}) {
58 const { promiseDocumentLoaded, promiseEvent, promiseObserved } =
59 ChromeUtils.importESModule(
60 "resource://gre/modules/ExtensionUtils.sys.mjs"
63 let windowlessBrowser = Services.appShell.createWindowlessBrowser(true);
66 let loadContext = windowlessBrowser.docShell.QueryInterface(
69 loadContext.usePrivateBrowsing = true;
72 let chromeShell = windowlessBrowser.docShell.QueryInterface(
76 const system = Services.scriptSecurityManager.getSystemPrincipal();
77 chromeShell.createAboutBlankDocumentViewer(system, system);
78 windowlessBrowser.browsingContext.useGlobalHistory = false;
80 Services.io.newURI("chrome://extensions/content/dummy.xhtml"),
82 triggeringPrincipal: system,
86 await promiseObserved(
87 "chrome-document-global-created",
88 win => win.document == chromeShell.document
91 let chromeDoc = await promiseDocumentLoaded(chromeShell.document);
93 let browser = chromeDoc.createXULElement("browser");
94 browser.setAttribute("type", "content");
95 browser.setAttribute("disableglobalhistory", "true");
96 browser.setAttribute("remote", "true");
98 let promise = promiseEvent(browser, "XULFrameLoaderCreated");
99 chromeDoc.documentElement.appendChild(browser);
103 return { windowlessBrowser, browser };
106 // Supplies the unique IDs for tasks created by SpecialPowers.spawn(),
107 // used to bounce assertion messages back down to the correct child.
110 // The default actor to send assertions to if a task originated in a
111 // window without a test harness.
112 let defaultAssertHandler;
114 export class SpecialPowersParent extends JSWindowActorParent {
118 this._messageManager = Services.mm;
119 this._serviceWorkerListener = null;
121 this._observer = this.observe.bind(this);
123 this.didDestroy = this.uninit.bind(this);
125 this._registerObservers = {
129 if (!this._topics.includes(topic)) {
130 this._topics.push(topic);
131 Services.obs.addObserver(this, topic);
134 observe(aSubject, aTopic, aData) {
137 case "csp-on-violate-policy":
138 // the subject is either an nsIURI or an nsISupportsCString
140 if (aSubject instanceof Ci.nsIURI) {
141 subject = aSubject.asciiSpec;
142 } else if (aSubject instanceof Ci.nsISupportsCString) {
143 subject = aSubject.data;
145 throw new Error("Subject must be nsIURI or nsISupportsCString");
151 this._self.sendAsyncMessage("specialpowers-" + aTopic, msg);
153 case "xfo-on-violate-policy":
155 if (aSubject instanceof Ci.nsIURI) {
156 uriSpec = aSubject.asciiSpec;
158 throw new Error("Subject must be nsIURI");
164 this._self.sendAsyncMessage("specialpowers-" + aTopic, msg);
167 this._self.sendAsyncMessage("specialpowers-" + aTopic, msg);
172 this._basePrefs = null;
175 this._crashDumpDir = null;
176 this._processCrashObserversRegistered = false;
177 this._chromeScriptListeners = [];
178 this._extensions = new Map();
179 this._taskActors = new Map();
182 static registerActor() {
183 ChromeUtils.registerWindowActor("SpecialPowers", {
187 esModuleURI: "resource://testing-common/SpecialPowersChild.sys.mjs",
189 "chrome-document-global-created",
190 "content-document-global-created",
194 esModuleURI: "resource://testing-common/SpecialPowersParent.sys.mjs",
199 static unregisterActor() {
200 ChromeUtils.unregisterWindowActor("SpecialPowers");
204 Services.obs.addObserver(this._observer, "http-on-modify-request");
206 // We would like to check that tests don't leave service workers around
207 // after they finish, but that information lives in the parent process.
208 // Ideally, we'd be able to tell the child processes whenever service
209 // workers are registered or unregistered so they would know at all times,
210 // but service worker lifetimes are complicated enough to make that
211 // difficult. For the time being, let the child process know when a test
212 // registers a service worker so it can ask, synchronously, at the end if
213 // the service worker had unregister called on it.
214 let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
215 Ci.nsIServiceWorkerManager
218 this._serviceWorkerListener = {
227 swm.addListener(this._serviceWorkerListener);
229 this.getBaselinePrefs();
233 if (defaultAssertHandler === this) {
234 defaultAssertHandler = null;
237 var obs = Services.obs;
238 obs.removeObserver(this._observer, "http-on-modify-request");
239 this._registerObservers._topics.splice(0).forEach(element => {
240 obs.removeObserver(this._registerObservers, element);
242 this._removeProcessCrashObservers();
244 let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
245 Ci.nsIServiceWorkerManager
247 swm.removeListener(this._serviceWorkerListener);
250 observe(aSubject, aTopic, aData) {
251 function addDumpIDToMessage(propertyName) {
253 var id = aSubject.getPropertyAsAString(propertyName);
258 message.dumpIDs.push({ id, extension: "dmp" });
259 message.dumpIDs.push({ id, extension: "extra" });
264 case "http-on-modify-request":
265 if (aSubject instanceof Ci.nsIChannel) {
266 let uri = aSubject.URI.spec;
267 this.sendAsyncMessage("specialpowers-http-notify-request", { uri });
271 case "ipc:content-shutdown":
272 aSubject = aSubject.QueryInterface(Ci.nsIPropertyBag2);
273 if (!aSubject.hasKey("abnormal")) {
274 return; // This is a normal shutdown, ignore it
277 var message = { type: "crash-observed", dumpIDs: [] };
278 addDumpIDToMessage("dumpID");
279 this.sendAsyncMessage("SPProcessCrashService", message);
285 if (!this._crashDumpDir) {
286 this._crashDumpDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
287 this._crashDumpDir.append("minidumps");
289 return this._crashDumpDir;
292 _getPendingCrashDumpDir() {
293 if (!this._pendingCrashDumpDir) {
294 this._pendingCrashDumpDir = Services.dirsvc.get("UAppData", Ci.nsIFile);
295 this._pendingCrashDumpDir.append("Crash Reports");
296 this._pendingCrashDumpDir.append("pending");
298 return this._pendingCrashDumpDir;
301 _deleteCrashDumpFiles(aFilenames) {
302 var crashDumpDir = this._getCrashDumpDir();
303 if (!crashDumpDir.exists()) {
307 var success = !!aFilenames.length;
308 aFilenames.forEach(function (crashFilename) {
309 var file = crashDumpDir.clone();
310 file.append(crashFilename);
320 _findCrashDumpFiles(aToIgnore) {
321 var crashDumpDir = this._getCrashDumpDir();
322 var entries = crashDumpDir.exists() && crashDumpDir.directoryEntries;
327 var crashDumpFiles = [];
328 while (entries.hasMoreElements()) {
329 var file = entries.nextFile;
330 var path = String(file.path);
331 if (path.match(/\.(dmp|extra)$/) && !aToIgnore[path]) {
332 crashDumpFiles.push(path);
335 return crashDumpFiles.concat();
338 _deletePendingCrashDumpFiles() {
339 var crashDumpDir = this._getPendingCrashDumpDir();
341 if (crashDumpDir.exists()) {
342 let entries = crashDumpDir.directoryEntries;
343 while (entries.hasMoreElements()) {
344 let file = entries.nextFile;
354 _addProcessCrashObservers() {
355 if (this._processCrashObserversRegistered) {
359 Services.obs.addObserver(this._observer, "ipc:content-shutdown");
360 this._processCrashObserversRegistered = true;
363 _removeProcessCrashObservers() {
364 if (!this._processCrashObserversRegistered) {
368 Services.obs.removeObserver(this._observer, "ipc:content-shutdown");
369 this._processCrashObserversRegistered = false;
373 this.sendAsyncMessage("SPServiceWorkerRegistered", { registered: true });
377 return Services.io.newURI(url);
379 _notifyCategoryAndObservers(subject, topic, data) {
380 const serviceMarker = "service,";
382 // First create observers from the category manager.
386 for (let { value: contractID } of Services.catMan.enumerateCategory(
390 if (contractID.substring(0, serviceMarker.length) == serviceMarker) {
391 contractID = contractID.substring(serviceMarker.length);
392 factoryFunction = "getService";
394 factoryFunction = "createInstance";
398 let handler = Cc[contractID][factoryFunction]();
400 let observer = handler.QueryInterface(Ci.nsIObserver);
401 observers.push(observer);
406 // Next enumerate the registered observers.
407 for (let observer of Services.obs.enumerateObservers(topic)) {
408 if (observer instanceof Ci.nsIObserver && !observers.includes(observer)) {
409 observers.push(observer);
413 observers.forEach(function (observer) {
415 observer.observe(subject, topic, data);
421 Iterate through one atomic set of pref actions and perform sets/clears as appropriate.
422 All actions performed must modify the relevant pref.
424 Returns whether we need to wait for a refresh driver tick for the pref to
425 have effect. This is only needed for ui. and font. prefs, which affect the
426 look and feel code and have some change-coalescing going on.
428 _applyPrefs(actions) {
429 let requiresRefresh = false;
430 for (let pref of actions) {
431 // This logic should match PrefRequiresRefresh in reftest.sys.mjs
434 pref.name == "layout.css.prefers-color-scheme.content-override" ||
435 pref.name.startsWith("ui.") ||
436 pref.name.startsWith("browser.display.") ||
437 pref.name.startsWith("font.");
438 if (pref.action == "set") {
439 this._setPref(pref.name, pref.type, pref.value, pref.iid);
440 } else if (pref.action == "clear") {
441 Services.prefs.clearUserPref(pref.name);
444 return requiresRefresh;
448 * Take in a list of pref changes to make, pushes their current values
449 * onto the restore stack, and makes the changes. When the test
450 * finishes, these changes are reverted.
452 * |inPrefs| must be an object with up to two properties: "set" and "clear".
453 * pushPrefEnv will set prefs as indicated in |inPrefs.set| and will unset
454 * the prefs indicated in |inPrefs.clear|.
456 * For example, you might pass |inPrefs| as:
458 * inPrefs = {'set': [['foo.bar', 2], ['magic.pref', 'baz']],
459 * 'clear': [['clear.this'], ['also.this']] };
461 * Notice that |set| and |clear| are both an array of arrays. In |set|, each
462 * of the inner arrays must have the form [pref_name, value] or [pref_name,
463 * value, iid]. (The latter form is used for prefs with "complex" values.)
465 * In |clear|, each inner array should have the form [pref_name].
467 * If you set the same pref more than once (or both set and clear a pref),
468 * the behavior of this method is undefined.
470 pushPrefEnv(inPrefs) {
471 return doPrefEnvOp(() => {
472 let pendingActions = [];
473 let cleanupActions = [];
475 for (let [action, prefs] of Object.entries(inPrefs)) {
476 for (let pref of prefs) {
480 let type = PREF_TYPES[Services.prefs.getPrefType(name)];
481 let originalValue = null;
483 if (pref.length == 3) {
486 } else if (pref.length == 2) {
490 /* If pref is not found or invalid it doesn't exist. */
491 if (type !== "INVALID") {
493 (Services.prefs.prefHasUserValue(name) && action == "clear") ||
496 originalValue = this._getPref(name, type);
498 } else if (action == "set") {
499 /* name doesn't exist, so 'clear' is pointless */
505 if (type === "INVALID") {
506 type = PREF_TYPES[typeof value];
508 if (type === "INVALID") {
509 throw new Error("Unexpected preference type for " + name);
512 pendingActions.push({ action, type, name, value, iid });
514 /* Push original preference value or clear into cleanup array */
515 var cleanupTodo = { type, name, value: originalValue, iid };
516 if (originalValue == null) {
517 cleanupTodo.action = "clear";
519 cleanupTodo.action = "set";
521 cleanupActions.push(cleanupTodo);
525 prefUndoStack.push(cleanupActions);
526 let requiresRefresh = this._applyPrefs(pendingActions);
527 return { requiresRefresh };
532 return doPrefEnvOp(() => {
533 let env = prefUndoStack.pop();
535 let requiresRefresh = this._applyPrefs(env);
536 return { popped: true, requiresRefresh };
538 return { popped: false, requiresRefresh: false };
543 let requiresRefresh = false;
544 while (prefUndoStack.length) {
545 requiresRefresh |= this.popPrefEnv().requiresRefresh;
547 return { requiresRefresh };
550 _setPref(name, type, value, iid) {
553 return Services.prefs.setBoolPref(name, value);
555 return Services.prefs.setIntPref(name, value);
557 return Services.prefs.setCharPref(name, value);
559 return Services.prefs.setComplexValue(name, iid, value);
561 return Services.prefs.setStringPref(name, value);
563 switch (typeof value) {
565 return Services.prefs.setBoolPref(name, value);
567 return Services.prefs.setIntPref(name, value);
569 return Services.prefs.setStringPref(name, value);
572 `Unexpected preference type: ${type} for ${name} with value ${value} and type ${typeof value}`
576 _getPref(name, type, defaultValue, iid) {
579 if (defaultValue !== undefined) {
580 return Services.prefs.getBoolPref(name, defaultValue);
582 return Services.prefs.getBoolPref(name);
584 if (defaultValue !== undefined) {
585 return Services.prefs.getIntPref(name, defaultValue);
587 return Services.prefs.getIntPref(name);
589 if (defaultValue !== undefined) {
590 return Services.prefs.getCharPref(name, defaultValue);
592 return Services.prefs.getCharPref(name);
594 return Services.prefs.getComplexValue(name, iid);
596 if (defaultValue !== undefined) {
597 return Services.prefs.getStringPref(name, defaultValue);
599 return Services.prefs.getStringPref(name);
602 `Unexpected preference type: ${type} for preference ${name}`
607 this._basePrefs = this._getAllPreferences();
610 _comparePrefs(base, target, ignorePrefs, partialMatches) {
612 for (const [key, value] of base) {
613 if (ignorePrefs.includes(key)) {
616 let partialFind = false;
617 partialMatches.forEach(pm => {
618 if (key.startsWith(pm)) {
626 if (value === target.get(key)) {
629 if (!failures.includes(key)) {
636 comparePrefsToBaseline(ignorePrefs) {
637 let newPrefs = this._getAllPreferences();
639 // find all items in ignorePrefs that end in *, add to partialMatch
640 let partialMatch = [];
641 if (ignorePrefs === undefined) {
644 ignorePrefs.forEach(pref => {
645 if (pref.endsWith("*")) {
646 partialMatch.push(pref.split("*")[0]);
650 // find all new prefs different than old
651 let rv1 = this._comparePrefs(
658 // find all old prefs different than new (in case we delete)
659 let rv2 = this._comparePrefs(
666 let failures = [...new Set([...rv1, ...rv2])];
669 failures.forEach(f => {
670 if (this._basePrefs.get(f)) {
673 PREF_TYPES[Services.prefs.getPrefType(f)],
674 this._basePrefs.get(f)
677 Services.prefs.clearUserPref(f);
681 if (ignorePrefs.length > 1) {
687 _getAllPreferences() {
688 let names = new Map();
689 for (let prefName of Services.prefs.getChildList("")) {
690 let prefType = PREF_TYPES[Services.prefs.getPrefType(prefName)];
691 let prefValue = this._getPref(prefName, prefType);
692 names.set(prefName, prefValue);
697 _toggleMuteAudio(aMuted) {
698 let browser = this.browsingContext.top.embedderElement;
709 Services.perms.addFromPrincipal(
718 Services.perms.removeFromPrincipal(perm.principal, perm.type);
721 throw new Error(`Unexpected permission op: ${perm.op}`);
725 pushPermissions(inPermissions) {
726 let pendingPermissions = [];
727 let cleanupPermissions = [];
729 for (let permission of inPermissions) {
730 let { principal } = permission;
731 if (principal.isSystemPrincipal) {
735 let originalValue = Services.perms.testPermissionFromPrincipal(
740 let perm = permission.allow;
741 if (typeof perm === "boolean") {
742 perm = Ci.nsIPermissionManager[perm ? "ALLOW_ACTION" : "DENY_ACTION"];
745 if (permission.remove) {
746 perm = Ci.nsIPermissionManager.UNKNOWN_ACTION;
749 if (originalValue == perm) {
755 type: permission.type,
760 typeof permission.expireType === "number" ? permission.expireType : 0, // default: EXPIRE_NEVER
762 typeof permission.expireTime === "number" ? permission.expireTime : 0,
765 var cleanupTodo = Object.assign({}, todo);
767 if (permission.remove) {
771 pendingPermissions.push(todo);
773 if (originalValue == Ci.nsIPermissionManager.UNKNOWN_ACTION) {
774 cleanupTodo.op = "remove";
776 cleanupTodo.value = originalValue;
777 cleanupTodo.permission = originalValue;
779 cleanupPermissions.push(cleanupTodo);
782 permissionUndoStack.push(cleanupPermissions);
784 for (let perm of pendingPermissions) {
790 if (permissionUndoStack.length) {
791 for (let perm of permissionUndoStack.pop()) {
798 while (permissionUndoStack.length) {
799 this.popPermissions();
803 _spawnChrome(task, args, caller, imports) {
804 let sb = new lazy.SpecialPowersSandbox(
807 this.sendAsyncMessage("Assert", data);
812 for (let [global, prop] of Object.entries({
813 windowGlobalParent: "manager",
814 browsingContext: "browsingContext",
816 Object.defineProperty(sb.sandbox, global, {
824 return sb.execute(task, args, caller);
828 * messageManager callback function
829 * This will get requests from our API in the window and process them in chrome for it
831 // eslint-disable-next-line complexity
832 async receiveMessage(aMessage) {
833 let startTime = Cu.now();
834 // Try block so we can use a finally statement to add a profiler marker
835 // despite all the return statements.
837 // We explicitly return values in the below code so that this function
838 // doesn't trigger a flurry of warnings about "does not always return
840 switch (aMessage.name) {
841 case "SPToggleMuteAudio":
842 return this._toggleMuteAudio(aMessage.data.mute);
847 case "SpecialPowers.Quit":
849 !AppConstants.RELEASE_OR_BETA &&
850 !AppConstants.DEBUG &&
851 !AppConstants.MOZ_CODE_COVERAGE &&
852 !AppConstants.ASAN &&
855 if (Services.profiler.IsActive()) {
856 let filename = Services.env.get("MOZ_PROFILER_SHUTDOWN");
858 await Services.profiler.dumpProfileToFileAsync(filename);
859 await Services.profiler.StopProfiler();
862 Cu.exitIfInAutomation();
864 Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
869 let bc = aMessage.data.browsingContext;
870 // Send a message to the child telling it to focus the window.
871 // If the message responds with a browsing context, then
872 // a child browsing context in a subframe should be focused.
873 // Iterate until nothing is returned and we get to the most
874 // deeply nested subframe that should be focused.
876 let spParent = bc.currentWindowGlobal.getActor("SpecialPowers");
878 bc = await spParent.sendQuery("EnsureFocus", {
879 blurSubframe: aMessage.data.blurSubframe,
882 } while (bc && !aMessage.data.blurSubframe);
885 case "SpecialPowers.Focus":
886 if (this.manager.rootFrameLoader) {
887 this.manager.rootFrameLoader.ownerElement.focus();
891 case "SpecialPowers.CreateFiles":
892 return (async () => {
894 if (!this._createdFiles) {
895 this._createdFiles = [];
897 let createdFiles = this._createdFiles;
900 aMessage.data.forEach(function (request) {
901 const filePerms = 0o666;
902 let testFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
904 testFile.appendRelativePath(request.name);
906 testFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, filePerms);
909 "@mozilla.org/network/file-output-stream;1"
910 ].createInstance(Ci.nsIFileOutputStream);
913 0x02 | 0x08 | 0x20, // PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE
918 outStream.write(request.data, request.data.length);
922 File.createFromFileName(testFile.path, request.options).then(
924 filePaths.push(file);
928 createdFiles.push(testFile);
931 await Promise.all(promises);
935 return Promise.reject(String(e));
938 case "SpecialPowers.RemoveFiles":
939 if (this._createdFiles) {
940 this._createdFiles.forEach(function (testFile) {
942 testFile.remove(false);
945 this._createdFiles = null;
952 case "EvictAllDocumentViewers":
953 this.browsingContext.top.sessionHistory.evictAllDocumentViewers();
956 case "getBaselinePrefs":
957 return this.getBaselinePrefs();
959 case "comparePrefsToBaseline":
960 return this.comparePrefsToBaseline(aMessage.data);
963 return this.pushPrefEnv(aMessage.data);
966 return this.popPrefEnv();
969 return this.flushPrefEnv();
971 case "PushPermissions":
972 return this.pushPermissions(aMessage.data);
974 case "PopPermissions":
975 return this.popPermissions();
977 case "FlushPermissions":
978 return this.flushPermissions();
980 case "SPPrefService": {
981 let prefs = Services.prefs;
982 let prefType = aMessage.json.prefType.toUpperCase();
983 let { prefName, prefValue, iid, defaultValue } = aMessage.json;
985 if (aMessage.json.op == "get") {
986 if (!prefName || !prefType) {
987 throw new SpecialPowersError(
988 "Invalid parameters for get in SPPrefService"
992 // return null if the pref doesn't exist
994 defaultValue === undefined &&
995 prefs.getPrefType(prefName) == prefs.PREF_INVALID
999 return this._getPref(prefName, prefType, defaultValue, iid);
1000 } else if (aMessage.json.op == "set") {
1001 if (!prefName || !prefType || prefValue === undefined) {
1002 throw new SpecialPowersError(
1003 "Invalid parameters for set in SPPrefService"
1007 return this._setPref(prefName, prefType, prefValue, iid);
1008 } else if (aMessage.json.op == "clear") {
1010 throw new SpecialPowersError(
1011 "Invalid parameters for clear in SPPrefService"
1015 prefs.clearUserPref(prefName);
1017 throw new SpecialPowersError("Invalid operation for SPPrefService");
1020 return undefined; // See comment at the beginning of this function.
1023 case "SPProcessCrashService": {
1024 switch (aMessage.json.op) {
1025 case "register-observer":
1026 this._addProcessCrashObservers();
1028 case "unregister-observer":
1029 this._removeProcessCrashObservers();
1031 case "delete-crash-dump-files":
1032 return this._deleteCrashDumpFiles(aMessage.json.filenames);
1033 case "find-crash-dump-files":
1034 return this._findCrashDumpFiles(
1035 aMessage.json.crashDumpFilesToIgnore
1037 case "delete-pending-crash-dump-files":
1038 return this._deletePendingCrashDumpFiles();
1040 throw new SpecialPowersError(
1041 "Invalid operation for SPProcessCrashService"
1044 return undefined; // See comment at the beginning of this function.
1047 case "SPProcessCrashManagerWait": {
1048 let promises = aMessage.json.crashIds.map(crashId => {
1049 return Services.crashmanager.ensureCrashIsPresent(crashId);
1051 return Promise.all(promises);
1054 case "SPPermissionManager": {
1055 let msg = aMessage.data;
1062 let hasPerm = Services.perms.testPermissionFromPrincipal(
1066 return hasPerm == Ci.nsIPermissionManager.ALLOW_ACTION;
1068 let testPerm = Services.perms.testPermissionFromPrincipal(
1072 return testPerm == msg.value;
1074 throw new SpecialPowersError(
1075 "Invalid operation for SPPermissionManager"
1078 return undefined; // See comment at the beginning of this function.
1081 case "SPObserverService": {
1082 let topic = aMessage.json.observerTopic;
1083 switch (aMessage.json.op) {
1085 let data = aMessage.json.observerData;
1086 Services.obs.notifyObservers(null, topic, data);
1089 this._registerObservers._add(topic);
1092 throw new SpecialPowersError(
1093 "Invalid operation for SPObserverervice"
1096 return undefined; // See comment at the beginning of this function.
1099 case "SPLoadChromeScript": {
1100 let id = aMessage.json.id;
1103 let jsScript = aMessage.json.function.body;
1104 if (aMessage.json.url) {
1105 scriptName = aMessage.json.url;
1106 } else if (aMessage.json.function) {
1108 aMessage.json.function.name ||
1109 "<loadChromeScript anonymous function>";
1111 throw new SpecialPowersError("SPLoadChromeScript: Invalid script");
1114 // Setup a chrome sandbox that has access to sendAsyncMessage
1115 // and {add,remove}MessageListener in order to communicate with
1117 let sb = new lazy.SpecialPowersSandbox(
1120 this.sendAsyncMessage("Assert", data);
1125 Object.assign(sb.sandbox, {
1126 createWindowlessBrowser,
1127 sendAsyncMessage: (name, message) => {
1128 this.sendAsyncMessage("SPChromeScriptMessage", {
1134 addMessageListener: (name, listener) => {
1135 this._chromeScriptListeners.push({ id, name, listener });
1137 removeMessageListener: (name, listener) => {
1138 let index = this._chromeScriptListeners.findIndex(function (obj) {
1140 obj.id == id && obj.name == name && obj.listener == listener
1144 this._chromeScriptListeners.splice(index, 1);
1147 actorParent: this.manager,
1150 // Evaluate the chrome script
1152 Cu.evalInSandbox(jsScript, sb.sandbox, "1.8", scriptName, 1);
1154 throw new SpecialPowersError(
1155 "Error while executing chrome script '" +
1165 return undefined; // See comment at the beginning of this function.
1168 case "SPChromeScriptMessage": {
1169 let id = aMessage.json.id;
1170 let name = aMessage.json.name;
1171 let message = aMessage.json.message;
1173 for (let listener of this._chromeScriptListeners) {
1174 if (listener.name == name && listener.id == id) {
1175 result = listener.listener(message);
1181 case "SPCleanUpSTSData": {
1182 let origin = aMessage.data.origin;
1183 let uri = Services.io.newURI(origin);
1184 let sss = Cc["@mozilla.org/ssservice;1"].getService(
1185 Ci.nsISiteSecurityService
1187 sss.resetState(uri);
1191 case "SPRequestDumpCoverageCounters": {
1192 return lazy.PerTestCoverageUtils.afterTest();
1195 case "SPRequestResetCoverageCounters": {
1196 return lazy.PerTestCoverageUtils.beforeTest();
1199 case "SPCheckServiceWorkers": {
1200 let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
1201 Ci.nsIServiceWorkerManager
1203 let regs = swm.getAllRegistrations();
1205 // XXX This code is shared with specialpowers.js.
1206 let workers = new Array(regs.length);
1207 for (let i = 0; i < regs.length; ++i) {
1208 let { scope, scriptSpec } = regs.queryElementAt(
1210 Ci.nsIServiceWorkerRegistrationInfo
1212 workers[i] = { scope, scriptSpec };
1217 case "SPLoadExtension": {
1218 let id = aMessage.data.id;
1219 let ext = aMessage.data.ext;
1220 if (AppConstants.platform === "android") {
1221 // Some extension APIs are partially implemented in Java, and the
1222 // interface between the JS and Java side (GeckoViewWebExtension)
1223 // expects extensions to be registered with the AddonManager.
1225 // For simplicity, default to using an Addon Manager (if not null).
1226 if (ext.useAddonManager === undefined) {
1227 ext.useAddonManager = "android-only";
1230 // delayedStartup is only supported in xpcshell
1231 if (ext.delayedStartup !== undefined) {
1233 `delayedStartup is only supported in xpcshell, use "useAddonManager".`
1237 let extension = lazy.ExtensionTestCommon.generate(ext);
1239 let resultListener = (...args) => {
1240 this.sendAsyncMessage("SPExtensionMessage", {
1247 let messageListener = (...args) => {
1249 this.sendAsyncMessage("SPExtensionMessage", {
1251 type: "testMessage",
1256 // Register pass/fail handlers.
1257 extension.on("test-result", resultListener);
1258 extension.on("test-eq", resultListener);
1259 extension.on("test-log", resultListener);
1260 extension.on("test-done", resultListener);
1262 extension.on("test-message", messageListener);
1264 this._extensions.set(id, extension);
1268 case "SPStartupExtension": {
1269 let id = aMessage.data.id;
1270 // This is either an Extension, or (if useAddonManager is set) a MockExtension.
1271 let extension = this._extensions.get(id);
1272 extension.on("startup", (eventName, ext) => {
1273 if (AppConstants.platform === "android") {
1274 // We need a way to notify the embedding layer that a new extension
1275 // has been installed, so that the java layer can be updated too.
1276 Services.obs.notifyObservers(null, "testing-installed-addon", id);
1278 // ext is always the "real" Extension object, even when "extension"
1279 // is a MockExtension.
1280 this.sendAsyncMessage("SPExtensionMessage", {
1282 type: "extensionSetId",
1283 args: [ext.id, ext.uuid],
1287 // Make sure the extension passes the packaging checks when
1288 // they're run on a bare archive rather than a running instance,
1289 // as the add-on manager runs them.
1290 let extensionData = new lazy.ExtensionData(extension.rootURI);
1291 return extensionData
1295 return extensionData.initAllLocales().then(() => {
1296 if (extensionData.errors.length) {
1297 return Promise.reject(
1298 "Extension contains packaging errors"
1305 // loadManifest() will throw if we're loading an embedded
1306 // extension, so don't worry about locale errors in that
1311 // browser tests do not call startup in ExtensionXPCShellUtils or MockExtension,
1312 // in that case we have an ID here and we need to set the override.
1314 await lazy.ExtensionTestCommon.setIncognitoOverride(extension);
1316 return extension.startup().then(
1319 dump(`Extension startup failed: ${e}\n${e.stack}`);
1326 case "SPExtensionMessage": {
1327 let id = aMessage.data.id;
1328 let extension = this._extensions.get(id);
1329 extension.testMessage(...aMessage.data.args);
1333 case "SPExtensionGrantActiveTab": {
1334 let { id, tabId } = aMessage.data;
1335 let { tabManager } = this._extensions.get(id);
1336 tabManager.addActiveTabPermission(tabManager.get(tabId).nativeTab);
1340 case "SPUnloadExtension": {
1341 let id = aMessage.data.id;
1342 let extension = this._extensions.get(id);
1343 this._extensions.delete(id);
1344 return extension.shutdown().then(() => {
1345 return extension._uninstallPromise;
1349 case "SPExtensionTerminateBackground": {
1350 let id = aMessage.data.id;
1351 let args = aMessage.data.args;
1352 let extension = this._extensions.get(id);
1353 return extension.terminateBackground(...args);
1356 case "SPExtensionWakeupBackground": {
1357 let id = aMessage.data.id;
1358 let extension = this._extensions.get(id);
1359 return extension.wakeupBackground();
1362 case "SetAsDefaultAssertHandler": {
1363 defaultAssertHandler = this;
1368 // Use a different variable for the profiler marker start time
1369 // so that a marker isn't added when we return, but instead when
1370 // our promise resolves.
1371 let spawnStartTime = startTime;
1372 startTime = undefined;
1373 let { browsingContext, task, args, caller, hasHarness, imports } =
1377 browsingContext.currentWindowGlobal.getActor("SpecialPowers");
1379 let taskId = nextTaskID++;
1381 spParent._taskActors.set(taskId, this);
1385 .sendQuery("Spawn", { task, args, caller, taskId, imports })
1387 ChromeUtils.addProfilerMarker(
1389 { startTime: spawnStartTime, category: "Test" },
1392 return spParent._taskActors.delete(taskId);
1396 case "SpawnChrome": {
1397 let { task, args, caller, imports } = aMessage.data;
1399 return this._spawnChrome(task, args, caller, imports);
1403 let { browsingContext, rect, background, resetScrollPosition } =
1406 return browsingContext.currentWindowGlobal
1407 .drawSnapshot(rect, 1.0, background, resetScrollPosition)
1408 .then(async image => {
1409 let hiddenFrame = new lazy.HiddenFrame();
1410 let win = await hiddenFrame.get();
1412 let canvas = win.document.createElement("canvas");
1413 canvas.width = image.width;
1414 canvas.height = image.height;
1416 const ctx = canvas.getContext("2d");
1417 ctx.drawImage(image, 0, 0);
1419 let data = ctx.getImageData(0, 0, image.width, image.height);
1420 hiddenFrame.destroy();
1425 case "SecurityState": {
1426 let { browsingContext } = aMessage.data;
1427 return browsingContext.secureBrowserUI.state;
1430 case "ProxiedAssert": {
1431 let { taskId, data } = aMessage.data;
1433 let actor = this._taskActors.get(taskId) || defaultAssertHandler;
1434 actor.sendAsyncMessage("Assert", data);
1439 case "SPRemoveAllServiceWorkers": {
1440 return lazy.ServiceWorkerCleanUp.removeAll();
1443 case "SPRemoveServiceWorkerDataForExampleDomain": {
1444 return lazy.ServiceWorkerCleanUp.removeFromHost("example.com");
1447 case "SPGenerateMediaControlKeyTestEvent": {
1448 // eslint-disable-next-line no-undef
1449 MediaControlService.generateMediaControlKey(aMessage.data.event);
1454 throw new SpecialPowersError(
1455 `Unrecognized Special Powers API: ${aMessage.name}`
1458 // This should be unreachable. If it ever becomes reachable, ESLint
1459 // will produce an error about inconsistent return values.
1462 ChromeUtils.addProfilerMarker(
1464 { startTime, category: "Test" },