summaryrefslogtreecommitdiffstats
path: root/browser/base/content/test/webextensions/browser_extension_update_background.js
blob: b0a4a31439f868830dc8e68803ab28dac265dcc2 (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
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
const { AddonManagerPrivate } = ChromeUtils.importESModule(
  "resource://gre/modules/AddonManager.sys.mjs"
);

const { AddonTestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/AddonTestUtils.sys.mjs"
);

AddonTestUtils.initMochitest(this);
AddonTestUtils.hookAMTelemetryEvents();

const ID = "update2@tests.mozilla.org";
const ID_ICON = "update_icon2@tests.mozilla.org";
const ID_PERMS = "update_perms@tests.mozilla.org";
const ID_LEGACY = "legacy_update@tests.mozilla.org";
const FAKE_INSTALL_TELEMETRY_SOURCE = "fake-install-source";

requestLongerTimeout(2);

function promiseViewLoaded(tab, viewid) {
  let win = tab.linkedBrowser.contentWindow;
  if (
    win.gViewController &&
    !win.gViewController.isLoading &&
    win.gViewController.currentViewId == viewid
  ) {
    return Promise.resolve();
  }

  return waitAboutAddonsViewLoaded(win.document);
}

function getBadgeStatus() {
  let menuButton = document.getElementById("PanelUI-menu-button");
  return menuButton.getAttribute("badge-status");
}

// Set some prefs that apply to all the tests in this file
add_setup(async function () {
  await SpecialPowers.pushPrefEnv({
    set: [
      // We don't have pre-pinned certificates for the local mochitest server
      ["extensions.install.requireBuiltInCerts", false],
      ["extensions.update.requireBuiltInCerts", false],
    ],
  });

  // Navigate away from the initial page so that about:addons always
  // opens in a new tab during tests
  BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, "about:robots");
  await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);

  registerCleanupFunction(async function () {
    // Return to about:blank when we're done
    BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, "about:blank");
    await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
  });
});

