summaryrefslogtreecommitdiffstats
path: root/devtools/server/tests/xpcshell/test_webext_apis.js
blob: 5a2f2b990ad1e26652bd7bf2f6a0bb83b11528ec (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
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

const { AddonManager } = ChromeUtils.importESModule(
  "resource://gre/modules/AddonManager.sys.mjs"
);
const { ExtensionTestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
);

const DistinctDevToolsServer = getDistinctDevToolsServer();
ExtensionTestUtils.init(this);

add_setup(async () => {
  Services.prefs.setBoolPref("extensions.blocklist.enabled", false);
  await startupAddonsManager();
});

// Basic request wrapper that sends a request and resolves on the next packet.
// Will only work for very basic scenarios, without events emitted on the server
// etc...
async function sendRequest(transport, request) {
  return new Promise(resolve => {
    transport.hooks = {
      onPacket: packet => {
        dump(`received packet: ${JSON.stringify(packet)}\n`);
        // Let's resolve only when we get a packet that is related to our
        // request. It is needed because some methods do not return the correct
        // response right away. This is the case of the `reload` method, which
        // receives a `addonListChanged` message first and then a `reload`
        // message.
        if (packet.from === request.to) {
          resolve(packet);
        }
      },
    };
    transport.send(request);
  });
}

// If this test case fails, please reach out to webext peers because
// https://github.com/mozilla/web-ext relies on the APIs tested here.
add_task(async function test_webext_run_apis() {
  DistinctDevToolsServer.init();
  DistinctDevToolsServer.registerAllActors();

  const transport = DistinctDevToolsServer.connectPipe();

  // After calling connectPipe, the root actor will be created on the server
  // and a packet will be emitted after a tick. Wait for the initial packet.
  await new Promise(resolve => {
    transport.hooks = { onPacket: resolve };
  });

  const getRootResponse = await sendRequest(transport, {
    to: "root",
    type: "getRoot",
  });

  ok(getRootResponse, "received a response after calling RootActor::getRoot");
  ok(getRootResponse.addonsActor, "getRoot returned an addonsActor id");

  // installTemporaryAddon
  const addonId = "test-addons-actor@mozilla.org";
  const addonPath = getFilePath("addons/web-extension", false, true);
  const promiseStarted = AddonTestUtils.promiseWebExtensionStartup(addonId);
  const { addon } = await sendRequest(transport, {
    to: getRootResponse.addonsActor,
    type: "installTemporaryAddon",
    addonPath,
    // The openDevTools parameter is not always passed by web-ext. This test
    // omits it, to make sure that the request without the flag is accepted.
    // openDevTools: false,
  });
  await promiseStarted;

  ok(addon, "addonsActor allows to install a temporary add-on");
  equal(addon.id, addonId, "temporary add-on is the expected one");
  equal(addon.actor, false, "temporary add-on does not have an actor");

  // listAddons
  let { addons } = await sendRequest(transport, {
    to: "root",
    type: "listAddons",
  });
  ok(Array.isArray(addons), "listAddons() returns a list of add-ons");
  equal(addons.length, 1, "expected an add-on installed");

  const installedAddon = addons[0];
  equal(installedAddon.id, addonId, "installed add-on is the expected one");
  ok(installedAddon.actor, "returned add-on has an actor");

  // reload
  const promiseReloaded = AddonTestUtils.promiseAddonEvent("onInstalled");
  const promiseRestarted = AddonTestUtils.promiseWebExtensionStartup(addonId);
  await sendRequest(transport, {
    to: installedAddon.actor,
    type: "reload",
  });
  await Promise.all([promiseReloaded, promiseRestarted]);

  // uninstallAddon
  const promiseUninstalled = new Promise(resolve => {
    const listener = {};
    listener.onUninstalled = uninstalledAddon => {
      if (uninstalledAddon.id == addonId) {
        AddonManager.removeAddonListener(listener);
        resolve();
      }
    };
    AddonManager.addAddonListener(listener);
  });
  await sendRequest(transport, {
    to: getRootResponse.addonsActor,
    type: "uninstallAddon",
    addonId,
  });
  await promiseUninstalled;

  ({ addons } = await sendRequest(transport, {
    to: "root",
    type: "listAddons",
  }));
  equal(addons.length, 0, "expected no add-on installed");

  // Attempt to uninstall an add-on that is (no longer) installed.
  let error = await sendRequest(transport, {
    to: getRootResponse.addonsActor,
    type: "uninstallAddon",
    addonId,
  });
  equal(
    error?.message,
    `Could not uninstall add-on "${addonId}"`,
    "expected error"
  );

  // Attempt to uninstall a non-temporarily loaded extension, which we do not
  // allow at the moment. We start by loading an extension, then we call the
  // `uninstallAddon`.
  const id = "not-a-temporary@extension";
  const extension = ExtensionTestUtils.loadExtension({
    manifest: {
      browser_specific_settings: { gecko: { id } },
    },
    useAddonManager: "permanent",
  });
  await extension.startup();

  error = await sendRequest(transport, {
    to: getRootResponse.addonsActor,
    type: "uninstallAddon",
    addonId: id,
  });
  equal(error?.message, `Could not uninstall add-on "${id}"`, "expected error");

  await extension.unload();

  transport.close();
});