mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-10 11:55:49 +00:00
Bug 1423725 add show/hide tabs api, r=rpl
MozReview-Commit-ID: 4z73ZTRE7kN --HG-- extra : rebase_source : 7683973921b07818c7a63ab8387e4ebe65705499
This commit is contained in:
parent
d78ea2931e
commit
636a2a5a8f
@ -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;
|
||||
|
@ -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": [
|
||||
|
@ -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]
|
||||
|
@ -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]);
|
||||
});
|
@ -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,
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user