Revision control

Copy as Markdown

Other Tools

/* 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/. */
/**
* Tests the many ways to set and clear tags on messages.
*/
requestLongerTimeout(2);
const { MessageGenerator } = ChromeUtils.importESModule(
);
const { storeState } = ChromeUtils.importESModule(
);
const gDbService = Cc["@mozilla.org/msgDatabase/msgDBService;1"].getService(
Ci.nsIMsgDBService
);
const knownTags = new Map([
["$label1", { label: "Important", color: "#FF0000" }],
["$label2", { label: "Work", color: "#FF9900" }],
["$label3", { label: "Personal", color: "#009900" }],
["$label4", { label: "To Do", color: "#3333FF" }],
["$label5", { label: "Later", color: "#993399" }],
]);
const tabmail = document.getElementById("tabmail");
const about3Pane = tabmail.currentAbout3Pane;
let testFolder;
add_setup(async function () {
const allTags = MailServices.tags.getAllTags();
Assert.deepEqual(
allTags.map(t => t.key),
[...knownTags.keys()],
"sanity check tag keys"
);
Assert.deepEqual(
allTags.map(t => ({ label: t.tag, color: t.color })),
[...knownTags.values()],
"sanity check tag labels"
);
// Add some non-default tags. We'll have 9 tags so that the tag added during
// tests brings the total to 10 and we can check that it isn't given an
// access key in the menus (as 10 is two digits it can't be an access key).
MailServices.tags.addTag("black", "#000000", null);
MailServices.tags.addTag("orange", "#ff6600", null);
MailServices.tags.addTag("pink!", "#ff33cc", null);
MailServices.tags.addTag("steelblue", "#4682b4", null);
knownTags.set("black", { label: "black", color: "#000000" });
knownTags.set("orange", { label: "orange", color: "#ff6600" });
knownTags.set("pink!", { label: "pink!", color: "#ff33cc" });
knownTags.set("steelblue", { label: "steelblue", color: "#4682b4" });
document.getElementById("toolbar-menubar").removeAttribute("autohide");
const stateChange = TestUtils.topicObserved("unified-toolbar-state-change");
storeState({ mail: ["tag-message"] });
await stateChange;
const generator = new MessageGenerator();
const account = MailServices.accounts.createLocalMailAccount();
const rootFolder = account.incomingServer.rootFolder.QueryInterface(
Ci.nsIMsgLocalMailFolder
);
testFolder = rootFolder
.createLocalSubfolder("tagsMenu")
.QueryInterface(Ci.nsIMsgLocalMailFolder);
testFolder.addMessageBatch(
generator.makeMessages({}).map(message => message.toMessageString())
);
registerCleanupFunction(() => {
MailServices.accounts.removeAccount(account, false);
storeState({});
MailServices.tags.deleteKey("black");
MailServices.tags.deleteKey("orange");
MailServices.tags.deleteKey("pink!");
MailServices.tags.deleteKey("steelblue");
});
});
/**
* Tests Tags menu items with a single message selected.
* @param {nsIMsgDBHdr} message - The message to operate on.
* @param {Function} openPopupCallback - One of the "open" functions below.
*/
async function subtestSingleMessage(message, openPopupCallback) {
info("checking the menu with no tags set");
Assert.equal(message.getStringProperty("keywords"), "");
let tagsPopup = await openPopupCallback();
await checkTagItems(tagsPopup, knownTags);
await promisePopupClosed(tagsPopup);
testFolder.addKeywordsToMessages([message], "$label1 pink!");
tagsPopup = await openPopupCallback();
await checkTagItems(tagsPopup, knownTags, ["$label1", "pink!"]);
info("adding a tag to the message");
const promptPromise = handleNewTagDialog("test", "#ccff00");
let changePromise = promiseKeywordsChanged(message);
tagsPopup.activateItem(tagsPopup.querySelector(`[id$="addNewTag"]`));
await promptPromise;
await promisePopupClosed(tagsPopup);
await changePromise;
Assert.equal(message.getStringProperty("keywords"), "$label1 pink! test");
const expectedTags = new Map(knownTags);
expectedTags.set("test", { label: "test", color: "#ccff00" });
tagsPopup = await openPopupCallback();
await checkTagItems(tagsPopup, expectedTags, ["$label1", "pink!", "test"]);
info("setting a tag on the message");
changePromise = promiseKeywordsChanged(message);
tagsPopup.activateItem(tagsPopup.querySelector(`[value="$label4"]`));
await promisePopupClosed(tagsPopup);
await changePromise;
Assert.equal(
message.getStringProperty("keywords"),
"$label1 pink! test $label4"
);
tagsPopup = await openPopupCallback();
await checkTagItems(tagsPopup, expectedTags, [
"$label1",
"pink!",
"test",
"$label4",
]);
info("removing a tag from the message");
changePromise = promiseKeywordsChanged(message);
tagsPopup.activateItem(tagsPopup.querySelector(`[value="pink!"]`));
await promisePopupClosed(tagsPopup);
await changePromise;
Assert.equal(message.getStringProperty("keywords"), "$label1 test $label4");
tagsPopup = await openPopupCallback();
await checkTagItems(tagsPopup, expectedTags, ["$label1", "test", "$label4"]);
info("removing all tags from the message");
changePromise = promiseKeywordsChanged(message);
tagsPopup.activateItem(tagsPopup.querySelector(`[id$="tagRemoveAll"]`));
await promisePopupClosed(tagsPopup);
await changePromise;
Assert.equal(message.getStringProperty("keywords"), "");
MailServices.tags.deleteKey("test");
tagsPopup = await openPopupCallback();
await checkTagItems(tagsPopup, knownTags, []);
info("opening the tags section of the preferences");
const tabOpenPromise = BrowserTestUtils.waitForEvent(
tabmail.tabContainer,
"TabOpen"
);
tagsPopup.activateItem(tagsPopup.querySelector(`[id$="manageTags"]`));
await promisePopupClosed(tagsPopup);
const {
detail: { tabInfo },
} = await tabOpenPromise;
if (tabInfo.browser.webProgress?.isLoadingDocument) {
await BrowserTestUtils.browserLoaded(tabInfo.browser);
}
Assert.equal(tabInfo.mode.name, "preferencesTab");
Assert.equal(tabInfo.paneID, "paneGeneral");
Assert.equal(tabInfo.scrollPaneTo, "tagsCategory");
tabmail.closeTab(tabInfo);
}
/**
* Tests Tags menu items with multiple messages selected. Unlike
* `subtestSingleMessage` this function selects the messages to operate on.
* @param {Function} openPopupCallback - One of the "open" functions below.
*/
async function subtestMultipleMessages(openPopupCallback) {
info("selecting multiple messages");
about3Pane.displayFolder(testFolder);
about3Pane.threadTree.selectedIndices = [2, 3, 4];
info("checking the menu with no tags set");
const messages = about3Pane.gDBView.getSelectedMsgHdrs();
Assert.equal(messages[0].getStringProperty("keywords"), "");
Assert.equal(messages[1].getStringProperty("keywords"), "");
Assert.equal(messages[2].getStringProperty("keywords"), "");
let tagsPopup = await openPopupCallback();
await checkTagItems(tagsPopup, knownTags);
await promisePopupClosed(tagsPopup);
testFolder.addKeywordsToMessages([messages[0]], "$label2");
testFolder.addKeywordsToMessages([messages[0], messages[2]], "pink!");
tagsPopup = await openPopupCallback();
await checkTagItems(tagsPopup, knownTags, ["$label2", "pink!"]);
info("adding a tag to the messages");
const promptPromise = handleNewTagDialog("test", "#ccff00");
let changePromise = promiseKeywordsChanged(messages[2]);
tagsPopup.activateItem(tagsPopup.querySelector(`[id$="addNewTag"]`));
await promptPromise;
await promisePopupClosed(tagsPopup);
await changePromise;
Assert.equal(messages[0].getStringProperty("keywords"), "$label2 pink! test");
Assert.equal(messages[1].getStringProperty("keywords"), "test");
Assert.equal(messages[2].getStringProperty("keywords"), "pink! test");
const expectedTags = new Map(knownTags);
expectedTags.set("test", { label: "test", color: "#ccff00" });
tagsPopup = await openPopupCallback();
await checkTagItems(tagsPopup, expectedTags, ["$label2", "pink!", "test"]);
info("setting a tag on the messages");
changePromise = promiseKeywordsChanged(messages[2]);
tagsPopup.activateItem(tagsPopup.querySelector(`[value="$label4"]`));
await promisePopupClosed(tagsPopup);
await changePromise;
Assert.equal(
messages[0].getStringProperty("keywords"),
"$label2 pink! test $label4"
);
Assert.equal(messages[1].getStringProperty("keywords"), "test $label4");
Assert.equal(messages[2].getStringProperty("keywords"), "pink! test $label4");
tagsPopup = await openPopupCallback();
await checkTagItems(tagsPopup, expectedTags, [
"$label2",
"pink!",
"test",
"$label4",
]);
info("removing a tag from the messages");
changePromise = promiseKeywordsChanged(messages[2]);
tagsPopup.activateItem(tagsPopup.querySelector(`[value="pink!"]`));
await promisePopupClosed(tagsPopup);
await changePromise;
Assert.equal(
messages[0].getStringProperty("keywords"),
"$label2 test $label4"
);
Assert.equal(messages[1].getStringProperty("keywords"), "test $label4");
Assert.equal(messages[2].getStringProperty("keywords"), "test $label4");
tagsPopup = await openPopupCallback();
await checkTagItems(tagsPopup, expectedTags, ["$label2", "test", "$label4"]);
info("removing all tags from the messages");
changePromise = promiseKeywordsChanged(messages[2]);
tagsPopup.activateItem(tagsPopup.querySelector(`[id$="tagRemoveAll"]`));
await promisePopupClosed(tagsPopup);
await changePromise;
Assert.equal(messages[0].getStringProperty("keywords"), "");
Assert.equal(messages[1].getStringProperty("keywords"), "");
Assert.equal(messages[2].getStringProperty("keywords"), "");
MailServices.tags.deleteKey("test");
}
/**
* Tests key shortcuts to set and clear tags.
* @param {nsIMsgDBHdr[]} messages - The currently selected messages.
* @param {Window} win - The window to simulate key presses in.
*/
async function subtestKeys(messages, win) {
for (const message of messages) {
Assert.equal(message.getStringProperty("keywords"), "");
}
let changePromise = promiseKeywordsChanged(messages.at(-1));
EventUtils.synthesizeKey("2", {}, win);
await changePromise;
for (const message of messages) {
Assert.equal(
message.getStringProperty("keywords"),
"$label2",
"$label2 should be added"
);
}
changePromise = promiseKeywordsChanged(messages.at(-1));
EventUtils.synthesizeKey("3", {}, win);
await changePromise;
for (const message of messages) {
Assert.equal(
message.getStringProperty("keywords"),
"$label2 $label3",
"$label3 should be added"
);
}
changePromise = promiseKeywordsChanged(messages.at(-1));
EventUtils.synthesizeKey("2", {}, win);
await changePromise;
for (const message of messages) {
Assert.equal(
message.getStringProperty("keywords"),
"$label3",
"$label2 should be removed"
);
}
changePromise = promiseKeywordsChanged(messages.at(-1));
EventUtils.synthesizeKey("3", {}, win);
await changePromise;
for (const message of messages) {
Assert.equal(
message.getStringProperty("keywords"),
"",
"$label3 should be added"
);
}
}
add_task(async function testMailContextFromThreadTree() {
const message = await selectMessageInAbout3Pane();
await subtestSingleMessage(
message,
openMailContextFromThreadTree.bind(undefined, 0)
);
await subtestMultipleMessages(
openMailContextFromThreadTree.bind(undefined, 2)
);
});
add_task(async function testMailContextFromMessagePane() {
const message = await selectMessageInAbout3Pane();
await subtestSingleMessage(message, openMailContextFromMessagePane);
});
add_task(async function testHeaderPopup() {
const message = await selectMessageInAbout3Pane();
await subtestSingleMessage(message, openHeaderPopup);
});
add_task(async function testToolbarPopup() {
const message = await selectMessageInAbout3Pane();
await subtestSingleMessage(message, openToolbarPopup);
await subtestMultipleMessages(openToolbarPopup);
});
add_task(async function testMessageMenu() {
const message = await selectMessageInAbout3Pane();
await subtestSingleMessage(message, openMessageMenu);
await subtestMultipleMessages(openMessageMenu);
}).skip(AppConstants.platform == "macosx");
add_task(async function testKeys() {
about3Pane.displayFolder(testFolder);
about3Pane.threadTree.selectedIndices = [5, 7];
info("testing keys with focus on the folder pane");
about3Pane.folderTree.focus();
await subtestKeys(about3Pane.gDBView.getSelectedMsgHdrs(), about3Pane);
info("testing keys with focus on the thread pane");
about3Pane.threadTree.table.body.focus();
await subtestKeys(about3Pane.gDBView.getSelectedMsgHdrs(), about3Pane);
info("testing keys with focus on the message pane");
await selectMessageInAbout3Pane();
const messagePaneBrowser =
about3Pane.messageBrowser.contentWindow.getMessagePaneBrowser();
messagePaneBrowser.focus();
await subtestKeys(
about3Pane.gDBView.getSelectedMsgHdrs(),
messagePaneBrowser.contentWindow
);
});
add_task(async function testMessageTab() {
const tabPromise = BrowserTestUtils.waitForEvent(window, "MsgLoaded");
const tab = window.OpenMessageInNewTab(testFolder.messages.getNext(), {
background: false,
});
const { detail: message } = await tabPromise;
await messageLoadedIn(tab.chromeBrowser);
await TestUtils.waitForTick();
info("testing mail context from messagepane");
await subtestSingleMessage(message, openMailContextFromMessagePane);
info("testing header popup");
await subtestSingleMessage(message, openHeaderPopup);
info("testing toolbar popup");
await subtestSingleMessage(message, openToolbarPopup);
if (AppConstants.platform != "macosx") {
info("testing message menu");
await subtestSingleMessage(message, openMessageMenu);
}
info("testing keys");
await subtestKeys([message], window);
tabmail.closeOtherTabs(0);
});
add_task(async function testMessageWindow() {
const winPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
window.MsgOpenNewWindowForMessage(testFolder.messages.getNext());
const win = await winPromise;
const { detail: message } = await BrowserTestUtils.waitForEvent(
win,
"MsgLoaded"
);
const aboutMessage = win.messageBrowser.contentWindow;
await messageLoadedIn(win.messageBrowser);
info("testing mail context from message pane");
await subtestSingleMessage(
message,
openMailContextFromMessagePane.bind(undefined, aboutMessage)
);
info("testing header popup");
await subtestSingleMessage(
message,
openHeaderPopup.bind(undefined, aboutMessage)
);
info("testing toolbar popup");
await subtestSingleMessage(message, openToolbarPopup.bind(undefined, win));
if (AppConstants.platform != "macosx") {
info("testing message menu");
await subtestSingleMessage(message, openMessageMenu.bind(undefined, win));
}
info("testing keys");
await subtestKeys([message], win);
await BrowserTestUtils.closeWindow(win);
});
/**
* Selects the first message in the thread pane and waits for it to load.
* @returns {nsIMsgDBHdr}
*/
async function selectMessageInAbout3Pane() {
about3Pane.displayFolder(testFolder);
about3Pane.threadTree.selectedIndex = 0;
await messageLoadedIn(about3Pane.messageBrowser);
return about3Pane.gDBView.hdrForFirstSelectedMessage;
}
/**
* Open the context menu from the thread pane and the Tags menu within.
* @param {integer} indexToClick - The index of the row to right-click on.
* @returns {MozMenuPopup}
*/
async function openMailContextFromThreadTree(indexToClick) {
const menu = about3Pane.document.getElementById("mailContext");
const tagsMenu = about3Pane.document.getElementById("mailContext-tags");
const tagsPopup = tagsMenu.menupopup;
EventUtils.synthesizeMouseAtCenter(
about3Pane.threadTree.getRowAtIndex(indexToClick),
{ type: "contextmenu" },
about3Pane
);
await BrowserTestUtils.waitForPopupEvent(menu, "shown");
tagsMenu.openMenu(true);
await BrowserTestUtils.waitForPopupEvent(tagsPopup, "shown");
await TestUtils.waitForTick();
return tagsPopup;
}
/**
* Open the context menu from the message pane and the Tags menu within.
* @param {Window} [aboutMessage] - If not given, the about:message window
* from the current tab in the main window.
* @returns {MozMenuPopup}
*/
async function openMailContextFromMessagePane(
aboutMessage = tabmail.currentAboutMessage
) {
const topWindow =
aboutMessage.parent == aboutMessage.top
? aboutMessage
: aboutMessage.parent;
const menu = topWindow.document.getElementById("mailContext");
const tagsMenu = topWindow.document.getElementById("mailContext-tags");
const tagsPopup = tagsMenu.menupopup;
const messagePaneBrowser = aboutMessage.getMessagePaneBrowser();
await BrowserTestUtils.synthesizeMouseAtCenter(
":root",
{ type: "contextmenu" },
messagePaneBrowser
);
await BrowserTestUtils.waitForPopupEvent(menu, "shown");
tagsMenu.openMenu(true);
await BrowserTestUtils.waitForPopupEvent(tagsPopup, "shown");
await TestUtils.waitForTick();
return tagsPopup;
}
/**
* Open the message header More button and the Tags menu within.
* @param {Window} [aboutMessage] - If not given, the about:message window
* from the current tab in the main window.
* @returns {MozMenuPopup}
*/
async function openHeaderPopup(aboutMessage = tabmail.currentAboutMessage) {
const button = aboutMessage.document.getElementById("otherActionsButton");
const popup = aboutMessage.document.getElementById("otherActionsPopup");
const tagsMenu = aboutMessage.document.getElementById("otherActionsTag");
const tagsPopup = tagsMenu.menupopup;
EventUtils.synthesizeMouseAtCenter(button, {}, aboutMessage);
await BrowserTestUtils.waitForPopupEvent(popup, "shown");
tagsMenu.openMenu(true);
await BrowserTestUtils.waitForPopupEvent(tagsPopup, "shown");
await TestUtils.waitForTick();
return tagsPopup;
}
/**
* Open the Tags toolbar button popup.
* @param {Window} [win] - If not given, the main window.
* @returns {MozMenuPopup}
*/
async function openToolbarPopup(win = window) {
await SimpleTest.promiseFocus(win);
const button = win.document.querySelector(
`#button-tag, #unifiedToolbarContent [item-id="tag-message"]`
);
const tagsPopup =
button.menupopup ?? win.document.getElementById("toolbarTagPopup");
EventUtils.synthesizeMouseAtCenter(button, {}, win);
await BrowserTestUtils.waitForPopupEvent(tagsPopup, "shown");
await TestUtils.waitForTick();
return tagsPopup;
}
/**
* Open the menu bar Message menu and the Tags menu within. This must not be
* called on macOS where the menu bar is inaccessible to tests.
* @param {Window} [win] - If not given, the main window.
* @returns {MozMenuPopup}
*/
async function openMessageMenu(win = window) {
await SimpleTest.promiseFocus(win);
const menu = win.document.getElementById("messageMenu");
const tagsMenu = win.document.getElementById("tagMenu");
const tagsPopup = tagsMenu.menupopup;
EventUtils.synthesizeMouseAtCenter(menu, {}, win);
await BrowserTestUtils.waitForPopupEvent(menu, "shown");
tagsMenu.openMenu(true);
await BrowserTestUtils.waitForPopupEvent(tagsPopup, "shown");
await TestUtils.waitForTick();
return tagsPopup;
}
/**
* Wait for a tags menu, and its parent if there is one, to be closed. If the
* menu is open, close it.
* @param {MozMenuPopup} tagsPopup
*/
async function promisePopupClosed(tagsPopup) {
const parentPopup = tagsPopup.parentNode.closest("menupopup");
if (tagsPopup.state == "open") {
tagsPopup.hidePopup();
}
if (parentPopup) {
if (parentPopup.state == "open") {
parentPopup.hidePopup();
}
await BrowserTestUtils.waitForPopupEvent(
tagsPopup.parentNode.closest("menupopup"),
"hidden"
);
}
await SimpleTest.promiseFocus(tagsPopup.ownerGlobal.top);
await TestUtils.waitForTick();
}
/**
* Check the tag items on a menu are present and correct.
* @param {MozMenuPopup} tagsPopup
* @param {Map<string, object>} expectedTags - A map of the tags that should
* be present on the menu. An entry's key is the tag key, and its value is
* an object containing the tag label and color.
* @param {string[]} expectedChecked
*/
function checkTagItems(tagsPopup, expectedTags, expectedChecked = []) {
let index = 0;
let element = tagsPopup.querySelector("menuseparator:last-of-type");
for (const [key, { label, color }] of expectedTags) {
index++;
element = element.nextElementSibling;
if (index < 10) {
Assert.equal(element.accessKey, index, "menu item accessKey");
Assert.equal(element.label, `${index} ${label}`, "menu item label");
} else {
Assert.equal(element.accessKey, null, "menu item accessKey");
Assert.equal(element.label.trim(), label, "menu item label");
}
Assert.equal(element.value, key, "menu item value");
const r = parseInt(color.substring(1, 3), 16);
const g = parseInt(color.substring(3, 5), 16);
const b = parseInt(color.substring(5, 7), 16);
Assert.equal(
element.style.color,
`rgb(${r}, ${g}, ${b})`,
"menu item color"
);
Assert.equal(
element.getAttribute("checked") == "true",
expectedChecked.includes(key),
"menu item checked state"
);
}
Assert.ok(!element.nextElementSibling, "no more tag menu items");
}
/**
* Wait for the new tag dialog to show, and fill it in.
* @param {string} tagToAdd
* @param {string} colorToUse
* @returns {Promise} - Resolves when the tag has shown, filled, and closed.
*/
function handleNewTagDialog(tagToAdd, colorToUse) {
return BrowserTestUtils.promiseAlertDialog(
undefined,
{
callback(win) {
const nameInput = win.document.getElementById("name");
const colorInput = win.document.getElementById("tagColorPicker");
const acceptButton = win.document
.querySelector("dialog")
.getButton("accept");
Assert.equal(
win.document.activeElement,
nameInput,
"name input has focus"
);
Assert.equal(nameInput.value, "", "name input is empty");
EventUtils.sendString(tagToAdd, win);
colorInput.value = colorToUse;
EventUtils.synthesizeMouseAtCenter(acceptButton, {}, win);
},
}
);
}
/**
* Wait for a message's tags to change.
* @param {nsIMsgDBHdr} header
* @returns {Promise} - Resolves when the header's tags change.
*/
function promiseKeywordsChanged(header) {
return new Promise(resolve => {
gDbService.registerPendingListener(testFolder, {
onHdrFlagsChanged() {},
onHdrDeleted() {},
onHdrAdded() {},
onParentChanged() {},
onAnnouncerGoingAway() {},
onReadChanged() {},
onJunkScoreChanged() {},
onHdrPropertyChanged(hdrToChange) {
if (hdrToChange == header) {
gDbService.unregisterPendingListener(this);
TestUtils.waitForTick().then(resolve);
}
},
onEvent() {},
});
});
}