From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../test/browser/browser_html_updates.js | 743 +++++++++++++++++++++ 1 file changed, 743 insertions(+) create mode 100644 toolkit/mozapps/extensions/test/browser/browser_html_updates.js (limited to 'toolkit/mozapps/extensions/test/browser/browser_html_updates.js') diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_updates.js b/toolkit/mozapps/extensions/test/browser/browser_html_updates.js new file mode 100644 index 0000000000..33dba886cd --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_html_updates.js @@ -0,0 +1,743 @@ +/* eslint max-len: ["error", 80] */ + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +AddonTestUtils.initMochitest(this); + +const server = AddonTestUtils.createHttpServer(); + +const initialAutoUpdate = AddonManager.autoUpdateDefault; +registerCleanupFunction(() => { + AddonManager.autoUpdateDefault = initialAutoUpdate; +}); + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.checkUpdateSecurity", false]], + }); + + Services.telemetry.clearEvents(); + registerCleanupFunction(() => { + cleanupPendingNotifications(); + }); +}); + +function loadDetailView(win, id) { + let doc = win.document; + let card = doc.querySelector(`addon-card[addon-id="${id}"]`); + let loaded = waitForViewLoad(win); + EventUtils.synthesizeMouseAtCenter(card, { clickCount: 1 }, win); + return loaded; +} + +add_task(async function testChangeAutoUpdates() { + let id = "test@mochi.test"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "Test", + browser_specific_settings: { gecko: { id } }, + }, + // Use permanent so the add-on can be updated. + useAddonManager: "permanent", + }); + + await extension.startup(); + let addon = await AddonManager.getAddonByID(id); + + let win = await loadInitialView("extension"); + let doc = win.document; + + let getInputs = updateRow => ({ + default: updatesRow.querySelector('input[value="1"]'), + on: updatesRow.querySelector('input[value="2"]'), + off: updatesRow.querySelector('input[value="0"]'), + checkForUpdate: updatesRow.querySelector('[action="update-check"]'), + }); + + await loadDetailView(win, id); + + let card = doc.querySelector(`addon-card[addon-id="${id}"]`); + ok(card.querySelector("addon-details"), "The card now has details"); + + let updatesRow = card.querySelector(".addon-detail-row-updates"); + let inputs = getInputs(updatesRow); + is(addon.applyBackgroundUpdates, 1, "Default is set"); + ok(inputs.default.checked, "The default option is selected"); + ok(inputs.checkForUpdate.hidden, "Update check is hidden"); + + inputs.on.click(); + is(addon.applyBackgroundUpdates, "2", "Updates are now enabled"); + ok(inputs.on.checked, "The on option is selected"); + ok(inputs.checkForUpdate.hidden, "Update check is hidden"); + + inputs.off.click(); + is(addon.applyBackgroundUpdates, "0", "Updates are now disabled"); + ok(inputs.off.checked, "The off option is selected"); + ok(!inputs.checkForUpdate.hidden, "Update check is visible"); + + // Go back to the list view and check the details view again. + let loaded = waitForViewLoad(win); + doc.querySelector(".back-button").click(); + await loaded; + + // Load the detail view again. + await loadDetailView(win, id); + + card = doc.querySelector(`addon-card[addon-id="${id}"]`); + updatesRow = card.querySelector(".addon-detail-row-updates"); + inputs = getInputs(updatesRow); + + ok(inputs.off.checked, "Off is still selected"); + + // Disable global updates. + let updated = BrowserTestUtils.waitForEvent(card, "update"); + AddonManager.autoUpdateDefault = false; + await updated; + + // Updates are still the same. + is(addon.applyBackgroundUpdates, "0", "Updates are now disabled"); + ok(inputs.off.checked, "The off option is selected"); + ok(!inputs.checkForUpdate.hidden, "Update check is visible"); + + // Check default. + inputs.default.click(); + is(addon.applyBackgroundUpdates, "1", "Default is set"); + ok(inputs.default.checked, "The default option is selected"); + ok(!inputs.checkForUpdate.hidden, "Update check is visible"); + + inputs.on.click(); + is(addon.applyBackgroundUpdates, "2", "Updates are now enabled"); + ok(inputs.on.checked, "The on option is selected"); + ok(inputs.checkForUpdate.hidden, "Update check is hidden"); + + // Enable updates again. + updated = BrowserTestUtils.waitForEvent(card, "update"); + AddonManager.autoUpdateDefault = true; + await updated; + + await closeView(win); + await extension.unload(); +}); + +async function setupExtensionWithUpdate( + id, + { releaseNotes, cancelUpdate } = {} +) { + let serverHost = `http://localhost:${server.identity.primaryPort}`; + let updatesPath = `/ext-updates-${id}.json`; + + let baseManifest = { + name: "Updates", + icons: { 48: "an-icon.png" }, + browser_specific_settings: { + gecko: { + id, + update_url: serverHost + updatesPath, + }, + }, + }; + + let updateXpi = AddonTestUtils.createTempWebExtensionFile({ + manifest: { + ...baseManifest, + version: "2", + // Include a permission in the updated extension, to make + // sure that we trigger the permission prompt as expected + // (and that we can accept or cancel the update by observing + // the underlying observerService notification). + permissions: ["http://*.example.com/*"], + }, + }); + + let releaseNotesExtra = {}; + if (releaseNotes) { + let notesPath = "/notes.txt"; + server.registerPathHandler(notesPath, (request, response) => { + if (releaseNotes == "ERROR") { + response.setStatusLine(null, 404, "Not Found"); + } else { + response.setStatusLine(null, 200, "OK"); + response.write(releaseNotes); + } + response.processAsync(); + response.finish(); + }); + releaseNotesExtra.update_info_url = serverHost + notesPath; + } + + let xpiFilename = `/update-${id}.xpi`; + server.registerFile(xpiFilename, updateXpi); + AddonTestUtils.registerJSON(server, updatesPath, { + addons: { + [id]: { + updates: [ + { + version: "2", + update_link: serverHost + xpiFilename, + ...releaseNotesExtra, + }, + ], + }, + }, + }); + + handlePermissionPrompt({ addonId: id, reject: cancelUpdate }); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + ...baseManifest, + version: "1", + }, + // Use permanent so the add-on can be updated. + useAddonManager: "permanent", + }); + await extension.startup(); + return extension; +} + +function disableAutoUpdates(card) { + // Check button should be hidden. + let updateCheckButton = card.querySelector('button[action="update-check"]'); + ok(updateCheckButton.hidden, "The button is initially hidden"); + + // Disable updates, update check button is now visible. + card.querySelector('input[name="autoupdate"][value="0"]').click(); + ok(!updateCheckButton.hidden, "The button is now visible"); + + // There shouldn't be an update shown to the user. + assertUpdateState({ card, shown: false }); +} + +function checkForUpdate(card, expected) { + let updateCheckButton = card.querySelector('button[action="update-check"]'); + let updateFound = BrowserTestUtils.waitForEvent(card, expected); + updateCheckButton.click(); + return updateFound; +} + +function installUpdate(card, expected) { + // Install the update. + let updateInstalled = BrowserTestUtils.waitForEvent(card, expected); + let updated = BrowserTestUtils.waitForEvent(card, "update"); + card.querySelector('panel-item[action="install-update"]').click(); + return Promise.all([updateInstalled, updated]); +} + +async function findUpdatesForAddonId(id) { + let addon = await AddonManager.getAddonByID(id); + await new Promise(resolve => { + addon.findUpdates( + { onUpdateAvailable: resolve }, + AddonManager.UPDATE_WHEN_USER_REQUESTED + ); + }); +} + +function assertUpdateState({ + card, + shown, + expanded = true, + releaseNotes = false, +}) { + let menuButton = card.querySelector(".more-options-button"); + ok( + menuButton.classList.contains("more-options-button-badged") == shown, + "The menu button is badged" + ); + let installButton = card.querySelector('panel-item[action="install-update"]'); + ok( + installButton.hidden != shown, + `The install button is ${shown ? "hidden" : "shown"}` + ); + if (expanded) { + let updateCheckButton = card.querySelector('button[action="update-check"]'); + ok( + updateCheckButton.hidden == shown, + `The update check button is ${shown ? "hidden" : "shown"}` + ); + + let { tabGroup } = card.details; + is(tabGroup.hidden, false, "The tab group is shown"); + let notesBtn = tabGroup.querySelector('[name="release-notes"]'); + is( + notesBtn.hidden, + !releaseNotes, + `The release notes button is ${releaseNotes ? "shown" : "hidden"}` + ); + } +} + +add_task(async function testUpdateAvailable() { + let id = "update@mochi.test"; + let extension = await setupExtensionWithUpdate(id); + + let win = await loadInitialView("extension"); + let doc = win.document; + + await loadDetailView(win, id); + + let card = doc.querySelector("addon-card"); + + // Disable updates and then check. + disableAutoUpdates(card); + await checkForUpdate(card, "update-found"); + + // There should now be an update. + assertUpdateState({ card, shown: true }); + + // The version was 1. + let versionRow = card.querySelector(".addon-detail-row-version"); + is(versionRow.lastChild.textContent, "1", "The version started as 1"); + + await installUpdate(card, "update-installed"); + + // The version is now 2. + versionRow = card.querySelector(".addon-detail-row-version"); + is(versionRow.lastChild.textContent, "2", "The version has updated"); + + // No update is shown again. + assertUpdateState({ card, shown: false }); + + // Check for updates again, there shouldn't be an update. + await checkForUpdate(card, "no-update"); + + await closeView(win); + await extension.unload(); +}); + +add_task(async function testReleaseNotesLoad() { + Services.telemetry.clearEvents(); + let id = "update-with-notes@mochi.test"; + let extension = await setupExtensionWithUpdate(id, { + releaseNotes: ` + + + + +

