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