Bug 1423725 add show/hide tabs api, r=rpl

MozReview-Commit-ID: 4z73ZTRE7kN

--HG--
extra : rebase_source : 7683973921b07818c7a63ab8387e4ebe65705499
This commit is contained in:
Shane Caraveo 2018-01-18 16:37:18 -07:00
parent d78ea2931e
commit 636a2a5a8f
8 changed files with 335 additions and 3 deletions

View File

@ -20,6 +20,11 @@ var {
ExtensionError,
} = ExtensionUtils;
const TABHIDE_PREFNAME = "extensions.webextensions.tabhide.enabled";
// WeakMap[Tab -> ExtensionID]
let hiddenTabs = new WeakMap();
let tabListener = {
tabReadyInitialized: false,
tabReadyPromises: new WeakMap(),
@ -77,6 +82,27 @@ let tabListener = {
};
this.tabs = class extends ExtensionAPI {
onShutdown(reason) {
if (!this.extension.hasPermission("tabHide")) {
return;
}
if (reason == "ADDON_DISABLE" ||
reason == "ADDON_UNINSTALL") {
// Show all hidden tabs if a tab managing extension is uninstalled or
// disabled. If a user has more than one, the extensions will need to
// self-manage re-hiding tabs.
for (let tab of this.extension.tabManager.query()) {
let nativeTab = tabTracker.getTab(tab.id);
if (hiddenTabs.get(nativeTab) === this.extension.id) {
hiddenTabs.delete(nativeTab);
if (nativeTab.ownerGlobal) {
nativeTab.ownerGlobal.gBrowser.showTab(nativeTab);
}
}
}
}
}
getAPI(context) {
let {extension} = context;
@ -275,6 +301,8 @@ this.tabs = class extends ExtensionAPI {
needed.push("discarded");
} else if (event.type == "TabShow") {
needed.push("hidden");
// Always remove the tab from the hiddenTabs map.
hiddenTabs.delete(event.originalTarget);
} else if (event.type == "TabHide") {
needed.push("hidden");
}
@ -989,6 +1017,47 @@ this.tabs = class extends ExtensionAPI {
tab.linkedBrowser.messageManager.sendAsyncMessage("Reader:ToggleReaderMode");
},
show(tabIds) {
if (!Services.prefs.getBoolPref(TABHIDE_PREFNAME, false)) {
throw new ExtensionError(`tabs.show is currently experimental and must be enabled with the ${TABHIDE_PREFNAME} preference.`);
}
if (!Array.isArray(tabIds)) {
tabIds = [tabIds];
}
for (let tabId of tabIds) {
let tab = tabTracker.getTab(tabId);
if (tab.ownerGlobal) {
hiddenTabs.delete(tab);
tab.ownerGlobal.gBrowser.showTab(tab);
}
}
},
hide(tabIds) {
if (!Services.prefs.getBoolPref(TABHIDE_PREFNAME, false)) {
throw new ExtensionError(`tabs.hide is currently experimental and must be enabled with the ${TABHIDE_PREFNAME} preference.`);
}
if (!Array.isArray(tabIds)) {
tabIds = [tabIds];
}
let hidden = [];
let tabs = tabIds.map(tabId => tabTracker.getTab(tabId));
for (let tab of tabs) {
if (tab.ownerGlobal && !tab.hidden) {
tab.ownerGlobal.gBrowser.hideTab(tab);
if (tab.hidden) {
hiddenTabs.set(tab, extension.id);
hidden.push(tabTracker.getId(tab));
}
}
}
return hidden;
},
},
};
return self;

View File

@ -12,7 +12,8 @@
"type": "string",
"enum": [
"activeTab",
"tabs"
"tabs",
"tabHide"
]
}]
}
@ -1277,6 +1278,40 @@
]
}
]
},
{
"name": "show",
"type": "function",
"description": "Shows one or more tabs.",
"permissions": ["tabHide"],
"async": true,
"parameters": [
{
"name": "tabIds",
"description": "The TAB ID or list of TAB IDs to show.",
"choices": [
{"type": "integer", "minimum": 0},
{"type": "array", "items": {"type": "integer", "minimum": 0}}
]
}
]
},
{
"name": "hide",
"type": "function",
"description": "Hides one or more tabs. The <code>\"tabHide\"</code> permission is required to hide tabs. Not all tabs are hidable. Returns an array of hidden tabs.",
"permissions": ["tabHide"],
"async": true,
"parameters": [
{
"name": "tabIds",
"description": "The TAB ID or list of TAB IDs to hide.",
"choices": [
{"type": "integer", "minimum": 0},
{"type": "array", "items": {"type": "integer", "minimum": 0}}
]
}
]
}
],
"events": [

View File

@ -153,6 +153,7 @@ skip-if = !e10s
[browser_ext_tabs_executeScript_no_create.js]
[browser_ext_tabs_executeScript_runAt.js]
[browser_ext_tabs_getCurrent.js]
[browser_ext_tabs_hide.js]
[browser_ext_tabs_insertCSS.js]
[browser_ext_tabs_lastAccessed.js]
[browser_ext_tabs_lazy.js]

View File

@ -0,0 +1,203 @@
"use strict";
const {Utils} = Cu.import("resource://gre/modules/sessionstore/Utils.jsm", {});
const triggeringPrincipal_base64 = Utils.SERIALIZED_SYSTEMPRINCIPAL;
// Ensure the pref prevents API use when the extension has the tabHide permission.
add_task(async function test_pref_disabled() {
async function background() {
let tabs = await browser.tabs.query({hidden: false});
let ids = tabs.map(tab => tab.id);
await browser.test.assertRejects(
browser.tabs.hide(ids),
/tabs.hide is currently experimental/,
"Got the expected error when pref not enabled"
).catch(err => {
browser.test.notifyFail("pref-test");
throw err;
});
browser.test.notifyPass("pref-test");
}
let extdata = {
manifest: {permissions: ["tabs", "tabHide"]},
background,
};
let extension = ExtensionTestUtils.loadExtension(extdata);
await extension.startup();
await extension.awaitFinish("pref-test");
await extension.unload();
});
add_task(async function test_tabs_showhide() {
await SpecialPowers.pushPrefEnv({
set: [["extensions.webextensions.tabhide.enabled", true]],
});
async function background() {
browser.test.onMessage.addListener(async (msg, data) => {
switch (msg) {
case "hideall": {
let tabs = await browser.tabs.query({hidden: false});
browser.test.assertEq(tabs.length, 5, "got 5 tabs");
let ids = tabs.map(tab => tab.id);
browser.test.log(`working with ids ${JSON.stringify(ids)}`);
let hidden = await browser.tabs.hide(ids);
browser.test.assertEq(hidden.length, 3, "hid 3 tabs");
tabs = await browser.tabs.query({hidden: true});
ids = tabs.map(tab => tab.id);
browser.test.assertEq(JSON.stringify(hidden.sort()),
JSON.stringify(ids.sort()), "hidden tabIds match");
browser.test.sendMessage("hidden", {hidden});
break;
}
case "showall": {
let tabs = await browser.tabs.query({hidden: true});
for (let tab of tabs) {
browser.test.assertTrue(tab.hidden, "tab is hidden");
}
let ids = tabs.map(tab => tab.id);
browser.tabs.show(ids);
browser.test.sendMessage("shown");
break;
}
}
});
}
let extdata = {
manifest: {permissions: ["tabs", "tabHide"]},
background,
};
let extension = ExtensionTestUtils.loadExtension(extdata);
await extension.startup();
let sessData = {
windows: [{
tabs: [
{entries: [{url: "about:blank", triggeringPrincipal_base64}]},
{entries: [{url: "https://example.com/", triggeringPrincipal_base64}]},
{entries: [{url: "https://mochi.test:8888/", triggeringPrincipal_base64}]},
],
}, {
tabs: [
{entries: [{url: "about:blank", triggeringPrincipal_base64}]},
{entries: [{url: "http://test1.example.com/", triggeringPrincipal_base64}]},
],
}],
};
// Set up a test session with 2 windows and 5 tabs.
let oldState = SessionStore.getBrowserState();
let restored = TestUtils.topicObserved("sessionstore-browser-state-restored");
SessionStore.setBrowserState(JSON.stringify(sessData));
await restored;
// Attempt to hide all the tabs, however the active tab in each window cannot
// be hidden, so the result will be 3 hidden tabs.
extension.sendMessage("hideall");
await extension.awaitMessage("hidden");
// We have 2 windows in this session. Otherwin is the non-current window.
// In each window, the first tab will be the selected tab and should not be
// hidden. The rest of the tabs should be hidden at this point. Hidden
// status was already validated inside the extension, this double checks
// from chrome code.
let otherwin;
for (let win of BrowserWindowIterator()) {
if (win != window) {
otherwin = win;
}
let tabs = Array.from(win.gBrowser.tabs.values());
ok(!tabs[0].hidden, "first tab not hidden");
for (let i = 1; i < tabs.length; i++) {
ok(tabs[i].hidden, "tab hidden value is correct");
}
}
// Test closing the last visible tab, the next tab which is hidden should become
// the selectedTab and will be visible.
ok(!otherwin.gBrowser.selectedTab.hidden, "selected tab is not hidden");
await BrowserTestUtils.removeTab(otherwin.gBrowser.selectedTab);
ok(!otherwin.gBrowser.selectedTab.hidden, "tab was unhidden");
// Showall will unhide any remaining hidden tabs.
extension.sendMessage("showall");
await extension.awaitMessage("shown");
// Check from chrome code that all tabs are visible again.
for (let win of BrowserWindowIterator()) {
let tabs = Array.from(win.gBrowser.tabs.values());
for (let i = 0; i < tabs.length; i++) {
ok(!tabs[i].hidden, "tab hidden value is correct");
}
}
// Close second window.
await BrowserTestUtils.closeWindow(otherwin);
await extension.unload();
// Restore pre-test state.
restored = TestUtils.topicObserved("sessionstore-browser-state-restored");
SessionStore.setBrowserState(oldState);
await restored;
});
// Test our shutdown handling. Currently this means any hidden tabs will be
// shown when a tabHide extension is shutdown. We additionally test the
// tabs.onUpdated listener gets called with hidden state changes.
add_task(async function test_tabs_shutdown() {
await SpecialPowers.pushPrefEnv({
set: [["extensions.webextensions.tabhide.enabled", true]],
});
let tabs = [
await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/", true, true),
await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/", true, true),
];
async function background() {
let tabs = await browser.tabs.query({url: "http://example.com/"});
let testTab = tabs[0];
browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if ("hidden" in changeInfo) {
browser.test.assertEq(tabId, testTab.id, "correct tab was hidden");
browser.test.assertTrue(changeInfo.hidden, "tab is hidden");
browser.test.sendMessage("changeInfo");
}
});
let hidden = await browser.tabs.hide(testTab.id);
browser.test.assertEq(hidden[0], testTab.id, "tab was hidden");
tabs = await browser.tabs.query({hidden: true});
browser.test.assertEq(tabs[0].id, testTab.id, "tab was hidden");
browser.test.sendMessage("ready");
}
let extdata = {
manifest: {permissions: ["tabs", "tabHide"]},
useAddonManager: "temporary", // For testing onShutdown.
background,
};
let extension = ExtensionTestUtils.loadExtension(extdata);
await extension.startup();
// test onUpdated
await Promise.all([
extension.awaitMessage("ready"),
extension.awaitMessage("changeInfo"),
]);
Assert.ok(tabs[0].hidden, "Tab is hidden by extension");
await extension.unload();
Assert.ok(!tabs[0].hidden, "Tab is not hidden after unloading extension");
await BrowserTestUtils.removeTab(tabs[0]);
await BrowserTestUtils.removeTab(tabs[1]);
});