// Helper function to test background updates.
async function backgroundUpdateTest(url, id, checkIconFn) {
  await SpecialPowers.pushPrefEnv({
    set: [
      // Turn on background updates
      ["extensions.update.enabled", true],

      // Point updates to the local mochitest server
      [
        "extensions.update.background.url",
        `${BASE}/browser_webext_update.json`,
      ],
    ],
  });

  // Install version 1.0 of the test extension
  let addon = await promiseInstallAddon(url, {
    source: FAKE_INSTALL_TELEMETRY_SOURCE,
  });
  let addonId = addon.id;

  ok(addon, "Addon was installed");
  is(getBadgeStatus(), "", "Should not start out with an addon alert badge");

  // Trigger an update check and wait for the update for this addon
  // to be downloaded.
  let updatePromise = promiseInstallEvent(addon, "onDownloadEnded");

  AddonManagerPrivate.backgroundUpdateCheck();
  await updatePromise;

  is(getBadgeStatus(), "addon-alert", "Should have addon alert badge");

  // Find the menu entry for the update
  await gCUITestUtils.openMainMenu();

  let addons = PanelUI.addonNotificationContainer;
  is(addons.children.length, 1, "Have a menu entry for the update");

  // Click the menu item
  let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:addons");
  let popupPromise = promisePopupNotificationShown("addon-webext-permissions");
  addons.children[0].click();

  // The click should hide the main menu. This is currently synchronous.
  ok(PanelUI.panel.state != "open", "Main menu is closed or closing.");

  // about:addons should load and go to the list of extensions
  let tab = await tabPromise;
  is(
    tab.linkedBrowser.currentURI.spec,
    "about:addons",
    "Browser is at about:addons"
  );

  const VIEW = "addons://list/extension";
  await promiseViewLoaded(tab, VIEW);
  let win = tab.linkedBrowser.contentWindow;
  ok(!win.gViewController.isLoading, "about:addons view is fully loaded");
  is(
    win.gViewController.currentViewId,
    VIEW,
    "about:addons is at extensions list"
  );

  // Wait for the permission prompt, check the contents
  let panel = await popupPromise;
  checkIconFn(panel.getAttribute("icon"));

  // The original extension has 1 promptable permission and the new one
  // has 2 (history and <all_urls>) plus 1 non-promptable permission (cookies).
  // So we should only see the 1 new promptable permission in the notification.
  let singlePermissionEl = document.getElementById(
    "addon-webext-perm-single-entry"
  );
  ok(!singlePermissionEl.hidden, "Single permission entry is not hidden");
  ok(singlePermissionEl.textContent, "Single permission entry text is set");

  // Cancel the update.
  panel.secondaryButton.click();

  addon = await AddonManager.getAddonByID(id);
  is(addon.version, "1.0", "Should still be running the old version");

  BrowserTestUtils.removeTab(tab);

  // Alert badge and hamburger menu items should be gone
  is(getBadgeStatus(), "", "Addon alert badge should be gone");

  await gCUITestUtils.openMainMenu();
  addons = PanelUI.addonNotificationContainer;
  is(addons.children.length, 0, "Update menu entries should be gone");
  await gCUITestUtils.hideMainMenu();

  // Re-check for an update
  updatePromise = promiseInstallEvent(addon, "onDownloadEnded");
  await AddonManagerPrivate.backgroundUpdateCheck();
  await updatePromise;

  is(getBadgeStatus(), "addon-alert", "Should have addon alert badge");

  // Find the menu entry for the update
  await gCUITestUtils.openMainMenu();

  addons = PanelUI.addonNotificationContainer;
  is(addons.children.length, 1, "Have a menu entry for the update");

  // Click the menu item
  tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:addons", true);
  popupPromise = promisePopupNotificationShown("addon-webext-permissions");

  addons.children[0].click();

  // Wait for about:addons to load
  tab = await tabPromise;
  is(tab.linkedBrowser.currentURI.spec, "about:addons");

  await promiseViewLoaded(tab, VIEW);
  win = tab.linkedBrowser.contentWindow;
  ok(!win.gViewController.isLoading, "about:addons view is fully loaded");
  is(
    win.gViewController.currentViewId,
    VIEW,
    "about:addons is at extensions list"
  );

  // Wait for the permission prompt and accept it this time
  updatePromise = waitForUpdate(addon);
  panel = await popupPromise;
  panel.button.click();

  addon = await updatePromise;
  is(addon.version, "2.0", "Should have upgraded to the new version");

  BrowserTestUtils.removeTab(tab);

  is(getBadgeStatus(), "", "Addon alert badge should be gone");

  await addon.uninstall();
  await SpecialPowers.popPrefEnv();

  // Test that the expected telemetry events have been recorded (and that they include the
  // permission_prompt event).
  const amEvents = AddonTestUtils.getAMTelemetryEvents();
  const updateEvents = amEvents
    .filter(evt => evt.method === "update")
    .map(evt => {
      delete evt.value;
      return evt;
    });

  Assert.deepEqual(
    updateEvents.map(evt => evt.extra && evt.extra.step),
    [
      // First update (cancelled).
      "started",
      "download_started",
      "download_completed",
      "permissions_prompt",
      "cancelled",
      // Second update (completed).
      "started",
      "download_started",
      "download_completed",
      "permissions_prompt",
      "completed",
    ],
    "Got the steps from the collected telemetry events"
  );

  const method = "update";
  const object = "extension";
  const baseExtra = {
    addon_id: addonId,
    source: FAKE_INSTALL_TELEMETRY_SOURCE,
    step: "permissions_prompt",
    updated_from: "app",
  };

  // Expect the telemetry events to have num_strings set to 1, as only the origin permissions is going
  // to be listed in the permission prompt.
  Assert.deepEqual(
    updateEvents.filter(
      evt => evt.extra && evt.extra.step === "permissions_prompt"
    ),
    [
      { method, object, extra: { ...baseExtra, num_strings: "1" } },
      { method, object, extra: { ...baseExtra, num_strings: "1" } },
    ],
    "Got the expected permission_prompts events"
  );
}

function checkDefaultIcon(icon) {
  is(
    icon,
    "chrome://mozapps/skin/extensions/extensionGeneric.svg",
    "Popup has the default extension icon"
  );
}

add_task(() =>
  backgroundUpdateTest(
    `${BASE}/browser_webext_update1.xpi`,
    ID,
    checkDefaultIcon
  )
);
function checkNonDefaultIcon(icon) {
  // The icon should come from the extension, don't bother with the precise
  // path, just make sure we've got a jar url pointing to the right path
  // inside the jar.
  ok(icon.startsWith("jar:file://"), "Icon is a jar url");
  ok(icon.endsWith("/icon.png"), "Icon is icon.png inside a jar");
}

add_task(() =>
  backgroundUpdateTest(
    `${BASE}/browser_webext_update_icon1.xpi`,
    ID_ICON,
    checkNonDefaultIcon
  )
);