Bug 1797764 - Part 1 : Support MV3 persistent events and background restart. r=darktrojan

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

--HG--
extra : rebase_source : 213cb0d9a0795bdd7ebc1452b81289fbd0e9bbcb
This commit is contained in:
John Bieling 2022-12-01 10:57:56 +00:00
Родитель ed9ae86a6f
Коммит 747d6cacff
60 изменённых файлов: 7223 добавлений и 2077 удалений

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

@ -30,7 +30,7 @@ const { ExtensionParent } = ChromeUtils.import(
"resource://gre/modules/ExtensionParent.jsm"
);
var { EventManager, ExtensionAPI, makeWidgetId } = ExtensionCommon;
var { EventManager, ExtensionAPIPersistent, makeWidgetId } = ExtensionCommon;
var { IconDetails, StartupCache } = ExtensionParent;
@ -102,7 +102,7 @@ function getIconData(icons, extension) {
return { style, legacy, realIcon };
}
var ToolbarButtonAPI = class extends ExtensionAPI {
var ToolbarButtonAPI = class extends ExtensionAPIPersistent {
constructor(extension, global) {
super(extension);
this.global = global;
@ -123,7 +123,10 @@ var ToolbarButtonAPI = class extends ExtensionAPI {
this.unpaint = this.unpaint.bind(this);
this.widgetId = makeWidgetId(extension.id);
this.id = `${this.widgetId}-${this.manifestName}-toolbarbutton`;
this.id =
this.manifestName == "action"
? `${this.widgetId}-browserAction-toolbarbutton`
: `${this.widgetId}-${this.manifestName}-toolbarbutton`;
this.eventQueue = [];
@ -442,7 +445,7 @@ var ToolbarButtonAPI = class extends ExtensionAPI {
if (!this.lastClickInfo) {
this.lastClickInfo = { button: 0, modifiers: [] };
}
this.emit("click", window);
this.emit("click", window, this.lastClickInfo);
}
}
delete this.lastClickInfo;
@ -701,6 +704,35 @@ var ToolbarButtonAPI = class extends ExtensionAPI {
return this.getContextData(this.getTargetFromDetails(details))[prop];
}
PERSISTENT_EVENTS = {
onClicked({ context, fire }) {
const { extension } = this;
const { tabManager, windowManager } = extension;
async function listener(_event, window, clickInfo) {
if (fire.wakeup) {
await fire.wakeup();
}
// TODO: We should double-check if the tab is already being closed by the time
// the background script got started and we converted the primed listener.
let win = windowManager.wrapWindow(window);
fire.sync(tabManager.convert(win.activeTab.nativeTab), clickInfo);
}
this.on("click", listener);
return {
unregister: () => {
this.off("click", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
};
/**
* WebExtension API.
*
@ -708,7 +740,6 @@ var ToolbarButtonAPI = class extends ExtensionAPI {
*/
getAPI(context) {
let { extension } = context;
let { tabManager, windowManager } = extension;
let action = this;
@ -716,21 +747,14 @@ var ToolbarButtonAPI = class extends ExtensionAPI {
[this.manifestName]: {
onClicked: new EventManager({
context,
name: `${this.manifestName}.onClicked`,
// browserAction was renamed to action in MV3, but its module name is
// still "browserAction" because that is the name used in ext-mail.json,
// independently from the manifest version.
module:
this.manifestName == "action" ? "browserAction" : this.manifestName,
event: "onClicked",
inputHandling: true,
register: fire => {
let listener = (event, window) => {
let win = windowManager.wrapWindow(window);
fire.sync(
tabManager.convert(win.activeTab.nativeTab),
this.lastClickInfo
);
};
action.on("click", listener);
return () => {
action.off("click", listener);
};
},
extensionApi: this,
}).api(),
async enable(tabId) {

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

@ -150,15 +150,78 @@ var accountsTracker = new (class extends EventEmitter {
}
})();
this.accounts = class extends ExtensionAPI {
close() {
this.accounts = class extends ExtensionAPIPersistent {
PERSISTENT_EVENTS = {
// For primed persistent events (deactivated background), the context is only
// available after fire.wakeup() has fulfilled (ensuring the convert() function
// has been called).
onCreated({ context, fire }) {
async function listener(_event, key, account) {
if (fire.wakeup) {
await fire.wakeup();
}
fire.sync(key, account);
}
accountsTracker.on("account-added", listener);
return {
unregister: () => {
accountsTracker.off("account-added", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onUpdated({ context, fire }) {
async function listener(_event, key, changedValues) {
if (fire.wakeup) {
await fire.wakeup();
}
fire.sync(key, changedValues);
}
accountsTracker.on("account-updated", listener);
return {
unregister: () => {
accountsTracker.off("account-updated", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onDeleted({ context, fire }) {
async function listener(_event, key) {
if (fire.wakeup) {
await fire.wakeup();
}
fire.sync(key);
}
accountsTracker.on("account-removed", listener);
return {
unregister: () => {
accountsTracker.off("account-removed", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
};
constructor(...args) {
super(...args);
accountsTracker.incrementListeners();
}
onShutdown() {
accountsTracker.decrementListeners();
}
getAPI(context) {
context.callOnClose(this);
accountsTracker.incrementListeners();
return {
accounts: {
async list(includeFolders) {
@ -200,45 +263,21 @@ this.accounts = class extends ExtensionAPI {
},
onCreated: new EventManager({
context,
name: "accounts.onCreated",
register: fire => {
let listener = (event, key, account) => {
fire.sync(key, account);
};
accountsTracker.on("account-added", listener);
return () => {
accountsTracker.off("account-added", listener);
};
},
module: "accounts",
event: "onCreated",
extensionApi: this,
}).api(),
onUpdated: new EventManager({
context,
name: "accounts.onUpdated",
register: fire => {
let listener = (event, key, changedValues) => {
fire.sync(key, changedValues);
};
accountsTracker.on("account-updated", listener);
return () => {
accountsTracker.off("account-updated", listener);
};
},
module: "accounts",
event: "onUpdated",
extensionApi: this,
}).api(),
onDeleted: new EventManager({
context,
name: "accounts.onDeleted",
register: fire => {
let listener = (event, key) => {
fire.sync(key);
};
accountsTracker.on("account-removed", listener);
return () => {
accountsTracker.off("account-removed", listener);
};
},
module: "accounts",
event: "onDeleted",
extensionApi: this,
}).api(),
},
};

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

@ -349,6 +349,9 @@ class ExtSearchBook extends AddrBookDirectory {
setLocalizedStringValue(aName, aValue) {}
async search(aQuery, aSearchString, aListener) {
try {
if (this.fire.wakeup) {
await this.fire.wakeup();
}
let { results, isCompleteResult } = await this.fire.async(
await addressBookCache.convert(
addressBookCache.addressBooks.get(this.UID)
@ -829,15 +832,258 @@ var addressBookCache = new (class extends EventEmitter {
}
})();
this.addressBook = class extends ExtensionAPI {
close() {
this.addressBook = class extends ExtensionAPIPersistent {
PERSISTENT_EVENTS = {
// For primed persistent events (deactivated background), the context is only
// available after fire.wakeup() has fulfilled (ensuring the convert() function
// has been called).
// addressBooks.*
onAddressBookCreated({ context, fire }) {
let listener = async (event, node) => {
if (fire.wakeup) {
await fire.wakeup();
}
fire.sync(await addressBookCache.convert(node));
};
addressBookCache.on("address-book-created", listener);
return {
unregister: () => {
addressBookCache.off("address-book-created", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onAddressBookUpdated({ context, fire }) {
let listener = async (event, node) => {
if (fire.wakeup) {
await fire.wakeup();
}
fire.sync(await addressBookCache.convert(node));
};
addressBookCache.on("address-book-updated", listener);
return {
unregister: () => {
addressBookCache.off("address-book-updated", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onAddressBookDeleted({ context, fire }) {
let listener = async (event, itemUID) => {
if (fire.wakeup) {
await fire.wakeup();
}
fire.sync(itemUID);
};
addressBookCache.on("address-book-deleted", listener);
return {
unregister: () => {
addressBookCache.off("address-book-deleted", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
// contacts.*
onContactCreated({ context, fire }) {
let listener = async (event, node) => {
if (fire.wakeup) {
await fire.wakeup();
}
fire.sync(await addressBookCache.convert(node));
};
addressBookCache.on("contact-created", listener);
return {
unregister: () => {
addressBookCache.off("contact-created", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onContactUpdated({ context, fire }) {
let listener = async (event, node, changes) => {
if (fire.wakeup) {
await fire.wakeup();
}
let filteredChanges = {};
// Find changes in flat properties stored in the vCard.
if (changes.hasOwnProperty("_vCard")) {
let oldVCardProperties = VCardProperties.fromVCard(
changes._vCard.oldValue
).toPropertyMap();
let newVCardProperties = VCardProperties.fromVCard(
changes._vCard.newValue
).toPropertyMap();
for (let [name, value] of oldVCardProperties) {
if (newVCardProperties.get(name) != value) {
filteredChanges[name] = {
oldValue: value,
newValue: newVCardProperties.get(name) ?? null,
};
}
}
for (let [name, value] of newVCardProperties) {
if (
!filteredChanges.hasOwnProperty(name) &&
oldVCardProperties.get(name) != value
) {
filteredChanges[name] = {
oldValue: oldVCardProperties.get(name) ?? null,
newValue: value,
};
}
}
}
for (let [name, value] of Object.entries(changes)) {
if (!filteredChanges.hasOwnProperty(name) && isCustomProperty(name)) {
filteredChanges[name] = value;
}
}
fire.sync(await addressBookCache.convert(node), filteredChanges);
};
addressBookCache.on("contact-updated", listener);
return {
unregister: () => {
addressBookCache.off("contact-updated", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onContactDeleted({ context, fire }) {
let listener = async (event, parentUID, itemUID) => {
if (fire.wakeup) {
await fire.wakeup();
}
fire.sync(parentUID, itemUID);
};
addressBookCache.on("contact-deleted", listener);
return {
unregister: () => {
addressBookCache.off("contact-deleted", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
// mailingLists.*
onMailingListCreated({ context, fire }) {
let listener = async (event, node) => {
fire.sync(await addressBookCache.convert(node));
};
addressBookCache.on("mailing-list-created", listener);
return {
unregister: () => {
addressBookCache.off("mailing-list-created", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onMailingListUpdated({ context, fire }) {
let listener = async (event, node) => {
if (fire.wakeup) {
await fire.wakeup();
}
fire.sync(await addressBookCache.convert(node));
};
addressBookCache.on("mailing-list-updated", listener);
return {
unregister: () => {
addressBookCache.off("mailing-list-updated", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onMailingListDeleted({ context, fire }) {
let listener = async (event, parentUID, itemUID) => {
if (fire.wakeup) {
await fire.wakeup();
}
fire.sync(parentUID, itemUID);
};
addressBookCache.on("mailing-list-deleted", listener);
return {
unregister: () => {
addressBookCache.off("mailing-list-deleted", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onMemberAdded({ context, fire }) {
let listener = async (event, node) => {
if (fire.wakeup) {
await fire.wakeup();
}
fire.sync(await addressBookCache.convert(node));
};
addressBookCache.on("mailing-list-member-added", listener);
return {
unregister: () => {
addressBookCache.off("mailing-list-member-added", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onMemberRemoved({ context, fire }) {
let listener = async (event, parentUID, itemUID) => {
if (fire.wakeup) {
await fire.wakeup();
}
fire.sync(parentUID, itemUID);
};
addressBookCache.on("mailing-list-member-removed", listener);
return {
unregister: () => {
addressBookCache.off("mailing-list-member-removed", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
};
constructor(...args) {
super(...args);
addressBookCache.incrementListeners();
}
onShutdown() {
addressBookCache.decrementListeners();
}
getAPI(context) {
context.callOnClose(this);
addressBookCache.incrementListeners();
return {
addressBooks: {
async openUI() {
@ -894,47 +1140,24 @@ this.addressBook = class extends ExtensionAPI {
await deletePromise;
},
// The module name is addressBook as defined in ext-mail.json.
onCreated: new EventManager({
context,
name: "addressBooks.onCreated",
register: fire => {
let listener = async (event, node) => {
fire.sync(await addressBookCache.convert(node));
};
addressBookCache.on("address-book-created", listener);
return () => {
addressBookCache.off("address-book-created", listener);
};
},
module: "addressBook",
event: "onAddressBookCreated",
extensionApi: this,
}).api(),
onUpdated: new EventManager({
context,
name: "addressBooks.onUpdated",
register: fire => {
let listener = async (event, node) => {
fire.sync(await addressBookCache.convert(node));
};
addressBookCache.on("address-book-updated", listener);
return () => {
addressBookCache.off("address-book-updated", listener);
};
},
module: "addressBook",
event: "onAddressBookUpdated",
extensionApi: this,
}).api(),
onDeleted: new EventManager({
context,
name: "addressBooks.onDeleted",
register: fire => {
let listener = (event, itemUID) => {
fire.sync(itemUID);
};
addressBookCache.on("address-book-deleted", listener);
return () => {
addressBookCache.off("address-book-deleted", listener);
};
},
module: "addressBook",
event: "onAddressBookDeleted",
extensionApi: this,
}).api(),
provider: {
@ -1208,84 +1431,24 @@ this.addressBook = class extends ExtensionAPI {
parentNode.item.deleteCards([node.item]);
},
// The module name is addressBook as defined in ext-mail.json.
onCreated: new EventManager({
context,
name: "contacts.onCreated",
register: fire => {
let listener = async (event, node) => {
fire.sync(await addressBookCache.convert(node));
};
addressBookCache.on("contact-created", listener);
return () => {
addressBookCache.off("contact-created", listener);
};
},
module: "addressBook",
event: "onContactCreated",
extensionApi: this,
}).api(),
onUpdated: new EventManager({
context,
name: "contacts.onUpdated",
register: fire => {
let listener = async (event, node, changes) => {
let filteredChanges = {};
// Find changes in flat properties stored in the vCard.
if (changes.hasOwnProperty("_vCard")) {
let oldVCardProperties = VCardProperties.fromVCard(
changes._vCard.oldValue
).toPropertyMap();
let newVCardProperties = VCardProperties.fromVCard(
changes._vCard.newValue
).toPropertyMap();
for (let [name, value] of oldVCardProperties) {
if (newVCardProperties.get(name) != value) {
filteredChanges[name] = {
oldValue: value,
newValue: newVCardProperties.get(name) ?? null,
};
}
}
for (let [name, value] of newVCardProperties) {
if (
!filteredChanges.hasOwnProperty(name) &&
oldVCardProperties.get(name) != value
) {
filteredChanges[name] = {
oldValue: oldVCardProperties.get(name) ?? null,
newValue: value,
};
}
}
}
for (let [name, value] of Object.entries(changes)) {
if (
!filteredChanges.hasOwnProperty(name) &&
isCustomProperty(name)
) {
filteredChanges[name] = value;
}
}
fire.sync(await addressBookCache.convert(node), filteredChanges);
};
addressBookCache.on("contact-updated", listener);
return () => {
addressBookCache.off("contact-updated", listener);
};
},
module: "addressBook",
event: "onContactUpdated",
extensionApi: this,
}).api(),
onDeleted: new EventManager({
context,
name: "contacts.onDeleted",
register: fire => {
let listener = (event, parentUID, itemUID) => {
fire.sync(parentUID, itemUID);
};
addressBookCache.on("contact-deleted", listener);
return () => {
addressBookCache.off("contact-deleted", listener);
};
},
module: "addressBook",
event: "onContactDeleted",
extensionApi: this,
}).api(),
},
mailingLists: {
@ -1375,75 +1538,36 @@ this.addressBook = class extends ExtensionAPI {
node.item.deleteCards([contactNode.item]);
},
// The module name is addressBook as defined in ext-mail.json.
onCreated: new EventManager({
context,
name: "mailingLists.onCreated",
register: fire => {
let listener = async (event, node) => {
fire.sync(await addressBookCache.convert(node));
};
addressBookCache.on("mailing-list-created", listener);
return () => {
addressBookCache.off("mailing-list-created", listener);
};
},
module: "addressBook",
event: "onMailingListCreated",
extensionApi: this,
}).api(),
onUpdated: new EventManager({
context,
name: "mailingLists.onUpdated",
register: fire => {
let listener = async (event, node) => {
fire.sync(await addressBookCache.convert(node));
};
addressBookCache.on("mailing-list-updated", listener);
return () => {
addressBookCache.off("mailing-list-updated", listener);
};
},
module: "addressBook",
event: "onMailingListUpdated",
extensionApi: this,
}).api(),
onDeleted: new EventManager({
context,
name: "mailingLists.onDeleted",
register: fire => {
let listener = (event, parentUID, itemUID) => {
fire.sync(parentUID, itemUID);
};
addressBookCache.on("mailing-list-deleted", listener);
return () => {
addressBookCache.off("mailing-list-deleted", listener);
};
},
module: "addressBook",
event: "onMailingListDeleted",
extensionApi: this,
}).api(),
onMemberAdded: new EventManager({
context,
name: "mailingLists.onMemberAdded",
register: fire => {
let listener = async (event, node) => {
fire.sync(await addressBookCache.convert(node));
};
addressBookCache.on("mailing-list-member-added", listener);
return () => {
addressBookCache.off("mailing-list-member-added", listener);
};
},
module: "addressBook",
event: "onMemberAdded",
extensionApi: this,
}).api(),
onMemberRemoved: new EventManager({
context,
name: "mailingLists.onMemberRemoved",
register: fire => {
let listener = (event, parentUID, itemUID) => {
fire.sync(parentUID, itemUID);
};
addressBookCache.on("mailing-list-member-removed", listener);
return () => {
addressBookCache.off("mailing-list-member-removed", listener);
};
},
module: "addressBook",
event: "onMemberRemoved",
extensionApi: this,
}).api(),
},
};

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

@ -452,11 +452,17 @@ class CloudFileAccount {
}
}
let result;
if (uploadId != -1) {
result = await this.extension.emit("uploadAbort", this, uploadId, window);
if (uploadId == -1) {
console.error(`No upload in progress for file ${file.path}`);
return false;
}
let result = await this.extension.emit(
"uploadAbort",
this,
uploadId,
window
);
if (result && result.length > 0) {
return true;
}
@ -519,7 +525,7 @@ function convertCloudFileAccount(nativeAccount) {
};
}
this.cloudFile = class extends ExtensionAPI {
this.cloudFile = class extends ExtensionAPIPersistent {
get providerType() {
return `ext-${this.extension.id}`;
}
@ -555,128 +561,201 @@ this.cloudFile = class extends ExtensionAPI {
cloudFileAccounts.unregisterProvider(this.providerType);
}
PERSISTENT_EVENTS = {
// For primed persistent events (deactivated background), the context is only
// available after fire.wakeup() has fulfilled (ensuring the convert() function
// has been called).
onFileUpload({ context, fire }) {
const { extension } = this;
const { tabManager } = extension;
async function listener(
_event,
account,
{ id, name, data },
tab,
relatedFileInfo
) {
if (fire.wakeup) {
await fire.wakeup();
}
tab = tab ? tabManager.convert(tab) : null;
account = convertCloudFileAccount(account);
return fire.async(account, { id, name, data }, tab, relatedFileInfo);
}
extension.on("uploadFile", listener);
return {
unregister: () => {
extension.off("uploadFile", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onFileUploadAbort({ context, fire }) {
const { extension } = this;
const { tabManager } = extension;
async function listener(_event, account, id, tab) {
if (fire.wakeup) {
await fire.wakeup();
}
tab = tab ? tabManager.convert(tab) : null;
account = convertCloudFileAccount(account);
return fire.async(account, id, tab);
}
extension.on("uploadAbort", listener);
return {
unregister: () => {
extension.off("uploadAbort", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onFileRename({ context, fire }) {
const { extension } = this;
const { tabManager } = extension;
async function listener(_event, account, id, newName, tab) {
if (fire.wakeup) {
await fire.wakeup();
}
tab = tab ? tabManager.convert(tab) : null;
account = convertCloudFileAccount(account);
return fire.async(account, id, newName, tab);
}
extension.on("renameFile", listener);
return {
unregister: () => {
extension.off("renameFile", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onFileDeleted({ context, fire }) {
const { extension } = this;
const { tabManager } = extension;
async function listener(_event, account, id, tab) {
if (fire.wakeup) {
await fire.wakeup();
}
tab = tab ? tabManager.convert(tab) : null;
account = convertCloudFileAccount(account);
return fire.async(account, id, tab);
}
extension.on("deleteFile", listener);
return {
unregister: () => {
extension.off("deleteFile", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onAccountAdded({ context, fire }) {
const self = this;
async function listener(_event, nativeAccount) {
if (nativeAccount.type != self.providerType) {
return null;
}
if (fire.wakeup) {
await fire.wakeup();
}
return fire.async(convertCloudFileAccount(nativeAccount));
}
cloudFileAccounts.on("accountAdded", listener);
return {
unregister: () => {
cloudFileAccounts.off("accountAdded", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onAccountDeleted({ context, fire }) {
const self = this;
async function listener(_event, key, type) {
if (self.providerType != type) {
return null;
}
if (fire.wakeup) {
await fire.wakeup();
}
return fire.async(key);
}
cloudFileAccounts.on("accountDeleted", listener);
return {
unregister: () => {
cloudFileAccounts.off("accountDeleted", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
};
getAPI(context) {
let self = this;
let { extension } = context;
let { tabManager } = extension;
return {
cloudFile: {
onFileUpload: new EventManager({
context,
name: "cloudFile.onFileUpload",
register: fire => {
let listener = (
event,
account,
{ id, name, data },
tab,
relatedFileInfo
) => {
tab = tab ? tabManager.convert(tab) : null;
account = convertCloudFileAccount(account);
return fire.async(
account,
{ id, name, data },
tab,
relatedFileInfo
);
};
context.extension.on("uploadFile", listener);
return () => {
context.extension.off("uploadFile", listener);
};
},
module: "cloudFile",
event: "onFileUpload",
extensionApi: this,
}).api(),
onFileUploadAbort: new EventManager({
context,
name: "cloudFile.onFileUploadAbort",
register: fire => {
let listener = (event, account, id, tab) => {
tab = tab ? tabManager.convert(tab) : null;
account = convertCloudFileAccount(account);
return fire.async(account, id, tab);
};
context.extension.on("uploadAbort", listener);
return () => {
context.extension.off("uploadAbort", listener);
};
},
module: "cloudFile",
event: "onFileUploadAbort",
extensionApi: this,
}).api(),
onFileRename: new EventManager({
context,
name: "cloudFile.onFileRename",
register: fire => {
let listener = (event, account, id, newName, tab) => {
tab = tab ? tabManager.convert(tab) : null;
account = convertCloudFileAccount(account);
return fire.async(account, id, newName, tab);
};
context.extension.on("renameFile", listener);
return () => {
context.extension.off("renameFile", listener);
};
},
module: "cloudFile",
event: "onFileRename",
extensionApi: this,
}).api(),
onFileDeleted: new EventManager({
context,
name: "cloudFile.onFileDeleted",
register: fire => {
let listener = (event, account, id, tab) => {
tab = tab ? tabManager.convert(tab) : null;
account = convertCloudFileAccount(account);
return fire.async(account, id, tab);
};
context.extension.on("deleteFile", listener);
return () => {
context.extension.off("deleteFile", listener);
};
},
module: "cloudFile",
event: "onFileDeleted",
extensionApi: this,
}).api(),
onAccountAdded: new EventManager({
context,
name: "cloudFile.onAccountAdded",
register: fire => {
let listener = (event, nativeAccount) => {
if (nativeAccount.type != this.providerType) {
return null;
}
return fire.async(convertCloudFileAccount(nativeAccount));
};
cloudFileAccounts.on("accountAdded", listener);
return () => {
cloudFileAccounts.off("accountAdded", listener);
};
},
module: "cloudFile",
event: "onAccountAdded",
extensionApi: this,
}).api(),
onAccountDeleted: new EventManager({
context,
name: "cloudFile.onAccountDeleted",
register: fire => {
let listener = (event, key, type) => {
if (this.providerType != type) {
return null;
}
return fire.async(key);
};
cloudFileAccounts.on("accountDeleted", listener);
return () => {
cloudFileAccounts.off("accountDeleted", listener);
};
},
module: "cloudFile",
event: "onAccountDeleted",
extensionApi: this,
}).api(),
async getAccount(accountId) {

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

@ -12,7 +12,35 @@ ChromeUtils.defineModuleGetter(
"resource:///modules/MailExtensionShortcuts.jsm"
);
this.commands = class extends ExtensionAPI {
this.commands = class extends ExtensionAPIPersistent {
PERSISTENT_EVENTS = {
// For primed persistent events (deactivated background), the context is only
// available after fire.wakeup() has fulfilled (ensuring the convert() function
// has been called).
onCommand({ context, fire }) {
const { extension } = this;
const { tabManager } = extension;
async function listener(eventName, commandName) {
if (fire.wakeup) {
await fire.wakeup();
}
let tab = tabManager.convert(tabTracker.activeTab);
fire.async(commandName, tab);
}
this.on("command", listener);
return {
unregister: () => {
this.off("command", listener);
},
convert(_fire, _context) {
fire = _fire;
context = _context;
},
};
},
};
static onUninstall(extensionId) {
return MailExtensionShortcuts.removeCommandsFromStorage(extensionId);
}
@ -39,20 +67,10 @@ this.commands = class extends ExtensionAPI {
reset: name => this.extension.shortcuts.resetCommand(name),
onCommand: new EventManager({
context,
name: "commands.onCommand",
module: "commands",
event: "onCommand",
inputHandling: true,
register: fire => {
let listener = (eventName, commandName) => {
let tab = context.extension.tabManager.convert(
tabTracker.activeTab
);
fire.async(commandName, tab);
};
this.on("command", listener);
return () => {
this.off("command", listener);
};
},
extensionApi: this,
}).api(),
},
};

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

@ -1145,7 +1145,256 @@ windowTracker.addCloseListener(
var composeWindowTracker = new Set();
windowTracker.addCloseListener(window => composeWindowTracker.delete(window));
this.compose = class extends ExtensionAPI {
this.compose = class extends ExtensionAPIPersistent {
PERSISTENT_EVENTS = {
// For primed persistent events (deactivated background), the context is only
// available after fire.wakeup() has fulfilled (ensuring the convert() function
// has been called).
onBeforeSend({ context, fire }) {
const { extension } = this;
const { tabManager, windowManager } = extension;
let listener = {
async handler(window, details) {
if (fire.wakeup) {
await fire.wakeup();
}
let win = windowManager.wrapWindow(window);
return fire.async(
tabManager.convert(win.activeTab.nativeTab),
details
);
},
extension,
};
beforeSendEventTracker.addListener(listener);
return {
unregister: () => {
beforeSendEventTracker.removeListener(listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onAfterSend({ context, fire }) {
const { extension } = this;
const { tabManager, windowManager } = extension;
let listener = {
async onSuccess(window, mode, messages, headerMessageId) {
let win = windowManager.wrapWindow(window);
let tab = tabManager.convert(win.activeTab.nativeTab);
if (fire.wakeup) {
await fire.wakeup();
}
let sendInfo = { mode, messages };
if (mode == "sendNow") {
sendInfo.headerMessageId = headerMessageId;
}
return fire.async(tab, sendInfo);
},
async onFailure(window, mode, exception) {
let win = windowManager.wrapWindow(window);
let tab = tabManager.convert(win.activeTab.nativeTab);
if (fire.wakeup) {
await fire.wakeup();
}
return fire.async(tab, {
mode,
messages: [],
error: exception.message,
});
},
modes: ["sendNow", "sendLater"],
extension,
};
afterSaveSendEventTracker.addListener(listener);
return {
unregister: () => {
afterSaveSendEventTracker.removeListener(listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onAfterSave({ context, fire }) {
const { extension } = this;
const { tabManager, windowManager } = extension;
let listener = {
async onSuccess(window, mode, messages, headerMessageId) {
if (fire.wakeup) {
await fire.wakeup();
}
let win = windowManager.wrapWindow(window);
let saveInfo = { mode, messages };
return fire.async(
tabManager.convert(win.activeTab.nativeTab),
saveInfo
);
},
async onFailure(window, mode, exception) {
if (fire.wakeup) {
await fire.wakeup();
}
let win = windowManager.wrapWindow(window);
return fire.async(tabManager.convert(win.activeTab.nativeTab), {
mode,
messages: [],
error: exception.message,
});
},
modes: ["draft", "template"],
extension,
};
afterSaveSendEventTracker.addListener(listener);
return {
unregister: () => {
afterSaveSendEventTracker.removeListener(listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onAttachmentAdded({ context, fire }) {
const { extension } = this;
const { tabManager } = extension;
async function listener(event) {
if (fire.wakeup) {
await fire.wakeup();
}
for (let attachment of event.detail) {
attachment = composeAttachmentTracker.convert(
attachment,
event.target.ownerGlobal
);
fire.async(tabManager.convert(event.target.ownerGlobal), attachment);
}
}
windowTracker.addListener("attachments-added", listener);
return {
unregister: () => {
windowTracker.removeListener("attachments-added", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onAttachmentRemoved({ context, fire }) {
const { extension } = this;
const { tabManager } = extension;
async function listener(event) {
if (fire.wakeup) {
await fire.wakeup();
}
for (let attachment of event.detail) {
let attachmentId = composeAttachmentTracker.getId(
attachment,
event.target.ownerGlobal
);
fire.async(
tabManager.convert(event.target.ownerGlobal),
attachmentId
);
composeAttachmentTracker.forgetAttachment(attachment);
}
}
windowTracker.addListener("attachments-removed", listener);
return {
unregister: () => {
windowTracker.removeListener("attachments-removed", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onIdentityChanged({ context, fire }) {
const { extension } = this;
const { tabManager } = extension;
async function listener(event) {
if (fire.wakeup) {
await fire.wakeup();
}
fire.async(
tabManager.convert(event.target.ownerGlobal),
event.target.getCurrentIdentityKey()
);
}
windowTracker.addListener("compose-from-changed", listener);
return {
unregister: () => {
windowTracker.removeListener("compose-from-changed", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onComposeStateChanged({ context, fire }) {
const { extension } = this;
const { tabManager } = extension;
async function listener(event) {
if (fire.wakeup) {
await fire.wakeup();
}
fire.async(
tabManager.convert(event.target.ownerGlobal),
composeStates.convert(event.detail)
);
}
windowTracker.addListener("compose-state-changed", listener);
return {
unregister: () => {
windowTracker.removeListener("compose-state-changed", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onActiveDictionariesChanged({ context, fire }) {
const { extension } = this;
const { tabManager } = extension;
async function listener(event) {
if (fire.wakeup) {
await fire.wakeup();
}
let activeDictionaries = event.detail.split(",");
fire.async(
tabManager.convert(event.target.ownerGlobal),
Cc["@mozilla.org/spellchecker/engine;1"]
.getService(Ci.mozISpellCheckingEngine)
.getDictionaryList()
.reduce((list, dict) => {
list[dict] = activeDictionaries.includes(dict);
return list;
}, {})
);
}
windowTracker.addListener("active-dictionaries-changed", listener);
return {
unregister: () => {
windowTracker.removeListener("active-dictionaries-changed", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
};
getAPI(context) {
function getComposeTab(tabId) {
let tab = tabManager.get(tabId);
@ -1160,206 +1409,60 @@ this.compose = class extends ExtensionAPI {
}
let { extension } = context;
let { tabManager, windowManager } = extension;
let { tabManager } = extension;
return {
compose: {
onBeforeSend: new EventManager({
context,
name: "compose.onBeforeSend",
module: "compose",
event: "onBeforeSend",
inputHandling: true,
register: fire => {
let listener = {
handler(window, details) {
let win = windowManager.wrapWindow(window);
return fire.async(
tabManager.convert(win.activeTab.nativeTab),
details
);
},
extension,
};
beforeSendEventTracker.addListener(listener);
return () => {
beforeSendEventTracker.removeListener(listener);
};
},
extensionApi: this,
}).api(),
onAfterSend: new EventManager({
context,
name: "compose.onAfterSend",
module: "compose",
event: "onAfterSend",
inputHandling: true,
register: fire => {
let listener = {
onSuccess(window, mode, messages, headerMessageId) {
let win = windowManager.wrapWindow(window);
let sendInfo = { mode, messages };
if (mode == "sendNow") {
sendInfo.headerMessageId = headerMessageId;
}
return fire.async(
tabManager.convert(win.activeTab.nativeTab),
sendInfo
);
},
onFailure(window, mode, exception) {
let win = windowManager.wrapWindow(window);
return fire.async(tabManager.convert(win.activeTab.nativeTab), {
mode,
messages: [],
error: exception.message,
});
},
modes: ["sendNow", "sendLater"],
extension,
};
afterSaveSendEventTracker.addListener(listener);
return () => {
afterSaveSendEventTracker.removeListener(listener);
};
},
extensionApi: this,
}).api(),
onAfterSave: new EventManager({
context,
name: "compose.onAfterSave",
module: "compose",
event: "onAfterSave",
inputHandling: true,
register: fire => {
let listener = {
onSuccess(window, mode, messages, headerMessageId) {
let win = windowManager.wrapWindow(window);
let saveInfo = { mode, messages };
return fire.async(
tabManager.convert(win.activeTab.nativeTab),
saveInfo
);
},
onFailure(window, mode, exception) {
let win = windowManager.wrapWindow(window);
return fire.async(tabManager.convert(win.activeTab.nativeTab), {
mode,
messages: [],
error: exception.message,
});
},
modes: ["draft", "template"],
extension,
};
afterSaveSendEventTracker.addListener(listener);
return () => {
afterSaveSendEventTracker.removeListener(listener);
};
},
extensionApi: this,
}).api(),
onAttachmentAdded: new ExtensionCommon.EventManager({
context,
name: "compose.onAttachmentAdded",
register(fire) {
async function callback(event) {
for (let attachment of event.detail) {
attachment = composeAttachmentTracker.convert(
attachment,
event.target.ownerGlobal
);
fire.async(
tabManager.convert(event.target.ownerGlobal),
attachment
);
}
}
windowTracker.addListener("attachments-added", callback);
return function() {
windowTracker.removeListener("attachments-added", callback);
};
},
module: "compose",
event: "onAttachmentAdded",
extensionApi: this,
}).api(),
onAttachmentRemoved: new ExtensionCommon.EventManager({
context,
name: "compose.onAttachmentRemoved",
register(fire) {
function callback(event) {
for (let attachment of event.detail) {
let attachmentId = composeAttachmentTracker.getId(
attachment,
event.target.ownerGlobal
);
fire.async(
tabManager.convert(event.target.ownerGlobal),
attachmentId
);
composeAttachmentTracker.forgetAttachment(attachment);
}
}
windowTracker.addListener("attachments-removed", callback);
return function() {
windowTracker.removeListener("attachments-removed", callback);
};
},
module: "compose",
event: "onAttachmentRemoved",
extensionApi: this,
}).api(),
onIdentityChanged: new ExtensionCommon.EventManager({
context,
name: "compose.onIdentityChanged",
register(fire) {
function callback(event) {
fire.async(
tabManager.convert(event.target.ownerGlobal),
event.target.getCurrentIdentityKey()
);
}
windowTracker.addListener("compose-from-changed", callback);
return function() {
windowTracker.removeListener("compose-from-changed", callback);
};
},
module: "compose",
event: "onIdentityChanged",
extensionApi: this,
}).api(),
onComposeStateChanged: new ExtensionCommon.EventManager({
context,
name: "compose.onComposeStateChanged",
register(fire) {
function callback(event) {
fire.async(
tabManager.convert(event.target.ownerGlobal),
composeStates.convert(event.detail)
);
}
windowTracker.addListener("compose-state-changed", callback);
return function() {
windowTracker.removeListener("compose-state-changed", callback);
};
},
module: "compose",
event: "onComposeStateChanged",
extensionApi: this,
}).api(),
onActiveDictionariesChanged: new ExtensionCommon.EventManager({
context,
name: "compose.onActiveDictionariesChanged",
register(fire) {
function callback(event) {
let activeDictionaries = event.detail.split(",");
fire.async(
tabManager.convert(event.target.ownerGlobal),
Cc["@mozilla.org/spellchecker/engine;1"]
.getService(Ci.mozISpellCheckingEngine)
.getDictionaryList()
.reduce((list, dict) => {
list[dict] = activeDictionaries.includes(dict);
return list;
}, {})
);
}
windowTracker.addListener("active-dictionaries-changed", callback);
return function() {
windowTracker.removeListener(
"active-dictionaries-changed",
callback
);
};
},
module: "compose",
event: "onActiveDictionariesChanged",
extensionApi: this,
}).api(),
async beginNew(messageId, details) {
let type = Ci.nsIMsgCompType.New;

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

@ -133,7 +133,13 @@ var folderTracker = new (class extends EventEmitter {
this.emit("folder-created", convertFolder(childFolder));
}
folderDeleted(oldFolder) {
this.emit("folder-deleted", convertFolder(oldFolder));
// Deleting an account, will trigger delete notifications for its folders,
// but the account lookup fails, so skip them.
let server = oldFolder.server;
let account = MailServices.accounts.FindAccountForServer(server);
if (account) {
this.emit("folder-deleted", convertFolder(oldFolder, account.key));
}
}
folderMoveCopyCompleted(move, srcFolder, targetFolder) {
// targetFolder is not the copied/moved folder, but its parent. Find the
@ -321,91 +327,160 @@ function waitForOperation(flags, uri) {
});
}
this.folders = class extends ExtensionAPI {
this.folders = class extends ExtensionAPIPersistent {
PERSISTENT_EVENTS = {
// For primed persistent events (deactivated background), the context is only
// available after fire.wakeup() has fulfilled (ensuring the convert() function
// has been called).
onCreated({ context, fire }) {
async function listener(event, createdMailFolder) {
if (fire.wakeup) {
await fire.wakeup();
}
fire.async(createdMailFolder);
}
folderTracker.on("folder-created", listener);
return {
unregister: () => {
folderTracker.off("folder-created", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onRenamed({ context, fire }) {
async function listener(event, originalMailFolder, renamedMailFolder) {
if (fire.wakeup) {
await fire.wakeup();
}
fire.async(originalMailFolder, renamedMailFolder);
}
folderTracker.on("folder-renamed", listener);
return {
unregister: () => {
folderTracker.off("folder-renamed", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onMoved({ context, fire }) {
async function listener(event, srcMailFolder, dstMailFolder) {
if (fire.wakeup) {
await fire.wakeup();
}
fire.async(srcMailFolder, dstMailFolder);
}
folderTracker.on("folder-moved", listener);
return {
unregister: () => {
folderTracker.off("folder-moved", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onCopied({ context, fire }) {
async function listener(event, srcMailFolder, dstMailFolder) {
if (fire.wakeup) {
await fire.wakeup();
}
fire.async(srcMailFolder, dstMailFolder);
}
folderTracker.on("folder-copied", listener);
return {
unregister: () => {
folderTracker.off("folder-copied", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onDeleted({ context, fire }) {
async function listener(event, deletedMailFolder) {
if (fire.wakeup) {
await fire.wakeup();
}
fire.async(deletedMailFolder);
}
folderTracker.on("folder-deleted", listener);
return {
unregister: () => {
folderTracker.off("folder-deleted", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onFolderInfoChanged({ context, fire }) {
async function listener(event, changedMailFolder, mailFolderInfo) {
if (fire.wakeup) {
await fire.wakeup();
}
fire.async(changedMailFolder, mailFolderInfo);
}
folderTracker.on("folder-info-changed", listener);
return {
unregister: () => {
folderTracker.off("folder-info-changed", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
};
getAPI(context) {
return {
folders: {
onCreated: new EventManager({
context,
name: "folders.onCreated",
register: fire => {
let listener = async (event, createdMailFolder) => {
fire.async(createdMailFolder);
};
folderTracker.on("folder-created", listener);
return () => {
folderTracker.off("folder-created", listener);
};
},
module: "folders",
event: "onCreated",
extensionApi: this,
}).api(),
onRenamed: new EventManager({
context,
name: "folders.onRenamed",
register: fire => {
let listener = async (
event,
originalMailFolder,
renamedMailFolder
) => {
fire.async(originalMailFolder, renamedMailFolder);
};
folderTracker.on("folder-renamed", listener);
return () => {
folderTracker.off("folder-renamed", listener);
};
},
module: "folders",
event: "onRenamed",
extensionApi: this,
}).api(),
onMoved: new EventManager({
context,
name: "folders.onMoved",
register: fire => {
let listener = async (event, srcMailFolder, dstMailFolder) => {
fire.async(srcMailFolder, dstMailFolder);
};
folderTracker.on("folder-moved", listener);
return () => {
folderTracker.off("folder-moved", listener);
};
},
module: "folders",
event: "onMoved",
extensionApi: this,
}).api(),
onCopied: new EventManager({
context,
name: "folders.onCopied",
register: fire => {
let listener = async (event, srcMailFolder, dstMailFolder) => {
fire.async(srcMailFolder, dstMailFolder);
};
folderTracker.on("folder-copied", listener);
return () => {
folderTracker.off("folder-copied", listener);
};
},
module: "folders",
event: "onCopied",
extensionApi: this,
}).api(),
onDeleted: new EventManager({
context,
name: "folders.onDeleted",
register: fire => {
let listener = async (event, deletedMailFolder) => {
fire.async(deletedMailFolder);
};
folderTracker.on("folder-deleted", listener);
return () => {
folderTracker.off("folder-deleted", listener);
};
},
module: "folders",
event: "onDeleted",
extensionApi: this,
}).api(),
onFolderInfoChanged: new EventManager({
context,
name: "folders.onFolderInfoChanged",
register: fire => {
let listener = async (event, changedMailFolder, mailFolderInfo) => {
fire.async(changedMailFolder, mailFolderInfo);
};
folderTracker.on("folder-info-changed", listener);
return () => {
folderTracker.off("folder-info-changed", listener);
};
},
module: "folders",
event: "onFolderInfoChanged",
extensionApi: this,
}).api(),
async create(parent, childName) {
// The schema file allows parent to be either a MailFolder or a

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

@ -190,15 +190,78 @@ var identitiesTracker = new (class extends EventEmitter {
}
})();
this.identities = class extends ExtensionAPI {
close() {
this.identities = class extends ExtensionAPIPersistent {
PERSISTENT_EVENTS = {
// For primed persistent events (deactivated background), the context is only
// available after fire.wakeup() has fulfilled (ensuring the convert() function
// has been called).
onCreated({ context, fire }) {
async function listener(event, key, identity) {
if (fire.wakeup) {
await fire.wakeup();
}
fire.sync(key, identity);
}
identitiesTracker.on("account-identity-added", listener);
return {
unregister: () => {
identitiesTracker.off("account-identity-added", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onUpdated({ context, fire }) {
async function listener(event, key, changedValues) {
if (fire.wakeup) {
await fire.wakeup();
}
fire.sync(key, changedValues);
}
identitiesTracker.on("account-identity-updated", listener);
return {
unregister: () => {
identitiesTracker.off("account-identity-updated", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onDeleted({ context, fire }) {
async function listener(event, key) {
if (fire.wakeup) {
await fire.wakeup();
}
fire.sync(key);
}
identitiesTracker.on("account-identity-removed", listener);
return {
unregister: () => {
identitiesTracker.off("account-identity-removed", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
};
constructor(...args) {
super(...args);
identitiesTracker.incrementListeners();
}
onShutdown() {
identitiesTracker.decrementListeners();
}
getAPI(context) {
context.callOnClose(this);
identitiesTracker.incrementListeners();
return {
identities: {
async list(accountId) {
@ -278,45 +341,21 @@ this.identities = class extends ExtensionAPI {
},
onCreated: new EventManager({
context,
name: "identities.onCreated",
register: fire => {
let listener = (event, key, identity) => {
fire.sync(key, identity);
};
identitiesTracker.on("account-identity-added", listener);
return () => {
identitiesTracker.off("account-identity-added", listener);
};
},
module: "identities",
event: "onCreated",
extensionApi: this,
}).api(),
onUpdated: new EventManager({
context,
name: "identities.onUpdated",
register: fire => {
let listener = (event, key, changedValues) => {
fire.sync(key, changedValues);
};
identitiesTracker.on("account-identity-updated", listener);
return () => {
identitiesTracker.off("account-identity-updated", listener);
};
},
module: "identities",
event: "onUpdated",
extensionApi: this,
}).api(),
onDeleted: new EventManager({
context,
name: "identities.onDeleted",
register: fire => {
let listener = (event, key) => {
fire.sync(key);
};
identitiesTracker.on("account-identity-removed", listener);
return () => {
identitiesTracker.off("account-identity-removed", listener);
};
},
module: "identities",
event: "onDeleted",
extensionApi: this,
}).api(),
},
};

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

@ -150,7 +150,59 @@ var uiListener = new (class extends EventEmitter {
}
})();
this.mailTabs = class extends ExtensionAPI {
this.mailTabs = class extends ExtensionAPIPersistent {
PERSISTENT_EVENTS = {
// For primed persistent events (deactivated background), the context is only
// available after fire.wakeup() has fulfilled (ensuring the convert() function
// has been called).
onDisplayedFolderChanged({ context, fire }) {
const { extension } = this;
const { tabManager } = extension;
async function listener(event, tab, folder) {
if (fire.wakeup) {
await fire.wakeup();
}
fire.sync(tabManager.convert(tab), convertFolder(folder));
}
uiListener.on("folder-changed", listener);
uiListener.incrementListeners();
return {
unregister: () => {
uiListener.off("folder-changed", listener);
uiListener.decrementListeners();
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onSelectedMessagesChanged({ context, fire }) {
const { extension } = this;
const { tabManager } = extension;
async function listener(event, tab, messages) {
if (fire.wakeup) {
await fire.wakeup();
}
let page = await messageListTracker.startList(messages, extension);
fire.sync(tabManager.convert(tab), page);
}
uiListener.on("messages-changed", listener);
uiListener.incrementListeners();
return {
unregister: () => {
uiListener.off("messages-changed", listener);
uiListener.decrementListeners();
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
};
getAPI(context) {
let { extension } = context;
let { tabManager } = extension;
@ -436,40 +488,16 @@ this.mailTabs = class extends ExtensionAPI {
onDisplayedFolderChanged: new EventManager({
context,
name: "mailTabs.onDisplayedFolderChanged",
register: fire => {
let listener = (event, tab, folder) => {
fire.sync(tabManager.convert(tab), convertFolder(folder));
};
uiListener.on("folder-changed", listener);
uiListener.incrementListeners();
return () => {
uiListener.off("folder-changed", listener);
uiListener.decrementListeners();
};
},
module: "mailTabs",
event: "onDisplayedFolderChanged",
extensionApi: this,
}).api(),
onSelectedMessagesChanged: new EventManager({
context,
name: "mailTabs.onSelectedMessagesChanged",
register: fire => {
let listener = async (event, tab, messages) => {
let page = await messageListTracker.startList(
messages,
extension
);
fire.sync(tabManager.convert(tab), page);
};
uiListener.on("messages-changed", listener);
uiListener.incrementListeners();
return () => {
uiListener.off("messages-changed", listener);
uiListener.decrementListeners();
};
},
module: "mailTabs",
event: "onSelectedMessagesChanged",
extensionApi: this,
}).api(),
},
};

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

@ -1019,7 +1019,7 @@ MenuItem.prototype = {
if (targetPattern) {
let targetUrls = [];
if (contextData.onImage || contextData.onAudio || contextData.onVideo) {
// TODO: double check if srcUrl is always set when we need it
// TODO: Double check if srcUrl is always set when we need it.
targetUrls.push(contextData.srcUrl);
}
if (contextData.onLink) {

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

@ -78,50 +78,81 @@ function getMsgHdr(properties) {
return msgHdr;
}
this.messageDisplay = class extends ExtensionAPI {
this.messageDisplay = class extends ExtensionAPIPersistent {
PERSISTENT_EVENTS = {
// For primed persistent events (deactivated background), the context is only
// available after fire.wakeup() has fulfilled (ensuring the convert() function
// has been called).
onMessageDisplayed({ context, fire }) {
const { extension } = this;
const { tabManager, windowManager } = extension;
let listener = {
async handleEvent(event) {
if (fire.wakeup) {
await fire.wakeup();
}
let win = windowManager.wrapWindow(event.target);
let tab = tabManager.convert(win.activeTab.nativeTab);
let msg = convertMessage(event.detail, extension);
fire.async(tab, msg);
},
};
windowTracker.addListener("MsgLoaded", listener);
return {
unregister: () => {
windowTracker.removeListener("MsgLoaded", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onMessagesDisplayed({ context, fire }) {
const { extension } = this;
const { tabManager, windowManager } = extension;
let listener = {
async handleEvent(event) {
if (fire.wakeup) {
await fire.wakeup();
}
let win = windowManager.wrapWindow(event.target);
let tab = tabManager.convert(win.activeTab.nativeTab);
getDisplayedMessages(win.activeTab, extension).then(msgs => {
fire.async(tab, msgs);
});
},
};
windowTracker.addListener("MsgsLoaded", listener);
return {
unregister: () => {
windowTracker.removeListener("MsgsLoaded", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
};
getAPI(context) {
let { extension } = context;
let { tabManager, windowManager } = extension;
let { tabManager } = extension;
return {
messageDisplay: {
onMessageDisplayed: new EventManager({
context,
name: "messageDisplay.onMessageDisplayed",
register: fire => {
let listener = {
handleEvent(event) {
let win = windowManager.wrapWindow(event.target);
let tab = tabManager.convert(win.activeTab.nativeTab);
let msg = convertMessage(event.detail, extension);
fire.async(tab, msg);
},
};
windowTracker.addListener("MsgLoaded", listener);
return () => {
windowTracker.removeListener("MsgLoaded", listener);
};
},
module: "messageDisplay",
event: "onMessageDisplayed",
extensionApi: this,
}).api(),
onMessagesDisplayed: new EventManager({
context,
name: "messageDisplay.onMessagesDisplayed",
register: fire => {
let listener = {
handleEvent(event) {
let win = windowManager.wrapWindow(event.target);
let tab = tabManager.convert(win.activeTab.nativeTab);
getDisplayedMessages(win.activeTab, extension).then(msgs => {
fire.async(tab, msgs);
});
},
};
windowTracker.addListener("MsgsLoaded", listener);
return () => {
windowTracker.removeListener("MsgsLoaded", listener);
};
},
module: "messageDisplay",
event: "onMessagesDisplayed",
extensionApi: this,
}).api(),
async getDisplayedMessage(tabId) {
let tab = tabManager.get(tabId);

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

@ -420,7 +420,136 @@ async function getMimeMessage(msgHdr, partName = "") {
: mimeMsg;
}
this.messages = class extends ExtensionAPI {
this.messages = class extends ExtensionAPIPersistent {
PERSISTENT_EVENTS = {
// For primed persistent events (deactivated background), the context is only
// available after fire.wakeup() has fulfilled (ensuring the convert() function
// has been called).
onNewMailReceived({ context, fire }) {
let listener = async (event, folder, newMessages) => {
let { extension } = this;
// The msgHdr could be gone after the wakeup, convert it early.
let page = await messageListTracker.startList(newMessages, extension);
if (fire.wakeup) {
await fire.wakeup();
}
fire.async(convertFolder(folder), page);
};
messageTracker.on("messages-received", listener);
return {
unregister: () => {
messageTracker.off("messages-received", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onUpdated({ context, fire }) {
let listener = async (event, message, properties) => {
let { extension } = this;
// The msgHdr could be gone after the wakeup, convert it early.
let convertedMessage = convertMessage(message, extension);
if (fire.wakeup) {
await fire.wakeup();
}
fire.async(convertedMessage, properties);
};
messageTracker.on("message-updated", listener);
return {
unregister: () => {
messageTracker.off("message-updated", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onMoved({ context, fire }) {
let listener = async (event, srcMessages, dstMessages) => {
let { extension } = this;
// The msgHdr could be gone after the wakeup, convert them early.
let srcPage = await messageListTracker.startList(
srcMessages,
extension
);
let dstPage = await messageListTracker.startList(
dstMessages,
extension
);
if (fire.wakeup) {
await fire.wakeup();
}
fire.async(srcPage, dstPage);
};
messageTracker.on("messages-moved", listener);
return {
unregister: () => {
messageTracker.off("messages-moved", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onCopied({ context, fire }) {
let listener = async (event, srcMessages, dstMessages) => {
let { extension } = this;
// The msgHdr could be gone after the wakeup, convert them early.
let srcPage = await messageListTracker.startList(
srcMessages,
extension
);
let dstPage = await messageListTracker.startList(
dstMessages,
extension
);
if (fire.wakeup) {
await fire.wakeup();
}
fire.async(srcPage, dstPage);
};
messageTracker.on("messages-copied", listener);
return {
unregister: () => {
messageTracker.off("messages-copied", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onDeleted({ context, fire }) {
let listener = async (event, deletedMessages) => {
let { extension } = this;
// The msgHdr could be gone after the wakeup, convert them early.
let deletedPage = await messageListTracker.startList(
deletedMessages,
extension
);
if (fire.wakeup) {
await fire.wakeup();
}
fire.async(deletedPage);
};
messageTracker.on("messages-deleted", listener);
return {
unregister: () => {
messageTracker.off("messages-deleted", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
};
getAPI(context) {
function collectMessagesInFolders(messageIds) {
let folderMap = new DefaultMap(() => new Set());
@ -572,96 +701,33 @@ this.messages = class extends ExtensionAPI {
messages: {
onNewMailReceived: new EventManager({
context,
name: "messages.onNewMailReceived",
register: fire => {
let listener = async (event, folder, newMessages) => {
let page = await messageListTracker.startList(
newMessages,
context.extension
);
fire.async(convertFolder(folder), page);
};
messageTracker.on("messages-received", listener);
return () => {
messageTracker.off("messages-received", listener);
};
},
module: "messages",
event: "onNewMailReceived",
extensionApi: this,
}).api(),
onUpdated: new EventManager({
context,
name: "messageDisplay.onUpdated",
register: fire => {
let listener = async (event, message, properties) => {
fire.async(
convertMessage(message, context.extension),
properties
);
};
messageTracker.on("message-updated", listener);
return () => {
messageTracker.off("message-updated", listener);
};
},
module: "messages",
event: "onUpdated",
extensionApi: this,
}).api(),
onMoved: new EventManager({
context,
name: "messageDisplay.onMoved",
register: fire => {
let listener = async (event, srcMessages, dstMessages) => {
let srcPage = await messageListTracker.startList(
srcMessages,
context.extension
);
let dstPage = await messageListTracker.startList(
dstMessages,
context.extension
);
fire.async(srcPage, dstPage);
};
messageTracker.on("messages-moved", listener);
return () => {
messageTracker.off("messages-moved", listener);
};
},
module: "messages",
event: "onMoved",
extensionApi: this,
}).api(),
onCopied: new EventManager({
context,
name: "messageDisplay.onCopied",
register: fire => {
let listener = async (event, srcMessages, dstMessages) => {
let srcPage = await messageListTracker.startList(
srcMessages,
context.extension
);
let dstPage = await messageListTracker.startList(
dstMessages,
context.extension
);
fire.async(srcPage, dstPage);
};
messageTracker.on("messages-copied", listener);
return () => {
messageTracker.off("messages-copied", listener);
};
},
module: "messages",
event: "onCopied",
extensionApi: this,
}).api(),
onDeleted: new EventManager({
context,
name: "messageDisplay.onDeleted",
register: fire => {
let listener = async (event, deletedMessages) => {
let deletedPage = await messageListTracker.startList(
deletedMessages,
context.extension
);
fire.async(deletedPage);
};
messageTracker.on("messages-deleted", listener);
return () => {
messageTracker.off("messages-deleted", listener);
};
},
module: "messages",
event: "onDeleted",
extensionApi: this,
}).api(),
async list({ accountId, path }) {
let uri = folderPathToURI(accountId, path);

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

@ -91,17 +91,153 @@ const allAttrs = new Set(["favIconUrl", "title"]);
const allProperties = new Set(["favIconUrl", "status", "title"]);
const restricted = new Set(["url", "favIconUrl", "title"]);
/**
* An EventManager for the tabs.onUpdated listener.
*/
class TabsUpdateFilterEventManager extends EventManager {
constructor({ context }) {
let { extension } = context;
let { tabManager } = extension;
this.tabs = class extends ExtensionAPIPersistent {
onShutdown(isAppShutdown) {
if (isAppShutdown) {
return;
}
for (let window of Services.wm.getEnumerator("mail:3pane")) {
let tabmail = window.document.getElementById("tabmail");
for (let i = tabmail.tabInfo.length; i > 0; i--) {
let nativeTabInfo = tabmail.tabInfo[i - 1];
let uri = nativeTabInfo.browser?.browsingContext.currentURI;
if (
uri &&
uri.scheme == "moz-extension" &&
uri.host == this.extension.uuid
) {
tabmail.closeTab(nativeTabInfo);
}
}
}
}
let register = (fire, filterProps) => {
tabEventRegistrar({ tabEvent, listener }) {
let { extension } = this;
let { tabManager } = extension;
return ({ context, fire }) => {
let listener2 = async (eventName, event, ...args) => {
if (!tabManager.canAccessTab(event.nativeTab)) {
return;
}
if (fire.wakeup) {
await fire.wakeup();
}
listener({ context, fire, event }, ...args);
};
tabTracker.on(tabEvent, listener2);
return {
unregister() {
tabTracker.off(tabEvent, listener2);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
};
}
PERSISTENT_EVENTS = {
// For primed persistent events (deactivated background), the context is only
// available after fire.wakeup() has fulfilled (ensuring the convert() function
// has been called) (handled by tabEventRegistrar).
onActivated: this.tabEventRegistrar({
tabEvent: "tab-activated",
listener: ({ context, fire, event }) => {
fire.async(event);
},
}),
onCreated: this.tabEventRegistrar({
tabEvent: "tab-created",
listener: ({ context, fire, event }) => {
let { extension } = this;
let { tabManager } = extension;
fire.async(tabManager.convert(event.nativeTabInfo, event.currentTab));
},
}),
onAttached: this.tabEventRegistrar({
tabEvent: "tab-attached",
listener: ({ context, fire, event }) => {
fire.async(event.tabId, {
newWindowId: event.newWindowId,
newPosition: event.newPosition,
});
},
}),
onDetached: this.tabEventRegistrar({
tabEvent: "tab-detached",
listener: ({ context, fire, event }) => {
fire.async(event.tabId, {
oldWindowId: event.oldWindowId,
oldPosition: event.oldPosition,
});
},
}),
onRemoved: this.tabEventRegistrar({
tabEvent: "tab-removed",
listener: ({ context, fire, event }) => {
fire.async(event.tabId, {
windowId: event.windowId,
isWindowClosing: event.isWindowClosing,
});
},
}),
onMoved({ context, fire }) {
let { tabManager } = this.extension;
let moveListener = async event => {
let nativeTab = event.target;
let nativeTabInfo = event.detail.tabInfo;
let tabmail = nativeTab.ownerDocument.getElementById("tabmail");
if (tabManager.canAccessTab(nativeTab)) {
if (fire.wakeup) {
await fire.wakeup();
}
fire.async(tabTracker.getId(nativeTabInfo), {
windowId: windowTracker.getId(nativeTab.ownerGlobal),
fromIndex: event.detail.idx,
toIndex: tabmail.tabInfo.indexOf(nativeTabInfo),
});
}
};
windowTracker.addListener("TabMove", moveListener);
return {
unregister() {
windowTracker.removeListener("TabMove", moveListener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
onUpdated({ context, fire }, [filterProps]) {
let filter = { ...filterProps };
let scheduledEvents = [];
if (
filter &&
filter.urls &&
!this.extension.hasPermission("tabs") &&
!this.extension.hasPermission("activeTab")
) {
console.error(
'Url filtering in tabs.onUpdated requires "tabs" or "activeTab" permission.'
);
return false;
}
if (filter.urls) {
// TODO: Consider following M-C
// Use additional parameter { restrictSchemes: false }.
filter.urls = new MatchPatternSet(filter.urls);
}
let needsModified = true;
@ -113,12 +249,16 @@ class TabsUpdateFilterEventManager extends EventManager {
filter.properties = allProperties;
}
function sanitize(changeInfo) {
function sanitize(tab, changeInfo) {
let result = {};
let nonempty = false;
let hasTabs = extension.hasPermission("tabs");
for (let prop in changeInfo) {
if (hasTabs || !restricted.has(prop)) {
// In practice, changeInfo contains at most one property from
// restricted. Therefore it is not necessary to cache the value
// of tab.hasTabPermission outside the loop.
// Unnecessarily accessing tab.hasTabPermission can cause bugs, see
// https://bugzilla.mozilla.org/show_bug.cgi?id=1694699#c21
if (tab.hasTabPermission || !restricted.has(prop)) {
nonempty = true;
result[prop] = changeInfo[prop];
}
@ -128,6 +268,8 @@ class TabsUpdateFilterEventManager extends EventManager {
function getWindowID(windowId) {
if (windowId === WindowBase.WINDOW_ID_CURRENT) {
// TODO: Consider following M-C
// Use windowTracker.getTopWindow(context).
return windowTracker.getId(windowTracker.topWindow);
}
return windowId;
@ -153,19 +295,42 @@ class TabsUpdateFilterEventManager extends EventManager {
return true;
}
let fireForTab = (tab, changed) => {
let fireForTab = async (tab, changed) => {
if (!matchFilters(tab, changed)) {
return;
}
let changeInfo = sanitize(changed);
let changeInfo = sanitize(tab, changed);
if (changeInfo) {
fire.async(tab.id, changeInfo, tab.convert());
let tabInfo = tab.convert();
// TODO: Consider following M-C
// Use tabTracker.maybeWaitForTabOpen(nativeTab).then(() => {}).
// Using a FIFO to keep order of events, in case the last one
// gets through without being placed on the async callback stack.
scheduledEvents.push([tab.id, changeInfo, tabInfo]);
if (fire.wakeup) {
await fire.wakeup();
}
fire.async(...scheduledEvents.shift());
}
};
let listener = event => {
/* TODO: Consider following M-C
// Ignore any events prior to TabOpen and events that are triggered while
// tabs are swapped between windows.
if (event.originalTarget.initializingTab) {
return;
}
if (!extension.canAccessWindow(event.originalTarget.ownerGlobal)) {
return;
}
*/
let changeInfo = {};
let { extension } = this;
let { tabManager } = extension;
let tab = tabManager.getWrapper(event.detail.tabInfo);
let changed = event.detail.changed;
if (
@ -182,6 +347,8 @@ class TabsUpdateFilterEventManager extends EventManager {
};
let statusListener = ({ browser, status, url }) => {
let { extension } = this;
let { tabManager } = extension;
let tabmail = browser.ownerDocument.getElementById("tabmail");
let nativeTabInfo = tabmail.getTabForBrowser(browser);
if (nativeTabInfo) {
@ -202,60 +369,22 @@ class TabsUpdateFilterEventManager extends EventManager {
windowTracker.addListener("status", statusListener);
}
return () => {
if (needsModified) {
windowTracker.removeListener("TabAttrModified", listener);
}
if (filter.properties.has("status")) {
windowTracker.removeListener("status", statusListener);
}
return {
unregister() {
if (needsModified) {
windowTracker.removeListener("TabAttrModified", listener);
}
if (filter.properties.has("status")) {
windowTracker.removeListener("status", statusListener);
}
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
};
super({
context,
name: "tabs.onUpdated",
register,
});
}
addListener(callback, filter) {
let { extension } = this.context;
if (
filter &&
filter.urls &&
!extension.hasPermission("tabs") &&
!extension.hasPermission("activeTab")
) {
console.error(
'Url filtering in tabs.onUpdated requires "tabs" or "activeTab" permission.'
);
return false;
}
return super.addListener(callback, filter);
}
}
this.tabs = class extends ExtensionAPI {
onShutdown(isAppShutdown) {
if (isAppShutdown) {
return;
}
for (let window of Services.wm.getEnumerator("mail:3pane")) {
let tabmail = window.document.getElementById("tabmail");
for (let i = tabmail.tabInfo.length; i > 0; i--) {
let nativeTabInfo = tabmail.tabInfo[i - 1];
let uri = nativeTabInfo.browser?.browsingContext.currentURI;
if (
uri &&
uri.scheme == "moz-extension" &&
uri.host == this.extension.uuid
) {
tabmail.closeTab(nativeTabInfo);
}
}
}
}
},
};
getAPI(context) {
let { extension } = context;
@ -297,114 +426,52 @@ this.tabs = class extends ExtensionAPI {
tabs: {
onActivated: new EventManager({
context,
name: "tabs.onActivated",
register: fire => {
let listener = (eventName, event) => {
fire.async(event);
};
tabTracker.on("tab-activated", listener);
return () => {
tabTracker.off("tab-activated", listener);
};
},
module: "tabs",
event: "onActivated",
extensionApi: this,
}).api(),
onCreated: new EventManager({
context,
name: "tabs.onCreated",
register: fire => {
let listener = (eventName, event) => {
fire.async(
tabManager.convert(event.nativeTabInfo, event.currentTab)
);
};
tabTracker.on("tab-created", listener);
return () => {
tabTracker.off("tab-created", listener);
};
},
module: "tabs",
event: "onCreated",
extensionApi: this,
}).api(),
onAttached: new EventManager({
context,
name: "tabs.onAttached",
register: fire => {
let listener = (eventName, event) => {
fire.async(event.tabId, {
newWindowId: event.newWindowId,
newPosition: event.newPosition,
});
};
tabTracker.on("tab-attached", listener);
return () => {
tabTracker.off("tab-attached", listener);
};
},
module: "tabs",
event: "onAttached",
extensionApi: this,
}).api(),
onDetached: new EventManager({
context,
name: "tabs.onDetached",
register: fire => {
let listener = (eventName, event) => {
fire.async(event.tabId, {
oldWindowId: event.oldWindowId,
oldPosition: event.oldPosition,
});
};
tabTracker.on("tab-detached", listener);
return () => {
tabTracker.off("tab-detached", listener);
};
},
module: "tabs",
event: "onDetached",
extensionApi: this,
}).api(),
onRemoved: new EventManager({
context,
name: "tabs.onRemoved",
register: fire => {
let listener = (eventName, event) => {
fire.async(event.tabId, {
windowId: event.windowId,
isWindowClosing: event.isWindowClosing,
});
};
tabTracker.on("tab-removed", listener);
return () => {
tabTracker.off("tab-removed", listener);
};
},
module: "tabs",
event: "onRemoved",
extensionApi: this,
}).api(),
onMoved: new EventManager({
context,
name: "tabs.onMoved",
register: fire => {
let moveListener = event => {
let nativeTab = event.target;
let nativeTabInfo = event.detail.tabInfo;
let tabmail = nativeTab.ownerDocument.getElementById("tabmail");
fire.async(tabTracker.getId(nativeTabInfo), {
windowId: windowTracker.getId(nativeTab.ownerGlobal),
fromIndex: event.detail.idx,
toIndex: tabmail.tabInfo.indexOf(nativeTabInfo),
});
};
windowTracker.addListener("TabMove", moveListener);
return () => {
windowTracker.removeListener("TabMove", moveListener);
};
},
module: "tabs",
event: "onMoved",
extensionApi: this,
}).api(),
onUpdated: new TabsUpdateFilterEventManager({ context }).api(),
onUpdated: new EventManager({
context,
module: "tabs",
event: "onUpdated",
extensionApi: this,
}).api(),
async create(createProperties) {
let window = await getNormalWindowReady(

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

@ -52,7 +52,7 @@ class Theme {
if (startupData && startupData.lwtData) {
Object.assign(this, startupData);
} else {
// TODO: Update this part after bug 1550090
// TODO: Update this part after bug 1550090.
this.lwtStyles = {};
this.lwtDarkStyles = null;
if (darkDetails) {
@ -418,8 +418,15 @@ class Theme {
this.theme = class extends ExtensionAPIPersistent {
PERSISTENT_EVENTS = {
// For primed persistent events (deactivated background), the context is only
// available after fire.wakeup() has fulfilled (ensuring the convert() function
// has been called).
onUpdated({ fire, context }) {
let callback = (event, theme, windowId) => {
let callback = async (event, theme, windowId) => {
if (fire.wakeup) {
await fire.wakeup();
}
if (windowId) {
// Force access validation for incognito mode by getting the window.
if (windowTracker.getWindow(windowId, context, false)) {
@ -435,9 +442,9 @@ this.theme = class extends ExtensionAPIPersistent {
unregister() {
onUpdatedEmitter.off("theme-updated", callback);
},
convert(_fire, _context) {
fire = _fire;
context = _context;
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},

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

@ -5,41 +5,7 @@
// The ext-* files are imported into the same scopes.
/* import-globals-from ext-mail.js */
/**
* An event manager API provider which listens for a DOM event in any browser
* window, and calls the given listener function whenever an event is received.
* That listener function receives a `fire` object, which it can use to dispatch
* events to the extension, and a DOM event object.
*
* @param {BaseContext} context
* The extension context which the event manager belongs to.
* @param {string} name
* The API name of the event manager, e.g.,"runtime.onMessage".
* @param {string} event
* The name of the DOM event to listen for.
* @param {Function} listener
* The listener function to call when a DOM event is received.
*
* @returns {object} An injectable api for the new event.
*/
function WindowEventManager(context, name, event, listener) {
let register = fire => {
let listener2 = (window, ...args) => {
if (context.canAccessWindow(window)) {
listener(fire, window, ...args);
}
};
windowTracker.addListener(event, listener2);
return () => {
windowTracker.removeListener(event, listener2);
};
};
return new EventManager({ context, name, register }).api();
}
this.windows = class extends ExtensionAPI {
this.windows = class extends ExtensionAPIPersistent {
onShutdown(isAppShutdown) {
if (isAppShutdown) {
return;
@ -52,62 +18,124 @@ this.windows = class extends ExtensionAPI {
}
}
windowEventRegistrar({ windowEvent, listener }) {
let { extension } = this;
return ({ context, fire }) => {
let listener2 = async (window, ...args) => {
if (!extension.canAccessWindow(window)) {
return;
}
if (fire.wakeup) {
await fire.wakeup();
}
listener({ context, fire, window }, ...args);
};
windowTracker.addListener(windowEvent, listener2);
return {
unregister() {
windowTracker.removeListener(windowEvent, listener2);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
};
}
PERSISTENT_EVENTS = {
// For primed persistent events (deactivated background), the context is only
// available after fire.wakeup() has fulfilled (ensuring the convert() function
// has been called) (handled by windowEventRegistrar).
onCreated: this.windowEventRegistrar({
windowEvent: "domwindowopened",
listener: ({ context, fire, window }) => {
fire.async(this.extension.windowManager.convert(window));
},
}),
onRemoved: this.windowEventRegistrar({
windowEvent: "domwindowclosed",
listener: ({ context, fire, window }) => {
fire.async(windowTracker.getId(window));
},
}),
onFocusChanged({ context, fire }) {
let { extension } = this;
// Keep track of the last windowId used to fire an onFocusChanged event
let lastOnFocusChangedWindowId;
let scheduledEvents = [];
let listener = async event => {
// Wait a tick to avoid firing a superfluous WINDOW_ID_NONE
// event when switching focus between two Thunderbird windows.
// Note: This is not working for Linux, where we still get the -1
await Promise.resolve();
let windowId = WindowBase.WINDOW_ID_NONE;
let window = Services.focus.activeWindow;
if (window) {
if (!extension.canAccessWindow(window)) {
return;
}
windowId = windowTracker.getId(window);
}
// Using a FIFO to keep order of events, in case the last one
// gets through without being placed on the async callback stack.
scheduledEvents.push(windowId);
if (fire.wakeup) {
await fire.wakeup();
}
let scheduledWindowId = scheduledEvents.shift();
if (scheduledWindowId !== lastOnFocusChangedWindowId) {
lastOnFocusChangedWindowId = scheduledWindowId;
fire.async(scheduledWindowId);
}
};
windowTracker.addListener("focus", listener);
windowTracker.addListener("blur", listener);
return {
unregister() {
windowTracker.removeListener("focus", listener);
windowTracker.removeListener("blur", listener);
},
convert(newFire, extContext) {
fire = newFire;
context = extContext;
},
};
},
};
getAPI(context) {
const { extension } = context;
const { windowManager } = extension;
return {
windows: {
onCreated: WindowEventManager(
onCreated: new EventManager({
context,
"windows.onCreated",
"domwindowopened",
(fire, window) => {
fire.async(windowManager.convert(window));
}
),
module: "windows",
event: "onCreated",
extensionApi: this,
}).api(),
onRemoved: WindowEventManager(
onRemoved: new EventManager({
context,
"windows.onRemoved",
"domwindowclosed",
(fire, window) => {
fire.async(windowTracker.getId(window));
}
),
module: "windows",
event: "onRemoved",
extensionApi: this,
}).api(),
onFocusChanged: new EventManager({
context,
name: "windows.onFocusChanged",
register: fire => {
// Keep track of the last windowId used to fire an onFocusChanged event
let lastOnFocusChangedWindowId;
let listener = event => {
// Wait a tick to avoid firing a superfluous WINDOW_ID_NONE
// event when switching focus between two Thunderbird windows.
Promise.resolve().then(() => {
let windowId = WindowBase.WINDOW_ID_NONE;
let window = Services.focus.activeWindow;
if (window) {
if (!context.canAccessWindow(window)) {
return;
}
windowId = windowTracker.getId(window);
}
if (windowId !== lastOnFocusChangedWindowId) {
fire.async(windowId);
lastOnFocusChangedWindowId = windowId;
}
});
};
windowTracker.addListener("focus", listener);
windowTracker.addListener("blur", listener);
return () => {
windowTracker.removeListener("focus", listener);
windowTracker.removeListener("blur", listener);
};
},
module: "windows",
event: "onFocusChanged",
extensionApi: this,
}).api(),
get(windowId, getInfo) {

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

@ -634,10 +634,6 @@
{
"name": "node",
"$ref": "ContactNode"
},
{
"name": "id",
"type": "string"
}
]
},

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

@ -15,6 +15,7 @@ tags = webextensions
[browser_ext_addressBooksUI.js]
tags = addrbook
[browser_ext_browserAction.js]
[browser_ext_browserAction_popup_click.js]
[browser_ext_browserAction_properties.js]
[browser_ext_cloudFile.js]
support-files = data/cloudFile1.txt data/cloudFile2.txt
@ -43,6 +44,7 @@ support-files = data/cloudFile1.txt data/cloudFile2.txt
[browser_ext_compose_saveTemplate.js]
[browser_ext_compose_sendMessage.js]
[browser_ext_composeAction.js]
[browser_ext_composeAction_popup_click.js]
[browser_ext_composeAction_properties.js]
[browser_ext_composeScripts.js]
[browser_ext_contentScripts.js]
@ -62,7 +64,11 @@ tags = contextmenu
[browser_ext_message_external.js]
support-files = messages/attachedMessageSample.eml
[browser_ext_messageDisplay.js]
[browser_ext_messageDisplay_headerMessageId.js]
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_properties.js]
[browser_ext_messageDisplayScripts.js]
[browser_ext_quickFilter.js]
@ -70,6 +76,8 @@ support-files = messages/attachedMessageSample.eml
[browser_ext_tabs_content.js]
[browser_ext_tabs_events.js]
[browser_ext_tabs_query.js]
[browser_ext_themes_onUpdated.js]
[browser_ext_windows.js]
[browser_ext_windows_events.js]
[browser_ext_windows_types.js]

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

@ -9,7 +9,7 @@ const { AddonManager } = ChromeUtils.import(
let account;
let messages;
add_task(async () => {
add_setup(async () => {
account = createAccount();
let rootFolder = account.incomingServer.rootFolder;
let subFolders = rootFolder.subFolders;
@ -32,150 +32,53 @@ add_task(async () => {
await BrowserTestUtils.browserLoaded(window.getMessagePaneBrowser());
});
// This test clicks on the action button to open the popup.
add_task(async function test_popup_open_with_click() {
info("3-pane tab");
await run_popup_test({
actionType: "browser_action",
testType: "open-with-mouse-click",
window,
});
await run_popup_test({
actionType: "browser_action",
testType: "open-with-mouse-click",
disable_button: true,
window,
});
await run_popup_test({
actionType: "browser_action",
testType: "open-with-mouse-click",
use_default_popup: true,
window,
});
await run_popup_test({
actionType: "browser_action",
testType: "open-with-mouse-click",
default_area: "tabstoolbar",
window,
});
await run_popup_test({
actionType: "browser_action",
testType: "open-with-mouse-click",
disable_button: true,
default_area: "tabstoolbar",
window,
});
await run_popup_test({
actionType: "browser_action",
testType: "open-with-mouse-click",
use_default_popup: true,
default_area: "tabstoolbar",
window,
});
info("Message window");
let messageWindow = await openMessageInWindow(messages.getNext());
await run_popup_test({
actionType: "browser_action",
testType: "open-with-mouse-click",
default_windows: ["messageDisplay"],
window: messageWindow,
});
await run_popup_test({
actionType: "browser_action",
testType: "open-with-mouse-click",
default_windows: ["messageDisplay"],
disable_button: true,
window: messageWindow,
});
await run_popup_test({
actionType: "browser_action",
testType: "open-with-mouse-click",
default_windows: ["messageDisplay"],
use_default_popup: true,
window: messageWindow,
});
messageWindow.close();
});
// This test uses a command from the menus API to open the popup.
add_task(async function test_popup_open_with_menu_command() {
info("3-pane tab");
await run_popup_test({
actionType: "browser_action",
testType: "open-with-menu-command",
use_default_popup: true,
window,
});
for (let area of [null, "tabstoolbar"]) {
let testConfig = {
actionType: "browser_action",
testType: "open-with-menu-command",
default_area: area,
window,
};
await run_popup_test({
actionType: "browser_action",
testType: "open-with-menu-command",
disable_button: true,
window,
});
await run_popup_test({
actionType: "browser_action",
testType: "open-with-menu-command",
window,
});
await run_popup_test({
actionType: "browser_action",
testType: "open-with-menu-command",
default_area: "tabstoolbar",
use_default_popup: true,
window,
});
await run_popup_test({
actionType: "browser_action",
testType: "open-with-menu-command",
default_area: "tabstoolbar",
disable_button: true,
window,
});
await run_popup_test({
actionType: "browser_action",
testType: "open-with-menu-command",
default_area: "tabstoolbar",
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());
await run_popup_test({
actionType: "browser_action",
testType: "open-with-menu-command",
use_default_popup: true,
default_windows: ["messageDisplay"],
window: messageWindow,
});
{
let messageWindow = await openMessageInWindow(messages.getNext());
let testConfig = {
actionType: "browser_action",
testType: "open-with-menu-command",
default_windows: ["messageDisplay"],
window: messageWindow,
};
await run_popup_test({
actionType: "browser_action",
testType: "open-with-menu-command",
disable_button: true,
default_windows: ["messageDisplay"],
window: messageWindow,
});
await run_popup_test({
actionType: "browser_action",
testType: "open-with-menu-command",
default_windows: ["messageDisplay"],
window: messageWindow,
});
messageWindow.close();
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() {

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

@ -0,0 +1,158 @@
/* 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());
});
// This test clicks on the action button to open the popup.
add_task(async function test_popup_open_with_click() {
info("3-pane tab");
{
let testConfig = {
actionType: "browser_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,
});
await run_popup_test({
...testConfig,
default_area: "tabstoolbar",
});
await run_popup_test({
...testConfig,
disable_button: true,
default_area: "tabstoolbar",
});
await run_popup_test({
...testConfig,
use_default_popup: true,
default_area: "tabstoolbar",
});
}
info("Message window");
{
let messageWindow = await openMessageInWindow(messages.getNext());
let testConfig = {
actionType: "browser_action",
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();
}
});
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);
});

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

@ -514,7 +514,7 @@ add_task(async function test_without_UI() {
* cloudFile.onFileRename and cloudFile.onFileUploadAbort listeners with UI
* interaction.
*/
add_task(async function test_compose_window() {
add_task(async function test_compose_window_MV2() {
let testFiles = {
cloudFile1: new FileUtils.File(getTestFilePath("data/cloudFile1.txt")),
cloudFile2: new FileUtils.File(getTestFilePath("data/cloudFile2.txt")),
@ -760,6 +760,359 @@ add_task(async function test_compose_window() {
composeWindow.close();
});
/**
* Test persistent cloudFile.* events (onFileUpload, onFileDeleted, onFileRename,
* onFileUploadAbort, onAccountAdded, onAccountDeleted) with UI interaction and
* background terminations and background restarts.
*/
add_task(async function test_compose_window_MV3_event_page() {
let testFiles = {
cloudFile1: new FileUtils.File(getTestFilePath("data/cloudFile1.txt")),
cloudFile2: new FileUtils.File(getTestFilePath("data/cloudFile2.txt")),
};
let uploads = {};
let composeWindow;
async function background() {
let abortResolveCallback;
// Whenever the extension starts or wakes up, the eventCounter is reset and
// allows to observe the order of events fired. In case of a wake-up, the
// first observed event is the one that woke up the background.
let eventCounter = 0;
browser.cloudFile.onFileUpload.addListener(
async (uploadAccount, { id, name, data }, tab, relatedFileInfo) => {
eventCounter++;
browser.test.assertEq(
eventCounter,
1,
"onFileUpload should be the wake up event"
);
let [
{ cloudAccountId, composeTabId, aborting },
] = await window.sendMessage("getEnvironment");
browser.test.assertEq(tab.id, composeTabId);
browser.test.assertEq(uploadAccount.id, cloudAccountId);
browser.test.assertEq(name, "cloudFile1.txt");
// eslint-disable-next-line mozilla/use-isInstance
browser.test.assertTrue(data instanceof File);
let content = await data.text();
browser.test.assertEq(content, "you got the moves!\n");
browser.test.assertEq(undefined, relatedFileInfo);
if (aborting) {
let abortPromise = new Promise(resolve => {
abortResolveCallback = resolve;
});
browser.test.sendMessage("uploadStarted", id);
await abortPromise;
setTimeout(() => {
browser.test.sendMessage("uploadAborted");
});
return { aborted: true };
}
setTimeout(() => {
browser.test.sendMessage("uploadFinished", id);
});
return { url: "https://example.com/" + name };
}
);
browser.cloudFile.onFileRename.addListener(
async (account, id, newName, tab) => {
eventCounter++;
browser.test.assertEq(
eventCounter,
1,
"onFileRename should be the wake up event"
);
let [
{ cloudAccountId, fileId, composeTabId },
] = await window.sendMessage("getEnvironment");
browser.test.assertEq(tab.id, composeTabId);
browser.test.assertEq(account.id, cloudAccountId);
browser.test.assertEq(id, fileId);
browser.test.assertEq(newName, "cloudFile3.txt");
setTimeout(() => {
browser.test.sendMessage("renameFinished", id);
});
return { url: "https://example.com/" + newName };
}
);
browser.cloudFile.onFileDeleted.addListener(async (account, id, tab) => {
eventCounter++;
browser.test.assertEq(
eventCounter,
1,
"onFileDeleted should be the wake up event"
);
let [{ cloudAccountId, fileId, composeTabId }] = await window.sendMessage(
"getEnvironment"
);
browser.test.assertEq(tab.id, composeTabId);
browser.test.assertEq(account.id, cloudAccountId);
browser.test.assertEq(id, fileId);
setTimeout(() => {
browser.test.sendMessage("deleteFinished");
});
});
browser.cloudFile.onFileUploadAbort.addListener(
async (account, id, tab) => {
eventCounter++;
browser.test.assertEq(
eventCounter,
2,
"onFileUploadAbort should not be the wake up event"
);
let [
{ cloudAccountId, fileId, composeTabId },
] = await window.sendMessage("getEnvironment");
browser.test.assertEq(tab.id, composeTabId);
browser.test.assertEq(account.id, cloudAccountId);
browser.test.assertEq(id, fileId);
abortResolveCallback();
}
);
browser.cloudFile.onAccountAdded.addListener(account => {
eventCounter++;
browser.test.assertEq(
eventCounter,
1,
"onAccountAdded should be the wake up event"
);
browser.test.sendMessage("accountCreated", account.id);
});
browser.cloudFile.onAccountDeleted.addListener(async accountId => {
eventCounter++;
browser.test.assertEq(
eventCounter,
1,
"onAccountDeleted should be the wake up event"
);
let [{ cloudAccountId }] = await window.sendMessage("getEnvironment");
browser.test.assertEq(accountId, cloudAccountId);
browser.test.notifyPass("finished");
});
browser.runtime.onInstalled.addListener(async () => {
eventCounter++;
let [composeTab] = await browser.tabs.query({
windowType: "messageCompose",
});
await window.sendMessage("setEnvironment", {
composeTabId: composeTab.id,
});
browser.test.sendMessage("installed");
});
browser.test.sendMessage("background started");
}
let extension = ExtensionTestUtils.loadExtension({
files: {
"background.js": background,
"utils.js": await getUtilsJS(),
},
useAddonManager: "permanent",
manifest: {
manifest_version: 3,
cloud_file: {
name: "mochitest",
management_url: "/content/management.html",
},
browser_specific_settings: { gecko: { id: "cloudfile@mochi.test" } },
background: { scripts: ["utils.js", "background.js"] },
},
});
function uploadFile(
id,
filename,
expectedErrorStatus = Cr.NS_OK,
expectedErrorMessage
) {
let cloudFileAccount = cloudFileAccounts.getAccount(id);
if (typeof expectedErrorStatus == "string") {
expectedErrorStatus = cloudFileAccounts.constants[expectedErrorStatus];
}
return cloudFileAccount.uploadFile(composeWindow, testFiles[filename]).then(
upload => {
Assert.equal(Cr.NS_OK, expectedErrorStatus);
uploads[filename] = upload;
},
status => {
Assert.equal(
status.result,
expectedErrorStatus,
`Error status should be correct for ${testFiles[filename].leafName}`
);
Assert.equal(
status.message,
expectedErrorMessage,
`Error message should be correct for ${testFiles[filename].leafName}`
);
}
);
}
function startUpload(id, filename) {
let cloudFileAccount = cloudFileAccounts.getAccount(id);
return cloudFileAccount
.uploadFile(composeWindow, testFiles[filename])
.catch(() => {});
}
function cancelUpload(id, filename) {
let cloudFileAccount = cloudFileAccounts.getAccount(id);
return cloudFileAccount.cancelFileUpload(
composeWindow,
testFiles[filename]
);
}
function renameFile(
id,
uploadId,
{ newName, newUrl },
expectedErrorStatus = Cr.NS_OK,
expectedErrorMessage
) {
let cloudFileAccount = cloudFileAccounts.getAccount(id);
if (typeof expectedErrorStatus == "string") {
expectedErrorStatus = cloudFileAccounts.constants[expectedErrorStatus];
}
return cloudFileAccount.renameFile(composeWindow, uploadId, newName).then(
upload => {
Assert.equal(Cr.NS_OK, expectedErrorStatus);
Assert.equal(upload.name, newName, "New name should match.");
Assert.equal(upload.url, newUrl, "New url should match.");
},
status => {
Assert.equal(status.result, expectedErrorStatus);
Assert.equal(
status.message,
expectedErrorMessage,
`Error message should be correct.`
);
}
);
}
function deleteFile(id, uploadId) {
let cloudFileAccount = cloudFileAccounts.getAccount(id);
return cloudFileAccount.deleteFile(composeWindow, uploadId);
}
let environment = {};
extension.onMessage("setEnvironment", data => {
if (data.composeTabId) {
environment.composeTabId = data.composeTabId;
}
extension.sendMessage();
});
extension.onMessage("getEnvironment", () => {
extension.sendMessage(environment);
});
let account = createAccount();
addIdentity(account);
composeWindow = await openComposeWindow(account);
await focusWindow(composeWindow);
await extension.startup();
await extension.awaitMessage("installed");
await extension.awaitMessage("background started");
function checkPersistentListeners({ primed }) {
// A persistent event is referenced by its moduleName as defined in
// ext-mails.json, not by its actual namespace.
const persistent_events = [
"onFileUpload",
"onFileRename",
"onFileDeleted",
"onFileUploadAbort",
"onAccountAdded",
"onAccountDeleted",
];
for (let eventName of persistent_events) {
assertPersistentListeners(extension, "cloudFile", eventName, {
primed,
});
}
}
// Verify persistent listener, not yet primed.
checkPersistentListeners({ primed: false });
// Create account.
await extension.terminateBackground({ disableResetIdleForTest: true });
checkPersistentListeners({ primed: true });
cloudFileAccounts.createAccount("ext-cloudfile@mochi.test");
await extension.awaitMessage("background started");
environment.cloudAccountId = await extension.awaitMessage("accountCreated");
checkPersistentListeners({ primed: false });
// Upload.
await extension.terminateBackground({ disableResetIdleForTest: true });
checkPersistentListeners({ primed: true });
uploadFile(environment.cloudAccountId, "cloudFile1");
await extension.awaitMessage("background started");
environment.fileId = await extension.awaitMessage("uploadFinished");
checkPersistentListeners({ primed: false });
// Rename.
await extension.terminateBackground({ disableResetIdleForTest: true });
checkPersistentListeners({ primed: true });
renameFile(environment.cloudAccountId, environment.fileId, {
newName: "cloudFile3.txt",
newUrl: "https://example.com/cloudFile3.txt",
});
await extension.awaitMessage("background started");
await extension.awaitMessage("renameFinished");
checkPersistentListeners({ primed: false });
// Delete.
await extension.terminateBackground({ disableResetIdleForTest: true });
checkPersistentListeners({ primed: true });
deleteFile(environment.cloudAccountId, environment.fileId);
await extension.awaitMessage("background started");
await extension.awaitMessage("deleteFinished");
checkPersistentListeners({ primed: false });
// Aborted upload.
await extension.terminateBackground({ disableResetIdleForTest: true });
checkPersistentListeners({ primed: true });
environment.aborting = true;
startUpload(environment.cloudAccountId, "cloudFile1");
await extension.awaitMessage("background started");
environment.fileId = await extension.awaitMessage("uploadStarted");
cancelUpload(environment.cloudAccountId, "cloudFile1");
await extension.awaitMessage("uploadAborted");
checkPersistentListeners({ primed: false });
// Remove account.
await extension.terminateBackground({ disableResetIdleForTest: true });
checkPersistentListeners({ primed: true });
cloudFileAccounts.removeAccount(environment.cloudAccountId);
await extension.awaitMessage("background started");
checkPersistentListeners({ primed: false });
await extension.awaitFinish("finished");
await extension.unload();
composeWindow.close();
});
/**
* Test cloudFiles without accounts and removed local files.
*/
@ -834,8 +1187,8 @@ add_task(async function test_incomplete_cloudFiles() {
browser.compose.updateAttachment(composerTab.id, attachmentId, {
name: "cloudFile3",
}),
`CloudFile Error: Account not found: account7`,
"browser.compose.updateAttachment() should reject, if the local file does not exist."
`CloudFile Error: Account not found: ${createdAccount.id}`,
"browser.compose.updateAttachment() should reject, if the account does not exist."
);
browser.test.notifyPass("finished");

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

@ -124,7 +124,7 @@ async function testExecuteComposeActionWithOptions(options = {}) {
await extension.unload();
}
add_task(async function prepare_test() {
add_setup(async () => {
gAccount = createAccount();
addIdentity(gAccount);
});

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

@ -139,7 +139,7 @@ async function testExecuteMessageDisplayActionWithOptions(msg, options = {}) {
await extension.unload();
}
add_task(async function prepare_test() {
add_setup(async () => {
let account = createAccount();
let rootFolder = account.incomingServer.rootFolder;
let subFolders = rootFolder.subFolders;

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

@ -2,189 +2,189 @@
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
add_task(async function test_user_defined_commands() {
const testCommands = [
// Ctrl Shortcuts
{
name: "toggle-ctrl-a",
shortcut: "Ctrl+A",
key: "A",
// Does not work in compose window on Linux.
skip: ["messageCompose"],
modifiers: {
accelKey: true,
},
var testCommands = [
// Ctrl Shortcuts
{
name: "toggle-ctrl-a",
shortcut: "Ctrl+A",
key: "A",
// Does not work in compose window on Linux.
skip: ["messageCompose"],
modifiers: {
accelKey: true,
},
{
name: "toggle-ctrl-up",
shortcut: "Ctrl+Up",
key: "VK_UP",
modifiers: {
accelKey: true,
},
},
{
name: "toggle-ctrl-up",
shortcut: "Ctrl+Up",
key: "VK_UP",
modifiers: {
accelKey: true,
},
// Alt Shortcuts
{
name: "toggle-alt-a",
shortcut: "Alt+A",
key: "A",
// Does not work in compose window on Mac.
skip: ["messageCompose"],
modifiers: {
altKey: true,
},
},
// Alt Shortcuts
{
name: "toggle-alt-a",
shortcut: "Alt+A",
key: "A",
// Does not work in compose window on Mac.
skip: ["messageCompose"],
modifiers: {
altKey: true,
},
{
name: "toggle-alt-down",
shortcut: "Alt+Down",
key: "VK_DOWN",
modifiers: {
altKey: true,
},
},
{
name: "toggle-alt-down",
shortcut: "Alt+Down",
key: "VK_DOWN",
modifiers: {
altKey: true,
},
// Mac Shortcuts
{
name: "toggle-command-shift-page-up",
shortcutMac: "Command+Shift+PageUp",
key: "VK_PAGE_UP",
modifiers: {
accelKey: true,
shiftKey: true,
},
},
// Mac Shortcuts
{
name: "toggle-command-shift-page-up",
shortcutMac: "Command+Shift+PageUp",
key: "VK_PAGE_UP",
modifiers: {
accelKey: true,
shiftKey: true,
},
{
name: "toggle-mac-control-shift+period",
shortcut: "Ctrl+Shift+Period",
shortcutMac: "MacCtrl+Shift+Period",
key: "VK_PERIOD",
modifiers: {
ctrlKey: true,
shiftKey: true,
},
},
{
name: "toggle-mac-control-shift+period",
shortcut: "Ctrl+Shift+Period",
shortcutMac: "MacCtrl+Shift+Period",
key: "VK_PERIOD",
modifiers: {
ctrlKey: true,
shiftKey: true,
},
// Ctrl+Shift Shortcuts
{
name: "toggle-ctrl-shift-left",
shortcut: "Ctrl+Shift+Left",
key: "VK_LEFT",
modifiers: {
accelKey: true,
shiftKey: true,
},
},
// Ctrl+Shift Shortcuts
{
name: "toggle-ctrl-shift-left",
shortcut: "Ctrl+Shift+Left",
key: "VK_LEFT",
modifiers: {
accelKey: true,
shiftKey: true,
},
{
name: "toggle-ctrl-shift-1",
shortcut: "Ctrl+Shift+1",
key: "1",
modifiers: {
accelKey: true,
shiftKey: true,
},
},
{
name: "toggle-ctrl-shift-1",
shortcut: "Ctrl+Shift+1",
key: "1",
modifiers: {
accelKey: true,
shiftKey: true,
},
// Alt+Shift Shortcuts
{
name: "toggle-alt-shift-1",
shortcut: "Alt+Shift+1",
key: "1",
modifiers: {
altKey: true,
shiftKey: true,
},
},
// Alt+Shift Shortcuts
{
name: "toggle-alt-shift-1",
shortcut: "Alt+Shift+1",
key: "1",
modifiers: {
altKey: true,
shiftKey: true,
},
{
name: "toggle-alt-shift-a",
shortcut: "Alt+Shift+A",
key: "A",
// Does not work in compose window on Mac.
skip: ["messageCompose"],
modifiers: {
altKey: true,
shiftKey: true,
},
},
{
name: "toggle-alt-shift-a",
shortcut: "Alt+Shift+A",
key: "A",
// Does not work in compose window on Mac.
skip: ["messageCompose"],
modifiers: {
altKey: true,
shiftKey: true,
},
{
name: "toggle-alt-shift-right",
shortcut: "Alt+Shift+Right",
key: "VK_RIGHT",
modifiers: {
altKey: true,
shiftKey: true,
},
},
{
name: "toggle-alt-shift-right",
shortcut: "Alt+Shift+Right",
key: "VK_RIGHT",
modifiers: {
altKey: true,
shiftKey: true,
},
// Function keys
{
name: "function-keys-Alt+Shift+F3",
shortcut: "Alt+Shift+F3",
key: "VK_F3",
modifiers: {
altKey: true,
shiftKey: true,
},
},
// Function keys
{
name: "function-keys-Alt+Shift+F3",
shortcut: "Alt+Shift+F3",
key: "VK_F3",
modifiers: {
altKey: true,
shiftKey: true,
},
{
name: "function-keys-F2",
shortcut: "F2",
key: "VK_F2",
modifiers: {
altKey: false,
shiftKey: false,
},
},
{
name: "function-keys-F2",
shortcut: "F2",
key: "VK_F2",
modifiers: {
altKey: false,
shiftKey: false,
},
// Misc Shortcuts
{
name: "valid-command-with-unrecognized-property-name",
shortcut: "Alt+Shift+3",
key: "3",
modifiers: {
altKey: true,
shiftKey: true,
},
unrecognized_property: "with-a-random-value",
},
// Misc Shortcuts
{
name: "valid-command-with-unrecognized-property-name",
shortcut: "Alt+Shift+3",
key: "3",
modifiers: {
altKey: true,
shiftKey: true,
},
{
name: "spaces-in-shortcut-name",
shortcut: " Alt + Shift + 2 ",
key: "2",
modifiers: {
altKey: true,
shiftKey: true,
},
unrecognized_property: "with-a-random-value",
},
{
name: "spaces-in-shortcut-name",
shortcut: " Alt + Shift + 2 ",
key: "2",
modifiers: {
altKey: true,
shiftKey: true,
},
{
name: "toggle-ctrl-space",
shortcut: "Ctrl+Space",
key: "VK_SPACE",
modifiers: {
accelKey: true,
},
},
{
name: "toggle-ctrl-space",
shortcut: "Ctrl+Space",
key: "VK_SPACE",
modifiers: {
accelKey: true,
},
{
name: "toggle-ctrl-comma",
shortcut: "Ctrl+Comma",
key: "VK_COMMA",
modifiers: {
accelKey: true,
},
},
{
name: "toggle-ctrl-comma",
shortcut: "Ctrl+Comma",
key: "VK_COMMA",
modifiers: {
accelKey: true,
},
{
name: "toggle-ctrl-period",
shortcut: "Ctrl+Period",
key: "VK_PERIOD",
modifiers: {
accelKey: true,
},
},
{
name: "toggle-ctrl-period",
shortcut: "Ctrl+Period",
key: "VK_PERIOD",
modifiers: {
accelKey: true,
},
{
name: "toggle-ctrl-alt-v",
shortcut: "Ctrl+Alt+V",
key: "V",
modifiers: {
accelKey: true,
altKey: true,
},
},
{
name: "toggle-ctrl-alt-v",
shortcut: "Ctrl+Alt+V",
key: "V",
modifiers: {
accelKey: true,
altKey: true,
},
];
},
];
add_task(async function test_user_defined_commands() {
let win1 = await openNewMailWindow();
let commands = {};
@ -218,7 +218,10 @@ add_task(async function test_user_defined_commands() {
function background() {
browser.commands.onCommand.addListener((commandName, activeTab) => {
browser.test.sendMessage("oncommand", { commandName, activeTab });
browser.test.sendMessage("oncommand event received", {
commandName,
activeTab,
});
});
browser.test.sendMessage("ready");
}
@ -253,7 +256,7 @@ add_task(async function test_user_defined_commands() {
continue;
}
EventUtils.synthesizeKey(testCommand.key, testCommand.modifiers, window);
let message = await extension.awaitMessage("oncommand");
let message = await extension.awaitMessage("oncommand event received");
is(
message.commandName,
testCommand.name,
@ -309,7 +312,7 @@ add_task(async function test_user_defined_commands() {
"Expected keyset of window #3 to have the correct number of children"
);
// Confirm that the commands are registered to both windows.
// Confirm that the commands are registered to all windows.
await focusWindow(win1);
await runTest(win1, "mail");
@ -319,12 +322,221 @@ add_task(async function test_user_defined_commands() {
await focusWindow(win3);
await runTest(win3, "messageCompose");
// Mitigation for "waiting for vsync to be disabled" error.
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(r => win3.setTimeout(r, 250));
// Unload the extension and confirm that the keysets have been removed from all windows.
await extension.unload();
keyset = win1.document.getElementById(keysetID);
is(keyset, null, "Expected keyset to be removed from the window #1");
keyset = win2.document.getElementById(keysetID);
is(keyset, null, "Expected keyset to be removed from the window #2");
keyset = win3.document.getElementById(keysetID);
is(keyset, null, "Expected keyset to be removed from the window #3");
await BrowserTestUtils.closeWindow(win1);
await BrowserTestUtils.closeWindow(win2);
await BrowserTestUtils.closeWindow(win3);
SimpleTest.endMonitorConsole();
await waitForConsole;
});
add_task(async function test_commands_MV3_event_page() {
let win1 = await openNewMailWindow();
let commands = {};
let isMac = AppConstants.platform == "macosx";
let totalMacOnlyCommands = 0;
let numberNumericCommands = 4;
for (let testCommand of testCommands) {
let command = {
suggested_key: {},
};
if (testCommand.shortcut) {
command.suggested_key.default = testCommand.shortcut;
}
if (testCommand.shortcutMac) {
command.suggested_key.mac = testCommand.shortcutMac;
}
if (testCommand.shortcutMac && !testCommand.shortcut) {
totalMacOnlyCommands++;
}
if (testCommand.unrecognized_property) {
command.unrecognized_property = testCommand.unrecognized_property;
}
commands[testCommand.name] = command;
}
function background() {
// Whenever the extension starts or wakes up, the eventCounter is reset and
// allows to observe the order of events fired. In case of a wake-up, the
// first observed event is the one that woke up the background.
let eventCounter = 0;
browser.commands.onCommand.addListener(async (commandName, activeTab) => {
browser.test.sendMessage("oncommand event received", {
eventCount: ++eventCounter,
commandName,
activeTab,
});
});
browser.test.sendMessage("ready");
}
let extension = ExtensionTestUtils.loadExtension({
files: {
"background.js": background,
"utils.js": await getUtilsJS(),
},
manifest: {
manifest_version: 3,
background: { scripts: ["utils.js", "background.js"] },
browser_specific_settings: { gecko: { id: "cloudfile@mochi.test" } },
commands,
},
});
SimpleTest.waitForExplicitFinish();
let waitForConsole = new Promise(resolve => {
SimpleTest.monitorConsole(resolve, [
{
message: /Reading manifest: Warning processing commands.*.unrecognized_property: An unexpected property was found/,
},
]);
});
// Unrecognized_property in manifest triggers warning.
ExtensionTestUtils.failOnSchemaWarnings(false);
await extension.startup();
ExtensionTestUtils.failOnSchemaWarnings(true);
await extension.awaitMessage("ready");
// Check for persistent listener.
assertPersistentListeners(extension, "commands", "onCommand", {
primed: false,
});
let gEventCounter = 0;
async function runTest(window, expectedTabType) {
// The second run will terminate the background script before each keypress,
// verifying that the background script is waking up correctly.
for (let terminateBackground of [false, true]) {
for (let testCommand of testCommands) {
if (testCommand.skip && testCommand.skip.includes(expectedTabType)) {
continue;
}
if (testCommand.shortcutMac && !testCommand.shortcut && !isMac) {
continue;
}
if (terminateBackground) {
gEventCounter = 0;
}
if (terminateBackground) {
// Terminate the background and verify the primed persistent listener.
await extension.terminateBackground({
disableResetIdleForTest: true,
});
assertPersistentListeners(extension, "commands", "onCommand", {
primed: true,
});
EventUtils.synthesizeKey(
testCommand.key,
testCommand.modifiers,
window
);
// Wait for background restart.
await extension.awaitMessage("ready");
} else {
EventUtils.synthesizeKey(
testCommand.key,
testCommand.modifiers,
window
);
}
let message = await extension.awaitMessage("oncommand event received");
is(
testCommand.name,
message.commandName,
`onCommand listener should fire with the correct command name`
);
is(
expectedTabType,
message.activeTab.type,
`onCommand listener should fire with the correct tab type`
);
is(
++gEventCounter,
message.eventCount,
`Event counter should be correct`
);
}
}
}
// Create another window after the extension is loaded.
let win2 = await openNewMailWindow();
let totalTestCommands =
Object.keys(testCommands).length + numberNumericCommands;
let expectedCommandsRegistered = isMac
? totalTestCommands
: totalTestCommands - totalMacOnlyCommands;
let account = createAccount();
addIdentity(account);
let win3 = await openComposeWindow(account);
// Some key combinations do not work if the TO field has focus.
win3.document.querySelector("editor").focus();
// Confirm the keysets have been added to both windows.
let keysetID = `ext-keyset-id-${makeWidgetId(extension.id)}`;
let keyset = win1.document.getElementById(keysetID);
ok(keyset != null, "Expected keyset to exist");
is(
keyset.children.length,
expectedCommandsRegistered,
"Expected keyset of window #1 to have the correct number of children"
);
keyset = win2.document.getElementById(keysetID);
ok(keyset != null, "Expected keyset to exist");
is(
keyset.children.length,
expectedCommandsRegistered,
"Expected keyset of window #2 to have the correct number of children"
);
keyset = win3.document.getElementById(keysetID);
ok(keyset != null, "Expected keyset to exist");
is(
keyset.children.length,
expectedCommandsRegistered,
"Expected keyset of window #3 to have the correct number of children"
);
// Confirm that the commands are registered to all windows.
await focusWindow(win1);
await runTest(win1, "mail");
await focusWindow(win2);
await runTest(win2, "mail");
await focusWindow(win3);
await runTest(win3, "messageCompose");
// Unload the extension and confirm that the keysets have been removed from all windows.
await extension.unload();
// Confirm that the keysets have been removed from both windows after the extension is unloaded.
keyset = win1.document.getElementById(keysetID);
is(keyset, null, "Expected keyset to be removed from the window #1");

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

@ -11,115 +11,36 @@ const { AppConstants } = ChromeUtils.importESModule(
let account;
add_task(async () => {
add_setup(async () => {
account = createAccount();
addIdentity(account);
});
// This test clicks on the action button to open the popup.
add_task(async function test_popup_open_with_click() {
let composeWindow = await openComposeWindow(account);
await focusWindow(composeWindow);
await run_popup_test({
actionType: "compose_action",
testType: "open-with-mouse-click",
window: composeWindow,
});
await run_popup_test({
actionType: "compose_action",
testType: "open-with-mouse-click",
disable_button: true,
window: composeWindow,
});
await run_popup_test({
actionType: "compose_action",
testType: "open-with-mouse-click",
use_default_popup: true,
window: composeWindow,
});
await run_popup_test({
actionType: "compose_action",
testType: "open-with-mouse-click",
default_area: "formattoolbar",
window: composeWindow,
});
await run_popup_test({
actionType: "compose_action",
testType: "open-with-mouse-click",
default_area: "formattoolbar",
disable_button: true,
window: composeWindow,
});
await run_popup_test({
actionType: "compose_action",
testType: "open-with-mouse-click",
default_area: "formattoolbar",
use_default_popup: true,
window: composeWindow,
});
composeWindow.close();
Services.xulStore.removeDocument(
"chrome://messenger/content/messengercompose/messengercompose.xhtml"
);
});
// This test uses a command from the menus API to open the popup.
add_task(async function test_popup_open_with_menu_command() {
let composeWindow = await openComposeWindow(account);
await focusWindow(composeWindow);
await run_popup_test({
actionType: "compose_action",
testType: "open-with-menu-command",
use_default_popup: true,
default_area: "maintoolbar",
window: composeWindow,
});
for (let area of ["maintoolbar", "formattoolbar"]) {
let testConfig = {
actionType: "compose_action",
testType: "open-with-menu-command",
default_area: area,
window: composeWindow,
};
await run_popup_test({
actionType: "compose_action",
testType: "open-with-menu-command",
use_default_popup: true,
default_area: "formattoolbar",
window: composeWindow,
});
await run_popup_test({
actionType: "compose_action",
testType: "open-with-menu-command",
disable_button: true,
default_area: "maintoolbar",
window: composeWindow,
});
await run_popup_test({
actionType: "compose_action",
testType: "open-with-menu-command",
disable_button: true,
default_area: "formattoolbar",
window: composeWindow,
});
await run_popup_test({
actionType: "compose_action",
testType: "open-with-menu-command",
default_area: "maintoolbar",
window: composeWindow,
});
await run_popup_test({
actionType: "compose_action",
testType: "open-with-menu-command",
default_area: "formattoolbar",
window: composeWindow,
});
await run_popup_test({
...testConfig,
});
await run_popup_test({
...testConfig,
use_default_popup: true,
});
await run_popup_test({
...testConfig,
disable_button: true,
});
}
composeWindow.close();
});

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

@ -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"
);
const { AppConstants } = ChromeUtils.importESModule(
"resource://gre/modules/AppConstants.sys.mjs"
);
let account;
add_setup(async () => {
account = createAccount();
addIdentity(account);
});
// This test clicks on the action button to open the popup.
add_task(async function test_popup_open_with_click() {
for (let area of [null, "formattoolbar"]) {
let composeWindow = await openComposeWindow(account);
await focusWindow(composeWindow);
await run_popup_test({
actionType: "compose_action",
testType: "open-with-mouse-click",
window: composeWindow,
default_area: area,
});
await run_popup_test({
actionType: "compose_action",
testType: "open-with-mouse-click",
window: composeWindow,
default_area: area,
disable_button: true,
});
await run_popup_test({
actionType: "compose_action",
testType: "open-with-mouse-click",
window: composeWindow,
default_area: area,
use_default_popup: true,
});
composeWindow.close();
Services.xulStore.removeDocument(
"chrome://messenger/content/messengercompose/messengercompose.xhtml"
);
}
});
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);
});

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

@ -16,7 +16,8 @@ var { ExtensionSupport } = ChromeUtils.import(
"resource:///modules/ExtensionSupport.jsm"
);
addIdentity(createAccount());
let account = createAccount();
let defaultIdentity = addIdentity(account);
function findWindow(subject) {
let windows = Array.from(Services.wm.getEnumerator("msgcompose"));
@ -2112,3 +2113,161 @@ add_task(async function test_without_permission() {
await extension.awaitFinish("finished");
await extension.unload();
});
add_task(async function test_attachment_MV3_event_pages() {
let files = {
"background.js": async () => {
// Whenever the extension starts or wakes up, the eventCounter is reset and
// allows to observe the order of events fired. In case of a wake-up, the
// first observed event is the one that woke up the background.
let eventCounter = 0;
browser.compose.onAttachmentAdded.addListener(async (tab, attachment) => {
browser.test.sendMessage("attachment added", {
eventCount: ++eventCounter,
attachment,
});
});
browser.compose.onAttachmentRemoved.addListener(
async (tab, attachmentId) => {
browser.test.sendMessage("attachment removed", {
eventCount: ++eventCounter,
attachmentId,
});
}
);
browser.test.sendMessage("background started");
},
"utils.js": await getUtilsJS(),
};
let extension = ExtensionTestUtils.loadExtension({
files,
manifest: {
manifest_version: 3,
background: { scripts: ["utils.js", "background.js"] },
permissions: ["accountsRead", "compose", "messagesRead"],
browser_specific_settings: {
gecko: { id: "compose.attachment@mochi.test" },
},
},
});
async function addAttachment(ordinal) {
let attachment = Cc[
"@mozilla.org/messengercompose/attachment;1"
].createInstance(Ci.nsIMsgAttachment);
attachment.name = `${ordinal}.txt`;
attachment.url = `data:text/plain,I'm the ${ordinal} attachment!`;
attachment.size = attachment.url.length - 16;
await composeWindow.AddAttachments([attachment]);
return attachment;
}
async function removeAttachment(attachment) {
let item = composeWindow.gAttachmentBucket.findItemForAttachment(
attachment
);
await composeWindow.RemoveAttachments([item]);
}
function checkPersistentListeners({ primed }) {
// A persistent event is referenced by its moduleName as defined in
// ext-mails.json, not by its actual namespace.
const persistent_events = [
"compose.onAttachmentAdded",
"compose.onAttachmentRemoved",
];
for (let event of persistent_events) {
let [moduleName, eventName] = event.split(".");
assertPersistentListeners(extension, moduleName, eventName, {
primed,
});
}
}
let composeWindow = await openComposeWindow(account);
await focusWindow(composeWindow);
await extension.startup();
await extension.awaitMessage("background started");
// The listeners should be persistent, but not primed.
checkPersistentListeners({ primed: false });
// Trigger events without terminating the background first.
let rawFirstAttachment = await addAttachment("first");
let addedFirst = await extension.awaitMessage("attachment added");
Assert.equal(
"first.txt",
rawFirstAttachment.name,
"Created attachment should be correct"
);
Assert.equal(
"first.txt",
addedFirst.attachment.name,
"Attachment returned by onAttachmentAdded should be correct"
);
Assert.equal(1, addedFirst.eventCount, "Event counter should be correct");
await removeAttachment(rawFirstAttachment);
let removedFirst = await extension.awaitMessage("attachment removed");
Assert.equal(
addedFirst.attachment.id,
removedFirst.attachmentId,
"Attachment id returned by onAttachmentRemoved should be correct"
);
Assert.equal(2, removedFirst.eventCount, "Event counter should be correct");
// Terminate background and re-trigger onAttachmentAdded event.
await extension.terminateBackground({ disableResetIdleForTest: true });
// The listeners should be primed.
checkPersistentListeners({ primed: true });
let rawSecondAttachment = await addAttachment("second");
let addedSecond = await extension.awaitMessage("attachment added");
Assert.equal(
"second.txt",
rawSecondAttachment.name,
"Created attachment should be correct"
);
Assert.equal(
"second.txt",
addedSecond.attachment.name,
"Attachment returned by onAttachmentAdded should be correct"
);
Assert.equal(1, addedSecond.eventCount, "Event counter should be correct");
// The background should have been restarted.
await extension.awaitMessage("background started");
// The listeners should no longer be primed.
checkPersistentListeners({ primed: false });
// Terminate background and re-trigger onAttachmentRemoved event.
await extension.terminateBackground({ disableResetIdleForTest: true });
// The listeners should be primed.
checkPersistentListeners({ primed: true });
await removeAttachment(rawSecondAttachment);
let removedSecond = await extension.awaitMessage("attachment removed");
Assert.equal(
addedSecond.attachment.id,
removedSecond.attachmentId,
"Attachment id returned by onAttachmentRemoved should be correct"
);
Assert.equal(1, removedSecond.eventCount, "Event counter should be correct");
// The background should have been restarted.
await extension.awaitMessage("background started");
// The listeners should no longer be primed.
checkPersistentListeners({ primed: false });
await extension.unload();
composeWindow.close();
});

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

@ -444,6 +444,168 @@ add_task(async function testHeaders() {
await extension.unload();
});
add_task(async function test_onIdentityChanged_MV3_event_pages() {
let files = {
"background.js": async () => {
// Whenever the extension starts or wakes up, the eventCounter is reset and
// allows to observe the order of events fired. In case of a wake-up, the
// first observed event is the one that woke up the background.
let eventCounter = 0;
browser.compose.onIdentityChanged.addListener(async (tab, identityId) => {
browser.test.sendMessage("identity changed", {
eventCount: ++eventCounter,
identityId,
});
});
browser.compose.onComposeStateChanged.addListener(async (tab, state) => {
browser.test.sendMessage("compose state changed", {
eventCount: ++eventCounter,
state,
});
});
browser.test.sendMessage("background started");
},
"utils.js": await getUtilsJS(),
};
let extension = ExtensionTestUtils.loadExtension({
files,
manifest: {
manifest_version: 3,
background: { scripts: ["utils.js", "background.js"] },
permissions: ["accountsRead", "addressBooks", "compose", "messagesRead"],
browser_specific_settings: { gecko: { id: "compose@mochi.test" } },
},
});
function changeIdentity(newIdentity) {
let composeDocument = composeWindow.document;
let identityList = composeDocument.getElementById("msgIdentity");
let identityItem = identityList.querySelector(
`[identitykey="${newIdentity}"]`
);
ok(identityItem);
identityList.selectedItem = identityItem;
composeWindow.LoadIdentity(false);
}
function setToAddr(to) {
composeWindow.SetComposeDetails({ to });
}
function checkPersistentListeners({ primed }) {
// A persistent event is referenced by its moduleName as defined in
// ext-mails.json, not by its actual namespace.
const persistent_events = [
"compose.onIdentityChanged",
"compose.onComposeStateChanged",
];
for (let event of persistent_events) {
let [moduleName, eventName] = event.split(".");
assertPersistentListeners(extension, moduleName, eventName, {
primed,
});
}
}
let composeWindow = await openComposeWindow(account);
await focusWindow(composeWindow);
await extension.startup();
await extension.awaitMessage("background started");
// The listeners should be persistent, but not primed.
checkPersistentListeners({ primed: false });
// Trigger events without terminating the background first.
changeIdentity(nonDefaultIdentity.key);
{
let rv = await extension.awaitMessage("identity changed");
Assert.deepEqual(
{
eventCount: 1,
identityId: nonDefaultIdentity.key,
},
rv,
"The non-primed onIdentityChanged event should return the correct values"
);
}
setToAddr("user@invalid.net");
{
let rv = await extension.awaitMessage("compose state changed");
Assert.deepEqual(
{
eventCount: 2,
state: {
canSendNow: true,
canSendLater: true,
},
},
rv,
"The non-primed onComposeStateChanged should return the correct values"
);
}
// Terminate background and re-trigger onIdentityChanged event.
await extension.terminateBackground({ disableResetIdleForTest: true });
// The listeners should be primed.
checkPersistentListeners({ primed: true });
changeIdentity(defaultIdentity.key);
{
let rv = await extension.awaitMessage("identity changed");
Assert.deepEqual(
{
eventCount: 1,
identityId: defaultIdentity.key,
},
rv,
"The primed onIdentityChanged event should return the correct values"
);
}
// The background should have been restarted.
await extension.awaitMessage("background started");
// The listeners should no longer be primed.
checkPersistentListeners({ primed: false });
// Terminate background and re-trigger onComposeStateChanged event.
await extension.terminateBackground({ disableResetIdleForTest: true });
// The listeners should be primed.
checkPersistentListeners({ primed: true });
setToAddr("invalid");
{
let rv = await extension.awaitMessage("compose state changed");
Assert.deepEqual(
{
eventCount: 1,
state: {
canSendNow: false,
canSendLater: false,
},
},
rv,
"The primed onComposeStateChanged should return the correct values"
);
}
// The background should have been restarted.
await extension.awaitMessage("background started");
// The listeners should no longer be primed.
checkPersistentListeners({ primed: false });
await extension.unload();
composeWindow.close();
});
add_task(async function testCustomHeaders() {
let files = {
"background.js": async () => {

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

@ -5,7 +5,7 @@
let account = createAccount();
let defaultIdentity = addIdentity(account);
add_task(async function testDictionaries() {
add_task(async function test_dictionaries() {
let files = {
"background.js": async () => {
function verifyDictionaries(dictionaries, expected) {
@ -103,3 +103,112 @@ add_task(async function testDictionaries() {
await extension.awaitFinish("finished");
await extension.unload();
});
add_task(async function test_onActiveDictionariesChanged_MV3_event_pages() {
let files = {
"background.js": async () => {
// Whenever the extension starts or wakes up, hasFired is set to false. In
// case of a wake-up, the first fired event is the one that woke up the background.
let hasFired = false;
browser.compose.onActiveDictionariesChanged.addListener(
async (tab, dictionaries) => {
// Only send the first event after background wake-up, this should be
// the only one expected.
if (!hasFired) {
hasFired = true;
browser.test.sendMessage(
"onActiveDictionariesChanged received",
dictionaries
);
}
}
);
browser.test.sendMessage("background started");
},
"utils.js": await getUtilsJS(),
};
let extension = ExtensionTestUtils.loadExtension({
files,
manifest: {
manifest_version: 3,
background: { scripts: ["utils.js", "background.js"] },
permissions: ["compose"],
browser_specific_settings: {
gecko: { id: "compose.dictionary@xpcshell.test" },
},
},
});
function checkPersistentListeners({ primed }) {
// A persistent event is referenced by its moduleName as defined in
// ext-mails.json, not by its actual namespace.
const persistent_events = ["compose.onActiveDictionariesChanged"];
for (let event of persistent_events) {
let [moduleName, eventName] = event.split(".");
assertPersistentListeners(extension, moduleName, eventName, {
primed,
});
}
}
async function setActiveDictionaries(activeDictionaries) {
let installedDictionaries = Cc["@mozilla.org/spellchecker/engine;1"]
.getService(Ci.mozISpellCheckingEngine)
.getDictionaryList();
for (let dict of activeDictionaries) {
if (!installedDictionaries.includes(dict)) {
throw new Error(`Dictionary not found: ${dict}`);
}
}
await composeWindow.ComposeChangeLanguage(activeDictionaries);
}
let composeWindow = await openComposeWindow(account);
await focusWindow(composeWindow);
await extension.startup();
await extension.awaitMessage("background started");
// The listeners should be persistent, but not primed.
checkPersistentListeners({ primed: false });
// Trigger onActiveDictionariesChanged without terminating the background first.
setActiveDictionaries(["en-US"]);
let newActiveDictionary1 = await extension.awaitMessage(
"onActiveDictionariesChanged received"
);
Assert.equal(
newActiveDictionary1["en-US"],
true,
"Returned active dictionary should be correct"
);
// Terminate background and re-trigger onActiveDictionariesChanged.
await extension.terminateBackground({ disableResetIdleForTest: true });
// The listeners should be primed.
checkPersistentListeners({ primed: true });
setActiveDictionaries([]);
let newActiveDictionary2 = await extension.awaitMessage(
"onActiveDictionariesChanged received"
);
Assert.equal(
newActiveDictionary2["en-US"],
false,
"Returned active dictionary should be correct"
);
// The background should have been restarted.
await extension.awaitMessage("background started");
// The listener should no longer be primed.
checkPersistentListeners({ primed: false });
await extension.unload();
composeWindow.close();
});

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

@ -906,3 +906,105 @@ add_task(async function testMultipleListeners() {
);
});
});
add_task(async function test_MV3_event_pages() {
let files = {
"background.js": async () => {
// Whenever the extension starts or wakes up, hasFired is set to false. In
// case of a wake-up, the first fired event is the one that woke up the background.
let hasFired = false;
browser.compose.onBeforeSend.addListener((tab, details) => {
// Only send the first event after background wake-up, this should be
// the only one expected.
if (!hasFired) {
hasFired = true;
browser.test.sendMessage("onBeforeSend received", details);
}
// Let us abort, so we do not have to re-open the compose window for
// multiple tests.
return {
cancel: true,
};
});
browser.test.sendMessage("background started");
},
"utils.js": await getUtilsJS(),
};
let extension = ExtensionTestUtils.loadExtension({
files,
manifest: {
manifest_version: 3,
background: { scripts: ["utils.js", "background.js"] },
permissions: ["compose"],
browser_specific_settings: {
gecko: { id: "compose.onBeforeSend@xpcshell.test" },
},
},
});
function checkPersistentListeners({ primed }) {
// A persistent event is referenced by its moduleName as defined in
// ext-mails.json, not by its actual namespace.
const persistent_events = ["compose.onBeforeSend"];
for (let event of persistent_events) {
let [moduleName, eventName] = event.split(".");
assertPersistentListeners(extension, moduleName, eventName, {
primed,
});
}
}
function beginSend() {
composeWindow.GenericSendMessage(Ci.nsIMsgCompDeliverMode.Now).catch(() => {
// This test is ignoring errors thrown by GenericSendMessage, but looks
// at didTryToSendMessage of the mocked CompleteGenericSendMessage to
// check if onBeforeSend aborted the send process.
});
}
let composeWindow = await openComposeWindow(account);
await focusWindow(composeWindow);
await extension.startup();
await extension.awaitMessage("background started");
// The listeners should be persistent, but not primed.
checkPersistentListeners({ primed: false });
// Trigger onBeforeSend without terminating the background first.
composeWindow.SetComposeDetails({ to: "first@invalid.net" });
beginSend();
let firstDetails = await extension.awaitMessage("onBeforeSend received");
Assert.equal(
"first@invalid.net",
firstDetails.to,
"Returned details should be correct"
);
// Terminate background and re-trigger onBeforeSend.
await extension.terminateBackground({ disableResetIdleForTest: true });
// The listeners should be primed.
checkPersistentListeners({ primed: true });
composeWindow.SetComposeDetails({ to: "second@invalid.net" });
beginSend();
let secondDetails = await extension.awaitMessage("onBeforeSend received");
Assert.equal(
"second@invalid.net",
secondDetails.to,
"Returned details should be correct"
);
// The background should have been restarted.
await extension.awaitMessage("background started");
// The listener should no longer be primed.
checkPersistentListeners({ primed: false });
await extension.unload();
composeWindow.close();
});

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

@ -61,25 +61,27 @@ function getSmtpIdentity(senderName, smtpServer) {
var gServer;
var gLocalRootFolder;
let gPopAccount;
let gLocalAccount;
add_setup(() => {
gServer = setupServerDaemon();
gServer.start();
// Test needs a non-local default account to be able to send messages.
let popAccount = createAccount("pop3");
let localAccount = createAccount("local");
MailServices.accounts.defaultAccount = popAccount;
gPopAccount = createAccount("pop3");
gLocalAccount = createAccount("local");
MailServices.accounts.defaultAccount = gPopAccount;
let identity = getSmtpIdentity(
"identity@foo.invalid",
getBasicSmtpServer(gServer.port)
);
popAccount.addIdentity(identity);
popAccount.defaultIdentity = identity;
gPopAccount.addIdentity(identity);
gPopAccount.defaultIdentity = identity;
// Test is using the Sent folder and Outbox folder of the local account.
gLocalRootFolder = localAccount.incomingServer.rootFolder;
gLocalRootFolder = gLocalAccount.incomingServer.rootFolder;
gLocalRootFolder.createSubfolder("Sent", null);
gLocalRootFolder.createSubfolder("Drafts", null);
gLocalRootFolder.createSubfolder("Fcc", null);
@ -323,3 +325,92 @@ add_task(async function test_saveAsDraft_with_additional_fcc() {
},
});
});
// Test onAfterSave when saving drafts for MV3
add_task(async function test_onAfterSave_MV3_event_pages() {
let files = {
"background.js": async () => {
// Whenever the extension starts or wakes up, hasFired is set to false. In
// case of a wake-up, the first fired event is the one that woke up the background.
let hasFired = false;
browser.compose.onAfterSave.addListener((tab, saveInfo) => {
// Only send the first event after background wake-up, this should be
// the only one expected.
if (!hasFired) {
hasFired = true;
browser.test.sendMessage("onAfterSave received", saveInfo);
}
});
browser.test.sendMessage("background started");
},
"utils.js": await getUtilsJS(),
};
let extension = ExtensionTestUtils.loadExtension({
files,
manifest: {
manifest_version: 3,
background: { scripts: ["utils.js", "background.js"] },
permissions: ["compose"],
browser_specific_settings: {
gecko: { id: "compose.onAfterSave@xpcshell.test" },
},
},
});
function checkPersistentListeners({ primed }) {
// A persistent event is referenced by its moduleName as defined in
// ext-mails.json, not by its actual namespace.
const persistent_events = ["compose.onAfterSave"];
for (let event of persistent_events) {
let [moduleName, eventName] = event.split(".");
assertPersistentListeners(extension, moduleName, eventName, {
primed,
});
}
}
let composeWindow = await openComposeWindow(gPopAccount);
await focusWindow(composeWindow);
await extension.startup();
await extension.awaitMessage("background started");
// The listeners should be persistent, but not primed.
checkPersistentListeners({ primed: false });
// Trigger onAfterSave without terminating the background first.
composeWindow.SetComposeDetails({ to: "first@invalid.net" });
composeWindow.SaveAsDraft();
let firstSaveInfo = await extension.awaitMessage("onAfterSave received");
Assert.equal(
"draft",
firstSaveInfo.mode,
"Returned SaveInfo should be correct"
);
// Terminate background and re-trigger onAfterSave.
await extension.terminateBackground({ disableResetIdleForTest: true });
// The listeners should be primed.
checkPersistentListeners({ primed: true });
composeWindow.SetComposeDetails({ to: "second@invalid.net" });
composeWindow.SaveAsDraft();
let secondSaveInfo = await extension.awaitMessage("onAfterSave received");
Assert.equal(
"draft",
secondSaveInfo.mode,
"Returned SaveInfo should be correct"
);
// The background should have been restarted.
await extension.awaitMessage("background started");
// The listener should no longer be primed.
checkPersistentListeners({ primed: false });
await extension.unload();
composeWindow.close();
});

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

@ -61,25 +61,27 @@ function getSmtpIdentity(senderName, smtpServer) {
var gServer;
var gLocalRootFolder;
let gPopAccount;
let gLocalAccount;
add_setup(() => {
gServer = setupServerDaemon();
gServer.start();
// Test needs a non-local default account to be able to send messages.
let popAccount = createAccount("pop3");
let localAccount = createAccount("local");
MailServices.accounts.defaultAccount = popAccount;
gPopAccount = createAccount("pop3");
gLocalAccount = createAccount("local");
MailServices.accounts.defaultAccount = gPopAccount;
let identity = getSmtpIdentity(
"identity@foo.invalid",
getBasicSmtpServer(gServer.port)
);
popAccount.addIdentity(identity);
popAccount.defaultIdentity = identity;
gPopAccount.addIdentity(identity);
gPopAccount.defaultIdentity = identity;
// Test is using the Sent folder and Outbox folder of the local account.
gLocalRootFolder = localAccount.incomingServer.rootFolder;
gLocalRootFolder = gLocalAccount.incomingServer.rootFolder;
gLocalRootFolder.createSubfolder("Sent", null);
gLocalRootFolder.createSubfolder("Templates", null);
gLocalRootFolder.createSubfolder("Fcc", null);
@ -339,3 +341,92 @@ add_task(async function test_saveAsTemplate_with_additional_fcc() {
},
});
});
// Test onAfterSave when saving templates for MV3
add_task(async function test_onAfterSave_MV3_event_pages() {
let files = {
"background.js": async () => {
// Whenever the extension starts or wakes up, hasFired is set to false. In
// case of a wake-up, the first fired event is the one that woke up the background.
let hasFired = false;
browser.compose.onAfterSave.addListener((tab, saveInfo) => {
// Only send the first event after background wake-up, this should be
// the only one expected.
if (!hasFired) {
hasFired = true;
browser.test.sendMessage("onAfterSave received", saveInfo);
}
});
browser.test.sendMessage("background started");
},
"utils.js": await getUtilsJS(),
};
let extension = ExtensionTestUtils.loadExtension({
files,
manifest: {
manifest_version: 3,
background: { scripts: ["utils.js", "background.js"] },
permissions: ["compose"],
browser_specific_settings: {
gecko: { id: "compose.onAfterSave@xpcshell.test" },
},
},
});
function checkPersistentListeners({ primed }) {
// A persistent event is referenced by its moduleName as defined in
// ext-mails.json, not by its actual namespace.
const persistent_events = ["compose.onAfterSave"];
for (let event of persistent_events) {
let [moduleName, eventName] = event.split(".");
assertPersistentListeners(extension, moduleName, eventName, {
primed,
});
}
}
let composeWindow = await openComposeWindow(gPopAccount);
await focusWindow(composeWindow);
await extension.startup();
await extension.awaitMessage("background started");
// The listeners should be persistent, but not primed.
checkPersistentListeners({ primed: false });
// Trigger onAfterSave without terminating the background first.
composeWindow.SetComposeDetails({ to: "first@invalid.net" });
composeWindow.SaveAsTemplate();
let firstSaveInfo = await extension.awaitMessage("onAfterSave received");
Assert.equal(
"template",
firstSaveInfo.mode,
"Returned SaveInfo should be correct"
);
// Terminate background and re-trigger onAfterSave.
await extension.terminateBackground({ disableResetIdleForTest: true });
// The listeners should be primed.
checkPersistentListeners({ primed: true });
composeWindow.SetComposeDetails({ to: "second@invalid.net" });
composeWindow.SaveAsTemplate();
let secondSaveInfo = await extension.awaitMessage("onAfterSave received");
Assert.equal(
"template",
secondSaveInfo.mode,
"Returned SaveInfo should be correct"
);
// The background should have been restarted.
await extension.awaitMessage("background started");
// The listener should no longer be primed.
checkPersistentListeners({ primed: false });
await extension.unload();
composeWindow.close();
});

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

@ -68,25 +68,27 @@ function tracksentMessages(aSubject, aTopic, aMsgID) {
var gServer;
var gOutbox;
var gSentMessages = [];
let gPopAccount;
let gLocalAccount;
add_setup(() => {
gServer = setupServerDaemon();
gServer.start();
// Test needs a non-local default account to be able to send messages.
let popAccount = createAccount("pop3");
let localAccount = createAccount("local");
MailServices.accounts.defaultAccount = popAccount;
gPopAccount = createAccount("pop3");
gLocalAccount = createAccount("local");
MailServices.accounts.defaultAccount = gPopAccount;
let identity = getSmtpIdentity(
"identity@foo.invalid",
getBasicSmtpServer(gServer.port)
);
popAccount.addIdentity(identity);
popAccount.defaultIdentity = identity;
gPopAccount.addIdentity(identity);
gPopAccount.defaultIdentity = identity;
// Test is using the Sent folder and Outbox folder of the local account.
let rootFolder = localAccount.incomingServer.rootFolder;
let rootFolder = gLocalAccount.incomingServer.rootFolder;
rootFolder.createSubfolder("Sent", null);
MailServices.accounts.setSpecialFolders();
gOutbox = rootFolder.getChildNamed("Outbox");
@ -639,3 +641,93 @@ add_task(async function test_onComposeStateChanged() {
await extension.awaitFinish("finished");
await extension.unload();
});
// Test onAfterSend for MV3
add_task(async function test_onAfterSend_MV3_event_pages() {
let files = {
"background.js": async () => {
// Whenever the extension starts or wakes up, hasFired is set to false. In
// case of a wake-up, the first fired event is the one that woke up the background.
let hasFired = false;
browser.compose.onAfterSend.addListener(async (tab, sendInfo) => {
// Only send the first event after background wake-up, this should be
// the only one expected.
if (!hasFired) {
hasFired = true;
browser.test.sendMessage("onAfterSend received", sendInfo);
}
});
browser.test.sendMessage("background started");
},
"utils.js": await getUtilsJS(),
};
let extension = ExtensionTestUtils.loadExtension({
files,
manifest: {
manifest_version: 3,
background: { scripts: ["utils.js", "background.js"] },
permissions: ["compose"],
browser_specific_settings: {
gecko: { id: "compose.onAfterSend@xpcshell.test" },
},
},
});
function checkPersistentListeners({ primed }) {
// A persistent event is referenced by its moduleName as defined in
// ext-mails.json, not by its actual namespace.
const persistent_events = ["compose.onAfterSend"];
for (let event of persistent_events) {
let [moduleName, eventName] = event.split(".");
assertPersistentListeners(extension, moduleName, eventName, {
primed,
});
}
}
await extension.startup();
await extension.awaitMessage("background started");
// The listeners should be persistent, but not primed.
checkPersistentListeners({ primed: false });
// Trigger onAfterSend without terminating the background first.
let firstComposeWindow = await openComposeWindow(gPopAccount);
await focusWindow(firstComposeWindow);
firstComposeWindow.SetComposeDetails({ to: "first@invalid.net" });
firstComposeWindow.SetComposeDetails({ subject: "First message" });
firstComposeWindow.SendMessage();
let firstSaveInfo = await extension.awaitMessage("onAfterSend received");
Assert.equal(
"sendNow",
firstSaveInfo.mode,
"Returned SaveInfo should be correct"
);
// Terminate background and re-trigger onAfterSend.
await extension.terminateBackground({ disableResetIdleForTest: true });
// The listeners should be primed.
checkPersistentListeners({ primed: true });
let secondComposeWindow = await openComposeWindow(gPopAccount);
await focusWindow(secondComposeWindow);
secondComposeWindow.SetComposeDetails({ to: "second@invalid.net" });
secondComposeWindow.SetComposeDetails({ subject: "Second message" });
secondComposeWindow.SendMessage();
let secondSaveInfo = await extension.awaitMessage("onAfterSend received");
Assert.equal(
"sendNow",
secondSaveInfo.mode,
"Returned SaveInfo should be correct"
);
// The background should have been restarted.
await extension.awaitMessage("background started");
// The listener should no longer be primed.
checkPersistentListeners({ primed: false });
await extension.unload();
});

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

@ -4,7 +4,7 @@
let account, rootFolder, subFolders;
add_setup(async function() {
add_setup(async () => {
account = createAccount();
rootFolder = account.incomingServer.rootFolder;
rootFolder.createSubfolder("test1", null);
@ -915,3 +915,136 @@ add_task(async function test_setSelectedMessages() {
tabmail.closeOtherTabs(tabmail.tabModes.folder.tabs[0]);
window.gFolderTreeView.selectFolder(rootFolder);
});
add_task(async function test_MV3_event_pages() {
let files = {
"background.js": async () => {
// Whenever the extension starts or wakes up, hasFired is set to false. In
// case of a wake-up, the first fired event is the one that woke up the background.
let hasFired = false;
for (let eventName of [
"onDisplayedFolderChanged",
"onSelectedMessagesChanged",
]) {
browser.mailTabs[eventName].addListener((...args) => {
// Only send the first event after background wake-up, this should be
// the only one expected.
if (!hasFired) {
hasFired = true;
browser.test.sendMessage(`${eventName} received`, args);
}
});
}
browser.test.sendMessage("background started");
},
"utils.js": await getUtilsJS(),
};
let extension = ExtensionTestUtils.loadExtension({
files,
manifest: {
manifest_version: 3,
background: { scripts: ["utils.js", "background.js"] },
permissions: ["accountsRead", "messagesRead"],
browser_specific_settings: {
gecko: { id: "mailtabs@mochi.test" },
},
},
});
function checkPersistentListeners({ primed }) {
// A persistent event is referenced by its moduleName as defined in
// ext-mails.json, not by its actual namespace.
const persistent_events = [
"mailTabs.onDisplayedFolderChanged",
"mailTabs.onSelectedMessagesChanged",
];
for (let event of persistent_events) {
let [moduleName, eventName] = event.split(".");
assertPersistentListeners(extension, moduleName, eventName, {
primed,
});
}
}
await extension.startup();
await extension.awaitMessage("background started");
// The listeners should be persistent, but not primed.
checkPersistentListeners({ primed: false });
await extension.terminateBackground({ disableResetIdleForTest: true });
// Verify the primed persistent listeners.
checkPersistentListeners({ primed: true });
// Select a folder.
{
window.gFolderTreeView.selectFolder(subFolders.test1);
let displayInfo = await extension.awaitMessage(
"onDisplayedFolderChanged received"
);
Assert.deepEqual(
[
{
active: true,
type: "mail",
},
{ name: "test1", path: "/test1" },
],
[
{
active: displayInfo[0].active,
type: displayInfo[0].type,
},
{ name: displayInfo[1].name, path: displayInfo[1].path },
],
"The primed onDisplayedFolderChanged event should return the correct values"
);
await extension.awaitMessage("background started");
// The listeners should be persistent, but not primed.
checkPersistentListeners({ primed: false });
await extension.terminateBackground({ disableResetIdleForTest: true });
// Verify the primed persistent listeners.
checkPersistentListeners({ primed: true });
}
// Select multiple messages.
{
let messages = [...subFolders.test1.messages].slice(0, 5);
window.gFolderDisplay.selectMessages(messages);
let displayInfo = await extension.awaitMessage(
"onSelectedMessagesChanged received"
);
Assert.deepEqual(
[
"Big Meeting Today",
"Small Party Tomorrow",
"Huge Shindig Yesterday",
"Tiny Wedding In a Fortnight",
"Red Document Needs Attention",
],
displayInfo[1].messages.map(e => e.subject),
"The primed onSelectedMessagesChanged event should return the correct values"
);
Assert.deepEqual(
{
active: true,
type: "mail",
},
{
active: displayInfo[0].active,
type: displayInfo[0].type,
},
"The primed onSelectedMessagesChanged event should return the correct values"
);
await extension.awaitMessage("background started");
// The listeners should be persistent, but not primed.
checkPersistentListeners({ primed: false });
}
await extension.unload();
});

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

@ -266,7 +266,7 @@ function getExtensionDetails(...permissions) {
};
}
add_setup(async function() {
add_setup(async () => {
await Services.search.init();
gAccount = createAccount();

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

@ -159,7 +159,7 @@ function getExtensionDetails(...permissions) {
};
}
add_task(async function set_up() {
add_setup(async () => {
await Services.search.init();
gAccount = createAccount();

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

@ -717,27 +717,27 @@ add_task(async function testOpenMessagesInDefault() {
let promisedTabs = [];
promisedTabs.push(
browser.messageDisplay.open({
headerMessageId: messages1[0].headerMessageId,
messageId: messages1[0].id,
})
);
promisedTabs.push(
browser.messageDisplay.open({
headerMessageId: messages1[1].headerMessageId,
messageId: messages1[1].id,
})
);
promisedTabs.push(
browser.messageDisplay.open({
headerMessageId: messages1[2].headerMessageId,
messageId: messages1[2].id,
})
);
promisedTabs.push(
browser.messageDisplay.open({
headerMessageId: messages1[3].headerMessageId,
messageId: messages1[3].id,
})
);
promisedTabs.push(
browser.messageDisplay.open({
headerMessageId: messages1[4].headerMessageId,
messageId: messages1[4].id,
})
);
let openedTabs = await Promise.allSettled(promisedTabs);
@ -772,3 +772,342 @@ add_task(async function testOpenMessagesInDefault() {
await extension.awaitFinish();
await extension.unload();
});
add_task(async function test_MV3_event_pages_onMessageDisplayed() {
let files = {
"background.js": async () => {
// Whenever the extension starts or wakes up, hasFired is set to false. In
// case of a wake-up, the first fired event is the one that woke up the background.
let hasFired = false;
browser.messageDisplay.onMessageDisplayed.addListener((tab, message) => {
// Only send the first event after background wake-up, this should be
// the only one expected.
if (!hasFired) {
hasFired = true;
browser.test.sendMessage("onMessageDisplayed received", {
tab,
message,
});
}
});
browser.test.sendMessage("background started");
},
"utils.js": await getUtilsJS(),
};
let extension = ExtensionTestUtils.loadExtension({
files,
manifest: {
manifest_version: 3,
background: { scripts: ["utils.js", "background.js"] },
permissions: ["accountsRead", "messagesRead"],
browser_specific_settings: {
gecko: { id: "onMessageDisplayed@mochi.test" },
},
},
});
function checkPersistentListeners({ primed }) {
// A persistent event is referenced by its moduleName as defined in
// ext-mails.json, not by its actual namespace.
const persistent_events = ["messageDisplay.onMessageDisplayed"];
for (let event of persistent_events) {
let [moduleName, eventName] = event.split(".");
assertPersistentListeners(extension, moduleName, eventName, {
primed,
});
}
}
await extension.startup();
await extension.awaitMessage("background started");
// The listeners should be persistent, but not primed.
checkPersistentListeners({ primed: false });
await extension.terminateBackground({ disableResetIdleForTest: true });
// Verify the primed persistent listeners.
checkPersistentListeners({ primed: true });
// Select a message.
{
window.gFolderTreeView.selectFolder(gFolder);
window.gFolderDisplay.selectMessages([gMessages[2]]);
let displayInfo = await extension.awaitMessage(
"onMessageDisplayed received"
);
Assert.equal(
displayInfo.message.subject,
"Huge Shindig Yesterday",
"The primed onMessageDisplayed event should return the correct message."
);
Assert.deepEqual(
{
active: true,
type: "mail",
},
{
active: displayInfo.tab.active,
type: displayInfo.tab.type,
},
"The primed onMessageDisplayed event should return the correct values"
);
await extension.awaitMessage("background started");
// The listeners should be persistent, but not primed.
checkPersistentListeners({ primed: false });
}
await extension.terminateBackground({ disableResetIdleForTest: true });
// Verify the primed persistent listeners.
checkPersistentListeners({ primed: true });
// Open a message in a window.
{
let messageWindow = await openMessageInWindow(gMessages[0]);
let displayInfo = await extension.awaitMessage(
"onMessageDisplayed received"
);
Assert.equal(
displayInfo.message.subject,
"Big Meeting Today",
"The primed onMessageDisplayed event should return the correct message."
);
Assert.deepEqual(
{
active: true,
type: "messageDisplay",
},
{
active: displayInfo.tab.active,
type: displayInfo.tab.type,
},
"The primed onMessageDisplayed event should return the correct values"
);
await extension.awaitMessage("background started");
// The listeners should be persistent, but not primed.
checkPersistentListeners({ primed: false });
messageWindow.close();
}
await extension.terminateBackground({ disableResetIdleForTest: true });
// Verify the primed persistent listeners.
checkPersistentListeners({ primed: true });
// Open a message in a tab.
{
await openMessageInTab(gMessages[1]);
let displayInfo = await extension.awaitMessage(
"onMessageDisplayed received"
);
Assert.equal(
displayInfo.message.subject,
"Small Party Tomorrow",
"The primed onMessageDisplayed event should return the correct message."
);
Assert.deepEqual(
{
active: true,
type: "messageDisplay",
},
{
active: displayInfo.tab.active,
type: displayInfo.tab.type,
},
"The primed onMessageDisplayed event should return the correct values"
);
await extension.awaitMessage("background started");
// The listeners should be persistent, but not primed.
checkPersistentListeners({ primed: false });
document.getElementById("tabmail").closeTab();
}
await extension.unload();
});
add_task(async function test_MV3_event_pages_onMessagesDisplayed() {
let files = {
"background.js": async () => {
// Whenever the extension starts or wakes up, hasFired is set to false. In
// case of a wake-up, the first fired event is the one that woke up the background.
let hasFired = false;
browser.messageDisplay.onMessagesDisplayed.addListener(
(tab, messages) => {
// Only send the first event after background wake-up, this should be
// the only one expected.
if (!hasFired) {
hasFired = true;
browser.test.sendMessage("onMessagesDisplayed received", {
tab,
messages,
});
}
}
);
browser.test.sendMessage("background started");
},
"utils.js": await getUtilsJS(),
};
let extension = ExtensionTestUtils.loadExtension({
files,
manifest: {
manifest_version: 3,
background: { scripts: ["utils.js", "background.js"] },
permissions: ["accountsRead", "messagesRead"],
browser_specific_settings: {
gecko: { id: "onMessagesDisplayed@mochi.test" },
},
},
});
function checkPersistentListeners({ primed }) {
// A persistent event is referenced by its moduleName as defined in
// ext-mails.json, not by its actual namespace.
const persistent_events = ["messageDisplay.onMessagesDisplayed"];
for (let event of persistent_events) {
let [moduleName, eventName] = event.split(".");
assertPersistentListeners(extension, moduleName, eventName, {
primed,
});
}
}
await extension.startup();
await extension.awaitMessage("background started");
// The listeners should be persistent, but not primed.
checkPersistentListeners({ primed: false });
await extension.terminateBackground({ disableResetIdleForTest: true });
// Verify the primed persistent listeners.
checkPersistentListeners({ primed: true });
// Select multiple messages.
{
window.gFolderTreeView.selectFolder(gFolder);
window.gFolderDisplay.selectMessages(gMessages);
let displayInfo = await extension.awaitMessage(
"onMessagesDisplayed received"
);
Assert.equal(
displayInfo.messages.length,
5,
"The primed onMessagesDisplayed event should return the correct number of messages."
);
Assert.deepEqual(
[
"Big Meeting Today",
"Small Party Tomorrow",
"Huge Shindig Yesterday",
"Tiny Wedding In a Fortnight",
"Red Document Needs Attention",
],
displayInfo.messages.map(e => e.subject),
"The primed onMessagesDisplayed event should return the correct messages."
);
Assert.deepEqual(
{
active: true,
type: "mail",
},
{
active: displayInfo.tab.active,
type: displayInfo.tab.type,
},
"The primed onMessagesDisplayed event should return the correct values"
);
await extension.awaitMessage("background started");
// The listeners should be persistent, but not primed.
checkPersistentListeners({ primed: false });
}
await extension.terminateBackground({ disableResetIdleForTest: true });
// Verify the primed persistent listeners.
checkPersistentListeners({ primed: true });
// Open a message in a window.
{
let messageWindow = await openMessageInWindow(gMessages[0]);
let displayInfo = await extension.awaitMessage(
"onMessagesDisplayed received"
);
Assert.equal(
displayInfo.messages.length,
1,
"The primed onMessagesDisplayed event should return the correct number of messages."
);
Assert.equal(
displayInfo.messages[0].subject,
"Big Meeting Today",
"The primed onMessagesDisplayed event should return the correct message."
);
Assert.deepEqual(
{
active: true,
type: "messageDisplay",
},
{
active: displayInfo.tab.active,
type: displayInfo.tab.type,
},
"The primed onMessagesDisplayed event should return the correct values"
);
await extension.awaitMessage("background started");
// The listeners should be persistent, but not primed.
checkPersistentListeners({ primed: false });
messageWindow.close();
}
await extension.terminateBackground({ disableResetIdleForTest: true });
// Verify the primed persistent listeners.
checkPersistentListeners({ primed: true });
// Open a message in a tab.
{
await openMessageInTab(gMessages[1]);
let displayInfo = await extension.awaitMessage(
"onMessagesDisplayed received"
);
Assert.equal(
displayInfo.messages.length,
1,
"The primed onMessagesDisplayed event should return the correct number of messages."
);
Assert.equal(
displayInfo.messages[0].subject,
"Small Party Tomorrow",
"The primed onMessagesDisplayed event should return the correct message."
);
Assert.deepEqual(
{
active: true,
type: "messageDisplay",
},
{
active: displayInfo.tab.active,
type: displayInfo.tab.type,
},
"The primed onMessagesDisplayed event should return the correct values"
);
await extension.awaitMessage("background started");
// The listeners should be persistent, but not primed.
checkPersistentListeners({ primed: false });
document.getElementById("tabmail").closeTab();
}
await extension.unload();
});

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

@ -9,7 +9,7 @@ const { AddonManager } = ChromeUtils.import(
let account;
let messages;
add_task(async () => {
add_setup(async () => {
account = createAccount();
let rootFolder = account.incomingServer.rootFolder;
let subFolders = rootFolder.subFolders;
@ -32,132 +32,76 @@ add_task(async () => {
await BrowserTestUtils.browserLoaded(window.getMessagePaneBrowser());
});
// This test clicks on the action button to open the popup.
add_task(async function test_popup_open_with_click() {
info("3-pane tab");
await run_popup_test({
actionType: "message_display_action",
testType: "open-with-mouse-click",
window,
});
await run_popup_test({
actionType: "message_display_action",
testType: "open-with-mouse-click",
disable_button: true,
window,
});
await run_popup_test({
actionType: "message_display_action",
testType: "open-with-mouse-click",
use_default_popup: true,
window,
});
info("Message tab");
await openMessageInTab(messages.getNext());
await run_popup_test({
actionType: "message_display_action",
testType: "open-with-mouse-click",
window,
});
await run_popup_test({
actionType: "message_display_action",
testType: "open-with-mouse-click",
disable_button: true,
window,
});
await run_popup_test({
actionType: "message_display_action",
testType: "open-with-mouse-click",
use_default_popup: true,
window,
});
document.getElementById("tabmail").closeTab();
info("Message window");
let messageWindow = await openMessageInWindow(messages.getNext());
await run_popup_test({
actionType: "message_display_action",
testType: "open-with-mouse-click",
window: messageWindow,
});
await run_popup_test({
actionType: "message_display_action",
testType: "open-with-mouse-click",
disable_button: true,
window: messageWindow,
});
await run_popup_test({
actionType: "message_display_action",
testType: "open-with-mouse-click",
use_default_popup: true,
window: messageWindow,
});
messageWindow.close();
});
// This test uses a command from the menus API to open the popup.
add_task(async function test_popup_open_with_menu_command() {
info("3-pane tab");
await run_popup_test({
actionType: "message_display_action",
testType: "open-with-menu-command",
window,
});
await run_popup_test({
actionType: "message_display_action",
testType: "open-with-menu-command",
use_default_popup: true,
window,
});
await run_popup_test({
actionType: "message_display_action",
testType: "open-with-menu-command",
disable_button: true,
window,
});
{
let testConfig = {
actionType: "message_display_action",
testType: "open-with-menu-command",
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 tab");
await openMessageInTab(messages.getNext());
await run_popup_test({
actionType: "message_display_action",
testType: "open-with-menu-command",
window,
});
await run_popup_test({
actionType: "message_display_action",
testType: "open-with-menu-command",
use_default_popup: true,
window,
});
await run_popup_test({
actionType: "message_display_action",
testType: "open-with-menu-command",
disable_button: true,
window,
});
document.getElementById("tabmail").closeTab();
{
await openMessageInTab(messages.getNext());
let testConfig = {
actionType: "message_display_action",
testType: "open-with-menu-command",
window,
};
await run_popup_test({
...testConfig,
});
await run_popup_test({
...testConfig,
use_default_popup: true,
});
await run_popup_test({
...testConfig,
disable_button: true,
});
document.getElementById("tabmail").closeTab();
}
info("Message window");
let messageWindow = await openMessageInWindow(messages.getNext());
await run_popup_test({
actionType: "message_display_action",
testType: "open-with-menu-command",
window: messageWindow,
});
await run_popup_test({
actionType: "message_display_action",
testType: "open-with-menu-command",
use_default_popup: true,
window: messageWindow,
});
await run_popup_test({
actionType: "message_display_action",
testType: "open-with-menu-command",
disable_button: true,
window: messageWindow,
});
messageWindow.close();
{
let messageWindow = await openMessageInWindow(messages.getNext());
let testConfig = {
actionType: "message_display_action",
testType: "open-with-menu-command",
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() {

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

@ -0,0 +1,194 @@
/* 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/. */
requestLongerTimeout(2);
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());
});
// This test clicks on the action button to open the popup.
add_task(async function test_popup_open_with_click() {
info("3-pane tab");
{
let testConfig = {
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 = {
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 = {
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();
}
});
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);
});

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

@ -5,7 +5,7 @@
let account, messages;
let messagePane = document.getElementById("messagepane");
add_task(async () => {
add_setup(async () => {
account = createAccount();
let rootFolder = account.incomingServer.rootFolder;
rootFolder.createSubfolder("messageDisplayScripts", null);

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

@ -0,0 +1,96 @@
/* 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/. */
add_setup(async () => {
let account = createAccount();
let rootFolder = account.incomingServer.rootFolder;
rootFolder.createSubfolder("folder0", null);
let subFolders = {};
for (let folder of rootFolder.subFolders) {
subFolders[folder.name] = folder;
}
createMessages(subFolders.folder0, 5);
});
add_task(async function testOpenMessagesInDefault() {
let extension = ExtensionTestUtils.loadExtension({
files: {
"background.js": async () => {
// Verify startup conditions.
let accounts = await browser.accounts.list();
browser.test.assertEq(
1,
accounts.length,
`number of accounts should be correct`
);
let folder0 = accounts[0].folders.find(f => f.name == "folder0");
browser.test.assertTrue(!!folder0, "folder should exist");
let { messages: messages1 } = await browser.messages.list(folder0);
browser.test.assertEq(
5,
messages1.length,
`number of messages should be correct`
);
// Open multiple messages using their headerMessageIds.
let promisedTabs = [];
promisedTabs.push(
await browser.messageDisplay.open({
headerMessageId: messages1[0].headerMessageId,
})
);
promisedTabs.push(
await browser.messageDisplay.open({
headerMessageId: messages1[1].headerMessageId,
})
);
promisedTabs.push(
await browser.messageDisplay.open({
headerMessageId: messages1[2].headerMessageId,
})
);
promisedTabs.push(
await browser.messageDisplay.open({
headerMessageId: messages1[3].headerMessageId,
})
);
promisedTabs.push(
await browser.messageDisplay.open({
headerMessageId: messages1[4].headerMessageId,
})
);
let openedTabs = await Promise.allSettled(promisedTabs);
for (let i = 0; i < 5; i++) {
browser.test.assertEq(
"fulfilled",
openedTabs[i].status,
`Promise for the opened message should have been fulfilled for message ${i}`
);
let msg = await browser.messageDisplay.getDisplayedMessage(
openedTabs[i].value.id
);
browser.test.assertEq(
messages1[i].id,
msg.id,
`Should see the correct message in window ${i}`
);
await browser.tabs.remove(openedTabs[i].value.id);
}
browser.test.notifyPass();
},
"utils.js": await getUtilsJS(),
},
manifest: {
background: { scripts: ["utils.js", "background.js"] },
permissions: ["accountsRead", "messagesRead", "tabs"],
},
});
await extension.startup();
await extension.awaitFinish();
await extension.unload();
});

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

@ -4,7 +4,7 @@
let account, rootFolder, subFolders;
add_task(async () => {
add_setup(async () => {
account = createAccount();
rootFolder = account.incomingServer.rootFolder;
subFolders = rootFolder.subFolders;

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

@ -13,357 +13,593 @@ add_task(async () => {
let messages = [...testFolder.messages];
let extension = ExtensionTestUtils.loadExtension({
background: async () => {
let listener = {
events: [],
currentPromise: null,
pushEvent(...args) {
browser.test.log(JSON.stringify(args));
this.events.push(args);
if (this.currentPromise) {
let p = this.currentPromise;
this.currentPromise = null;
p.resolve(args);
}
},
onCreated(...args) {
this.pushEvent("onCreated", ...args);
},
onUpdated(...args) {
this.pushEvent("onUpdated", ...args);
},
onActivated(...args) {
this.pushEvent("onActivated", ...args);
},
onRemoved(...args) {
this.pushEvent("onRemoved", ...args);
},
async nextEvent() {
if (this.events.length == 0) {
return new Promise(resolve => (this.currentPromise = { resolve }));
}
return Promise.resolve(this.events[0]);
},
async checkEvent(expectedEvent, ...expectedArgs) {
await this.nextEvent();
let [actualEvent, ...actualArgs] = this.events.shift();
browser.test.assertEq(expectedEvent, actualEvent);
browser.test.assertEq(expectedArgs.length, actualArgs.length);
for (let i = 0; i < expectedArgs.length; i++) {
browser.test.assertEq(typeof expectedArgs[i], typeof actualArgs[i]);
if (typeof expectedArgs[i] == "object") {
for (let key of Object.keys(expectedArgs[i])) {
browser.test.assertEq(expectedArgs[i][key], actualArgs[i][key]);
}
} else {
browser.test.assertEq(expectedArgs[i], actualArgs[i]);
}
}
return actualArgs;
},
async pageLoad(tab) {
while (true) {
// Read the first event without consuming it.
let [
actualEvent,
actualTabId,
actualInfo,
actualTab,
] = await this.nextEvent();
browser.test.assertEq("onUpdated", actualEvent);
browser.test.assertEq(tab, actualTabId);
if (
actualInfo.status == "loading" ||
actualTab.url == "about:blank"
) {
// We're not interested in these events. Take them off the list.
browser.test.log("Skipping this event.");
this.events.shift();
} else {
break;
}
}
await this.checkEvent(
"onUpdated",
tab,
{ status: "complete" },
{
id: tab,
windowId: initialWindow,
active: true,
mailTab: false,
}
);
},
};
browser.tabs.onCreated.addListener(listener.onCreated.bind(listener));
browser.tabs.onUpdated.addListener(listener.onUpdated.bind(listener), {
properties: ["status"],
});
browser.tabs.onActivated.addListener(listener.onActivated.bind(listener));
browser.tabs.onRemoved.addListener(listener.onRemoved.bind(listener));
browser.test.log(
"Collect the ID of the initial tab (there must be only one) and window."
);
let initialTabs = await browser.tabs.query({});
browser.test.assertEq(1, initialTabs.length);
browser.test.assertEq(0, initialTabs[0].index);
browser.test.assertTrue(initialTabs[0].mailTab);
browser.test.assertEq("mail", initialTabs[0].type);
let [{ id: initialTab, windowId: initialWindow }] = initialTabs;
browser.test.log("Add a first content tab and wait for it to load.");
await browser.tabs.create({ url: browser.runtime.getURL("page1.html") });
let [{ id: contentTab1 }] = await listener.checkEvent("onCreated", {
index: 1,
windowId: initialWindow,
active: true,
mailTab: false,
type: "content",
});
browser.test.assertTrue(contentTab1 != initialTab);
await listener.pageLoad(contentTab1);
browser.test.assertEq(
"content",
(await browser.tabs.get(contentTab1)).type
);
browser.test.log("Add a second content tab and wait for it to load.");
await browser.tabs.create({ url: browser.runtime.getURL("page2.html") });
let [{ id: contentTab2 }] = await listener.checkEvent("onCreated", {
index: 2,
windowId: initialWindow,
active: true,
mailTab: false,
type: "content",
});
browser.test.assertTrue(![initialTab, contentTab1].includes(contentTab2));
await listener.pageLoad(contentTab2);
browser.test.assertEq(
"content",
(await browser.tabs.get(contentTab2)).type
);
browser.test.log("Add the calendar tab.");
browser.test.sendMessage("openCalendarTab");
let [{ id: calendarTab }] = await listener.checkEvent("onCreated", {
index: 3,
windowId: initialWindow,
active: true,
mailTab: false,
type: "calendar",
});
browser.test.assertTrue(
![initialTab, contentTab1, contentTab2].includes(calendarTab)
);
browser.test.log("Add the task tab.");
browser.test.sendMessage("openTaskTab");
let [{ id: taskTab }] = await listener.checkEvent("onCreated", {
index: 4,
windowId: initialWindow,
active: true,
mailTab: false,
type: "tasks",
});
browser.test.assertTrue(
![initialTab, contentTab1, contentTab2, calendarTab].includes(taskTab)
);
browser.test.log("Open a folder in a tab.");
browser.test.sendMessage("openFolderTab");
let [{ id: folderTab }] = await listener.checkEvent("onCreated", {
index: 5,
windowId: initialWindow,
active: true,
mailTab: true,
type: "mail",
});
browser.test.assertTrue(
![initialTab, contentTab1, contentTab2, calendarTab, taskTab].includes(
folderTab
)
);
browser.test.log("Open a first message in a tab.");
browser.test.sendMessage("openMessageTab", false);
// In some circumstances this onUpdated event and the onCreated event
// happen out of order. We're not interested in the onUpdated event
// so just throw it away.
let unwantedEvent = await listener.nextEvent();
if (unwantedEvent[0] == "onUpdated") {
listener.events.shift();
}
let [{ id: messageTab1 }] = await listener.checkEvent("onCreated", {
index: 6,
windowId: initialWindow,
active: true,
mailTab: false,
type: "messageDisplay",
});
browser.test.assertTrue(
![
initialTab,
contentTab1,
contentTab2,
calendarTab,
taskTab,
folderTab,
].includes(messageTab1)
);
await listener.pageLoad(messageTab1);
browser.test.log(
"Open a second message in a tab. In the background, just because."
);
browser.test.sendMessage("openMessageTab", true);
let [{ id: messageTab2 }] = await listener.checkEvent("onCreated", {
index: 7,
windowId: initialWindow,
active: false,
mailTab: false,
type: "messageDisplay",
});
browser.test.assertTrue(
![
initialTab,
contentTab1,
contentTab2,
calendarTab,
taskTab,
folderTab,
messageTab1,
].includes(messageTab2)
);
browser.test.log(
"Activate each of the tabs in a somewhat random order to test the onActivated event."
);
for (let tab of [
initialTab,
calendarTab,
messageTab1,
taskTab,
contentTab1,
messageTab2,
folderTab,
contentTab2,
]) {
await browser.tabs.update(tab, { active: true });
if ([messageTab1, messageTab2].includes(tab)) {
await listener.checkEvent(
"onUpdated",
tab,
{ status: "loading" },
{
id: tab,
windowId: initialWindow,
active: true,
mailTab: false,
}
);
}
await listener.checkEvent("onActivated", {
tabId: tab,
windowId: initialWindow,
});
if ([messageTab1, messageTab2].includes(tab)) {
await listener.pageLoad(tab);
}
}
browser.test.log(
"Remove the first content tab. This was not active so no new tab should be activated."
);
await browser.tabs.remove(contentTab1);
await listener.checkEvent("onRemoved", contentTab1, {
windowId: initialWindow,
isWindowClosing: false,
});
browser.test.log(
"Remove the second content tab. This was active, and the calendar tab is after it, so that should be activated."
);
await browser.tabs.remove(contentTab2);
await listener.checkEvent("onRemoved", contentTab2, {
windowId: initialWindow,
isWindowClosing: false,
});
await listener.checkEvent("onActivated", {
tabId: calendarTab,
windowId: initialWindow,
});
browser.test.log("Remove the remaining tabs.");
for (let tab of [
taskTab,
messageTab1,
messageTab2,
folderTab,
calendarTab,
]) {
await browser.tabs.remove(tab);
await listener.checkEvent("onRemoved", tab, {
windowId: initialWindow,
isWindowClosing: false,
});
}
await listener.checkEvent("onActivated", {
tabId: initialTab,
windowId: initialWindow,
});
browser.test.assertEq(0, listener.events.length);
browser.test.notifyPass("finished");
},
files: {
"page1.html": "<html><body>Page 1</body></html>",
"page2.html": "<html><body>Page 2</body></html>",
"background.js": async () => {
// Executes a command, but first loads a second extension with terminated
// background and waits for it to be restarted due to the executed command.
async function capturePrimedEvent(eventName, callback) {
let eventPageExtensionReadyPromise = window.waitForMessage();
browser.test.sendMessage("capturePrimedEvent", eventName);
await eventPageExtensionReadyPromise;
let eventPageExtensionFinishedPromise = window.waitForMessage();
callback();
return eventPageExtensionFinishedPromise;
}
let listener = {
events: [],
currentPromise: null,
pushEvent(...args) {
browser.test.log(JSON.stringify(args));
this.events.push(args);
if (this.currentPromise) {
let p = this.currentPromise;
this.currentPromise = null;
p.resolve(args);
}
},
onCreated(...args) {
this.pushEvent("onCreated", ...args);
},
onUpdated(...args) {
this.pushEvent("onUpdated", ...args);
},
onActivated(...args) {
this.pushEvent("onActivated", ...args);
},
onRemoved(...args) {
this.pushEvent("onRemoved", ...args);
},
async nextEvent() {
if (this.events.length == 0) {
return new Promise(
resolve => (this.currentPromise = { resolve })
);
}
return Promise.resolve(this.events[0]);
},
async checkEvent(expectedEvent, ...expectedArgs) {
await this.nextEvent();
let [actualEvent, ...actualArgs] = this.events.shift();
browser.test.assertEq(expectedEvent, actualEvent);
browser.test.assertEq(expectedArgs.length, actualArgs.length);
for (let i = 0; i < expectedArgs.length; i++) {
browser.test.assertEq(
typeof expectedArgs[i],
typeof actualArgs[i]
);
if (typeof expectedArgs[i] == "object") {
for (let key of Object.keys(expectedArgs[i])) {
browser.test.assertEq(
expectedArgs[i][key],
actualArgs[i][key]
);
}
} else {
browser.test.assertEq(expectedArgs[i], actualArgs[i]);
}
}
return actualArgs;
},
async pageLoad(tab) {
while (true) {
// Read the first event without consuming it.
let [
actualEvent,
actualTabId,
actualInfo,
actualTab,
] = await this.nextEvent();
browser.test.assertEq("onUpdated", actualEvent);
browser.test.assertEq(tab, actualTabId);
if (
actualInfo.status == "loading" ||
actualTab.url == "about:blank"
) {
// We're not interested in these events. Take them off the list.
browser.test.log("Skipping this event.");
this.events.shift();
} else {
break;
}
}
await this.checkEvent(
"onUpdated",
tab,
{ status: "complete" },
{
id: tab,
windowId: initialWindow,
active: true,
mailTab: false,
}
);
},
};
browser.tabs.onCreated.addListener(listener.onCreated.bind(listener));
browser.tabs.onUpdated.addListener(listener.onUpdated.bind(listener), {
properties: ["status"],
});
browser.tabs.onActivated.addListener(
listener.onActivated.bind(listener)
);
browser.tabs.onRemoved.addListener(listener.onRemoved.bind(listener));
browser.test.log(
"Collect the ID of the initial tab (there must be only one) and window."
);
let initialTabs = await browser.tabs.query({});
browser.test.assertEq(1, initialTabs.length);
browser.test.assertEq(0, initialTabs[0].index);
browser.test.assertTrue(initialTabs[0].mailTab);
browser.test.assertEq("mail", initialTabs[0].type);
let [{ id: initialTab, windowId: initialWindow }] = initialTabs;
browser.test.log("Add a first content tab and wait for it to load.");
window.assertDeepEqual(
[
{
index: 1,
windowId: initialWindow,
active: true,
mailTab: false,
type: "content",
},
],
await capturePrimedEvent("onCreated", () =>
browser.tabs.create({
url: browser.runtime.getURL("page1.html"),
})
)
);
let [{ id: contentTab1 }] = await listener.checkEvent("onCreated", {
index: 1,
windowId: initialWindow,
active: true,
mailTab: false,
type: "content",
});
browser.test.assertTrue(contentTab1 != initialTab);
await listener.pageLoad(contentTab1);
browser.test.assertEq(
"content",
(await browser.tabs.get(contentTab1)).type
);
browser.test.log("Add a second content tab and wait for it to load.");
// The external extension is looking for the onUpdated event, it either be
// a loading or completed event. Compare with whatever the local extension
// is getting.
let locContentTabUpdateInfoPromise = new Promise(resolve => {
let listener = (...args) => {
browser.tabs.onUpdated.removeListener(listener);
resolve(args);
};
browser.tabs.onUpdated.addListener(listener, {
properties: ["status"],
});
});
let primedContentTabUpdateInfo = await capturePrimedEvent(
"onUpdated",
() =>
browser.tabs.create({
url: browser.runtime.getURL("page2.html"),
})
);
let [{ id: contentTab2 }] = await listener.checkEvent("onCreated", {
index: 2,
windowId: initialWindow,
active: true,
mailTab: false,
type: "content",
});
let locContentTabUpdateInfo = await locContentTabUpdateInfoPromise;
window.assertDeepEqual(
locContentTabUpdateInfo,
primedContentTabUpdateInfo,
"primed onUpdated event and non-primed onUpdeated event should receive the same values",
{ strict: true }
);
browser.test.assertTrue(
![initialTab, contentTab1].includes(contentTab2)
);
await listener.pageLoad(contentTab2);
browser.test.assertEq(
"content",
(await browser.tabs.get(contentTab2)).type
);
browser.test.log("Add the calendar tab.");
window.assertDeepEqual(
[
{
index: 3,
windowId: initialWindow,
active: true,
mailTab: false,
type: "calendar",
},
],
await capturePrimedEvent("onCreated", () =>
browser.test.sendMessage("openCalendarTab")
)
);
let [{ id: calendarTab }] = await listener.checkEvent("onCreated", {
index: 3,
windowId: initialWindow,
active: true,
mailTab: false,
type: "calendar",
});
browser.test.assertTrue(
![initialTab, contentTab1, contentTab2].includes(calendarTab)
);
browser.test.log("Add the task tab.");
window.assertDeepEqual(
[
{
index: 4,
windowId: initialWindow,
active: true,
mailTab: false,
type: "tasks",
},
],
await capturePrimedEvent("onCreated", () =>
browser.test.sendMessage("openTaskTab")
)
);
let [{ id: taskTab }] = await listener.checkEvent("onCreated", {
index: 4,
windowId: initialWindow,
active: true,
mailTab: false,
type: "tasks",
});
browser.test.assertTrue(
![initialTab, contentTab1, contentTab2, calendarTab].includes(taskTab)
);
browser.test.log("Open a folder in a tab.");
window.assertDeepEqual(
[
{
index: 5,
windowId: initialWindow,
active: true,
mailTab: true,
type: "mail",
},
],
await capturePrimedEvent("onCreated", () =>
browser.test.sendMessage("openFolderTab")
)
);
let [{ id: folderTab }] = await listener.checkEvent("onCreated", {
index: 5,
windowId: initialWindow,
active: true,
mailTab: true,
type: "mail",
});
browser.test.assertTrue(
![
initialTab,
contentTab1,
contentTab2,
calendarTab,
taskTab,
].includes(folderTab)
);
browser.test.log("Open a first message in a tab.");
window.assertDeepEqual(
[
{
index: 6,
windowId: initialWindow,
active: true,
mailTab: false,
type: "messageDisplay",
},
],
await capturePrimedEvent("onCreated", () =>
browser.test.sendMessage("openMessageTab", false)
)
);
// In some circumstances this onUpdated event and the onCreated event
// happen out of order. We're not interested in the onUpdated event
// so just throw it away.
let unwantedEvent = await listener.nextEvent();
if (unwantedEvent[0] == "onUpdated") {
listener.events.shift();
}
let [{ id: messageTab1 }] = await listener.checkEvent("onCreated", {
index: 6,
windowId: initialWindow,
active: true,
mailTab: false,
type: "messageDisplay",
});
browser.test.assertTrue(
![
initialTab,
contentTab1,
contentTab2,
calendarTab,
taskTab,
folderTab,
].includes(messageTab1)
);
await listener.pageLoad(messageTab1);
browser.test.log(
"Open a second message in a tab. In the background, just because."
);
window.assertDeepEqual(
[
{
index: 7,
windowId: initialWindow,
active: false,
mailTab: false,
type: "messageDisplay",
},
],
await capturePrimedEvent("onCreated", () =>
browser.test.sendMessage("openMessageTab", true)
)
);
let [{ id: messageTab2 }] = await listener.checkEvent("onCreated", {
index: 7,
windowId: initialWindow,
active: false,
mailTab: false,
type: "messageDisplay",
});
browser.test.assertTrue(
![
initialTab,
contentTab1,
contentTab2,
calendarTab,
taskTab,
folderTab,
messageTab1,
].includes(messageTab2)
);
browser.test.log(
"Activate each of the tabs in a somewhat random order to test the onActivated event."
);
for (let tab of [
initialTab,
calendarTab,
messageTab1,
taskTab,
contentTab1,
messageTab2,
folderTab,
contentTab2,
]) {
window.assertDeepEqual(
[{ tabId: tab, windowId: initialWindow }],
await capturePrimedEvent("onActivated", () =>
browser.tabs.update(tab, { active: true })
)
);
if ([messageTab1, messageTab2].includes(tab)) {
await listener.checkEvent(
"onUpdated",
tab,
{ status: "loading" },
{
id: tab,
windowId: initialWindow,
active: true,
mailTab: false,
}
);
}
await listener.checkEvent("onActivated", {
tabId: tab,
windowId: initialWindow,
});
if ([messageTab1, messageTab2].includes(tab)) {
await listener.pageLoad(tab);
}
}
browser.test.log(
"Remove the first content tab. This was not active so no new tab should be activated."
);
window.assertDeepEqual(
[contentTab1, { windowId: initialWindow, isWindowClosing: false }],
await capturePrimedEvent("onRemoved", () =>
browser.tabs.remove(contentTab1)
)
);
await listener.checkEvent("onRemoved", contentTab1, {
windowId: initialWindow,
isWindowClosing: false,
});
browser.test.log(
"Remove the second content tab. This was active, and the calendar tab is after it, so that should be activated."
);
window.assertDeepEqual(
[contentTab2, { windowId: initialWindow, isWindowClosing: false }],
await capturePrimedEvent("onRemoved", () =>
browser.tabs.remove(contentTab2)
)
);
await listener.checkEvent("onRemoved", contentTab2, {
windowId: initialWindow,
isWindowClosing: false,
});
await listener.checkEvent("onActivated", {
tabId: calendarTab,
windowId: initialWindow,
});
browser.test.log("Remove the remaining tabs.");
for (let tab of [
taskTab,
messageTab1,
messageTab2,
folderTab,
calendarTab,
]) {
window.assertDeepEqual(
[tab, { windowId: initialWindow, isWindowClosing: false }],
await capturePrimedEvent("onRemoved", () =>
browser.tabs.remove(tab)
)
);
await listener.checkEvent("onRemoved", tab, {
windowId: initialWindow,
isWindowClosing: false,
});
}
await listener.checkEvent("onActivated", {
tabId: initialTab,
windowId: initialWindow,
});
browser.test.assertEq(0, listener.events.length);
browser.test.notifyPass("finished");
},
"utils.js": await getUtilsJS(),
},
manifest: {
background: { scripts: ["utils.js", "background.js"] },
permissions: ["tabs"],
},
});
extension.onMessage("openCalendarTab", async () => {
// Function to start an event page extension (MV3), which can be called whenever
// the main test is about to trigger an event. The extension terminates its
// background and listens for that single event, verifying it is waking up correctly.
async function event_page_extension(eventName, actionCallback) {
let ext = ExtensionTestUtils.loadExtension({
files: {
"background.js": async () => {
// Whenever the extension starts or wakes up, hasFired is set to false. In
// case of a wake-up, the first fired event is the one that woke up the background.
let hasFired = false;
let eventName = browser.runtime.getManifest().description;
if (["onCreated", "onActivated", "onRemoved"].includes(eventName)) {
browser.tabs[eventName].addListener(async (...args) => {
// Only send the first event after background wake-up, this should
// be the only one expected.
if (!hasFired) {
hasFired = true;
browser.test.sendMessage(`${eventName} received`, args);
}
});
}
if (eventName == "onUpdated") {
browser.tabs.onUpdated.addListener(
(...args) => {
// Only send the first event after background wake-up, this should
// be the only one expected.
if (!hasFired) {
hasFired = true;
browser.test.sendMessage("onUpdated received", args);
}
},
{
properties: ["status"],
}
);
}
browser.test.sendMessage("background started");
},
},
manifest: {
manifest_version: 3,
description: eventName,
background: { scripts: ["background.js"] },
permissions: ["tabs"],
browser_specific_settings: {
gecko: { id: `tabs.eventpage.${eventName}@mochi.test` },
},
},
});
await ext.startup();
await ext.awaitMessage("background started");
// The listener should be persistent, but not primed.
assertPersistentListeners(ext, "tabs", eventName, { primed: false });
await ext.terminateBackground({ disableResetIdleForTest: true });
// Verify the primed persistent listener.
assertPersistentListeners(ext, "tabs", eventName, { primed: true });
await actionCallback();
let rv = await ext.awaitMessage(`${eventName} received`);
await ext.awaitMessage("background started");
// The listener should be persistent, but not primed.
assertPersistentListeners(ext, "tabs", eventName, { primed: false });
await ext.unload();
return rv;
}
extension.onMessage("openCalendarTab", () => {
let calendarTabButton = document.getElementById("calendarButton");
EventUtils.synthesizeMouseAtCenter(calendarTabButton, { clickCount: 1 });
EventUtils.synthesizeMouseAtCenter(calendarTabButton, {
clickCount: 1,
});
});
extension.onMessage("openTaskTab", async () => {
let calendarTabButton = document.getElementById("tasksButton");
EventUtils.synthesizeMouseAtCenter(calendarTabButton, { clickCount: 1 });
extension.onMessage("openTaskTab", () => {
let taskTabButton = document.getElementById("tasksButton");
EventUtils.synthesizeMouseAtCenter(taskTabButton, { clickCount: 1 });
});
extension.onMessage("openFolderTab", async () => {
extension.onMessage("openFolderTab", () => {
tabmail.openTab("folder", { folder: rootFolder, background: false });
});
extension.onMessage("openMessageTab", async background => {
extension.onMessage("openMessageTab", background => {
let msgHdr = messages.shift();
tabmail.openTab("message", { msgHdr, background });
});
extension.onMessage("capturePrimedEvent", async eventName => {
let primedEventData = await event_page_extension(eventName, () => {
// Resume execution in the main test, after the event page extension is
// ready to capture the event with deactivated background.
extension.sendMessage();
});
extension.sendMessage(...primedEventData);
});
await extension.startup();
await extension.awaitFinish("finished");
await extension.unload();

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

@ -0,0 +1,150 @@
"use strict";
// This test checks whether browser.theme.onUpdated works
// when a static theme is applied
const ACCENT_COLOR = "#a14040";
const TEXT_COLOR = "#fac96e";
const BACKGROUND =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0" +
"DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
add_task(async function test_on_updated() {
const theme = ExtensionTestUtils.loadExtension({
manifest: {
theme: {
images: {
theme_frame: "image1.png",
},
colors: {
frame: ACCENT_COLOR,
tab_background_text: TEXT_COLOR,
},
},
},
files: {
"image1.png": BACKGROUND,
},
});
const extension = ExtensionTestUtils.loadExtension({
background() {
browser.theme.onUpdated.addListener(updateInfo => {
browser.test.sendMessage("theme-updated", updateInfo);
});
},
});
await extension.startup();
info("Testing update event on static theme startup");
let updatedPromise = extension.awaitMessage("theme-updated");
await theme.startup();
const { theme: receivedTheme, windowId } = await updatedPromise;
Assert.ok(!windowId, "No window id in static theme update event");
Assert.ok(
receivedTheme.images.theme_frame.includes("image1.png"),
"Theme theme_frame image should be applied"
);
Assert.equal(
receivedTheme.colors.frame,
ACCENT_COLOR,
"Theme frame color should be applied"
);
Assert.equal(
receivedTheme.colors.tab_background_text,
TEXT_COLOR,
"Theme tab_background_text color should be applied"
);
info("Testing update event on static theme unload");
updatedPromise = extension.awaitMessage("theme-updated");
await theme.unload();
const updateInfo = await updatedPromise;
Assert.ok(!windowId, "No window id in static theme update event on unload");
Assert.equal(
Object.keys(updateInfo.theme),
0,
"unloading theme sends empty theme in update event"
);
await extension.unload();
});
add_task(async function test_on_updated_eventpage() {
await SpecialPowers.pushPrefEnv({
set: [["extensions.eventPages.enabled", true]],
});
const theme = ExtensionTestUtils.loadExtension({
manifest: {
theme: {
images: {
theme_frame: "image1.png",
},
colors: {
frame: ACCENT_COLOR,
tab_background_text: TEXT_COLOR,
},
},
},
files: {
"image1.png": BACKGROUND,
},
});
const extension = ExtensionTestUtils.loadExtension({
files: {
"background.js": () => {
// Whenever the extension starts or wakes up, the eventCounter is reset
// and allows to observe the order of events fired. In case of a wake-up,
// the first observed event is the one that woke up the background.
let eventCounter = 0;
browser.theme.onUpdated.addListener(async updateInfo => {
browser.test.sendMessage("theme-updated", {
eventCount: ++eventCounter,
...updateInfo,
});
});
},
"utils.js": await getUtilsJS(),
},
manifest: {
manifest_version: 3,
background: { scripts: ["utils.js", "background.js"] },
browser_specific_settings: { gecko: { id: "themes@mochi.test" } },
},
});
await extension.startup();
assertPersistentListeners(extension, "theme", "onUpdated", {
primed: false,
});
await extension.terminateBackground({ disableResetIdleForTest: true });
assertPersistentListeners(extension, "theme", "onUpdated", {
primed: true,
});
info("Testing update event on static theme startup");
await theme.startup();
const {
eventCount,
theme: receivedTheme,
windowId,
} = await extension.awaitMessage("theme-updated");
Assert.equal(eventCount, 1, "Event counter should be correct");
Assert.ok(!windowId, "No window id in static theme update event");
Assert.ok(
receivedTheme.images.theme_frame.includes("image1.png"),
"Theme theme_frame image should be applied"
);
await theme.unload();
await extension.awaitMessage("theme-updated");
await extension.unload();
await SpecialPowers.popPrefEnv();
});

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

@ -13,6 +13,17 @@ add_task(async () => {
let extension = ExtensionTestUtils.loadExtension({
files: {
"background.js": async () => {
// Executes a command, but first loads a second extension with terminated
// background and waits for it to be restarted due to the executed command.
async function capturePrimedEvent(eventName, callback) {
let eventPageExtensionReadyPromise = window.waitForMessage();
browser.test.sendMessage("capturePrimedEvent", eventName);
await eventPageExtensionReadyPromise;
let eventPageExtensionFinishedPromise = window.waitForMessage();
callback();
return eventPageExtensionFinishedPromise;
}
let listener = {
tabEvents: [],
windowEvents: [],
@ -102,7 +113,7 @@ add_task(async () => {
browser.test.log("Open a new main window (messenger.xhtml).");
browser.test.sendMessage("openMainWindow");
let primedMainWindowInfo = await window.sendMessage("openMainWindow");
let [
{ id: mainWindow },
] = await listener.checkEvent("windows.onCreated", { type: "normal" });
@ -112,10 +123,22 @@ add_task(async () => {
active: true,
mailTab: true,
});
window.assertDeepEqual(
[
{
id: mainWindow,
type: "normal",
},
],
primedMainWindowInfo
);
browser.test.log("Open a compose window (messengercompose.xhtml).");
await browser.compose.beginNew();
let primedComposeWindowInfo = await capturePrimedEvent(
"onCreated",
() => browser.compose.beginNew()
);
let [{ id: composeWindow }] = await listener.checkEvent(
"windows.onCreated",
{
@ -128,10 +151,21 @@ add_task(async () => {
active: true,
mailTab: false,
});
window.assertDeepEqual(
[
{
id: composeWindow,
type: "messageCompose",
},
],
primedComposeWindowInfo
);
browser.test.log("Open a message in a window (messageWindow.xhtml).");
await window.sendMessage("openDisplayWindow");
let primedDisplayWindowInfo = await window.sendMessage(
"openDisplayWindow"
);
let [{ id: displayWindow }] = await listener.checkEvent(
"windows.onCreated",
{
@ -144,15 +178,26 @@ add_task(async () => {
active: true,
mailTab: false,
});
window.assertDeepEqual(
[
{
id: displayWindow,
type: "messageDisplay",
},
],
primedDisplayWindowInfo
);
browser.test.log("Open a page in a popup window.");
await browser.windows.create({
url: "test.html",
type: "popup",
width: 800,
height: 500,
});
let primedPopupWindowInfo = await capturePrimedEvent("onCreated", () =>
browser.windows.create({
url: "test.html",
type: "popup",
width: 800,
height: 500,
})
);
let [{ id: popupWindow }] = await listener.checkEvent(
"windows.onCreated",
{
@ -167,45 +212,94 @@ add_task(async () => {
active: true,
mailTab: false,
});
window.assertDeepEqual(
[
{
id: popupWindow,
type: "popup",
width: 800,
height: 500,
},
],
primedPopupWindowInfo
);
browser.test.log("Pause to lets windows load properly.");
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(resolve => setTimeout(resolve, 2500));
browser.test.log("Change focused window.");
let focusInfoPromise = new Promise(resolve => {
let listener = windowId => {
browser.windows.onFocusChanged.removeListener(listener);
resolve(windowId);
};
browser.windows.onFocusChanged.addListener(listener);
});
let [primedFocusInfo] = await capturePrimedEvent("onFocusChanged", () =>
browser.windows.update(composeWindow, { focused: true })
);
let focusInfo = await focusInfoPromise;
let platformInfo = await browser.runtime.getPlatformInfo();
let expectedWindow = ["mac", "win"].includes(platformInfo.os)
? composeWindow
: browser.windows.WINDOW_ID_NONE;
window.assertDeepEqual(expectedWindow, primedFocusInfo);
window.assertDeepEqual(expectedWindow, focusInfo);
browser.test.log("Close the new main window.");
await browser.windows.remove(mainWindow);
let primedMainWindowRemoveInfo = await capturePrimedEvent(
"onRemoved",
() => browser.windows.remove(mainWindow)
);
await listener.checkEvent("windows.onRemoved", mainWindow);
await listener.checkEvent("tabs.onRemoved", mainTab, {
windowId: mainWindow,
isWindowClosing: true,
});
window.assertDeepEqual([mainWindow], primedMainWindowRemoveInfo);
browser.test.log("Close the compose window.");
await browser.windows.remove(composeWindow);
let primedComposWindowRemoveInfo = await capturePrimedEvent(
"onRemoved",
() => browser.windows.remove(composeWindow)
);
await listener.checkEvent("windows.onRemoved", composeWindow);
await listener.checkEvent("tabs.onRemoved", composeTab, {
windowId: composeWindow,
isWindowClosing: true,
});
window.assertDeepEqual([composeWindow], primedComposWindowRemoveInfo);
browser.test.log("Close the message window.");
await browser.windows.remove(displayWindow);
let primedDisplayWindowRemoveInfo = await capturePrimedEvent(
"onRemoved",
() => browser.windows.remove(displayWindow)
);
await listener.checkEvent("windows.onRemoved", displayWindow);
await listener.checkEvent("tabs.onRemoved", displayTab, {
windowId: displayWindow,
isWindowClosing: true,
});
window.assertDeepEqual([displayWindow], primedDisplayWindowRemoveInfo);
browser.test.log("Close the popup window.");
await browser.windows.remove(popupWindow);
let primedPopupWindowRemoveInfo = await capturePrimedEvent(
"onRemoved",
() => browser.windows.remove(popupWindow)
);
await listener.checkEvent("windows.onRemoved", popupWindow);
await listener.checkEvent("tabs.onRemoved", popupTab, {
windowId: popupWindow,
isWindowClosing: true,
});
window.assertDeepEqual([popupWindow], primedPopupWindowRemoveInfo);
let finalWindows = await browser.windows.getAll({ populate: true });
browser.test.assertEq(1, finalWindows.length);
@ -225,15 +319,86 @@ add_task(async () => {
},
});
// Function to start an event page extension (MV3), which can be called whenever
// the main test is about to trigger an event. The extension terminates its
// background and listens for that single event, verifying it is waking up correctly.
async function event_page_extension(eventName, actionCallback) {
let ext = ExtensionTestUtils.loadExtension({
files: {
"background.js": async () => {
// Whenever the extension starts or wakes up, hasFired is set to false. In
// case of a wake-up, the first fired event is the one that woke up the background.
let hasFired = false;
let eventName = browser.runtime.getManifest().description;
if (
["onCreated", "onFocusChanged", "onRemoved"].includes(eventName)
) {
browser.windows[eventName].addListener(async (...args) => {
// Only send the first event after background wake-up, this should
// be the only one expected.
if (!hasFired) {
hasFired = true;
browser.test.sendMessage(`${eventName} received`, args);
}
});
}
browser.test.sendMessage("background started");
},
},
manifest: {
manifest_version: 3,
description: eventName,
background: { scripts: ["background.js"] },
browser_specific_settings: {
gecko: { id: `windows.eventpage.${eventName}@mochi.test` },
},
},
});
await ext.startup();
await ext.awaitMessage("background started");
// The listener should be persistent, but not primed.
assertPersistentListeners(ext, "windows", eventName, { primed: false });
await ext.terminateBackground({ disableResetIdleForTest: true });
// Verify the primed persistent listener.
assertPersistentListeners(ext, "windows", eventName, { primed: true });
await actionCallback();
let rv = await ext.awaitMessage(`${eventName} received`);
await ext.awaitMessage("background started");
// The listener should be persistent, but not primed.
assertPersistentListeners(ext, "windows", eventName, { primed: false });
await ext.unload();
return rv;
}
extension.onMessage("openMainWindow", async () => {
let primedEventData = await event_page_extension("onCreated", () => {
return window.MsgOpenNewWindowForFolder(testFolder.URI);
});
extension.sendMessage(...primedEventData);
});
extension.onMessage("openDisplayWindow", async () => {
let primedEventData = await event_page_extension("onCreated", () => {
return openMessageInWindow([...testFolder.messages][0]);
});
extension.sendMessage(...primedEventData);
});
extension.onMessage("capturePrimedEvent", async eventName => {
let primedEventData = await event_page_extension(eventName, () => {
// Resume execution of the main test, after the event page extension has
// primed its event listeners.
extension.sendMessage();
});
extension.sendMessage(...primedEventData);
});
await extension.startup();
await extension.awaitMessage("openMainWindow");
window.MsgOpenNewWindowForFolder(testFolder.URI);
await extension.awaitMessage("openDisplayWindow");
await openMessageInWindow([...testFolder.messages][0]);
extension.sendMessage("continue");
await extension.awaitFinish("finished");
await extension.unload();
});

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

@ -16,6 +16,9 @@ var { ExtensionCommon } = ChromeUtils.import(
);
var { makeWidgetId } = ExtensionCommon;
// Persistent Listener test functionality
var { assertPersistentListeners } = ExtensionTestUtils.testAssertions;
// There are shutdown issues for which multiple rejections are left uncaught.
// This bug should be fixed, but for the moment this directory is whitelisted.
//
@ -125,8 +128,20 @@ function createAccount(type = "none") {
}
function cleanUpAccount(account) {
info(`Cleaning up account ${account.toString()}`);
let serverKey = account.incomingServer.key;
let serverType = account.incomingServer.type;
info(
`Cleaning up ${serverType} account ${account.key} and server ${serverKey}`
);
MailServices.accounts.removeAccount(account, true);
try {
let server = MailServices.accounts.getIncomingServer(serverKey);
if (server) {
info(`Cleaning up leftover ${serverType} server ${serverKey}`);
MailServices.accounts.removeIncomingServer(server, false);
}
} catch (e) {}
}
function addIdentity(account, email = "mochitest@localhost") {
@ -752,6 +767,9 @@ async function run_popup_test(configData) {
configData.apiName = configData.actionType.replace(/_([a-z])/g, function(g) {
return g[1].toUpperCase();
});
configData.moduleName =
configData.actionType == "action" ? "browserAction" : configData.apiName;
let backend_script = configData.backend_script;
let extensionDetails = {
@ -795,7 +813,8 @@ async function run_popup_test(configData) {
},
},
manifest: {
applications: {
manifest_version: configData.manifest_version || 2,
browser_specific_settings: {
gecko: {
id: `${configData.actionType}@mochi.test`,
},
@ -816,7 +835,7 @@ async function run_popup_test(configData) {
await new Promise(resolve => win.setTimeout(resolve));
await extension.awaitMessage("ready");
let buttonId = `${configData.actionType}_mochi_test-${configData.apiName}-toolbarbutton`;
let buttonId = `${configData.actionType}_mochi_test-${configData.moduleName}-toolbarbutton`;
let toolbarId;
switch (configData.actionType) {
case "compose_action":
@ -825,6 +844,7 @@ async function run_popup_test(configData) {
toolbarId = "FormatToolbar";
}
break;
case "action":
case "browser_action":
toolbarId = "mail-bar3";
if (configData.default_area == "tabstoolbar") {
@ -874,6 +894,38 @@ async function run_popup_test(configData) {
let label = button.querySelector(".toolbarbutton-text");
is(label.value, "This is a test", "Correct label");
if (
!configData.use_default_popup &&
configData?.manifest_version == 3
) {
assertPersistentListeners(
extension,
configData.moduleName,
"onClicked",
{
primed: false,
}
);
}
if (configData.terminateBackground) {
await extension.terminateBackground({
disableResetIdleForTest: true,
});
if (
!configData.use_default_popup &&
configData?.manifest_version == 3
) {
assertPersistentListeners(
extension,
configData.moduleName,
"onClicked",
{
primed: true,
}
);
}
}
let clickedPromise;
if (!configData.disable_button) {
clickedPromise = extension.awaitMessage("actionButtonClicked");
@ -883,13 +935,49 @@ async function run_popup_test(configData) {
// We're testing that nothing happens. Give it time to potentially happen.
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(resolve => win.setTimeout(resolve, 500));
// In case the background was terminated, it should not restart.
// If it does, we will get an extra "ready" message and fail.
// Listeners should still be primed.
if (
configData.terminateBackground &&
configData?.manifest_version == 3
) {
assertPersistentListeners(
extension,
configData.moduleName,
"onClicked",
{
primed: true,
}
);
}
} else {
await clickedPromise;
let hasFiredBefore = await clickedPromise;
await promiseAnimationFrame(win);
await new Promise(resolve => win.setTimeout(resolve));
is(win.document.getElementById(buttonId), button);
label = button.querySelector(".toolbarbutton-text");
is(label.value, "New title", "Correct label");
if (configData.terminateBackground) {
// The onClicked event should have restarted the background script.
await extension.awaitMessage("ready");
// Could be undefined, but it must not be true
is(false, !!hasFiredBefore);
}
if (
!configData.use_default_popup &&
configData?.manifest_version == 3
) {
assertPersistentListeners(
extension,
configData.moduleName,
"onClicked",
{
primed: false,
}
);
}
}
// Check the open state of the action button.
@ -903,6 +991,7 @@ async function run_popup_test(configData) {
await new Promise(resolve => win.setTimeout(resolve));
ok(!win.document.getElementById(buttonId), "Button destroyed");
ok(
!Services.xulStore
.getValue(win.location.href, toolbarId, "currentset")
@ -936,6 +1025,7 @@ async function run_popup_test(configData) {
} else {
// Without popup.
extensionDetails.files["background.js"] = async function() {
let hasFiredBefore = false;
browser.test.log("nopopup background script ran");
browser[window.apiName].onClicked.addListener(async (tab, info) => {
browser.test.assertEq("object", typeof tab);
@ -946,7 +1036,8 @@ async function run_popup_test(configData) {
browser.test.log(`Tab ID is ${tab.id}`);
await browser[window.apiName].setTitle({ title: "New title" });
await new Promise(resolve => window.setTimeout(resolve));
browser.test.sendMessage("actionButtonClicked");
browser.test.sendMessage("actionButtonClicked", hasFiredBefore);
hasFiredBefore = true;
});
browser.test.sendMessage("ready");
};
@ -957,7 +1048,7 @@ async function run_popup_test(configData) {
extensionDetails.manifest.permissions = ["menus"];
backend_script = async function(extension, configData) {
let win = configData.window;
let buttonId = `${configData.actionType}_mochi_test-${configData.apiName}-toolbarbutton`;
let buttonId = `${configData.actionType}_mochi_test-${configData.moduleName}-toolbarbutton`;
let menuId = "toolbar-context-menu";
if (
configData.actionType == "compose_action" &&

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

@ -4,36 +4,75 @@
// Functions for extensions to use, so that we avoid repeating ourselves.
function assertDeepEqual(expected, actual) {
function assertDeepEqual(
expected,
actual,
description = "Values should be equal",
options = {}
) {
let ok;
let strict = !!options?.strict;
try {
ok = assertDeepEqualNested(expected, actual, strict);
} catch (e) {
ok = false;
}
if (!ok) {
browser.test.fail(
`Deep equal test. \n Expected value: ${JSON.stringify(
expected
)} \n Actual value: ${JSON.stringify(actual)},
${description}`
);
}
}
function assertDeepEqualNested(expected, actual, strict) {
if (expected === null) {
browser.test.assertTrue(actual === null);
return;
return actual === null;
}
if (["boolean", "number", "string"].includes(typeof expected)) {
browser.test.assertEq(typeof expected, typeof actual);
browser.test.assertEq(expected, actual);
return;
return typeof expected == typeof actual && expected == actual;
}
if (Array.isArray(expected)) {
browser.test.assertTrue(Array.isArray(actual));
browser.test.assertEq(expected.length, actual.length);
let ok = 0;
let all = 0;
for (let i = 0; i < expected.length; i++) {
assertDeepEqual(expected[i], actual[i]);
all++;
if (assertDeepEqualNested(expected[i], actual[i], strict)) {
ok++;
}
}
return;
return (
Array.isArray(actual) && expected.length == actual.length && all == ok
);
}
let expectedKeys = Object.keys(expected);
let actualKeys = Object.keys(actual);
// Ignore any extra keys on the actual object.
browser.test.assertTrue(expectedKeys.length <= actualKeys.length);
// Ignore any extra keys on the actual object in non-strict mode (default).
let lengthOk = strict
? expectedKeys.length == actualKeys.length
: expectedKeys.length <= actualKeys.length;
browser.test.assertTrue(lengthOk);
let ok = 0;
let all = 0;
for (let key of expectedKeys) {
all++;
browser.test.assertTrue(actualKeys.includes(key), `Key ${key} exists`);
assertDeepEqual(expected[key], actual[key]);
if (assertDeepEqualNested(expected[key], actual[key], strict)) {
ok++;
}
}
return all == ok && lengthOk;
}
function waitForMessage() {

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

@ -21,6 +21,9 @@ var { PromiseTestUtils } = ChromeUtils.import(
"resource://testing-common/mailnews/PromiseTestUtils.jsm"
);
// Persistent Listener test functionality
var { assertPersistentListeners } = ExtensionTestUtils.testAssertions;
ExtensionTestUtils.init(this);
var IS_IMAP = false;
@ -76,8 +79,20 @@ function createAccount(type = "none") {
}
function cleanUpAccount(account) {
info(`Cleaning up account ${account.toString()}`);
let serverKey = account.incomingServer.key;
let serverType = account.incomingServer.type;
info(
`Cleaning up ${serverType} account ${account.key} and server ${serverKey}`
);
MailServices.accounts.removeAccount(account, true);
try {
let server = MailServices.accounts.getIncomingServer(serverKey);
if (server) {
info(`Cleaning up leftover ${serverType} server ${serverKey}`);
MailServices.accounts.removeIncomingServer(server, false);
}
} catch (e) {}
}
registerCleanupFunction(() => {

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

@ -859,7 +859,7 @@ add_task(async function test_accounts_events() {
identity: "user@invalidImap",
});
let localAccountKey = await window.sendMessage("createAccount", {
type: "local",
type: "none",
identity: "user@invalidLocal",
});
let popAccountKey = await window.sendMessage("createAccount", {
@ -869,9 +869,9 @@ add_task(async function test_accounts_events() {
// Update account identities.
let accounts = await browser.accounts.list();
let imapAccount = accounts.find(a => a.type == "imap");
let localAccount = accounts.find(a => a.type == "none");
let popAccount = accounts.find(a => a.type == "pop3");
let imapAccount = accounts.find(a => a.id == imapAccountKey);
let localAccount = accounts.find(a => a.id == localAccountKey);
let popAccount = accounts.find(a => a.id == popAccountKey);
let id1 = await browser.identities.create(imapAccount.id, {
composeHtml: true,
@ -954,7 +954,7 @@ add_task(async function test_accounts_events() {
id: "account8",
type: "none",
identities: [],
name: "Local Folders",
name: "account8user on localhost",
folders: null,
},
},
@ -1077,7 +1077,7 @@ add_task(async function test_accounts_events() {
});
extension.onMessage("removeAccount", details => {
let account = MailServices.accounts.getAccount(details.accountKey);
MailServices.accounts.removeAccount(account, true);
cleanUpAccount(account);
extension.sendMessage();
});

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

@ -0,0 +1,220 @@
/* 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";
var { ExtensionTestUtils } = ChromeUtils.import(
"resource://testing-common/ExtensionXPCShellUtils.jsm"
);
var { AddonTestUtils } = ChromeUtils.import(
"resource://testing-common/AddonTestUtils.jsm"
);
ExtensionTestUtils.mockAppInfo();
AddonTestUtils.maybeInit(this);
add_task(async function test_accounts_MV3_event_pages() {
await AddonTestUtils.promiseStartupManager();
let files = {
"background.js": async () => {
// Whenever the extension starts or wakes up, the eventCounter is reset and
// allows to observe the order of events fired. In case of a wake-up, the
// first observed event is the one that woke up the background.
let eventCounter = 0;
for (let eventName of ["onCreated", "onUpdated", "onDeleted"]) {
browser.accounts[eventName].addListener(async (...args) => {
browser.test.sendMessage(`${eventName} event received`, {
eventCount: ++eventCounter,
args,
});
});
}
browser.test.sendMessage("background started");
},
"utils.js": await getUtilsJS(),
};
let extension = ExtensionTestUtils.loadExtension({
files,
manifest: {
manifest_version: 3,
background: { scripts: ["utils.js", "background.js"] },
permissions: ["accountsRead", "accountsIdentities"],
},
});
function checkPersistentListeners({ primed }) {
// A persistent event is referenced by its moduleName as defined in
// ext-mails.json, not by its actual namespace.
const persistent_events = [
"accounts.onCreated",
"accounts.onUpdated",
"accounts.onDeleted",
];
for (let event of persistent_events) {
let [moduleName, eventName] = event.split(".");
assertPersistentListeners(extension, moduleName, eventName, {
primed,
});
}
}
let testData = [
{
type: "imap",
identity: "user@invalidImap",
expectedUpdate: true,
expectedName: accountKey => `Mail for ${accountKey}user@localhost`,
expectedType: "imap",
updatedName: "Test1",
},
{
type: "pop3",
identity: "user@invalidPop",
expectedUpdate: false,
expectedName: accountKey => `${accountKey}user on localhost`,
expectedType: "pop3",
updatedName: "Test2",
},
{
type: "none",
identity: "user@invalidLocal",
expectedUpdate: false,
expectedName: accountKey => `${accountKey}user on localhost`,
expectedType: "none",
updatedName: "Test3",
},
{
type: "local",
identity: "user@invalidLocal",
expectedUpdate: false,
expectedName: accountKey => "Local Folders",
expectedType: "none",
updatedName: "Test4",
},
];
await extension.startup();
await extension.awaitMessage("background started");
// Verify persistent listener, not yet primed.
checkPersistentListeners({ primed: false });
// Create.
for (let details of testData) {
await extension.terminateBackground({ disableResetIdleForTest: true });
// Verify the primed persistent listeners.
checkPersistentListeners({ primed: true });
let account = createAccount(details.type);
details.account = account;
{
let rv = await extension.awaitMessage("onCreated event received");
Assert.deepEqual(
{
eventCount: 1,
args: [
details.account.key,
{
id: details.account.key,
name: details.expectedName(account.key),
type: details.expectedType,
folders: null,
identities: [],
},
],
},
rv,
"The primed onCreated event should return the correct values"
);
}
if (details.expectedUpdate) {
let rv = await extension.awaitMessage("onUpdated event received");
Assert.deepEqual(
{
eventCount: 2,
args: [
details.account.key,
{ id: details.account.key, name: "Mail for user@localhost" },
],
},
rv,
"The non-primed onUpdated event should return the correct values"
);
}
// The background should have been restarted.
await extension.awaitMessage("background started");
// The listener should no longer be primed.
checkPersistentListeners({ primed: false });
}
// Update.
for (let details of testData) {
await extension.terminateBackground({ disableResetIdleForTest: true });
// Verify the primed persistent listeners.
checkPersistentListeners({ primed: true });
let account = MailServices.accounts.getAccount(details.account.key);
account.incomingServer.prettyName = details.updatedName;
let rv = await extension.awaitMessage("onUpdated event received");
Assert.deepEqual(
{
eventCount: 1,
args: [
details.account.key,
{
id: details.account.key,
name: details.updatedName,
},
],
},
rv,
"The primed onUpdated event should return the correct values"
);
// The background should have been restarted.
await extension.awaitMessage("background started");
// The listener should no longer be primed.
checkPersistentListeners({ primed: false });
}
// Delete.
for (let details of testData) {
await extension.terminateBackground({ disableResetIdleForTest: true });
// Verify the primed persistent listeners.
checkPersistentListeners({ primed: true });
cleanUpAccount(details.account);
let rv = await extension.awaitMessage("onDeleted event received");
Assert.deepEqual(
{
eventCount: 1,
args: [details.account.key],
},
rv,
"The primed onDeleted event should return the correct values"
);
// The background should have been restarted.
await extension.awaitMessage("background started");
// The listener should no longer be primed.
checkPersistentListeners({ primed: false });
}
await extension.unload();
await AddonTestUtils.promiseShutdownManager();
});

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

@ -17,8 +17,22 @@ XPCOMUtils.defineLazyModuleGetters(this, {
AddrBookUtils: "resource:///modules/AddrBookUtils.jsm",
});
add_task(async function setup() {
var { AddonTestUtils } = ChromeUtils.import(
"resource://testing-common/AddonTestUtils.jsm"
);
ExtensionTestUtils.mockAppInfo();
AddonTestUtils.maybeInit(this);
add_setup(async () => {
Services.prefs.setIntPref("ldap_2.servers.osx.dirType", -1);
registerCleanupFunction(() => {
// Make sure any open database is given a chance to close.
Services.startup.advanceShutdownPhase(
Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
);
});
});
add_task(async function test_addressBooks() {
@ -999,6 +1013,428 @@ add_task(async function test_addressBooks() {
await extension.unload();
});
add_task(async function test_addressBooks_MV3_event_pages() {
await AddonTestUtils.promiseStartupManager();
let files = {
"background.js": async () => {
// Whenever the extension starts or wakes up, hasFired is set to false. In
// case of a wake-up, the first fired event is the one that woke up the background.
let hasFired = false;
// Create and register event listener.
for (let event of [
"addressBooks.onCreated",
"addressBooks.onUpdated",
"addressBooks.onDeleted",
"contacts.onCreated",
"contacts.onUpdated",
"contacts.onDeleted",
"mailingLists.onCreated",
"mailingLists.onUpdated",
"mailingLists.onDeleted",
"mailingLists.onMemberAdded",
"mailingLists.onMemberRemoved",
]) {
let [apiName, eventName] = event.split(".");
browser[apiName][eventName].addListener((...args) => {
// Only send the first event after background wake-up, this should be
// the only one expected.
if (!hasFired) {
hasFired = true;
browser.test.sendMessage(`${apiName}.${eventName} received`, args);
}
});
}
browser.test.sendMessage("background started");
},
"utils.js": await getUtilsJS(),
};
let extension = ExtensionTestUtils.loadExtension({
files,
manifest: {
manifest_version: 3,
background: { scripts: ["utils.js", "background.js"] },
permissions: ["addressBooks"],
browser_specific_settings: { gecko: { id: "addressbook@xpcshell.test" } },
},
});
let parent = MailServices.ab.getDirectory("jsaddrbook://abook.sqlite");
function findContact(id) {
for (let child of parent.childCards) {
if (child.UID == id) {
return child;
}
}
return null;
}
function findMailingList(id) {
for (let list of parent.childNodes) {
if (list.UID == id) {
return list;
}
}
return null;
}
function outsideEvent(action, ...args) {
switch (action) {
case "createAddressBook": {
let dirPrefId = MailServices.ab.newAddressBook(
"external add",
"",
Ci.nsIAbManager.JS_DIRECTORY_TYPE
);
let book = MailServices.ab.getDirectoryFromId(dirPrefId);
return [book, dirPrefId];
}
case "updateAddressBook": {
let book = MailServices.ab.getDirectoryFromId(args[0]);
book.dirName = "external edit";
return [];
}
case "deleteAddressBook": {
let book = MailServices.ab.getDirectoryFromId(args[0]);
MailServices.ab.deleteAddressBook(book.URI);
return [];
}
case "createContact": {
let contact = new AddrBookCard();
contact.firstName = "external";
contact.lastName = "add";
contact.primaryEmail = "test@invalid";
let newContact = parent.addCard(contact);
return [parent.UID, newContact.UID];
}
case "updateContact": {
let contact = findContact(args[0]);
if (contact) {
contact.firstName = "external";
contact.lastName = "edit";
parent.modifyCard(contact);
return [];
}
break;
}
case "deleteContact": {
let contact = findContact(args[0]);
if (contact) {
parent.deleteCards([contact]);
return [];
}
break;
}
case "createMailingList": {
let list = Cc[
"@mozilla.org/addressbook/directoryproperty;1"
].createInstance(Ci.nsIAbDirectory);
list.isMailList = true;
list.dirName = "external add";
let newList = parent.addMailList(list);
return [parent.UID, newList.UID];
}
case "updateMailingList": {
let list = findMailingList(args[0]);
if (list) {
list.dirName = "external edit";
list.editMailListToDatabase(null);
return [];
}
break;
}
case "deleteMailingList": {
let list = findMailingList(args[0]);
if (list) {
parent.deleteDirectory(list);
return [];
}
break;
}
case "addMailingListMember": {
let list = findMailingList(args[0]);
let contact = findContact(args[1]);
if (list && contact) {
list.addCard(contact);
equal(1, list.childCards.length);
return [];
}
break;
}
case "removeMailingListMember": {
let list = findMailingList(args[0]);
let contact = findContact(args[1]);
if (list && contact) {
list.deleteCards([contact]);
equal(0, list.childCards.length);
ok(findContact(args[1]), "Contact was not removed");
return [];
}
break;
}
}
throw new Error(
`Message "${action}" passed to handler didn't do anything.`
);
}
function checkPersistentListeners({ primed }) {
// A persistent event is referenced by its moduleName as defined in
// ext-mails.json, not by its actual namespace.
const persistent_events = [
"addressBook.onAddressBookCreated",
"addressBook.onAddressBookUpdated",
"addressBook.onAddressBookDeleted",
"addressBook.onContactCreated",
"addressBook.onContactUpdated",
"addressBook.onContactDeleted",
"addressBook.onMailingListCreated",
"addressBook.onMailingListUpdated",
"addressBook.onMailingListDeleted",
"addressBook.onMemberAdded",
"addressBook.onMemberRemoved",
];
for (let event of persistent_events) {
let [moduleName, eventName] = event.split(".");
assertPersistentListeners(extension, moduleName, eventName, {
primed,
});
}
}
await extension.startup();
await extension.awaitMessage("background started");
checkPersistentListeners({ primed: false });
// addressBooks.onCreated.
await extension.terminateBackground({ disableResetIdleForTest: true });
checkPersistentListeners({ primed: true });
let [newBook, dirPrefId] = outsideEvent("createAddressBook");
// The event should have restarted the background.
await extension.awaitMessage("background started");
Assert.deepEqual(
[
{
id: newBook.UID,
type: "addressBook",
name: "external add",
readOnly: false,
remote: false,
},
],
await extension.awaitMessage("addressBooks.onCreated received"),
"The primed addressBooks.onCreated event should return the correct values"
);
checkPersistentListeners({ primed: false });
// addressBooks.onUpdated.
await extension.terminateBackground({ disableResetIdleForTest: true });
checkPersistentListeners({ primed: true });
outsideEvent("updateAddressBook", dirPrefId);
// The event should have restarted the background.
await extension.awaitMessage("background started");
Assert.deepEqual(
[
{
id: newBook.UID,
type: "addressBook",
name: "external edit",
readOnly: false,
remote: false,
},
],
await extension.awaitMessage("addressBooks.onUpdated received"),
"The primed addressBooks.onUpdated event should return the correct values"
);
checkPersistentListeners({ primed: false });
// addressBooks.onDeleted.
await extension.terminateBackground({ disableResetIdleForTest: true });
checkPersistentListeners({ primed: true });
outsideEvent("deleteAddressBook", dirPrefId);
// The event should have restarted the background.
await extension.awaitMessage("background started");
Assert.deepEqual(
[newBook.UID],
await extension.awaitMessage("addressBooks.onDeleted received"),
"The primed addressBooks.onDeleted event should return the correct values"
);
checkPersistentListeners({ primed: false });
// contacts.onCreated.
await extension.terminateBackground({ disableResetIdleForTest: true });
checkPersistentListeners({ primed: true });
let [parentId1, contactId] = outsideEvent("createContact");
// The event should have restarted the background.
await extension.awaitMessage("background started");
let [createdNode] = await extension.awaitMessage(
"contacts.onCreated received"
);
Assert.deepEqual(
{
type: "contact",
parentId: parentId1,
id: contactId,
},
{
type: createdNode.type,
parentId: createdNode.parentId,
id: createdNode.id,
},
"The primed contacts.onCreated event should return the correct values"
);
checkPersistentListeners({ primed: false });
// contacts.onUpdated.
await extension.terminateBackground({ disableResetIdleForTest: true });
checkPersistentListeners({ primed: true });
outsideEvent("updateContact", contactId);
// The event should have restarted the background.
await extension.awaitMessage("background started");
let [updatedNode, changedProperties] = await extension.awaitMessage(
"contacts.onUpdated received"
);
Assert.deepEqual(
[
{ type: "contact", parentId: parentId1, id: contactId },
{ LastName: { oldValue: "add", newValue: "edit" } },
],
[
{
type: updatedNode.type,
parentId: updatedNode.parentId,
id: updatedNode.id,
},
changedProperties,
],
"The primed contacts.onUpdated event should return the correct values"
);
checkPersistentListeners({ primed: false });
// mailingLists.onCreated.
await extension.terminateBackground({ disableResetIdleForTest: true });
checkPersistentListeners({ primed: true });
let [parentId2, listId] = outsideEvent("createMailingList");
// The event should have restarted the background.
await extension.awaitMessage("background started");
Assert.deepEqual(
[
{
type: "mailingList",
parentId: parentId2,
id: listId,
name: "external add",
nickName: "",
description: "",
readOnly: false,
remote: false,
},
],
await extension.awaitMessage("mailingLists.onCreated received"),
"The primed mailingLists.onCreated event should return the correct values"
);
checkPersistentListeners({ primed: false });
// mailingList.onUpdated.
await extension.terminateBackground({ disableResetIdleForTest: true });
checkPersistentListeners({ primed: true });
outsideEvent("updateMailingList", listId);
// The event should have restarted the background.
await extension.awaitMessage("background started");
Assert.deepEqual(
[
{
type: "mailingList",
parentId: parentId2,
id: listId,
name: "external edit",
nickName: "",
description: "",
readOnly: false,
remote: false,
},
],
await extension.awaitMessage("mailingLists.onUpdated received"),
"The primed mailingLists.onUpdated event should return the correct values"
);
checkPersistentListeners({ primed: false });
// mailingList.onMemberAdded.
await extension.terminateBackground({ disableResetIdleForTest: true });
checkPersistentListeners({ primed: true });
outsideEvent("addMailingListMember", listId, contactId);
// The event should have restarted the background.
await extension.awaitMessage("background started");
let [addedNode] = await extension.awaitMessage(
"mailingLists.onMemberAdded received"
);
Assert.deepEqual(
{ type: "contact", parentId: listId, id: contactId },
{ type: addedNode.type, parentId: addedNode.parentId, id: addedNode.id },
"The primed mailingLists.onMemberAdded event should return the correct values"
);
checkPersistentListeners({ primed: false });
// mailingList.onMemberRemoved.
await extension.terminateBackground({ disableResetIdleForTest: true });
checkPersistentListeners({ primed: true });
outsideEvent("removeMailingListMember", listId, contactId);
// The event should have restarted the background.
await extension.awaitMessage("background started");
Assert.deepEqual(
[listId, contactId],
await extension.awaitMessage("mailingLists.onMemberRemoved received"),
"The primed mailingLists.onMemberRemoved event should return the correct values"
);
checkPersistentListeners({ primed: false });
// mailingList.onDeleted.
await extension.terminateBackground({ disableResetIdleForTest: true });
checkPersistentListeners({ primed: true });
outsideEvent("deleteMailingList", listId);
// The event should have restarted the background.
await extension.awaitMessage("background started");
Assert.deepEqual(
[parentId2, listId],
await extension.awaitMessage("mailingLists.onDeleted received"),
"The primed mailingLists.onDeleted event should return the correct values"
);
checkPersistentListeners({ primed: false });
// contacts.onDeleted.
await extension.terminateBackground({ disableResetIdleForTest: true });
checkPersistentListeners({ primed: true });
outsideEvent("deleteContact", contactId);
// The event should have restarted the background.
await extension.awaitMessage("background started");
Assert.deepEqual(
[parentId1, contactId],
await extension.awaitMessage("contacts.onDeleted received"),
"The primed contacts.onDeleted event should return the correct values"
);
checkPersistentListeners({ primed: false });
await extension.unload();
await AddonTestUtils.promiseShutdownManager();
});
add_task(async function test_photos() {
async function background() {
let events = [];
@ -1605,10 +2041,3 @@ add_task(async function test_photos() {
await extension.awaitFinish("addressBooksPhotos");
await extension.unload();
});
registerCleanupFunction(() => {
// Make sure any open database is given a chance to close.
Services.startup.advanceShutdownPhase(
Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
);
});

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

@ -11,8 +11,16 @@ var { LDAPServer } = ChromeUtils.import(
"resource://testing-common/LDAPServer.jsm"
);
add_task(async function setup() {
add_setup(async () => {
Services.prefs.setIntPref("ldap_2.servers.osx.dirType", -1);
registerCleanupFunction(() => {
LDAPServer.close();
// Make sure any open database is given a chance to close.
Services.startup.advanceShutdownPhase(
Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
);
});
});
add_task(async function test_quickSearch() {
@ -145,10 +153,6 @@ add_task(async function test_quickSearch_types() {
Ci.nsIAbManager.LDAP_DIRECTORY_TYPE
);
registerCleanupFunction(async () => {
LDAPServer.close();
});
async function background() {
function checkCards(cards, expectedNames) {
browser.test.assertEq(expectedNames.length, cards.length);
@ -232,11 +236,3 @@ add_task(async function test_quickSearch_types() {
await extension.awaitFinish("addressBooks");
await extension.unload();
});
registerCleanupFunction(() => {
LDAPServer.close();
// Make sure any open database is given a chance to close.
Services.startup.advanceShutdownPhase(
Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
);
});

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

@ -8,9 +8,16 @@ var { ExtensionTestUtils } = ChromeUtils.import(
"resource://testing-common/ExtensionXPCShellUtils.jsm"
);
add_task(async function setup() {
add_setup(async () => {
Services.prefs.setIntPref("ldap_2.servers.osx.dirType", -1);
registerCleanupFunction(() => {
// Make sure any open database is given a chance to close.
Services.startup.advanceShutdownPhase(
Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
);
});
let historyAB = MailServices.ab.getDirectory("jsaddrbook://history.sqlite");
let contact1 = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
@ -139,10 +146,3 @@ add_task(async function test_addressBooks_readonly() {
await extension.awaitFinish("addressBooks");
await extension.unload();
});
registerCleanupFunction(() => {
// Make sure any open database is given a chance to close.
Services.startup.advanceShutdownPhase(
Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
);
});

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

@ -11,7 +11,7 @@ var { LDAPServer } = ChromeUtils.import(
"resource://testing-common/LDAPServer.jsm"
);
add_task(async function setup() {
add_setup(async () => {
// If nsIAbLDAPDirectory doesn't exist in our build options, someone has
// specified --disable-ldap.
if (!("nsIAbLDAPDirectory" in Ci)) {
@ -28,8 +28,12 @@ add_task(async function setup() {
Ci.nsIAbManager.LDAP_DIRECTORY_TYPE
);
registerCleanupFunction(async () => {
registerCleanupFunction(() => {
LDAPServer.close();
// Make sure any open database is given a chance to close.
Services.startup.advanceShutdownPhase(
Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
);
});
});
@ -95,10 +99,3 @@ add_task(async function test_addressBooks_remote() {
await extension.awaitFinish("addressBooks");
await extension.unload();
});
registerCleanupFunction(() => {
// Make sure any open database is given a chance to close.
Services.startup.advanceShutdownPhase(
Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
);
});

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

@ -0,0 +1,443 @@
/* 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";
var { ExtensionTestUtils } = ChromeUtils.import(
"resource://testing-common/ExtensionXPCShellUtils.jsm"
);
var { AddonTestUtils } = ChromeUtils.import(
"resource://testing-common/AddonTestUtils.jsm"
);
ExtensionTestUtils.mockAppInfo();
AddonTestUtils.maybeInit(this);
registerCleanupFunction(async () => {
// Remove the temporary MozillaMailnews folder, which is not deleted in time when
// the cleanupFunction registered by AddonTestUtils.maybeInit() checks for left over
// files in the temp folder.
// Note: PathUtils.tempDir points to the system temp folder, which is different.
let path = PathUtils.join(
Services.dirsvc.get("TmpD", Ci.nsIFile).path,
"MozillaMailnews"
);
await IOUtils.remove(path, { recursive: true });
});
// Test events and persistent events for Manifest V3 for onCreated, onRenamed,
// onMoved, onCopied and onDeleted.
add_task(
{
skip_if: () => IS_NNTP,
},
async function test_folders_MV3_event_pages() {
await AddonTestUtils.promiseStartupManager();
let account = createAccount();
let rootFolder = account.incomingServer.rootFolder;
addIdentity(account, "id1@invalid");
let files = {
"background.js": () => {
for (let eventName of [
"onCreated",
"onDeleted",
"onCopied",
"onRenamed",
"onMoved",
"onFolderInfoChanged",
]) {
browser.folders[eventName].addListener(async (...args) => {
browser.test.log(`${eventName} received: ${JSON.stringify(args)}`);
browser.test.sendMessage(`${eventName} received`, args);
});
}
browser.test.sendMessage("background started");
},
"utils.js": await getUtilsJS(),
};
let extension = ExtensionTestUtils.loadExtension({
files,
manifest: {
manifest_version: 3,
background: { scripts: ["utils.js", "background.js"] },
permissions: ["accountsRead"],
},
});
// Function to start an event page extension (MV3), which can be called whenever
// the main test is about to trigger an event. The extension terminates its
// background and listens for that single event, verifying it is waking up correctly.
async function event_page_extension(eventName, actionCallback) {
let ext = ExtensionTestUtils.loadExtension({
files: {
"background.js": async () => {
// Whenever the extension starts or wakes up, hasFired is set to false. In
// case of a wake-up, the first fired event is the one that woke up the background.
let hasFired = false;
let _eventName = browser.runtime.getManifest().description;
browser.folders[_eventName].addListener(async (...args) => {
// Only send the first event after background wake-up, this should
// be the only one expected.
if (!hasFired) {
hasFired = true;
browser.test.sendMessage(`${_eventName} received`, args);
}
});
browser.test.sendMessage("background started");
},
},
manifest: {
manifest_version: 3,
description: eventName,
background: { scripts: ["background.js"] },
permissions: ["accountsRead"],
},
});
await ext.startup();
await ext.awaitMessage("background started");
// The listener should be persistent, but not primed.
assertPersistentListeners(ext, "folders", eventName, { primed: false });
await ext.terminateBackground({ disableResetIdleForTest: true });
// Verify the primed persistent listener.
assertPersistentListeners(ext, "folders", eventName, { primed: true });
await actionCallback();
let rv = await ext.awaitMessage(`${eventName} received`);
await ext.awaitMessage("background started");
// The listener should be persistent, but not primed.
assertPersistentListeners(ext, "folders", eventName, { primed: false });
await ext.unload();
return rv;
}
await extension.startup();
await extension.awaitMessage("background started");
// Create a test folder before terminating the background script, to make sure
// everything is sane.
rootFolder.createSubfolder("TestFolder", null);
await extension.awaitMessage("onCreated received");
if (IS_IMAP) {
// IMAP creates a default Trash folder on the fly.
await extension.awaitMessage("onCreated received");
}
// Create SubFolder1.
{
rootFolder.createSubfolder("SubFolder1", null);
let createData = await extension.awaitMessage("onCreated received");
Assert.deepEqual(
[
{
accountId: account.key,
name: "SubFolder1",
path: "/SubFolder1",
},
],
createData,
"The onCreated event should return the correct values"
);
// Collect all onFolderInfoChanged events. Order is not fixed.
let changeEvents = [];
let expectedChanges = [];
changeEvents.push(
await extension.awaitMessage("onFolderInfoChanged received")
);
changeEvents.push(
await extension.awaitMessage("onFolderInfoChanged received")
);
expectedChanges.push([
{ accountId: "account1", name: "TestFolder", path: "/TestFolder" },
{ totalMessageCount: 0, unreadMessageCount: 0 },
]);
expectedChanges.push([
{ accountId: "account1", name: "SubFolder1", path: "/SubFolder1" },
{ totalMessageCount: 0, unreadMessageCount: 0 },
]);
if (IS_IMAP) {
changeEvents.push(
await extension.awaitMessage("onFolderInfoChanged received")
);
changeEvents.push(
await extension.awaitMessage("onFolderInfoChanged received")
);
expectedChanges.push([
{
accountId: "account1",
name: "Trash",
path: "/Trash",
type: "trash",
},
{ totalMessageCount: 0, unreadMessageCount: 0 },
]);
expectedChanges.push([
{ accountId: "account1", name: "Junk", path: "", type: "junk" },
{ totalMessageCount: 0, unreadMessageCount: 0 },
]);
}
Assert.deepEqual(
changeEvents.sort((a, b) => a[0].name > b[0].name),
expectedChanges.sort((a, b) => a[0].name > b[0].name),
"The onFolderInfoChanged event should return the correct values"
);
}
// Create SubFolder2 (used for primed onFolderInfoChanged).
{
let primedChangeData = await event_page_extension(
"onFolderInfoChanged",
() => {
rootFolder.createSubfolder("SubFolder2", null);
}
);
let changeData = await extension.awaitMessage(
"onFolderInfoChanged received"
);
let createData = await extension.awaitMessage("onCreated received");
Assert.deepEqual(
changeData,
primedChangeData,
"The primed onFolderInfoChanged event should return the correct values"
);
Assert.deepEqual(
[
{
accountId: account.key,
name: "SubFolder2",
path: "/SubFolder2",
},
],
createData,
"The onCreated event should return the correct values"
);
}
// Rename.
{
let primedRenameData = await event_page_extension("onRenamed", () => {
rootFolder.getChildNamed("SubFolder2").rename("SubFolder3", null);
});
let renameData = await extension.awaitMessage("onRenamed received");
Assert.deepEqual(
primedRenameData,
renameData,
"The primed onRenamed event should return the correct values"
);
Assert.deepEqual(
[
{
accountId: account.key,
name: "SubFolder2",
path: "/SubFolder2",
},
{
accountId: account.key,
name: "SubFolder3",
path: "/SubFolder3",
},
],
renameData,
"The onRenamed event should return the correct values"
);
if (IS_IMAP) {
// IMAP fires an additional delete and create event.
let deleteData = await extension.awaitMessage("onDeleted received");
Assert.deepEqual(
[
{
accountId: account.key,
name: "SubFolder2",
path: "/SubFolder2",
},
],
deleteData,
"The onDeleted event should return the correct MailFolder values."
);
let createData = await extension.awaitMessage("onCreated received");
Assert.deepEqual(
[
{
accountId: account.key,
name: "/SubFolder3", // FIXME: There should be no leading slash, no
path: "//SubFolder3", // other test tested this so far.
},
],
createData,
"The onCreated event should return the correct MailFolder values."
);
}
}
// Copy.
{
let primedCopyData = await event_page_extension("onCopied", () => {
MailServices.copy.copyFolder(
rootFolder.getChildNamed("SubFolder3"),
rootFolder.getChildNamed("SubFolder1"),
false,
null,
null
);
});
let copyData = await extension.awaitMessage("onCopied received");
Assert.deepEqual(
primedCopyData,
copyData,
"The primed onCopied event should return the correct values"
);
Assert.deepEqual(
[
{
accountId: account.key,
name: "SubFolder3",
path: "/SubFolder3",
},
{
accountId: account.key,
name: "SubFolder3",
path: "/SubFolder1/SubFolder3",
},
],
copyData,
"The onCopied event should return the correct values"
);
if (IS_IMAP) {
// IMAP fires an additional create event.
let createData = await extension.awaitMessage("onCreated received");
Assert.deepEqual(
[
{
accountId: account.key,
name: "SubFolder3",
path: "/SubFolder1/SubFolder3",
},
],
createData,
"The onCreated event should return the correct MailFolder values."
);
}
}
// Move.
{
let primedMoveData = await event_page_extension("onMoved", () => {
MailServices.copy.copyFolder(
rootFolder.getChildNamed("SubFolder1").getChildNamed("SubFolder3"),
rootFolder.getChildNamed("SubFolder3"),
true,
null,
null
);
});
let moveData = await extension.awaitMessage("onMoved received");
Assert.deepEqual(
primedMoveData,
moveData,
"The primed onMoved event should return the correct values"
);
Assert.deepEqual(
[
{
accountId: account.key,
name: "SubFolder3",
path: "/SubFolder1/SubFolder3",
},
{
accountId: account.key,
name: "SubFolder3",
path: "/SubFolder3/SubFolder3",
},
],
moveData,
"The onMoved event should return the correct values"
);
if (IS_IMAP) {
// IMAP fires additional rename and delete events.
let renameData = await extension.awaitMessage("onRenamed received");
Assert.deepEqual(
[
{
accountId: account.key,
name: "SubFolder3",
path: "/SubFolder1/SubFolder3",
},
{
accountId: account.key,
name: "SubFolder3",
path: "/SubFolder3/SubFolder3",
},
],
renameData,
"The onRenamed event should return the correct MailFolder values."
);
let deleteData = await extension.awaitMessage("onDeleted received");
Assert.deepEqual(
[
{
accountId: account.key,
name: "SubFolder3",
path: "/SubFolder1/SubFolder3",
},
],
deleteData,
"The onDeleted event should return the correct MailFolder values."
);
}
}
// Delete.
{
let primedDeleteData = await event_page_extension("onDeleted", () => {
let subFolder1 = rootFolder.getChildNamed("SubFolder3");
subFolder1.propagateDelete(
subFolder1.getChildNamed("SubFolder3"),
true,
null
);
});
let deleteData = await extension.awaitMessage("onDeleted received");
Assert.deepEqual(
primedDeleteData,
deleteData,
"The primed onDeleted event should return the correct values"
);
Assert.deepEqual(
[
{
accountId: account.key,
name: "SubFolder3",
path: "/SubFolder3/SubFolder3",
},
],
deleteData,
"The onDeleted event should return the correct values"
);
}
await extension.awaitMessage("onFolderInfoChanged received");
await extension.unload();
cleanUpAccount(account);
await AddonTestUtils.promiseShutdownManager();
}
);

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

@ -0,0 +1,146 @@
/* 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";
var { ExtensionTestUtils } = ChromeUtils.import(
"resource://testing-common/ExtensionXPCShellUtils.jsm"
);
var { AddonTestUtils } = ChromeUtils.import(
"resource://testing-common/AddonTestUtils.jsm"
);
ExtensionTestUtils.mockAppInfo();
AddonTestUtils.maybeInit(this);
add_task(async function test_identities_MV3_event_pages() {
await AddonTestUtils.promiseStartupManager();
let account1 = createAccount();
addIdentity(account1, "id1@invalid");
let files = {
"background.js": async () => {
// Whenever the extension starts or wakes up, hasFired is set to false. In
// case of a wake-up, the first fired event is the one that woke up the background.
let hasFired = false;
for (let eventName of ["onCreated", "onUpdated", "onDeleted"]) {
browser.identities[eventName].addListener((...args) => {
// Only send the first event after background wake-up, this should be the
// only one expected.
if (!hasFired) {
hasFired = true;
browser.test.sendMessage(`${eventName} received`, args);
}
});
}
browser.test.sendMessage("background started");
},
"utils.js": await getUtilsJS(),
};
let extension = ExtensionTestUtils.loadExtension({
files,
manifest: {
manifest_version: 3,
background: { scripts: ["utils.js", "background.js"] },
permissions: ["accountsRead", "accountsIdentities"],
browser_specific_settings: { gecko: { id: "identities@xpcshell.test" } },
},
});
function checkPersistentListeners({ primed }) {
// A persistent event is referenced by its moduleName as defined in
// ext-mails.json, not by its actual namespace.
const persistent_events = [
"identities.onCreated",
"identities.onUpdated",
"identities.onDeleted",
];
for (let event of persistent_events) {
let [moduleName, eventName] = event.split(".");
assertPersistentListeners(extension, moduleName, eventName, {
primed,
});
}
}
await extension.startup();
await extension.awaitMessage("background started");
// Verify persistent listener, not yet primed.
checkPersistentListeners({ primed: false });
await extension.terminateBackground({ disableResetIdleForTest: true });
// Verify the primed persistent listeners.
checkPersistentListeners({ primed: true });
// Create.
let id2 = addIdentity(account1, "id2@invalid");
let createData = await extension.awaitMessage("onCreated received");
Assert.deepEqual(
[
"id2",
{
accountId: "account1",
id: "id2",
label: "",
name: "",
email: "id2@invalid",
replyTo: "",
organization: "",
composeHtml: true,
signature: "",
signatureIsPlainText: true,
},
],
createData,
"The primed onCreated event should return the correct values"
);
await extension.awaitMessage("background started");
// Verify persistent listener, not yet primed.
checkPersistentListeners({ primed: false });
await extension.terminateBackground({ disableResetIdleForTest: true });
// Verify the primed persistent listeners.
checkPersistentListeners({ primed: true });
// Update
id2.fullName = "Updated Name";
let updateData = await extension.awaitMessage("onUpdated received");
Assert.deepEqual(
["id2", { name: "Updated Name", accountId: "account1", id: "id2" }],
updateData,
"The primed onUpdated event should return the correct values"
);
await extension.awaitMessage("background started");
// Verify persistent listener, not yet primed.
checkPersistentListeners({ primed: false });
await extension.terminateBackground({ disableResetIdleForTest: true });
// Verify the primed persistent listeners.
checkPersistentListeners({ primed: true });
// Delete
account1.removeIdentity(id2);
let deleteData = await extension.awaitMessage("onDeleted received");
Assert.deepEqual(
["id2"],
deleteData,
"The primed onDeleted event should return the correct values"
);
// The background should have been restarted.
await extension.awaitMessage("background started");
// The listener should no longer be primed.
checkPersistentListeners({ primed: false });
await extension.unload();
cleanUpAccount(account1);
await AddonTestUtils.promiseShutdownManager();
});

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

@ -13,6 +13,76 @@ var { TestUtils } = ChromeUtils.importESModule(
var { ExtensionsUI } = ChromeUtils.import(
"resource:///modules/ExtensionsUI.jsm"
);
var { AddonTestUtils } = ChromeUtils.import(
"resource://testing-common/AddonTestUtils.jsm"
);
ExtensionTestUtils.mockAppInfo();
AddonTestUtils.maybeInit(this);
registerCleanupFunction(async () => {
// Remove the temporary MozillaMailnews folder, which is not deleted in time when
// the cleanupFunction registered by AddonTestUtils.maybeInit() checks for left over
// files in the temp folder.
// Note: PathUtils.tempDir points to the system temp folder, which is different.
let path = PathUtils.join(
Services.dirsvc.get("TmpD", Ci.nsIFile).path,
"MozillaMailnews"
);
await IOUtils.remove(path, { recursive: true });
});
// Function to start an event page extension (MV3), which can be called whenever
// the main test is about to trigger an event. The extension terminates its
// background and listens for that single event, verifying it is waking up correctly.
async function event_page_extension(eventName, actionCallback) {
let ext = ExtensionTestUtils.loadExtension({
files: {
"background.js": async () => {
// Whenever the extension starts or wakes up, hasFired is set to false. In
// case of a wake-up, the first fired event is the one that woke up the background.
let hasFired = false;
let _eventName = browser.runtime.getManifest().description;
browser.messages[_eventName].addListener(async (...args) => {
// Only send the first event after background wake-up, this should
// be the only one expected.
if (!hasFired) {
hasFired = true;
browser.test.sendMessage(`${_eventName} received`, args);
}
});
browser.test.sendMessage("background started");
},
},
manifest: {
manifest_version: 3,
description: eventName,
background: { scripts: ["background.js"] },
browser_specific_settings: {
gecko: { id: "event_page_extension@mochi.test" },
},
permissions: ["accountsRead", "messagesRead", "messagesMove"],
},
});
await ext.startup();
await ext.awaitMessage("background started");
// The listener should be persistent, but not primed.
assertPersistentListeners(ext, "messages", eventName, { primed: false });
await ext.terminateBackground({ disableResetIdleForTest: true });
// Verify the primed persistent listener.
assertPersistentListeners(ext, "messages", eventName, { primed: true });
await actionCallback();
let rv = await ext.awaitMessage(`${eventName} received`);
await ext.awaitMessage("background started");
// The listener should be persistent, but not primed.
assertPersistentListeners(ext, "messages", eventName, { primed: false });
await ext.unload();
return rv;
}
let account, rootFolder, subFolders;
add_task(
@ -136,8 +206,19 @@ add_task(
skip_if: () => IS_NNTP,
},
async function test_update() {
await AddonTestUtils.promiseStartupManager();
let files = {
"background.js": async () => {
async function capturePrimedEvent(eventName, callback) {
let eventPageExtensionReadyPromise = window.waitForMessage();
browser.test.sendMessage("capturePrimedEvent", eventName);
await eventPageExtensionReadyPromise;
let eventPageExtensionFinishedPromise = window.waitForMessage();
callback();
return eventPageExtensionFinishedPromise;
}
function newUpdatePromise(numberOfEventsToCollapse = 1) {
return new Promise(resolve => {
let seenEvents = {};
@ -178,32 +259,64 @@ add_task(
// Test that setting flagged works.
let updatePromise = newUpdatePromise();
await browser.messages.update(message.id, { flagged: true });
let primedUpdatedInfo = await capturePrimedEvent("onUpdated", () =>
browser.messages.update(message.id, { flagged: true })
);
let updateInfo = await updatePromise;
window.assertDeepEqual(
[updateInfo.msg, updateInfo.props],
primedUpdatedInfo,
"The primed and non-primed onUpdated events should return the same values",
{ strict: true }
);
browser.test.assertEq(message.id, updateInfo.msg.id);
window.assertDeepEqual({ flagged: true }, updateInfo.props);
await window.sendMessage("flagged");
// Test that setting read works.
updatePromise = newUpdatePromise();
await browser.messages.update(message.id, { read: true });
primedUpdatedInfo = await capturePrimedEvent("onUpdated", () =>
browser.messages.update(message.id, { read: true })
);
updateInfo = await updatePromise;
window.assertDeepEqual(
[updateInfo.msg, updateInfo.props],
primedUpdatedInfo,
"The primed and non-primed onUpdated events should return the same values",
{ strict: true }
);
browser.test.assertEq(message.id, updateInfo.msg.id);
window.assertDeepEqual({ read: true }, updateInfo.props);
await window.sendMessage("read");
// Test that setting junk works.
updatePromise = newUpdatePromise();
await browser.messages.update(message.id, { junk: true });
primedUpdatedInfo = await capturePrimedEvent("onUpdated", () =>
browser.messages.update(message.id, { junk: true })
);
updateInfo = await updatePromise;
window.assertDeepEqual(
[updateInfo.msg, updateInfo.props],
primedUpdatedInfo,
"The primed and non-primed onUpdated events should return the same values",
{ strict: true }
);
browser.test.assertEq(message.id, updateInfo.msg.id);
window.assertDeepEqual({ junk: true }, updateInfo.props);
await window.sendMessage("junk");
// Test that setting one tag works.
updatePromise = newUpdatePromise();
await browser.messages.update(message.id, { tags: [tags[0].key] });
primedUpdatedInfo = await capturePrimedEvent("onUpdated", () =>
browser.messages.update(message.id, { tags: [tags[0].key] })
);
updateInfo = await updatePromise;
window.assertDeepEqual(
[updateInfo.msg, updateInfo.props],
primedUpdatedInfo,
"The primed and non-primed onUpdated events should return the same values",
{ strict: true }
);
browser.test.assertEq(message.id, updateInfo.msg.id);
window.assertDeepEqual({ tags: [tags[0].key] }, updateInfo.props);
await window.sendMessage("tags1");
@ -288,6 +401,9 @@ add_task(
manifest: {
background: { scripts: ["utils.js", "background.js"] },
permissions: ["accountsRead", "messagesRead"],
browser_specific_settings: {
gecko: { id: "messages.update@mochi.test" },
},
},
});
@ -296,91 +412,110 @@ add_task(
ok(!message.isRead);
equal(message.getStringProperty("keywords"), "testkeyword");
extension.onMessage("capturePrimedEvent", async eventName => {
let primedEventData = await event_page_extension(eventName, () => {
// Resume execution in the main test, after the event page extension is
// ready to capture the event with deactivated background.
extension.sendMessage();
});
extension.sendMessage(...primedEventData);
});
extension.onMessage("flagged", async () => {
await TestUtils.waitForCondition(() => message.isFlagged);
extension.sendMessage();
});
extension.onMessage("read", async () => {
await TestUtils.waitForCondition(() => message.isRead);
extension.sendMessage();
});
extension.onMessage("junk", async () => {
await TestUtils.waitForCondition(
() => message.getStringProperty("junkscore") == 100
);
extension.sendMessage();
});
extension.onMessage("tags1", async () => {
if (IS_IMAP) {
// Only IMAP sets the junk/nonjunk keyword.
await TestUtils.waitForCondition(
() =>
message.getStringProperty("keywords") == "testkeyword junk $label1"
);
} else {
await TestUtils.waitForCondition(
() => message.getStringProperty("keywords") == "testkeyword $label1"
);
}
extension.sendMessage();
});
extension.onMessage("tags2", async () => {
if (IS_IMAP) {
await TestUtils.waitForCondition(
() =>
message.getStringProperty("keywords") ==
"testkeyword junk $label2 $label3"
);
} else {
await TestUtils.waitForCondition(
() =>
message.getStringProperty("keywords") ==
"testkeyword $label2 $label3"
);
}
extension.sendMessage();
});
extension.onMessage("empty", async () => {
await TestUtils.waitForCondition(() => message.isFlagged);
await TestUtils.waitForCondition(() => message.isRead);
if (IS_IMAP) {
await TestUtils.waitForCondition(
() =>
message.getStringProperty("keywords") ==
"testkeyword junk $label2 $label3"
);
} else {
await TestUtils.waitForCondition(
() =>
message.getStringProperty("keywords") ==
"testkeyword $label2 $label3"
);
}
extension.sendMessage();
});
extension.onMessage("clear", async () => {
await TestUtils.waitForCondition(() => !message.isFlagged);
await TestUtils.waitForCondition(() => !message.isRead);
await TestUtils.waitForCondition(
() => message.getStringProperty("junkscore") == 0
);
if (IS_IMAP) {
await TestUtils.waitForCondition(
() => message.getStringProperty("keywords") == "testkeyword nonjunk"
);
} else {
await TestUtils.waitForCondition(
() => message.getStringProperty("keywords") == "testkeyword"
);
}
extension.sendMessage();
});
await extension.startup();
extension.sendMessage({
folder: { accountId: account.key, path: "/test0" },
size: message.messageSize,
});
await extension.awaitMessage("flagged");
await TestUtils.waitForCondition(() => message.isFlagged);
extension.sendMessage();
await extension.awaitMessage("read");
await TestUtils.waitForCondition(() => message.isRead);
extension.sendMessage();
await extension.awaitMessage("junk");
await TestUtils.waitForCondition(
() => message.getStringProperty("junkscore") == 100
);
extension.sendMessage();
await extension.awaitMessage("tags1");
if (IS_IMAP) {
// Only IMAP sets the junk/nonjunk keyword.
await TestUtils.waitForCondition(
() =>
message.getStringProperty("keywords") == "testkeyword junk $label1"
);
} else {
await TestUtils.waitForCondition(
() => message.getStringProperty("keywords") == "testkeyword $label1"
);
}
extension.sendMessage();
await extension.awaitMessage("tags2");
if (IS_IMAP) {
await TestUtils.waitForCondition(
() =>
message.getStringProperty("keywords") ==
"testkeyword junk $label2 $label3"
);
} else {
await TestUtils.waitForCondition(
() =>
message.getStringProperty("keywords") == "testkeyword $label2 $label3"
);
}
extension.sendMessage();
await extension.awaitMessage("empty");
await TestUtils.waitForCondition(() => message.isFlagged);
await TestUtils.waitForCondition(() => message.isRead);
if (IS_IMAP) {
await TestUtils.waitForCondition(
() =>
message.getStringProperty("keywords") ==
"testkeyword junk $label2 $label3"
);
} else {
await TestUtils.waitForCondition(
() =>
message.getStringProperty("keywords") == "testkeyword $label2 $label3"
);
}
extension.sendMessage();
await extension.awaitMessage("clear");
await TestUtils.waitForCondition(() => !message.isFlagged);
await TestUtils.waitForCondition(() => !message.isRead);
await TestUtils.waitForCondition(
() => message.getStringProperty("junkscore") == 0
);
if (IS_IMAP) {
await TestUtils.waitForCondition(
() => message.getStringProperty("keywords") == "testkeyword nonjunk"
);
} else {
await TestUtils.waitForCondition(
() => message.getStringProperty("keywords") == "testkeyword"
);
}
extension.sendMessage();
await extension.awaitFinish("finished");
await extension.unload();
await AddonTestUtils.promiseShutdownManager();
}
);
@ -389,15 +524,24 @@ add_task(
skip_if: () => IS_NNTP,
},
async function test_move_copy_delete() {
await AddonTestUtils.promiseStartupManager();
let files = {
"background.js": async () => {
async function capturePrimedEvent(eventName, callback) {
let eventPageExtensionReadyPromise = window.waitForMessage();
browser.test.sendMessage("capturePrimedEvent", eventName);
await eventPageExtensionReadyPromise;
let eventPageExtensionFinishedPromise = window.waitForMessage();
callback();
return eventPageExtensionFinishedPromise;
}
async function checkMessagesInFolder(expectedKeys, folder) {
let expectedSubjects = expectedKeys.map(k => messages[k].subject);
browser.test.log("expected: " + expectedSubjects);
let { messages: actualMessages } = await browser.messages.list(
folder
);
browser.test.log("actual: " + actualMessages.map(m => m.subject));
browser.test.assertEq(expectedSubjects.length, actualMessages.length);
for (let m of actualMessages) {
@ -517,7 +661,18 @@ add_task(
// Move one message to another folder.
let movePromise = newMovePromise();
await browser.messages.move([messages.Red.id], testFolder2);
let primedMoveInfo = await capturePrimedEvent("onMoved", () =>
browser.messages.move([messages.Red.id], testFolder2)
);
window.assertDeepEqual(
await movePromise,
{
srcMsgs: primedMoveInfo[0].messages,
dstMsgs: primedMoveInfo[1].messages,
},
"The primed and non-primed onMoved events should return the same values",
{ strict: true }
);
await checkEventInformation(
movePromise,
["Red"],
@ -533,7 +688,18 @@ add_task(
// And back again.
movePromise = newMovePromise();
await browser.messages.move([messages.Red.id], testFolder1);
primedMoveInfo = await capturePrimedEvent("onMoved", () =>
browser.messages.move([messages.Red.id], testFolder1)
);
window.assertDeepEqual(
await movePromise,
{
srcMsgs: primedMoveInfo[0].messages,
dstMsgs: primedMoveInfo[1].messages,
},
"The primed and non-primed onMoved events should return the same values",
{ strict: true }
);
await checkEventInformation(
movePromise,
["Red"],
@ -549,9 +715,20 @@ add_task(
// Move two messages to another folder.
movePromise = newMovePromise();
await browser.messages.move(
[messages.Green.id, messages.My.id],
testFolder2
primedMoveInfo = await capturePrimedEvent("onMoved", () =>
browser.messages.move(
[messages.Green.id, messages.My.id],
testFolder2
)
);
window.assertDeepEqual(
await movePromise,
{
srcMsgs: primedMoveInfo[0].messages,
dstMsgs: primedMoveInfo[1].messages,
},
"The primed and non-primed onMoved events should return the same values",
{ strict: true }
);
await checkEventInformation(
movePromise,
@ -565,7 +742,18 @@ add_task(
// Move one back again.
movePromise = newMovePromise();
await browser.messages.move([messages.My.id], testFolder1);
primedMoveInfo = await capturePrimedEvent("onMoved", () =>
browser.messages.move([messages.My.id], testFolder1)
);
window.assertDeepEqual(
await movePromise,
{
srcMsgs: primedMoveInfo[0].messages,
dstMsgs: primedMoveInfo[1].messages,
},
"The primed and non-primed onMoved events should return the same values",
{ strict: true }
);
await checkEventInformation(movePromise, ["My"], messages, testFolder1);
await checkMessagesInFolder(
["Red", "Blue", "My", "Happy"],
@ -646,7 +834,18 @@ add_task(
// Copy one message to another folder.
let copyPromise = newCopyPromise();
await browser.messages.copy([messages.Happy.id], testFolder2);
let primedCopyInfo = await capturePrimedEvent("onCopied", () =>
browser.messages.copy([messages.Happy.id], testFolder2)
);
window.assertDeepEqual(
await copyPromise,
{
srcMsgs: primedCopyInfo[0].messages,
dstMsgs: primedCopyInfo[1].messages,
},
"The primed and non-primed onCopied events should return the same values",
{ strict: true }
);
await checkEventInformation(
copyPromise,
["Happy"],
@ -670,9 +869,22 @@ add_task(
// Delete the copied message.
let deletePromise = newDeletePromise();
await browser.messages.delete([folder2Messages[0].id], true);
let primedDeleteLog = await capturePrimedEvent("onDeleted", () =>
browser.messages.delete([folder2Messages[0].id], true)
);
// Check if the delete information is correct.
let deleteLog = await deletePromise;
window.assertDeepEqual(
[
{
id: null,
messages: deleteLog,
},
],
primedDeleteLog,
"The primed and non-primed onDeleted events should return the same values",
{ strict: true }
);
browser.test.assertEq(1, deleteLog.length);
browser.test.assertEq(folder2Messages[0].id, deleteLog[0].id);
// Check if the message was deleted.
@ -686,8 +898,18 @@ add_task(
// Move a message to the trash.
movePromise = newMovePromise();
browser.test.log("this is the other failing bit");
await browser.messages.move([messages.Green.id], trashFolder);
primedMoveInfo = await capturePrimedEvent("onMoved", () =>
browser.messages.move([messages.Green.id], trashFolder)
);
window.assertDeepEqual(
await movePromise,
{
srcMsgs: primedMoveInfo[0].messages,
dstMsgs: primedMoveInfo[1].messages,
},
"The primed and non-primed onMoved events should return the same values",
{ strict: true }
);
await checkEventInformation(
movePromise,
["Green"],
@ -723,17 +945,30 @@ add_task(
"messagesRead",
"messagesDelete",
],
browser_specific_settings: {
gecko: { id: "messages.move@mochi.test" },
},
},
});
Services.prefs.setIntPref("extensions.webextensions.messagesPerPage", 1000);
extension.onMessage("capturePrimedEvent", async eventName => {
let primedEventData = await event_page_extension(eventName, () => {
// Resume execution in the main test, after the event page extension is
// ready to capture the event with deactivated background.
extension.sendMessage();
});
extension.sendMessage(...primedEventData);
});
await extension.startup();
extension.sendMessage(account.key);
await extension.awaitFinish("finished");
await extension.unload();
Services.prefs.clearUserPref("extensions.webextensions.messagesPerPage");
await AddonTestUtils.promiseShutdownManager();
}
);
@ -767,6 +1002,9 @@ add_task(
files,
manifest: {
background: { scripts: ["utils.js", "background.js"] },
browser_specific_settings: {
gecko: { id: "messages.delete@mochi.test" },
},
permissions: ["accountsRead", "messagesMove", "messagesRead"],
},
});
@ -782,7 +1020,7 @@ add_task(
{
skip_if: () => IS_NNTP,
},
async function test_move_anc_copy_without_permission() {
async function test_move_and_copy_without_permission() {
let files = {
"background.js": async () => {
let [accountId] = await window.waitForMessage();
@ -816,6 +1054,9 @@ add_task(
files,
manifest: {
background: { scripts: ["utils.js", "background.js"] },
browser_specific_settings: {
gecko: { id: "messages.move@mochi.test" },
},
permissions: ["messagesRead", "accountsRead"],
},
});

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

@ -5,8 +5,80 @@
var { ExtensionTestUtils } = ChromeUtils.import(
"resource://testing-common/ExtensionXPCShellUtils.jsm"
);
var { AddonTestUtils } = ChromeUtils.import(
"resource://testing-common/AddonTestUtils.jsm"
);
ExtensionTestUtils.mockAppInfo();
AddonTestUtils.maybeInit(this);
registerCleanupFunction(async () => {
// Remove the temporary MozillaMailnews folder, which is not deleted in time when
// the cleanupFunction registered by AddonTestUtils.maybeInit() checks for left over
// files in the temp folder.
// Note: PathUtils.tempDir points to the system temp folder, which is different.
let path = PathUtils.join(
Services.dirsvc.get("TmpD", Ci.nsIFile).path,
"MozillaMailnews"
);
await IOUtils.remove(path, { recursive: true });
});
// Function to start an event page extension (MV3), which can be called whenever
// the main test is about to trigger an event. The extension terminates its
// background and listens for that single event, verifying it is waking up correctly.
async function event_page_extension(eventName, actionCallback) {
let ext = ExtensionTestUtils.loadExtension({
files: {
"background.js": async () => {
// Whenever the extension starts or wakes up, hasFired is set to false. In
// case of a wake-up, the first fired event is the one that woke up the background.
let hasFired = false;
let _eventName = browser.runtime.getManifest().description;
browser.messages[_eventName].addListener(async (...args) => {
// Only send the first event after background wake-up, this should
// be the only one expected.
if (!hasFired) {
hasFired = true;
browser.test.sendMessage(`${_eventName} received`, args);
}
});
browser.test.sendMessage("background started");
},
},
manifest: {
manifest_version: 3,
description: eventName,
background: { scripts: ["background.js"] },
browser_specific_settings: {
gecko: { id: "event_page_extension@mochi.test" },
},
permissions: ["accountsRead", "messagesRead", "messagesMove"],
},
});
await ext.startup();
await ext.awaitMessage("background started");
// The listener should be persistent, but not primed.
assertPersistentListeners(ext, "messages", eventName, { primed: false });
await ext.terminateBackground({ disableResetIdleForTest: true });
// Verify the primed persistent listener.
assertPersistentListeners(ext, "messages", eventName, { primed: true });
await actionCallback();
let rv = await ext.awaitMessage(`${eventName} received`);
await ext.awaitMessage("background started");
// The listener should be persistent, but not primed.
assertPersistentListeners(ext, "messages", eventName, { primed: false });
await ext.unload();
return rv;
}
add_task(async function() {
await AddonTestUtils.promiseStartupManager();
let account = createAccount();
let inbox = await createSubfolder(account.incomingServer.rootFolder, "test1");
@ -17,7 +89,10 @@ add_task(async function() {
{ accountId: "account1", name: "test1", path: "/test1" },
folder
);
browser.test.sendMessage("newMessages", messageList.messages);
browser.test.sendMessage("onNewMailReceived event received", [
folder,
messageList,
]);
});
},
"utils.js": await getUtilsJS(),
@ -40,22 +115,37 @@ add_task(async function() {
inbox.biffState = Ci.nsIMsgFolder.nsMsgBiffState_NewMail;
let inboxMessages = [...inbox.messages];
let newMessages = await extension.awaitMessage("newMessages");
equal(newMessages.length, 1);
equal(newMessages[0].subject, inboxMessages[0].subject);
let newMessages = await extension.awaitMessage(
"onNewMailReceived event received"
);
equal(newMessages[1].messages.length, 1);
equal(newMessages[1].messages[0].subject, inboxMessages[0].subject);
// Create 2 more new messages.
await createMessages(inbox, 2);
inbox.hasNewMessages = true;
inbox.setNumNewMessages(2);
inbox.biffState = Ci.nsIMsgFolder.nsMsgBiffState_NewMail;
let primedOnNewMailReceivedEventData = await event_page_extension(
"onNewMailReceived",
async () => {
await createMessages(inbox, 2);
inbox.hasNewMessages = true;
inbox.setNumNewMessages(2);
inbox.biffState = Ci.nsIMsgFolder.nsMsgBiffState_NewMail;
}
);
inboxMessages = [...inbox.messages];
newMessages = await extension.awaitMessage("newMessages");
equal(newMessages.length, 2);
equal(newMessages[0].subject, inboxMessages[1].subject);
equal(newMessages[1].subject, inboxMessages[2].subject);
newMessages = await extension.awaitMessage(
"onNewMailReceived event received"
);
Assert.deepEqual(
primedOnNewMailReceivedEventData,
newMessages,
"The primed and non-primed onNewMailReceived events should return the same values"
);
equal(newMessages[1].messages.length, 2);
equal(newMessages[1].messages[0].subject, inboxMessages[1].subject);
equal(newMessages[1].messages[1].subject, inboxMessages[2].subject);
await extension.unload();
await AddonTestUtils.promiseShutdownManager();
});

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

@ -8,8 +8,7 @@ var { ExtensionTestUtils } = ChromeUtils.import(
"resource://testing-common/ExtensionXPCShellUtils.jsm"
);
// Create some folders and populate them.
add_task(async function setup() {
add_task(async function test_query() {
let account = createAccount();
let textAttachment = {

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

@ -6,6 +6,8 @@ tags = local webextensions
[include:xpcshell.ini]
[test_ext_accounts.js]
[test_ext_accounts_mv3.js]
[test_ext_identities_mv3.js]
[test_ext_addressBook.js]
support-files = images/**
tags = addrbook

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

@ -1,6 +1,7 @@
[test_ext_experiments.js]
tags = addrbook
[test_ext_folders.js] # NNTP disabled (no support for folder operations).
[test_ext_folders_mv3.js] # NNTP disabled (no support for folder operations).
[test_ext_messages.js] # NNTP disabled (no support for Trash folder).
[test_ext_messages_attachments.js] # IMAP disabled (doesn't work with test server).
support-files = messages/**