Bug 1383160 - Fix Android pageAction popup behavior.
MozReview-Commit-ID: 66PnjFv4IIx
--- a/mobile/android/components/extensions/ext-pageAction.js
+++ b/mobile/android/components/extensions/ext-pageAction.js
@@ -45,21 +45,17 @@ class PageAction extends EventEmitter {
id: `{${extension.uuid}}`,
clickCallback: () => {
let tab = tabTracker.activeTab;
this.tabManager.addActiveTabPermission(tab);
let popup = this.tabContext.get(tab.id).popup || this.defaults.popup;
if (popup) {
- let win = Services.wm.getMostRecentWindow("navigator:browser");
- win.BrowserApp.addTab(popup, {
- selected: true,
- parentId: win.BrowserApp.selectedTab.id,
- });
+ tabTracker.openExtensionPopupTab(popup);
} else {
this.emit("click", tab);
}
},
};
this.shouldShow = false;
--- a/mobile/android/components/extensions/ext-utils.js
+++ b/mobile/android/components/extensions/ext-utils.js
@@ -245,24 +245,84 @@ global.WindowEventManager = class extend
return () => {
windowTracker.removeListener(event, listener2);
};
});
}
};
class TabTracker extends TabTrackerBase {
+ constructor() {
+ super();
+
+ // Keep track of the extension popup tab.
+ this._extensionPopupTabWeak = null;
+ }
+
init() {
if (this.initialized) {
return;
}
this.initialized = true;
windowTracker.addListener("TabClose", this);
windowTracker.addListener("TabOpen", this);
+
+ // Register a listener for the Tab:Selected global event,
+ // so that we can close the popup when a popup tab has been
+ // unselected.
+ GlobalEventDispatcher.registerListener(this, [
+ "Tab:Selected",
+ ]);
+ }
+
+ /**
+ * Returns the currently opened popup tab if any
+ */
+ get extensionPopupTab() {
+ if (this._extensionPopupTabWeak) {
+ const tab = this._extensionPopupTabWeak.get();
+
+ // Return the native tab only if the tab has not been removed in the meantime.
+ if (tab.browser) {
+ return tab;
+ }
+
+ // Clear the tracked popup tab if it has been closed in the meantime.
+ this._extensionPopupTabWeak = null;
+ }
+
+ return undefined;
+ }
+
+ /**
+ * Open a pageAction/browserAction popup url in a tab and keep track of
+ * its weak reference (to be able to customize the activedTab using the tab parentId,
+ * to skip it in the tabs.query and to set the parent tab as active when the popup
+ * tab is currently selected).
+ *
+ * @param {string} popup
+ * The popup url to open in a tab.
+ */
+ openExtensionPopupTab(popup) {
+ let win = windowTracker.topWindow;
+ if (!win) {
+ throw new ExtensionError(`Unable to open a popup without an active window`);
+ }
+
+ if (this.extensionPopupTab) {
+ win.BrowserApp.closeTab(this.extensionPopupTab);
+ }
+
+ this.init();
+
+ this._extensionPopupTabWeak = Cu.getWeakReference(win.BrowserApp.addTab(popup, {
+ selected: true,
+ parentId: win.BrowserApp.selectedTab.id,
+ }));
}
getId(nativeTab) {
return nativeTab.id;
}
getTab(id, default_ = undefined) {
let win = windowTracker.topWindow;
@@ -283,30 +343,55 @@ class TabTracker extends TabTrackerBase
* events for them.
*
* @param {Event} event
* A DOM event to handle.
* @private
*/
handleEvent(event) {
const {BrowserApp} = event.target.ownerGlobal;
- let nativeTab = BrowserApp.getTabForBrowser(event.target);
+ const nativeTab = BrowserApp.getTabForBrowser(event.target);
switch (event.type) {
case "TabOpen":
this.emitCreated(nativeTab);
break;
case "TabClose":
this.emitRemoved(nativeTab, false);
break;
}
}
/**
+ * Required by the GlobalEventDispatcher module. This event will get
+ * called whenever one of the registered listeners fires.
+ * @param {string} event The event which fired.
+ * @param {object} data Information about the event which fired.
+ */
+ onEvent(event, data) {
+ const {BrowserApp} = windowTracker.topWindow;
+
+ switch (event) {
+ case "Tab:Selected": {
+ // If a new tab has been selected while an extension popup tab is still open,
+ // close it immediately.
+ const nativeTab = BrowserApp.getTabForId(data.id);
+
+ const popupTab = tabTracker.extensionPopupTab;
+ if (popupTab && popupTab !== nativeTab) {
+ BrowserApp.closeTab(popupTab);
+ }
+
+ break;
+ }
+ }
+ }
+
+ /**
* Emits a "tab-created" event for the given tab element.
*
* @param {NativeTab} nativeTab
* The tab element which is being created.
* @private
*/
emitCreated(nativeTab) {
this.emit("tab-created", {nativeTab});
@@ -321,16 +406,27 @@ class TabTracker extends TabTrackerBase
* True if the tab is being removed because the browser window is
* closing.
* @private
*/
emitRemoved(nativeTab, isWindowClosing) {
let windowId = windowTracker.getId(nativeTab.browser.ownerGlobal);
let tabId = this.getId(nativeTab);
+ if (this.extensionPopupTab && this.extensionPopupTab === nativeTab) {
+ this._extensionPopupTabWeak = null;
+
+ // Select the parent tab when the closed tab was an extension popup tab.
+ const {BrowserApp} = windowTracker.topWindow;
+ const popupParentTab = BrowserApp.getTabForId(nativeTab.parentId);
+ if (popupParentTab) {
+ BrowserApp.selectTab(popupParentTab);
+ }
+ }
+
Services.tm.dispatchToMainThread(() => {
this.emit("tab-removed", {nativeTab, tabId, windowId, isWindowClosing});
});
}
getBrowserData(browser) {
let result = {
tabId: -1,
@@ -346,20 +442,29 @@ class TabTracker extends TabTrackerBase
result.tabId = this.getId(nativeTab);
}
}
return result;
}
get activeTab() {
- let window = windowTracker.topWindow;
- if (window && window.BrowserApp) {
- return window.BrowserApp.selectedTab;
+ let win = windowTracker.topWindow;
+ if (win && win.BrowserApp) {
+ const selectedTab = win.BrowserApp.selectedTab;
+
+ // If the current tab is an extension popup tab, we use the parentId to retrieve
+ // and return the tab that was selected when the popup tab has been opened.
+ if (selectedTab === this.extensionPopupTab) {
+ return win.BrowserApp.getTabForId(selectedTab.parentId);
+ }
+
+ return selectedTab;
}
+
return null;
}
}
windowTracker = new WindowTracker();
tabTracker = new TabTracker();
Object.assign(global, {tabTracker, windowTracker});
@@ -401,16 +506,33 @@ class Tab extends TabBase {
return this.nativeTab.lastTouchedAt;
}
get pinned() {
return false;
}
get active() {
+ // If there is an extension popup tab and it is active,
+ // then the parent tab of the extension popup tab is active
+ // (while the extension popup tab will not be included in the
+ // tabs.query results).
+ if (tabTracker.extensionPopupTab) {
+ if (tabTracker.extensionPopupTab.getActive() &&
+ this.nativeTab.id === tabTracker.extensionPopupTab.parentId) {
+ return true;
+ }
+
+ // Never return true for an active extension popup, e.g. so that
+ // the popup tab will not be part of the results of querying
+ // all the active tabs.
+ if (tabTracker.extensionPopupTab === this.nativeTab) {
+ return false;
+ }
+ }
return this.nativeTab.getActive();
}
get selected() {
return this.nativeTab.getActive();
}
get status() {
--- a/mobile/android/components/extensions/test/mochitest/test_ext_activeTab_permission.html
+++ b/mobile/android/components/extensions/test/mochitest/test_ext_activeTab_permission.html
@@ -10,16 +10,17 @@
</head>
<body>
<script type="text/javascript">
"use strict";
var {BrowserActions} = SpecialPowers.Cu.import("resource://gre/modules/BrowserActions.jsm", {});
var {PageActions} = SpecialPowers.Cu.import("resource://gre/modules/PageActions.jsm", {});
+var {Services} = SpecialPowers.Cu.import("resource://gre/modules/Services.jsm", {});
function pageLoadedContentScript() {
browser.test.sendMessage("page-loaded", window.location.href);
}
add_task(async function test_activeTab_pageAction() {
async function background() {
function contentScriptCode() {
@@ -197,12 +198,144 @@ add_task(async function test_activeTab_b
info("Click the browserAction");
BrowserActions.synthesizeClick(uuid);
await extension.awaitFinish("browser_action.activeTab.done");
await extension.unload();
});
+add_task(async function test_activeTab_pageAction_popup() {
+ async function background() {
+ await browser.tabs.create({url: "http://example.com#test_activeTab_pageAction_popup"});
+ const tabs = await browser.tabs.query({active: true});
+ await browser.pageAction.show(tabs[0].id);
+
+ browser.test.log(`pageAction shown on tab ${tabs[0].id}`);
+
+ browser.test.sendMessage("background_page.ready", {activeTabId: tabs[0].id});
+ }
+
+ async function popupScript() {
+ function contentScriptCode() {
+ browser.test.log("content script executed");
+
+ return "tabs.executeScript result";
+ }
+
+ const tabs = await browser.tabs.query({active: true});
+ const tab = tabs[0];
+
+ browser.test.log(`extension popup tab opened loaded for activeTab ${tab.id}`);
+
+ browser.test.sendMessage("extension_popup.activeTab", tab.id);
+
+ const [result] = await browser.tabs.executeScript(tab.id, {
+ code: `(${contentScriptCode})()`,
+ }).catch(error => {
+ // Make the test to fail fast if something goes wrong.
+ browser.test.fail(`Unexpected exception on tabs.executeScript: ${error}`);
+ browser.test.notifyFail("page_action_popup.activeTab.done");
+ throw error;
+ });
+
+ browser.test.assertEq("tabs.executeScript result", result,
+ "Got the expected result from tabs.executeScript");
+
+ browser.test.notifyPass("page_action_popup.activeTab.done");
+ }
+
+ let popupHtml = `<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <h1>Extension Popup</h1>
+ <script src="popup.js"><\/script>
+ </body>
+ </html>
+ `;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ "name": "PageAction Extension",
+ "page_action": {
+ "default_title": "Page Action",
+ "default_icon": {
+ "18": "extension.png",
+ },
+ "default_popup": "popup.html",
+ },
+ "content_scripts": [
+ {
+ "js": ["page_loaded.js"],
+ "matches": ["http://example.com/*"],
+ "run_at": "document_end",
+ },
+ ],
+ "permissions": ["activeTab"],
+ },
+ files: {
+ "extension.png": TEST_ICON_ARRAYBUFFER,
+ "page_loaded.js": pageLoadedContentScript,
+ "popup.html": popupHtml,
+ "popup.js": popupScript,
+ },
+ });
+
+ await extension.startup();
+
+ const {activeTabId} = await extension.awaitMessage("background_page.ready");
+
+ const uuid = `{${extension.uuid}}`;
+
+ ok(PageActions.isShown(uuid), "page action is shown");
+
+ info("Wait the new tab to be loaded");
+ const loadedURL = await extension.awaitMessage("page-loaded");
+
+ is(loadedURL, "http://example.com/#test_activeTab_pageAction_popup",
+ "The expected URL has been loaded in a new tab");
+
+ PageActions.synthesizeClick(uuid);
+
+ const popupActiveTabId = await extension.awaitMessage("extension_popup.activeTab");
+
+ // Check that while the extension popup tab is selected the active tab is still the tab
+ // from which the user has opened the extension popup.
+ is(popupActiveTabId, activeTabId,
+ "Got the expected tabId while the extension popup tab was selected");
+
+ await extension.awaitFinish("page_action_popup.activeTab.done");
+
+ const chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+ const BrowserApp = chromeWin.BrowserApp;
+
+ const popupTab = BrowserApp.selectedTab;
+ const popupTabId = popupTab.id;
+
+ let onceTabClosed = new Promise(resolve => {
+ BrowserApp.deck.addEventListener("TabClose", resolve, {once: true});
+ });
+
+ // Switch to the parent tab of the popup tab.
+ // (which should make the extension popup tab to be closed automatically)
+ BrowserApp.selectTab(BrowserApp.getTabForId(popupTab.parentId));
+
+ info("Wait for the extension popup tab to be closed once the parent tab has been selected");
+
+ await onceTabClosed;
+
+ is(BrowserApp.getTabForId(popupTabId), null,
+ "The extension popup tab should have been closed");
+
+ // Close the tab that opened the extension popup before exiting the test.
+ BrowserApp.closeTab(BrowserApp.selectedTab);
+
+ await extension.unload();
+});
+
</script>
</body>
</html>