mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 1197422 - Part 2: [webext] Implement the pageAction API. r=billm ui-r=bwinton
This commit is contained in:
parent
6336eb1bba
commit
d7cd4b0dea
@ -1,3 +1,7 @@
|
||||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
|
||||
"resource:///modules/CustomizableUI.jsm");
|
||||
|
||||
@ -14,22 +18,11 @@ var {
|
||||
// WeakMap[Extension -> BrowserAction]
|
||||
var browserActionMap = new WeakMap();
|
||||
|
||||
// WeakMap[Extension -> docshell]
|
||||
// This map is a cache of the windowless browser that's used to render ImageData
|
||||
// for the browser_action icon.
|
||||
var imageRendererMap = new WeakMap();
|
||||
|
||||
function browserActionOf(extension)
|
||||
{
|
||||
return browserActionMap.get(extension);
|
||||
}
|
||||
|
||||
function makeWidgetId(id)
|
||||
{
|
||||
id = id.toLowerCase();
|
||||
return id.replace(/[^a-z0-9_-]/g, "_");
|
||||
}
|
||||
|
||||
var nextActionId = 0;
|
||||
|
||||
// Responsible for the browser_action section of the manifest as well
|
||||
@ -43,10 +36,8 @@ function BrowserAction(options, extension)
|
||||
this.title = new DefaultWeakMap(extension.localize(options.default_title));
|
||||
this.badgeText = new DefaultWeakMap();
|
||||
this.badgeBackgroundColor = new DefaultWeakMap();
|
||||
this.icon = new DefaultWeakMap(options.default_icon);
|
||||
this.icon = new DefaultWeakMap(IconDetails.normalize({path: options.default_icon}, extension));
|
||||
this.popup = new DefaultWeakMap(options.default_popup);
|
||||
|
||||
this.context = null;
|
||||
}
|
||||
|
||||
BrowserAction.prototype = {
|
||||
@ -95,67 +86,7 @@ BrowserAction.prototype = {
|
||||
},
|
||||
|
||||
togglePopup(node, popupResource) {
|
||||
let popupURL = this.extension.baseURI.resolve(popupResource);
|
||||
|
||||
let document = node.ownerDocument;
|
||||
let panel = document.createElement("panel");
|
||||
panel.setAttribute("class", "browser-action-panel");
|
||||
panel.setAttribute("type", "arrow");
|
||||
panel.setAttribute("flip", "slide");
|
||||
node.appendChild(panel);
|
||||
|
||||
panel.addEventListener("popuphidden", () => {
|
||||
this.context.unload();
|
||||
this.context = null;
|
||||
panel.remove();
|
||||
});
|
||||
|
||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
let browser = document.createElementNS(XUL_NS, "browser");
|
||||
browser.setAttribute("type", "content");
|
||||
browser.setAttribute("disableglobalhistory", "true");
|
||||
panel.appendChild(browser);
|
||||
|
||||
let loadListener = () => {
|
||||
panel.removeEventListener("load", loadListener);
|
||||
|
||||
this.context = new ExtensionPage(this.extension, {
|
||||
type: "popup",
|
||||
contentWindow: browser.contentWindow,
|
||||
uri: Services.io.newURI(popupURL, null, null),
|
||||
docShell: browser.docShell,
|
||||
});
|
||||
GlobalManager.injectInDocShell(browser.docShell, this.extension, this.context);
|
||||
browser.setAttribute("src", popupURL);
|
||||
|
||||
let contentLoadListener = () => {
|
||||
browser.removeEventListener("load", contentLoadListener);
|
||||
|
||||
let contentViewer = browser.docShell.contentViewer;
|
||||
let width = {}, height = {};
|
||||
try {
|
||||
contentViewer.getContentSize(width, height);
|
||||
[width, height] = [width.value, height.value];
|
||||
} catch (e) {
|
||||
// getContentSize can throw
|
||||
[width, height] = [400, 400];
|
||||
}
|
||||
|
||||
let window = document.defaultView;
|
||||
width /= window.devicePixelRatio;
|
||||
height /= window.devicePixelRatio;
|
||||
width = Math.min(width, 800);
|
||||
height = Math.min(height, 800);
|
||||
|
||||
browser.setAttribute("width", width);
|
||||
browser.setAttribute("height", height);
|
||||
|
||||
let anchor = document.getAnonymousElementByAttribute(node, "class", "toolbarbutton-icon");
|
||||
panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, false);
|
||||
};
|
||||
browser.addEventListener("load", contentLoadListener, true);
|
||||
};
|
||||
panel.addEventListener("load", loadListener);
|
||||
openPanel(node, popupResource, this.extension);
|
||||
},
|
||||
|
||||
// Initialize the toolbar icon and popup given that |tab| is the
|
||||
@ -192,7 +123,7 @@ BrowserAction.prototype = {
|
||||
if (Array.isArray(color)) {
|
||||
color = `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
|
||||
}
|
||||
badgeNode.style.backgroundColor = color;
|
||||
badgeNode.style.backgroundColor = color || "";
|
||||
}
|
||||
|
||||
let iconURL = this.getIcon(tab, node);
|
||||
@ -202,26 +133,8 @@ BrowserAction.prototype = {
|
||||
// Note: tab is allowed to be null here.
|
||||
getIcon(tab, node) {
|
||||
let icon = this.icon.get(tab);
|
||||
|
||||
let url;
|
||||
if (typeof(icon) != "object") {
|
||||
url = icon;
|
||||
} else {
|
||||
let window = node.ownerDocument.defaultView;
|
||||
let utils = window.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Components.interfaces.nsIDOMWindowUtils);
|
||||
let res = {value: 1}
|
||||
utils.getResolution(res);
|
||||
|
||||
let size = res.value == 1 ? 19 : 38;
|
||||
url = icon[size];
|
||||
}
|
||||
|
||||
if (url) {
|
||||
return this.extension.baseURI.resolve(url);
|
||||
} else {
|
||||
return "chrome://browser/content/extension.svg";
|
||||
}
|
||||
return IconDetails.getURL(icon, node.ownerDocument.defaultView,
|
||||
this.extension);
|
||||
},
|
||||
|
||||
// Update the toolbar button for a given window.
|
||||
@ -288,37 +201,8 @@ extensions.on("shutdown", (type, extension) => {
|
||||
browserActionMap.get(extension).shutdown();
|
||||
browserActionMap.delete(extension);
|
||||
}
|
||||
imageRendererMap.delete(extension);
|
||||
});
|
||||
|
||||
function convertImageDataToPNG(extension, imageData)
|
||||
{
|
||||
let webNav = imageRendererMap.get(extension);
|
||||
if (!webNav) {
|
||||
webNav = Services.appShell.createWindowlessBrowser(false);
|
||||
let principal = Services.scriptSecurityManager.createCodebasePrincipal(extension.baseURI,
|
||||
{addonId: extension.id});
|
||||
let interfaceRequestor = webNav.QueryInterface(Ci.nsIInterfaceRequestor);
|
||||
let docShell = interfaceRequestor.getInterface(Ci.nsIDocShell);
|
||||
|
||||
GlobalManager.injectInDocShell(docShell, extension, null);
|
||||
|
||||
docShell.createAboutBlankContentViewer(principal);
|
||||
}
|
||||
|
||||
let document = webNav.document;
|
||||
let canvas = document.createElement("canvas");
|
||||
canvas.width = imageData.width;
|
||||
canvas.height = imageData.height;
|
||||
canvas.getContext("2d").putImageData(imageData, 0, 0);
|
||||
|
||||
let url = canvas.toDataURL("image/png");
|
||||
|
||||
canvas.remove();
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
extensions.registerAPI((extension, context) => {
|
||||
return {
|
||||
browserAction: {
|
||||
@ -346,12 +230,8 @@ extensions.registerAPI((extension, context) => {
|
||||
|
||||
setIcon: function(details, callback) {
|
||||
let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
|
||||
if (details.imageData) {
|
||||
let url = convertImageDataToPNG(extension, details.imageData);
|
||||
browserActionOf(extension).setProperty(tab, "icon", url);
|
||||
} else {
|
||||
browserActionOf(extension).setProperty(tab, "icon", details.path);
|
||||
}
|
||||
let icon = IconDetails.normalize(details, extension, context);
|
||||
browserActionOf(extension).setProperty(tab, "icon", icon);
|
||||
},
|
||||
|
||||
setBadgeText: function(details) {
|
||||
|
246
browser/components/extensions/ext-pageAction.js
Normal file
246
browser/components/extensions/ext-pageAction.js
Normal file
@ -0,0 +1,246 @@
|
||||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
||||
var {
|
||||
EventManager,
|
||||
DefaultWeakMap,
|
||||
ignoreEvent,
|
||||
runSafe,
|
||||
} = ExtensionUtils;
|
||||
|
||||
// WeakMap[Extension -> PageAction]
|
||||
var pageActionMap = new WeakMap();
|
||||
|
||||
|
||||
// Handles URL bar icons, including the |page_action| manifest entry
|
||||
// and associated API.
|
||||
function PageAction(options, extension)
|
||||
{
|
||||
this.extension = extension;
|
||||
this.id = makeWidgetId(extension.id) + "-page-action";
|
||||
|
||||
let title = extension.localize(options.default_title || "");
|
||||
let popup = extension.localize(options.default_popup || "");
|
||||
if (popup) {
|
||||
popup = extension.baseURI.resolve(popup);
|
||||
}
|
||||
|
||||
this.defaults = {
|
||||
show: false,
|
||||
title: title,
|
||||
icon: IconDetails.normalize({ path: options.default_icon }, extension,
|
||||
null, true),
|
||||
popup: popup && extension.baseURI.resolve(popup),
|
||||
};
|
||||
|
||||
this.tabContext = new TabContext(tab => Object.create(this.defaults),
|
||||
extension);
|
||||
|
||||
this.tabContext.on("location-change", this.handleLocationChange.bind(this));
|
||||
|
||||
// WeakMap[ChromeWindow -> <xul:image>]
|
||||
this.buttons = new WeakMap();
|
||||
|
||||
EventEmitter.decorate(this);
|
||||
}
|
||||
|
||||
PageAction.prototype = {
|
||||
// Returns the value of the property |prop| for the given tab, where
|
||||
// |prop| is one of "show", "title", "icon", "popup".
|
||||
getProperty(tab, prop) {
|
||||
return this.tabContext.get(tab)[prop];
|
||||
},
|
||||
|
||||
// Sets the value of the property |prop| for the given tab to the
|
||||
// given value, symmetrically to |getProperty|.
|
||||
//
|
||||
// If |tab| is currently selected, updates the page action button to
|
||||
// reflect the new value.
|
||||
setProperty(tab, prop, value) {
|
||||
this.tabContext.get(tab)[prop] = value;
|
||||
if (tab.selected) {
|
||||
this.updateButton(tab.ownerDocument.defaultView);
|
||||
}
|
||||
},
|
||||
|
||||
// Updates the page action button in the given window to reflect the
|
||||
// properties of the currently selected tab:
|
||||
//
|
||||
// Updates "tooltiptext" and "aria-label" to match "title" property.
|
||||
// Updates "image" to match the "icon" property.
|
||||
// Shows or hides the icon, based on the "show" property.
|
||||
updateButton(window) {
|
||||
let tabData = this.tabContext.get(window.gBrowser.selectedTab);
|
||||
|
||||
if (!(tabData.show || this.buttons.has(window))) {
|
||||
// Don't bother creating a button for a window until it actually
|
||||
// needs to be shown.
|
||||
return;
|
||||
}
|
||||
|
||||
let button = this.getButton(window);
|
||||
|
||||
if (tabData.show) {
|
||||
// Update the title and icon only if the button is visible.
|
||||
|
||||
if (tabData.title) {
|
||||
button.setAttribute("tooltiptext", tabData.title);
|
||||
button.setAttribute("aria-label", tabData.title);
|
||||
} else {
|
||||
button.removeAttribute("tooltiptext");
|
||||
button.removeAttribute("aria-label");
|
||||
}
|
||||
|
||||
let icon = IconDetails.getURL(tabData.icon, window, this.extension);
|
||||
button.setAttribute("src", icon);
|
||||
}
|
||||
|
||||
button.hidden = !tabData.show;
|
||||
},
|
||||
|
||||
// Create an |image| node and add it to the |urlbar-icons|
|
||||
// container in the given window.
|
||||
addButton(window) {
|
||||
let document = window.document;
|
||||
|
||||
let button = document.createElement("image");
|
||||
button.id = this.id;
|
||||
button.setAttribute("class", "urlbar-icon");
|
||||
|
||||
button.addEventListener("click", event => {
|
||||
if (event.button == 0) {
|
||||
this.handleClick(window);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("urlbar-icons").appendChild(button);
|
||||
|
||||
return button;
|
||||
},
|
||||
|
||||
// Returns the page action button for the given window, creating it if
|
||||
// it doesn't already exist.
|
||||
getButton(window) {
|
||||
if (!this.buttons.has(window)) {
|
||||
let button = this.addButton(window);
|
||||
this.buttons.set(window, button);
|
||||
}
|
||||
|
||||
return this.buttons.get(window);
|
||||
},
|
||||
|
||||
// Handles a click event on the page action button for the given
|
||||
// window.
|
||||
// If the page action has a |popup| property, a panel is opened to
|
||||
// that URL. Otherwise, a "click" event is emitted, and dispatched to
|
||||
// the any click listeners in the add-on.
|
||||
handleClick(window) {
|
||||
let tab = window.gBrowser.selectedTab;
|
||||
let popup = this.tabContext.get(tab).popup;
|
||||
|
||||
if (popup) {
|
||||
openPanel(this.getButton(window), popup, this.extension);
|
||||
} else {
|
||||
this.emit("click", tab);
|
||||
}
|
||||
},
|
||||
|
||||
handleLocationChange(eventType, tab, fromBrowse) {
|
||||
if (fromBrowse) {
|
||||
this.tabContext.clear(tab);
|
||||
}
|
||||
this.updateButton(tab.ownerDocument.defaultView);
|
||||
},
|
||||
|
||||
shutdown() {
|
||||
this.tabContext.shutdown();
|
||||
|
||||
for (let window of WindowListManager.browserWindows()) {
|
||||
if (this.buttons.has(window)) {
|
||||
this.buttons.get(window).remove();
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
PageAction.for = extension => {
|
||||
return pageActionMap.get(extension);
|
||||
};
|
||||
|
||||
|
||||
extensions.on("manifest_page_action", (type, directive, extension, manifest) => {
|
||||
let pageAction = new PageAction(manifest.page_action, extension);
|
||||
pageActionMap.set(extension, pageAction);
|
||||
});
|
||||
|
||||
extensions.on("shutdown", (type, extension) => {
|
||||
if (pageActionMap.has(extension)) {
|
||||
pageActionMap.get(extension).shutdown();
|
||||
pageActionMap.delete(extension);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
extensions.registerAPI((extension, context) => {
|
||||
return {
|
||||
pageAction: {
|
||||
onClicked: new EventManager(context, "pageAction.onClicked", fire => {
|
||||
let listener = (evt, tab) => {
|
||||
fire(TabManager.convert(extension, tab));
|
||||
};
|
||||
let pageAction = PageAction.for(extension);
|
||||
|
||||
pageAction.on("click", listener);
|
||||
return () => {
|
||||
pageAction.off("click", listener);
|
||||
};
|
||||
}).api(),
|
||||
|
||||
show(tabId) {
|
||||
let tab = TabManager.getTab(tabId);
|
||||
PageAction.for(extension).setProperty(tab, "show", true);
|
||||
},
|
||||
|
||||
hide(tabId) {
|
||||
let tab = TabManager.getTab(tabId);
|
||||
PageAction.for(extension).setProperty(tab, "show", false);
|
||||
},
|
||||
|
||||
setTitle(details) {
|
||||
let tab = TabManager.getTab(details.tabId);
|
||||
PageAction.for(extension).setProperty(tab, "title", details.title);
|
||||
},
|
||||
|
||||
getTitle(details, callback) {
|
||||
let tab = TabManager.getTab(details.tabId);
|
||||
let title = PageAction.for(extension).getProperty(tab, "title");
|
||||
runSafe(context, callback, title);
|
||||
},
|
||||
|
||||
setIcon(details, callback) {
|
||||
let tab = TabManager.getTab(details.tabId);
|
||||
let icon = IconDetails.normalize(details, extension, context);
|
||||
PageAction.for(extension).setProperty(tab, "icon", icon);
|
||||
},
|
||||
|
||||
setPopup(details) {
|
||||
let tab = TabManager.getTab(details.tabId);
|
||||
// Note: Chrome resolves arguments to setIcon relative to the calling
|
||||
// context, but resolves arguments to setPopup relative to the extension
|
||||
// root.
|
||||
// For internal consistency, we currently resolve both relative to the
|
||||
// calling context.
|
||||
let url = details.popup && context.uri.resolve(details.popup);
|
||||
PageAction.for(extension).setProperty(tab, "popup", url);
|
||||
},
|
||||
|
||||
getPopup(details, callback) {
|
||||
let tab = TabManager.getTab(details.tabId);
|
||||
let popup = PageAction.for(extension).getProperty(tab, "popup");
|
||||
runSafe(context, callback, popup);
|
||||
},
|
||||
}
|
||||
};
|
||||
});
|
@ -1,3 +1,7 @@
|
||||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
|
||||
"resource://gre/modules/PrivateBrowsingUtils.jsm");
|
||||
|
||||
@ -10,6 +14,239 @@ var {
|
||||
// modules. All of the code is installed on |global|, which is a scope
|
||||
// shared among the different ext-*.js scripts.
|
||||
|
||||
|
||||
// Manages icon details for toolbar buttons in the |pageAction| and
|
||||
// |browserAction| APIs.
|
||||
global.IconDetails = {
|
||||
// Accepted icon sizes.
|
||||
SIZES: ["19", "38"],
|
||||
|
||||
// Normalizes the various acceptable input formats into an object
|
||||
// with two properties, "19" and "38", containing icon URLs.
|
||||
normalize(details, extension, context=null, localize=false) {
|
||||
let result = {};
|
||||
|
||||
if (details.imageData) {
|
||||
let imageData = details.imageData;
|
||||
|
||||
if (imageData instanceof Cu.getGlobalForObject(imageData).ImageData) {
|
||||
imageData = {"19": imageData};
|
||||
}
|
||||
|
||||
for (let size of this.SIZES) {
|
||||
if (size in imageData) {
|
||||
result[size] = this.convertImageDataToPNG(imageData[size], context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (details.path) {
|
||||
let path = details.path;
|
||||
if (typeof path != "object") {
|
||||
path = {"19": path};
|
||||
}
|
||||
|
||||
let baseURI = context ? context.uri : extension.baseURI;
|
||||
|
||||
for (let size of this.SIZES) {
|
||||
if (size in path) {
|
||||
let url = path[size];
|
||||
if (localize) {
|
||||
url = extension.localize(url);
|
||||
}
|
||||
|
||||
url = baseURI.resolve(path[size]);
|
||||
|
||||
// The Chrome documentation specifies these parameters as
|
||||
// relative paths. We currently accept absolute URLs as well,
|
||||
// which means we need to check that the extension is allowed
|
||||
// to load them.
|
||||
try {
|
||||
Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
|
||||
extension.principal, url,
|
||||
Services.scriptSecurityManager.DISALLOW_SCRIPT);
|
||||
} catch (e if !context) {
|
||||
// If there's no context, it's because we're handling this
|
||||
// as a manifest directive. Log a warning rather than
|
||||
// raising an error, but don't accept the URL in any case.
|
||||
extension.manifestError(`Access to URL '${url}' denied`);
|
||||
continue;
|
||||
}
|
||||
|
||||
result[size] = url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
// Returns the appropriate icon URL for the given icons object and the
|
||||
// screen resolution of the given window.
|
||||
getURL(icons, window, extension) {
|
||||
const DEFAULT = "chrome://browser/content/extension.svg";
|
||||
|
||||
// Use the higher resolution image if we're doing any up-scaling
|
||||
// for high resolution monitors.
|
||||
let res = window.devicePixelRatio;
|
||||
let size = res > 1 ? "38" : "19";
|
||||
|
||||
return icons[size] || icons["19"] || icons["38"] || DEFAULT;
|
||||
},
|
||||
|
||||
convertImageDataToPNG(imageData, context) {
|
||||
let document = context.contentWindow.document;
|
||||
let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
|
||||
canvas.width = imageData.width;
|
||||
canvas.height = imageData.height;
|
||||
canvas.getContext("2d").putImageData(imageData, 0, 0);
|
||||
|
||||
return canvas.toDataURL("image/png");
|
||||
}
|
||||
};
|
||||
|
||||
global.makeWidgetId = id => {
|
||||
id = id.toLowerCase();
|
||||
// FIXME: This allows for collisions.
|
||||
return id.replace(/[^a-z0-9_-]/g, "_");
|
||||
}
|
||||
|
||||
// Open a panel anchored to the given node, containing a browser opened
|
||||
// to the given URL, owned by the given extension. If |popupURL| is not
|
||||
// an absolute URL, it is resolved relative to the given extension's
|
||||
// base URL.
|
||||
global.openPanel = (node, popupURL, extension) => {
|
||||
let document = node.ownerDocument;
|
||||
|
||||
let popupURI = Services.io.newURI(popupURL, null, extension.baseURI);
|
||||
|
||||
Services.scriptSecurityManager.checkLoadURIWithPrincipal(
|
||||
extension.principal, popupURI,
|
||||
Services.scriptSecurityManager.DISALLOW_SCRIPT);
|
||||
|
||||
let panel = document.createElement("panel");
|
||||
panel.setAttribute("id", makeWidgetId(extension.id) + "-panel");
|
||||
panel.setAttribute("class", "browser-extension-panel");
|
||||
panel.setAttribute("type", "arrow");
|
||||
panel.setAttribute("flip", "slide");
|
||||
|
||||
let anchor;
|
||||
if (node.localName == "toolbarbutton") {
|
||||
// Toolbar buttons are a special case. The panel becomes a child of
|
||||
// the button, and is anchored to the button's icon.
|
||||
node.appendChild(panel);
|
||||
anchor = document.getAnonymousElementByAttribute(node, "class", "toolbarbutton-icon");
|
||||
} else {
|
||||
// In all other cases, the panel is anchored to the target node
|
||||
// itself, and is a child of a popupset node.
|
||||
document.getElementById("mainPopupSet").appendChild(panel);
|
||||
anchor = node;
|
||||
}
|
||||
|
||||
let context;
|
||||
panel.addEventListener("popuphidden", () => {
|
||||
context.unload();
|
||||
panel.remove();
|
||||
});
|
||||
|
||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
let browser = document.createElementNS(XUL_NS, "browser");
|
||||
browser.setAttribute("type", "content");
|
||||
browser.setAttribute("disableglobalhistory", "true");
|
||||
panel.appendChild(browser);
|
||||
|
||||
let loadListener = () => {
|
||||
panel.removeEventListener("load", loadListener);
|
||||
|
||||
context = new ExtensionPage(extension, {
|
||||
type: "popup",
|
||||
contentWindow: browser.contentWindow,
|
||||
uri: popupURI,
|
||||
docShell: browser.docShell,
|
||||
});
|
||||
GlobalManager.injectInDocShell(browser.docShell, extension, context);
|
||||
browser.setAttribute("src", context.uri.spec);
|
||||
|
||||
let contentLoadListener = () => {
|
||||
browser.removeEventListener("load", contentLoadListener, true);
|
||||
|
||||
let contentViewer = browser.docShell.contentViewer;
|
||||
let width = {}, height = {};
|
||||
try {
|
||||
contentViewer.getContentSize(width, height);
|
||||
[width, height] = [width.value, height.value];
|
||||
} catch (e) {
|
||||
// getContentSize can throw
|
||||
[width, height] = [400, 400];
|
||||
}
|
||||
|
||||
let window = document.defaultView;
|
||||
width /= window.devicePixelRatio;
|
||||
height /= window.devicePixelRatio;
|
||||
width = Math.min(width, 800);
|
||||
height = Math.min(height, 800);
|
||||
|
||||
browser.setAttribute("width", width);
|
||||
browser.setAttribute("height", height);
|
||||
|
||||
panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, false);
|
||||
};
|
||||
browser.addEventListener("load", contentLoadListener, true);
|
||||
};
|
||||
panel.addEventListener("load", loadListener);
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
// Manages tab-specific context data, and dispatching tab select events
|
||||
// across all windows.
|
||||
global.TabContext = function TabContext(getDefaults, extension) {
|
||||
this.extension = extension;
|
||||
this.getDefaults = getDefaults;
|
||||
|
||||
this.tabData = new WeakMap();
|
||||
|
||||
AllWindowEvents.addListener("progress", this);
|
||||
AllWindowEvents.addListener("TabSelect", this);
|
||||
|
||||
EventEmitter.decorate(this);
|
||||
}
|
||||
|
||||
TabContext.prototype = {
|
||||
get(tab) {
|
||||
if (!this.tabData.has(tab)) {
|
||||
this.tabData.set(tab, this.getDefaults(tab));
|
||||
}
|
||||
|
||||
return this.tabData.get(tab);
|
||||
},
|
||||
|
||||
clear(tab) {
|
||||
this.tabData.delete(tab);
|
||||
},
|
||||
|
||||
handleEvent(event) {
|
||||
if (event.type == "TabSelect") {
|
||||
let tab = event.target;
|
||||
this.emit("tab-select", tab);
|
||||
this.emit("location-change", tab);
|
||||
}
|
||||
},
|
||||
|
||||
onLocationChange(browser, webProgress, request, locationURI, flags) {
|
||||
let gBrowser = browser.ownerDocument.defaultView.gBrowser;
|
||||
if (browser === gBrowser.selectedBrowser) {
|
||||
let tab = gBrowser.getTabForBrowser(browser);
|
||||
this.emit("location-change", tab, true);
|
||||
}
|
||||
},
|
||||
|
||||
shutdown() {
|
||||
AllWindowEvents.removeListener("progress", this);
|
||||
AllWindowEvents.removeListener("TabSelect", this);
|
||||
},
|
||||
};
|
||||
|
||||
// Manages mapping between XUL tabs and extension tab IDs.
|
||||
global.TabManager = {
|
||||
_tabs: new WeakMap(),
|
||||
@ -39,9 +276,7 @@ global.TabManager = {
|
||||
|
||||
getTab(tabId) {
|
||||
// FIXME: Speed this up without leaking memory somehow.
|
||||
let e = Services.wm.getEnumerator("navigator:browser");
|
||||
while (e.hasMoreElements()) {
|
||||
let window = e.getNext();
|
||||
for (let window of WindowListManager.browserWindows()) {
|
||||
if (!window.gBrowser) {
|
||||
continue;
|
||||
}
|
||||
@ -132,9 +367,7 @@ global.WindowManager = {
|
||||
},
|
||||
|
||||
getWindow(id) {
|
||||
let e = Services.wm.getEnumerator("navigator:browser");
|
||||
while (e.hasMoreElements()) {
|
||||
let window = e.getNext();
|
||||
for (let window of WindowListManager.browserWindows(true)) {
|
||||
if (this.getId(window) == id) {
|
||||
return window;
|
||||
}
|
||||
@ -172,15 +405,25 @@ global.WindowListManager = {
|
||||
_openListeners: new Set(),
|
||||
_closeListeners: new Set(),
|
||||
|
||||
// Returns an iterator for all browser windows. Unless |includeIncomplete| is
|
||||
// true, only fully-loaded windows are returned.
|
||||
*browserWindows(includeIncomplete = false) {
|
||||
let e = Services.wm.getEnumerator("navigator:browser");
|
||||
while (e.hasMoreElements()) {
|
||||
let window = e.getNext();
|
||||
if (includeIncomplete || window.document.readyState == "complete") {
|
||||
yield window;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
addOpenListener(listener, fireOnExisting = true) {
|
||||
if (this._openListeners.length == 0 && this._closeListeners.length == 0) {
|
||||
Services.ww.registerNotification(this);
|
||||
}
|
||||
this._openListeners.add(listener);
|
||||
|
||||
let e = Services.wm.getEnumerator("navigator:browser");
|
||||
while (e.hasMoreElements()) {
|
||||
let window = e.getNext();
|
||||
for (let window of this.browserWindows(true)) {
|
||||
if (window.document.readyState != "complete") {
|
||||
window.addEventListener("load", this);
|
||||
} else if (fireOnExisting) {
|
||||
@ -263,7 +506,11 @@ global.AllWindowEvents = {
|
||||
list.add(listener);
|
||||
|
||||
if (needOpenListener) {
|
||||
WindowListManager.addOpenListener(this.openListener);
|
||||
WindowListManager.addOpenListener(this.openListener, false);
|
||||
}
|
||||
|
||||
for (let window of WindowListManager.browserWindows()) {
|
||||
this.addWindowListener(window, type, listener);
|
||||
}
|
||||
},
|
||||
|
||||
@ -283,9 +530,7 @@ global.AllWindowEvents = {
|
||||
}
|
||||
}
|
||||
|
||||
let e = Services.wm.getEnumerator("navigator:browser");
|
||||
while (e.hasMoreElements()) {
|
||||
let window = e.getNext();
|
||||
for (let window of WindowListManager.browserWindows()) {
|
||||
if (type == "progress") {
|
||||
window.gBrowser.removeTabsProgressListener(listener);
|
||||
} else {
|
||||
@ -294,15 +539,19 @@ global.AllWindowEvents = {
|
||||
}
|
||||
},
|
||||
|
||||
addWindowListener(window, eventType, listener) {
|
||||
if (eventType == "progress") {
|
||||
window.gBrowser.addTabsProgressListener(listener);
|
||||
} else {
|
||||
window.addEventListener(eventType, listener);
|
||||
}
|
||||
},
|
||||
|
||||
// Runs whenever the "load" event fires for a new window.
|
||||
openListener(window) {
|
||||
for (let [eventType, listeners] of AllWindowEvents._listeners) {
|
||||
for (let listener of listeners) {
|
||||
if (eventType == "progress") {
|
||||
window.gBrowser.addTabsProgressListener(listener);
|
||||
} else {
|
||||
window.addEventListener(eventType, listener);
|
||||
}
|
||||
this.addWindowListener(window, eventType, listener);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -7,6 +7,7 @@ browser.jar:
|
||||
content/browser/ext-utils.js
|
||||
content/browser/ext-contextMenus.js
|
||||
content/browser/ext-browserAction.js
|
||||
content/browser/ext-pageAction.js
|
||||
content/browser/ext-tabs.js
|
||||
content/browser/ext-windows.js
|
||||
content/browser/ext-bookmarks.js
|
||||
|
@ -7,7 +7,9 @@ support-files =
|
||||
[browser_ext_simple.js]
|
||||
[browser_ext_currentWindow.js]
|
||||
[browser_ext_browserAction_simple.js]
|
||||
[browser_ext_browserAction_icon.js]
|
||||
[browser_ext_browserAction_pageAction_icon.js]
|
||||
[browser_ext_pageAction_context.js]
|
||||
[browser_ext_pageAction_popup.js]
|
||||
[browser_ext_contextMenus.js]
|
||||
[browser_ext_getViews.js]
|
||||
[browser_ext_tabs_executeScript.js]
|
||||
|
@ -1,40 +0,0 @@
|
||||
add_task(function* () {
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
"browser_action": {},
|
||||
"background": {
|
||||
"page": "background.html",
|
||||
}
|
||||
},
|
||||
|
||||
files: {
|
||||
"background.html": `<canvas id="canvas" width="2" height="2">
|
||||
<script src="background.js"></script>`,
|
||||
|
||||
"background.js": function() {
|
||||
var canvas = document.getElementById("canvas");
|
||||
var canvasContext = canvas.getContext("2d");
|
||||
|
||||
canvasContext.clearRect(0, 0, canvas.width, canvas.height);
|
||||
canvasContext.fillStyle = "green";
|
||||
canvasContext.fillRect(0, 0, 1, 1);
|
||||
|
||||
var url = canvas.toDataURL("image/png");
|
||||
var imageData = canvasContext.getImageData(0, 0, canvas.width, canvas.height);
|
||||
browser.browserAction.setIcon({imageData});
|
||||
|
||||
browser.test.sendMessage("imageURL", url);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
let [_, url] = yield Promise.all([extension.startup(), extension.awaitMessage("imageURL")]);
|
||||
|
||||
let widgetId = makeWidgetId(extension.id) + "-browser-action";
|
||||
let node = CustomizableUI.getWidget(widgetId).forWindow(window).node;
|
||||
|
||||
let image = node.getAttribute("image");
|
||||
is(image, url, "image is correct");
|
||||
|
||||
yield extension.unload();
|
||||
});
|
@ -0,0 +1,345 @@
|
||||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
// Test that various combinations of icon details specs, for both paths
|
||||
// and ImageData objects, result in the correct image being displayed in
|
||||
// all display resolutions.
|
||||
add_task(function* testDetailsObjects() {
|
||||
function background() {
|
||||
function getImageData(color) {
|
||||
var canvas = document.createElement("canvas");
|
||||
canvas.width = 2;
|
||||
canvas.height = 2;
|
||||
var canvasContext = canvas.getContext("2d");
|
||||
|
||||
canvasContext.clearRect(0, 0, canvas.width, canvas.height);
|
||||
canvasContext.fillStyle = color;
|
||||
canvasContext.fillRect(0, 0, 1, 1);
|
||||
|
||||
return {
|
||||
url: canvas.toDataURL("image/png"),
|
||||
imageData: canvasContext.getImageData(0, 0, canvas.width, canvas.height),
|
||||
};
|
||||
}
|
||||
|
||||
var imageData = {
|
||||
red: getImageData("red"),
|
||||
green: getImageData("green"),
|
||||
};
|
||||
|
||||
var iconDetails = [
|
||||
// Only paths.
|
||||
{ details: { "path": "a.png" },
|
||||
resolutions: {
|
||||
"1": browser.runtime.getURL("data/a.png"),
|
||||
"2": browser.runtime.getURL("data/a.png"), } },
|
||||
{ details: { "path": "/a.png" },
|
||||
resolutions: {
|
||||
"1": browser.runtime.getURL("a.png"),
|
||||
"2": browser.runtime.getURL("a.png"), } },
|
||||
{ details: { "path": { "19": "a.png" } },
|
||||
resolutions: {
|
||||
"1": browser.runtime.getURL("data/a.png"),
|
||||
"2": browser.runtime.getURL("data/a.png"), } },
|
||||
{ details: { "path": { "38": "a.png" } },
|
||||
resolutions: {
|
||||
"1": browser.runtime.getURL("data/a.png"),
|
||||
"2": browser.runtime.getURL("data/a.png"), } },
|
||||
{ details: { "path": { "19": "a.png", "38": "a-x2.png" } },
|
||||
resolutions: {
|
||||
"1": browser.runtime.getURL("data/a.png"),
|
||||
"2": browser.runtime.getURL("data/a-x2.png"), } },
|
||||
|
||||
// Only ImageData objects.
|
||||
{ details: { "imageData": imageData.red.imageData },
|
||||
resolutions: {
|
||||
"1": imageData.red.url,
|
||||
"2": imageData.red.url, } },
|
||||
{ details: { "imageData": { "19": imageData.red.imageData } },
|
||||
resolutions: {
|
||||
"1": imageData.red.url,
|
||||
"2": imageData.red.url, } },
|
||||
{ details: { "imageData": { "38": imageData.red.imageData } },
|
||||
resolutions: {
|
||||
"1": imageData.red.url,
|
||||
"2": imageData.red.url, } },
|
||||
{ details: { "imageData": {
|
||||
"19": imageData.red.imageData,
|
||||
"38": imageData.green.imageData } },
|
||||
resolutions: {
|
||||
"1": imageData.red.url,
|
||||
"2": imageData.green.url, } },
|
||||
|
||||
// Mixed path and imageData objects.
|
||||
//
|
||||
// The behavior is currently undefined if both |path| and
|
||||
// |imageData| specify icons of the same size.
|
||||
{ details: {
|
||||
"path": { "19": "a.png" },
|
||||
"imageData": { "38": imageData.red.imageData } },
|
||||
resolutions: {
|
||||
"1": browser.runtime.getURL("data/a.png"),
|
||||
"2": imageData.red.url, } },
|
||||
{ details: {
|
||||
"path": { "38": "a.png" },
|
||||
"imageData": { "19": imageData.red.imageData } },
|
||||
resolutions: {
|
||||
"1": imageData.red.url,
|
||||
"2": browser.runtime.getURL("data/a.png"), } },
|
||||
|
||||
// A path or ImageData object by itself is treated as a 19px icon.
|
||||
{ details: {
|
||||
"path": "a.png",
|
||||
"imageData": { "38": imageData.red.imageData } },
|
||||
resolutions: {
|
||||
"1": browser.runtime.getURL("data/a.png"),
|
||||
"2": imageData.red.url, } },
|
||||
{ details: {
|
||||
"path": { "38": "a.png" },
|
||||
"imageData": imageData.red.imageData, },
|
||||
resolutions: {
|
||||
"1": imageData.red.url,
|
||||
"2": browser.runtime.getURL("data/a.png"), } },
|
||||
];
|
||||
|
||||
// Allow serializing ImageData objects for logging.
|
||||
ImageData.prototype.toJSON = () => "<ImageData>";
|
||||
|
||||
var tabId;
|
||||
|
||||
browser.test.onMessage.addListener((msg, test) => {
|
||||
if (msg != "setIcon") {
|
||||
browser.test.fail("expecting 'setIcon' message");
|
||||
}
|
||||
|
||||
var details = iconDetails[test.index];
|
||||
var expectedURL = details.resolutions[test.resolution];
|
||||
|
||||
var detailString = JSON.stringify(details);
|
||||
browser.test.log(`Setting browerAction/pageAction to ${detailString} expecting URL ${expectedURL}`)
|
||||
|
||||
browser.browserAction.setIcon(Object.assign({tabId}, details.details));
|
||||
browser.pageAction.setIcon(Object.assign({tabId}, details.details));
|
||||
|
||||
browser.test.sendMessage("imageURL", expectedURL);
|
||||
});
|
||||
|
||||
// Generate a list of tests and resolutions to send back to the test
|
||||
// context.
|
||||
//
|
||||
// This process is a bit convoluted, because the outer test context needs
|
||||
// to handle checking the button nodes and changing the screen resolution,
|
||||
// but it can't pass us icon definitions with ImageData objects. This
|
||||
// shouldn't be a problem, since structured clones should handle ImageData
|
||||
// objects without issue. Unfortunately, |cloneInto| implements a slightly
|
||||
// different algorithm than we use in web APIs, and does not handle them
|
||||
// correctly.
|
||||
var tests = [];
|
||||
for (var [idx, icon] of iconDetails.entries()) {
|
||||
for (var res of Object.keys(icon.resolutions)) {
|
||||
tests.push({ index: idx, resolution: Number(res) });
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by resolution, so we don't needlessly switch back and forth
|
||||
// between each test.
|
||||
tests.sort(test => test.resolution);
|
||||
|
||||
browser.tabs.query({ active: true, currentWindow: true }, tabs => {
|
||||
tabId = tabs[0].id;
|
||||
browser.pageAction.show(tabId);
|
||||
|
||||
browser.test.sendMessage("ready", tests);
|
||||
});
|
||||
}
|
||||
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
"browser_action": {},
|
||||
"page_action": {},
|
||||
"background": {
|
||||
"page": "data/background.html",
|
||||
}
|
||||
},
|
||||
|
||||
files: {
|
||||
"data/background.html": `<script src="background.js"></script>`,
|
||||
"data/background.js": background,
|
||||
},
|
||||
});
|
||||
|
||||
const RESOLUTION_PREF = "layout.css.devPixelsPerPx";
|
||||
registerCleanupFunction(() => {
|
||||
SpecialPowers.clearUserPref(RESOLUTION_PREF);
|
||||
});
|
||||
|
||||
let browserActionId = makeWidgetId(extension.id) + "-browser-action";
|
||||
let pageActionId = makeWidgetId(extension.id) + "-page-action";
|
||||
|
||||
let [, tests] = yield Promise.all([extension.startup(), extension.awaitMessage("ready")]);
|
||||
|
||||
for (let test of tests) {
|
||||
SpecialPowers.setCharPref(RESOLUTION_PREF, String(test.resolution));
|
||||
is(window.devicePixelRatio, test.resolution, "window has the required resolution");
|
||||
|
||||
extension.sendMessage("setIcon", test);
|
||||
|
||||
let imageURL = yield extension.awaitMessage("imageURL");
|
||||
|
||||
let browserActionButton = document.getElementById(browserActionId);
|
||||
is(browserActionButton.getAttribute("image"), imageURL, "browser action has the correct image");
|
||||
|
||||
let pageActionImage = document.getElementById(pageActionId);
|
||||
is(pageActionImage.src, imageURL, "page action has the correct image");
|
||||
}
|
||||
|
||||
yield extension.unload();
|
||||
});
|
||||
|
||||
// Test that default icon details in the manifest.json file are handled
|
||||
// correctly.
|
||||
add_task(function *testDefaultDetails() {
|
||||
// TODO: Test localized variants.
|
||||
let icons = [
|
||||
"foo/bar.png",
|
||||
"/foo/bar.png",
|
||||
{ "19": "foo/bar.png" },
|
||||
{ "38": "foo/bar.png" },
|
||||
{ "19": "foo/bar.png", "38": "baz/quux.png" },
|
||||
];
|
||||
|
||||
let expectedURL = new RegExp(String.raw`^moz-extension://[^/]+/foo/bar\.png$`);
|
||||
|
||||
for (let icon of icons) {
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
"browser_action": { "default_icon": icon },
|
||||
"page_action": { "default_icon": icon },
|
||||
},
|
||||
|
||||
background: function () {
|
||||
browser.tabs.query({ active: true, currentWindow: true }, tabs => {
|
||||
var tabId = tabs[0].id;
|
||||
|
||||
browser.pageAction.show(tabId);
|
||||
browser.test.sendMessage("ready");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
yield Promise.all([extension.startup(), extension.awaitMessage("ready")]);
|
||||
|
||||
let browserActionId = makeWidgetId(extension.id) + "-browser-action";
|
||||
let pageActionId = makeWidgetId(extension.id) + "-page-action";
|
||||
|
||||
let browserActionButton = document.getElementById(browserActionId);
|
||||
let image = browserActionButton.getAttribute("image");
|
||||
|
||||
ok(expectedURL.test(image), `browser action image ${image} matches ${expectedURL}`);
|
||||
|
||||
let pageActionImage = document.getElementById(pageActionId);
|
||||
image = pageActionImage.src;
|
||||
|
||||
ok(expectedURL.test(image), `page action image ${image} matches ${expectedURL}`);
|
||||
|
||||
yield extension.unload();
|
||||
|
||||
let node = document.getElementById(pageActionId);
|
||||
is(node, undefined, "pageAction image removed from document");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Check that attempts to load a privileged URL as an icon image fail.
|
||||
add_task(function* testSecureURLsDenied() {
|
||||
|
||||
// Test URLs passed to setIcon.
|
||||
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
"browser_action": {},
|
||||
"page_action": {},
|
||||
},
|
||||
|
||||
background: function () {
|
||||
browser.tabs.query({ active: true, currentWindow: true }, tabs => {
|
||||
var tabId = tabs[0].id;
|
||||
|
||||
var urls = ["chrome://browser/content/browser.xul",
|
||||
"javascript:true"];
|
||||
|
||||
for (var url of urls) {
|
||||
for (var api of ["pageAction", "browserAction"]) {
|
||||
try {
|
||||
browser[api].setIcon({tabId, path: url});
|
||||
|
||||
browser.test.fail(`Load of '${url}' succeeded. Expected failure.`);
|
||||
browser.test.notifyFail("setIcon security tests");
|
||||
return;
|
||||
} catch (e) {
|
||||
// We can't actually inspect the error here, since the
|
||||
// error object belongs to the privileged scope of the API,
|
||||
// rather than to the extension scope that calls into it.
|
||||
// Just assume it's the expected security error, for now.
|
||||
browser.test.succeed(`Load of '${url}' failed. Expected failure.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
browser.test.notifyPass("setIcon security tests");
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
yield extension.startup();
|
||||
|
||||
yield extension.awaitFinish();
|
||||
yield extension.unload();
|
||||
|
||||
|
||||
// Test URLs included in the manifest.
|
||||
|
||||
let urls = ["chrome://browser/content/browser.xul",
|
||||
"javascript:true"];
|
||||
|
||||
let matchURLForbidden = url => ({
|
||||
message: new RegExp(`Loading extension.*Access to.*'${url}' denied`),
|
||||
});
|
||||
|
||||
let messages = [matchURLForbidden(urls[0]),
|
||||
matchURLForbidden(urls[1]),
|
||||
matchURLForbidden(urls[0]),
|
||||
matchURLForbidden(urls[1])];
|
||||
|
||||
let waitForConsole = new Promise(resolve => {
|
||||
// Not necessary in browser-chrome tests, but monitorConsole gripes
|
||||
// if we don't call it.
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
SimpleTest.monitorConsole(resolve, messages);
|
||||
});
|
||||
|
||||
extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
"browser_action": {
|
||||
"default_icon": {
|
||||
"19": urls[0],
|
||||
"38": urls[1],
|
||||
},
|
||||
},
|
||||
"page_action": {
|
||||
"default_icon": {
|
||||
"19": urls[0],
|
||||
"38": urls[1],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
yield extension.startup();
|
||||
yield extension.unload();
|
||||
|
||||
SimpleTest.endMonitorConsole();
|
||||
yield waitForConsole;
|
||||
});
|
@ -0,0 +1,209 @@
|
||||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
add_task(function* testTabSwitchContext() {
|
||||
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
"page_action": {
|
||||
"default_icon": "default.png",
|
||||
"default_popup": "default.html",
|
||||
"default_title": "Default Title",
|
||||
},
|
||||
"permissions": ["tabs"],
|
||||
},
|
||||
|
||||
background: function () {
|
||||
var details = [
|
||||
{ "icon": browser.runtime.getURL("default.png"),
|
||||
"popup": browser.runtime.getURL("default.html"),
|
||||
"title": "Default Title" },
|
||||
{ "icon": browser.runtime.getURL("1.png"),
|
||||
"popup": browser.runtime.getURL("default.html"),
|
||||
"title": "Default Title" },
|
||||
{ "icon": browser.runtime.getURL("2.png"),
|
||||
"popup": browser.runtime.getURL("2.html"),
|
||||
"title": "Title 2" },
|
||||
];
|
||||
|
||||
var tabs = [];
|
||||
|
||||
var tests = [
|
||||
expect => {
|
||||
browser.test.log("Initial state. No icon visible.");
|
||||
expect(null);
|
||||
},
|
||||
expect => {
|
||||
browser.test.log("Show the icon on the first tab, expect default properties.");
|
||||
browser.pageAction.show(tabs[0]);
|
||||
expect(details[0]);
|
||||
},
|
||||
expect => {
|
||||
browser.test.log("Change the icon. Expect default properties excluding the icon.");
|
||||
browser.pageAction.setIcon({ tabId: tabs[0], path: "1.png" });
|
||||
expect(details[1]);
|
||||
},
|
||||
expect => {
|
||||
browser.test.log("Create a new tab. No icon visible.");
|
||||
browser.tabs.create({ active: true, url: "about:blank?0" }, tab => {
|
||||
tabs.push(tab.id);
|
||||
expect(null);
|
||||
});
|
||||
},
|
||||
expect => {
|
||||
browser.test.log("Change properties. Expect new properties.");
|
||||
var tabId = tabs[1];
|
||||
browser.pageAction.show(tabId);
|
||||
browser.pageAction.setIcon({ tabId, path: "2.png" });
|
||||
browser.pageAction.setPopup({ tabId, popup: "2.html" });
|
||||
browser.pageAction.setTitle({ tabId, title: "Title 2" });
|
||||
|
||||
expect(details[2]);
|
||||
},
|
||||
expect => {
|
||||
browser.test.log("Navigate to a new page. Expect icon hidden.");
|
||||
|
||||
// TODO: This listener should not be necessary, but the |tabs.update|
|
||||
// callback currently fires too early in e10s windows.
|
||||
browser.tabs.onUpdated.addListener(function listener(tabId, changed) {
|
||||
if (tabId == tabs[1] && changed.url) {
|
||||
browser.tabs.onUpdated.removeListener(listener);
|
||||
expect(null);
|
||||
}
|
||||
});
|
||||
|
||||
browser.tabs.update(tabs[1], { url: "about:blank?1" });
|
||||
},
|
||||
expect => {
|
||||
browser.test.log("Show the icon. Expect default properties again.");
|
||||
browser.pageAction.show(tabs[1]);
|
||||
expect(details[0]);
|
||||
},
|
||||
expect => {
|
||||
browser.test.log("Switch back to the first tab. Expect previously set properties.");
|
||||
browser.tabs.update(tabs[0], { active: true }, () => {
|
||||
expect(details[1]);
|
||||
});
|
||||
},
|
||||
expect => {
|
||||
browser.test.log("Hide the icon on tab 2. Switch back, expect hidden.");
|
||||
browser.pageAction.hide(tabs[1]);
|
||||
browser.tabs.update(tabs[1], { active: true }, () => {
|
||||
expect(null);
|
||||
});
|
||||
},
|
||||
expect => {
|
||||
browser.test.log("Switch back to tab 1. Expect previous results again.");
|
||||
browser.tabs.remove(tabs[1], () => {
|
||||
expect(details[1]);
|
||||
});
|
||||
},
|
||||
expect => {
|
||||
browser.test.log("Hide the icon. Expect hidden.");
|
||||
browser.pageAction.hide(tabs[0]);
|
||||
expect(null);
|
||||
},
|
||||
];
|
||||
|
||||
// Gets the current details of the page action, and returns a
|
||||
// promise that resolves to an object containing them.
|
||||
function getDetails() {
|
||||
return new Promise(resolve => {
|
||||
return browser.tabs.query({ active: true, currentWindow: true }, resolve);
|
||||
}).then(tabs => {
|
||||
var tabId = tabs[0].id;
|
||||
|
||||
return Promise.all([
|
||||
new Promise(resolve => browser.pageAction.getTitle({tabId}, resolve)),
|
||||
new Promise(resolve => browser.pageAction.getPopup({tabId}, resolve))])
|
||||
}).then(details => {
|
||||
return Promise.resolve({ title: details[0],
|
||||
popup: details[1] });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Runs the next test in the `tests` array, checks the results,
|
||||
// and passes control back to the outer test scope.
|
||||
function nextTest() {
|
||||
var test = tests.shift();
|
||||
|
||||
test(expecting => {
|
||||
function finish() {
|
||||
// Check that the actual icon has the expected values, then
|
||||
// run the next test.
|
||||
browser.test.sendMessage("nextTest", expecting, tests.length);
|
||||
}
|
||||
|
||||
if (expecting) {
|
||||
// Check that the API returns the expected values, and then
|
||||
// run the next test.
|
||||
getDetails().then(details => {
|
||||
browser.test.assertEq(expecting.title, details.title,
|
||||
"expected value from getTitle");
|
||||
|
||||
browser.test.assertEq(expecting.popup, details.popup,
|
||||
"expected value from getPopup");
|
||||
|
||||
finish();
|
||||
});
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
browser.test.onMessage.addListener((msg) => {
|
||||
if (msg != "runNextTest") {
|
||||
browser.test.fail("Expecting 'runNextTest' message");
|
||||
}
|
||||
|
||||
nextTest();
|
||||
});
|
||||
|
||||
browser.tabs.query({ active: true, currentWindow: true }, resultTabs => {
|
||||
tabs[0] = resultTabs[0].id;
|
||||
|
||||
nextTest();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
let pageActionId = makeWidgetId(extension.id) + "-page-action";
|
||||
|
||||
function checkDetails(details) {
|
||||
let image = document.getElementById(pageActionId);
|
||||
if (details == null) {
|
||||
ok(image == null || image.hidden, "image is hidden");
|
||||
} else {
|
||||
ok(image, "image exists");
|
||||
|
||||
is(image.src, details.icon, "icon URL is correct");
|
||||
is(image.getAttribute("tooltiptext"), details.title, "image title is correct");
|
||||
is(image.getAttribute("aria-label"), details.title, "image aria-label is correct");
|
||||
// TODO: Popup URL.
|
||||
}
|
||||
}
|
||||
|
||||
let awaitFinish = new Promise(resolve => {
|
||||
extension.onMessage("nextTest", (expecting, testsRemaining) => {
|
||||
checkDetails(expecting);
|
||||
|
||||
if (testsRemaining) {
|
||||
extension.sendMessage("runNextTest")
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
yield extension.startup();
|
||||
|
||||
yield awaitFinish;
|
||||
|
||||
yield extension.unload();
|
||||
|
||||
let node = document.getElementById(pageActionId);
|
||||
is(node, undefined, "pageAction image removed from document");
|
||||
});
|
@ -0,0 +1,215 @@
|
||||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
function promisePopupShown(popup) {
|
||||
return new Promise(resolve => {
|
||||
if (popup.popupOpen) {
|
||||
resolve();
|
||||
} else {
|
||||
let onPopupShown = event => {
|
||||
popup.removeEventListener("popupshown", onPopupShown);
|
||||
resolve();
|
||||
};
|
||||
popup.addEventListener("popupshown", onPopupShown);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
add_task(function* testPageActionPopup() {
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
"background": {
|
||||
"page": "data/background.html"
|
||||
},
|
||||
"page_action": {
|
||||
"default_popup": "popup-a.html"
|
||||
}
|
||||
},
|
||||
|
||||
files: {
|
||||
"popup-a.html": `<script src="popup-a.js"></script>`,
|
||||
"popup-a.js": function() {
|
||||
browser.runtime.sendMessage("from-popup-a");
|
||||
},
|
||||
|
||||
"data/popup-b.html": `<script src="popup-b.js"></script>`,
|
||||
"data/popup-b.js": function() {
|
||||
browser.runtime.sendMessage("from-popup-b");
|
||||
},
|
||||
|
||||
"data/background.html": `<script src="background.js"></script>`,
|
||||
|
||||
"data/background.js": function() {
|
||||
var tabId;
|
||||
|
||||
var tests = [
|
||||
() => {
|
||||
sendClick({ expectEvent: false, expectPopup: "a" });
|
||||
},
|
||||
() => {
|
||||
sendClick({ expectEvent: false, expectPopup: "a" });
|
||||
},
|
||||
() => {
|
||||
browser.pageAction.setPopup({ tabId, popup: "popup-b.html" });
|
||||
sendClick({ expectEvent: false, expectPopup: "b" });
|
||||
},
|
||||
() => {
|
||||
sendClick({ expectEvent: false, expectPopup: "b" });
|
||||
},
|
||||
() => {
|
||||
browser.pageAction.setPopup({ tabId, popup: "" });
|
||||
sendClick({ expectEvent: true, expectPopup: null });
|
||||
},
|
||||
() => {
|
||||
sendClick({ expectEvent: true, expectPopup: null });
|
||||
},
|
||||
() => {
|
||||
browser.pageAction.setPopup({ tabId, popup: "/popup-a.html" });
|
||||
sendClick({ expectEvent: false, expectPopup: "a" });
|
||||
},
|
||||
];
|
||||
|
||||
var expect = {};
|
||||
function sendClick({ expectEvent, expectPopup }) {
|
||||
expect = { event: expectEvent, popup: expectPopup };
|
||||
browser.test.sendMessage("send-click");
|
||||
}
|
||||
|
||||
browser.runtime.onMessage.addListener(msg => {
|
||||
if (expect.popup) {
|
||||
browser.test.assertEq(msg, `from-popup-${expect.popup}`,
|
||||
"expected popup opened");
|
||||
} else {
|
||||
browser.test.fail("unexpected popup");
|
||||
}
|
||||
|
||||
expect.popup = null;
|
||||
browser.test.sendMessage("next-test");
|
||||
});
|
||||
|
||||
browser.pageAction.onClicked.addListener(() => {
|
||||
if (expect.event) {
|
||||
browser.test.succeed("expected click event received");
|
||||
} else {
|
||||
browser.test.fail("unexpected click event");
|
||||
}
|
||||
|
||||
expect.event = false;
|
||||
browser.test.sendMessage("next-test");
|
||||
});
|
||||
|
||||
browser.test.onMessage.addListener((msg) => {
|
||||
if (msg != "next-test") {
|
||||
browser.test.fail("Expecting 'next-test' message");
|
||||
}
|
||||
|
||||
if (tests.length) {
|
||||
var test = tests.shift();
|
||||
test();
|
||||
} else {
|
||||
browser.test.notifyPass("pageaction-tests-done");
|
||||
}
|
||||
});
|
||||
|
||||
browser.tabs.query({ active: true, currentWindow: true }, tabs => {
|
||||
tabId = tabs[0].id;
|
||||
|
||||
browser.pageAction.show(tabId);
|
||||
browser.test.sendMessage("next-test");
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let pageActionId = makeWidgetId(extension.id) + "-page-action";
|
||||
let panelId = makeWidgetId(extension.id) + "-panel";
|
||||
|
||||
extension.onMessage("send-click", () => {
|
||||
let image = document.getElementById(pageActionId);
|
||||
|
||||
let evt = new MouseEvent("click", {});
|
||||
image.dispatchEvent(evt);
|
||||
});
|
||||
|
||||
extension.onMessage("next-test", Task.async(function* () {
|
||||
let panel = document.getElementById(panelId);
|
||||
if (panel) {
|
||||
yield promisePopupShown(panel);
|
||||
panel.hidePopup();
|
||||
|
||||
panel = document.getElementById(panelId);
|
||||
is(panel, undefined, "panel successfully removed from document after hiding");
|
||||
}
|
||||
|
||||
extension.sendMessage("next-test");
|
||||
}));
|
||||
|
||||
|
||||
yield Promise.all([extension.startup(), extension.awaitFinish("pageaction-tests-done")]);
|
||||
|
||||
yield extension.unload();
|
||||
|
||||
let node = document.getElementById(pageActionId);
|
||||
is(node, undefined, "pageAction image removed from document");
|
||||
|
||||
let panel = document.getElementById(panelId);
|
||||
is(panel, undefined, "pageAction panel removed from document");
|
||||
});
|
||||
|
||||
|
||||
add_task(function* testPageActionSecurity() {
|
||||
const URL = "chrome://browser/content/browser.xul";
|
||||
|
||||
let matchURLForbidden = url => ({
|
||||
message: new RegExp(`Loading extension.*Access to.*'${URL}' denied`),
|
||||
});
|
||||
|
||||
let messages = [/Access to restricted URI denied/,
|
||||
/Access to restricted URI denied/];
|
||||
|
||||
let waitForConsole = new Promise(resolve => {
|
||||
// Not necessary in browser-chrome tests, but monitorConsole gripes
|
||||
// if we don't call it.
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
SimpleTest.monitorConsole(resolve, messages);
|
||||
});
|
||||
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
"browser_action": { "default_popup": URL },
|
||||
"page_action": { "default_popup": URL },
|
||||
},
|
||||
|
||||
background: function () {
|
||||
browser.tabs.query({ active: true, currentWindow: true }, tabs => {
|
||||
var tabId = tabs[0].id;
|
||||
|
||||
browser.pageAction.show(tabId);
|
||||
browser.test.sendMessage("ready");
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
yield Promise.all([extension.startup(), extension.awaitMessage("ready")]);
|
||||
|
||||
let browserActionId = makeWidgetId(extension.id) + "-browser-action";
|
||||
let pageActionId = makeWidgetId(extension.id) + "-page-action";
|
||||
|
||||
let browserAction = document.getElementById(browserActionId);
|
||||
let evt = new CustomEvent("command", {});
|
||||
browserAction.dispatchEvent(evt);
|
||||
|
||||
let pageAction = document.getElementById(pageActionId);
|
||||
evt = new MouseEvent("click", {});
|
||||
pageAction.dispatchEvent(evt);
|
||||
|
||||
yield extension.unload();
|
||||
|
||||
let node = document.getElementById(pageActionId);
|
||||
is(node, undefined, "pageAction image removed from document");
|
||||
|
||||
SimpleTest.endMonitorConsole();
|
||||
yield waitForConsole;
|
||||
});
|
@ -642,6 +642,7 @@ BrowserGlue.prototype = {
|
||||
|
||||
ExtensionManagement.registerScript("chrome://browser/content/ext-utils.js");
|
||||
ExtensionManagement.registerScript("chrome://browser/content/ext-browserAction.js");
|
||||
ExtensionManagement.registerScript("chrome://browser/content/ext-pageAction.js");
|
||||
ExtensionManagement.registerScript("chrome://browser/content/ext-contextMenus.js");
|
||||
ExtensionManagement.registerScript("chrome://browser/content/ext-tabs.js");
|
||||
ExtensionManagement.registerScript("chrome://browser/content/ext-windows.js");
|
||||
|
@ -937,6 +937,9 @@ toolbar .toolbarbutton-1:-moz-any(@primaryToolbarButtons@) > :-moz-any(.toolbarb
|
||||
|
||||
.urlbar-icon {
|
||||
padding: 0 3px;
|
||||
/* 16x16 icon with border-box sizing */
|
||||
width: 22px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
#urlbar-search-footer {
|
||||
@ -1942,7 +1945,7 @@ toolbarbutton.chevron > .toolbarbutton-icon {
|
||||
-moz-margin-end: 0 !important;
|
||||
}
|
||||
|
||||
.browser-action-panel > .panel-arrowcontainer > .panel-arrowcontent {
|
||||
.browser-extension-panel > .panel-arrowcontainer > .panel-arrowcontent {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
@ -1728,6 +1728,9 @@ toolbar .toolbarbutton-1 > .toolbarbutton-menubutton-button {
|
||||
|
||||
.urlbar-icon {
|
||||
padding: 0 3px;
|
||||
/* 16x16 icon with border-box sizing */
|
||||
width: 22px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
#urlbar-search-footer {
|
||||
@ -2015,7 +2018,6 @@ richlistitem[type~="action"][actiontype="switchtab"][selected="true"] > .ac-url-
|
||||
#page-report-button {
|
||||
list-style-image: url("chrome://browser/skin/urlbar-popup-blocked@2x.png");
|
||||
-moz-image-region: rect(0, 32px, 32px, 0);
|
||||
width: 22px;
|
||||
}
|
||||
|
||||
#page-report-button:hover:active,
|
||||
@ -3617,7 +3619,7 @@ notification[value="loop-sharing-notification"] .messageImage {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.browser-action-panel > .panel-arrowcontainer > .panel-arrowcontent {
|
||||
.browser-extension-panel > .panel-arrowcontainer > .panel-arrowcontent {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
@ -1325,6 +1325,9 @@ html|*.urlbar-input:-moz-lwtheme::-moz-placeholder,
|
||||
|
||||
.urlbar-icon {
|
||||
padding: 0 3px;
|
||||
/* 16x16 icon with border-box sizing */
|
||||
width: 22px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.search-go-container {
|
||||
@ -2802,7 +2805,7 @@ notification[value="loop-sharing-notification"] .messageImage {
|
||||
%include browser-aero.css
|
||||
}
|
||||
|
||||
.browser-action-panel > .panel-arrowcontainer > .panel-arrowcontent {
|
||||
.browser-extension-panel > .panel-arrowcontainer > .panel-arrowcontent {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
@ -1125,7 +1125,7 @@ SimpleTest.monitorConsole = function (continuation, msgs, forbidUnexpectedMsgs)
|
||||
}
|
||||
|
||||
function msgMatches(msg, pat) {
|
||||
for (k in pat) {
|
||||
for (var k in pat) {
|
||||
if (!(k in msg)) {
|
||||
return false;
|
||||
}
|
||||
|
@ -23,7 +23,6 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/devtools/shared/event-emitter.js");
|
||||
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Locale",
|
||||
"resource://gre/modules/Locale.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Log",
|
||||
@ -441,12 +440,10 @@ this.Extension.generate = function(id, data)
|
||||
let components = filename.split("/");
|
||||
let path = "";
|
||||
for (let component of components.slice(0, -1)) {
|
||||
path += component;
|
||||
path += component + "/";
|
||||
if (!zipW.hasEntry(path)) {
|
||||
zipW.addEntryDirectory(path, time, false);
|
||||
}
|
||||
|
||||
path += "/";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -45,7 +45,8 @@ function runSafeSync(context, f, ...args)
|
||||
try {
|
||||
args = Cu.cloneInto(args, context.cloneScope);
|
||||
} catch (e) {
|
||||
dump(`runSafe failure\n${context.cloneScope}\n${Error().stack}`);
|
||||
Cu.reportError(e);
|
||||
dump(`runSafe failure: cloning into ${context.cloneScope}: ${e}\n\n${Error().stack}`);
|
||||
}
|
||||
return runSafeSyncWithoutClone(f, ...args);
|
||||
}
|
||||
@ -57,7 +58,8 @@ function runSafe(context, f, ...args)
|
||||
try {
|
||||
args = Cu.cloneInto(args, context.cloneScope);
|
||||
} catch (e) {
|
||||
dump(`runSafe failure\n${context.cloneScope}\n${Error().stack}`);
|
||||
Cu.reportError(e);
|
||||
dump(`runSafe failure: cloning into ${context.cloneScope}: ${e}\n\n${Error().stack}`);
|
||||
}
|
||||
return runSafeWithoutClone(f, ...args);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user