webextension-helpers.js (6180B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 /* globals browser */ 5 6 "use strict"; 7 8 /** 9 * Test helpers shared by the devtools server xpcshell tests related to webextensions. 10 */ 11 12 const { FileUtils } = ChromeUtils.importESModule( 13 "resource://gre/modules/FileUtils.sys.mjs" 14 ); 15 const { ExtensionTestUtils } = ChromeUtils.importESModule( 16 "resource://testing-common/ExtensionXPCShellUtils.sys.mjs" 17 ); 18 19 const { 20 CommandsFactory, 21 } = require("resource://devtools/shared/commands/commands-factory.js"); 22 23 /** 24 * Loads and starts up a test extension given the provided extension configuration. 25 * 26 * @param {object} extConfig - The extension configuration object 27 * @return {ExtensionWrapper} extension - Resolves with an extension object once the 28 * extension has started up. 29 */ 30 async function startupExtension(extConfig) { 31 const extension = ExtensionTestUtils.loadExtension(extConfig); 32 33 await extension.startup(); 34 35 return extension; 36 } 37 exports.startupExtension = startupExtension; 38 39 /** 40 * Initializes the extensionStorage actor for a given extension. This is effectively 41 * what happens when the addon storage panel is opened in the browser. 42 * 43 * @param {string} - id, The addon id 44 * @return {object} - Resolves with the DevTools "commands" objact and the extensionStorage 45 * resource/front. 46 */ 47 async function openAddonStoragePanel(id) { 48 const commands = await CommandsFactory.forAddon(id); 49 await commands.targetCommand.startListening(); 50 51 // Fetch the EXTENSION_STORAGE resource. 52 // Unfortunately, we can't use resourceCommand.waitForNextResource as it would destroy 53 // the actor by immediately unwatching for the resource type. 54 const extensionStorage = await new Promise(resolve => { 55 commands.resourceCommand.watchResources( 56 [commands.resourceCommand.TYPES.EXTENSION_STORAGE], 57 { 58 onAvailable(resources) { 59 resolve(resources[0]); 60 }, 61 } 62 ); 63 }); 64 65 return { commands, extensionStorage }; 66 } 67 exports.openAddonStoragePanel = openAddonStoragePanel; 68 69 /** 70 * Builds the extension configuration object passed into ExtensionTestUtils.loadExtension 71 * 72 * @param {object} options - Options, if any, to add to the configuration 73 * @param {Function} options.background - A function comprising the test extension's 74 * background script if provided 75 * @param {object} options.files - An object whose keys correspond to file names and 76 * values map to the file contents 77 * @param {object} options.manifest - An object representing the extension's manifest 78 * @return {object} - The extension configuration object 79 */ 80 function getExtensionConfig(options = {}) { 81 const { manifest, ...otherOptions } = options; 82 const baseConfig = { 83 manifest: { 84 ...manifest, 85 permissions: ["storage"], 86 }, 87 useAddonManager: "temporary", 88 }; 89 return { 90 ...baseConfig, 91 ...otherOptions, 92 }; 93 } 94 exports.getExtensionConfig = getExtensionConfig; 95 96 /** 97 * Shared files for a test extension that has no background page but adds storage 98 * items via a transient extension page in a tab 99 */ 100 const ext_no_bg = { 101 files: { 102 "extension_page_in_tab.html": `<!DOCTYPE html> 103 <html> 104 <head> 105 <meta charset="utf-8"> 106 </head> 107 <body> 108 <h1>Extension Page in a Tab</h1> 109 <script src="extension_page_in_tab.js"></script> 110 </body> 111 </html>`, 112 "extension_page_in_tab.js": extensionScriptWithMessageListener, 113 }, 114 }; 115 exports.ext_no_bg = ext_no_bg; 116 117 /** 118 * An extension script that can be used in any extension context (e.g. as a background 119 * script or as an extension page script loaded in a tab). 120 */ 121 async function extensionScriptWithMessageListener() { 122 let fireOnChanged = false; 123 browser.storage.onChanged.addListener(() => { 124 if (fireOnChanged) { 125 // Do not fire it again until explicitly requested again using the "storage-local-fireOnChanged" test message. 126 fireOnChanged = false; 127 browser.test.sendMessage("storage-local-onChanged"); 128 } 129 }); 130 131 browser.test.onMessage.addListener(async (msg, ...args) => { 132 let item = null; 133 switch (msg) { 134 case "storage-local-set": 135 await browser.storage.local.set(args[0]); 136 break; 137 case "storage-local-get": 138 item = await browser.storage.local.get(args[0]); 139 break; 140 case "storage-local-remove": 141 await browser.storage.local.remove(args[0]); 142 break; 143 case "storage-local-clear": 144 await browser.storage.local.clear(); 145 break; 146 case "storage-local-fireOnChanged": { 147 // Allow the storage.onChanged listener to send a test event 148 // message when onChanged is being fired. 149 fireOnChanged = true; 150 // Do not fire fireOnChanged:done. 151 return; 152 } 153 default: 154 browser.test.fail(`Unexpected test message: ${msg}`); 155 } 156 157 browser.test.sendMessage(`${msg}:done`, item); 158 }); 159 // window is available in background scripts 160 // eslint-disable-next-line no-undef 161 browser.test.sendMessage("extension-origin", window.location.origin); 162 } 163 exports.extensionScriptWithMessageListener = extensionScriptWithMessageListener; 164 165 /** 166 * Shutdown procedure common to all tasks. 167 * 168 * @param {object} extension - The test extension 169 * @param {object} commands - The web extension commands used by the DevTools to interact with the backend 170 */ 171 async function shutdown(extension, commands) { 172 if (commands) { 173 await commands.destroy(); 174 } 175 await extension.unload(); 176 } 177 exports.shutdown = shutdown; 178 179 /** 180 * Mocks the missing 'storage/permanent' directory needed by the "indexedDB" 181 * storage actor's 'populateStoresForHosts' method. This 182 * directory exists in a full browser i.e. mochitest. 183 */ 184 function createMissingIndexedDBDirs() { 185 const dir = Services.dirsvc.get("ProfD", Ci.nsIFile).clone(); 186 dir.append("storage"); 187 if (!dir.exists()) { 188 dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); 189 } 190 dir.append("permanent"); 191 if (!dir.exists()) { 192 dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); 193 } 194 195 return dir; 196 } 197 exports.createMissingIndexedDBDirs = createMissingIndexedDBDirs;