View File

@ -1,6 +1,10 @@
"use strict";
add_task(async function test_tabs_mediaIndicators() {
await SpecialPowers.pushPrefEnv({
set: [["extensions.webextensions.tabhide.enabled", true]],
});
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
// setBrowserSharing is called when a request for media icons occurs. We're
// just testing that extension tabs get the info and are updated when it is
@ -25,6 +29,12 @@ add_task(async function test_tabs_mediaIndicators() {
tabs = await browser.tabs.query({screen: "Screen"});
browser.test.assertEq(tabs.length, 0, "screen sharing tab was not found");
// Verify we cannot hide a sharing tab.
let hidden = await browser.tabs.hide(testTab.id);
browser.test.assertEq(hidden.length, 0, "unable to hide sharing tab");
tabs = await browser.tabs.query({hidden: true});
browser.test.assertEq(tabs.length, 0, "unable to hide sharing tab");
browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (testTab.id !== tabId) {
return;
@ -39,7 +49,7 @@ add_task(async function test_tabs_mediaIndicators() {
}
let extdata = {
manifest: {permissions: ["tabs"]},
manifest: {permissions: ["tabs", "tabHide"]},
useAddonManager: "temporary",
background,
};

View File

@ -20,7 +20,7 @@
* promiseContentDimensions alterContent
* promisePrefChangeObserved openContextMenuInFrame
* promiseAnimationFrame getCustomizableUIPanelID
* awaitEvent
* awaitEvent BrowserWindowIterator
*/
// There are shutdown issues for which multiple rejections are left uncaught.
@ -484,3 +484,13 @@ function awaitEvent(eventName, id) {
Management.on(eventName, listener);
});
}
function* BrowserWindowIterator() {
let windowsEnum = Services.wm.getEnumerator("navigator:browser");
while (windowsEnum.hasMoreElements()) {
let currentWindow = windowsEnum.getNext();
if (!currentWindow.closed) {
yield currentWindow;
}
}
}

View File

@ -116,6 +116,7 @@ webextPerms.description.privacy=Read and modify privacy settings
webextPerms.description.proxy=Control browser proxy settings
webextPerms.description.sessions=Access recently closed tabs
webextPerms.description.tabs=Access browser tabs
webextPerms.description.tabHide=Hide and show browser tabs
webextPerms.description.topSites=Access browsing history
webextPerms.description.unlimitedStorage=Store unlimited amount of client-side data
webextPerms.description.webNavigation=Access browser activity during navigation

View File

@ -5026,6 +5026,9 @@ pref("extensions.webextensions.remote", false);
// unless other process sandboxing and extension remoting prefs are changed.
pref("extensions.webextensions.protocol.remote", true);
// Disable tab hiding API by default.
pref("extensions.webextensions.tabhide.enabled", false);
// Report Site Issue button
pref("extensions.webcompat-reporter.newIssueEndpoint", "https://webcompat.com/issues/new");
#if defined(MOZ_DEV_EDITION) || defined(NIGHTLY_BUILD)