Bug 1383160 - Fix Android pageAction popup behavior. r=mixedpuppy

MozReview-Commit-ID: 66PnjFv4IIx

--HG--
extra : rebase_source : c5fdbe75cea18bcbfeedc8916c2b9c00ec20a429
This commit is contained in:
Luca Greco 2017-08-15 22:55:13 +02:00
parent d0c4b71df2
commit b1d2a209c5
3 changed files with 260 additions and 9 deletions

View File

@ -50,11 +50,7 @@ class PageAction extends EventEmitter {
let popup = this.tabContext.get(tab.id).popup || this.defaults.popup; let popup = this.tabContext.get(tab.id).popup || this.defaults.popup;
if (popup) { if (popup) {
let win = Services.wm.getMostRecentWindow("navigator:browser"); tabTracker.openExtensionPopupTab(popup);
win.BrowserApp.addTab(popup, {
selected: true,
parentId: win.BrowserApp.selectedTab.id,
});
} else { } else {
this.emit("click", tab); this.emit("click", tab);
} }

View File

@ -250,6 +250,13 @@ global.WindowEventManager = class extends EventManager {
}; };
class TabTracker extends TabTrackerBase { class TabTracker extends TabTrackerBase {
constructor() {
super();
// Keep track of the extension popup tab.
this._extensionPopupTabWeak = null;
}
init() { init() {
if (this.initialized) { if (this.initialized) {
return; return;
@ -258,6 +265,59 @@ class TabTracker extends TabTrackerBase {
windowTracker.addListener("TabClose", this); windowTracker.addListener("TabClose", this);
windowTracker.addListener("TabOpen", 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) { getId(nativeTab) {
@ -288,7 +348,7 @@ class TabTracker extends TabTrackerBase {
*/ */
handleEvent(event) { handleEvent(event) {
const {BrowserApp} = event.target.ownerGlobal; const {BrowserApp} = event.target.ownerGlobal;
let nativeTab = BrowserApp.getTabForBrowser(event.target); const nativeTab = BrowserApp.getTabForBrowser(event.target);
switch (event.type) { switch (event.type) {
case "TabOpen": case "TabOpen":
@ -301,6 +361,31 @@ class TabTracker extends TabTrackerBase {
} }
} }
/**
* 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. * Emits a "tab-created" event for the given tab element.
* *
@ -326,6 +411,17 @@ class TabTracker extends TabTrackerBase {
let windowId = windowTracker.getId(nativeTab.browser.ownerGlobal); let windowId = windowTracker.getId(nativeTab.browser.ownerGlobal);
let tabId = this.getId(nativeTab); 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(() => { Services.tm.dispatchToMainThread(() => {
this.emit("tab-removed", {nativeTab, tabId, windowId, isWindowClosing}); this.emit("tab-removed", {nativeTab, tabId, windowId, isWindowClosing});
}); });
@ -351,10 +447,19 @@ class TabTracker extends TabTrackerBase {
} }
get activeTab() { get activeTab() {
let window = windowTracker.topWindow; let win = windowTracker.topWindow;
if (window && window.BrowserApp) { if (win && win.BrowserApp) {
return window.BrowserApp.selectedTab; 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; return null;
} }
} }
@ -406,6 +511,23 @@ class Tab extends TabBase {
} }
get active() { 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(); return this.nativeTab.getActive();
} }

View File

@ -15,6 +15,7 @@
var {BrowserActions} = SpecialPowers.Cu.import("resource://gre/modules/BrowserActions.jsm", {}); var {BrowserActions} = SpecialPowers.Cu.import("resource://gre/modules/BrowserActions.jsm", {});
var {PageActions} = SpecialPowers.Cu.import("resource://gre/modules/PageActions.jsm", {}); var {PageActions} = SpecialPowers.Cu.import("resource://gre/modules/PageActions.jsm", {});
var {Services} = SpecialPowers.Cu.import("resource://gre/modules/Services.jsm", {});
function pageLoadedContentScript() { function pageLoadedContentScript() {
browser.test.sendMessage("page-loaded", window.location.href); browser.test.sendMessage("page-loaded", window.location.href);
@ -202,6 +203,138 @@ add_task(async function test_activeTab_browserAction() {
await extension.unload(); 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> </script>
</body> </body>