Bug 1797764 - Add MV3 support for menus API and support for '_execute_action' command and the 'action' context. r=mkmelin

Differential Revision: https://phabricator.services.mozilla.com/D164499

--HG--
extra : amend_source : 6a74f23222e6c75718549dce1f166e52af860b3d
This commit is contained in:
John Bieling 2022-12-13 21:30:13 +11:00
Родитель 7063053c36
Коммит 51e3986fe6
20 изменённых файлов: 1894 добавлений и 982 удалений

Просмотреть файл

@ -32,6 +32,7 @@ XPCOMUtils.defineLazyGetter(lazy, "messageDisplayActionFor", () => {
return lazy.ExtensionParent.apiManager.global.messageDisplayActionFor;
});
const EXECUTE_ACTION = "_execute_action";
const EXECUTE_BROWSER_ACTION = "_execute_browser_action";
const EXECUTE_MSG_DISPLAY_ACTION = "_execute_message_display_action";
const EXECUTE_COMPOSE_ACTION = "_execute_compose_action";
@ -60,7 +61,12 @@ class MailExtensionShortcuts extends ExtensionShortcuts {
// therefore the listeners for these elements will be garbage collected.
keyElement.addEventListener("command", event => {
let action;
if (name == EXECUTE_BROWSER_ACTION) {
if (
name == EXECUTE_BROWSER_ACTION &&
this.extension.manifestVersion < 3
) {
action = lazy.browserActionFor(this.extension);
} else if (name == EXECUTE_ACTION && this.extension.manifestVersion > 2) {
action = lazy.browserActionFor(this.extension);
} else if (name == EXECUTE_COMPOSE_ACTION) {
action = lazy.composeActionFor(this.extension);

Просмотреть файл

@ -117,16 +117,24 @@ class ContextMenusClickPropHandler {
this.menus = class extends ExtensionAPI {
getAPI(context) {
let { extension } = context;
let onClickedProp = new ContextMenusClickPropHandler(context);
let pendingMenuEvent;
return {
menus: {
create(createProperties, callback) {
if (createProperties.id === null) {
let caller = context.getCaller();
if (extension.persistentBackground && createProperties.id === null) {
createProperties.id = ++gNextMenuItemID;
}
let { onclick } = createProperties;
if (onclick && !context.extension.persistentBackground) {
throw new ExtensionError(
`Property "onclick" cannot be used in menus.create, replace with an "onClicked" event listener.`
);
}
delete createProperties.onclick;
context.childManager
.callParentAsyncFunction("menus.create", [createProperties])
@ -139,7 +147,7 @@ this.menus = class extends ExtensionAPI {
}
})
.catch(error => {
context.withLastError(error, null, () => {
context.withLastError(error, caller, () => {
if (callback) {
context.runSafeWithoutClone(callback);
}
@ -150,6 +158,11 @@ this.menus = class extends ExtensionAPI {
update(id, updateProperties) {
let { onclick } = updateProperties;
if (onclick && !context.extension.persistentBackground) {
throw new ExtensionError(
`Property "onclick" cannot be used in menus.update, replace with an "onClicked" event listener.`
);
}
delete updateProperties.onclick;
return context.childManager
.callParentAsyncFunction("menus.update", [id, updateProperties])
@ -253,6 +266,24 @@ this.menus = class extends ExtensionAPI {
Services.obs.addObserver(pendingMenuEvent, "on-prepare-contextmenu");
Services.tm.dispatchToMainThread(pendingMenuEvent);
},
onClicked: new EventManager({
context,
name: "menus.onClicked",
register: fire => {
let listener = (info, tab) => {
withHandlingUserInput(context.contentWindow, () =>
fire.sync(info, tab)
);
};
let event = context.childManager.getParentEvent("menus.onClicked");
event.addListener(listener);
return () => {
event.removeListener(listener);
};
},
}).api(),
},
};
}

Просмотреть файл

@ -228,6 +228,9 @@
"scopes": [
"addon_parent"
],
"events": [
"startup"
],
"paths": [
[
"menus"

Просмотреть файл

@ -125,12 +125,14 @@ this.browserAction = class extends ToolbarButtonAPI {
// This needs to work in normal window and message window.
let tab = tabTracker.activeTab;
let browser = tab.linkedBrowser || tab.getBrowser();
const action =
this.extension.manifestVersion < 3 ? "onBrowserAction" : "onAction";
global.actionContextMenu({
tab,
pageUrl: browser.currentURI.spec,
extension: this.extension,
onBrowserAction: true,
[action]: true,
menu,
});
}

Просмотреть файл

@ -24,7 +24,7 @@ var { DefaultMap, ExtensionError } = ExtensionUtils;
var { ExtensionParent } = ChromeUtils.import(
"resource://gre/modules/ExtensionParent.jsm"
);
var { IconDetails } = ExtensionParent;
var { IconDetails, StartupCache } = ExtensionParent;
var { ExtensionCommon } = ChromeUtils.import(
"resource://gre/modules/ExtensionCommon.jsm"
@ -38,6 +38,12 @@ const ACTION_MENU_TOP_LEVEL_LIMIT = 6;
// this cannot be a weak map.
var gMenuMap = new Map();
// Map[Extension -> Map[ID -> MenuCreateProperties]]
// The map object for each extension is a reference to the same
// object in StartupCache.menus. This provides a non-async
// getter for that object.
var gStartupCache = new Map();
// Map[Extension -> MenuItem]
var gRootItems = new Map();
@ -124,6 +130,7 @@ var gMenuBuilder = {
let rootElements;
if (
contextData.onAction ||
contextData.onBrowserAction ||
contextData.onComposeAction ||
contextData.onMessageDisplayAction
@ -255,7 +262,10 @@ var gMenuBuilder = {
if (forceManifestIcons) {
for (let rootElement of children) {
// Display the extension icon on the root element.
if (root.extension.manifest.icons) {
if (
root.extension.manifest.icons &&
rootElement.getAttribute("type") !== "checkbox"
) {
this.setMenuItemIcon(
rootElement,
root.extension,
@ -432,17 +442,22 @@ var gMenuBuilder = {
info.modifiers = clickModifiersFromEvent(event);
info.button = button;
let _execute_action =
item.extension.manifestVersion < 3
? "_execute_browser_action"
: "_execute_action";
// Allow menus to open various actions supported in webext prior
// to notifying onclicked.
let actionFor = {
_execute_browser_action: global.browserActionFor,
[_execute_action]: global.browserActionFor,
_execute_compose_action: global.composeActionFor,
_execute_message_display_action: global.messageDisplayActionFor,
}[item.command];
if (actionFor) {
let win = event.target.ownerGlobal;
actionFor(item.extension).triggerAction(win);
return;
}
item.extension.emit(
@ -549,6 +564,7 @@ var gMenuBuilder = {
}
if (
contextData.onAction ||
contextData.onBrowserAction ||
contextData.onComposeAction ||
contextData.onMessageDisplayAction
@ -594,7 +610,7 @@ var gMenuBuilder = {
itemsToCleanUp: new Set(),
};
// Called from pageAction or browserAction popup.
// Called from different action popups.
global.actionContextMenu = function(contextData) {
contextData.originalViewType = "tab";
gMenuBuilder.build(contextData);
@ -610,6 +626,7 @@ const contextsMap = {
isTextSelected: "selection",
onVideo: "video",
onAction: "action",
onBrowserAction: "browser_action",
onComposeAction: "compose_action",
onMessageDisplayAction: "message_display_action",
@ -775,42 +792,46 @@ async function addMenuEventInfo(
}
}
function MenuItem(extension, createProperties, isRoot = false) {
this.extension = extension;
this.children = [];
this.parent = null;
this.tabManager = extension.tabManager;
class MenuItem {
constructor(extension, createProperties, isRoot = false) {
this.extension = extension;
this.children = [];
this.parent = null;
this.tabManager = extension.tabManager;
this.setDefaults();
this.setProps(createProperties);
this.setDefaults();
this.setProps(createProperties);
if (!this.hasOwnProperty("_id")) {
this.id = gNextMenuItemID++;
if (!this.hasOwnProperty("_id")) {
this.id = gNextMenuItemID++;
}
// If the item is not the root and has no parent
// it must be a child of the root.
if (!isRoot && !this.parent) {
this.root.addChild(this);
}
}
// If the item is not the root and has no parent
// it must be a child of the root.
if (!isRoot && !this.parent) {
this.root.addChild(this);
}
}
MenuItem.prototype = {
setProps(createProperties) {
for (let propName in createProperties) {
if (createProperties[propName] === null) {
static mergeProps(obj, properties) {
for (let propName in properties) {
if (properties[propName] === null) {
// Omitted optional argument.
continue;
}
this[propName] = createProperties[propName];
obj[propName] = properties[propName];
}
if ("icons" in createProperties) {
if (createProperties.icons === null) {
this.icons = null;
} else if (typeof createProperties.icons == "string") {
this.icons = { 16: createProperties.icons };
if ("icons" in properties) {
if (properties.icons === null) {
obj.icons = null;
} else if (typeof properties.icons == "string") {
obj.icons = { 16: properties.icons };
}
}
}
setProps(createProperties) {
MenuItem.mergeProps(this, createProperties);
if (createProperties.documentUrlPatterns != null) {
this.documentUrlMatchPattern = new MatchPatternSet(
@ -834,7 +855,7 @@ MenuItem.prototype = {
if (createProperties.parentId && !createProperties.contexts) {
this.contexts = this.parent.contexts;
}
},
}
setDefaults() {
this.setProps({
@ -844,7 +865,7 @@ MenuItem.prototype = {
enabled: true,
visible: true,
});
},
}
set id(id) {
if (this.hasOwnProperty("_id")) {
@ -855,11 +876,11 @@ MenuItem.prototype = {
throw new ExtensionError(`ID already exists: ${id}`);
}
this._id = id;
},
}
get id() {
return this._id;
},
}
get elementId() {
let id = this.id;
@ -871,7 +892,7 @@ MenuItem.prototype = {
id = `_${id}`;
}
return `${makeWidgetId(this.extension.id)}-menuitem-${id}`;
},
}
ensureValidParentId(parentId) {
if (parentId === undefined) {
@ -890,7 +911,25 @@ MenuItem.prototype = {
);
}
}
},
}
/**
* When updating menu properties we need to ensure parents exist
* in the cache map before children. That allows the menus to be
* created in the correct sequence on startup. This reparents the
* tree starting from this instance of MenuItem.
*/
reparentInCache() {
let { id, extension } = this;
let cachedMap = gStartupCache.get(extension);
let createProperties = cachedMap.get(id);
cachedMap.delete(id);
cachedMap.set(id, createProperties);
for (let child of this.children) {
child.reparentInCache();
}
}
set parentId(parentId) {
this.ensureValidParentId(parentId);
@ -905,11 +944,11 @@ MenuItem.prototype = {
let menuMap = gMenuMap.get(this.extension);
menuMap.get(parentId).addChild(this);
}
},
}
get parentId() {
return this.parent ? this.parent.id : undefined;
},
}
addChild(child) {
if (child.parent) {
@ -917,7 +956,7 @@ MenuItem.prototype = {
}
this.children.push(child);
child.parent = this;
},
}
detachChild(child) {
let idx = this.children.indexOf(child);
@ -926,7 +965,7 @@ MenuItem.prototype = {
}
this.children.splice(idx, 1);
child.parent = null;
},
}
get root() {
let extension = this.extension;
@ -940,7 +979,7 @@ MenuItem.prototype = {
}
return gRootItems.get(extension);
},
}
remove() {
if (this.parent) {
@ -953,10 +992,14 @@ MenuItem.prototype = {
let menuMap = gMenuMap.get(this.extension);
menuMap.delete(this.id);
// Menu items are saved if !extension.persistentBackground.
if (gStartupCache.get(this.extension)?.delete(this.id)) {
StartupCache.save();
}
if (this.root == this) {
gRootItems.delete(this.extension);
}
},
}
async getClickInfo(contextData, wasChecked) {
let info = {
@ -974,7 +1017,7 @@ MenuItem.prototype = {
}
return info;
},
}
enabledForContext(contextData) {
if (!this.visible) {
@ -1035,8 +1078,8 @@ MenuItem.prototype = {
}
return true;
},
};
}
}
// While any extensions are active, this Tracker registers to observe/listen
// for menu events from both Tools and context menus, both content and chrome.
@ -1209,7 +1252,7 @@ const menuTracker = {
},
};
this.menus = class extends ExtensionAPI {
this.menus = class extends ExtensionAPIPersistent {
constructor(extension) {
super(extension);
@ -1219,6 +1262,38 @@ this.menus = class extends ExtensionAPI {
gMenuMap.set(extension, new Map());
}
restoreFromCache() {
let { extension } = this;
// ensure extension has not shutdown
if (!this.extension) {
return;
}
for (let createProperties of gStartupCache.get(extension).values()) {
// The order of menu creation is significant, see reparentInCache.
let menuItem = new MenuItem(extension, createProperties);
gMenuMap.get(extension).set(menuItem.id, menuItem);
}
// Used for testing
extension.emit("webext-menus-created", gMenuMap.get(extension));
}
async onStartup() {
let { extension } = this;
if (extension.persistentBackground) {
return;
}
// Using the map retains insertion order.
let cachedMenus = await StartupCache.menus.get(extension.id, () => {
return new Map();
});
gStartupCache.set(extension, cachedMenus);
if (!cachedMenus.size) {
return;
}
this.restoreFromCache();
}
onShutdown() {
let { extension } = this;
@ -1226,6 +1301,7 @@ this.menus = class extends ExtensionAPI {
gMenuMap.delete(extension);
gRootItems.delete(extension);
gShownMenuItems.delete(extension);
gStartupCache.delete(extension);
gOnShownSubscribers.delete(extension);
if (!gMenuMap.size) {
menuTracker.unregister();
@ -1233,6 +1309,116 @@ this.menus = class extends ExtensionAPI {
}
}
PERSISTENT_EVENTS = {
onShown({ fire }) {
let { extension } = this;
let listener = async (event, menuIds, contextData) => {
let info = {
menuIds,
contexts: Array.from(getMenuContexts(contextData)),
};
let nativeTab = contextData.tab;
// The menus.onShown event is fired before the user has consciously
// interacted with an extension, so we require permissions before
// exposing sensitive contextual data.
let contextUrl = contextData.inFrame
? contextData.frameUrl
: contextData.pageUrl;
let ownerDocumentUrl = contextData.menu.ownerDocument.location.href;
let contextScheme;
if (contextUrl) {
contextScheme = Services.io.newURI(contextUrl).scheme;
}
let includeSensitiveData =
(nativeTab &&
extension.tabManager.hasActiveTabPermission(nativeTab)) ||
(contextUrl && extension.allowedOrigins.matches(contextUrl)) ||
(MESSAGE_PROTOCOLS.includes(contextScheme) &&
extension.hasPermission("messagesRead")) ||
(ownerDocumentUrl ==
"chrome://messenger/content/messengercompose/messengercompose.xhtml" &&
extension.hasPermission("compose"));
await addMenuEventInfo(
info,
contextData,
extension,
includeSensitiveData
);
let tab = nativeTab && extension.tabManager.convert(nativeTab);
fire.sync(info, tab);
};
gOnShownSubscribers.get(extension).add(listener);
extension.on("webext-menu-shown", listener);
return {
unregister() {
const listeners = gOnShownSubscribers.get(extension);
listeners.delete(listener);
if (listeners.size === 0) {
gOnShownSubscribers.delete(extension);
}
extension.off("webext-menu-shown", listener);
},
convert(_fire) {
fire = _fire;
},
};
},
onHidden({ fire }) {
let { extension } = this;
let listener = () => {
fire.sync();
};
extension.on("webext-menu-hidden", listener);
return {
unregister() {
extension.off("webext-menu-hidden", listener);
},
convert(_fire) {
fire = _fire;
},
};
},
onClicked({ context, fire }) {
let { extension } = this;
let listener = async (event, info, nativeTab) => {
let { linkedBrowser } = nativeTab || tabTracker.activeTab;
let tab = nativeTab && extension.tabManager.convert(nativeTab);
if (fire.wakeup) {
// force the wakeup, thus the call to convert to get the context.
await fire.wakeup();
// If while waiting the tab disappeared we bail out.
if (
!linkedBrowser.ownerGlobal.gBrowser.getTabForBrowser(linkedBrowser)
) {
console.error(
`menus.onClicked: target tab closed during background startup.`
);
return;
}
}
context.withPendingBrowser(linkedBrowser, () => fire.sync(info, tab));
};
extension.on("webext-menu-menuitem-click", listener);
return {
unregister() {
extension.off("webext-menu-menuitem-click", listener);
},
convert(_fire, _context) {
fire = _fire;
context = _context;
},
};
},
};
getAPI(context) {
let { extension } = context;
@ -1244,90 +1430,74 @@ this.menus = class extends ExtensionAPI {
onShown: new EventManager({
context,
name: "menus.onShown",
register: fire => {
let listener = async (event, menuIds, contextData) => {
let info = {
menuIds,
contexts: Array.from(getMenuContexts(contextData)),
};
let nativeTab = contextData.tab;
// The menus.onShown event is fired before the user has consciously
// interacted with an extension, so we require permissions before
// exposing sensitive contextual data.
let contextUrl = contextData.inFrame
? contextData.frameUrl
: contextData.pageUrl;
let ownerDocumentUrl =
contextData.menu.ownerDocument.location.href;
let contextScheme;
if (contextUrl) {
contextScheme = Services.io.newURI(contextUrl).scheme;
}
let includeSensitiveData =
(nativeTab &&
extension.tabManager.hasActiveTabPermission(nativeTab)) ||
(contextUrl && extension.allowedOrigins.matches(contextUrl)) ||
(MESSAGE_PROTOCOLS.includes(contextScheme) &&
extension.hasPermission("messagesRead")) ||
(ownerDocumentUrl ==
"chrome://messenger/content/messengercompose/messengercompose.xhtml" &&
extension.hasPermission("compose"));
await addMenuEventInfo(
info,
contextData,
extension,
includeSensitiveData
);
let tab = nativeTab && extension.tabManager.convert(nativeTab);
fire.sync(info, tab);
};
gOnShownSubscribers.get(extension).add(context);
extension.on("webext-menu-shown", listener);
return () => {
const contexts = gOnShownSubscribers.get(extension);
contexts.delete(context);
if (contexts.size === 0) {
gOnShownSubscribers.delete(extension);
}
extension.off("webext-menu-shown", listener);
};
},
module: "menus",
event: "onShown",
extensionApi: this,
}).api(),
onHidden: new EventManager({
context,
name: "menus.onHidden",
register: fire => {
let listener = () => {
fire.sync();
};
extension.on("webext-menu-hidden", listener);
return () => {
extension.off("webext-menu-hidden", listener);
};
},
module: "menus",
event: "onHidden",
extensionApi: this,
}).api(),
onClicked: new EventManager({
context,
module: "menus",
event: "onClicked",
extensionApi: this,
}).api(),
create(createProperties) {
// event pages require id
if (!extension.persistentBackground) {
if (!createProperties.id) {
throw new ExtensionError(
"menus.create requires an id for non-persistent background scripts."
);
}
if (gMenuMap.get(extension).has(createProperties.id)) {
throw new ExtensionError(
`The menu id ${createProperties.id} already exists in menus.create.`
);
}
}
// Note that the id is required by the schema. If the addon did not set
// it, the implementation of menus.create in the child should
// have added it.
// it, the implementation of menus.create in the child will add it for
// extensions with persistent backgrounds, but not otherwise.
let menuItem = new MenuItem(extension, createProperties);
gMenuMap.get(extension).set(menuItem.id, menuItem);
if (!extension.persistentBackground) {
// Only cache properties that are necessary.
let cached = {};
MenuItem.mergeProps(cached, createProperties);
gStartupCache.get(extension).set(menuItem.id, cached);
StartupCache.save();
}
},
update(id, updateProperties) {
let menuItem = gMenuMap.get(extension).get(id);
if (menuItem) {
menuItem.setProps(updateProperties);
if (!menuItem) {
return;
}
menuItem.setProps(updateProperties);
// Update the startup cache for non-persistent extensions.
if (extension.persistentBackground) {
return;
}
let cached = gStartupCache.get(extension).get(id);
let reparent =
updateProperties.parentId != null &&
cached.parentId != updateProperties.parentId;
MenuItem.mergeProps(cached, updateProperties);
if (reparent) {
// The order of menu creation is significant, see reparentInCache.
menuItem.reparentInCache();
}
StartupCache.save();
},
remove(id) {
@ -1342,27 +1512,15 @@ this.menus = class extends ExtensionAPI {
if (root) {
root.remove();
}
// Should be empty, just extra assurance.
if (!extension.persistentBackground) {
let cached = gStartupCache.get(extension);
if (cached.size) {
cached.clear();
StartupCache.save();
}
}
},
onClicked: new EventManager({
context,
name: "menus.onClicked",
inputHandling: true,
register: fire => {
let listener = (event, info, nativeTab) => {
let { linkedBrowser } = nativeTab || tabTracker.activeTab;
let tab = nativeTab && extension.tabManager.convert(nativeTab);
context.withPendingBrowser(linkedBrowser, () =>
fire.sync(info, tab)
);
};
extension.on("webext-menu-menuitem-click", listener);
return () => {
extension.off("webext-menu-menuitem-click", listener);
};
},
}).api(),
},
};
}

Просмотреть файл

@ -17,7 +17,7 @@
"commands": {
"type": "object",
"optional": true,
"description": "A <em>dictionary object</em> defining one or more commands as <em>name-value</em> pairs, the <em>name</em> being the name of the command and the <em>value</em> being a :ref:`commands.CommandsShortcut`. The <em>name</em> may also be one of the following built-in special shortcuts: \n * <value>_execute_browser_action</value> \n * <value>_execute_compose_action</value> \n * <value>_execute_message_display_action</value>\nExample: <literalinclude>includes/commands/manifest.json<lang>JSON</lang></literalinclude>",
"description": "A <em>dictionary object</em> defining one or more commands as <em>name-value</em> pairs, the <em>name</em> being the name of the command and the <em>value</em> being a :ref:`commands.CommandsShortcut`. The <em>name</em> may also be one of the following built-in special shortcuts: \n * <value>_execute_action</value> \n * <value>_execute_compose_action</value> \n * <value>_execute_message_display_action</value>\nExample: <literalinclude>includes/commands/manifest.json<lang>JSON</lang></literalinclude>",
"additionalProperties": {
"type": "object",
"additionalProperties": {

Просмотреть файл

@ -44,28 +44,45 @@
"types": [
{
"id": "ContextType",
"type": "string",
"enum": [
"all",
"page",
"frame",
"selection",
"link",
"editable",
"password",
"image",
"video",
"audio",
"browser_action",
"compose_action",
"message_display_action",
"tab",
"message_list",
"folder_pane",
"compose_attachments",
"message_attachments",
"all_message_attachments",
"tools_menu"
"choices": [
{
"type": "string",
"enum": [
"all",
"page",
"frame",
"selection",
"link",
"editable",
"password",
"image",
"video",
"audio",
"compose_action",
"message_display_action",
"tab",
"message_list",
"folder_pane",
"compose_attachments",
"message_attachments",
"all_message_attachments",
"tools_menu"
]
},
{
"type": "string",
"max_manifest_version": 2,
"enum": [
"browser_action"
]
},
{
"type": "string",
"min_manifest_version": 3,
"enum": [
"action"
]
}
],
"description": "The different contexts a menu can appear in. Specifying <value>all</value> is equivalent to the combination of all other contexts excluding <value>tab</value> and <value>tools_menu</value>. More information about each context can be found in the `Supported UI Elements <|link-ui-elements|>`__ article on developer.thunderbird.net."
},
@ -490,7 +507,7 @@
"command": {
"type": "string",
"optional": true,
"description": "Specifies a command to issue for the context click. Currently supports internal commands <value>_execute_browser_action</value>, <value>_execute_compose_action</value> and <value>_execute_message_display_action</value>."
"description": "Specifies a command to issue for the context click. Currently supports internal commands <value>_execute_action</value>, <value>_execute_compose_action</value> and <value>_execute_message_display_action</value>."
}
}
},

Просмотреть файл

@ -9,13 +9,16 @@ prefs =
mailnews.start_page.override_url=about:blank
mailnews.start_page.url=about:blank
subsuite = thunderbird
support-files = ../xpcshell/data/utils.js
support-files =
head_menus.js
../xpcshell/data/utils.js
tags = webextensions
[browser_ext_addressBooksUI.js]
tags = addrbook
[browser_ext_browserAction.js]
[browser_ext_browserAction_popup_click.js]
[browser_ext_browserAction_popup_click_mv3_event_pages.js]
[browser_ext_browserAction_properties.js]
[browser_ext_cloudFile.js]
support-files = data/cloudFile1.txt data/cloudFile2.txt
@ -45,6 +48,7 @@ support-files = data/cloudFile1.txt data/cloudFile2.txt
[browser_ext_compose_sendMessage.js]
[browser_ext_composeAction.js]
[browser_ext_composeAction_popup_click.js]
[browser_ext_composeAction_popup_click_mv3_event_pages.js]
[browser_ext_composeAction_properties.js]
[browser_ext_composeScripts.js]
[browser_ext_contentScripts.js]
@ -69,6 +73,7 @@ skip-if = true
reason = FixMe: This is messing up msgHdr of test messages and breaks the following tests.
[browser_ext_messageDisplayAction.js]
[browser_ext_messageDisplayAction_popup_click.js]
[browser_ext_messageDisplayAction_popup_click_mv3_event_pages.js]
[browser_ext_messageDisplayAction_properties.js]
[browser_ext_messageDisplayScripts.js]
[browser_ext_quickFilter.js]

Просмотреть файл

@ -33,7 +33,7 @@ add_setup(async () => {
});
// This test uses a command from the menus API to open the popup.
add_task(async function test_popup_open_with_menu_command() {
add_task(async function test_popup_open_with_menu_command_mv2() {
info("3-pane tab");
for (let area of [null, "tabstoolbar"]) {
let testConfig = {
@ -81,6 +81,56 @@ add_task(async function test_popup_open_with_menu_command() {
}
});
add_task(async function test_popup_open_with_menu_command_mv3() {
info("3-pane tab");
for (let area of [null, "tabstoolbar"]) {
let testConfig = {
manifest_version: 3,
actionType: "action",
testType: "open-with-menu-command",
default_area: area,
window,
};
await run_popup_test({
...testConfig,
});
await run_popup_test({
...testConfig,
use_default_popup: true,
});
await run_popup_test({
...testConfig,
disable_button: true,
});
}
info("Message window");
{
let messageWindow = await openMessageInWindow(messages.getNext());
let testConfig = {
manifest_version: 3,
actionType: "action",
testType: "open-with-menu-command",
default_windows: ["messageDisplay"],
window: messageWindow,
};
await run_popup_test({
...testConfig,
});
await run_popup_test({
...testConfig,
use_default_popup: true,
});
await run_popup_test({
...testConfig,
disable_button: true,
});
messageWindow.close();
}
});
add_task(async function test_theme_icons() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {

Просмотреть файл

@ -94,65 +94,3 @@ add_task(async function test_popup_open_with_click() {
messageWindow.close();
}
});
async function subtest_popup_open_with_click_MV3_event_pages(
terminateBackground
) {
info("3-pane tab");
for (let area of [null, "tabstoolbar"]) {
let testConfig = {
actionType: "action",
manifest_version: 3,
terminateBackground,
testType: "open-with-mouse-click",
default_area: area,
window,
};
await run_popup_test({
...testConfig,
});
await run_popup_test({
...testConfig,
disable_button: true,
});
await run_popup_test({
...testConfig,
use_default_popup: true,
});
}
info("Message window");
{
let messageWindow = await openMessageInWindow(messages.getNext());
let testConfig = {
actionType: "action",
manifest_version: 3,
terminateBackground,
testType: "open-with-mouse-click",
default_windows: ["messageDisplay"],
window: messageWindow,
};
await run_popup_test({
...testConfig,
});
await run_popup_test({
...testConfig,
disable_button: true,
});
await run_popup_test({
...testConfig,
use_default_popup: true,
});
messageWindow.close();
}
}
// This MV3 test clicks on the action button to open the popup.
add_task(async function test_event_pages_without_background_termination() {
await subtest_popup_open_with_click_MV3_event_pages(false);
});
// This MV3 test clicks on the action button to open the popup (background termination).
add_task(async function test_event_pages_with_background_termination() {
await subtest_popup_open_with_click_MV3_event_pages(true);
});

Просмотреть файл

@ -0,0 +1,95 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const { AddonManager } = ChromeUtils.import(
"resource://gre/modules/AddonManager.jsm"
);
let account;
let messages;
add_setup(async () => {
account = createAccount();
let rootFolder = account.incomingServer.rootFolder;
let subFolders = rootFolder.subFolders;
createMessages(subFolders[0], 10);
messages = subFolders[0].messages;
// This tests selects a folder, so make sure the folder pane is visible.
if (
document.getElementById("folderpane_splitter").getAttribute("state") ==
"collapsed"
) {
window.MsgToggleFolderPane();
}
if (window.IsMessagePaneCollapsed()) {
window.MsgToggleMessagePane();
}
window.gFolderTreeView.selectFolder(subFolders[0]);
window.gFolderDisplay.selectViewIndex(0);
await BrowserTestUtils.browserLoaded(window.getMessagePaneBrowser());
});
async function subtest_popup_open_with_click_MV3_event_pages(
terminateBackground
) {
info("3-pane tab");
for (let area of [null, "tabstoolbar"]) {
let testConfig = {
actionType: "action",
manifest_version: 3,
terminateBackground,
testType: "open-with-mouse-click",
default_area: area,
window,
};
await run_popup_test({
...testConfig,
});
await run_popup_test({
...testConfig,
disable_button: true,
});
await run_popup_test({
...testConfig,
use_default_popup: true,
});
}
info("Message window");
{
let messageWindow = await openMessageInWindow(messages.getNext());
let testConfig = {
actionType: "action",
manifest_version: 3,
terminateBackground,
testType: "open-with-mouse-click",
default_windows: ["messageDisplay"],
window: messageWindow,
};
await run_popup_test({
...testConfig,
});
await run_popup_test({
...testConfig,
disable_button: true,
});
await run_popup_test({
...testConfig,
use_default_popup: true,
});
messageWindow.close();
}
}
// This MV3 test clicks on the action button to open the popup.
add_task(async function test_event_pages_without_background_termination() {
await subtest_popup_open_with_click_MV3_event_pages(false);
});
// This MV3 test clicks on the action button to open the popup (background termination).
add_task(async function test_event_pages_with_background_termination() {
await subtest_popup_open_with_click_MV3_event_pages(true);
});

Просмотреть файл

@ -2,7 +2,7 @@
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
async function testExecuteBrowserActionWithOptions(options = {}) {
async function testExecuteBrowserActionWithOptions_mv2(options = {}) {
// Make sure the mouse isn't hovering over the browserAction widget.
let folderTree = document.getElementById("folderTree");
EventUtils.synthesizeMouseAtCenter(folderTree, { type: "mouseover" }, window);
@ -47,11 +47,9 @@ async function testExecuteBrowserActionWithOptions(options = {}) {
extensionOptions.background = () => {
browser.test.onMessage.addListener((message, withPopup) => {
browser.commands.onCommand.addListener(commandName => {
if (commandName == "_execute_browser_action") {
browser.test.fail(
"The onCommand listener should never fire for _execute_browser_action."
);
}
browser.test.fail(
"The onCommand listener should never fire for a valid _execute_* command."
);
});
browser.browserAction.onClicked.addListener(() => {
@ -101,12 +99,120 @@ async function testExecuteBrowserActionWithOptions(options = {}) {
await extension.unload();
}
add_task(async function test_execute_browser_action_with_popup() {
await testExecuteBrowserActionWithOptions({
add_task(async function test_execute_browser_action_with_popup_mv2() {
await testExecuteBrowserActionWithOptions_mv2({
withPopup: true,
});
});
add_task(async function test_execute_browser_action_without_popup() {
await testExecuteBrowserActionWithOptions();
add_task(async function test_execute_browser_action_without_popup_mv2() {
await testExecuteBrowserActionWithOptions_mv2();
});
async function testExecuteActionWithOptions_mv3(options = {}) {
// Make sure the mouse isn't hovering over the action widget.
let folderTree = document.getElementById("folderTree");
EventUtils.synthesizeMouseAtCenter(folderTree, { type: "mouseover" }, window);
let extensionOptions = {};
extensionOptions.manifest = {
manifest_version: 3,
commands: {
_execute_action: {
suggested_key: {
default: "Alt+Shift+J",
},
},
},
action: {
browser_style: true,
},
};
if (options.withPopup) {
extensionOptions.manifest.action.default_popup = "popup.html";
extensionOptions.files = {
"popup.html": `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="popup.js"></script>
</head>
<body>
Popup
</body>
</html>
`,
"popup.js": function() {
browser.runtime.sendMessage("from-action-popup");
},
};
}
extensionOptions.background = () => {
browser.test.onMessage.addListener((message, withPopup) => {
browser.commands.onCommand.addListener(commandName => {
browser.test.fail(
"The onCommand listener should never fire for a valid _execute_* command."
);
});
browser.action.onClicked.addListener(() => {
if (withPopup) {
browser.test.fail(
"The onClick listener should never fire if the action has a popup."
);
browser.test.notifyFail("execute-action-on-clicked-fired");
} else {
browser.test.notifyPass("execute-action-on-clicked-fired");
}
});
browser.runtime.onMessage.addListener(msg => {
if (msg == "from-action-popup") {
browser.test.notifyPass("execute-action-popup-opened");
}
});
browser.test.sendMessage("send-keys");
});
};
let extension = ExtensionTestUtils.loadExtension(extensionOptions);
extension.onMessage("send-keys", () => {
EventUtils.synthesizeKey("j", { altKey: true, shiftKey: true });
});
await extension.startup();
await SimpleTest.promiseFocus(window);
// trigger setup of listeners in background and the send-keys msg
extension.sendMessage("withPopup", options.withPopup);
if (options.withPopup) {
await extension.awaitFinish("execute-action-popup-opened");
if (!getBrowserActionPopup(extension)) {
await awaitExtensionPanel(extension);
}
await closeBrowserAction(extension);
} else {
await extension.awaitFinish("execute-action-on-clicked-fired");
}
await extension.unload();
}
add_task(async function test_execute_browser_action_with_popup_mv3() {
await testExecuteActionWithOptions_mv3({
withPopup: true,
});
});
add_task(async function test_execute_browser_action_without_popup_mv3() {
await testExecuteActionWithOptions_mv3();
});

Просмотреть файл

@ -64,11 +64,9 @@ async function testExecuteComposeActionWithOptions(options = {}) {
browser.test.onMessage.addListener((message, withPopup) => {
browser.commands.onCommand.addListener(commandName => {
if (commandName == "_execute_compose_action") {
browser.test.fail(
"The onCommand listener should never fire for _execute_compose_action."
);
}
browser.test.fail(
"The onCommand listener should never fire for a valid _execute_* command."
);
});
browser.composeAction.onClicked.addListener(() => {

Просмотреть файл

@ -52,11 +52,9 @@ async function testExecuteMessageDisplayActionWithOptions(msg, options = {}) {
extensionOptions.background = () => {
browser.test.onMessage.addListener((message, withPopup) => {
browser.commands.onCommand.addListener(commandName => {
if (commandName == "_execute_message_display_action") {
browser.test.fail(
"The onCommand listener should never fire for _execute_message_display_action."
);
}
browser.test.fail(
"The onCommand listener should never fire for a valid _execute_* command."
);
});
browser.messageDisplayAction.onClicked.addListener(() => {

Просмотреть файл

@ -51,45 +51,3 @@ add_task(async function test_popup_open_with_click() {
);
}
});
async function subtest_popup_open_with_click_MV3_event_pages(
terminateBackground
) {
for (let area of [null, "formattoolbar"]) {
let composeWindow = await openComposeWindow(account);
await focusWindow(composeWindow);
let testConfig = {
manifest_version: 3,
terminateBackground,
actionType: "compose_action",
testType: "open-with-mouse-click",
window: composeWindow,
default_area: area,
};
await run_popup_test({
...testConfig,
});
await run_popup_test({
...testConfig,
disable_button: true,
});
await run_popup_test({
...testConfig,
use_default_popup: true,
});
composeWindow.close();
Services.xulStore.removeDocument(
"chrome://messenger/content/messengercompose/messengercompose.xhtml"
);
}
}
// This MV3 test clicks on the action button to open the popup.
add_task(async function test_event_pages_without_background_termination() {
await subtest_popup_open_with_click_MV3_event_pages(false);
});
// This MV3 test clicks on the action button to open the popup (background termination).
add_task(async function test_event_pages_with_background_termination() {
await subtest_popup_open_with_click_MV3_event_pages(true);
});

Просмотреть файл

@ -0,0 +1,59 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const { AddonManager } = ChromeUtils.import(
"resource://gre/modules/AddonManager.jsm"
);
const { AppConstants } = ChromeUtils.importESModule(
"resource://gre/modules/AppConstants.sys.mjs"
);
let account;
add_setup(async () => {
account = createAccount();
addIdentity(account);
});
async function subtest_popup_open_with_click_MV3_event_pages(
terminateBackground
) {
for (let area of [null, "formattoolbar"]) {
let composeWindow = await openComposeWindow(account);
await focusWindow(composeWindow);
let testConfig = {
manifest_version: 3,
terminateBackground,
actionType: "compose_action",
testType: "open-with-mouse-click",
window: composeWindow,
default_area: area,
};
await run_popup_test({
...testConfig,
});
await run_popup_test({
...testConfig,
disable_button: true,
});
await run_popup_test({
...testConfig,
use_default_popup: true,
});
composeWindow.close();
Services.xulStore.removeDocument(
"chrome://messenger/content/messengercompose/messengercompose.xhtml"
);
}
}
// This MV3 test clicks on the action button to open the popup.
add_task(async function test_event_pages_without_background_termination() {
await subtest_popup_open_with_click_MV3_event_pages(false);
});
// This MV3 test clicks on the action button to open the popup (background termination).
add_task(async function test_event_pages_with_background_termination() {
await subtest_popup_open_with_click_MV3_event_pages(true);
});

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -2,8 +2,6 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
requestLongerTimeout(2);
const { AddonManager } = ChromeUtils.import(
"resource://gre/modules/AddonManager.jsm"
);
@ -105,90 +103,3 @@ add_task(async function test_popup_open_with_click() {
messageWindow.close();
}
});
async function subtest_popup_open_with_click_MV3_event_pages(
terminateBackground
) {
info("3-pane tab");
{
let testConfig = {
manifest_version: 3,
terminateBackground,
actionType: "message_display_action",
testType: "open-with-mouse-click",
window,
};
await run_popup_test({
...testConfig,
});
await run_popup_test({
...testConfig,
disable_button: true,
});
await run_popup_test({
...testConfig,
use_default_popup: true,
});
}
info("Message tab");
{
await openMessageInTab(messages.getNext());
let testConfig = {
manifest_version: 3,
terminateBackground,
actionType: "message_display_action",
testType: "open-with-mouse-click",
window,
};
await run_popup_test({
...testConfig,
});
await run_popup_test({
...testConfig,
disable_button: true,
});
await run_popup_test({
...testConfig,
use_default_popup: true,
});
document.getElementById("tabmail").closeTab();
}
info("Message window");
{
let messageWindow = await openMessageInWindow(messages.getNext());
let testConfig = {
manifest_version: 3,
terminateBackground,
actionType: "message_display_action",
testType: "open-with-mouse-click",
window: messageWindow,
};
await run_popup_test({
...testConfig,
});
await run_popup_test({
...testConfig,
disable_button: true,
});
await run_popup_test({
...testConfig,
use_default_popup: true,
});
messageWindow.close();
}
}
// This MV3 test clicks on the action button to open the popup.
add_task(async function test_event_pages_without_background_termination() {
await subtest_popup_open_with_click_MV3_event_pages(false);
});
// This MV3 test clicks on the action button to open the popup (background termination).
add_task(async function test_event_pages_with_background_termination() {
await subtest_popup_open_with_click_MV3_event_pages(true);
});

Просмотреть файл

@ -0,0 +1,120 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const { AddonManager } = ChromeUtils.import(
"resource://gre/modules/AddonManager.jsm"
);
let account;
let messages;
add_setup(async () => {
account = createAccount();
let rootFolder = account.incomingServer.rootFolder;
let subFolders = rootFolder.subFolders;
createMessages(subFolders[0], 10);
messages = subFolders[0].messages;
// This tests selects a folder, so make sure the folder pane is visible.
if (
document.getElementById("folderpane_splitter").getAttribute("state") ==
"collapsed"
) {
window.MsgToggleFolderPane();
}
if (window.IsMessagePaneCollapsed()) {
window.MsgToggleMessagePane();
}
window.gFolderTreeView.selectFolder(subFolders[0]);
window.gFolderDisplay.selectViewIndex(0);
await BrowserTestUtils.browserLoaded(window.getMessagePaneBrowser());
});
async function subtest_popup_open_with_click_MV3_event_pages(
terminateBackground
) {
info("3-pane tab");
{
let testConfig = {
manifest_version: 3,
terminateBackground,
actionType: "message_display_action",
testType: "open-with-mouse-click",
window,
};
await run_popup_test({
...testConfig,
});
await run_popup_test({
...testConfig,
disable_button: true,
});
await run_popup_test({
...testConfig,
use_default_popup: true,
});
}
info("Message tab");
{
await openMessageInTab(messages.getNext());
let testConfig = {
manifest_version: 3,
terminateBackground,
actionType: "message_display_action",
testType: "open-with-mouse-click",
window,
};
await run_popup_test({
...testConfig,
});
await run_popup_test({
...testConfig,
disable_button: true,
});
await run_popup_test({
...testConfig,
use_default_popup: true,
});
document.getElementById("tabmail").closeTab();
}
info("Message window");
{
let messageWindow = await openMessageInWindow(messages.getNext());
let testConfig = {
manifest_version: 3,
terminateBackground,
actionType: "message_display_action",
testType: "open-with-mouse-click",
window: messageWindow,
};
await run_popup_test({
...testConfig,
});
await run_popup_test({
...testConfig,
disable_button: true,
});
await run_popup_test({
...testConfig,
use_default_popup: true,
});
messageWindow.close();
}
}
// This MV3 test clicks on the action button to open the popup.
add_task(async function test_event_pages_without_background_termination() {
await subtest_popup_open_with_click_MV3_event_pages(false);
});
// This MV3 test clicks on the action button to open the popup (background termination).
add_task(async function test_event_pages_with_background_termination() {
await subtest_popup_open_with_click_MV3_event_pages(true);
});

Просмотреть файл

@ -0,0 +1,533 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { ExtensionPermissions } = ChromeUtils.import(
"resource://gre/modules/ExtensionPermissions.jsm"
);
const { mailTestUtils } = ChromeUtils.import(
"resource://testing-common/mailnews/MailTestUtils.jsm"
);
const treeClick = mailTestUtils.treeClick.bind(null, EventUtils, window);
var URL_BASE =
"http://mochi.test:8888/browser/comm/mail/components/extensions/test/browser/data";
/**
* Left-click on something and wait for the context menu to appear.
* For elements in the parent process only.
*
* @param {Element} menu - The <menu> that should appear.
* @param {Element} element - The element to be clicked on.
* @returns {Promise} A promise that resolves when the menu appears.
*/
function leftClick(menu, element) {
let shownPromise = BrowserTestUtils.waitForEvent(menu, "popupshown");
EventUtils.synthesizeMouseAtCenter(element, {}, element.ownerGlobal);
return shownPromise;
}
/**
* Right-click on something and wait for the context menu to appear.
* For elements in the parent process only.
*
* @param {Element} menu - The <menu> that should appear.
* @param {Element} element - The element to be clicked on.
* @returns {Promise} A promise that resolves when the menu appears.
*/
function rightClick(menu, element) {
let shownPromise = BrowserTestUtils.waitForEvent(menu, "popupshown");
EventUtils.synthesizeMouseAtCenter(
element,
{ type: "contextmenu" },
element.ownerGlobal
);
return shownPromise;
}
/**
* Right-click on something in a content document and wait for the context
* menu to appear.
*
* @param {Element} menu - The <menu> that should appear.
* @param {string} selector - CSS selector of the element to be clicked on.
* @param {Element} browser - <browser> containing the element.
* @returns {Promise} A promise that resolves when the menu appears.
*/
async function rightClickOnContent(menu, selector, browser) {
let shownPromise = BrowserTestUtils.waitForEvent(menu, "popupshown");
await BrowserTestUtils.synthesizeMouseAtCenter(
selector,
{ type: "contextmenu" },
browser
);
return shownPromise;
}
/**
* Check the parameters of a browser.onShown event was fired.
*
* @see mail/components/extensions/schemas/menus.json
*
* @param extension
* @param {object} expectedInfo
* @param {Array?} expectedInfo.menuIds
* @param {Array?} expectedInfo.contexts
* @param {Array?} expectedInfo.attachments
* @param {object?} expectedInfo.displayedFolder
* @param {object?} expectedInfo.selectedFolder
* @param {Array?} expectedInfo.selectedMessages
* @param {RegExp?} expectedInfo.pageUrl
* @param {string?} expectedInfo.selectionText
* @param {object} expectedTab
* @param {boolean} expectedTab.active
* @param {integer} expectedTab.index
* @param {boolean} expectedTab.mailTab
*/
async function checkShownEvent(extension, expectedInfo, expectedTab) {
let [info, tab] = await extension.awaitMessage("onShown");
Assert.deepEqual(info.menuIds, expectedInfo.menuIds);
Assert.deepEqual(info.contexts, expectedInfo.contexts);
Assert.equal(
!!info.attachments,
!!expectedInfo.attachments,
"attachments in info"
);
if (expectedInfo.attachments) {
Assert.equal(info.attachments.length, expectedInfo.attachments.length);
for (let i = 0; i < expectedInfo.attachments.length; i++) {
Assert.equal(info.attachments[i].name, expectedInfo.attachments[i].name);
Assert.equal(info.attachments[i].size, expectedInfo.attachments[i].size);
}
}
for (let infoKey of ["displayedFolder", "selectedFolder"]) {
Assert.equal(
!!info[infoKey],
!!expectedInfo[infoKey],
`${infoKey} in info`
);
if (expectedInfo[infoKey]) {
Assert.equal(info[infoKey].accountId, expectedInfo[infoKey].accountId);
Assert.equal(info[infoKey].path, expectedInfo[infoKey].path);
Assert.ok(Array.isArray(info[infoKey].subFolders));
}
}
Assert.equal(
!!info.selectedMessages,
!!expectedInfo.selectedMessages,
"selectedMessages in info"
);
if (expectedInfo.selectedMessages) {
Assert.equal(info.selectedMessages.id, null);
Assert.equal(
info.selectedMessages.messages.length,
expectedInfo.selectedMessages.messages.length
);
for (let i = 0; i < expectedInfo.selectedMessages.messages.length; i++) {
Assert.equal(
info.selectedMessages.messages[i].subject,
expectedInfo.selectedMessages.messages[i].subject
);
}
}
Assert.equal(!!info.pageUrl, !!expectedInfo.pageUrl, "pageUrl in info");
if (expectedInfo.pageUrl) {
if (typeof expectedInfo.pageUrl == "string") {
Assert.equal(info.pageUrl, expectedInfo.pageUrl);
} else {
Assert.ok(info.pageUrl.match(expectedInfo.pageUrl));
}
}
Assert.equal(
!!info.selectionText,
!!expectedInfo.selectionText,
"selectionText in info"
);
if (expectedInfo.selectionText) {
Assert.equal(info.selectionText, expectedInfo.selectionText);
}
Assert.equal(tab.active, expectedTab.active, "tab is active");
Assert.equal(tab.index, expectedTab.index, "tab index");
Assert.equal(tab.mailTab, expectedTab.mailTab, "tab is mailTab");
}
/**
* Check the parameters of a browser.onClicked event was fired.
*
* @see mail/components/extensions/schemas/menus.json
*
* @param extension
* @param {object} expectedInfo
* @param {string?} expectedInfo.selectionText
* @param {string?} expectedInfo.linkText
* @param {RegExp?} expectedInfo.pageUrl
* @param {RegExp?} expectedInfo.linkUrl
* @param {RegExp?} expectedInfo.srcUrl
* @param {object} expectedTab
* @param {boolean} expectedTab.active
* @param {integer} expectedTab.index
* @param {boolean} expectedTab.mailTab
*/
async function checkClickedEvent(extension, expectedInfo, expectedTab) {
let [info, tab] = await extension.awaitMessage("onClicked");
Assert.equal(info.selectionText, expectedInfo.selectionText, "selectionText");
Assert.equal(info.linkText, expectedInfo.linkText, "linkText");
if (expectedInfo.menuItemId) {
Assert.equal(info.menuItemId, expectedInfo.menuItemId, "menuItemId");
}
for (let infoKey of ["pageUrl", "linkUrl", "srcUrl"]) {
Assert.equal(
!!info[infoKey],
!!expectedInfo[infoKey],
`${infoKey} in info`
);
if (expectedInfo[infoKey]) {
if (typeof expectedInfo[infoKey] == "string") {
Assert.equal(info[infoKey], expectedInfo[infoKey]);
} else {
Assert.ok(info[infoKey].match(expectedInfo[infoKey]));
}
}
}
Assert.equal(tab.active, expectedTab.active, "tab is active");
Assert.equal(tab.index, expectedTab.index, "tab index");
Assert.equal(tab.mailTab, expectedTab.mailTab, "tab is mailTab");
}
async function getMenuExtension(manifest) {
let details = {
files: {
"background.js": async () => {
let contexts = [
"audio",
"compose_action",
"message_display_action",
"editable",
"frame",
"image",
"link",
"page",
"password",
"selection",
"tab",
"video",
"message_list",
"folder_pane",
"compose_attachments",
"tools_menu",
];
if (browser.runtime.getManifest().manifest_version > 2) {
contexts.push("action");
} else {
contexts.push("browser_action");
}
for (let context of contexts) {
browser.menus.create({
id: context,
title: context,
contexts: [context],
});
}
browser.menus.onShown.addListener((...args) => {
browser.test.sendMessage("onShown", args);
});
browser.menus.onClicked.addListener((...args) => {
browser.test.sendMessage("onClicked", args);
});
browser.test.sendMessage("menus-created");
},
},
manifest: {
browser_specific_settings: {
gecko: {
id: "menus@mochi.test",
},
},
background: { scripts: ["background.js"] },
...manifest,
},
useAddonManager: "temporary",
};
if (!details.manifest.permissions) {
details.manifest.permissions = [];
}
details.manifest.permissions.push("menus");
console.log(JSON.stringify(details, 2));
let extension = ExtensionTestUtils.loadExtension(details);
if (details.manifest.host_permissions) {
// MV3 has to manually grant the requested permission.
await ExtensionPermissions.add("menus@mochi.test", {
permissions: [],
origins: details.manifest.host_permissions,
});
}
return extension;
}
async function subtest_content(
extension,
extensionHasPermission,
browser,
pageUrl,
tab
) {
if (
browser.webProgress?.isLoadingDocument ||
!browser.currentURI ||
browser.currentURI?.spec == "about:blank"
) {
await BrowserTestUtils.browserLoaded(
browser,
undefined,
url => url != "about:blank"
);
}
let ownerDocument = browser.ownerDocument;
let menu = ownerDocument.getElementById(browser.getAttribute("context"));
await BrowserTestUtils.synthesizeMouseAtCenter("body", {}, browser);
info("Test a part of the page with no content.");
await rightClickOnContent(menu, "body", browser);
Assert.ok(menu.querySelector("#menus_mochi_test-menuitem-_page"));
let hiddenPromise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
menu.hidePopup();
await hiddenPromise;
// Sometimes, the popup will open then instantly disappear. It seems to
// still be hiding after the previous appearance. If we wait a little bit,
// this doesn't happen.
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(r => setTimeout(r, 250));
await checkShownEvent(
extension,
{
menuIds: ["page"],
contexts: ["page", "all"],
pageUrl: extensionHasPermission ? pageUrl : undefined,
},
tab
);
info("Test selection.");
await SpecialPowers.spawn(browser, [], () => {
let text = content.document.querySelector("p");
content.getSelection().selectAllChildren(text);
});
await rightClickOnContent(menu, "p", browser);
Assert.ok(menu.querySelector("#menus_mochi_test-menuitem-_selection"));
await checkShownEvent(
extension,
{
pageUrl: extensionHasPermission ? pageUrl : undefined,
selectionText: extensionHasPermission ? "This is text." : undefined,
menuIds: ["selection"],
contexts: ["selection", "all"],
},
tab
);
hiddenPromise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
let clickedPromise = checkClickedEvent(
extension,
{
pageUrl,
selectionText: "This is text.",
},
tab
);
menu.activateItem(
menu.querySelector("#menus_mochi_test-menuitem-_selection")
);
await clickedPromise;
await hiddenPromise;
// Sometimes, the popup will open then instantly disappear. It seems to
// still be hiding after the previous appearance. If we wait a little bit,
// this doesn't happen.
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(r => setTimeout(r, 250));
await BrowserTestUtils.synthesizeMouseAtCenter("body", {}, browser); // Select nothing.
info("Test link.");
await rightClickOnContent(menu, "a", browser);
Assert.ok(menu.querySelector("#menus_mochi_test-menuitem-_link"));
await checkShownEvent(
extension,
{
pageUrl: extensionHasPermission ? pageUrl : undefined,
menuIds: ["link"],
contexts: ["link", "all"],
},
tab
);
hiddenPromise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
clickedPromise = checkClickedEvent(
extension,
{
pageUrl,
linkUrl: "http://mochi.test:8888/",
linkText: "This is a link with text.",
},
tab
);
menu.activateItem(menu.querySelector("#menus_mochi_test-menuitem-_link"));
await clickedPromise;
await hiddenPromise;
// Sometimes, the popup will open then instantly disappear. It seems to
// still be hiding after the previous appearance. If we wait a little bit,
// this doesn't happen.
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(r => setTimeout(r, 250));
info("Test image.");
await rightClickOnContent(menu, "img", browser);
Assert.ok(menu.querySelector("#menus_mochi_test-menuitem-_image"));
await checkShownEvent(
extension,
{
pageUrl: extensionHasPermission ? pageUrl : undefined,
menuIds: ["image"],
contexts: ["image", "all"],
},
tab
);
hiddenPromise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
clickedPromise = checkClickedEvent(
extension,
{
pageUrl,
srcUrl: `${URL_BASE}/tb-logo.png`,
},
tab
);
menu.activateItem(menu.querySelector("#menus_mochi_test-menuitem-_image"));
await clickedPromise;
await hiddenPromise;
// Sometimes, the popup will open then instantly disappear. It seems to
// still be hiding after the previous appearance. If we wait a little bit,
// this doesn't happen.
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(r => setTimeout(r, 250));
}
// Test UI elements which have been made accessible for the menus API.
// Assumed to be run after subtest_content, so we know everything has finished
// loading.
async function subtest_element(
extension,
extensionHasPermission,
element,
pageUrl,
tab
) {
for (let selectedTest of [false, true]) {
element.focus();
if (selectedTest) {
element.value = "This is selected text.";
element.select();
} else {
element.value = "";
}
let event = await rightClick(element.ownerGlobal, element);
let menu = event.target;
let trigger = menu.triggerNode;
let menuitem = menu.querySelector("#menus_mochi_test-menuitem-_editable");
Assert.equal(
element.id,
trigger.id,
"Contextmenu of correct element has been triggered."
);
Assert.equal(
menuitem.id,
"menus_mochi_test-menuitem-_editable",
"Contextmenu includes menu."
);
await checkShownEvent(
extension,
{
menuIds: selectedTest ? ["editable", "selection"] : ["editable"],
contexts: selectedTest
? ["editable", "selection", "all"]
: ["editable", "all"],
pageUrl: extensionHasPermission ? pageUrl : undefined,
selectionText:
extensionHasPermission && selectedTest
? "This is selected text."
: undefined,
},
tab
);
// With text being selected, there will be two "context" entries in an
// extension submenu. Open the submenu.
let submenu = null;
if (selectedTest) {
for (let foundMenu of menu.querySelectorAll(
"[id^='menus_mochi_test-menuitem-']"
)) {
if (!foundMenu.id.startsWith("menus_mochi_test-menuitem-_")) {
submenu = foundMenu;
}
}
Assert.ok(submenu, "Submenu found.");
let submenuPromise = BrowserTestUtils.waitForEvent(
element.ownerGlobal,
"popupshown"
);
submenu.openMenu(true);
await submenuPromise;
}
let hiddenPromise = BrowserTestUtils.waitForEvent(
element.ownerGlobal,
"popuphidden"
);
let clickedPromise = checkClickedEvent(
extension,
{
pageUrl,
selectionText: selectedTest ? "This is selected text." : undefined,
},
tab
);
if (submenu) {
submenu.menupopup.activateItem(menuitem);
} else {
menu.activateItem(menuitem);
}
await clickedPromise;
await hiddenPromise;
// Sometimes, the popup will open then instantly disappear. It seems to
// still be hiding after the previous appearance. If we wait a little bit,
// this doesn't happen.
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(r => setTimeout(r, 250));
}
}