summaryrefslogtreecommitdiffstats
path: root/devtools/server/tests/xpcshell/webextension-helpers.js
blob: 46968f09e79d8ddbd0882e9f8ff8424fb5d31c2b (plain)
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
/* 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.importESModule(
  "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
);

const {
  CommandsFactory,
} = require("resource://devtools/shared/commands/commands-factory.js");

/**
 * 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 given 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 DevTools "commands" objact and the extensionStorage
 * resource/front.
 */
async function openAddonStoragePanel(id) {
  const commands = await CommandsFactory.forAddon(id);
  await commands.targetCommand.startListening();

  // Fetch the EXTENSION_STORAGE resource.
  // Unfortunately, we can't use resourceCommand.waitForNextResource as it would destroy
  // the actor by immediately unwatching for the resource type.
  const extensionStorage = await new Promise(resolve => {
    commands.resourceCommand.watchResources(
      [commands.resourceCommand.TYPES.EXTENSION_STORAGE],
      {
        onAvailable(resources) {
          resolve(resources[0]);
        },
      }
    );
  });

  return { commands, extensionStorage };
}
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} commands - The web extension commands used by the DevTools to interact with the backend
 */
async function shutdown(extension, commands) {
  if (commands) {
    await commands.destroy();
  }
  await extension.unload();
}
exports.shutdown = shutdown;

/**
 * Mocks the missing 'storage/permanent' directory needed by the "indexedDB"
 * storage actor's 'populateStoresForHosts' method. 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;