diff --git a/app/scripts/fetch_xulrunner b/app/scripts/fetch_xulrunner index c8c62f5b27..fbc339d199 100755 --- a/app/scripts/fetch_xulrunner +++ b/app/scripts/fetch_xulrunner @@ -468,10 +468,22 @@ function modify_omni { replace_line ' for \(const addon of this.pendingUninstallAddons\) \{' \ ' const pendingUninstalls = [...this.pendingUninstallAddons]; this.pendingUninstallAddons.clear(); - for (const addon of pendingUninstalls) {' $file + for (let i = 0; i < pendingUninstalls.length; i++) { + const addon = pendingUninstalls[i];' $file replace_line ' if \(isPending\(addon, "uninstall"\)\) \{' \ - ' const current = await AddonManager.getAddonByID(addon.id); + ' let current; + try { + current = await AddonManager.getAddonByID(addon.id); + } catch (error) { + console.error(error); + this.pendingUninstallAddons.add(addon); + } if (this.isConnected) { + for (const pending of pendingUninstalls.slice(i)) { + if (isPending(pending, "uninstall")) { + this.pendingUninstallAddons.add(pending); + } + } return; } if (current === addon && isPending(current, "uninstall")) {' $file diff --git a/app/scripts/tests/addon-list-pending-uninstall.test.mjs b/app/scripts/tests/addon-list-pending-uninstall.test.mjs index e6cc6b1d0c..8637d3e8ba 100644 --- a/app/scripts/tests/addon-list-pending-uninstall.test.mjs +++ b/app/scripts/tests/addon-list-pending-uninstall.test.mjs @@ -15,7 +15,9 @@ const source = readFileSync(sourcePath, 'utf8') .replace('customElements.define("addon-list", AddonList);', 'globalThis.AddonList = AddonList;'); function createList(currentById) { + const errors = []; const context = { + console: { error: error => errors.push(error) }, HTMLElement: class {}, ChromeUtils: { importESModule: () => ({ AddonManager: { @@ -31,6 +33,7 @@ function createList(currentById) { list.removeListener = () => {}; list.updateAddon = () => {}; list.removePendingUninstallBar = () => {}; + list.errors = errors; return list; } @@ -81,3 +84,34 @@ test('a reconnect during lookup leaves newly queued removals intact', async () = assert.equal(uninstalls, 0); assert.equal(list.pendingUninstallAddons.has(fresh), true); }); + +test('a reconnect mid-loop preserves the unprocessed pending removal', async () => { + let resolveSecond; + let firstUninstalls = 0; + let secondUninstalls = 0; + const first = { id: 'first@test', pendingUninstall: true, + uninstall: () => firstUninstalls++ }; + const second = { id: 'second@test', pendingUninstall: true, + uninstall: () => secondUninstalls++ }; + const list = createList(id => id === first.id ? Promise.resolve(first) + : new Promise(resolve => { resolveSecond = resolve; })); + list.pendingUninstallAddons.add(first); + list.pendingUninstallAddons.add(second); + const teardown = list.disconnectedCallback(); + await new Promise(resolve => setImmediate(resolve)); + assert.equal(firstUninstalls, 1); + list.isConnected = true; + resolveSecond(second); + await teardown; + assert.equal(secondUninstalls, 0); + assert.equal(list.pendingUninstallAddons.has(second), true); +}); + +test('a failed current-add-on lookup retains the pending removal', async () => { + const old = { id: 'plugin@test', pendingUninstall: true }; + const list = createList(() => Promise.reject(new Error('lookup failed'))); + list.pendingUninstallAddons.add(old); + await list.disconnectedCallback(); + assert.equal(list.pendingUninstallAddons.has(old), true); + assert.equal(list.errors.length, 1); +});