My release notes

+ + + Go somewhere + + + `, + }); + + let win = await loadInitialView("extension"); + let doc = win.document; + + await loadDetailView(win, id); + + let card = doc.querySelector("addon-card"); + let { deck, tabGroup } = card.details; + + // Disable updates and then check. + disableAutoUpdates(card); + await checkForUpdate(card, "update-found"); + + // There should now be an update. + assertUpdateState({ card, shown: true, releaseNotes: true }); + + info("Check release notes"); + let notesBtn = tabGroup.querySelector('[name="release-notes"]'); + let notes = card.querySelector("update-release-notes"); + let loading = BrowserTestUtils.waitForEvent(notes, "release-notes-loading"); + let loaded = BrowserTestUtils.waitForEvent(notes, "release-notes-loaded"); + // Don't use notesBtn.click() since it causes an assertion to fail. + // See bug 1551621 for more info. + EventUtils.synthesizeMouseAtCenter(notesBtn, {}, win); + await loading; + is( + doc.l10n.getAttributes(notes.firstElementChild).id, + "release-notes-loading", + "The loading message is shown" + ); + await loaded; + info("Checking HTML release notes"); + let [h1, ul, a] = notes.children; + is(h1.tagName, "H1", "There's a heading"); + is(h1.textContent, "My release notes", "The heading has content"); + is(ul.tagName, "UL", "There's a list"); + is(ul.children.length, 1, "There's one item in the list"); + let [li] = ul.children; + is(li.tagName, "LI", "There's a list item"); + is(li.textContent, "A thing", "The text is set"); + ok(!li.hasAttribute("onclick"), "The onclick was removed"); + ok(!notes.querySelector("link"), "The link tag was removed"); + ok(!notes.querySelector("script"), "The script tag was removed"); + is(a.textContent, "Go somewhere", "The link text is preserved"); + is(a.href, "http://example.com/", "The link href is preserved"); + + info("Verify the link opened in a new tab"); + let tabOpened = BrowserTestUtils.waitForNewTab(gBrowser, a.href); + a.click(); + let tab = await tabOpened; + BrowserTestUtils.removeTab(tab); + + let originalContent = notes.innerHTML; + + info("Switch away and back to release notes"); + // Load details view. + let detailsBtn = tabGroup.querySelector('.tab-button[name="details"]'); + let viewChanged = BrowserTestUtils.waitForEvent(deck, "view-changed"); + detailsBtn.click(); + await viewChanged; + + // Load release notes again, verify they weren't loaded. + viewChanged = BrowserTestUtils.waitForEvent(deck, "view-changed"); + let notesCached = BrowserTestUtils.waitForEvent( + notes, + "release-notes-cached" + ); + notesBtn.click(); + await viewChanged; + await notesCached; + is(notes.innerHTML, originalContent, "The content didn't change"); + + info("Install the update to clean it up"); + await installUpdate(card, "update-installed"); + + // There's no more update but release notes are still shown. + assertUpdateState({ card, shown: false, releaseNotes: true }); + + await closeView(win); + await extension.unload(); +}); + +add_task(async function testReleaseNotesError() { + let id = "update-with-notes-error@mochi.test"; + let extension = await setupExtensionWithUpdate(id, { releaseNotes: "ERROR" }); + + let win = await loadInitialView("extension"); + let doc = win.document; + + await loadDetailView(win, id); + + let card = doc.querySelector("addon-card"); + let { deck, tabGroup } = card.details; + + // Disable updates and then check. + disableAutoUpdates(card); + await checkForUpdate(card, "update-found"); + + // There should now be an update. + assertUpdateState({ card, shown: true, releaseNotes: true }); + + info("Check release notes"); + let notesBtn = tabGroup.querySelector('[name="release-notes"]'); + let notes = card.querySelector("update-release-notes"); + let loading = BrowserTestUtils.waitForEvent(notes, "release-notes-loading"); + let errored = BrowserTestUtils.waitForEvent(notes, "release-notes-error"); + // Don't use notesBtn.click() since it causes an assertion to fail. + // See bug 1551621 for more info. + EventUtils.synthesizeMouseAtCenter(notesBtn, {}, win); + await loading; + is( + doc.l10n.getAttributes(notes.firstElementChild).id, + "release-notes-loading", + "The loading message is shown" + ); + await errored; + is( + doc.l10n.getAttributes(notes.firstElementChild).id, + "release-notes-error", + "The error message is shown" + ); + + info("Switch away and back to release notes"); + // Load details view. + let detailsBtn = tabGroup.querySelector('.tab-button[name="details"]'); + let viewChanged = BrowserTestUtils.waitForEvent(deck, "view-changed"); + detailsBtn.click(); + await viewChanged; + + // Load release notes again, verify they weren't loaded. + viewChanged = BrowserTestUtils.waitForEvent(deck, "view-changed"); + let notesCached = BrowserTestUtils.waitForEvent( + notes, + "release-notes-cached" + ); + notesBtn.click(); + await viewChanged; + await notesCached; + + info("Install the update to clean it up"); + await installUpdate(card, "update-installed"); + + await closeView(win); + await extension.unload(); +}); + +add_task(async function testUpdateCancelled() { + let id = "update@mochi.test"; + let extension = await setupExtensionWithUpdate(id, { cancelUpdate: true }); + + let win = await loadInitialView("extension"); + let doc = win.document; + + await loadDetailView(win, "update@mochi.test"); + let card = doc.querySelector("addon-card"); + + // Disable updates and then check. + disableAutoUpdates(card); + await checkForUpdate(card, "update-found"); + + // There should now be an update. + assertUpdateState({ card, shown: true }); + + // The add-on starts as version 1. + let versionRow = card.querySelector(".addon-detail-row-version"); + is(versionRow.lastChild.textContent, "1", "The version started as 1"); + + // Force the install to be cancelled. + let install = card.updateInstall; + ok(install, "There was an install found"); + + await installUpdate(card, "update-cancelled"); + + // The add-on is still version 1. + versionRow = card.querySelector(".addon-detail-row-version"); + is(versionRow.lastChild.textContent, "1", "The version hasn't changed"); + + // The update has been removed. + assertUpdateState({ card, shown: false }); + + await closeView(win); + await extension.unload(); +}); + +add_task(async function testAvailableUpdates() { + let ids = ["update1@mochi.test", "update2@mochi.test", "update3@mochi.test"]; + let addons = await Promise.all(ids.map(id => setupExtensionWithUpdate(id))); + + // Disable global add-on updates. + AddonManager.autoUpdateDefault = false; + + let win = await loadInitialView("extension"); + let doc = win.document; + let updatesMessage = doc.getElementById("updates-message"); + let categoryUtils = new CategoryUtilities(win); + + let availableCat = categoryUtils.get("available-updates"); + + ok(availableCat.hidden, "Available updates is hidden"); + is(availableCat.badgeCount, 0, "There are no updates"); + ok(updatesMessage, "There is an updates message"); + is_element_hidden(updatesMessage, "The message is hidden"); + ok(!updatesMessage.message.textContent, "The message is empty"); + ok(!updatesMessage.button.textContent, "The button is empty"); + + // Check for all updates. + let updatesFound = TestUtils.topicObserved("EM-update-check-finished"); + doc.querySelector('#page-options [action="check-for-updates"]').click(); + + is_element_visible(updatesMessage, "The message is visible"); + ok(!updatesMessage.message.textContent, "The message is empty"); + ok(updatesMessage.button.hidden, "The view updates button is hidden"); + + // Make sure the message gets populated by fluent. + await TestUtils.waitForCondition( + () => updatesMessage.message.textContent, + "wait for message text" + ); + + await updatesFound; + + // The button should be visible, and should get some text from fluent. + ok(!updatesMessage.button.hidden, "The view updates button is visible"); + await TestUtils.waitForCondition( + () => updatesMessage.button.textContent, + "wait for button text" + ); + + // Wait for the available updates count to finalize, it's async. + await BrowserTestUtils.waitForCondition(() => availableCat.badgeCount == 3); + + // The category shows the correct update count. + ok(!availableCat.hidden, "Available updates is visible"); + is(availableCat.badgeCount, 3, "There are 3 updates"); + + // Go to the available updates page. + let loaded = waitForViewLoad(win); + availableCat.click(); + await loaded; + + // Check the updates are shown. + let cards = doc.querySelectorAll("addon-card"); + is(cards.length, 3, "There are 3 cards"); + + // Each card should have an update. + for (let card of cards) { + assertUpdateState({ card, shown: true, expanded: false }); + } + + // Check the detail page for the first add-on. + await loadDetailView(win, ids[0]); + is( + categoryUtils.getSelectedViewId(), + "addons://list/extension", + "The extensions category is selected" + ); + + // Go back to the last view. + loaded = waitForViewLoad(win); + doc.querySelector(".back-button").click(); + await loaded; + + // We're back on the updates view. + is( + categoryUtils.getSelectedViewId(), + "addons://updates/available", + "The available updates category is selected" + ); + + // Find the cards again. + cards = doc.querySelectorAll("addon-card"); + is(cards.length, 3, "There are 3 cards"); + + // Install the first update. + await installUpdate(cards[0], "update-installed"); + assertUpdateState({ card: cards[0], shown: false, expanded: false }); + + // The count goes down but the card stays. + is(availableCat.badgeCount, 2, "There are only 2 updates now"); + is( + doc.querySelectorAll("addon-card").length, + 3, + "All 3 cards are still visible on the updates page" + ); + + // Install the other two updates. + await installUpdate(cards[1], "update-installed"); + assertUpdateState({ card: cards[1], shown: false, expanded: false }); + await installUpdate(cards[2], "update-installed"); + assertUpdateState({ card: cards[2], shown: false, expanded: false }); + + // The count goes down but the card stays. + is(availableCat.badgeCount, 0, "There are no more updates"); + is( + doc.querySelectorAll("addon-card").length, + 3, + "All 3 cards are still visible on the updates page" + ); + + // Enable global add-on updates again. + AddonManager.autoUpdateDefault = true; + + await closeView(win); + await Promise.all(addons.map(addon => addon.unload())); +}); + +add_task(async function testUpdatesShownOnLoad() { + let id = "has-update@mochi.test"; + let addon = await setupExtensionWithUpdate(id); + + // Find the update for our addon. + AddonManager.autoUpdateDefault = false; + await findUpdatesForAddonId(id); + + let win = await loadInitialView("extension"); + let categoryUtils = new CategoryUtilities(win); + let updatesButton = categoryUtils.get("available-updates"); + + ok(!updatesButton.hidden, "The updates button is shown"); + is(updatesButton.badgeCount, 1, "There is an update"); + + let loaded = waitForViewLoad(win); + updatesButton.click(); + await loaded; + + let cards = win.document.querySelectorAll("addon-card"); + + is(cards.length, 1, "There is one update card"); + + let card = cards[0]; + is(card.addon.id, id, "The update is for the expected add-on"); + + await installUpdate(card, "update-installed"); + + ok(!updatesButton.hidden, "The updates button is still shown"); + is(updatesButton.badgeCount, 0, "There are no more updates"); + + info("Check that the updates section is hidden when re-opened"); + await closeView(win); + win = await loadInitialView("extension"); + categoryUtils = new CategoryUtilities(win); + updatesButton = categoryUtils.get("available-updates"); + + ok(updatesButton.hidden, "Available updates is hidden"); + is(updatesButton.badgeCount, 0, "There are no updates"); + + AddonManager.autoUpdateDefault = true; + await closeView(win); + await addon.unload(); +}); + +add_task(async function testPromptOnBackgroundUpdateCheck() { + const id = "test-prompt-on-background-check@mochi.test"; + const extension = await setupExtensionWithUpdate(id); + + AddonManager.autoUpdateDefault = false; + + const addon = await AddonManager.getAddonByID(id); + await AddonTestUtils.promiseFindAddonUpdates( + addon, + AddonManager.UPDATE_WHEN_PERIODIC_UPDATE + ); + let win = await loadInitialView("extension"); + + let card = getAddonCard(win, id); + + const promisePromptInfo = promisePermissionPrompt(id); + await installUpdate(card, "update-installed"); + const promptInfo = await promisePromptInfo; + ok(promptInfo, "Got a permission prompt as expected"); + + AddonManager.autoUpdateDefault = true; + + await closeView(win); + await extension.unload(); +}); + +add_task(async function testNoUpdateAvailableOnUnrelatedAddonCards() { + let idNoUpdate = "no-update@mochi.test"; + + let extensionNoUpdate = ExtensionTestUtils.loadExtension({ + useAddonManager: "temporary", + manifest: { + name: "TestAddonNoUpdate", + browser_specific_settings: { gecko: { id: idNoUpdate } }, + }, + }); + await extensionNoUpdate.startup(); + + let win = await loadInitialView("extension"); + + let cardNoUpdate = getAddonCard(win, idNoUpdate); + ok(cardNoUpdate, `Got AddonCard for ${idNoUpdate}`); + + // Assert that there is not an update badge + assertUpdateState({ card: cardNoUpdate, shown: false, expanded: false }); + + // Trigger a onNewInstall event by install another unrelated addon. + const XPI_URL = `${SECURE_TESTROOT}../xpinstall/amosigned.xpi`; + let install = await AddonManager.getInstallForURL(XPI_URL); + await AddonManager.installAddonFromAOM( + gBrowser.selectedBrowser, + win.document.documentURIObject, + install + ); + + // Cancel the install used to trigger the onNewInstall install event. + await install.cancel(); + // Assert that the previously installed addon isn't marked with the + // update available badge after installing an unrelated addon. + assertUpdateState({ card: cardNoUpdate, shown: false, expanded: false }); + + await closeView(win); + await extensionNoUpdate.unload(); +}); -- cgit v1.2.3