Bug 1420681 - add forceDiscard property to tabs.discard - V4
MozReview-Commit-ID: 8tHxOngj1ln
--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -445,24 +445,30 @@ this.tabs = class extends ExtensionAPI {
}
for (let tabId of tabs) {
let nativeTab = tabTracker.getTab(tabId);
nativeTab.ownerGlobal.gBrowser.removeTab(nativeTab);
}
},
- async discard(tabIds) {
+ async discard(tabIds, discardProperties) {
if (!Array.isArray(tabIds)) {
tabIds = [tabIds];
}
let tabs = tabIds.map(tabId => tabTracker.getTab(tabId));
for (let tab of tabs) {
- tab.ownerGlobal.gBrowser.discardBrowser(tab.linkedBrowser);
+ tab.ownerGlobal.gBrowser.discardBrowser(tab.linkedBrowser, discardProperties &&
+ discardProperties.forceDiscard);
+ }
+
+ // For Chrome compatibility.
+ if (tabs.length == 1 && !tabs[0].linkedPanel) {
+ return tabManager.convert(tabs[0]);
}
},
async update(tabId, updateProperties) {
let nativeTab = getTabOrActive(tabId);
let tabbrowser = nativeTab.ownerGlobal.gBrowser;
--- a/browser/components/extensions/schemas/tabs.json
+++ b/browser/components/extensions/schemas/tabs.json
@@ -878,25 +878,49 @@
"parameters": []
}
]
},
{
"name": "discard",
"type": "function",
"description": "discards one or more tabs.",
- "async": true,
+ "async": "callback",
"parameters": [
{
"name": "tabIds",
"description": "The tab or list of tabs to discard.",
"choices": [
{"type": "integer", "minimum": 0},
{"type": "array", "items": {"type": "integer", "minimum": 0}}
]
+ },
+ {
+ "type": "object",
+ "name": "discardProperties",
+ "optional": true,
+ "properties": {
+ "forceDiscard": {
+ "type": "boolean",
+ "optional": true,
+ "description": "Forces discard regardless of whether an onbeforeunload handler wants to prompt. Will suppress display of prompt, but will run all onbeforeunload handlers. Default is `false`."
+ }
+ }
+ },
+ {
+ "type": "function",
+ "name": "callback",
+ "optional": true,
+ "parameters": [
+ {
+ "type": "object",
+ "name": "tab",
+ "description": "Tab that was discarded, undefined if discard was unsuccessful."
+ }
+ ]
}
]
},
{
"name": "detectLanguage",
"type": "function",
"description": "Detects the primary language of the content in a tab.",
"async": "callback",
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -19,16 +19,17 @@ support-files =
file_bypass_cache.sjs
file_language_fr_en.html
file_language_ja.html
file_language_tlh.html
file_dummy.html
file_title.html
file_inspectedwindow_reload_target.sjs
file_indexedDB.html
+ file_onbeforeunload.html
file_serviceWorker.html
locale/chrome.manifest
webNav_createdTarget.html
webNav_createdTargetSource.html
webNav_createdTargetSource_subframe.html
serviceWorker.js
searchSuggestionEngine.xml
searchSuggestionEngine.sjs
--- a/browser/components/extensions/test/browser/browser_ext_tabs_discard.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_discard.js
@@ -1,62 +1,120 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
/* global gBrowser SessionStore */
"use strict";
+const BASE_URL = "http://mochi.test:8888/browser/browser/components/extensions/test/browser/";
+
add_task(async function test_discarded() {
+ await SpecialPowers.pushPrefEnv({
+ "set": [
+ ["dom.require_user_interaction_for_beforeunload", false],
+ ],
+ });
+
let extension = ExtensionTestUtils.loadExtension({
manifest: {
"permissions": ["tabs"],
},
background: async function() {
+ function awaitDiscardTab(tabIds, param1) {
+ return new Promise(resolve => {
+ let count = 0;
+ let expectedTabIds = Array.isArray(tabIds) ? tabIds : [tabIds];
+ browser.tabs.onUpdated.addListener(async function onUpdatedListener(updatedTabId, updatedInfo) {
+ if ("discarded" in updatedInfo && expectedTabIds.includes(updatedTabId)) {
+ if (++count == expectedTabIds.length) {
+ browser.tabs.onUpdated.removeListener(onUpdatedListener);
+ resolve();
+ }
+ }
+ });
+ browser.tabs.discard(tabIds, param1);
+ });
+ }
+
+ function awaitDiscardTabCallback(params) {
+ return new Promise(resolve => {
+ let callback = tab => {
+ resolve(tab);
+ };
+ browser.tabs.discard(...params, callback);
+ });
+ }
+
let tabs = await browser.tabs.query({currentWindow: true});
tabs.sort((tab1, tab2) => tab1.index - tab2.index);
- async function finishTest() {
- try {
- await browser.tabs.discard(tabs[0].id);
- await browser.tabs.discard(tabs[2].id);
- browser.test.succeed("attempting to discard an already discarded tab or the active tab should not throw error");
- } catch (e) {
- browser.test.fail("attempting to discard an already discarded tab or the active tab should not throw error");
- }
- let discardedTab = await browser.tabs.get(tabs[2].id);
- browser.test.assertEq(false, discardedTab.discarded, "attempting to discard the active tab should not have succeeded");
+ await awaitDiscardTab(tabs[0].id);
+ browser.test.succeed("tab is discarded");
- await browser.test.assertRejects(browser.tabs.discard(999999999), /Invalid tab ID/, "attempt to discard invalid tabId should throw");
- await browser.test.assertRejects(browser.tabs.discard([999999999, tabs[1].id]), /Invalid tab ID/, "attempt to discard a valid and invalid tabId should throw");
- discardedTab = await browser.tabs.get(tabs[1].id);
- browser.test.assertEq(false, discardedTab.discarded, "tab is still not discarded");
-
- browser.test.notifyPass("test-finished");
+ try {
+ await browser.tabs.discard(tabs[0].id);
+ browser.test.succeed("attempting to discard an already discarded tab");
+ } catch (e) {
+ browser.test.fail("attempting to discard an already discarded tab");
+ }
+ try {
+ await browser.tabs.discard(tabs[3].id);
+ browser.test.succeed("attempting to discard the active tab should not throw error");
+ } catch (e) {
+ browser.test.fail("attempting to discard the active tab should not throw error");
}
- browser.tabs.onUpdated.addListener(async function(tabId, updatedInfo) {
- if ("discarded" in updatedInfo) {
- browser.test.assertEq(tabId, tabs[0].id, "discarding tab triggered onUpdated");
- let discardedTab = await browser.tabs.get(tabs[0].id);
- browser.test.assertEq(true, discardedTab.discarded, "discarded tab discard property");
+ let discardedTab = await browser.tabs.get(tabs[3].id);
+ browser.test.assertEq(false, discardedTab.discarded, "attempting to discard the active tab should not have succeeded");
+
+ await browser.test.assertRejects(browser.tabs.discard(999999999), /Invalid tab ID/, "attempt to discard invalid tabId should throw");
+ await browser.test.assertRejects(browser.tabs.discard([999999999, tabs[1].id]), /Invalid tab ID/, "attempt to discard a valid and invalid tabId should throw");
+ discardedTab = await browser.tabs.get(tabs[1].id);
+ browser.test.assertEq(false, discardedTab.discarded, "tab is still not discarded");
+
+ await browser.tabs.update(tabs[0].id, {active: true});
+
+ discardedTab = await awaitDiscardTabCallback([tabs[1].id]);
+ browser.test.assertEq(true, discardedTab.discarded, "callback runs when discarding a tab, and returns a Tab object showing the tab was discarded");
+
+ discardedTab = await awaitDiscardTabCallback([tabs[0].id]);
+ browser.test.assertEq(undefined, discardedTab, "callback runs when attempting to discard a tab which cannot be discarded, and returns undefined");
- await finishTest();
- }
- });
+ await browser.tabs.discard(tabs[3].id);
+ discardedTab = await browser.tabs.get(tabs[3].id);
+ browser.test.assertEq(false, discardedTab.discarded, "attempt to discard a tab with a blocking beforeunload handler should not have succeeded");
+
+ await awaitDiscardTab(tabs[3].id, {forceDiscard: true});
+ browser.test.succeed("tab with a blocking beforeunload handler is discarded when forceDiscard property is true");
- browser.tabs.discard(tabs[0].id);
+ await awaitDiscardTab([tabs[2].id, tabs[4].id], {forceDiscard: true});
+ browser.test.succeed("array of tabs which include a tab with a blocking beforeunload handler are discarded when forceDiscard property is true");
+
+ discardedTab = await awaitDiscardTabCallback([tabs[5].id, {forceDiscard: true}]);
+ browser.test.assertEq(true, discardedTab.discarded, "calling discardTab with single tab with a blocking beforeunload handler, forceDiscard property, and callback, discards properly; callback returns tab object");
+
+ discardedTab = await awaitDiscardTabCallback([[tabs[6].id, tabs[7].id], {forceDiscard: true}]);
+ browser.test.assertEq(undefined, discardedTab, "calling discardTab with array of tabs with blocking beforeunload handlers, forceDiscard property, and callback, discards properly; callback returns undefined");
+
+ browser.test.notifyPass("test-finished");
},
});
BrowserTestUtils.loadURI(gBrowser.browsers[0], "http://example.com");
await BrowserTestUtils.browserLoaded(gBrowser.browsers[0]);
- let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com");
- let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com");
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com");
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com");
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URL + "file_onbeforeunload.html");
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URL + "file_onbeforeunload.html");
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URL + "file_onbeforeunload.html");
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URL + "file_onbeforeunload.html");
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URL + "file_onbeforeunload.html");
await extension.startup();
await extension.awaitFinish("test-finished");
await extension.unload();
- await BrowserTestUtils.removeTab(tab1);
- await BrowserTestUtils.removeTab(tab2);
+ while (gBrowser.tabs.length > 1) {
+ await BrowserTestUtils.removeTab(gBrowser.tabs[1]);
+ }
});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/file_onbeforeunload.html
@@ -0,0 +1,14 @@
+<html>
+<head>
+<script>
+"use strict";
+
+window.onbeforeunload = function() {
+ return true;
+};
+</script>
+</head>
+<body>
+TEST PAGE
+</body>
+</html>