Bug 1378647 - support creating lazy tabs from extensions, r?rpl
MozReview-Commit-ID: 9QMkNtCQG6P
--- a/browser/components/extensions/parent/ext-tabs.js
+++ b/browser/components/extensions/parent/ext-tabs.js
@@ -9,16 +9,18 @@ ChromeUtils.defineModuleGetter(this, "Ex
ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
ChromeUtils.defineModuleGetter(this, "PromiseUtils",
"resource://gre/modules/PromiseUtils.jsm");
ChromeUtils.defineModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
ChromeUtils.defineModuleGetter(this, "SessionStore",
"resource:///modules/sessionstore/SessionStore.jsm");
+ChromeUtils.defineModuleGetter(this, "Utils",
+ "resource://gre/modules/sessionstore/Utils.jsm");
XPCOMUtils.defineLazyGetter(this, "strBundle", function() {
return Services.strings.createBundle("chrome://global/locale/extensions.properties");
});
var {
ExtensionError,
} = ExtensionUtils;
@@ -554,45 +556,76 @@ this.tabs = class extends ExtensionAPI {
if (!containerId) {
return Promise.reject({message: `No cookie store exists with ID ${createProperties.cookieStoreId}`});
}
options.userContextId = containerId;
}
}
- // Make sure things like about:blank and data: URIs never inherit,
- // and instead always get a NullPrincipal.
- options.disallowInheritPrincipal = true;
+ // Only set disallowInheritPrincipal on non-discardable urls as it
+ // will override creating a lazy browser. Setting triggeringPrincipal
+ // will ensure other cases are handled, but setting it may prevent
+ // creating about and data urls.
+ let discardable = url && !url.startsWith("about:") && !url.startsWith("data:");
+ if (!discardable) {
+ // Make sure things like about:blank and data: URIs never inherit,
+ // and instead always get a NullPrincipal.
+ options.disallowInheritPrincipal = true;
+ } else {
+ options.triggeringPrincipal = context.principal;
+ }
tabListener.initTabReady();
let currentTab = window.gBrowser.selectedTab;
if (createProperties.openerTabId !== null) {
options.ownerTab = tabTracker.getTab(createProperties.openerTabId);
options.openerBrowser = options.ownerTab.linkedBrowser;
if (options.ownerTab.ownerGlobal !== window) {
return Promise.reject({message: "Opener tab must be in the same window as the tab being created"});
}
}
- if (createProperties.index != null) {
- options.index = createProperties.index;
+ // Simple properties
+ const properties = ["index", "pinned", "title"];
+ for (let prop of properties) {
+ if (createProperties[prop] != null) {
+ options[prop] = createProperties[prop];
+ }
}
- if (createProperties.pinned != null) {
- options.pinned = createProperties.pinned;
- }
-
- let nativeTab = window.gBrowser.addTab(url || window.BROWSER_NEW_TAB_URL, options);
-
let active = true;
if (createProperties.active !== null) {
active = createProperties.active;
}
+ if (createProperties.discarded) {
+ if (active) {
+ return Promise.reject({message: `Active tabs cannot be created and discarded.`});
+ }
+ if (createProperties.pinned) {
+ return Promise.reject({message: `Pinned tabs cannot be created and discarded.`});
+ }
+ if (!discardable) {
+ return Promise.reject({message: `Cannot create a discarded new tab, about or data urls.`});
+ }
+ options.createLazyBrowser = true;
+ }
+
+ let nativeTab = window.gBrowser.addTab(url || window.BROWSER_NEW_TAB_URL, options);
+ if (createProperties.discarded) {
+ SessionStore.setTabState(nativeTab, JSON.stringify({
+ entries: [{
+ url: url,
+ title: options.title,
+ triggeringPrincipal_base64: Utils.serializePrincipal(context.principal),
+ }],
+ }));
+ }
+
if (active) {
window.gBrowser.selectedTab = nativeTab;
}
if (active && !url) {
window.focusAndSelectUrlBar();
}
--- a/browser/components/extensions/schemas/tabs.json
+++ b/browser/components/extensions/schemas/tabs.json
@@ -551,16 +551,17 @@
"url": {
"type": "string",
"optional": true,
"description": "The URL to navigate the tab to initially. Fully-qualified URLs must include a scheme (i.e. 'http://www.google.com', not 'www.google.com'). Relative URLs will be relative to the current page within the extension. Defaults to the New Tab Page."
},
"active": {
"type": "boolean",
"optional": true,
+ "default": true,
"description": "Whether the tab should become the active tab in the window. Does not affect whether the window is focused (see $(ref:windows.update)). Defaults to <var>true</var>."
},
"selected": {
"deprecated": "Please use <em>active</em>.",
"unsupported": true,
"type": "boolean",
"optional": true,
"description": "Whether the tab should become the selected tab in the window. Defaults to <var>true</var>"
@@ -578,17 +579,29 @@
},
"cookieStoreId": {
"type": "string",
"optional": true,
"description": "The CookieStoreId for the tab that opened this tab."
},
"openInReaderMode": {
"type": "boolean",
- "optional": true, "description": "Whether the document in the tab should be opened in reader mode."}
+ "optional": true,
+ "description": "Whether the document in the tab should be opened in reader mode."
+ },
+ "discarded": {
+ "type": "boolean",
+ "optional": true,
+ "description": "Whether the tab is discarded when created."
+ },
+ "title": {
+ "type": "string",
+ "optional": true,
+ "description": "The title used for display if the tab is created in discarded mode."
+ }
}
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": [
{
--- a/browser/components/extensions/test/browser/browser_ext_tabs_discarded.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_discarded.js
@@ -6,70 +6,43 @@
const {Utils} = ChromeUtils.import("resource://gre/modules/sessionstore/Utils.jsm", {});
const triggeringPrincipal_base64 = Utils.SERIALIZED_SYSTEMPRINCIPAL;
let lazyTabState = {entries: [{url: "http://example.com/", triggeringPrincipal_base64, title: "Example Domain"}]};
add_task(async function test_discarded() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
- "permissions": ["tabs"],
+ "permissions": ["tabs", "webNavigation"],
},
background: async function() {
- let onCreatedTabData = [];
- let discardedEventData = [];
-
- async function finishTest() {
- browser.test.assertEq(0, discardedEventData.length, "number of discarded events fired");
-
- onCreatedTabData.sort((data1, data2) => data1.index - data2.index);
- browser.test.assertEq(false, onCreatedTabData[0].discarded, "non-lazy tab onCreated discard property");
- browser.test.assertEq(true, onCreatedTabData[1].discarded, "lazy tab onCreated discard property");
-
- let tabs = await browser.tabs.query({currentWindow: true});
- tabs.sort((tab1, tab2) => tab1.index - tab2.index);
-
- browser.test.assertEq(false, tabs[1].discarded, "non-lazy tab query discard property");
- browser.test.assertEq(true, tabs[2].discarded, "lazy tab query discard property");
-
- let updatedTab = await browser.tabs.update(tabs[2].id, {active: true});
+ browser.webNavigation.onCompleted.addListener(async (details) => {
+ browser.test.log(`webNav onCompleted received for ${details.tabId}`);
+ let updatedTab = await browser.tabs.get(details.tabId);
browser.test.assertEq(false, updatedTab.discarded, "lazy to non-lazy update discard property");
- browser.test.assertEq(false, discardedEventData[0], "lazy to non-lazy onUpdated discard property");
-
browser.test.notifyPass("test-finished");
- }
-
- browser.tabs.onUpdated.addListener(function(tabId, updatedInfo) {
- if ("discarded" in updatedInfo) {
- discardedEventData.push(updatedInfo.discarded);
- }
- });
+ }, {url: [{hostContains: "example.com"}]});
browser.tabs.onCreated.addListener(function(tab) {
- onCreatedTabData.push({discarded: tab.discarded, index: tab.index});
- if (onCreatedTabData.length == 2) {
- finishTest();
- }
+ browser.test.assertEq(true, tab.discarded, "non-lazy tab onCreated discard property");
+ browser.tabs.update(tab.id, {active: true});
});
},
});
await extension.startup();
- let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com");
-
- let tab2 = BrowserTestUtils.addTab(gBrowser, "about:blank", {createLazyBrowser: true});
- SessionStore.setTabState(tab2, JSON.stringify(lazyTabState));
+ let testTab = BrowserTestUtils.addTab(gBrowser, "about:blank", {createLazyBrowser: true});
+ SessionStore.setTabState(testTab, JSON.stringify(lazyTabState));
await extension.awaitFinish("test-finished");
await extension.unload();
- BrowserTestUtils.removeTab(tab1);
- BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(testTab);
});
// If discard is called immediately after creating a new tab, the new tab may not have loaded,
// and the sessionstore for that tab is not ready for discarding. The result was a corrupted
// sessionstore for the tab, which when the tab was activated, resulted in a tab with partial
// state.
add_task(async function test_create_then_discard() {
let extension = ExtensionTestUtils.loadExtension({
@@ -100,8 +73,44 @@ add_task(async function test_create_then
createdTab = await browser.tabs.create({url: "http://example.com/", active: false});
browser.tabs.discard(createdTab.id);
},
});
await extension.startup();
await extension.awaitFinish("test-finished");
await extension.unload();
});
+
+add_task(async function test_create_discarded() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "permissions": ["tabs", "webNavigation"],
+ },
+
+ background: async function() {
+ let tabOpts = {
+ url: "http://example.com/",
+ active: false,
+ discarded: true,
+ title: "discarded tab",
+ };
+
+ browser.webNavigation.onCompleted.addListener(async (details) => {
+ let activeTab = await browser.tabs.get(details.tabId);
+ browser.test.assertEq(tabOpts.url, activeTab.url, "restored tab url matches active tab url");
+ browser.test.assertEq("mochitest index /", activeTab.title, "restored tab title is correct");
+ browser.tabs.remove(details.tabId);
+ browser.test.notifyPass("test-finished");
+ }, {url: [{hostContains: "example.com"}]});
+
+ browser.tabs.onCreated.addListener(tab => {
+ browser.test.assertEq(tabOpts.url, tab.url, "lazy tab url is correct");
+ browser.test.assertEq(tabOpts.title, tab.title, "lazy tab title is correct");
+ browser.tabs.update(tab.id, {active: true});
+ });
+
+ browser.tabs.create(tabOpts);
+ },
+ });
+ await extension.startup();
+ await extension.awaitFinish("test-finished");
+ await extension.unload();
+});