Bug 1250784: Part 3 - [webext] Add suport for runtime.openOptionsPage. r=Mossop

MozReview-Commit-ID: 9izx4uX0Szd

--HG--
extra : rebase_source : d474f77b37007f8b7e3118781af4b3d8d64aac61
This commit is contained in:
Kris Maglione 2016-03-09 17:10:29 -08:00
parent 7dfe8256b3
commit 128a5928cb
10 changed files with 364 additions and 39 deletions

View File

@ -6270,45 +6270,51 @@ var MailIntegration = {
};
function BrowserOpenAddonsMgr(aView) {
if (aView) {
let emWindow;
let browserWindow;
return new Promise(resolve => {
if (aView) {
let emWindow;
let browserWindow;
var receivePong = function receivePong(aSubject, aTopic, aData) {
let browserWin = aSubject.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShellTreeItem)
.rootTreeItem
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
if (!emWindow || browserWin == window /* favor the current window */) {
emWindow = aSubject;
browserWindow = browserWin;
var receivePong = function receivePong(aSubject, aTopic, aData) {
let browserWin = aSubject.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShellTreeItem)
.rootTreeItem
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
if (!emWindow || browserWin == window /* favor the current window */) {
emWindow = aSubject;
browserWindow = browserWin;
}
}
Services.obs.addObserver(receivePong, "EM-pong", false);
Services.obs.notifyObservers(null, "EM-ping", "");
Services.obs.removeObserver(receivePong, "EM-pong");
if (emWindow) {
emWindow.loadView(aView);
browserWindow.gBrowser.selectedTab =
browserWindow.gBrowser._getTabForContentWindow(emWindow);
emWindow.focus();
resolve(emWindow);
return;
}
}
Services.obs.addObserver(receivePong, "EM-pong", false);
Services.obs.notifyObservers(null, "EM-ping", "");
Services.obs.removeObserver(receivePong, "EM-pong");
if (emWindow) {
emWindow.loadView(aView);
browserWindow.gBrowser.selectedTab =
browserWindow.gBrowser._getTabForContentWindow(emWindow);
emWindow.focus();
return;
switchToTabHavingURI("about:addons", true);
if (aView) {
// This must be a new load, else the ping/pong would have
// found the window above.
Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
Services.obs.removeObserver(observer, aTopic);
aSubject.loadView(aView);
resolve(aSubject);
}, "EM-loaded", false);
} else {
resolve();
}
}
var newLoad = !switchToTabHavingURI("about:addons", true);
if (aView) {
// This must be a new load, else the ping/pong would have
// found the window above.
Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
Services.obs.removeObserver(observer, aTopic);
aSubject.loadView(aView);
}, "EM-loaded", false);
}
});
}
function AddKeywordForSearchField() {

View File

@ -3,8 +3,24 @@
/* eslint-disable mozilla/balanced-listeners */
extensions.on("uninstall", (msg, extension) => {
if (extension.uninstallURL) {
let browser = Services.wm.getMostRecentWindow("navigator:browser").gBrowser;
let browser = WindowManager.topWindow.gBrowser;
browser.addTab(extension.uninstallURL, {relatedToCurrent: true});
}
});
global.openOptionsPage = (extension) => {
let window = WindowManager.topWindow;
if (!window) {
return Promise.reject({message: "No browser window available"});
}
if (extension.manifest.options_ui.open_in_tab) {
window.switchToTabHavingURI(extension.manifest.options_ui.page, true);
return Promise.resolve();
}
let viewId = `addons://detail/${encodeURIComponent(extension.id)}/preferences`;
return window.BrowserOpenAddonsMgr(viewId);
};

View File

@ -27,6 +27,7 @@ support-files =
[browser_ext_commands_onCommand.js]
[browser_ext_getViews.js]
[browser_ext_lastError.js]
[browser_ext_runtime_openOptionsPage.js]
[browser_ext_runtime_setUninstallURL.js]
[browser_ext_tabs_audio.js]
[browser_ext_tabs_captureVisibleTab.js]

View File

@ -0,0 +1,228 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
function* loadExtension(options) {
let extension = ExtensionTestUtils.loadExtension({
useAddonManager: true,
manifest: Object.assign({
"permissions": ["tabs"],
}, options.manifest),
files: {
"options.html": `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="options.js" type="text/javascript"></script>
</head>
</html>`,
"options.js": function() {
browser.runtime.sendMessage("options.html");
browser.runtime.onMessage.addListener((msg, sender, respond) => {
if (msg == "ping") {
respond("pong");
}
});
},
},
background: options.background,
});
yield extension.startup();
return extension;
}
add_task(function* test_inline_options() {
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
let extension = yield loadExtension({
manifest: {
"options_ui": {
"page": "options.html",
},
},
background: function() {
let _optionsPromise;
let awaitOptions = () => {
browser.test.assertFalse(_optionsPromise, "Should not be awaiting options already");
return new Promise(resolve => {
_optionsPromise = {resolve};
});
};
browser.runtime.onMessage.addListener((msg, sender) => {
if (msg == "options.html") {
if (_optionsPromise) {
_optionsPromise.resolve(sender.tab);
_optionsPromise = null;
} else {
browser.test.fail("Saw unexpected options page load");
}
}
});
let firstTab, optionsTab;
browser.tabs.query({currentWindow: true, active: true}).then(tabs => {
firstTab = tabs[0].id;
browser.test.log("Open options page. Expect fresh load.");
return Promise.all([
browser.runtime.openOptionsPage(),
awaitOptions(),
]);
}).then(([, tab]) => {
browser.test.assertEq("about:addons", tab.url, "Tab contains AddonManager");
browser.test.assertTrue(tab.active, "Tab is active");
browser.test.assertTrue(tab.id != firstTab, "Tab is a new tab");
optionsTab = tab.id;
browser.test.log("Switch tabs.");
return browser.tabs.update(firstTab, {active: true});
}).then(() => {
browser.test.log("Open options page again. Expect tab re-selected, no new load.");
return browser.runtime.openOptionsPage();
}).then(() => {
return browser.tabs.query({currentWindow: true, active: true});
}).then(([tab]) => {
browser.test.assertEq(optionsTab, tab.id, "Tab is the same as the previous options tab");
browser.test.assertEq("about:addons", tab.url, "Tab contains AddonManager");
browser.test.log("Ping options page.");
return new Promise(resolve => browser.tabs.sendMessage(optionsTab, "ping", resolve));
}).then(() => {
browser.test.log("Got pong.");
browser.test.log("Remove options tab.");
return browser.tabs.remove(optionsTab);
}).then(() => {
browser.test.log("Open options page again. Expect fresh load.");
return Promise.all([
browser.runtime.openOptionsPage(),
awaitOptions(),
]);
}).then(([, tab]) => {
browser.test.assertEq("about:addons", tab.url, "Tab contains AddonManager");
browser.test.assertTrue(tab.active, "Tab is active");
browser.test.assertTrue(tab.id != optionsTab, "Tab is a new tab");
return browser.tabs.remove(tab.id);
}).then(() => {
browser.test.notifyPass("options-ui");
}).catch(error => {
browser.test.log(`Error: ${error} :: ${error.stack}`);
browser.test.notifyFail("options-ui");
});
},
});
yield extension.awaitFinish("options-ui");
yield extension.unload();
yield BrowserTestUtils.removeTab(tab);
});
add_task(function* test_tab_options() {
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
let extension = yield loadExtension({
manifest: {
"options_ui": {
"page": "options.html",
"open_in_tab": true,
},
},
background: function() {
let _optionsPromise;
let awaitOptions = () => {
browser.test.assertFalse(_optionsPromise, "Should not be awaiting options already");
return new Promise(resolve => {
_optionsPromise = {resolve};
});
};
browser.runtime.onMessage.addListener((msg, sender) => {
if (msg == "options.html") {
if (_optionsPromise) {
_optionsPromise.resolve(sender.tab);
_optionsPromise = null;
} else {
browser.test.fail("Saw unexpected options page load");
}
}
});
let optionsURL = browser.extension.getURL("options.html");
let firstTab, optionsTab;
browser.tabs.query({currentWindow: true, active: true}).then(tabs => {
firstTab = tabs[0].id;
browser.test.log("Open options page. Expect fresh load.");
return Promise.all([
browser.runtime.openOptionsPage(),
awaitOptions(),
]);
}).then(([, tab]) => {
browser.test.assertEq(optionsURL, tab.url, "Tab contains options.html");
browser.test.assertTrue(tab.active, "Tab is active");
browser.test.assertTrue(tab.id != firstTab, "Tab is a new tab");
optionsTab = tab.id;
browser.test.log("Switch tabs.");
return browser.tabs.update(firstTab, {active: true});
}).then(() => {
browser.test.log("Open options page again. Expect tab re-selected, no new load.");
return browser.runtime.openOptionsPage();
}).then(() => {
return browser.tabs.query({currentWindow: true, active: true});
}).then(([tab]) => {
browser.test.assertEq(optionsTab, tab.id, "Tab is the same as the previous options tab");
browser.test.assertEq(optionsURL, tab.url, "Tab contains options.html");
// Unfortunately, we can't currently do this, since onMessage doesn't
// currently support responses when there are multiple listeners.
//
// browser.test.log("Ping options page.");
// return new Promise(resolve => browser.runtime.sendMessage("ping", resolve));
browser.test.log("Remove options tab.");
return browser.tabs.remove(optionsTab);
}).then(() => {
browser.test.log("Open options page again. Expect fresh load.");
return Promise.all([
browser.runtime.openOptionsPage(),
awaitOptions(),
]);
}).then(([, tab]) => {
browser.test.assertEq(optionsURL, tab.url, "Tab contains options.html");
browser.test.assertTrue(tab.active, "Tab is active");
browser.test.assertTrue(tab.id != optionsTab, "Tab is a new tab");
return browser.tabs.remove(tab.id);
}).then(() => {
browser.test.notifyPass("options-ui-tab");
}).catch(error => {
browser.test.log(`Error: ${error} :: ${error.stack}`);
browser.test.notifyFail("options-ui-tab");
});
},
});
yield extension.awaitFinish("options-ui-tab");
yield extension.unload();
yield BrowserTestUtils.removeTab(tab);
});

View File

@ -17,6 +17,7 @@
"ExtensionManagement": true,
"ExtensionPage": true,
"GlobalManager": true,
"openOptionsPage": true,
"runSafe": true,
"runSafeSync": true,
"runSafeSyncWithoutClone": true,

View File

@ -989,6 +989,65 @@ this.Extension.generateXPI = function(id, data) {
return file;
};
/**
* A skeleton Extension-like object, used for testing, which installs an
* add-on via the add-on manager when startup() is called, and
* uninstalles it on shutdown().
*/
function MockExtension(id, file, rootURI) {
this.id = id;
this.file = file;
this.rootURI = rootURI;
this._extension = null;
this._extensionPromise = new Promise(resolve => {
let onstartup = (msg, extension) => {
if (extension.id == this.id) {
Management.off("startup", onstartup);
this._extension = extension;
resolve(extension);
}
};
Management.on("startup", onstartup);
});
}
MockExtension.prototype = {
testMessage(...args) {
return this._extension.testMessage(...args);
},
on(...args) {
this._extensionPromise.then(extension => {
extension.on(...args);
});
},
off(...args) {
this._extensionPromise.then(extension => {
extension.off(...args);
});
},
startup() {
return AddonManager.installTemporaryAddon(this.file).then(addon => {
this.addon = addon;
return this._extensionPromise;
});
},
shutdown() {
this.addon.uninstall(true);
return this.cleanupGeneratedFile();
},
cleanupGeneratedFile() {
flushJarCache(this.file);
return OS.File.remove(this.file.path);
},
};
/**
* Generates a new extension using |Extension.generateXPI|, and initializes a
* new |Extension| instance which will execute it.
@ -1002,6 +1061,10 @@ this.Extension.generate = function(id, data) {
let fileURI = Services.io.newFileURI(file);
let jarURI = Services.io.newURI("jar:" + fileURI.spec + "!/", null, null);
if (data.useAddonManager) {
return new MockExtension(id, file, jarURI);
}
return new Extension({
id,
resourceURI: jarURI,

View File

@ -439,7 +439,9 @@ this.MessageChannel = {
*/
removeListener(targets, messageName, handler) {
for (let target of [].concat(targets)) {
this.messageManagers.get(target).removeHandler(messageName, handler);
if (this.messageManagers.has(target)) {
this.messageManagers.get(target).removeHandler(messageName, handler);
}
}
},

View File

@ -68,6 +68,14 @@ extensions.registerSchemaAPI("runtime", null, (extension, context) => {
return Promise.resolve(ExtensionUtils.PlatformInfo);
},
openOptionsPage: function() {
if (!extension.manifest.options_ui) {
return Promise.reject({message: "No `options_ui` declared"});
}
return openOptionsPage(extension).then(() => {});
},
setUninstallURL: function(url) {
if (url.length == 0) {
return Promise.resolve();

View File

@ -134,7 +134,6 @@
},
{
"name": "openOptionsPage",
"unsupported": true,
"type": "function",
"description": "<p>Open your Extension's options page, if possible.</p><p>The precise behavior may depend on your manifest's <code>$(topic:optionsV2)[options_ui]</code> or <code>$(topic:options)[options_page]</code> key, or what the browser happens to support at the time.</p><p>If your Extension does not declare an options page, or the browser failed to create one for some other reason, the callback will set $(ref:lastError).</p>",
"async": "callback",

View File

@ -3818,8 +3818,9 @@ this.XPIProvider = {
* @param aFile
* An nsIFile for the unpacked add-on directory or XPI file.
*
* @return a Promise that rejects if the add-on is not a valid restartless
* add-on or if the same ID is already temporarily installed
* @return a Promise that resolves to an Addon object on success, or rejects
* if the add-on is not a valid restartless add-on or if the
* same ID is already temporarily installed
*/
installTemporaryAddon: Task.async(function*(aFile) {
let addon = yield loadManifestFromFile(aFile, TemporaryInstallLocation);