diff --git a/calendar/base/content/dialogs/calendar-event-dialog-attendees-custom-elements.js b/calendar/base/content/dialogs/calendar-event-dialog-attendees-custom-elements.js index 53f47b6973..9e9be33375 100644 --- a/calendar/base/content/dialogs/calendar-event-dialog-attendees-custom-elements.js +++ b/calendar/base/content/dialogs/calendar-event-dialog-attendees-custom-elements.js @@ -9,6 +9,7 @@ // Wrap in a block to prevent leaking to window scope. { const { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm"); + const { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm"); /** * MozCalendarEventFreebusyTimebar is a widget showing the time slot labels - dates and a number of @@ -764,43 +765,7 @@ */ _resolveListByName(value) { let entries = MailServices.headerParser.makeFromDisplayAddress(value); - return entries.length ? this._findListInAddrBooks(entries[0].name) : null; - } - - /** - * Finds list in the address books. - * - * @param {String} entryName Value against which dirName is checked - * @returns {Object} Found list or null - */ - _findListInAddrBooks(entryname) { - let allAddressBooks = MailServices.ab.directories; - - while (allAddressBooks.hasMoreElements()) { - let abDir = null; - try { - abDir = allAddressBooks.getNext().QueryInterface(Ci.nsIAbDirectory); - } catch (ex) { - cal.WARN("[eventDialog] Error Encountered" + ex); - } - - if (abDir != null && abDir.supportsMailingLists) { - let dirs = abDir.childNodes; - while (dirs.hasMoreElements()) { - let dir = null; - try { - dir = dirs.getNext().QueryInterface(Ci.nsIAbDirectory); - } catch (ex) { - cal.WARN("[eventDialog] Error Encountered" + ex); - } - - if (dir && dir.isMailList && dir.dirName == entryname) { - return dir; - } - } - } - } - return null; + return entries.length ? MailUtils.findListInAddressBooks(entries[0].name) : null; } /** diff --git a/mail/base/modules/MailUtils.jsm b/mail/base/modules/MailUtils.jsm index 66a8b613e9..4e9cc50731 100644 --- a/mail/base/modules/MailUtils.jsm +++ b/mail/base/modules/MailUtils.jsm @@ -533,4 +533,30 @@ var MailUtils = { } return null; }, + + /** + * Finds a mailing list anywhere in the address books. + * + * @param {string} entryName - Value against which dirName is checked. + * @returns {nsIAbDirectory|null} - Found list or null. + */ + findListInAddressBooks(entryName) { + let allAddressBooks = MailServices.ab.directories; + + while (allAddressBooks.hasMoreElements()) { + let abDir = allAddressBooks.getNext().QueryInterface(Ci.nsIAbDirectory); + + if (abDir.supportsMailingLists) { + let dirs = abDir.childNodes; + + while (dirs.hasMoreElements()) { + let dir = dirs.getNext().QueryInterface(Ci.nsIAbDirectory); + if (dir.isMailList && dir.dirName == entryName) { + return dir; + } + } + } + } + return null; + }, }; diff --git a/mail/components/addrbook/content/addressbook.js b/mail/components/addrbook/content/addressbook.js index d0909dad7e..ea89d4e26d 100644 --- a/mail/components/addrbook/content/addressbook.js +++ b/mail/components/addrbook/content/addressbook.js @@ -530,10 +530,17 @@ function SetStatusText(total) { ).replace("#1", total); } } else { - statusText = gAddressBookBundle.getFormattedString("totalContactStatus", [ - getSelectedDirectory().dirName, - total, - ]); + let selectedDirectory = getSelectedDirectory(); + // The result of getSelectedDirectory may be null, like when there's a + // mailing list just being created in a brand new address book. + if (selectedDirectory) { + statusText = gAddressBookBundle.getFormattedString( + "totalContactStatus", + [selectedDirectory.dirName, total] + ); + } else { + statusText = ""; + } } gStatusText.setAttribute("value", statusText); diff --git a/mail/components/extensions/test/browser/browser_ext_menus.js b/mail/components/extensions/test/browser/browser_ext_menus.js index 76a8744a83..19175b57a1 100644 --- a/mail/components/extensions/test/browser/browser_ext_menus.js +++ b/mail/components/extensions/test/browser/browser_ext_menus.js @@ -4,17 +4,11 @@ let gAccount, gFolders; -function treeClick(tree, row, column, event) { - let coords = tree.getCoordsForCellItem(row, tree.columns[column], "cell"); - let treeChildren = tree.lastElementChild; - EventUtils.synthesizeMouse( - treeChildren, - coords.x + coords.width / 2, - coords.y + coords.height / 2, - event, - window - ); -} +const { mailTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/mailTestUtils.js" +); + +const treeClick = mailTestUtils.treeClick.bind(null, EventUtils, window); async function checkEvent(extension, menuIds, contexts) { let [event, tab] = await extension.awaitMessage("onShown"); diff --git a/mailnews/addrbook/test/browser/browser.ini b/mailnews/addrbook/test/browser/browser.ini new file mode 100644 index 0000000000..38cf578ef1 --- /dev/null +++ b/mailnews/addrbook/test/browser/browser.ini @@ -0,0 +1,14 @@ +[DEFAULT] +head = head.js +prefs = + ldap_2.servers.osx.description= + ldap_2.servers.osx.dirType=-1 + ldap_2.servers.osx.uri= + mail.provider.suppress_dialog_on_startup=true + mail.spotlight.firstRunDone=true + mail.winsearch.firstRunDone=true + mailnews.start_page.override_url=about:blank + mailnews.start_page.url=about:blank +subsuite = thunderbird + +[browser_mailing_lists.js] diff --git a/mailnews/addrbook/test/browser/browser_mailing_lists.js b/mailnews/addrbook/test/browser/browser_mailing_lists.js new file mode 100644 index 0000000000..c5b9e37aa7 --- /dev/null +++ b/mailnews/addrbook/test/browser/browser_mailing_lists.js @@ -0,0 +1,483 @@ +/* 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/. */ + +/* globals DisplayNameUtils, fixIterator, MailServices, MailUtils */ + +const { mailTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/mailTestUtils.js" +); + +const inputs = { + abName: "Mochitest Address Book", + mlName: "Mochitest Mailing List", + nickName: "Nicky", + description: "Just a test mailing list.", + addresses: [ + "alan@example.com", + "betty@example.com", + "clyde@example.com", + "deb@example.com", + ], + modification: " (modified)", +}; + +const getDisplayedAddress = address => `${address} <${address}>`; + +let global = {}; + +/** + * Set up: create a new address book to hold the mailing list. + */ +add_task(async () => { + let abWindow = await openAddressBookWindow(); + let addressBook = await createNewAddressBook(abWindow, inputs.abName); + + let dirTree = abWindow.document.getElementById("dirTree"); + + /** + * Click a row in the address book list (tree). + * + * @param {number} row - The tree row to click. + * @param {number} clickCount - Number of clicks to synthesize. + */ + let dirTreeClick = (row, clickCount) => { + mailTestUtils.treeClick(EventUtils, abWindow, dirTree, row, 0, { + clickCount, + }); + }; + + global = { + abWindow, + addressBook, + dirTree, + dirTreeClick, + mailListUID: undefined, + }; +}); + +/** + * Create a new mailing list with some addresses, in the new address book. + */ +add_task(async () => { + let mailingListWindowPromise = BrowserTestUtils.promiseAlertDialog( + null, + "chrome://messenger/content/addressbook/abMailListDialog.xul", + // A callback that can interact with the mailing list dialog. + async mlWindow => { + let mlDocument = mlWindow.document; + let mlDocElement = mlDocument.documentElement; + + let listName = mlDocument.getElementById("ListName"); + let listNameFocusEvent = await BrowserTestUtils.waitForEvent( + listName, + "focus" + ); + + let abPopup = mlDocument.getElementById("abPopup"); + let listNickName = mlDocument.getElementById("ListNickName"); + let listDescription = mlDocument.getElementById("ListDescription"); + let addressInput1 = mlDocument.getElementById("addressCol1#1"); + let addressInputsCount = mlDocument + .getElementById("addressingWidget") + .querySelectorAll("input").length; + + is( + abPopup.label, + global.addressBook.dirName, + "the correct address book is selected in the menu" + ); + is( + abPopup.value, + global.addressBook.URI, + "the address book selected in the menu has the correct address book URI" + ); + is(listNameFocusEvent.type, "focus", "list name field is focused"); + is(listName.value, "", "no text in the list name field"); + is(listNickName.value, "", "no text in the list nickname field"); + is(listDescription.value, "", "no text in the description field"); + is(addressInput1.value, "", "no text in the addresses list"); + is(addressInputsCount, 1, "only one address list input exists"); + + EventUtils.sendString(inputs.mlName, mlWindow); + + // Tab to nickname input. + EventUtils.sendKey("TAB", mlWindow); + EventUtils.sendString(inputs.nickName, mlWindow); + + // Tab to description input. + EventUtils.sendKey("TAB", mlWindow); + EventUtils.sendString(inputs.description, mlWindow); + + // Tab to address input and add addresses zero and one by entering + // both of them there. + EventUtils.sendKey("TAB", mlWindow); + EventUtils.sendString(inputs.addresses.slice(0, 2).join(", "), mlWindow); + + mlDocElement.getButton("accept").click(); + } + ); + + is( + global.dirTree.view.getCellText(2, global.dirTree.columns[0]), + inputs.abName, + `address book ("${inputs.abName}") is displayed in the address book list` + ); + + // Select the address book. + global.dirTreeClick(2, 1); + + // Open the new mailing list dialog, the callback above interacts with it. + EventUtils.synthesizeMouseAtCenter( + global.abWindow.document.getElementById("button-newlist"), + { clickCount: 1 }, + global.abWindow + ); + + await mailingListWindowPromise; + + // Confirm that the mailing list and addresses were saved in the backend. + + ok( + DisplayNameUtils.getCardForEmail(inputs.addresses[0]).card, + "address zero was saved" + ); + ok( + DisplayNameUtils.getCardForEmail(inputs.addresses[1]).card, + "address one was saved" + ); + + let childCards = [...global.addressBook.childCards]; + + ok( + childCards.find(card => card.primaryEmail == inputs.addresses[0]), + "address zero was saved in the correct address book" + ); + ok( + childCards.find(card => card.primaryEmail == inputs.addresses[1]), + "address one was saved in the correct address book" + ); + + let mailList = MailUtils.findListInAddressBooks(inputs.mlName); + + // Save the mailing list UID so we can confirm it is the same later. + global.mailListUID = mailList.UID; + + ok(mailList, "mailing list was created"); + ok( + global.addressBook.hasMailListWithName(inputs.mlName), + "mailing list was created in the correct address book" + ); + is(mailList.dirName, inputs.mlName, "mailing list name was saved"); + is( + mailList.listNickName, + inputs.nickName, + "mailing list nick name was saved" + ); + is( + mailList.description, + inputs.description, + "mailing list description was saved" + ); + + let listCards = [...fixIterator(mailList.addressLists, Ci.nsIAbCard)]; + + ok( + listCards[0].hasEmailAddress(inputs.addresses[0]), + "address zero was saved in the mailing list" + ); + ok( + listCards[1].hasEmailAddress(inputs.addresses[1]), + "address one was saved in the mailing list" + ); + is(listCards.length, 2, "two cards exist in the mailing list"); +}); + +/** + * Open the mailing list dialog and modify the mailing list. + */ +add_task(async () => { + let mailingListWindowPromise = BrowserTestUtils.promiseAlertDialog( + null, + "chrome://messenger/content/addressbook/abEditListDialog.xul", + // A callback that can interact with the mailing list dialog. + async mlWindow => { + let mlDocument = mlWindow.document; + let mlDocElement = mlDocument.documentElement; + + // The address input nodes are not there yet when the dialog window is + // loaded, so wait until they exist. + await mailTestUtils.awaitElementExistence( + MutationObserver, + mlDocument, + "addressingWidget", + "addressCol1#3" + ); + + await BrowserTestUtils.waitForEvent( + mlDocument.getElementById("addressCol1#3"), + "focus" + ); + + let listName = mlDocument.getElementById("ListName"); + let listNickName = mlDocument.getElementById("ListNickName"); + let listDescription = mlDocument.getElementById("ListDescription"); + let addressInput1 = mlDocument.getElementById("addressCol1#1"); + let addressInput2 = mlDocument.getElementById("addressCol1#2"); + + is(listName.value, inputs.mlName, "list name is displayed correctly"); + is( + listNickName.value, + inputs.nickName, + "list nickname is displayed correctly" + ); + is( + listDescription.value, + inputs.description, + "list description is displayed correctly" + ); + is( + addressInput1 && addressInput1.value, + getDisplayedAddress(inputs.addresses[0]), + "address zero is displayed correctly" + ); + is( + addressInput2 && addressInput2.value, + getDisplayedAddress(inputs.addresses[1]), + "address one is displayed correctly" + ); + + let textInputs = mlDocument.querySelectorAll(".textbox-addressingWidget"); + is(textInputs.length, 3, "no extraneous addresses are displayed"); + + // Add addresses two and three. + EventUtils.sendString(inputs.addresses.slice(2, 4).join(", "), mlWindow); + EventUtils.sendKey("RETURN", mlWindow); + await new Promise(resolve => mlWindow.setTimeout(resolve)); + + // Delete the address in the second row (address one). + EventUtils.synthesizeMouseAtCenter( + addressInput2, + { clickCount: 1 }, + mlWindow + ); + EventUtils.synthesizeKey("a", { accelKey: true }, mlWindow); + EventUtils.sendKey("BACK_SPACE", mlWindow); + + // Modify the list's name, nick name, and description fields. + let modifyField = id => { + EventUtils.synthesizeMouseAtCenter(id, { clickCount: 1 }, mlWindow); + EventUtils.sendKey("END", mlWindow); + EventUtils.sendString(inputs.modification, mlWindow); + }; + modifyField(listName); + modifyField(listNickName); + modifyField(listDescription); + + mlDocElement.getButton("accept").click(); + } + ); + + is( + global.dirTree.view.getCellText(2, global.dirTree.columns[0]), + inputs.abName, + `address book ("${inputs.abName}") is displayed in the address book list` + ); + + // Double-click on the address book name to reveal the mailing list. + global.dirTreeClick(2, 2); + + is( + global.dirTree.view.getCellText(3, global.dirTree.columns[0]), + inputs.mlName, + `mailing list ("${inputs.mlName}") is displayed in the address book list` + ); + + // Open the mailing list dialog, the callback above interacts with it. + global.dirTreeClick(3, 2); + + await mailingListWindowPromise; + + // Confirm that the mailing list and addresses were saved in the backend. + + ok( + DisplayNameUtils.getCardForEmail(inputs.addresses[2]).card, + "address two was saved" + ); + ok( + DisplayNameUtils.getCardForEmail(inputs.addresses[3]).card, + "address three was saved" + ); + + let childCards = [...global.addressBook.childCards]; + + ok( + childCards.find(card => card.primaryEmail == inputs.addresses[2]), + "address two was saved in the correct address book" + ); + ok( + childCards.find(card => card.primaryEmail == inputs.addresses[3]), + "address three was saved in the correct address book" + ); + + let mailList = MailUtils.findListInAddressBooks( + inputs.mlName + inputs.modification + ); + + is(mailList && mailList.UID, global.mailListUID, "mailing list still exists"); + + ok( + global.addressBook.hasMailListWithName(inputs.mlName + inputs.modification), + "mailing list is still in the correct address book" + ); + is( + mailList.dirName, + inputs.mlName + inputs.modification, + "modified mailing list name was saved" + ); + is( + mailList.listNickName, + inputs.nickName + inputs.modification, + "modified mailing list nick name was saved" + ); + is( + mailList.description, + inputs.description + inputs.modification, + "modified mailing list description was saved" + ); + + let listCards = [...fixIterator(mailList.addressLists, Ci.nsIAbCard)]; + + ok( + listCards[0].hasEmailAddress(inputs.addresses[0]), + "address zero was saved in the mailing list (is still there)" + ); + ok( + listCards[1].hasEmailAddress(inputs.addresses[2]), + "address two was saved in the mailing list" + ); + ok( + listCards[2].hasEmailAddress(inputs.addresses[3]), + "address three was saved in the mailing list" + ); + + let hasAddressOne = listCards.find(card => + card.hasEmailAddress(inputs.addresses[1]) + ); + + ok(!hasAddressOne, "address one was deleted from the mailing list"); + + is(listCards.length, 3, "three cards exist in the mailing list"); +}); + +/** + * Open the mailing list dialog and confirm the changes are displayed. + */ +add_task(async () => { + let mailingListWindowPromise = BrowserTestUtils.promiseAlertDialog( + null, + "chrome://messenger/content/addressbook/abEditListDialog.xul", + // A callback that can interact with the mailing list dialog. + async mailingListWindow => { + let mlDocument = mailingListWindow.document; + let mlDocElement = mlDocument.documentElement; + + // The address input nodes are not there yet when the dialog window is + // loaded, so wait until they exist. + await mailTestUtils.awaitElementExistence( + MutationObserver, + mlDocument, + "addressingWidget", + "addressCol1#4" + ); + + await BrowserTestUtils.waitForEvent( + mlDocument.getElementById("addressCol1#4"), + "focus" + ); + + let listName = mlDocument.getElementById("ListName"); + let listNickName = mlDocument.getElementById("ListNickName"); + let listDescription = mlDocument.getElementById("ListDescription"); + let addressInput1 = mlDocument.getElementById("addressCol1#1"); + let addressInput2 = mlDocument.getElementById("addressCol1#2"); + let addressInput3 = mlDocument.getElementById("addressCol1#3"); + + is( + listName.value, + inputs.mlName + inputs.modification, + "modified list name is displayed correctly" + ); + is( + listNickName.value, + inputs.nickName + inputs.modification, + "modified list nickname is displayed correctly" + ); + is( + listDescription.value, + inputs.description + inputs.modification, + "modified list description is displayed correctly" + ); + is( + addressInput1 && addressInput1.value, + getDisplayedAddress(inputs.addresses[0]), + "address zero is displayed correctly (is still there)" + ); + is( + addressInput2 && addressInput2.value, + getDisplayedAddress(inputs.addresses[2]), + "address two is displayed correctly" + ); + is( + addressInput3 && addressInput3.value, + getDisplayedAddress(inputs.addresses[3]), + "address three is displayed correctly" + ); + + let textInputs = mlDocument.querySelectorAll(".textbox-addressingWidget"); + is(textInputs.length, 4, "no extraneous addresses are displayed"); + + mlDocElement.getButton("cancel").click(); + } + ); + + is( + global.dirTree.view.getCellText(3, global.dirTree.columns[0]), + inputs.mlName, + `mailing list ("${inputs.mlName}") is displayed in the address book list` + ); + + // Open the mailing list dialog, the callback above interacts with it. + global.dirTreeClick(3, 2); + + await mailingListWindowPromise; +}); + +/** + * Tear down: delete the address book and close the address book window. + */ +add_task(async () => { + let mailingListWindowPromise = BrowserTestUtils.promiseAlertDialog( + "accept", + "chrome://global/content/commonDialog.xul" + ); + + is( + global.dirTree.view.getCellText(2, global.dirTree.columns[0]), + inputs.abName, + `address book ("${inputs.abName}") is displayed in the address book list` + ); + + global.dirTreeClick(2, 1); + EventUtils.sendKey("DELETE", global.abWindow); + + await mailingListWindowPromise; + + let addressBook = [...MailServices.ab.directories].find( + directory => directory.dirName == inputs.abName + ); + + ok(!addressBook, "address book was deleted"); + + global.abWindow.close(); +}); diff --git a/mailnews/addrbook/test/browser/head.js b/mailnews/addrbook/test/browser/head.js new file mode 100644 index 0000000000..66574fa7a9 --- /dev/null +++ b/mailnews/addrbook/test/browser/head.js @@ -0,0 +1,55 @@ +/* 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/. */ + +/* globals MailServices */ + +async function openAddressBookWindow() { + let addressBookWindowPromise = BrowserTestUtils.domWindowOpened( + null, + async win => { + // This test function waits until the "load" event has happened. + await BrowserTestUtils.waitForEvent(win, "load"); + + return ( + win.document.documentURI == + "chrome://messenger/content/addressbook/addressbook.xul" + ); + } + ); + + const addressBookButton = document.getElementById("button-address"); + EventUtils.synthesizeMouseAtCenter(addressBookButton, { clickCount: 1 }); + + let abWindow = await addressBookWindowPromise; + + await new Promise(resolve => abWindow.setTimeout(resolve)); + + ok(abWindow && abWindow instanceof Window, "address book window was opened"); + + return abWindow; +} + +async function createNewAddressBook(abWindow, abName) { + let newAddressBookPromise = BrowserTestUtils.promiseAlertDialog( + null, + "chrome://messenger/content/addressbook/abAddressBookNameDialog.xul", + abNameDialog => { + EventUtils.sendString(abName, abNameDialog); + abNameDialog.document.documentElement.getButton("accept").click(); + } + ); + + // Using the UI was unreliable so just call the function. + abWindow.AbNewAddressBook(); + + await newAddressBookPromise; + + let addressBook = [...MailServices.ab.directories].find( + directory => directory.dirName == abName + ); + + ok(addressBook, "a new address book was created"); + + return addressBook; +} diff --git a/mailnews/addrbook/test/moz.build b/mailnews/addrbook/test/moz.build index 281cb83cd3..7189d7a51a 100644 --- a/mailnews/addrbook/test/moz.build +++ b/mailnews/addrbook/test/moz.build @@ -7,3 +7,7 @@ XPCSHELL_TESTS_MANIFESTS += [ 'unit/xpcshell-jsaddrbook.ini', 'unit/xpcshell-mdbaddrbook.ini', ] + +BROWSER_CHROME_MANIFESTS += [ + 'browser/browser.ini', +] diff --git a/mailnews/test/resources/mailTestUtils.js b/mailnews/test/resources/mailTestUtils.js index 92c0dbeb81..78f5a472d1 100644 --- a/mailnews/test/resources/mailTestUtils.js +++ b/mailnews/test/resources/mailTestUtils.js @@ -548,4 +548,61 @@ var mailTestUtils = { } return -1; }, + + /** + * Click on a particular cell in a tree. `window` is not defined here in this + * file, so we can't provide it as a default argument. Similarly, we pass in + * `EventUtils` as an argument because importing it here does not work + * because `window` is not defined. + * + * @param {Object} EventUtils - The EventUtils object. + * @param {Window} win - The window the tree is in. + * @param {Element} tree - The tree element. + * @param {number} row - The tree row to click on. + * @param {number} column - The tree column to click on. + * @param {Object} event - The mouse event to synthesize, e.g. `{ clickCount: 2 }`. + */ + treeClick(EventUtils, win, tree, row, column, event) { + let coords = tree.getCoordsForCellItem(row, tree.columns[column], "cell"); + let treeChildren = tree.lastElementChild; + EventUtils.synthesizeMouse( + treeChildren, + coords.x + coords.width / 2, + coords.y + coords.height / 2, + event, + win + ); + }, + + /** + * For waiting until an element exists in a given document. Pass in the + * `MutationObserver` as an argument because importing it here does not work + * because `window` is not defined here. + * + * @param {Object} MutationObserver - The MutationObserver object. + * @param {Document} doc - Document that contains the elements. + * @param {string} observedNodeId - Id of the element to observe. + * @param {string} awaitedNodeId - Id of the element that will soon exist. + * @return {Promise.} - A promise fulfilled when the element exists. + */ + awaitElementExistence(MutationObserver, doc, observedNodeId, awaitedNodeId) { + return new Promise(resolve => { + let outerObserver = new MutationObserver((mutationsList, observer) => { + for (let mutation of mutationsList) { + if (mutation.type == "childList" && mutation.addedNodes.length) { + let element = doc.getElementById(awaitedNodeId); + + if (element) { + observer.disconnect(); + resolve(); + return; + } + } + } + }); + + let nodeToObserve = doc.getElementById(observedNodeId); + outerObserver.observe(nodeToObserve, { childList: true }); + }); + }, };