diff --git a/browser/components/extensions/ext-tabs.js b/browser/components/extensions/ext-tabs.js index 135a853f0f50..d806676ff106 100644 --- a/browser/components/extensions/ext-tabs.js +++ b/browser/components/extensions/ext-tabs.js @@ -42,44 +42,46 @@ function getSender(context, target, sender) { } } -// WeakMap[ExtensionContext -> {tab, parentWindow}] -var pageDataMap = new WeakMap(); +function getDocShellOwner(docShell) { + let browser = docShell.chromeEventHandler; + + let xulWindow = browser.ownerGlobal; + + let {gBrowser} = xulWindow; + if (gBrowser) { + let tab = gBrowser.getTabForBrowser(browser); + + return {xulWindow, tab}; + } + + return {}; +} /* eslint-disable mozilla/balanced-listeners */ // This listener fires whenever an extension page opens in a tab // (either initiated by the extension or the user). Its job is to fill // in some tab-specific details and keep data around about the // ExtensionContext. -extensions.on("page-load", (type, page, params, sender, delegate) => { +extensions.on("page-load", (type, context, params, sender, delegate) => { if (params.type == "tab" || params.type == "popup") { - let browser = params.docShell.chromeEventHandler; + let {xulWindow, tab} = getDocShellOwner(params.docShell); - let parentWindow = browser.ownerGlobal; - page.windowId = WindowManager.getId(parentWindow); - - let tab = parentWindow.gBrowser.getTabForBrowser(browser); + // FIXME: Handle tabs being moved between windows. + context.windowId = WindowManager.getId(xulWindow); if (tab) { sender.tabId = TabManager.getId(tab); - page.tabId = TabManager.getId(tab); + context.tabId = TabManager.getId(tab); } - - pageDataMap.set(page, {tab, parentWindow}); } delegate.getSender = getSender; }); -extensions.on("page-unload", (type, page) => { - pageDataMap.delete(page); -}); - -extensions.on("page-shutdown", (type, page) => { - if (pageDataMap.has(page)) { - let {tab, parentWindow} = pageDataMap.get(page); - pageDataMap.delete(page); - +extensions.on("page-shutdown", (type, context) => { + if (context.type == "tab") { + let {xulWindow, tab} = getDocShellOwner(context.docShell); if (tab) { - parentWindow.gBrowser.removeTab(tab); + xulWindow.gBrowser.removeTab(tab); } } }); @@ -96,9 +98,9 @@ extensions.on("fill-browser-data", (type, browser, data, result) => { /* eslint-enable mozilla/balanced-listeners */ global.currentWindow = function(context) { - let pageData = pageDataMap.get(context); - if (pageData) { - return pageData.parentWindow; + let {xulWindow} = getDocShellOwner(context.docShell); + if (xulWindow) { + return xulWindow; } return WindowManager.topWindow; }; diff --git a/browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage.js b/browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage.js index abe9ed0a24ec..9a3eed8b0fdd 100644 --- a/browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage.js +++ b/browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage.js @@ -257,3 +257,66 @@ add_task(function* test_options_no_manifest() { yield extension.awaitFinish("options-no-manifest"); yield extension.unload(); }); + +add_task(function* test_inline_options_uninstall() { + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/"); + + let extension = yield loadExtension({ + manifest: { + "options_ui": { + "page": "options.html", + }, + }, + + background: function() { + let _optionsPromise; + let awaitOptions = () => { + browser.test.assertFalse(_optionsPromise, "Should not be awaiting options already"); + + return new Promise(resolve => { + _optionsPromise = {resolve}; + }); + }; + + browser.runtime.onMessage.addListener((msg, sender) => { + if (msg == "options.html") { + if (_optionsPromise) { + _optionsPromise.resolve(sender.tab); + _optionsPromise = null; + } else { + browser.test.fail("Saw unexpected options page load"); + } + } + }); + + let firstTab; + browser.tabs.query({currentWindow: true, active: true}).then(tabs => { + firstTab = tabs[0].id; + + browser.test.log("Open options page. Expect fresh load."); + return Promise.all([ + browser.runtime.openOptionsPage(), + awaitOptions(), + ]); + }).then(([, tab]) => { + browser.test.assertEq("about:addons", tab.url, "Tab contains AddonManager"); + browser.test.assertTrue(tab.active, "Tab is active"); + browser.test.assertTrue(tab.id != firstTab, "Tab is a new tab"); + + browser.test.sendMessage("options-ui-open"); + }).catch(error => { + browser.test.fail(`Error: ${error} :: ${error.stack}`); + }); + }, + }); + + yield extension.awaitMessage("options-ui-open"); + yield extension.unload(); + + is(gBrowser.selectedBrowser.currentURI.spec, "about:addons", + "Add-on manager tab should still be open"); + + yield BrowserTestUtils.removeTab(gBrowser.selectedTab); + + yield BrowserTestUtils.removeTab(tab); +}); diff --git a/toolkit/components/extensions/Extension.jsm b/toolkit/components/extensions/Extension.jsm index d91f783adbb6..264577ca1e30 100644 --- a/toolkit/components/extensions/Extension.jsm +++ b/toolkit/components/extensions/Extension.jsm @@ -277,6 +277,11 @@ ExtensionContext = class extends BaseContext { } } + get docShell() { + return this.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDocShell); + } + get cloneScope() { return this.contentWindow; }