mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-16 23:05:42 +00:00
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:
parent
7dfe8256b3
commit
128a5928cb
@ -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() {
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
@ -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]
|
||||
|
@ -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);
|
||||
});
|
@ -17,6 +17,7 @@
|
||||
"ExtensionManagement": true,
|
||||
"ExtensionPage": true,
|
||||
"GlobalManager": true,
|
||||
"openOptionsPage": true,
|
||||
"runSafe": true,
|
||||
"runSafeSync": true,
|
||||
"runSafeSyncWithoutClone": true,
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user