summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/addrbook/test/browser
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/components/addrbook/test/browser')
-rw-r--r--comm/mail/components/addrbook/test/browser/browser.ini37
-rw-r--r--comm/mail/components/addrbook/test/browser/browser_cardDAV_init.js664
-rw-r--r--comm/mail/components/addrbook/test/browser/browser_cardDAV_oAuth.js143
-rw-r--r--comm/mail/components/addrbook/test/browser/browser_cardDAV_properties.js245
-rw-r--r--comm/mail/components/addrbook/test/browser/browser_cardDAV_sync.js138
-rw-r--r--comm/mail/components/addrbook/test/browser/browser_contact_sidebar.js470
-rw-r--r--comm/mail/components/addrbook/test/browser/browser_contact_tree.js1261
-rw-r--r--comm/mail/components/addrbook/test/browser/browser_directory_tree.js982
-rw-r--r--comm/mail/components/addrbook/test/browser/browser_display_card.js1020
-rw-r--r--comm/mail/components/addrbook/test/browser/browser_display_multiple.js468
-rw-r--r--comm/mail/components/addrbook/test/browser/browser_drag_drop.js417
-rw-r--r--comm/mail/components/addrbook/test/browser/browser_edit_async.js363
-rw-r--r--comm/mail/components/addrbook/test/browser/browser_edit_card.js3517
-rw-r--r--comm/mail/components/addrbook/test/browser/browser_edit_photo.js866
-rw-r--r--comm/mail/components/addrbook/test/browser/browser_ldap_search.js180
-rw-r--r--comm/mail/components/addrbook/test/browser/browser_mailing_lists.js474
-rw-r--r--comm/mail/components/addrbook/test/browser/browser_open_actions.js157
-rw-r--r--comm/mail/components/addrbook/test/browser/browser_search.js139
-rw-r--r--comm/mail/components/addrbook/test/browser/browser_telemetry.js59
-rw-r--r--comm/mail/components/addrbook/test/browser/data/addressbook.sjs47
-rw-r--r--comm/mail/components/addrbook/test/browser/data/addressbooks.sjs62
-rw-r--r--comm/mail/components/addrbook/test/browser/data/auth_headers.sjs26
-rw-r--r--comm/mail/components/addrbook/test/browser/data/dns.sjs48
-rw-r--r--comm/mail/components/addrbook/test/browser/data/photo1.jpgbin0 -> 36775 bytes
-rw-r--r--comm/mail/components/addrbook/test/browser/data/photo2.jpgbin0 -> 38826 bytes
-rw-r--r--comm/mail/components/addrbook/test/browser/data/principal.sjs38
-rw-r--r--comm/mail/components/addrbook/test/browser/data/redirect_auto.sjs21
-rw-r--r--comm/mail/components/addrbook/test/browser/data/token.sjs36
-rw-r--r--comm/mail/components/addrbook/test/browser/head.js445
29 files changed, 12323 insertions, 0 deletions
diff --git a/comm/mail/components/addrbook/test/browser/browser.ini b/comm/mail/components/addrbook/test/browser/browser.ini
new file mode 100644
index 0000000000..99d7d9190d
--- /dev/null
+++ b/comm/mail/components/addrbook/test/browser/browser.ini
@@ -0,0 +1,37 @@
+[DEFAULT]
+head = head.js
+prefs =
+ carddav.setup.loglevel=Debug
+ carddav.sync.loglevel=Debug
+ ldap_2.servers.osx.dirType=-1
+ mail.provider.suppress_dialog_on_startup=true
+ mail.spotlight.firstRunDone=true
+ mail.winsearch.firstRunDone=true
+ mailnews.oauth.loglevel=Debug
+ mailnews.start_page.override_url=about:blank
+ mailnews.start_page.url=about:blank
+ signon.rememberSignons=true
+subsuite = thunderbird
+support-files = data/**
+tags = addrbook
+
+[browser_cardDAV_init.js]
+[browser_cardDAV_oAuth.js]
+tags = oauth
+[browser_cardDAV_properties.js]
+[browser_cardDAV_sync.js]
+[browser_contact_sidebar.js]
+[browser_contact_tree.js]
+[browser_directory_tree.js]
+[browser_display_card.js]
+[browser_display_multiple.js]
+[browser_drag_drop.js]
+[browser_edit_async.js]
+[browser_edit_card.js]
+[browser_edit_photo.js]
+[browser_ldap_search.js]
+support-files = ../../../../../mailnews/addrbook/test/unit/data/ldap_contacts.json
+[browser_mailing_lists.js]
+[browser_open_actions.js]
+[browser_search.js]
+[browser_telemetry.js]
diff --git a/comm/mail/components/addrbook/test/browser/browser_cardDAV_init.js b/comm/mail/components/addrbook/test/browser/browser_cardDAV_init.js
new file mode 100644
index 0000000000..36e44a84c7
--- /dev/null
+++ b/comm/mail/components/addrbook/test/browser/browser_cardDAV_init.js
@@ -0,0 +1,664 @@
+/* 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 { CardDAVDirectory } = ChromeUtils.import(
+ "resource:///modules/CardDAVDirectory.jsm"
+);
+const { CardDAVServer } = ChromeUtils.import(
+ "resource://testing-common/CardDAVServer.jsm"
+);
+const { DNS } = ChromeUtils.import("resource:///modules/DNS.jsm");
+
+// A list of books returned by CardDAVServer unless changed.
+const DEFAULT_BOOKS = [
+ {
+ label: "Not This One",
+ url: "/addressbooks/me/default/",
+ },
+ {
+ label: "CardDAV Test",
+ url: "/addressbooks/me/test/",
+ },
+];
+
+async function wrappedTest(testInitCallback, ...attemptArgs) {
+ Services.logins.removeAllLogins();
+
+ CardDAVServer.open("alice", "alice");
+ if (testInitCallback) {
+ await testInitCallback();
+ }
+
+ let abWindow = await openAddressBookWindow();
+
+ let dialogPromise = promiseLoadSubDialog(
+ "chrome://messenger/content/addressbook/abCardDAVDialog.xhtml"
+ ).then(async function (dialogWindow) {
+ for (let args of attemptArgs) {
+ if (args.url?.startsWith("/")) {
+ args.url = CardDAVServer.origin + args.url;
+ }
+ await attemptInit(dialogWindow, args);
+ }
+ dialogWindow.document.querySelector("dialog").getButton("cancel").click();
+ });
+ abWindow.createBook(Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE);
+ await dialogPromise;
+ CardDAVServer.resetHandlers();
+
+ await closeAddressBookWindow();
+ await CardDAVServer.close();
+
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 0, "no faulty logins were saved");
+}
+
+async function attemptInit(
+ dialogWindow,
+ {
+ username,
+ url,
+ certError,
+ password,
+ savePassword,
+ expectedStatus = "carddav-connection-error",
+ expectedBooks = [],
+ }
+) {
+ let dialogDocument = dialogWindow.document;
+ let acceptButton = dialogDocument.querySelector("dialog").getButton("accept");
+
+ let usernameInput = dialogDocument.getElementById("carddav-username");
+ let urlInput = dialogDocument.getElementById("carddav-location");
+ let statusMessage = dialogDocument.getElementById("carddav-statusMessage");
+ let availableBooks = dialogDocument.getElementById("carddav-availableBooks");
+
+ if (username) {
+ usernameInput.select();
+ EventUtils.sendString(username, dialogWindow);
+ }
+ if (url) {
+ urlInput.select();
+ EventUtils.sendString(url, dialogWindow);
+ }
+
+ let certPromise =
+ certError === undefined ? Promise.resolve() : handleCertError();
+ let promptPromise =
+ password === undefined
+ ? Promise.resolve()
+ : handlePasswordPrompt(username, password, savePassword);
+
+ acceptButton.click();
+
+ Assert.equal(
+ statusMessage.getAttribute("data-l10n-id"),
+ "carddav-loading",
+ "Correct status message"
+ );
+
+ await certPromise;
+ await promptPromise;
+ await BrowserTestUtils.waitForEvent(dialogWindow, "status-changed");
+
+ Assert.equal(
+ statusMessage.getAttribute("data-l10n-id"),
+ expectedStatus,
+ "Correct status message"
+ );
+
+ Assert.equal(
+ availableBooks.childElementCount,
+ expectedBooks.length,
+ "Expected number of address books found"
+ );
+ for (let i = 0; i < expectedBooks.length; i++) {
+ Assert.equal(availableBooks.children[i].label, expectedBooks[i].label);
+ if (expectedBooks[i].url.startsWith("/")) {
+ Assert.equal(
+ availableBooks.children[i].value,
+ `${CardDAVServer.origin}${expectedBooks[i].url}`
+ );
+ } else {
+ Assert.equal(availableBooks.children[i].value, expectedBooks[i].url);
+ }
+ Assert.ok(availableBooks.children[i].checked);
+ }
+}
+
+function handleCertError() {
+ return BrowserTestUtils.promiseAlertDialog(
+ "cancel",
+ "chrome://pippki/content/exceptionDialog.xhtml"
+ );
+}
+
+function handlePasswordPrompt(expectedUsername, password, savePassword = true) {
+ return BrowserTestUtils.promiseAlertDialog(null, undefined, {
+ async callback(prompt) {
+ await TestUtils.waitForCondition(
+ () => Services.focus.activeWindow == prompt,
+ "waiting for prompt to become active"
+ );
+
+ if (!password) {
+ prompt.document.querySelector("dialog").getButton("cancel").click();
+ return;
+ }
+
+ if (expectedUsername) {
+ Assert.equal(
+ prompt.document.getElementById("loginTextbox").value,
+ expectedUsername
+ );
+ } else {
+ prompt.document.getElementById("loginTextbox").value = "alice";
+ }
+ prompt.document.getElementById("password1Textbox").value = password;
+
+ let checkbox = prompt.document.getElementById("checkbox");
+ Assert.greater(checkbox.getBoundingClientRect().width, 0);
+ Assert.ok(checkbox.checked);
+
+ if (!savePassword) {
+ EventUtils.synthesizeMouseAtCenter(checkbox, {}, prompt);
+ Assert.ok(!checkbox.checked);
+ }
+
+ prompt.document.querySelector("dialog").getButton("accept").click();
+ },
+ });
+}
+
+/** Test URLs that don't respond. */
+add_task(function testBadURLs() {
+ return wrappedTest(
+ null,
+ { url: "mochi.test:8888" },
+ { url: "http://mochi.test:8888" },
+ { url: "https://mochi.test:8888" }
+ );
+});
+
+/** Test a server with a certificate problem. */
+add_task(function testBadSSL() {
+ return wrappedTest(null, {
+ url: "https://expired.example.com/",
+ certError: true,
+ });
+});
+
+/** Test an ordinary HTTP server that doesn't support CardDAV. */
+add_task(function testNotACardDAVServer() {
+ return wrappedTest(
+ () => {
+ CardDAVServer.server.registerPathHandler("/", null);
+ CardDAVServer.server.registerPathHandler("/.well-known/carddav", null);
+ },
+ {
+ url: "/",
+ }
+ );
+});
+
+/** Test a CardDAV server without the /.well-known/carddav response. */
+add_task(function testNoWellKnown() {
+ return wrappedTest(
+ () =>
+ CardDAVServer.server.registerPathHandler("/.well-known/carddav", null),
+ {
+ url: "/",
+ password: "alice",
+ expectedStatus: null,
+ expectedBooks: DEFAULT_BOOKS,
+ }
+ );
+});
+
+/** Test cancelling the password prompt when it appears. */
+add_task(function testPasswordCancelled() {
+ return wrappedTest(null, {
+ url: "/",
+ password: null,
+ });
+});
+
+/** Test entering the wrong password, then retrying with the right one. */
+add_task(function testBadPassword() {
+ return wrappedTest(
+ null,
+ {
+ url: "/",
+ password: "bob",
+ },
+ {
+ url: "/",
+ password: "alice",
+ expectedStatus: null,
+ expectedBooks: DEFAULT_BOOKS,
+ }
+ );
+});
+
+/** Test that entering the full URL of a book links to (only) that book. */
+add_task(function testDirectLink() {
+ return wrappedTest(null, {
+ url: "/addressbooks/me/test/",
+ password: "alice",
+ expectedStatus: null,
+ expectedBooks: [DEFAULT_BOOKS[1]],
+ });
+});
+
+/** Test that entering only a username finds the right URL. */
+add_task(function testEmailGoodPreset() {
+ return wrappedTest(
+ async () => {
+ // The server is open but we need it on a specific port.
+ await CardDAVServer.close();
+ CardDAVServer.open("alice@test.invalid", "alice", 9999);
+ },
+ {
+ username: "alice@test.invalid",
+ password: "alice",
+ expectedStatus: null,
+ expectedBooks: DEFAULT_BOOKS,
+ }
+ );
+});
+
+/** Test that entering only a bad username fails appropriately. */
+add_task(function testEmailBadPreset() {
+ return wrappedTest(null, {
+ username: "alice@bad.invalid",
+ expectedStatus: "carddav-known-incompatible",
+ });
+});
+
+/**
+ * Test that we correctly use DNS discovery. This uses the mochitest server
+ * (files in the data directory) instead of CardDAVServer because the latter
+ * can't speak HTTPS, and we only do DNS discovery for HTTPS.
+ */
+add_task(async function testDNS() {
+ let _srv = DNS.srv;
+ let _txt = DNS.txt;
+
+ DNS.srv = function (name) {
+ Assert.equal(name, "_carddavs._tcp.dnstest.invalid");
+ return [{ prio: 0, weight: 0, host: "example.org", port: 443 }];
+ };
+ DNS.txt = function (name) {
+ Assert.equal(name, "_carddavs._tcp.dnstest.invalid");
+ return [
+ {
+ data: "path=/browser/comm/mail/components/addrbook/test/browser/data/dns.sjs",
+ },
+ ];
+ };
+
+ let abWindow = await openAddressBookWindow();
+ let dialogPromise = promiseLoadSubDialog(
+ "chrome://messenger/content/addressbook/abCardDAVDialog.xhtml"
+ ).then(async function (dialogWindow) {
+ await attemptInit(dialogWindow, {
+ username: "carol@dnstest.invalid",
+ password: "carol",
+ expectedStatus: null,
+ expectedBooks: [
+ {
+ label: "You found me!",
+ url: "https://example.org/browser/comm/mail/components/addrbook/test/browser/data/addressbook.sjs",
+ },
+ ],
+ });
+ dialogWindow.document.querySelector("dialog").getButton("cancel").click();
+ });
+ abWindow.createBook(Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE);
+ await dialogPromise;
+
+ DNS.srv = _srv;
+ DNS.txt = _txt;
+ await closeAddressBookWindow();
+});
+
+/**
+ * Test doing everything correctly, including creating the directory and
+ * doing the initial sync.
+ */
+add_task(async function testEveryThingOK() {
+ CardDAVServer.open("alice", "alice");
+
+ let abWindow = await openAddressBookWindow();
+
+ Assert.equal(abWindow.booksList.rowCount, 3);
+
+ let dialogPromise = promiseLoadSubDialog(
+ "chrome://messenger/content/addressbook/abCardDAVDialog.xhtml"
+ ).then(async function (dialogWindow) {
+ await attemptInit(dialogWindow, {
+ url: CardDAVServer.origin,
+ password: "alice",
+ expectedStatus: null,
+ expectedBooks: DEFAULT_BOOKS,
+ });
+
+ let availableBooks = dialogWindow.document.getElementById(
+ "carddav-availableBooks"
+ );
+ availableBooks.children[0].checked = false;
+
+ dialogWindow.document.querySelector("dialog").getButton("accept").click();
+ });
+ let syncPromise = new Promise(resolve => {
+ let observer = {
+ observe(directory) {
+ Services.obs.removeObserver(this, "addrbook-directory-synced");
+ resolve(directory);
+ },
+ };
+ Services.obs.addObserver(observer, "addrbook-directory-synced");
+ });
+
+ abWindow.createBook(Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE);
+
+ await dialogPromise;
+ let directory = await syncPromise;
+ let davDirectory = CardDAVDirectory.forFile(directory.fileName);
+
+ Assert.equal(
+ Services.prefs.getStringPref(`${directory.dirPrefId}.carddav.url`, ""),
+ CardDAVServer.url
+ );
+ Assert.equal(
+ Services.prefs.getStringPref(`${directory.dirPrefId}.carddav.token`, ""),
+ "http://mochi.test/sync/0"
+ );
+ Assert.equal(
+ Services.prefs.getStringPref(`${directory.dirPrefId}.carddav.username`, ""),
+ "alice"
+ );
+ Assert.notEqual(davDirectory._syncTimer, null, "sync scheduled");
+
+ let logins = Services.logins.findLogins(CardDAVServer.origin, null, "");
+ Assert.equal(logins.length, 1, "login was saved");
+ Assert.equal(logins[0].username, "alice");
+ Assert.equal(logins[0].password, "alice");
+
+ Assert.equal(abWindow.booksList.rowCount, 4);
+ Assert.equal(
+ abWindow.booksList.getRowAtIndex(2).querySelector(".bookRow-name")
+ .textContent,
+ "CardDAV Test"
+ );
+ Assert.equal(abWindow.booksList.selectedIndex, 2, "new book got selected");
+
+ await closeAddressBookWindow();
+
+ // Don't close the server or delete the directory, they're needed below.
+});
+
+/**
+ * Tests adding a second directory on the same server. The auth prompt should
+ * show again, even though we've saved the credentials in the previous test.
+ */
+add_task(async function testEveryThingOKAgain() {
+ // Ensure at least a second has passed since the previous test, since we use
+ // context identifiers based on the current time in seconds.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 1000));
+
+ let abWindow = await openAddressBookWindow();
+
+ Assert.equal(abWindow.booksList.rowCount, 4);
+
+ let dialogPromise = promiseLoadSubDialog(
+ "chrome://messenger/content/addressbook/abCardDAVDialog.xhtml"
+ ).then(async function (dialogWindow) {
+ await attemptInit(dialogWindow, {
+ url: CardDAVServer.origin,
+ password: "alice",
+ expectedStatus: null,
+ expectedBooks: [DEFAULT_BOOKS[0]],
+ });
+
+ dialogWindow.document.querySelector("dialog").getButton("accept").click();
+ });
+ let syncPromise = TestUtils.topicObserved("addrbook-directory-synced");
+
+ abWindow.createBook(Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE);
+
+ await dialogPromise;
+ let [directory] = await syncPromise;
+ let davDirectory = CardDAVDirectory.forFile(directory.fileName);
+
+ Assert.equal(
+ Services.prefs.getStringPref(`${directory.dirPrefId}.carddav.url`, ""),
+ CardDAVServer.altURL
+ );
+ Assert.equal(
+ Services.prefs.getStringPref(`${directory.dirPrefId}.carddav.token`, ""),
+ "http://mochi.test/sync/0"
+ );
+ Assert.equal(
+ Services.prefs.getStringPref(`${directory.dirPrefId}.carddav.username`, ""),
+ "alice"
+ );
+ Assert.notEqual(davDirectory._syncTimer, null, "sync scheduled");
+
+ let logins = Services.logins.findLogins(CardDAVServer.origin, null, "");
+ Assert.equal(logins.length, 1, "login was saved");
+ Assert.equal(logins[0].username, "alice");
+ Assert.equal(logins[0].password, "alice");
+
+ Assert.equal(abWindow.booksList.rowCount, 5);
+ Assert.equal(
+ abWindow.booksList.getRowAtIndex(2).querySelector(".bookRow-name")
+ .textContent,
+ "CardDAV Test"
+ );
+ Assert.equal(
+ abWindow.booksList.getRowAtIndex(3).querySelector(".bookRow-name")
+ .textContent,
+ "Not This One"
+ );
+ Assert.equal(abWindow.booksList.selectedIndex, 3, "new book got selected");
+
+ await closeAddressBookWindow();
+ await CardDAVServer.close();
+
+ let otherDirectory = MailServices.ab.getDirectoryFromId(
+ "ldap_2.servers.CardDAVTest"
+ );
+ await promiseDirectoryRemoved(directory.URI);
+ await promiseDirectoryRemoved(otherDirectory.URI);
+
+ Services.logins.removeAllLogins();
+});
+
+/**
+ * Test setting up a directory but not saving the password. The username
+ * should be saved and no further password prompt should appear. We can't test
+ * restarting Thunderbird but if we could the password prompt would appear
+ * next time the directory makes a request.
+ */
+add_task(async function testNoSavePassword() {
+ CardDAVServer.open("alice", "alice");
+
+ let abWindow = await openAddressBookWindow();
+
+ Assert.equal(abWindow.booksList.rowCount, 3);
+
+ let dialogPromise = promiseLoadSubDialog(
+ "chrome://messenger/content/addressbook/abCardDAVDialog.xhtml"
+ ).then(async function (dialogWindow) {
+ await attemptInit(dialogWindow, {
+ url: CardDAVServer.origin,
+ password: "alice",
+ savePassword: false,
+ expectedStatus: null,
+ expectedBooks: DEFAULT_BOOKS,
+ });
+
+ let availableBooks = dialogWindow.document.getElementById(
+ "carddav-availableBooks"
+ );
+ availableBooks.children[0].checked = false;
+
+ dialogWindow.document.querySelector("dialog").getButton("accept").click();
+ });
+ let syncPromise = TestUtils.topicObserved("addrbook-directory-synced");
+
+ abWindow.createBook(Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE);
+ await dialogPromise;
+ let [directory] = await syncPromise;
+ let davDirectory = CardDAVDirectory.forFile(directory.fileName);
+
+ Assert.equal(
+ Services.prefs.getStringPref(`${directory.dirPrefId}.carddav.url`, ""),
+ CardDAVServer.url
+ );
+ Assert.equal(
+ Services.prefs.getStringPref(`${directory.dirPrefId}.carddav.token`, ""),
+ "http://mochi.test/sync/0"
+ );
+ Assert.equal(
+ Services.prefs.getStringPref(`${directory.dirPrefId}.carddav.username`, ""),
+ "alice"
+ );
+ Assert.notEqual(davDirectory._syncTimer, null, "sync scheduled");
+
+ let logins = Services.logins.findLogins(CardDAVServer.origin, null, "");
+ Assert.equal(logins.length, 0, "login was NOT saved");
+
+ Assert.equal(abWindow.booksList.rowCount, 4);
+ Assert.equal(
+ abWindow.booksList.getRowAtIndex(2).querySelector(".bookRow-name")
+ .textContent,
+ "CardDAV Test"
+ );
+ Assert.equal(abWindow.booksList.selectedIndex, 2, "new book got selected");
+
+ await closeAddressBookWindow();
+
+ // Disable sync as we're going to start the address book manager again.
+ directory.setIntValue("carddav.syncinterval", 0);
+
+ // Don't close the server or delete the directory, they're needed below.
+});
+
+/**
+ * Tests saving a previously unsaved password. This uses the directory from
+ * the previous test and simulates a restart of the address book manager.
+ */
+add_task(async function testSavePasswordLater() {
+ let reloadPromise = TestUtils.topicObserved("addrbook-reloaded");
+ Services.obs.notifyObservers(null, "addrbook-reload");
+ await reloadPromise;
+
+ Assert.equal(MailServices.ab.directories.length, 3);
+ let directory = MailServices.ab.getDirectoryFromId(
+ "ldap_2.servers.CardDAVTest"
+ );
+ let davDirectory = CardDAVDirectory.forFile(directory.fileName);
+
+ let promptPromise = handlePasswordPrompt("alice", "alice");
+ let syncPromise = TestUtils.topicObserved("addrbook-directory-synced");
+ davDirectory.fetchAllFromServer();
+ await promptPromise;
+ await syncPromise;
+
+ Assert.equal(
+ Services.prefs.getStringPref(`${directory.dirPrefId}.carddav.username`, ""),
+ "alice",
+ "username was saved"
+ );
+
+ let logins = Services.logins.findLogins(CardDAVServer.origin, null, "");
+ Assert.equal(logins.length, 1, "login was saved");
+ Assert.equal(logins[0].username, "alice");
+ Assert.equal(logins[0].password, "alice");
+
+ await CardDAVServer.close();
+
+ await promiseDirectoryRemoved(directory.URI);
+
+ Services.logins.removeAllLogins();
+});
+
+/**
+ * Tests that an address book can still be created if the server returns no
+ * name. The hostname of the server is used instead.
+ */
+add_task(async function testNoName() {
+ CardDAVServer._books = CardDAVServer.books;
+ CardDAVServer.books = { "/addressbooks/me/noname/": undefined };
+ CardDAVServer.open("alice", "alice");
+
+ let abWindow = await openAddressBookWindow();
+
+ Assert.equal(abWindow.booksList.rowCount, 3);
+
+ let dialogPromise = promiseLoadSubDialog(
+ "chrome://messenger/content/addressbook/abCardDAVDialog.xhtml"
+ ).then(async function (dialogWindow) {
+ await attemptInit(dialogWindow, {
+ url: CardDAVServer.origin,
+ password: "alice",
+ expectedStatus: null,
+ expectedBooks: [{ label: "noname", url: "/addressbooks/me/noname/" }],
+ });
+
+ dialogWindow.document.querySelector("dialog").getButton("accept").click();
+ });
+ let syncPromise = new Promise(resolve => {
+ let observer = {
+ observe(directory) {
+ Services.obs.removeObserver(this, "addrbook-directory-synced");
+ resolve(directory);
+ },
+ };
+ Services.obs.addObserver(observer, "addrbook-directory-synced");
+ });
+
+ abWindow.createBook(Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE);
+
+ await dialogPromise;
+ let directory = await syncPromise;
+ let davDirectory = CardDAVDirectory.forFile(directory.fileName);
+
+ Assert.equal(
+ Services.prefs.getStringPref(`${directory.dirPrefId}.carddav.url`, ""),
+ `${CardDAVServer.origin}/addressbooks/me/noname/`
+ );
+ Assert.equal(
+ Services.prefs.getStringPref(`${directory.dirPrefId}.carddav.token`, ""),
+ "http://mochi.test/sync/0"
+ );
+ Assert.equal(
+ Services.prefs.getStringPref(`${directory.dirPrefId}.carddav.username`, ""),
+ "alice"
+ );
+ Assert.notEqual(davDirectory._syncTimer, null, "sync scheduled");
+
+ let logins = Services.logins.findLogins(CardDAVServer.origin, null, "");
+ Assert.equal(logins.length, 1, "login was saved");
+ Assert.equal(logins[0].username, "alice");
+ Assert.equal(logins[0].password, "alice");
+
+ Assert.equal(abWindow.booksList.rowCount, 4);
+ Assert.equal(
+ abWindow.booksList.getRowAtIndex(2).querySelector(".bookRow-name")
+ .textContent,
+ "noname"
+ );
+
+ await closeAddressBookWindow();
+ await CardDAVServer.close();
+ CardDAVServer.books = CardDAVServer._books;
+
+ await promiseDirectoryRemoved(directory.URI);
+
+ Services.logins.removeAllLogins();
+});
diff --git a/comm/mail/components/addrbook/test/browser/browser_cardDAV_oAuth.js b/comm/mail/components/addrbook/test/browser/browser_cardDAV_oAuth.js
new file mode 100644
index 0000000000..137a13e221
--- /dev/null
+++ b/comm/mail/components/addrbook/test/browser/browser_cardDAV_oAuth.js
@@ -0,0 +1,143 @@
+/* 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/. */
+
+// Creates address books in various configurations (current and legacy) and
+// performs requests in each of them to prove that OAuth2 authentication is
+// working as expected.
+
+var { CardDAVDirectory } = ChromeUtils.import(
+ "resource:///modules/CardDAVDirectory.jsm"
+);
+
+var LoginInfo = Components.Constructor(
+ "@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo,
+ "init"
+);
+
+// Ideal login info. This is what would be saved if you created a new calendar.
+const ORIGIN = "oauth://mochi.test";
+const SCOPE = "test_scope";
+const USERNAME = "bob@test.invalid";
+const VALID_TOKEN = "bobs_refresh_token";
+
+const PATH = "comm/mail/components/addrbook/test/browser/data/";
+const URL = `http://mochi.test:8888/browser/${PATH}`;
+
+/**
+ * Set a string pref for the given directory.
+ *
+ * @param {string} dirPrefId
+ * @param {string} key
+ * @param {string} value
+ */
+function setPref(dirPrefId, key, value) {
+ Services.prefs.setStringPref(`ldap_2.servers.${dirPrefId}.${key}`, value);
+}
+
+/**
+ * Clear any existing saved logins and add the given ones.
+ *
+ * @param {string[][]} - Zero or more arrays consisting of origin, realm,
+ * username, and password.
+ */
+function setLogins(...logins) {
+ Services.logins.removeAllLogins();
+ for (let [origin, realm, username, password] of logins) {
+ Services.logins.addLogin(
+ new LoginInfo(origin, null, realm, username, password, "", "")
+ );
+ }
+}
+
+/**
+ * Create a directory with the given id, perform a request, and check that the
+ * correct authorisation header was used. If the user is required to
+ * re-authenticate with the provider, check that the new token is stored in the
+ * right place.
+ *
+ * @param {string} dirPrefId - Pref ID of the new directory.
+ * @param {string} uid - UID of the new directory.
+ * @param {string} [newTokenUsername] - If given, re-authentication must happen
+ * and the new token stored with this user name.
+ */
+async function subtest(dirPrefId, uid, newTokenUsername) {
+ let directory = new CardDAVDirectory();
+ directory._dirPrefId = dirPrefId;
+ directory._uid = uid;
+ directory.__prefBranch = Services.prefs.getBranch(
+ `ldap_2.servers.${dirPrefId}.`
+ );
+ directory.__prefBranch.setStringPref("carddav.url", URL);
+
+ let response = await directory._makeRequest("auth_headers.sjs");
+ Assert.equal(response.status, 200);
+ let headers = JSON.parse(response.text);
+
+ if (newTokenUsername) {
+ Assert.equal(headers.authorization, "Bearer new_access_token");
+
+ let logins = Services.logins
+ .findLogins(ORIGIN, null, SCOPE)
+ .filter(l => l.username == newTokenUsername);
+ Assert.equal(logins.length, 1);
+ Assert.equal(logins[0].username, newTokenUsername);
+ Assert.equal(logins[0].password, "new_refresh_token");
+ } else {
+ Assert.equal(headers.authorization, "Bearer bobs_access_token");
+ }
+
+ Services.logins.removeAllLogins();
+}
+
+// Test making a request when there is no matching token stored.
+
+/** No token stored, no username set. */
+add_task(function testAddressBookOAuth_uid_none() {
+ let dirPrefId = "uid_none";
+ let uid = "testAddressBookOAuth_uid_none";
+ return subtest(dirPrefId, uid, uid);
+});
+
+// Test making a request when there IS a matching token, but the server rejects
+// it. Currently a new token is not requested on failure.
+
+/** Expired token stored with UID. */
+add_task(function testAddressBookOAuth_uid_expired() {
+ let dirPrefId = "uid_expired";
+ let uid = "testAddressBookOAuth_uid_expired";
+ setLogins([ORIGIN, SCOPE, uid, "expired_token"]);
+ return subtest(dirPrefId, uid, uid);
+}).skip(); // Broken.
+
+// Test making a request with a valid token.
+
+/** Valid token stored with UID. This is the old way of storing the token. */
+add_task(function testAddressBookOAuth_uid_valid() {
+ let dirPrefId = "uid_valid";
+ let uid = "testAddressBookOAuth_uid_valid";
+ setLogins([ORIGIN, SCOPE, uid, VALID_TOKEN]);
+ return subtest(dirPrefId, uid);
+});
+
+/** Valid token stored with username, exact scope. */
+add_task(function testAddressBookOAuth_username_validSingle() {
+ let dirPrefId = "username_validSingle";
+ let uid = "testAddressBookOAuth_username_validSingle";
+ setPref(dirPrefId, "carddav.username", USERNAME);
+ setLogins(
+ [ORIGIN, SCOPE, USERNAME, VALID_TOKEN],
+ [ORIGIN, "other_scope", USERNAME, "other_refresh_token"]
+ );
+ return subtest(dirPrefId, uid);
+});
+
+/** Valid token stored with username, many scopes. */
+add_task(function testAddressBookOAuth_username_validMultiple() {
+ let dirPrefId = "username_validMultiple";
+ let uid = "testAddressBookOAuth_username_validMultiple";
+ setPref(dirPrefId, "carddav.username", USERNAME);
+ setLogins([ORIGIN, "scope test_scope other_scope", USERNAME, VALID_TOKEN]);
+ return subtest(dirPrefId, uid);
+});
diff --git a/comm/mail/components/addrbook/test/browser/browser_cardDAV_properties.js b/comm/mail/components/addrbook/test/browser/browser_cardDAV_properties.js
new file mode 100644
index 0000000000..0acd0b3540
--- /dev/null
+++ b/comm/mail/components/addrbook/test/browser/browser_cardDAV_properties.js
@@ -0,0 +1,245 @@
+/* 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 CardDAV properties dialog.
+ */
+
+const { CardDAVDirectory } = ChromeUtils.import(
+ "resource:///modules/CardDAVDirectory.jsm"
+);
+const { CardDAVServer } = ChromeUtils.import(
+ "resource://testing-common/CardDAVServer.jsm"
+);
+
+add_task(async () => {
+ const INTERVAL_PREF = "ldap_2.servers.props.carddav.syncinterval";
+ const TOKEN_PREF = "ldap_2.servers.props.carddav.token";
+ const TOKEN_VALUE = "http://mochi.test/sync/0";
+ const URL_PREF = "ldap_2.servers.props.carddav.url";
+ const URL_VALUE = "https://mochi.test/carddav/test";
+
+ let dirPrefId = MailServices.ab.newAddressBook(
+ "props",
+ undefined,
+ Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE
+ );
+ Assert.equal(dirPrefId, "ldap_2.servers.props");
+ Assert.equal([...MailServices.ab.directories].length, 3);
+
+ let directory = MailServices.ab.getDirectoryFromId(dirPrefId);
+ let davDirectory = CardDAVDirectory.forFile(directory.fileName);
+ registerCleanupFunction(async () => {
+ Assert.equal(davDirectory._syncTimer, null, "sync timer cleaned up");
+ });
+ Assert.equal(directory.dirType, Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE);
+
+ Services.prefs.setIntPref(INTERVAL_PREF, 0);
+ Services.prefs.setStringPref(TOKEN_PREF, TOKEN_VALUE);
+ Services.prefs.setStringPref(URL_PREF, URL_VALUE);
+
+ Assert.ok(davDirectory);
+ Assert.equal(davDirectory._serverURL, URL_VALUE);
+ Assert.equal(davDirectory._syncToken, TOKEN_VALUE);
+ Assert.equal(davDirectory._syncTimer, null, "no sync scheduled");
+ Assert.equal(davDirectory.readOnly, false);
+
+ let abWindow = await openAddressBookWindow();
+ let abDocument = abWindow.document;
+ let booksList = abWindow.booksList;
+
+ openDirectory(directory);
+
+ Assert.equal(booksList.rowCount, 4);
+ Assert.equal(booksList.getIndexForUID(directory.UID), 2);
+ Assert.equal(booksList.selectedIndex, 2);
+
+ let menu = abDocument.getElementById("bookContext");
+ let menuItem = abDocument.getElementById("bookContextProperties");
+
+ let subtest = async function (expectedValues, newValues, buttonAction) {
+ Assert.equal(booksList.selectedIndex, 2);
+
+ let shownPromise = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(
+ booksList.getRowAtIndex(2),
+ { type: "contextmenu" },
+ abWindow
+ );
+ await shownPromise;
+
+ Assert.ok(BrowserTestUtils.is_visible(menuItem));
+
+ let dialogPromise = promiseLoadSubDialog(
+ "chrome://messenger/content/addressbook/abCardDAVProperties.xhtml"
+ ).then(async function (dialogWindow) {
+ let dialogDocument = dialogWindow.document;
+
+ let nameInput = dialogDocument.getElementById("carddav-name");
+ Assert.equal(nameInput.value, expectedValues.name);
+ if ("name" in newValues) {
+ nameInput.value = newValues.name;
+ }
+
+ let urlInput = dialogDocument.getElementById("carddav-url");
+ Assert.equal(urlInput.value, expectedValues.url);
+ if ("url" in newValues) {
+ urlInput.value = newValues.url;
+ }
+
+ let refreshActiveInput = dialogDocument.getElementById(
+ "carddav-refreshActive"
+ );
+ let refreshIntervalInput = dialogDocument.getElementById(
+ "carddav-refreshInterval"
+ );
+
+ Assert.equal(refreshActiveInput.checked, expectedValues.refreshActive);
+ Assert.equal(
+ refreshIntervalInput.disabled,
+ !expectedValues.refreshActive
+ );
+ if (
+ "refreshActive" in newValues &&
+ newValues.refreshActive != expectedValues.refreshActive
+ ) {
+ EventUtils.synthesizeMouseAtCenter(
+ refreshActiveInput,
+ {},
+ dialogWindow
+ );
+ Assert.equal(refreshIntervalInput.disabled, !newValues.refreshActive);
+ }
+
+ Assert.equal(refreshIntervalInput.value, expectedValues.refreshInterval);
+ if ("refreshInterval" in newValues) {
+ refreshIntervalInput.value = newValues.refreshInterval;
+ }
+
+ let readOnlyInput = dialogDocument.getElementById("carddav-readOnly");
+
+ Assert.equal(readOnlyInput.checked, expectedValues.readOnly);
+ if ("readOnly" in newValues) {
+ readOnlyInput.checked = newValues.readOnly;
+ }
+
+ dialogDocument.querySelector("dialog").getButton(buttonAction).click();
+ });
+ menu.activateItem(menuItem);
+ await dialogPromise;
+
+ await new Promise(resolve => abWindow.setTimeout(resolve));
+ };
+
+ info("Open the dialog and cancel it. Nothing should change.");
+ await subtest(
+ {
+ name: "props",
+ url: URL_VALUE,
+ refreshActive: false,
+ refreshInterval: 30,
+ readOnly: false,
+ },
+ {},
+ "cancel"
+ );
+
+ Assert.equal(davDirectory.dirName, "props");
+ Assert.equal(davDirectory._serverURL, URL_VALUE);
+ Assert.equal(davDirectory.getIntValue("carddav.syncinterval", -1), 0);
+ Assert.equal(davDirectory._syncTimer, null, "no sync scheduled");
+ Assert.equal(davDirectory.readOnly, false);
+
+ info("Open the dialog and accept it. Nothing should change.");
+ await subtest(
+ {
+ name: "props",
+ url: URL_VALUE,
+ refreshActive: false,
+ refreshInterval: 30,
+ readOnly: false,
+ },
+ {},
+ "accept"
+ );
+
+ Assert.equal(davDirectory.dirName, "props");
+ Assert.equal(davDirectory._serverURL, URL_VALUE);
+ Assert.equal(davDirectory.getIntValue("carddav.syncinterval", -1), 0);
+ Assert.equal(davDirectory._syncTimer, null, "no sync scheduled");
+ Assert.equal(davDirectory.readOnly, false);
+
+ info("Open the dialog and change the values.");
+ await subtest(
+ {
+ name: "props",
+ url: URL_VALUE,
+ refreshActive: false,
+ refreshInterval: 30,
+ readOnly: false,
+ },
+ {
+ name: "CardDAV Properties Test",
+ refreshActive: true,
+ refreshInterval: 30,
+ readOnly: true,
+ },
+ "accept"
+ );
+
+ Assert.equal(davDirectory.dirName, "CardDAV Properties Test");
+ Assert.equal(davDirectory._serverURL, URL_VALUE);
+ Assert.equal(davDirectory.getIntValue("carddav.syncinterval", -1), 30);
+ Assert.notEqual(davDirectory._syncTimer, null, "sync scheduled");
+ let currentSyncTimer = davDirectory._syncTimer;
+ Assert.equal(davDirectory.readOnly, true);
+
+ info("Open the dialog and accept it. Nothing should change.");
+ await subtest(
+ {
+ name: "CardDAV Properties Test",
+ url: URL_VALUE,
+ refreshActive: true,
+ refreshInterval: 30,
+ readOnly: true,
+ },
+ {},
+ "accept"
+ );
+
+ Assert.equal(davDirectory.dirName, "CardDAV Properties Test");
+ Assert.equal(davDirectory._serverURL, URL_VALUE);
+ Assert.equal(davDirectory.getIntValue("carddav.syncinterval", -1), 30);
+ Assert.equal(
+ davDirectory._syncTimer,
+ currentSyncTimer,
+ "same sync scheduled"
+ );
+ Assert.equal(davDirectory.readOnly, true);
+
+ info("Open the dialog and change the interval.");
+ await subtest(
+ {
+ name: "CardDAV Properties Test",
+ url: URL_VALUE,
+ refreshActive: true,
+ refreshInterval: 30,
+ readOnly: true,
+ },
+ { refreshInterval: 60 },
+ "accept"
+ );
+
+ Assert.equal(davDirectory.dirName, "CardDAV Properties Test");
+ Assert.equal(davDirectory._serverURL, URL_VALUE);
+ Assert.equal(davDirectory.getIntValue("carddav.syncinterval", -1), 60);
+ Assert.greater(
+ davDirectory._syncTimer,
+ currentSyncTimer,
+ "new sync scheduled"
+ );
+ Assert.equal(davDirectory.readOnly, true);
+
+ await promiseDirectoryRemoved(directory.URI);
+});
diff --git a/comm/mail/components/addrbook/test/browser/browser_cardDAV_sync.js b/comm/mail/components/addrbook/test/browser/browser_cardDAV_sync.js
new file mode 100644
index 0000000000..1c4e4fb07a
--- /dev/null
+++ b/comm/mail/components/addrbook/test/browser/browser_cardDAV_sync.js
@@ -0,0 +1,138 @@
+/* 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 CardDAV synchronization.
+ */
+
+const { CardDAVDirectory } = ChromeUtils.import(
+ "resource:///modules/CardDAVDirectory.jsm"
+);
+const { CardDAVServer } = ChromeUtils.import(
+ "resource://testing-common/CardDAVServer.jsm"
+);
+
+add_task(async () => {
+ CardDAVServer.open();
+ registerCleanupFunction(async () => {
+ await CardDAVServer.close();
+ });
+
+ let dirPrefId = MailServices.ab.newAddressBook(
+ "sync",
+ undefined,
+ Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE
+ );
+ Assert.equal(dirPrefId, "ldap_2.servers.sync");
+ Assert.equal([...MailServices.ab.directories].length, 3);
+
+ let directory = MailServices.ab.getDirectoryFromId(dirPrefId);
+ let davDirectory = CardDAVDirectory.forFile(directory.fileName);
+ Assert.equal(directory.dirType, Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE);
+
+ Services.prefs.setStringPref(
+ "ldap_2.servers.sync.carddav.token",
+ "http://mochi.test/sync/0"
+ );
+ Services.prefs.setStringPref(
+ "ldap_2.servers.sync.carddav.url",
+ CardDAVServer.url
+ );
+
+ Assert.ok(davDirectory);
+ Assert.equal(davDirectory._serverURL, CardDAVServer.url);
+ Assert.equal(davDirectory._syncToken, "http://mochi.test/sync/0");
+
+ let abWindow = await openAddressBookWindow();
+ let abDocument = abWindow.document;
+
+ // This test becomes unreliable if we don't pause for a moment.
+ await new Promise(resolve => abWindow.setTimeout(resolve, 500));
+
+ openDirectory(directory);
+ checkNamesListed();
+
+ let menu = abDocument.getElementById("bookContext");
+ let menuItem = abDocument.getElementById("bookContextSynchronize");
+ let openContext = async (index, itemHidden) => {
+ let shownPromise = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(
+ abWindow.booksList.getRowAtIndex(index),
+ { type: "contextmenu" },
+ abWindow
+ );
+ await shownPromise;
+ Assert.equal(menuItem.hidden, itemHidden);
+ };
+
+ for (let index of [1, 3]) {
+ await openContext(index, true);
+
+ let hiddenPromise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ menu.hidePopup();
+ await hiddenPromise;
+ }
+
+ CardDAVServer.putCardInternal(
+ "first.vcf",
+ "BEGIN:VCARD\r\nUID:first\r\nFN:First\r\nEND:VCARD\r\n"
+ );
+
+ Assert.equal(davDirectory._syncTimer, null, "no sync scheduled");
+
+ let syncedPromise = TestUtils.topicObserved("addrbook-directory-synced");
+ await openContext(2, false);
+ menu.activateItem(menuItem);
+ await syncedPromise;
+
+ await new Promise(resolve => setTimeout(resolve));
+ Assert.notEqual(davDirectory._syncTimer, null, "first sync scheduled");
+ let currentSyncTimer = davDirectory._syncTimer;
+
+ checkNamesListed("First");
+
+ CardDAVServer.putCardInternal(
+ "second.vcf",
+ "BEGIN:VCARD\r\nUID:second\r\nFN:Second\r\nEND:VCARD\r\n"
+ );
+
+ syncedPromise = TestUtils.topicObserved("addrbook-directory-synced");
+ await openContext(2, false);
+ menu.activateItem(menuItem);
+ await syncedPromise;
+
+ await new Promise(resolve => setTimeout(resolve));
+ Assert.greater(
+ davDirectory._syncTimer,
+ currentSyncTimer,
+ "second sync not the same as the first"
+ );
+ currentSyncTimer = davDirectory._syncTimer;
+
+ checkNamesListed("First", "Second");
+
+ CardDAVServer.deleteCardInternal("second.vcf");
+ CardDAVServer.putCardInternal(
+ "third.vcf",
+ "BEGIN:VCARD\r\nUID:third\r\nFN:Third\r\nEND:VCARD\r\n"
+ );
+
+ syncedPromise = TestUtils.topicObserved("addrbook-directory-synced");
+ await openContext(2, false);
+ menu.activateItem(menuItem);
+ await syncedPromise;
+
+ await new Promise(resolve => setTimeout(resolve));
+ Assert.greater(
+ davDirectory._syncTimer,
+ currentSyncTimer,
+ "third sync not the same as the second"
+ );
+
+ checkNamesListed("First", "Third");
+
+ await closeAddressBookWindow();
+ await promiseDirectoryRemoved(directory.URI);
+ Assert.equal(davDirectory._syncTimer, null, "sync timer cleaned up");
+});
diff --git a/comm/mail/components/addrbook/test/browser/browser_contact_sidebar.js b/comm/mail/components/addrbook/test/browser/browser_contact_sidebar.js
new file mode 100644
index 0000000000..3fb0f70b25
--- /dev/null
+++ b/comm/mail/components/addrbook/test/browser/browser_contact_sidebar.js
@@ -0,0 +1,470 @@
+/* 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/. */
+
+var { mailTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MailTestUtils.jsm"
+);
+var dragService = Cc["@mozilla.org/widget/dragservice;1"].getService(
+ Ci.nsIDragService
+);
+
+add_task(async function () {
+ MailServices.accounts.createLocalMailAccount();
+ let account = MailServices.accounts.accounts[0];
+ account.addIdentity(MailServices.accounts.createIdentity());
+
+ let book1 = createAddressBook("Book 1");
+ book1.addCard(createContact("daniel", "test"));
+ book1.addCard(createContact("jonathan", "test"));
+ book1.addCard(createContact("năthån", "test"));
+
+ let book2 = createAddressBook("Book 2");
+ book2.addCard(createContact("danielle", "test"));
+ book2.addCard(createContact("katherine", "test"));
+ book2.addCard(createContact("natalie", "test"));
+ book2.addCard(createContact("sūsãnáh", "test"));
+
+ let list = createMailingList("pèóplë named tēst");
+ book2.addMailList(list);
+
+ registerCleanupFunction(async function () {
+ MailServices.accounts.removeAccount(account, true);
+ await promiseDirectoryRemoved(book1.URI);
+ await promiseDirectoryRemoved(book2.URI);
+ });
+
+ // Open a compose window.
+
+ let params = Cc[
+ "@mozilla.org/messengercompose/composeparams;1"
+ ].createInstance(Ci.nsIMsgComposeParams);
+ params.composeFields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+
+ let composeWindowPromise = BrowserTestUtils.domWindowOpened();
+ MailServices.compose.OpenComposeWindowWithParams(null, params);
+ let composeWindow = await composeWindowPromise;
+ await BrowserTestUtils.waitForEvent(composeWindow, "compose-editor-ready");
+ await TestUtils.waitForCondition(
+ () => Services.focus.activeWindow == composeWindow
+ );
+ let composeDocument = composeWindow.document;
+ let toAddrInput = composeDocument.getElementById("toAddrInput");
+ let toAddrRow = composeDocument.getElementById("addressRowTo");
+ let ccAddrInput = composeDocument.getElementById("ccAddrInput");
+ let ccAddrRow = composeDocument.getElementById("addressRowCc");
+ let bccAddrInput = composeDocument.getElementById("bccAddrInput");
+ let bccAddrRow = composeDocument.getElementById("addressRowBcc");
+
+ // The compose window waits before deciding whether to open the sidebar.
+ // We must wait longer.
+ await new Promise(resolve => composeWindow.setTimeout(resolve, 100));
+
+ // Make sure the contacts sidebar is open.
+
+ let sidebar = composeDocument.getElementById("contactsSidebar");
+ if (BrowserTestUtils.is_hidden(sidebar)) {
+ EventUtils.synthesizeKey("KEY_F9", {}, composeWindow);
+ }
+ let sidebarBrowser = composeDocument.getElementById("contactsBrowser");
+ await TestUtils.waitForCondition(
+ () =>
+ sidebarBrowser.currentURI.spec.includes("abContactsPanel.xhtml") &&
+ sidebarBrowser.contentDocument.readyState == "complete"
+ );
+ let sidebarWindow = sidebarBrowser.contentWindow;
+ let sidebarDocument = sidebarBrowser.contentDocument;
+
+ let abList = sidebarDocument.getElementById("addressbookList");
+ let searchBox = sidebarDocument.getElementById("peopleSearchInput");
+ let cardsList = sidebarDocument.getElementById("abResultsTree");
+ let cardsContext = sidebarDocument.getElementById("cardProperties");
+ let toButton = sidebarDocument.getElementById("toButton");
+ let ccButton = sidebarDocument.getElementById("ccButton");
+ let bccButton = sidebarDocument.getElementById("bccButton");
+
+ await TestUtils.waitForCondition(() => cardsList.view.rowCount != 0);
+ checkListNames(
+ [
+ "daniel test",
+ "danielle test",
+ "jonathan test",
+ "katherine test",
+ "natalie test",
+ "năthån test",
+ "pèóplë named tēst",
+ "sūsãnáh test",
+ ],
+ "all contacts are shown"
+ );
+
+ Assert.equal(cardsList.view.selection.count, 0, "no contact selected");
+ Assert.ok(toButton.disabled, "to button disabled with no contact selected");
+ Assert.ok(ccButton.disabled, "cc button disabled with no contact selected");
+ Assert.ok(bccButton.disabled, "bcc button disabled with no contact selected");
+
+ function clickOnRow(row, event) {
+ mailTestUtils.treeClick(
+ EventUtils,
+ sidebarWindow,
+ cardsList,
+ row,
+ 0,
+ event
+ );
+ }
+
+ async function doMenulist(value) {
+ let shownPromise = BrowserTestUtils.waitForEvent(abList, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(abList, {}, sidebarWindow);
+ await shownPromise;
+ let hiddenPromise = BrowserTestUtils.waitForEvent(abList, "popuphidden");
+ EventUtils.synthesizeMouseAtCenter(
+ abList.querySelector(`[value="${value}"]`),
+ {},
+ sidebarWindow
+ );
+ await hiddenPromise;
+ }
+
+ async function doContextMenu(row, command) {
+ clickOnRow(row, {});
+ let shownPromise = BrowserTestUtils.waitForEvent(
+ cardsContext,
+ "popupshown"
+ );
+ clickOnRow(row, { type: "contextmenu" });
+ await shownPromise;
+ let hiddenPromise = BrowserTestUtils.waitForEvent(
+ cardsContext,
+ "popuphidden"
+ );
+ cardsContext.activateItem(
+ cardsContext.querySelector(`[command="${command}"]`)
+ );
+ await hiddenPromise;
+ }
+
+ function checkListNames(expectedNames, message) {
+ let actualNames = [];
+ for (let row = 0; row < cardsList.view.rowCount; row++) {
+ actualNames.push(
+ cardsList.view.getCellText(row, cardsList.columns.GeneratedName)
+ );
+ }
+
+ Assert.deepEqual(actualNames, expectedNames, message);
+ }
+
+ function checkPills(row, expectedPills) {
+ let actualPills = Array.from(
+ row.querySelectorAll("mail-address-pill"),
+ p => p.label
+ );
+ Assert.deepEqual(
+ actualPills,
+ expectedPills,
+ "message recipients match expected"
+ );
+ }
+
+ function clearPills() {
+ for (let input of [toAddrInput, ccAddrInput, bccAddrInput]) {
+ EventUtils.synthesizeMouseAtCenter(input, {}, composeWindow);
+ EventUtils.synthesizeKey(
+ "a",
+ {
+ accelKey: AppConstants.platform == "macosx",
+ ctrlKey: AppConstants.platform != "macosx",
+ },
+ composeWindow
+ );
+ EventUtils.synthesizeKey("KEY_Delete", {}, composeWindow);
+ }
+ checkPills(toAddrRow, []);
+ checkPills(ccAddrRow, []);
+ checkPills(bccAddrRow, []);
+ }
+
+ async function inABEditingMode() {
+ let topWindow = Services.wm.getMostRecentWindow("mail:3pane");
+ let abWindow = await topWindow.toAddressBook();
+ await new Promise(resolve => abWindow.setTimeout(resolve));
+ await TestUtils.waitForCondition(
+ () => abWindow.detailsPane.isEditing,
+ "entering editing mode"
+ );
+ let tabmail = topWindow.document.getElementById("tabmail");
+ let tab = tabmail.tabInfo.find(
+ t => t.browser?.currentURI.spec == "about:addressbook"
+ );
+ tabmail.closeTab(tab);
+ }
+
+ /**
+ * Make sure the "edit contact" menuitem only shows up for the correct
+ * contacts, and it properly opens the address book tab.
+ *
+ * @param {int} row - The row index to activate.
+ * @param {boolean} isEditable - If the selected contact should be editable.
+ */
+ async function checkEditContact(row, isEditable) {
+ clickOnRow(row, {});
+ let shownPromise = BrowserTestUtils.waitForEvent(
+ cardsContext,
+ "popupshown"
+ );
+ clickOnRow(row, { type: "contextmenu" });
+ await shownPromise;
+
+ let hiddenPromise = BrowserTestUtils.waitForEvent(
+ cardsContext,
+ "popuphidden"
+ );
+
+ Assert.equal(
+ cardsContext.querySelector("#abContextBeforeEditContact").hidden,
+ !isEditable
+ );
+ Assert.equal(
+ cardsContext.querySelector("#abContextEditContact").hidden,
+ !isEditable
+ );
+
+ // If it's an editable row, we should see the edit contact menu items.
+ if (isEditable) {
+ cardsContext.activateItem(
+ cardsContext.querySelector("#abContextEditContact")
+ );
+ await hiddenPromise;
+ await inABEditingMode();
+ composeWindow.focus();
+ await TestUtils.waitForCondition(
+ () => Services.focus.activeWindow == composeWindow
+ );
+ } else {
+ cardsContext.activateItem(
+ cardsContext.querySelector(`[command="cmd_addrBcc"]`)
+ );
+ await hiddenPromise;
+ }
+ }
+
+ // Click on a contact and make sure is editable.
+ await checkEditContact(2, true);
+ // Click on a mailing list and make sure is NOT editable.
+ await checkEditContact(6, false);
+
+ // Check that the address book picker works.
+
+ await doMenulist(book1.URI);
+ await TestUtils.waitForCondition(() => cardsList.view.rowCount != 0);
+ checkListNames(
+ ["daniel test", "jonathan test", "năthån test"],
+ "book1 contacts are shown"
+ );
+
+ await doMenulist(book2.URI);
+ await TestUtils.waitForCondition(() => cardsList.view.rowCount != 3);
+ checkListNames(
+ [
+ "danielle test",
+ "katherine test",
+ "natalie test",
+ "pèóplë named tēst",
+ "sūsãnáh test",
+ ],
+ "book2 contacts are shown"
+ );
+
+ await doMenulist("moz-abdirectory://?");
+ await TestUtils.waitForCondition(() => cardsList.view.rowCount != 5);
+ checkListNames(
+ [
+ "daniel test",
+ "danielle test",
+ "jonathan test",
+ "katherine test",
+ "natalie test",
+ "năthån test",
+ "pèóplë named tēst",
+ "sūsãnáh test",
+ ],
+ "all contacts are shown"
+ );
+
+ // Check that the search works.
+
+ EventUtils.synthesizeMouseAtCenter(searchBox, {}, sidebarWindow);
+
+ EventUtils.synthesizeKey("a", { accelKey: true }, sidebarWindow);
+ EventUtils.sendString("dan", sidebarWindow);
+ await TestUtils.waitForCondition(() => cardsList.view.rowCount != 8);
+ checkListNames(
+ ["daniel test", "danielle test"],
+ "matching contacts are shown"
+ );
+
+ EventUtils.synthesizeKey("a", { accelKey: true }, sidebarWindow);
+ EventUtils.sendString("kat", sidebarWindow);
+ await TestUtils.waitForCondition(() => cardsList.view.rowCount != 2);
+ checkListNames(["katherine test"], "matching contacts are shown");
+
+ EventUtils.synthesizeKey("KEY_Escape", { accelKey: true }, sidebarWindow);
+ await TestUtils.waitForCondition(() => cardsList.view.rowCount != 1);
+ checkListNames(
+ [
+ "daniel test",
+ "danielle test",
+ "jonathan test",
+ "katherine test",
+ "natalie test",
+ "năthån test",
+ "pèóplë named tēst",
+ "sūsãnáh test",
+ ],
+ "all contacts are shown"
+ );
+
+ // Check that double-clicking works.
+
+ clickOnRow(1, { clickCount: 2 });
+ checkPills(toAddrRow, ["danielle test <danielle.test@invalid>"]);
+
+ clickOnRow(3, { clickCount: 2 });
+ checkPills(toAddrRow, [
+ "danielle test <danielle.test@invalid>",
+ "katherine test <katherine.test@invalid>",
+ ]);
+
+ clickOnRow(6, { clickCount: 2 });
+ checkPills(toAddrRow, [
+ "danielle test <danielle.test@invalid>",
+ "katherine test <katherine.test@invalid>",
+ "pèóplë named tēst <pèóplë named tēst>",
+ ]);
+
+ clearPills();
+
+ // Check that drag and drop to the recipients section works.
+
+ clickOnRow(5, {});
+
+ dragService.startDragSessionForTests(Ci.nsIDragService.DRAGDROP_ACTION_NONE);
+ let [result, dataTransfer] = EventUtils.synthesizeDragOver(
+ cardsList,
+ toAddrInput,
+ null,
+ null,
+ sidebarWindow,
+ composeWindow
+ );
+ EventUtils.synthesizeDropAfterDragOver(
+ result,
+ dataTransfer,
+ toAddrInput,
+ composeWindow
+ );
+
+ dragService.endDragSession(true);
+ checkPills(toAddrRow, ["năthån test <năthån.test@invalid>"]);
+
+ clearPills();
+
+ // Check that the "Add to" buttons work.
+
+ clickOnRow(7, {});
+
+ Assert.ok(!toButton.disabled, "to button enabled with a contact selected");
+ Assert.ok(!ccButton.disabled, "cc button enabled with a contact selected");
+ Assert.ok(!bccButton.disabled, "bcc button enabled with a contact selected");
+
+ EventUtils.synthesizeMouseAtCenter(toButton, {}, sidebarWindow);
+ checkPills(toAddrRow, ["sūsãnáh test <sūsãnáh.test@invalid>"]);
+
+ clickOnRow(0, {});
+ EventUtils.synthesizeMouseAtCenter(ccButton, {}, sidebarWindow);
+ Assert.ok(BrowserTestUtils.is_visible(ccAddrRow), "cc row visible");
+ checkPills(ccAddrRow, ["daniel test <daniel.test@invalid>"]);
+
+ clickOnRow(2, {});
+ EventUtils.synthesizeMouseAtCenter(bccButton, {}, sidebarWindow);
+ Assert.ok(BrowserTestUtils.is_visible(bccAddrRow), "bcc row visible");
+ checkPills(bccAddrRow, ["jonathan test <jonathan.test@invalid>"]);
+
+ clearPills();
+
+ // Check that the context menu works.
+
+ await doContextMenu(7, "cmd_addrTo");
+ checkPills(toAddrRow, ["sūsãnáh test <sūsãnáh.test@invalid>"]);
+
+ await doContextMenu(4, "cmd_addrCc");
+ checkPills(ccAddrRow, ["natalie test <natalie.test@invalid>"]);
+
+ await doContextMenu(2, "cmd_addrBcc");
+ checkPills(bccAddrRow, ["jonathan test <jonathan.test@invalid>"]);
+
+ clearPills();
+
+ let promptPromise = BrowserTestUtils.promiseAlertDialog("accept");
+ let deletedPromise = TestUtils.topicObserved(
+ "addrbook-contact-deleted",
+ c => c.displayName == "daniel test"
+ );
+ doContextMenu(0, "cmd_delete");
+ await promptPromise;
+ await deletedPromise;
+ await TestUtils.waitForCondition(() => cardsList.view.rowCount != 8);
+ checkListNames(
+ [
+ "danielle test",
+ "jonathan test",
+ "katherine test",
+ "natalie test",
+ "năthån test",
+ "pèóplë named tēst",
+ "sūsãnáh test",
+ ],
+ "all contacts are shown"
+ );
+
+ // Check that the keyboard commands work.
+
+ promptPromise = BrowserTestUtils.promiseAlertDialog("accept");
+ deletedPromise = TestUtils.topicObserved(
+ "addrbook-contact-deleted",
+ c => c.displayName == "danielle test"
+ );
+ clickOnRow(0, {});
+ EventUtils.synthesizeKey("KEY_Delete", {}, sidebarWindow);
+ await promptPromise;
+ await deletedPromise;
+ await TestUtils.waitForCondition(() => cardsList.view.rowCount != 7);
+ checkListNames(
+ [
+ "jonathan test",
+ "katherine test",
+ "natalie test",
+ "năthån test",
+ "pèóplë named tēst",
+ "sūsãnáh test",
+ ],
+ "all contacts are shown"
+ );
+
+ // TODO sidebar context menu
+
+ // Close the compose window and clean up.
+
+ EventUtils.synthesizeKey("KEY_F9", {}, composeWindow);
+ await TestUtils.waitForCondition(() => BrowserTestUtils.is_hidden(sidebar));
+
+ promptPromise = BrowserTestUtils.promiseAlertDialog("extra1");
+ let closePromise = BrowserTestUtils.windowClosed(composeWindow);
+ composeWindow.goDoCommand("cmd_close");
+ await promptPromise;
+ await closePromise;
+});
diff --git a/comm/mail/components/addrbook/test/browser/browser_contact_tree.js b/comm/mail/components/addrbook/test/browser/browser_contact_tree.js
new file mode 100644
index 0000000000..f502fe855a
--- /dev/null
+++ b/comm/mail/components/addrbook/test/browser/browser_contact_tree.js
@@ -0,0 +1,1261 @@
+/* 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/. */
+
+function rightClickOnIndex(index) {
+ let abWindow = getAddressBookWindow();
+ let cardsList = abWindow.cardsPane.cardsList;
+ let menu = abWindow.document.getElementById("cardContext");
+
+ let shownPromise = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(
+ cardsList.getRowAtIndex(index),
+ { type: "contextmenu" },
+ abWindow
+ );
+ return shownPromise;
+}
+
+/**
+ * Tests that additions and removals are accurately displayed, or not
+ * displayed if they happen outside the current address book.
+ */
+add_task(async function test_additions_and_removals() {
+ async function deleteRowWithPrompt(index) {
+ let promptPromise = BrowserTestUtils.promiseAlertDialogOpen("accept");
+ EventUtils.synthesizeMouseAtCenter(
+ cardsList.getRowAtIndex(index),
+ {},
+ abWindow
+ );
+ EventUtils.synthesizeKey("VK_DELETE", {}, abWindow);
+ await promptPromise;
+ await new Promise(r => abWindow.setTimeout(r));
+ await new Promise(r => abWindow.setTimeout(r));
+ }
+
+ let bookA = createAddressBook("book A");
+ let contactA1 = bookA.addCard(createContact("contact", "A1"));
+ let bookB = createAddressBook("book B");
+ let contactB1 = bookB.addCard(createContact("contact", "B1"));
+
+ let abWindow = await openAddressBookWindow();
+ let cardsList = abWindow.cardsPane.cardsList;
+
+ await openAllAddressBooks();
+ info("Performing check #1");
+ checkCardsListed(contactA1, contactB1);
+
+ // While in bookA, add a contact and list. Check that they show up.
+ openDirectory(bookA);
+ checkCardsListed(contactA1);
+ let contactA2 = bookA.addCard(createContact("contact", "A2")); // Add A2.
+ checkCardsListed(contactA1, contactA2);
+ let listC = bookA.addMailList(createMailingList("list C")); // Add C.
+ checkDirectoryDisplayed(bookA);
+ checkCardsListed(contactA1, contactA2, listC);
+ listC.addCard(contactA1);
+ checkCardsListed(contactA1, contactA2, listC);
+
+ await openAllAddressBooks();
+ info("Performing check #2");
+ checkCardsListed(contactA1, contactA2, contactB1, listC);
+
+ // While in listC, add a member and remove a member. Check that they show up
+ // or disappear as appropriate.
+ openDirectory(listC);
+ checkCardsListed(contactA1);
+ listC.addCard(contactA2);
+ checkCardsListed(contactA1, contactA2);
+ await deleteRowWithPrompt(0);
+ checkCardsListed(contactA2);
+ Assert.equal(cardsList.currentIndex, 0);
+
+ await openAllAddressBooks();
+ info("Performing check #3");
+ checkCardsListed(contactA1, contactA2, contactB1, listC);
+
+ // While in bookA, delete a contact. Check it disappears.
+ openDirectory(bookA);
+ checkCardsListed(contactA1, contactA2, listC);
+ await deleteRowWithPrompt(0); // Delete A1.
+ checkCardsListed(contactA2, listC);
+ Assert.equal(cardsList.currentIndex, 0);
+ // Now do some things in an unrelated book. Check nothing changes here.
+ let contactB2 = bookB.addCard(createContact("contact", "B2")); // Add B2.
+ checkCardsListed(contactA2, listC);
+ let listD = bookB.addMailList(createMailingList("list D")); // Add D.
+ checkDirectoryDisplayed(bookA);
+ checkCardsListed(contactA2, listC);
+ listD.addCard(contactB1);
+ checkCardsListed(contactA2, listC);
+
+ await openAllAddressBooks();
+ info("Performing check #4");
+ checkCardsListed(contactA2, contactB1, contactB2, listC, listD);
+
+ // While in listC, do some things in an unrelated list. Check nothing
+ // changes here.
+ openDirectory(listC);
+ checkCardsListed(contactA2);
+ listD.addCard(contactB2);
+ checkCardsListed(contactA2);
+ listD.deleteCards([contactB1]);
+ checkCardsListed(contactA2);
+ bookB.deleteCards([contactB1]);
+ checkCardsListed(contactA2);
+
+ await openAllAddressBooks();
+ info("Performing check #5");
+ checkCardsListed(contactA2, contactB2, listC, listD);
+
+ // While in bookA, do some things in an unrelated book. Check nothing
+ // changes here.
+ openDirectory(bookA);
+ checkCardsListed(contactA2, listC);
+ bookB.deleteDirectory(listD); // Delete D.
+ checkDirectoryDisplayed(bookA);
+ checkCardsListed(contactA2, listC);
+ await deleteRowWithPrompt(1); // Delete C.
+ checkCardsListed(contactA2);
+
+ // While in "All Address Books", make some changes and check that things
+ // appear or disappear as appropriate.
+ await openAllAddressBooks();
+ info("Performing check #6");
+ checkCardsListed(contactA2, contactB2);
+ let listE = bookB.addMailList(createMailingList("list E")); // Add E.
+ checkDirectoryDisplayed(null);
+ checkCardsListed(contactA2, contactB2, listE);
+ listE.addCard(contactB2);
+ checkCardsListed(contactA2, contactB2, listE);
+ listE.deleteCards([contactB2]);
+ checkCardsListed(contactA2, contactB2, listE);
+ bookB.deleteDirectory(listE); // Delete E.
+ checkDirectoryDisplayed(null);
+ checkCardsListed(contactA2, contactB2);
+ await deleteRowWithPrompt(1);
+ checkCardsListed(contactA2);
+ Assert.equal(cardsList.currentIndex, 0);
+ bookA.deleteCards([contactA2]);
+ checkCardsListed();
+ Assert.equal(cardsList.currentIndex, -1);
+
+ // While in "All Address Books", delete a directory that has contacts and
+ // mailing lists. They should disappear.
+ let contactA3 = bookA.addCard(createContact("contact", "A3")); // Add A3.
+ checkCardsListed(contactA3);
+ let listF = bookA.addMailList(createMailingList("list F")); // Add F.
+ checkCardsListed(contactA3, listF);
+ await promiseDirectoryRemoved(bookA.URI);
+ checkCardsListed();
+
+ abWindow.close();
+
+ await promiseDirectoryRemoved(bookB.URI);
+});
+
+/**
+ * Tests that added contacts are inserted in the right place in the list.
+ */
+add_task(async function test_insertion_order() {
+ await openAddressBookWindow();
+
+ let bookA = createAddressBook("book A");
+ openDirectory(bookA);
+ checkCardsListed();
+ let contactA2 = bookA.addCard(createContact("contact", "A2"));
+ checkCardsListed(contactA2);
+ let contactA1 = bookA.addCard(createContact("contact", "A1")); // Add first.
+ checkCardsListed(contactA1, contactA2);
+ let contactA5 = bookA.addCard(createContact("contact", "A5")); // Add last.
+ checkCardsListed(contactA1, contactA2, contactA5);
+ let contactA3 = bookA.addCard(createContact("contact", "A3")); // Add in the middle.
+ checkCardsListed(contactA1, contactA2, contactA3, contactA5);
+
+ // Flip sort direction.
+ await showSortMenu("sort", "GeneratedName descending");
+
+ checkCardsListed(contactA5, contactA3, contactA2, contactA1);
+ let contactA4 = bookA.addCard(createContact("contact", "A4")); // Add in the middle.
+ checkCardsListed(contactA5, contactA4, contactA3, contactA2, contactA1);
+ let contactA7 = bookA.addCard(createContact("contact", "A7")); // Add first.
+ checkCardsListed(
+ contactA7,
+ contactA5,
+ contactA4,
+ contactA3,
+ contactA2,
+ contactA1
+ );
+ let contactA0 = bookA.addCard(createContact("contact", "A0")); // Add last.
+ checkCardsListed(
+ contactA7,
+ contactA5,
+ contactA4,
+ contactA3,
+ contactA2,
+ contactA1,
+ contactA0
+ );
+
+ contactA3.displayName = "contact A6";
+ contactA3.lastName = "contact A3";
+ contactA3.primaryEmail = "contact.A6@invalid";
+ bookA.modifyCard(contactA3); // Rename, should change position.
+ checkCardsListed(
+ contactA7,
+ contactA3, // Actually A6.
+ contactA5,
+ contactA4,
+ contactA2,
+ contactA1,
+ contactA0
+ );
+
+ // Restore original sort direction.
+ await showSortMenu("sort", "GeneratedName ascending");
+
+ checkCardsListed(
+ contactA0,
+ contactA1,
+ contactA2,
+ contactA4,
+ contactA5,
+ contactA3, // Actually A6.
+ contactA7
+ );
+ await closeAddressBookWindow();
+
+ await promiseDirectoryRemoved(bookA.URI);
+});
+
+/**
+ * Tests the name column is updated when the format changes.
+ */
+add_task(async function test_name_column() {
+ const {
+ GENERATE_DISPLAY_NAME,
+ GENERATE_LAST_FIRST_ORDER,
+ GENERATE_FIRST_LAST_ORDER,
+ } = Ci.nsIAbCard;
+
+ let book = createAddressBook("book");
+ book.addCard(createContact("alpha", "tango", "kilo"));
+ book.addCard(createContact("bravo", "zulu", "quebec"));
+ book.addCard(createContact("charlie", "mike", "whiskey"));
+ book.addCard(createContact("delta", "foxtrot", "sierra"));
+ book.addCard(createContact("echo", "november", "uniform"));
+
+ let abWindow = await openAddressBookWindow();
+ let cardsList = abWindow.cardsPane.cardsList;
+
+ // Check the format is display name, ascending.
+ Assert.equal(
+ Services.prefs.getIntPref("mail.addr_book.lastnamefirst"),
+ GENERATE_DISPLAY_NAME
+ );
+
+ checkNamesListed("kilo", "quebec", "sierra", "uniform", "whiskey");
+
+ // Select the "delta foxtrot" contact. This should remain selected throughout.
+ cardsList.selectedIndex = 2;
+ Assert.equal(cardsList.selectedIndex, 2);
+
+ // Change the format to last, first.
+ await showSortMenu("format", GENERATE_LAST_FIRST_ORDER);
+ checkNamesListed(
+ "foxtrot, delta",
+ "mike, charlie",
+ "november, echo",
+ "tango, alpha",
+ "zulu, bravo"
+ );
+ Assert.equal(cardsList.selectedIndex, 0);
+ Assert.deepEqual(cardsList.selectedIndices, [0]);
+
+ // Change the format to first last.
+ await showSortMenu("format", GENERATE_FIRST_LAST_ORDER);
+ checkNamesListed(
+ "alpha tango",
+ "bravo zulu",
+ "charlie mike",
+ "delta foxtrot",
+ "echo november"
+ );
+ Assert.equal(cardsList.selectedIndex, 3);
+
+ // Flip the order to descending.
+ await showSortMenu("sort", "GeneratedName descending");
+
+ checkNamesListed(
+ "echo november",
+ "delta foxtrot",
+ "charlie mike",
+ "bravo zulu",
+ "alpha tango"
+ );
+ Assert.equal(cardsList.selectedIndex, 1);
+
+ // Change the format to last, first.
+ await showSortMenu("format", GENERATE_LAST_FIRST_ORDER);
+ checkNamesListed(
+ "zulu, bravo",
+ "tango, alpha",
+ "november, echo",
+ "mike, charlie",
+ "foxtrot, delta"
+ );
+ Assert.equal(cardsList.selectedIndex, 4);
+
+ // Change the format to display name.
+ await showSortMenu("format", GENERATE_DISPLAY_NAME);
+ checkNamesListed("whiskey", "uniform", "sierra", "quebec", "kilo");
+ Assert.equal(cardsList.selectedIndex, 2);
+
+ // Sort by email address, ascending.
+ await showSortMenu("sort", "EmailAddresses ascending");
+
+ checkNamesListed("kilo", "quebec", "whiskey", "sierra", "uniform");
+ Assert.equal(cardsList.selectedIndex, 3);
+
+ // Change the format to last, first.
+ await showSortMenu("format", GENERATE_LAST_FIRST_ORDER);
+ checkNamesListed(
+ "tango, alpha",
+ "zulu, bravo",
+ "mike, charlie",
+ "foxtrot, delta",
+ "november, echo"
+ );
+ Assert.equal(cardsList.selectedIndex, 3);
+
+ // Change the format to first last.
+ await showSortMenu("format", GENERATE_FIRST_LAST_ORDER);
+ checkNamesListed(
+ "alpha tango",
+ "bravo zulu",
+ "charlie mike",
+ "delta foxtrot",
+ "echo november"
+ );
+ Assert.equal(cardsList.selectedIndex, 3);
+
+ // Change the format to display name.
+ await showSortMenu("format", GENERATE_DISPLAY_NAME);
+ checkNamesListed("kilo", "quebec", "whiskey", "sierra", "uniform");
+ Assert.equal(cardsList.selectedIndex, 3);
+
+ // Restore original sort column and direction.
+ await showSortMenu("sort", "GeneratedName ascending");
+
+ checkNamesListed("kilo", "quebec", "sierra", "uniform", "whiskey");
+ Assert.equal(cardsList.selectedIndex, 2);
+
+ await closeAddressBookWindow();
+
+ await promiseDirectoryRemoved(book.URI);
+});
+
+/**
+ * Tests that sort order and name format survive closing and reopening.
+ */
+add_task(async function test_persistence() {
+ let book = createAddressBook("book");
+ book.addCard(createContact("alpha", "tango", "kilo"));
+ book.addCard(createContact("bravo", "zulu", "quebec"));
+ book.addCard(createContact("charlie", "mike", "whiskey"));
+ book.addCard(createContact("delta", "foxtrot", "sierra"));
+ book.addCard(createContact("echo", "november", "uniform"));
+
+ Services.xulStore.removeDocument("about:addressbook");
+ Services.prefs.clearUserPref("mail.addr_book.lastnamefirst");
+
+ await openAddressBookWindow();
+ checkNamesListed("kilo", "quebec", "sierra", "uniform", "whiskey");
+
+ info("sorting by GeneratedName, descending");
+ await showSortMenu("sort", "GeneratedName descending");
+ checkNamesListed("whiskey", "uniform", "sierra", "quebec", "kilo");
+
+ await closeAddressBookWindow();
+ info("address book closed, reopening");
+ await openAddressBookWindow();
+ checkNamesListed("whiskey", "uniform", "sierra", "quebec", "kilo");
+
+ info("sorting by EmailAddresses, ascending");
+ await showSortMenu("sort", "EmailAddresses ascending");
+ checkNamesListed("kilo", "quebec", "whiskey", "sierra", "uniform");
+
+ await closeAddressBookWindow();
+ info("address book closed, reopening");
+ await openAddressBookWindow();
+ checkNamesListed("kilo", "quebec", "whiskey", "sierra", "uniform");
+
+ info("setting name format to first last");
+ await showSortMenu("format", Ci.nsIAbCard.GENERATE_FIRST_LAST_ORDER);
+ checkNamesListed(
+ "alpha tango",
+ "bravo zulu",
+ "charlie mike",
+ "delta foxtrot",
+ "echo november"
+ );
+
+ await closeAddressBookWindow();
+ info("address book closed, reopening");
+ await openAddressBookWindow();
+ checkNamesListed(
+ "alpha tango",
+ "bravo zulu",
+ "charlie mike",
+ "delta foxtrot",
+ "echo november"
+ );
+
+ await closeAddressBookWindow();
+
+ Services.xulStore.removeDocument("about:addressbook");
+ Services.prefs.clearUserPref("mail.addr_book.lastnamefirst");
+
+ await promiseDirectoryRemoved(book.URI);
+});
+
+/**
+ * Tests the context menu compose items.
+ */
+add_task(async function test_context_menu_compose() {
+ MailServices.accounts.createLocalMailAccount();
+ let account = MailServices.accounts.accounts[0];
+ account.addIdentity(MailServices.accounts.createIdentity());
+
+ registerCleanupFunction(async () => {
+ MailServices.accounts.removeAccount(account, true);
+ });
+
+ let book = createAddressBook("Book");
+ let contactA = book.addCard(createContact("Contact", "A"));
+ let contactB = createContact("Contact", "B");
+ contactB.setProperty("SecondEmail", "b.contact@invalid");
+ contactB = book.addCard(contactB);
+ let contactC = createContact("Contact", "C");
+ contactC.primaryEmail = null;
+ contactC.setProperty("SecondEmail", "c.contact@invalid");
+ contactC = book.addCard(contactC);
+ let contactD = createContact("Contact", "D");
+ contactD.primaryEmail = null;
+ contactD = book.addCard(contactD);
+ let list = book.addMailList(createMailingList("List"));
+ list.addCard(contactA);
+ list.addCard(contactB);
+
+ let abWindow = await openAddressBookWindow();
+ let abDocument = abWindow.document;
+ let cardsList = abWindow.cardsPane.cardsList;
+
+ let menu = abDocument.getElementById("cardContext");
+ let writeMenuItem = abDocument.getElementById("cardContextWrite");
+ let writeMenu = abDocument.getElementById("cardContextWriteMenu");
+ let writeMenuSeparator = abDocument.getElementById(
+ "cardContextWriteSeparator"
+ );
+
+ openDirectory(book);
+
+ // Contact A, first and only email address.
+
+ let composeWindowPromise = BrowserTestUtils.domWindowOpened();
+
+ await rightClickOnIndex(0);
+ Assert.ok(!writeMenuItem.hidden, "write menu item shown");
+ Assert.ok(writeMenu.hidden, "write menu hidden");
+ Assert.ok(!writeMenuSeparator.hidden, "write menu separator shown");
+ menu.activateItem(writeMenuItem);
+
+ await checkComposeWindow(
+ await composeWindowPromise,
+ "Contact A <contact.a@invalid>"
+ );
+
+ // Contact B, first email address.
+
+ composeWindowPromise = BrowserTestUtils.domWindowOpened();
+
+ await rightClickOnIndex(1);
+ Assert.ok(writeMenuItem.hidden, "write menu item hidden");
+ Assert.ok(!writeMenu.hidden, "write menu shown");
+ Assert.ok(!writeMenuSeparator.hidden, "write menu separator shown");
+ let shownPromise = BrowserTestUtils.waitForEvent(writeMenu, "popupshown");
+ writeMenu.openMenu(true);
+ await shownPromise;
+ let subMenuItems = writeMenu.querySelectorAll("menuitem");
+ Assert.equal(subMenuItems.length, 2);
+ Assert.equal(subMenuItems[0].label, "Contact B <contact.b@invalid>");
+ Assert.equal(subMenuItems[1].label, "Contact B <b.contact@invalid>");
+
+ writeMenu.menupopup.activateItem(subMenuItems[0]);
+
+ await checkComposeWindow(
+ await composeWindowPromise,
+ "Contact B <contact.b@invalid>"
+ );
+
+ // Contact B, second email address.
+
+ composeWindowPromise = BrowserTestUtils.domWindowOpened();
+
+ await rightClickOnIndex(1);
+ Assert.ok(writeMenuItem.hidden, "write menu item hidden");
+ Assert.ok(!writeMenu.hidden, "write menu shown");
+ Assert.ok(!writeMenuSeparator.hidden, "write menu separator shown");
+ shownPromise = BrowserTestUtils.waitForEvent(writeMenu, "popupshown");
+ writeMenu.openMenu(true);
+ await shownPromise;
+ subMenuItems = writeMenu.querySelectorAll("menuitem");
+ Assert.equal(subMenuItems.length, 2);
+ Assert.equal(subMenuItems[0].label, "Contact B <contact.b@invalid>");
+ Assert.equal(subMenuItems[1].label, "Contact B <b.contact@invalid>");
+
+ writeMenu.menupopup.activateItem(subMenuItems[1]);
+
+ await checkComposeWindow(
+ await composeWindowPromise,
+ "Contact B <b.contact@invalid>"
+ );
+
+ // Contact C, second and only email address.
+
+ composeWindowPromise = BrowserTestUtils.domWindowOpened();
+
+ await rightClickOnIndex(2);
+ Assert.ok(!writeMenuItem.hidden, "write menu item shown");
+ Assert.ok(writeMenu.hidden, "write menu hidden");
+ Assert.ok(!writeMenuSeparator.hidden, "write menu separator shown");
+ menu.activateItem(writeMenuItem);
+
+ await checkComposeWindow(
+ await composeWindowPromise,
+ "Contact C <c.contact@invalid>"
+ );
+
+ // Contact D, no email address.
+
+ await rightClickOnIndex(3);
+ Assert.ok(writeMenuItem.hidden, "write menu item hidden");
+ Assert.ok(writeMenu.hidden, "write menu hidden");
+ Assert.ok(writeMenuSeparator.hidden, "write menu separator hidden");
+ menu.hidePopup();
+
+ // List.
+
+ composeWindowPromise = BrowserTestUtils.domWindowOpened();
+
+ await rightClickOnIndex(4);
+ Assert.ok(!writeMenuItem.hidden, "write menu item shown");
+ Assert.ok(writeMenu.hidden, "write menu hidden");
+ Assert.ok(!writeMenuSeparator.hidden, "write menu separator shown");
+ menu.activateItem(writeMenuItem);
+
+ await checkComposeWindow(await composeWindowPromise, "List <List>");
+
+ // Contact A and Contact D.
+
+ composeWindowPromise = BrowserTestUtils.domWindowOpened();
+
+ cardsList.selectedIndices = [0, 3];
+ await rightClickOnIndex(3);
+ Assert.ok(!writeMenuItem.hidden, "write menu item shown");
+ Assert.ok(writeMenu.hidden, "write menu hidden");
+ Assert.ok(!writeMenuSeparator.hidden, "write menu separator shown");
+ menu.activateItem(writeMenuItem);
+
+ await checkComposeWindow(
+ await composeWindowPromise,
+ "Contact A <contact.a@invalid>"
+ );
+
+ // Contact B and Contact C.
+
+ composeWindowPromise = BrowserTestUtils.domWindowOpened();
+
+ cardsList.selectedIndices = [1, 2];
+ await rightClickOnIndex(2);
+ Assert.ok(!writeMenuItem.hidden, "write menu item shown");
+ Assert.ok(writeMenu.hidden, "write menu hidden");
+ Assert.ok(!writeMenuSeparator.hidden, "write menu separator shown");
+ menu.activateItem(writeMenuItem);
+
+ await checkComposeWindow(
+ await composeWindowPromise,
+ "Contact B <contact.b@invalid>",
+ "Contact C <c.contact@invalid>"
+ );
+
+ // Contact B and List.
+
+ composeWindowPromise = BrowserTestUtils.domWindowOpened();
+
+ cardsList.selectedIndices = [1, 4];
+ await rightClickOnIndex(4);
+ Assert.ok(!writeMenuItem.hidden, "write menu item shown");
+ Assert.ok(writeMenu.hidden, "write menu hidden");
+ Assert.ok(!writeMenuSeparator.hidden, "write menu separator shown");
+ menu.activateItem(writeMenuItem);
+
+ await checkComposeWindow(
+ await composeWindowPromise,
+ "Contact B <contact.b@invalid>",
+ "List <List>"
+ );
+
+ await closeAddressBookWindow();
+
+ await promiseDirectoryRemoved(book.URI);
+});
+
+/**
+ * Tests the context menu edit items.
+ */
+add_task(async function test_context_menu_edit() {
+ let normalBook = createAddressBook("Normal Book");
+ let normalList = normalBook.addMailList(createMailingList("Normal List"));
+ let normalContact = normalBook.addCard(createContact("Normal", "Contact"));
+ normalList.addCard(normalContact);
+
+ let readOnlyBook = createAddressBook("Read-Only Book");
+ let readOnlyList = readOnlyBook.addMailList(
+ createMailingList("Read-Only List")
+ );
+ let readOnlyContact = readOnlyBook.addCard(
+ createContact("Read-Only", "Contact")
+ );
+ readOnlyList.addCard(readOnlyContact);
+ readOnlyBook.setBoolValue("readOnly", true);
+
+ let abWindow = await openAddressBookWindow();
+ let abDocument = abWindow.document;
+ let cardsList = abWindow.cardsPane.cardsList;
+
+ let menu = abDocument.getElementById("cardContext");
+ let editMenuItem = abDocument.getElementById("cardContextEdit");
+ let exportMenuItem = abDocument.getElementById("cardContextExport");
+
+ async function checkEditItems(index, hidden, isMailList = false) {
+ await rightClickOnIndex(index);
+
+ Assert.equal(
+ editMenuItem.hidden,
+ hidden,
+ `editMenuItem should be hidden=${hidden} on index ${index}`
+ );
+ Assert.equal(
+ exportMenuItem.hidden,
+ !isMailList,
+ `exportMenuItem should be hidden=${!isMailList} on index ${index}`
+ );
+
+ Assert.deepEqual(document.l10n.getAttributes(editMenuItem), {
+ id: isMailList
+ ? "about-addressbook-books-context-edit-list"
+ : "about-addressbook-books-context-edit",
+ args: null,
+ });
+
+ let hiddenPromise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ menu.hidePopup();
+ await hiddenPromise;
+ }
+
+ info("Testing Normal Book");
+ openDirectory(normalBook);
+ await checkEditItems(0, false); // normal contact
+ await checkEditItems(1, false, true); // normal list
+
+ cardsList.selectedIndices = [0, 1];
+ await checkEditItems(0, true); // normal contact + normal list
+ await checkEditItems(1, true); // normal contact + normal list
+
+ info("Testing Normal List");
+ openDirectory(normalList);
+ await checkEditItems(0, false); // normal contact
+
+ info("Testing Read-Only Book");
+ openDirectory(readOnlyBook);
+ await checkEditItems(0, true); // read-only contact
+ await checkEditItems(1, true, true); // read-only list
+
+ info("Testing Read-Only List");
+ openDirectory(readOnlyList);
+ await checkEditItems(0, true); // read-only contact
+
+ info("Testing All Address Books");
+ openAllAddressBooks();
+ await checkEditItems(0, false); // normal contact
+ await checkEditItems(1, false, true); // normal list
+ await checkEditItems(2, true); // read-only contact
+ await checkEditItems(3, true, true); // read-only list
+
+ cardsList.selectedIndices = [0, 1];
+ await checkEditItems(1, true); // normal contact + normal list
+
+ cardsList.selectedIndices = [0, 2];
+ await checkEditItems(2, true); // normal contact + read-only contact
+
+ cardsList.selectedIndices = [1, 3];
+ await checkEditItems(3, true); // normal list + read-only list
+
+ cardsList.selectedIndices = [0, 1, 2, 3];
+ await checkEditItems(3, true); // everything
+
+ await closeAddressBookWindow();
+
+ await promiseDirectoryRemoved(normalBook.URI);
+ await promiseDirectoryRemoved(readOnlyBook.URI);
+});
+
+/**
+ * Tests the context menu delete items.
+ */
+add_task(async function test_context_menu_delete() {
+ let normalBook = createAddressBook("Normal Book");
+ let normalList = normalBook.addMailList(createMailingList("Normal List"));
+ let normalContact = normalBook.addCard(createContact("Normal", "Contact"));
+ normalList.addCard(normalContact);
+
+ let readOnlyBook = createAddressBook("Read-Only Book");
+ let readOnlyList = readOnlyBook.addMailList(
+ createMailingList("Read-Only List")
+ );
+ let readOnlyContact = readOnlyBook.addCard(
+ createContact("Read-Only", "Contact")
+ );
+ readOnlyList.addCard(readOnlyContact);
+ readOnlyBook.setBoolValue("readOnly", true);
+
+ let abWindow = await openAddressBookWindow();
+ let abDocument = abWindow.document;
+ let cardsList = abWindow.cardsPane.cardsList;
+
+ let menu = abDocument.getElementById("cardContext");
+ let deleteMenuItem = abDocument.getElementById("cardContextDelete");
+ let removeMenuItem = abDocument.getElementById("cardContextRemove");
+
+ async function checkDeleteItems(index, deleteHidden, removeHidden, disabled) {
+ await rightClickOnIndex(index);
+
+ Assert.equal(
+ deleteMenuItem.hidden,
+ deleteHidden,
+ `deleteMenuItem.hidden on index ${index}`
+ );
+ Assert.equal(
+ deleteMenuItem.disabled,
+ disabled,
+ `deleteMenuItem.disabled on index ${index}`
+ );
+ Assert.equal(
+ removeMenuItem.hidden,
+ removeHidden,
+ `removeMenuItem.hidden on index ${index}`
+ );
+ Assert.equal(
+ removeMenuItem.disabled,
+ disabled,
+ `removeMenuItem.disabled on index ${index}`
+ );
+
+ let hiddenPromise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ menu.hidePopup();
+ await hiddenPromise;
+ }
+
+ info("Testing Normal Book");
+ openDirectory(normalBook);
+ await checkDeleteItems(0, false, true, false); // normal contact
+ await checkDeleteItems(1, false, true, false); // normal list
+
+ cardsList.selectedIndices = [0, 1];
+ await checkDeleteItems(0, false, true, false); // normal contact + normal list
+ await checkDeleteItems(1, false, true, false); // normal contact + normal list
+
+ info("Testing Normal List");
+ openDirectory(normalList);
+ await checkDeleteItems(0, true, false, false); // normal contact
+
+ info("Testing Read-Only Book");
+ openDirectory(readOnlyBook);
+ await checkDeleteItems(0, false, true, true); // read-only contact
+ await checkDeleteItems(1, false, true, true); // read-only list
+
+ info("Testing Read-Only List");
+ openDirectory(readOnlyList);
+ await checkDeleteItems(0, true, false, true); // read-only contact
+
+ info("Testing All Address Books");
+ openAllAddressBooks();
+ await checkDeleteItems(0, false, true, false); // normal contact
+ await checkDeleteItems(1, false, true, false); // normal list
+ await checkDeleteItems(2, false, true, true); // read-only contact
+ await checkDeleteItems(3, false, true, true); // read-only list
+
+ cardsList.selectedIndices = [0, 1];
+ await checkDeleteItems(1, false, true, false); // normal contact + normal list
+
+ cardsList.selectedIndices = [0, 2];
+ await checkDeleteItems(2, false, true, true); // normal contact + read-only contact
+
+ cardsList.selectedIndices = [1, 3];
+ await checkDeleteItems(3, false, true, true); // normal list + read-only list
+
+ cardsList.selectedIndices = [0, 1, 2, 3];
+ await checkDeleteItems(3, false, true, true); // everything
+
+ await closeAddressBookWindow();
+
+ await promiseDirectoryRemoved(normalBook.URI);
+ await promiseDirectoryRemoved(readOnlyBook.URI);
+});
+
+add_task(async function test_layout() {
+ function checkColumns(visibleColumns, sortColumn, sortDirection) {
+ let visibleHeaders = cardsHeader.querySelectorAll(
+ `th[is="tree-view-table-header-cell"]:not([hidden])`
+ );
+ Assert.deepEqual(
+ Array.from(visibleHeaders, h => h.id),
+ visibleColumns,
+ "visible columns are correct"
+ );
+
+ for (let header of visibleHeaders) {
+ let button = header.querySelector("button");
+ Assert.equal(
+ button.classList.contains("ascending"),
+ header.id == sortColumn && sortDirection == "ascending",
+ `${header.id} header is ascending`
+ );
+ Assert.equal(
+ button.classList.contains("descending"),
+ header.id == sortColumn && sortDirection == "descending",
+ `${header.id} header is descending`
+ );
+ }
+ }
+
+ function checkRowHeight(height) {
+ Assert.equal(cardsList.getRowAtIndex(0).clientHeight, height);
+ }
+
+ Services.prefs.setIntPref("mail.uidensity", 0);
+ personalBook.addCard(
+ createContact("contact", "one", undefined, "first@invalid")
+ );
+ personalBook.addCard(
+ createContact("contact", "two", undefined, "second@invalid")
+ );
+ personalBook.addCard(
+ createContact("contact", "three", undefined, "third@invalid")
+ );
+ personalBook.addCard(
+ createContact("contact", "four", undefined, "fourth@invalid")
+ );
+
+ let abWindow = await openAddressBookWindow();
+ let abDocument = abWindow.document;
+ let cardsList = abWindow.cardsPane.cardsList;
+ let cardsHeader = abWindow.cardsPane.table.header;
+ let sharedSplitter = abDocument.getElementById("sharedSplitter");
+
+ // Sanity check.
+
+ Assert.ok(
+ !abDocument.body.classList.contains("layout-table"),
+ "not table layout on opening"
+ );
+ Assert.equal(
+ sharedSplitter.resizeDirection,
+ "horizontal",
+ "splitter direction is horizontal"
+ );
+ Assert.equal(
+ sharedSplitter.resizeElement.id,
+ "cardsPane",
+ "splitter is affecting the cards pane"
+ );
+ Assert.equal(
+ cardsList.getAttribute("rows"),
+ "ab-card-row",
+ "list row implementation used"
+ );
+
+ // Switch layout to table.
+
+ await toggleLayout();
+
+ Assert.ok(
+ abDocument.body.classList.contains("layout-table"),
+ "layout changed"
+ );
+ Assert.equal(
+ sharedSplitter.resizeDirection,
+ "vertical",
+ "splitter direction is vertical"
+ );
+ Assert.equal(
+ sharedSplitter.resizeElement.id,
+ "detailsPane",
+ "splitter is affecting the details pane"
+ );
+ Assert.equal(
+ cardsList.getAttribute("rows"),
+ "ab-table-card-row",
+ "table row implementation used"
+ );
+
+ checkColumns(
+ ["GeneratedName", "EmailAddresses", "PhoneNumbers", "Addresses"],
+ "GeneratedName",
+ "ascending"
+ );
+ checkNamesListed(
+ "contact four",
+ "contact one",
+ "contact three",
+ "contact two"
+ );
+ checkRowHeight(18);
+
+ // Click the email addresses header to sort.
+
+ EventUtils.synthesizeMouseAtCenter(
+ cardsHeader.querySelector(`[id="EmailAddressesButton"]`),
+ {},
+ abWindow
+ );
+ checkColumns(
+ ["GeneratedName", "EmailAddresses", "PhoneNumbers", "Addresses"],
+ "EmailAddresses",
+ "ascending"
+ );
+ checkNamesListed(
+ "contact one",
+ "contact four",
+ "contact two",
+ "contact three"
+ );
+
+ // Click the email addresses header again to flip the sort.
+
+ EventUtils.synthesizeMouseAtCenter(
+ cardsHeader.querySelector(`[id="EmailAddressesButton"]`),
+ {},
+ abWindow
+ );
+ checkColumns(
+ ["GeneratedName", "EmailAddresses", "PhoneNumbers", "Addresses"],
+ "EmailAddresses",
+ "descending"
+ );
+ checkNamesListed(
+ "contact three",
+ "contact two",
+ "contact four",
+ "contact one"
+ );
+
+ // Add a column.
+
+ await showPickerMenu("toggle", "Title");
+ await TestUtils.waitForCondition(
+ () => !cardsHeader.querySelector(`[id="Title"]`).hidden
+ );
+ checkColumns(
+ ["GeneratedName", "EmailAddresses", "PhoneNumbers", "Addresses", "Title"],
+ "EmailAddresses",
+ "descending"
+ );
+
+ // Remove a column.
+
+ await showPickerMenu("toggle", "Addresses");
+ await TestUtils.waitForCondition(
+ () => cardsHeader.querySelector(`[id="Addresses"]`).hidden
+ );
+ checkColumns(
+ ["GeneratedName", "EmailAddresses", "PhoneNumbers", "Title"],
+ "EmailAddresses",
+ "descending"
+ );
+
+ // Change the density.
+
+ Services.prefs.setIntPref("mail.uidensity", 1);
+ checkRowHeight(22);
+
+ Services.prefs.setIntPref("mail.uidensity", 2);
+ checkRowHeight(32);
+
+ // Close and reopen the Address Book and check that settings were remembered.
+
+ await closeAddressBookWindow();
+
+ abWindow = await openAddressBookWindow();
+ abDocument = abWindow.document;
+ cardsList = abWindow.cardsPane.cardsList;
+ cardsHeader = abWindow.cardsPane.table.header;
+ sharedSplitter = abDocument.getElementById("sharedSplitter");
+
+ Assert.ok(
+ abDocument.body.classList.contains("layout-table"),
+ "table layout preserved on reopening"
+ );
+ Assert.equal(
+ sharedSplitter.resizeDirection,
+ "vertical",
+ "splitter direction preserved as vertical"
+ );
+ Assert.equal(
+ sharedSplitter.resizeElement.id,
+ "detailsPane",
+ "splitter preserved affecting the details pane"
+ );
+ Assert.equal(
+ cardsList.getAttribute("rows"),
+ "ab-table-card-row",
+ "table row implementation used"
+ );
+
+ checkColumns(
+ ["GeneratedName", "EmailAddresses", "PhoneNumbers", "Title"],
+ "EmailAddresses",
+ "descending"
+ );
+ checkNamesListed(
+ "contact three",
+ "contact two",
+ "contact four",
+ "contact one"
+ );
+ checkRowHeight(32);
+
+ // Reset layout to list.
+
+ await toggleLayout();
+
+ Assert.ok(
+ !abDocument.body.classList.contains("layout-table"),
+ "layout changed"
+ );
+ Assert.equal(
+ sharedSplitter.resizeDirection,
+ "horizontal",
+ "splitter direction is horizontal"
+ );
+ Assert.equal(
+ sharedSplitter.resizeElement.id,
+ "cardsPane",
+ "splitter is affecting the cards pane"
+ );
+ Assert.equal(
+ cardsList.getAttribute("rows"),
+ "ab-card-row",
+ "list row implementation used"
+ );
+
+ await closeAddressBookWindow();
+
+ Services.xulStore.removeDocument("about:addressbook");
+ Services.prefs.clearUserPref("mail.uidensity");
+ personalBook.deleteCards(personalBook.childCards);
+});
+
+add_task(async function test_placeholders() {
+ let writableBook = createAddressBook("Writable Book");
+ let readOnlyBook = createAddressBook("Read-Only Book");
+ readOnlyBook.setBoolValue("readOnly", true);
+
+ let abWindow = await openAddressBookWindow();
+ let placeholderCreateContact = abWindow.document.getElementById(
+ "placeholderCreateContact"
+ );
+
+ info("checking all address books");
+ await openAllAddressBooks();
+ checkPlaceholders(["placeholderEmptyBook", "placeholderCreateContact"]);
+
+ info("checking writable book");
+ await openDirectory(writableBook);
+ checkPlaceholders(["placeholderEmptyBook", "placeholderCreateContact"]);
+
+ let writableList = writableBook.addMailList(
+ createMailingList("Writable List")
+ );
+ checkPlaceholders();
+
+ info("checking writable list");
+ await openDirectory(writableList);
+ checkPlaceholders(["placeholderEmptyBook"]);
+
+ info("checking writable book");
+ await openDirectory(writableBook);
+ writableBook.deleteDirectory(writableList);
+ checkPlaceholders(["placeholderEmptyBook", "placeholderCreateContact"]);
+
+ info("checking read-only book");
+ await openDirectory(readOnlyBook);
+ checkPlaceholders(["placeholderEmptyBook"]);
+
+ // This wouldn't happen but we need to check the state in a read-only list.
+ readOnlyBook.setBoolValue("readOnly", false);
+ let readOnlyList = readOnlyBook.addMailList(
+ createMailingList("Read-Only List")
+ );
+ readOnlyBook.setBoolValue("readOnly", true);
+ checkPlaceholders();
+
+ info("checking read-only list");
+ await openDirectory(readOnlyList);
+ checkPlaceholders(["placeholderEmptyBook"]);
+
+ info("checking read-only book");
+ await openDirectory(readOnlyBook);
+ readOnlyBook.setBoolValue("readOnly", false);
+ readOnlyBook.deleteDirectory(readOnlyList);
+ readOnlyBook.setBoolValue("readOnly", true);
+ checkPlaceholders(["placeholderEmptyBook"]);
+
+ info("checking button opens a new contact to edit");
+ await openAllAddressBooks();
+ checkPlaceholders(["placeholderEmptyBook", "placeholderCreateContact"]);
+ EventUtils.synthesizeMouseAtCenter(placeholderCreateContact, {}, abWindow);
+
+ await TestUtils.waitForCondition(
+ () => abWindow.detailsPane.isEditing,
+ "entering editing mode"
+ );
+
+ await closeAddressBookWindow();
+ await promiseDirectoryRemoved(writableBook.URI);
+ await promiseDirectoryRemoved(readOnlyBook.URI);
+});
+
+/**
+ * Checks that mailling lists address books are shown in the table layout.
+ */
+add_task(async function test_list_table_layout() {
+ let book = createAddressBook("Book");
+ book.addCard(createContact("contact", "one"));
+ let list = createMailingList("list one");
+ book.addMailList(list);
+
+ let abWindow = await openAddressBookWindow();
+ let cardsList = abWindow.cardsPane.cardsList;
+ let cardsHeader = abWindow.cardsPane.table.header;
+
+ // Switch layout to table.
+
+ await toggleLayout();
+
+ await showPickerMenu("toggle", "addrbook");
+ await TestUtils.waitForCondition(
+ () => !cardsHeader.querySelector(`[id="addrbook"]`).hidden
+ );
+
+ // Check for the contact that the column is shown.
+ Assert.ok(
+ !cardsList.getRowAtIndex(0).querySelector(".addrbook-column").hidden,
+ "Address book column is shown."
+ );
+
+ Assert.ok(
+ cardsList
+ .getRowAtIndex(0)
+ .querySelector(".addrbook-column")
+ .textContent.includes("Book"),
+ "Address book column has the correct name for a contact."
+ );
+
+ Assert.ok(
+ cardsList
+ .getRowAtIndex(0)
+ .querySelector(".addrbook-column")
+ .textContent.includes("Book"),
+ "Address book column has the correct name for a list."
+ );
+
+ Services.xulStore.removeDocument("about:addressbook");
+ await closeAddressBookWindow();
+ await promiseDirectoryRemoved(book.URI);
+});
+
+/**
+ * Tests the option of showing the address book for All Address Book for the
+ * list view (vertical layout).
+ */
+add_task(async function test_list_all_address_book() {
+ let firstBook = createAddressBook("First Book");
+ let secondBook = createAddressBook("Second Book");
+ firstBook.addCard(createContact("contact", "one"));
+ secondBook.addCard(createContact("contact", "two"));
+ let list = createMailingList("list two");
+ secondBook.addMailList(list);
+
+ let abWindow = await openAddressBookWindow();
+ let cardsList = abWindow.cardsPane.cardsList;
+ let cardsHeader = abWindow.cardsPane.table.header;
+
+ info("Check that no address book suffix is present.");
+ Assert.ok(
+ !cardsList.getRowAtIndex(0).querySelector(".address-book-name"),
+ "No address book suffix is present."
+ );
+ Assert.ok(
+ !cardsList.getRowAtIndex(1).querySelector(".address-book-name"),
+ "No address book suffix is present."
+ );
+ Assert.ok(
+ !cardsList.getRowAtIndex(2).querySelector(".address-book-name"),
+ "No address book suffix is present."
+ );
+
+ info("Toggle the option to show address books.");
+ await showSortMenu("toggle", "addrbook");
+ await TestUtils.waitForCondition(
+ () => !cardsHeader.querySelector(`[id="addrbook"]`).hidden
+ );
+
+ Assert.ok(
+ cardsList
+ .getRowAtIndex(0)
+ .querySelector(".address-book-name")
+ .textContent.includes("First Book"),
+ "Address book suffix is present."
+ );
+ Assert.ok(
+ cardsList
+ .getRowAtIndex(1)
+ .querySelector(".address-book-name")
+ .textContent.includes("Second Book"),
+ "Address book suffix is present."
+ );
+ Assert.ok(
+ cardsList
+ .getRowAtIndex(2)
+ .querySelector(".address-book-name")
+ .textContent.includes("Second Book"),
+ "Address book suffix is present for a list."
+ );
+
+ info(`Select another address book and check that no address book suffix is
+ present for another book besides All Address Book`);
+ await openDirectory(secondBook);
+ Assert.ok(
+ !cardsList.getRowAtIndex(0).querySelector(".address-book-name"),
+ "Address book suffix is only present in All Address Book."
+ );
+
+ Services.xulStore.removeDocument("about:addressbook");
+ await closeAddressBookWindow();
+ await promiseDirectoryRemoved(firstBook.URI);
+ await promiseDirectoryRemoved(secondBook.URI);
+});
diff --git a/comm/mail/components/addrbook/test/browser/browser_directory_tree.js b/comm/mail/components/addrbook/test/browser/browser_directory_tree.js
new file mode 100644
index 0000000000..ee4b31ab7c
--- /dev/null
+++ b/comm/mail/components/addrbook/test/browser/browser_directory_tree.js
@@ -0,0 +1,982 @@
+/* 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/. */
+
+function rightClickOnIndex(index) {
+ let abWindow = getAddressBookWindow();
+ let booksList = abWindow.booksList;
+ let menu = abWindow.document.getElementById("bookContext");
+
+ let shownPromise = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(
+ booksList
+ .getRowAtIndex(index)
+ .querySelector(".bookRow-name, .listRow-name"),
+ { type: "contextmenu" },
+ abWindow
+ );
+ return shownPromise;
+}
+
+/**
+ * Tests that additions and removals are accurately displayed.
+ */
+add_task(async function test_additions_and_removals() {
+ function checkBooksOrder(...expected) {
+ function checkRow(index, { level, open, isList, text, uid }) {
+ info(`Row ${index}`);
+ let row = rows[index];
+
+ let containingList = row.closest("ul");
+ if (level == 1) {
+ Assert.equal(containingList.getAttribute("is"), "ab-tree-listbox");
+ } else if (level == 2) {
+ Assert.equal(containingList.parentNode.localName, "li");
+ containingList = containingList.parentNode.closest("ul");
+ Assert.equal(containingList.getAttribute("is"), "ab-tree-listbox");
+ }
+
+ let childList = row.querySelector("ul");
+ // NOTE: We're not explicitly handling open === false because no test
+ // needed it.
+ if (open) {
+ // Ancestor shouldn't have the collapsed class and the UL child list
+ // should be expanded and visible.
+ Assert.ok(!row.classList.contains("collapsed"));
+ Assert.greater(childList.clientHeight, 0);
+ } else if (childList) {
+ if (row.classList.contains("collapsed")) {
+ // If we have a UL child list and the ancestor element has a collapsed
+ // class, the child list shouldn't be visible.
+ Assert.equal(childList.clientHeight, 0);
+ } else if (childList.childNodes.length) {
+ // If the ancestor doesn't have the collapsed class, and the UL child
+ // list has at least one child node, the child list should be visible.
+ Assert.greater(childList.clientHeight, 0);
+ }
+ }
+
+ Assert.equal(row.classList.contains("listRow"), isList);
+ Assert.equal(row.querySelector("span").textContent, text);
+ Assert.equal(row.getAttribute("aria-label"), text);
+ Assert.equal(row.dataset.uid, uid);
+ }
+
+ let rows = abWindow.booksList.rows;
+ Assert.equal(rows.length, expected.length + 1);
+ for (let i = 0; i < expected.length; i++) {
+ let dir = expected[i].directory;
+ checkRow(i + 1, {
+ ...expected[i],
+ isList: dir.isMailList,
+ text: dir.dirName,
+ uid: dir.UID,
+ });
+ }
+ }
+
+ let abWindow = await openAddressBookWindow();
+
+ // Check the initial order.
+
+ checkDirectoryDisplayed(null);
+ checkBooksOrder(
+ { level: 1, directory: personalBook },
+ { level: 1, directory: historyBook }
+ );
+
+ // Add one book, *not* using the UI, and check that we don't move to it.
+
+ let newBook1 = createAddressBook("New Book 1");
+ checkDirectoryDisplayed(null);
+ checkBooksOrder(
+ { level: 1, directory: personalBook },
+ { level: 1, directory: newBook1 },
+ { level: 1, directory: historyBook }
+ );
+
+ // Add another book, using the UI, and check that we move to the new book.
+
+ let newBook2 = await createAddressBookWithUI("New Book 2");
+ checkDirectoryDisplayed(newBook2);
+ checkBooksOrder(
+ { level: 1, directory: personalBook },
+ { level: 1, directory: newBook1 },
+ { level: 1, directory: newBook2 },
+ { level: 1, directory: historyBook }
+ );
+
+ // Add some lists, *not* using the UI, and check that we don't move to them.
+
+ let list1 = newBook1.addMailList(createMailingList("New Book 1 - List 1"));
+ await new Promise(r => abWindow.setTimeout(r));
+ checkDirectoryDisplayed(newBook2);
+ checkBooksOrder(
+ { level: 1, directory: personalBook },
+ { level: 1, directory: newBook1, open: true },
+ { level: 2, directory: list1 },
+ { level: 1, directory: newBook2 },
+ { level: 1, directory: historyBook }
+ );
+
+ let list3 = newBook1.addMailList(createMailingList("New Book 1 - List 3"));
+ await new Promise(r => abWindow.setTimeout(r));
+ checkDirectoryDisplayed(newBook2);
+ checkBooksOrder(
+ { level: 1, directory: personalBook },
+ { level: 1, directory: newBook1, open: true },
+ { level: 2, directory: list1 },
+ { level: 2, directory: list3 },
+ { level: 1, directory: newBook2 },
+ { level: 1, directory: historyBook }
+ );
+
+ let list0 = newBook1.addMailList(createMailingList("New Book 1 - List 0"));
+ await new Promise(r => abWindow.setTimeout(r));
+ checkDirectoryDisplayed(newBook2);
+ checkBooksOrder(
+ { level: 1, directory: personalBook },
+ { level: 1, directory: newBook1, open: true },
+ { level: 2, directory: list0 },
+ { level: 2, directory: list1 },
+ { level: 2, directory: list3 },
+ { level: 1, directory: newBook2 },
+ { level: 1, directory: historyBook }
+ );
+
+ let list2 = newBook1.addMailList(createMailingList("New Book 1 - List 2"));
+ await new Promise(r => abWindow.setTimeout(r));
+ checkDirectoryDisplayed(newBook2);
+ checkBooksOrder(
+ { level: 1, directory: personalBook },
+ { level: 1, directory: newBook1, open: true },
+ { level: 2, directory: list0 },
+ { level: 2, directory: list1 },
+ { level: 2, directory: list2 },
+ { level: 2, directory: list3 },
+ { level: 1, directory: newBook2 },
+ { level: 1, directory: historyBook }
+ );
+
+ // Close the window and open it again. The tree should be as it was before.
+
+ await closeAddressBookWindow();
+ abWindow = await openAddressBookWindow();
+
+ checkDirectoryDisplayed(null);
+ checkBooksOrder(
+ { level: 1, directory: personalBook },
+ { level: 1, directory: newBook1, open: true },
+ { level: 2, directory: list0 },
+ { level: 2, directory: list1 },
+ { level: 2, directory: list2 },
+ { level: 2, directory: list3 },
+ { level: 1, directory: newBook2 },
+ { level: 1, directory: historyBook }
+ );
+
+ openDirectory(newBook2);
+
+ let list4 = newBook2.addMailList(createMailingList("New Book 2 - List 4"));
+ checkDirectoryDisplayed(newBook2);
+ checkBooksOrder(
+ { level: 1, directory: personalBook },
+ { level: 1, directory: newBook1, open: true },
+ { level: 2, directory: list0 },
+ { level: 2, directory: list1 },
+ { level: 2, directory: list2 },
+ { level: 2, directory: list3 },
+ { level: 1, directory: newBook2, open: true },
+ { level: 2, directory: list4 },
+ { level: 1, directory: historyBook }
+ );
+
+ // Add a new list, using the UI, and check that we move to it.
+
+ let list5 = await createMailingListWithUI(newBook2, "New Book 2 - List 5");
+ checkDirectoryDisplayed(list5);
+ checkBooksOrder(
+ { level: 1, directory: personalBook },
+ { level: 1, directory: newBook1, open: true },
+ { level: 2, directory: list0 },
+ { level: 2, directory: list1 },
+ { level: 2, directory: list2 },
+ { level: 2, directory: list3 },
+ { level: 1, directory: newBook2, open: true },
+ { level: 2, directory: list4 },
+ { level: 2, directory: list5 },
+ { level: 1, directory: historyBook }
+ );
+
+ let list6 = await createMailingListWithUI(newBook2, "New Book 2 - List 6");
+ checkDirectoryDisplayed(list6);
+ checkBooksOrder(
+ { level: 1, directory: personalBook },
+ { level: 1, directory: newBook1, open: true },
+ { level: 2, directory: list0 },
+ { level: 2, directory: list1 },
+ { level: 2, directory: list2 },
+ { level: 2, directory: list3 },
+ { level: 1, directory: newBook2, open: true },
+ { level: 2, directory: list4 },
+ { level: 2, directory: list5 },
+ { level: 2, directory: list6 },
+ { level: 1, directory: historyBook }
+ );
+ // Delete a list that isn't displayed, and check that we don't move.
+
+ newBook1.deleteDirectory(list3);
+ await new Promise(r => abWindow.setTimeout(r));
+ checkDirectoryDisplayed(list6);
+ checkBooksOrder(
+ { level: 1, directory: personalBook },
+ { level: 1, directory: newBook1, open: true },
+ { level: 2, directory: list0 },
+ { level: 2, directory: list1 },
+ { level: 2, directory: list2 },
+ { level: 1, directory: newBook2, open: true },
+ { level: 2, directory: list4 },
+ { level: 2, directory: list5 },
+ { level: 2, directory: list6 },
+ { level: 1, directory: historyBook }
+ );
+
+ // Select list5
+ let list5Row = abWindow.booksList.getRowForUID(list5.UID);
+ EventUtils.synthesizeMouseAtCenter(
+ list5Row.querySelector("span"),
+ {},
+ abWindow
+ );
+ checkDirectoryDisplayed(list5);
+
+ // Delete the displayed list, and check that we move to the next list under
+ // the same book.
+
+ newBook2.deleteDirectory(list5);
+ await new Promise(r => abWindow.setTimeout(r));
+ checkDirectoryDisplayed(list6);
+ checkBooksOrder(
+ { level: 1, directory: personalBook },
+ { level: 1, directory: newBook1, open: true },
+ { level: 2, directory: list0 },
+ { level: 2, directory: list1 },
+ { level: 2, directory: list2 },
+ { level: 1, directory: newBook2, open: true },
+ { level: 2, directory: list4 },
+ { level: 2, directory: list6 },
+ { level: 1, directory: historyBook }
+ );
+
+ // Delete the last list, and check we move to the previous list under the same
+ // book.
+ newBook2.deleteDirectory(list6);
+ await new Promise(r => abWindow.setTimeout(r));
+ checkDirectoryDisplayed(list4);
+ checkBooksOrder(
+ { level: 1, directory: personalBook },
+ { level: 1, directory: newBook1, open: true },
+ { level: 2, directory: list0 },
+ { level: 2, directory: list1 },
+ { level: 2, directory: list2 },
+ { level: 1, directory: newBook2, open: true },
+ { level: 2, directory: list4 },
+ { level: 1, directory: historyBook }
+ );
+
+ // Delete the displayed book, and check that we move to the next book.
+
+ await promiseDirectoryRemoved(newBook2.URI);
+ await new Promise(r => abWindow.setTimeout(r));
+ checkDirectoryDisplayed(historyBook);
+ checkBooksOrder(
+ { level: 1, directory: personalBook },
+ { level: 1, directory: newBook1, open: true },
+ { level: 2, directory: list0 },
+ { level: 2, directory: list1 },
+ { level: 2, directory: list2 },
+ { level: 1, directory: historyBook }
+ );
+
+ // Select a list in the first book, then delete the book. Check that we
+ // move to the next book.
+
+ openDirectory(list1);
+ await promiseDirectoryRemoved(newBook1.URI);
+ await new Promise(r => abWindow.setTimeout(r));
+ checkDirectoryDisplayed(historyBook);
+ checkBooksOrder(
+ { level: 1, directory: personalBook },
+ { level: 1, directory: historyBook }
+ );
+
+ await closeAddressBookWindow();
+});
+
+/**
+ * Tests that renaming or deleting books or lists is reflected in the UI.
+ */
+add_task(async function test_rename_and_delete() {
+ let abWindow = await openAddressBookWindow();
+
+ let abDocument = abWindow.document;
+ let booksList = abWindow.booksList;
+ let searchInput = abWindow.searchInput;
+ Assert.equal(booksList.rowCount, 3);
+
+ // Create a book.
+
+ EventUtils.synthesizeMouseAtCenter(booksList, {}, abWindow);
+ let newBook = await createAddressBookWithUI("New Book");
+ Assert.equal(booksList.rowCount, 4);
+ Assert.equal(booksList.getIndexForUID(newBook.UID), 2);
+ Assert.equal(booksList.selectedIndex, 2);
+ Assert.equal(abDocument.activeElement, booksList);
+
+ let bookRow = booksList.getRowAtIndex(2);
+ Assert.equal(bookRow.querySelector(".bookRow-name").textContent, "New Book");
+ Assert.equal(bookRow.getAttribute("aria-label"), "New Book");
+
+ await TestUtils.waitForCondition(
+ () => searchInput.placeholder == "Search New Book",
+ "search placeholder updated"
+ );
+
+ // Rename the book.
+
+ let menu = abDocument.getElementById("bookContext");
+ let propertiesMenuItem = abDocument.getElementById("bookContextProperties");
+
+ await rightClickOnIndex(2);
+
+ Assert.ok(BrowserTestUtils.is_visible(propertiesMenuItem));
+ Assert.ok(!propertiesMenuItem.disabled);
+ Assert.deepEqual(document.l10n.getAttributes(propertiesMenuItem), {
+ id: "about-addressbook-books-context-properties",
+ args: null,
+ });
+
+ let dialogPromise = promiseLoadSubDialog(
+ "chrome://messenger/content/addressbook/abAddressBookNameDialog.xhtml"
+ ).then(async function (dialogWindow) {
+ let dialogDocument = dialogWindow.document;
+
+ let nameInput = dialogDocument.getElementById("name");
+ Assert.equal(nameInput.value, "New Book");
+ nameInput.value = "Old Book";
+
+ dialogDocument.querySelector("dialog").getButton("accept").click();
+ });
+ menu.activateItem(propertiesMenuItem);
+ await dialogPromise;
+
+ Assert.equal(booksList.rowCount, 4);
+ Assert.equal(booksList.getIndexForUID(newBook.UID), 2);
+ Assert.equal(booksList.selectedIndex, 2);
+ Assert.equal(abDocument.activeElement, booksList);
+
+ bookRow = booksList.getRowAtIndex(2);
+ Assert.equal(bookRow.querySelector(".bookRow-name").textContent, "Old Book");
+ Assert.equal(bookRow.getAttribute("aria-label"), "Old Book");
+
+ await TestUtils.waitForCondition(
+ () => searchInput.placeholder == "Search Old Book",
+ "search placeholder updated"
+ );
+
+ // Create a list.
+
+ let newList = await createMailingListWithUI(newBook, "New List");
+ Assert.equal(booksList.rowCount, 5);
+ Assert.equal(booksList.getIndexForUID(newList.UID), 3);
+ Assert.equal(booksList.selectedIndex, 3);
+ Assert.equal(abDocument.activeElement, booksList);
+
+ let listRow = booksList.getRowAtIndex(3);
+ Assert.equal(
+ listRow.compareDocumentPosition(bookRow),
+ Node.DOCUMENT_POSITION_CONTAINS | Node.DOCUMENT_POSITION_PRECEDING
+ );
+ Assert.equal(listRow.querySelector(".listRow-name").textContent, "New List");
+ Assert.equal(listRow.getAttribute("aria-label"), "New List");
+
+ await TestUtils.waitForCondition(
+ () => searchInput.placeholder == "Search New List",
+ "search placeholder updated"
+ );
+
+ // Rename the list.
+
+ await rightClickOnIndex(3);
+
+ Assert.ok(BrowserTestUtils.is_visible(propertiesMenuItem));
+ Assert.deepEqual(document.l10n.getAttributes(propertiesMenuItem), {
+ id: "about-addressbook-books-context-edit-list",
+ args: null,
+ });
+
+ dialogPromise = promiseLoadSubDialog(
+ "chrome://messenger/content/addressbook/abEditListDialog.xhtml"
+ ).then(async function (dialogWindow) {
+ let dialogDocument = dialogWindow.document;
+
+ let nameInput = dialogDocument.getElementById("ListName");
+ Assert.equal(nameInput.value, "New List");
+ nameInput.value = "Old List";
+
+ dialogDocument.querySelector("dialog").getButton("accept").click();
+ });
+ menu.activateItem(propertiesMenuItem);
+ await dialogPromise;
+
+ Assert.equal(booksList.rowCount, 5);
+ Assert.equal(booksList.getIndexForUID(newList.UID), 3);
+ Assert.equal(booksList.selectedIndex, 3);
+ Assert.equal(abDocument.activeElement, booksList);
+
+ listRow = booksList.getRowAtIndex(3);
+ Assert.equal(listRow.querySelector(".listRow-name").textContent, "Old List");
+ Assert.equal(listRow.getAttribute("aria-label"), "Old List");
+
+ await TestUtils.waitForCondition(
+ () => searchInput.placeholder == "Search Old List",
+ "search placeholder updated"
+ );
+
+ // Delete the list.
+
+ let promptPromise = BrowserTestUtils.promiseAlertDialog("accept");
+ let selectPromise = BrowserTestUtils.waitForEvent(booksList, "select");
+ EventUtils.synthesizeKey("KEY_Delete", {}, abWindow);
+ await promptPromise;
+ await selectPromise;
+ Assert.equal(newBook.childNodes.length, 0, "list was actually deleted");
+ await new Promise(r => abWindow.setTimeout(r));
+
+ Assert.equal(booksList.rowCount, 4);
+ Assert.equal(booksList.getIndexForUID(newBook.UID), 2);
+ Assert.equal(booksList.getIndexForUID(newList.UID), -1);
+ // Moves to parent when last list is deleted.
+ Assert.equal(booksList.selectedIndex, 2);
+ Assert.equal(abDocument.activeElement, booksList);
+
+ bookRow = booksList.getRowAtIndex(2);
+ Assert.ok(!bookRow.classList.contains("children"));
+ Assert.ok(!bookRow.querySelector("ul, li"));
+
+ // Delete the book.
+
+ promptPromise = BrowserTestUtils.promiseAlertDialog("accept");
+ selectPromise = BrowserTestUtils.waitForEvent(booksList, "select");
+ EventUtils.synthesizeKey("KEY_Delete", {}, abWindow);
+ await promptPromise;
+ await selectPromise;
+ Assert.equal(
+ MailServices.ab.directories.length,
+ 2,
+ "book was actually deleted"
+ );
+
+ Assert.equal(booksList.rowCount, 3);
+ Assert.equal(booksList.getIndexForUID(newBook.UID), -1);
+ Assert.equal(booksList.selectedIndex, 2);
+ Assert.equal(abDocument.activeElement, booksList);
+
+ // Attempt to delete the All Address Books entry.
+ // Synthesizing the delete key here does not throw immediately.
+
+ booksList.selectedIndex = 0;
+ await Assert.rejects(
+ booksList.deleteSelected(),
+ /Cannot delete the All Address Books item/,
+ "Attempting to delete All Address Books should fail."
+ );
+
+ // Attempt to delete Personal Address Book.
+ // Synthesizing the delete key here does not throw immediately.
+
+ booksList.selectedIndex = 1;
+ await Assert.rejects(
+ booksList.deleteSelected(),
+ /Refusing to delete a built-in address book/,
+ "Attempting to delete Personal Address Book should fail."
+ );
+
+ // Attempt to delete Collected Addresses.
+ // Synthesizing the delete key here does not throw immediately.
+
+ booksList.selectedIndex = 2;
+ await Assert.rejects(
+ booksList.deleteSelected(),
+ /Refusing to delete a built-in address book/,
+ "Attempting to delete Collected Addresses should fail."
+ );
+
+ await closeAddressBookWindow();
+});
+
+/**
+ * Tests the context menu of the list.
+ */
+add_task(async function test_context_menu() {
+ let book = createAddressBook("Ordinary Book");
+ book.addMailList(createMailingList("Ordinary List"));
+ createAddressBook("CardDAV Book", Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE);
+
+ let abWindow = await openAddressBookWindow();
+ let abDocument = abWindow.document;
+ let booksList = abWindow.booksList;
+
+ let menu = abWindow.document.getElementById("bookContext");
+ let propertiesMenuItem = abDocument.getElementById("bookContextProperties");
+ let synchronizeMenuItem = abDocument.getElementById("bookContextSynchronize");
+ let printMenuItem = abDocument.getElementById("bookContextPrint");
+ let deleteMenuItem = abDocument.getElementById("bookContextDelete");
+ let removeMenuItem = abDocument.getElementById("bookContextRemove");
+ let startupDefaultItem = abDocument.getElementById(
+ "bookContextStartupDefault"
+ );
+
+ Assert.equal(booksList.rowCount, 6);
+
+ // Test that the menu does not show for All Address Books.
+
+ await rightClickOnIndex(0);
+ Assert.equal(booksList.selectedIndex, 0);
+ Assert.equal(abDocument.activeElement, booksList);
+
+ let visibleItems = [...menu.children].filter(BrowserTestUtils.is_visible);
+ Assert.equal(visibleItems.length, 1);
+ Assert.equal(
+ visibleItems[0],
+ startupDefaultItem,
+ "only the startup default item should be visible"
+ );
+ let hiddenPromise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ menu.hidePopup();
+
+ // Test directories that can't be deleted.
+
+ for (let index of [1, booksList.rowCount - 1]) {
+ await rightClickOnIndex(index);
+ Assert.equal(booksList.selectedIndex, index);
+ Assert.ok(BrowserTestUtils.is_visible(propertiesMenuItem));
+ Assert.ok(!propertiesMenuItem.disabled);
+ Assert.ok(!BrowserTestUtils.is_visible(synchronizeMenuItem));
+ Assert.ok(BrowserTestUtils.is_visible(printMenuItem));
+ Assert.ok(!printMenuItem.disabled);
+ Assert.ok(BrowserTestUtils.is_visible(deleteMenuItem));
+ Assert.ok(deleteMenuItem.disabled);
+ Assert.ok(!BrowserTestUtils.is_visible(removeMenuItem));
+ hiddenPromise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ menu.hidePopup();
+ await hiddenPromise;
+ Assert.equal(abDocument.activeElement, booksList);
+ }
+
+ // Test and delete CardDAV directory at index 4.
+
+ await rightClickOnIndex(4);
+ Assert.equal(booksList.selectedIndex, 4);
+ Assert.ok(BrowserTestUtils.is_visible(propertiesMenuItem));
+ Assert.ok(!propertiesMenuItem.disabled);
+ Assert.ok(BrowserTestUtils.is_visible(synchronizeMenuItem));
+ Assert.ok(!synchronizeMenuItem.disabled);
+ Assert.ok(BrowserTestUtils.is_visible(printMenuItem));
+ Assert.ok(!printMenuItem.disabled);
+ Assert.ok(!BrowserTestUtils.is_visible(deleteMenuItem));
+ Assert.ok(BrowserTestUtils.is_visible(removeMenuItem));
+ Assert.ok(!removeMenuItem.disabled);
+ let promptPromise = BrowserTestUtils.promiseAlertDialog("accept");
+ let selectPromise = BrowserTestUtils.waitForEvent(booksList, "select");
+ hiddenPromise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ menu.activateItem(removeMenuItem);
+ await promptPromise;
+ await selectPromise;
+ await hiddenPromise;
+ Assert.equal(abDocument.activeElement, booksList);
+
+ Assert.equal(booksList.rowCount, 5);
+ Assert.equal(booksList.selectedIndex, 4);
+ Assert.equal(menu.state, "closed");
+
+ // Test and delete list at index 3, then directory at index 2.
+
+ for (let index of [3, 2]) {
+ await new Promise(r => abWindow.setTimeout(r, 250));
+ await rightClickOnIndex(index);
+ Assert.equal(booksList.selectedIndex, index);
+ Assert.ok(BrowserTestUtils.is_visible(propertiesMenuItem));
+ Assert.ok(!propertiesMenuItem.disabled);
+ Assert.ok(!BrowserTestUtils.is_visible(synchronizeMenuItem));
+ Assert.ok(BrowserTestUtils.is_visible(printMenuItem));
+ Assert.ok(!printMenuItem.disabled);
+ Assert.ok(BrowserTestUtils.is_visible(deleteMenuItem));
+ Assert.ok(!deleteMenuItem.disabled);
+ Assert.ok(!BrowserTestUtils.is_visible(removeMenuItem));
+ promptPromise = BrowserTestUtils.promiseAlertDialog("accept");
+ selectPromise = BrowserTestUtils.waitForEvent(booksList, "select");
+ hiddenPromise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ menu.activateItem(deleteMenuItem);
+ await promptPromise;
+ await selectPromise;
+ await hiddenPromise;
+ Assert.equal(abDocument.activeElement, booksList);
+
+ if (index == 3) {
+ Assert.equal(booksList.rowCount, 4);
+ // Moves to parent when last list is deleted.
+ Assert.equal(booksList.selectedIndex, 2);
+ } else {
+ Assert.equal(booksList.rowCount, 3);
+ Assert.equal(booksList.selectedIndex, 2);
+ }
+ Assert.equal(menu.state, "closed");
+ }
+
+ // Test that the menu does not show beyond the last book.
+
+ EventUtils.synthesizeMouseAtCenter(
+ booksList,
+ 100,
+ booksList.clientHeight - 10,
+ { type: "contextmenu" },
+ abWindow
+ );
+ Assert.equal(booksList.selectedIndex, 2);
+ await new Promise(r => abWindow.setTimeout(r, 500));
+ Assert.equal(menu.state, "closed", "menu stayed closed as expected");
+ Assert.equal(abDocument.activeElement, booksList);
+
+ await closeAddressBookWindow();
+});
+
+/**
+ * Tests the menu button on each item.
+ */
+add_task(async function test_context_menu_button() {
+ let book = createAddressBook("Ordinary Book");
+ book.addMailList(createMailingList("Ordinary List"));
+
+ let abWindow = await openAddressBookWindow();
+ let booksList = abWindow.booksList;
+ let menu = abWindow.document.getElementById("bookContext");
+
+ for (let row of booksList.rows) {
+ info(row.querySelector(".bookRow-name, .listRow-name").textContent);
+ let button = row.querySelector(".bookRow-menu, .listRow-menu");
+ Assert.ok(BrowserTestUtils.is_hidden(button), "menu button is hidden");
+
+ EventUtils.synthesizeMouse(row, 100, 5, { type: "mousemove" }, abWindow);
+ Assert.ok(BrowserTestUtils.is_visible(button), "menu button is visible");
+
+ let shownPromise = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(button, {}, abWindow);
+ await shownPromise;
+
+ let buttonRect = button.getBoundingClientRect();
+ let menuRect = menu.getBoundingClientRect();
+ Assert.less(
+ Math.abs(menuRect.top - buttonRect.bottom),
+ 13,
+ "menu appeared near the button vertically"
+ );
+ Assert.less(
+ Math.abs(menuRect.left - buttonRect.left),
+ 20,
+ "menu appeared near the button horizontally"
+ );
+
+ let hiddenPromise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ menu.hidePopup();
+ await hiddenPromise;
+ }
+
+ await closeAddressBookWindow();
+
+ await promiseDirectoryRemoved(book.URI);
+});
+
+/**
+ * Tests that the collapsed state of books survives a reload of the page.
+ */
+add_task(async function test_collapse_expand() {
+ Services.xulStore.removeDocument("about:addressbook");
+
+ personalBook.addMailList(createMailingList("Personal List 1"));
+ personalBook.addMailList(createMailingList("Personal List 2"));
+
+ historyBook.addMailList(createMailingList("History List 1"));
+
+ let book1 = createAddressBook("Book 1");
+ book1.addMailList(createMailingList("Book 1 List 1"));
+ book1.addMailList(createMailingList("Book 1 List 2"));
+
+ let book2 = createAddressBook("Book 2");
+ book2.addMailList(createMailingList("Book 2 List 1"));
+ book2.addMailList(createMailingList("Book 2 List 2"));
+ book2.addMailList(createMailingList("Book 2 List 3"));
+
+ function getRowForBook(book) {
+ return abDocument.getElementById(`book-${book.UID}`);
+ }
+
+ function checkCollapsedState(book, expectedCollapsed) {
+ Assert.equal(
+ getRowForBook(book).classList.contains("collapsed"),
+ expectedCollapsed,
+ `${book.dirName} is ${expectedCollapsed ? "collapsed" : "expanded"}`
+ );
+ }
+
+ function toggleCollapsedState(book) {
+ let twisty = getRowForBook(book).querySelector(".twisty");
+ Assert.ok(
+ BrowserTestUtils.is_visible(twisty),
+ `twisty for ${book.dirName} is visible`
+ );
+ EventUtils.synthesizeMouseAtCenter(twisty, {}, abWindow);
+ }
+
+ let abWindow = await openAddressBookWindow();
+ let abDocument = abWindow.document;
+
+ checkCollapsedState(personalBook, false);
+ checkCollapsedState(book1, false);
+ checkCollapsedState(book2, false);
+ checkCollapsedState(historyBook, false);
+
+ toggleCollapsedState(personalBook);
+ toggleCollapsedState(book1);
+
+ info("Closing and re-opening");
+ await closeAddressBookWindow();
+ abWindow = await openAddressBookWindow();
+ abDocument = abWindow.document;
+
+ checkCollapsedState(personalBook, true);
+ checkCollapsedState(book1, true);
+ checkCollapsedState(book2, false);
+ checkCollapsedState(historyBook, false);
+
+ toggleCollapsedState(book1);
+ toggleCollapsedState(book2);
+ toggleCollapsedState(historyBook);
+
+ info("Closing and re-opening");
+ await closeAddressBookWindow();
+ abWindow = await openAddressBookWindow();
+ abDocument = abWindow.document;
+
+ checkCollapsedState(personalBook, true);
+ checkCollapsedState(book1, false);
+ checkCollapsedState(book2, true);
+ checkCollapsedState(historyBook, true);
+
+ toggleCollapsedState(personalBook);
+
+ info("Closing and re-opening");
+ await closeAddressBookWindow();
+ await promiseDirectoryRemoved(book2.URI);
+ abWindow = await openAddressBookWindow();
+ abDocument = abWindow.document;
+
+ checkCollapsedState(personalBook, false);
+ checkCollapsedState(book1, false);
+ checkCollapsedState(historyBook, true);
+
+ await closeAddressBookWindow();
+
+ personalBook.childNodes.forEach(list => personalBook.deleteDirectory(list));
+ historyBook.childNodes.forEach(list => historyBook.deleteDirectory(list));
+ await promiseDirectoryRemoved(book1.URI);
+ Services.xulStore.removeDocument("about:addressbook");
+});
+
+/**
+ * Tests that the chosen default directory (or lack thereof) is opened when
+ * the page opens.
+ */
+add_task(async function test_startup_directory() {
+ const URI_PREF = "mail.addr_book.view.startupURI";
+ const DEFAULT_PREF = "mail.addr_book.view.startupURIisDefault";
+
+ Services.prefs.clearUserPref(URI_PREF);
+ Services.prefs.clearUserPref(DEFAULT_PREF);
+
+ async function checkMenuItem(index, expectChecked, toggle = false) {
+ await rightClickOnIndex(index);
+
+ let menu = abWindow.document.getElementById("bookContext");
+ let item = abWindow.document.getElementById("bookContextStartupDefault");
+ Assert.equal(
+ item.hasAttribute("checked"),
+ expectChecked,
+ `directory at index ${index} is the default?`
+ );
+
+ let hiddenPromise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ if (toggle) {
+ menu.activateItem(item);
+ } else {
+ menu.hidePopup();
+ }
+ await hiddenPromise;
+ }
+
+ // With the defaults, All Address Books should open.
+ // No changes should be made to the prefs.
+
+ let abWindow = await openAddressBookWindow();
+ checkDirectoryDisplayed();
+ await checkMenuItem(0, true);
+ await checkMenuItem(1, false);
+ await checkMenuItem(2, false);
+ openDirectory(personalBook);
+ await closeAddressBookWindow();
+ Assert.ok(!Services.prefs.prefHasUserValue(URI_PREF));
+
+ // Now we'll set the default to "last-used".
+ // The last-used book should be saved.
+
+ abWindow = await openAddressBookWindow();
+ checkDirectoryDisplayed();
+ await checkMenuItem(0, true);
+ await checkMenuItem(1, false);
+ await checkMenuItem(2, false);
+ Services.prefs.setBoolPref(DEFAULT_PREF, false);
+ openDirectory(personalBook);
+ await closeAddressBookWindow();
+ Assert.equal(Services.prefs.getStringPref(URI_PREF), personalBook.URI);
+
+ // The last-used book should open.
+
+ abWindow = await openAddressBookWindow();
+ checkDirectoryDisplayed(personalBook);
+ await checkMenuItem(0, false);
+ await checkMenuItem(1, false);
+ await checkMenuItem(2, false);
+ openDirectory(historyBook);
+ await closeAddressBookWindow();
+ Assert.equal(Services.prefs.getStringPref(URI_PREF), historyBook.URI);
+
+ // The last-used book should open.
+ // We'll set a default directory again.
+
+ abWindow = await openAddressBookWindow();
+ checkDirectoryDisplayed(historyBook);
+ await checkMenuItem(0, false);
+ await checkMenuItem(1, false);
+ await checkMenuItem(2, false, true);
+ openDirectory(personalBook);
+ await closeAddressBookWindow();
+ Assert.ok(Services.prefs.getBoolPref(DEFAULT_PREF));
+ Assert.equal(Services.prefs.getStringPref(URI_PREF), historyBook.URI);
+
+ // Check that the saved default opens. Change the default.
+
+ abWindow = await openAddressBookWindow();
+ checkDirectoryDisplayed(historyBook);
+ await checkMenuItem(0, false);
+ await checkMenuItem(2, true);
+ await checkMenuItem(1, false, true);
+ await closeAddressBookWindow();
+ Assert.ok(Services.prefs.getBoolPref(DEFAULT_PREF));
+ Assert.equal(Services.prefs.getStringPref(URI_PREF), personalBook.URI);
+
+ // Check that the saved default opens. Change the default to All Address Books.
+
+ abWindow = await openAddressBookWindow();
+ checkDirectoryDisplayed(personalBook);
+ await checkMenuItem(1, true);
+ await checkMenuItem(2, false);
+ await checkMenuItem(0, false, true);
+ await closeAddressBookWindow();
+ Assert.ok(Services.prefs.getBoolPref(DEFAULT_PREF));
+ Assert.ok(!Services.prefs.prefHasUserValue(URI_PREF));
+
+ // Check that the saved default opens. Clear the default.
+
+ abWindow = await openAddressBookWindow();
+ checkDirectoryDisplayed();
+ await checkMenuItem(1, false);
+ await checkMenuItem(2, false);
+ await checkMenuItem(0, true, true);
+ await closeAddressBookWindow();
+ Assert.ok(!Services.prefs.getBoolPref(DEFAULT_PREF));
+ Assert.ok(!Services.prefs.prefHasUserValue(URI_PREF));
+});
+
+add_task(async function test_total_address_book_count() {
+ let book1 = createAddressBook("First Book");
+ let book2 = createAddressBook("Second Book");
+ book1.addMailList(createMailingList("Ordinary List"));
+
+ book1.addCard(createContact("contact1", "book 1"));
+ book1.addCard(createContact("contact2", "book 1"));
+ book1.addCard(createContact("contact3", "book 1"));
+
+ book2.addCard(createContact("contact1", "book 2"));
+
+ let abWindow = await openAddressBookWindow();
+ let abDocument = abWindow.document;
+ let booksList = abWindow.booksList;
+ let cardCount = abDocument.getElementById("cardCount");
+
+ await openAllAddressBooks();
+ Assert.deepEqual(abDocument.l10n.getAttributes(cardCount), {
+ id: "about-addressbook-card-count-all",
+ args: {
+ count: 5,
+ },
+ });
+
+ for (let [index, [name, count]] of [
+ ["Personal Address Book", 0],
+ ["First Book", 4],
+ ["Ordinary List", 0],
+ ["Second Book", 1],
+ ].entries()) {
+ booksList.getRowAtIndex(index + 1).click();
+ Assert.deepEqual(abDocument.l10n.getAttributes(cardCount), {
+ id: "about-addressbook-card-count",
+ args: { name, count },
+ });
+ }
+
+ // Create a contact and check that the count updates.
+ // Select second book.
+ booksList.getRowAtIndex(4).click();
+ let createdPromise = TestUtils.topicObserved("addrbook-contact-created");
+ book2.addCard(createContact("contact2", "book 2"));
+ await createdPromise;
+ Assert.deepEqual(
+ abDocument.l10n.getAttributes(cardCount),
+ {
+ id: "about-addressbook-card-count",
+ args: { name: "Second Book", count: 2 },
+ },
+ "Address Book count is updated on contact creation."
+ );
+
+ // Delete a contact an check that the count updates.
+ let promptPromise = BrowserTestUtils.promiseAlertDialog("accept");
+ let deletedPromise = TestUtils.topicObserved("addrbook-contact-deleted");
+ let cards = abWindow.cardsPane.cardsList;
+ EventUtils.synthesizeMouseAtCenter(cards.getRowAtIndex(0), {}, abWindow);
+ EventUtils.synthesizeKey("VK_DELETE", {}, abWindow);
+ await promptPromise;
+ await deletedPromise;
+ Assert.deepEqual(
+ abDocument.l10n.getAttributes(cardCount),
+ {
+ id: "about-addressbook-card-count",
+ args: { name: "Second Book", count: 1 },
+ },
+ "Address Book count is updated on contact deletion."
+ );
+
+ await closeAddressBookWindow();
+ await promiseDirectoryRemoved(book1.URI);
+ await promiseDirectoryRemoved(book2.URI);
+});
diff --git a/comm/mail/components/addrbook/test/browser/browser_display_card.js b/comm/mail/components/addrbook/test/browser/browser_display_card.js
new file mode 100644
index 0000000000..4d468ed646
--- /dev/null
+++ b/comm/mail/components/addrbook/test/browser/browser_display_card.js
@@ -0,0 +1,1020 @@
+/* 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);
+
+var { CalendarTestUtils } = ChromeUtils.import(
+ "resource://testing-common/calendar/CalendarTestUtils.jsm"
+);
+var { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
+var { VCardUtils } = ChromeUtils.import("resource:///modules/VCardUtils.jsm");
+var { AddrBookCard } = ChromeUtils.import(
+ "resource:///modules/AddrBookCard.jsm"
+);
+
+/** @implements {nsIExternalProtocolService} */
+let mockExternalProtocolService = {
+ _loadedURLs: [],
+ externalProtocolHandlerExists(aProtocolScheme) {},
+ getApplicationDescription(aScheme) {},
+ getProtocolHandlerInfo(aProtocolScheme) {},
+ getProtocolHandlerInfoFromOS(aProtocolScheme, aFound) {},
+ isExposedProtocol(aProtocolScheme) {},
+ loadURI(aURI, aWindowContext) {
+ this._loadedURLs.push(aURI.spec);
+ },
+ setProtocolHandlerDefaults(aHandlerInfo, aOSHandlerExists) {},
+ urlLoaded(aURL) {
+ return this._loadedURLs.includes(aURL);
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIExternalProtocolService"]),
+};
+
+add_setup(async function () {
+ // Card 0.
+ personalBook.addCard(
+ VCardUtils.vCardToAbCard("BEGIN:VCARD\r\nEND:VCARD\r\n")
+ );
+ // Card 1.
+ personalBook.addCard(
+ VCardUtils.vCardToAbCard(formatVCard`
+ BEGIN:VCARD
+ FN:basic person
+ EMAIL:basic@invalid
+ END:VCARD
+ `)
+ );
+ // Card 2.
+ personalBook.addCard(
+ VCardUtils.vCardToAbCard(formatVCard`
+ BEGIN:VCARD
+ FN:complex person
+ EMAIL:secondary@invalid
+ EMAIL;PREF=1:primary@invalid
+ EMAIL;TYPE=WORK:tertiary@invalid
+ TEL;VALUE=URI:tel:000-0000
+ TEL;TYPE=WORK,VOICE:callto:111-1111
+ TEL;TYPE=VOICE,WORK:222-2222
+ TEL;TYPE=HOME;TYPE=VIDEO:tel:333-3333
+ ADR:;;street,suburb;city;state;zip;country
+ ANNIVERSARY:2018-06-11
+ BDAY;VALUE=DATE:--0229
+ NOTE:mary had a little lamb\\nits fleece was white as snow\\nand everywhere t
+ hat mary went\\nthe lamb was sure to go
+ ORG:thunderbird;engineering
+ ROLE:sheriff
+ TITLE:senior engineering lead
+ TZ;VALUE=TEXT:Pacific/Auckland
+ URL;TYPE=work:https://www.thunderbird.net/
+ IMPP:xmpp:cowboy@example.org
+ END:VCARD
+ `)
+ );
+
+ MailServices.accounts.createLocalMailAccount();
+ let account = MailServices.accounts.accounts[0];
+ account.addIdentity(MailServices.accounts.createIdentity());
+
+ let calendar = CalendarTestUtils.createCalendar();
+
+ let mockExternalProtocolServiceCID = MockRegistrar.register(
+ "@mozilla.org/uriloader/external-protocol-service;1",
+ mockExternalProtocolService
+ );
+
+ registerCleanupFunction(async () => {
+ personalBook.deleteCards(personalBook.childCards);
+ MailServices.accounts.removeAccount(account, true);
+ CalendarTestUtils.removeCalendar(calendar);
+ MockRegistrar.unregister(mockExternalProtocolServiceCID);
+ });
+});
+
+/**
+ * Checks basic display.
+ */
+add_task(async function testDisplay() {
+ let abWindow = await openAddressBookWindow();
+ openDirectory(personalBook);
+
+ let abDocument = abWindow.document;
+ let cardsList = abDocument.getElementById("cards");
+ let detailsPane = abDocument.getElementById("detailsPane");
+
+ let viewContactName = abDocument.getElementById("viewContactName");
+ let viewPrimaryEmail = abDocument.getElementById("viewPrimaryEmail");
+ let editButton = abDocument.getElementById("editButton");
+
+ let emailAddressesSection = abDocument.getElementById("emailAddresses");
+ let phoneNumbersSection = abDocument.getElementById("phoneNumbers");
+ let addressesSection = abDocument.getElementById("addresses");
+ let notesSection = abDocument.getElementById("notes");
+ let websitesSection = abDocument.getElementById("websites");
+ let imppSection = abDocument.getElementById("instantMessaging");
+ let otherInfoSection = abDocument.getElementById("otherInfo");
+ let selectedCardsSection = abDocument.getElementById("selectedCards");
+
+ Assert.equal(cardsList.view.rowCount, personalBook.childCardCount);
+ Assert.ok(detailsPane.hidden);
+
+ // Card 0: an empty card.
+
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(0), {}, abWindow);
+ await TestUtils.waitForCondition(() =>
+ BrowserTestUtils.is_visible(detailsPane)
+ );
+
+ // Header.
+ Assert.equal(viewContactName.textContent, "");
+ Assert.equal(viewPrimaryEmail.textContent, "");
+
+ // Action buttons.
+ await checkActionButtons();
+ Assert.ok(BrowserTestUtils.is_visible(editButton));
+
+ Assert.ok(BrowserTestUtils.is_hidden(emailAddressesSection));
+ Assert.ok(BrowserTestUtils.is_hidden(phoneNumbersSection));
+ Assert.ok(BrowserTestUtils.is_hidden(addressesSection));
+ Assert.ok(BrowserTestUtils.is_hidden(notesSection));
+ Assert.ok(BrowserTestUtils.is_hidden(otherInfoSection));
+ Assert.ok(BrowserTestUtils.is_hidden(selectedCardsSection));
+
+ // Card 1: an basic card.
+
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(1), {}, abWindow);
+ await TestUtils.waitForCondition(() =>
+ BrowserTestUtils.is_visible(detailsPane)
+ );
+
+ // Header.
+ Assert.equal(viewContactName.textContent, "basic person");
+ Assert.equal(viewPrimaryEmail.textContent, "basic@invalid");
+
+ // Action buttons.
+ await checkActionButtons("basic@invalid", "basic person");
+ Assert.ok(BrowserTestUtils.is_visible(editButton));
+
+ // Email section.
+ Assert.ok(BrowserTestUtils.is_visible(emailAddressesSection));
+ let items = emailAddressesSection.querySelectorAll("li");
+ Assert.equal(items.length, 1);
+ Assert.equal(items[0].querySelector(".entry-type").textContent, "");
+ Assert.equal(
+ items[0].querySelector("a").href,
+ `mailto:basic%20person%20%3Cbasic%40invalid%3E`
+ );
+ Assert.equal(items[0].querySelector("a").textContent, "basic@invalid");
+
+ let composeWindowPromise = BrowserTestUtils.domWindowOpened();
+ EventUtils.synthesizeMouseAtCenter(items[0].querySelector("a"), {}, abWindow);
+ await checkComposeWindow(
+ await composeWindowPromise,
+ "basic person <basic@invalid>"
+ );
+
+ // Other sections.
+ Assert.ok(BrowserTestUtils.is_hidden(phoneNumbersSection));
+ Assert.ok(BrowserTestUtils.is_hidden(addressesSection));
+ Assert.ok(BrowserTestUtils.is_hidden(notesSection));
+ Assert.ok(BrowserTestUtils.is_hidden(otherInfoSection));
+ Assert.ok(BrowserTestUtils.is_hidden(selectedCardsSection));
+
+ // Card 2: an complex card.
+
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(2), {}, abWindow);
+ await TestUtils.waitForCondition(() =>
+ BrowserTestUtils.is_visible(detailsPane)
+ );
+
+ // Header.
+ Assert.equal(viewContactName.textContent, "complex person");
+ Assert.equal(viewPrimaryEmail.textContent, "primary@invalid");
+
+ // Action buttons.
+ await checkActionButtons(
+ "primary@invalid",
+ "complex person",
+ "primary@invalid secondary@invalid tertiary@invalid"
+ );
+ Assert.ok(BrowserTestUtils.is_visible(editButton));
+
+ // Email section.
+ Assert.ok(BrowserTestUtils.is_visible(emailAddressesSection));
+ items = emailAddressesSection.querySelectorAll("li");
+ Assert.equal(items.length, 3);
+
+ Assert.equal(items[0].querySelector(".entry-type").textContent, "");
+ Assert.equal(
+ items[0].querySelector("a").href,
+ `mailto:complex%20person%20%3Csecondary%40invalid%3E`
+ );
+ Assert.equal(items[0].querySelector("a").textContent, "secondary@invalid");
+
+ Assert.equal(items[1].querySelector(".entry-type").textContent, "");
+ Assert.equal(
+ items[1].querySelector("a").href,
+ `mailto:complex%20person%20%3Cprimary%40invalid%3E`
+ );
+ Assert.equal(items[1].querySelector("a").textContent, "primary@invalid");
+
+ Assert.equal(
+ items[2].querySelector(".entry-type").dataset.l10nId,
+ "about-addressbook-entry-type-work"
+ );
+ Assert.equal(
+ items[2].querySelector("a").href,
+ `mailto:complex%20person%20%3Ctertiary%40invalid%3E`
+ );
+ Assert.equal(items[2].querySelector("a").textContent, "tertiary@invalid");
+
+ composeWindowPromise = BrowserTestUtils.domWindowOpened();
+ EventUtils.synthesizeMouseAtCenter(items[2].querySelector("a"), {}, abWindow);
+ await checkComposeWindow(
+ await composeWindowPromise,
+ "complex person <tertiary@invalid>"
+ );
+
+ // Phone numbers section.
+ Assert.ok(BrowserTestUtils.is_visible(phoneNumbersSection));
+ items = phoneNumbersSection.querySelectorAll("li");
+ Assert.equal(items.length, 4);
+
+ Assert.equal(items[0].querySelector(".entry-type").textContent, "");
+ Assert.equal(items[0].querySelector(".entry-value a").href, `tel:0000000`);
+
+ Assert.equal(
+ items[1].querySelector(".entry-type").dataset.l10nId,
+ "about-addressbook-entry-type-work"
+ );
+ Assert.equal(items[1].querySelector(".entry-value").textContent, "111-1111");
+ Assert.equal(items[1].querySelector(".entry-value a").href, `callto:1111111`);
+
+ Assert.equal(
+ items[2].querySelector(".entry-type").dataset.l10nId,
+ "about-addressbook-entry-type-work"
+ );
+ Assert.equal(items[2].querySelector(".entry-value").textContent, "222-2222");
+
+ Assert.equal(
+ items[3].querySelector(".entry-type").dataset.l10nId,
+ "about-addressbook-entry-type-home"
+ );
+ Assert.equal(items[3].querySelector(".entry-value").textContent, "333-3333");
+ Assert.equal(items[3].querySelector(".entry-value a").href, `tel:3333333`);
+
+ // Addresses section.
+ Assert.ok(BrowserTestUtils.is_visible(addressesSection));
+ items = addressesSection.querySelectorAll("li");
+ Assert.equal(items.length, 1);
+
+ Assert.equal(items[0].querySelector(".entry-type").textContent, "");
+ Assert.equal(items[0].querySelector(".entry-value").childNodes.length, 11);
+ Assert.deepEqual(
+ Array.from(
+ items[0].querySelector(".entry-value").childNodes,
+ n => n.textContent
+ ),
+ ["street", "", "suburb", "", "city", "", "state", "", "zip", "", "country"]
+ );
+
+ // Notes section.
+ Assert.ok(BrowserTestUtils.is_visible(notesSection));
+ Assert.equal(
+ notesSection.querySelector("div").textContent,
+ "mary had a little lamb\nits fleece was white as snow\nand everywhere that mary went\nthe lamb was sure to go"
+ );
+
+ // Websites section
+ Assert.ok(BrowserTestUtils.is_visible(websitesSection));
+ items = websitesSection.querySelectorAll("li");
+ Assert.equal(items.length, 1);
+ Assert.equal(
+ items[0].children[0].dataset.l10nId,
+ "about-addressbook-entry-type-work"
+ );
+ Assert.equal(
+ items[0].children[1].querySelector("a").href,
+ "https://www.thunderbird.net/"
+ );
+ Assert.equal(
+ items[0].children[1].querySelector("a").textContent,
+ "www.thunderbird.net"
+ );
+ items[0].children[1].querySelector("a").scrollIntoView();
+ EventUtils.synthesizeMouseAtCenter(
+ items[0].children[1].querySelector("a"),
+ {},
+ abWindow
+ );
+ await TestUtils.waitForCondition(
+ () => mockExternalProtocolService.urlLoaded("https://www.thunderbird.net/"),
+ "attempted to load website in a browser"
+ );
+
+ // Instant messaging section
+ Assert.ok(BrowserTestUtils.is_visible(imppSection));
+ items = imppSection.querySelectorAll("li");
+ Assert.equal(items.length, 1);
+ Assert.equal(
+ items[0].children[1].querySelector("a").href,
+ "xmpp:cowboy@example.org"
+ );
+
+ // Other sections.
+ Assert.ok(BrowserTestUtils.is_visible(otherInfoSection));
+ items = otherInfoSection.querySelectorAll("li");
+ Assert.equal(items.length, 6, "number of <li> in section should be correct");
+ Assert.equal(
+ items[0].children[0].dataset.l10nId,
+ "about-addressbook-entry-name-birthday"
+ );
+ Assert.equal(items[0].children[1].textContent, "February 29");
+ Assert.equal(
+ items[1].children[0].dataset.l10nId,
+ "about-addressbook-entry-name-anniversary"
+ );
+ Assert.equal(items[1].children[1].textContent, "June 11, 2018");
+
+ Assert.equal(
+ items[2].children[0].dataset.l10nId,
+ "about-addressbook-entry-name-title"
+ );
+ Assert.equal(items[2].children[1].textContent, "senior engineering lead");
+ Assert.equal(
+ items[3].children[0].dataset.l10nId,
+ "about-addressbook-entry-name-role"
+ );
+ Assert.equal(items[3].children[1].textContent, "sheriff");
+ Assert.equal(
+ items[4].children[0].dataset.l10nId,
+ "about-addressbook-entry-name-organization"
+ );
+ Assert.deepEqual(
+ Array.from(
+ items[4].querySelector(".entry-value").childNodes,
+ n => n.textContent
+ ),
+ ["engineering", " • ", "thunderbird"]
+ );
+ Assert.equal(
+ items[5].children[0].dataset.l10nId,
+ "about-addressbook-entry-name-time-zone"
+ );
+ Assert.equal(items[5].children[1].firstChild.nodeValue, "Pacific/Auckland");
+ Assert.equal(
+ items[5].children[1].lastChild.getAttribute("is"),
+ "active-time"
+ );
+ Assert.equal(
+ items[5].children[1].lastChild.getAttribute("tz"),
+ "Pacific/Auckland"
+ );
+ Assert.ok(BrowserTestUtils.is_hidden(selectedCardsSection));
+
+ // Card 0, again, just to prove that everything was cleared properly.
+
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(0), {}, abWindow);
+ await TestUtils.waitForCondition(() =>
+ BrowserTestUtils.is_visible(detailsPane)
+ );
+
+ // Header.
+ Assert.equal(viewContactName.textContent, "");
+ Assert.equal(viewPrimaryEmail.textContent, "");
+
+ // Action buttons.
+ await checkActionButtons();
+ Assert.ok(BrowserTestUtils.is_visible(editButton));
+
+ Assert.ok(BrowserTestUtils.is_hidden(emailAddressesSection));
+ Assert.ok(BrowserTestUtils.is_hidden(phoneNumbersSection));
+ Assert.ok(BrowserTestUtils.is_hidden(addressesSection));
+ Assert.ok(BrowserTestUtils.is_hidden(notesSection));
+ Assert.ok(BrowserTestUtils.is_hidden(otherInfoSection));
+ Assert.ok(BrowserTestUtils.is_hidden(selectedCardsSection));
+
+ await closeAddressBookWindow();
+});
+
+/**
+ * Test the display of dates with various components missing.
+ */
+add_task(async function testDates() {
+ let abWindow = await openAddressBookWindow();
+ let otherInfoSection = abWindow.document.getElementById("otherInfo");
+
+ // Year only.
+
+ let yearCard = await addAndDisplayCard(formatVCard`
+ BEGIN:VCARD
+ EMAIL:xbasic3@invalid
+ ANNIVERSARY:2005
+ END:VCARD
+ `);
+ Assert.ok(BrowserTestUtils.is_visible(otherInfoSection));
+ let items = otherInfoSection.querySelectorAll("li");
+ Assert.equal(items.length, 1);
+ Assert.equal(
+ items[0].children[0].dataset.l10nId,
+ "about-addressbook-entry-name-anniversary"
+ );
+ Assert.equal(items[0].children[1].textContent, "2005");
+
+ // Year and month.
+
+ let yearMonthCard = await addAndDisplayCard(formatVCard`
+ BEGIN:VCARD
+ EMAIL:xbasic4@invalid
+ ANNIVERSARY:2006-06
+ END:VCARD
+ `);
+ Assert.ok(BrowserTestUtils.is_visible(otherInfoSection));
+ items = otherInfoSection.querySelectorAll("li");
+ Assert.equal(items.length, 1);
+ Assert.equal(
+ items[0].children[0].dataset.l10nId,
+ "about-addressbook-entry-name-anniversary"
+ );
+ Assert.equal(items[0].children[1].textContent, "June 2006");
+
+ // Month only.
+ let monthCard = await addAndDisplayCard(formatVCard`
+ BEGIN:VCARD
+ EMAIL:xbasic5@invalid
+ ANNIVERSARY:--12
+ END:VCARD
+ `);
+ Assert.ok(BrowserTestUtils.is_visible(otherInfoSection));
+ items = otherInfoSection.querySelectorAll("li");
+ Assert.equal(items.length, 1);
+ Assert.equal(
+ items[0].children[0].dataset.l10nId,
+ "about-addressbook-entry-name-anniversary"
+ );
+ Assert.equal(items[0].children[1].textContent, "December");
+
+ // Month and day.
+ let monthDayCard = await addAndDisplayCard(formatVCard`
+ BEGIN:VCARD
+ EMAIL:xbasic6@invalid
+ ANNIVERSARY;VALUE=DATE:--0704
+ END:VCARD
+ `);
+ Assert.ok(BrowserTestUtils.is_visible(otherInfoSection));
+ items = otherInfoSection.querySelectorAll("li");
+ Assert.equal(items.length, 1);
+ Assert.equal(
+ items[0].children[0].dataset.l10nId,
+ "about-addressbook-entry-name-anniversary"
+ );
+ Assert.equal(items[0].children[1].textContent, "July 4");
+
+ // Day only.
+ let dayCard = await addAndDisplayCard(formatVCard`
+ BEGIN:VCARD
+ EMAIL:xbasic7@invalid
+ ANNIVERSARY:---30
+ END:VCARD
+ `);
+ Assert.ok(BrowserTestUtils.is_visible(otherInfoSection));
+ items = otherInfoSection.querySelectorAll("li");
+ Assert.equal(items.length, 1);
+ Assert.equal(
+ items[0].children[0].dataset.l10nId,
+ "about-addressbook-entry-name-anniversary"
+ );
+ Assert.equal(items[0].children[1].textContent, "30");
+
+ await closeAddressBookWindow();
+ personalBook.deleteCards([
+ yearCard,
+ yearMonthCard,
+ monthCard,
+ monthDayCard,
+ dayCard,
+ ]);
+});
+
+/**
+ * Only an organisation name.
+ */
+add_task(async function testOrganisationNameOnly() {
+ let card = await addAndDisplayCard(
+ VCardUtils.vCardToAbCard(formatVCard`
+ BEGIN:VCARD
+ ORG:organisation
+ END:VCARD
+ `)
+ );
+
+ let abWindow = await getAddressBookWindow();
+ let viewContactName = abWindow.document.getElementById("viewContactName");
+ Assert.equal(viewContactName.textContent, "organisation");
+
+ await closeAddressBookWindow();
+ personalBook.deleteCards([card]);
+});
+
+/**
+ * Tests that custom properties (Custom1 etc.) are displayed.
+ */
+add_task(async function testCustomProperties() {
+ let card = new AddrBookCard();
+ card._properties = new Map([
+ ["PopularityIndex", 0],
+ ["Custom2", "custom two"],
+ ["Custom4", "custom four"],
+ [
+ "_vCard",
+ formatVCard`
+ BEGIN:VCARD
+ FN:custom person
+ X-CUSTOM3:x-custom three
+ X-CUSTOM4:x-custom four
+ END:VCARD
+ `,
+ ],
+ ]);
+ card = await addAndDisplayCard(card);
+
+ let abWindow = await getAddressBookWindow();
+ let otherInfoSection = abWindow.document.getElementById("otherInfo");
+
+ Assert.ok(BrowserTestUtils.is_visible(otherInfoSection));
+
+ let items = otherInfoSection.querySelectorAll("li");
+ Assert.equal(items.length, 3);
+ // Custom 1 has no value, should not display.
+ // Custom 2 has an old property value, should display that.
+
+ await TestUtils.waitForCondition(() => {
+ return items[0].children[0].textContent;
+ }, "text not created in time");
+
+ Assert.equal(items[0].children[0].textContent, "Custom 2");
+ Assert.equal(items[0].children[1].textContent, "custom two");
+ // Custom 3 has a vCard property value, should display that.
+ Assert.equal(items[1].children[0].textContent, "Custom 3");
+ Assert.equal(items[1].children[1].textContent, "x-custom three");
+ // Custom 4 has both types of value, the vCard value should be displayed.
+ Assert.equal(items[2].children[0].textContent, "Custom 4");
+ Assert.equal(items[2].children[1].textContent, "x-custom four");
+
+ await closeAddressBookWindow();
+ personalBook.deleteCards([card]);
+});
+
+/**
+ * Checks that the edit button is hidden for read-only contacts.
+ */
+add_task(async function testReadOnlyActions() {
+ let readOnlyBook = createAddressBook("Read-Only Book");
+ let readOnlyList = readOnlyBook.addMailList(
+ createMailingList("Read-Only List")
+ );
+ readOnlyBook.addCard(
+ VCardUtils.vCardToAbCard(formatVCard`
+ BEGIN:VCARD
+ FN:read-only person
+ END:VCARD
+ `)
+ );
+ readOnlyList.addCard(
+ readOnlyBook.addCard(
+ VCardUtils.vCardToAbCard(formatVCard`
+ BEGIN:VCARD
+ FN:read-only person with email
+ EMAIL:read.only@invalid
+ END:VCARD
+ `)
+ )
+ );
+ readOnlyBook.setBoolValue("readOnly", true);
+
+ let abWindow = await openAddressBookWindow();
+
+ let abDocument = abWindow.document;
+ let cardsList = abDocument.getElementById("cards");
+ let detailsPane = abDocument.getElementById("detailsPane");
+ let contactView = abDocument.getElementById("viewContact");
+
+ let actions = abDocument.getElementById("detailsActions");
+ let editButton = abDocument.getElementById("editButton");
+ let editForm = abDocument.getElementById("editContactForm");
+
+ let selectHandler = {
+ seenEvent: null,
+ selectedAtEvent: null,
+
+ reset() {
+ this.seenEvent = null;
+ this.selectedAtEvent = null;
+ },
+ handleEvent(event) {
+ this.seenEvent = event;
+ this.selectedAtEvent = cardsList.selectedIndex;
+ },
+ };
+
+ // Check contacts with the book displayed.
+
+ openDirectory(readOnlyBook);
+ Assert.equal(cardsList.view.rowCount, 3);
+ Assert.ok(BrowserTestUtils.is_hidden(detailsPane));
+
+ // Without email.
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(1), {}, abWindow);
+ Assert.ok(
+ BrowserTestUtils.is_visible(contactView),
+ "contact view should be shown"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_hidden(actions),
+ "actions section should be hidden"
+ );
+
+ // With email.
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(2), {}, abWindow);
+ Assert.ok(BrowserTestUtils.is_visible(actions), "actions section is shown");
+ await checkActionButtons("read.only@invalid", "read-only person with email");
+ Assert.ok(BrowserTestUtils.is_hidden(editButton), "editButton is hidden");
+
+ // Double clicking on the item will select but not edit it.
+ EventUtils.synthesizeMouseAtCenter(
+ cardsList.getRowAtIndex(1),
+ { clickCount: 1 },
+ abWindow
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ cardsList.getRowAtIndex(1),
+ { clickCount: 2 },
+ abWindow
+ );
+ // Wait one loop to see if edit form was opened.
+ await TestUtils.waitForTick();
+ Assert.ok(
+ BrowserTestUtils.is_visible(contactView),
+ "contact view should be shown"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_hidden(editForm),
+ "contact form should be hidden"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_hidden(actions),
+ "actions section should be hidden"
+ );
+ Assert.equal(
+ cardsList.table.body,
+ abDocument.activeElement,
+ "Cards list should be the active element"
+ );
+
+ selectHandler.reset();
+ cardsList.addEventListener("select", selectHandler, { once: true });
+ // Same with Enter on the second item.
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, abWindow);
+ await TestUtils.waitForCondition(
+ () => selectHandler.seenEvent,
+ `'select' event should get fired`
+ );
+ Assert.ok(
+ BrowserTestUtils.is_visible(contactView),
+ "contact view should be shown"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_hidden(editForm),
+ "contact form should be hidden"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_visible(actions),
+ "actions section should be shown"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_hidden(editButton),
+ "editButton should be hidden"
+ );
+
+ EventUtils.synthesizeKey("KEY_Enter", {}, abWindow);
+ await TestUtils.waitForTick();
+ Assert.ok(
+ BrowserTestUtils.is_visible(contactView),
+ "contact view should be shown"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_hidden(editForm),
+ "contact form should be hidden"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_visible(actions),
+ "actions section should be shown"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_hidden(editForm),
+ "contact form should be hidden"
+ );
+
+ // Check contacts with the list displayed.
+
+ openDirectory(readOnlyList);
+ Assert.equal(cardsList.view.rowCount, 1);
+ Assert.ok(BrowserTestUtils.is_hidden(detailsPane));
+
+ // With email.
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(0), {}, abWindow);
+ Assert.ok(BrowserTestUtils.is_visible(contactView));
+ Assert.ok(BrowserTestUtils.is_visible(actions), "actions section is shown");
+ await checkActionButtons("read.only@invalid", "read-only person with email");
+ Assert.ok(BrowserTestUtils.is_hidden(editButton), "editButton is hidden");
+
+ // Check contacts with All Address Books displayed.
+
+ openAllAddressBooks();
+ Assert.equal(cardsList.view.rowCount, 6);
+ Assert.ok(BrowserTestUtils.is_hidden(detailsPane));
+
+ // Basic person from Personal Address Books.
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(1), {}, abWindow);
+ Assert.ok(BrowserTestUtils.is_visible(contactView));
+ Assert.ok(BrowserTestUtils.is_visible(actions), "actions section is shown");
+ await checkActionButtons("basic@invalid", "basic person");
+ Assert.ok(BrowserTestUtils.is_visible(editButton), "edit button is shown");
+
+ // Without email.
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(4), {}, abWindow);
+ Assert.ok(BrowserTestUtils.is_visible(contactView));
+ Assert.ok(BrowserTestUtils.is_hidden(actions), "actions section is hidden");
+
+ // With email.
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(5), {}, abWindow);
+ Assert.ok(BrowserTestUtils.is_visible(actions), "actions section is shown");
+ await checkActionButtons("read.only@invalid", "read-only person with email");
+ Assert.ok(BrowserTestUtils.is_hidden(editButton), "editButton is hidden");
+
+ // Basic person again, to prove the buttons aren't hidden forever.
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(1), {}, abWindow);
+ Assert.ok(BrowserTestUtils.is_visible(contactView));
+ Assert.ok(BrowserTestUtils.is_visible(actions), "actions section is shown");
+ await checkActionButtons("basic@invalid", "basic person");
+ Assert.ok(BrowserTestUtils.is_visible(editButton), "edit button is shown");
+
+ await closeAddressBookWindow();
+ await promiseDirectoryRemoved(readOnlyBook.URI);
+});
+
+/**
+ * Tests that we correctly fix Google's bad escaping of colons in values, and
+ * other characters in URI values.
+ */
+add_task(async function testGoogleEscaping() {
+ let googleBook = createAddressBook("Google Book");
+ googleBook.wrappedJSObject._isGoogleCardDAV = true;
+ googleBook.addCard(
+ VCardUtils.vCardToAbCard(formatVCard`
+ BEGIN:VCARD
+ VERSION:3.0
+ N:test;en\\\\c\\:oding;;;
+ FN:en\\\\c\\:oding test
+ TITLE:title\\:title\\;title\\,title\\\\title\\\\\\:title\\\\\\;title\\\\\\,title\\\\\\\\
+ TEL:tel\\:0123\\\\4567
+ NOTE:notes\\:\\nnotes\\;\\nnotes\\,\\nnotes\\\\
+ URL:https\\://host/url\\:url\\;url\\,url\\\\url
+ END:VCARD
+ `)
+ );
+
+ let abWindow = await openAddressBookWindow();
+
+ let abDocument = abWindow.document;
+ let cardsList = abDocument.getElementById("cards");
+ let detailsPane = abDocument.getElementById("detailsPane");
+
+ let viewContactName = abDocument.getElementById("viewContactName");
+ let viewPrimaryEmail = abDocument.getElementById("viewPrimaryEmail");
+ let editButton = abDocument.getElementById("editButton");
+
+ let emailAddressesSection = abDocument.getElementById("emailAddresses");
+ let phoneNumbersSection = abDocument.getElementById("phoneNumbers");
+ let addressesSection = abDocument.getElementById("addresses");
+ let notesSection = abDocument.getElementById("notes");
+ let websitesSection = abDocument.getElementById("websites");
+ let imppSection = abDocument.getElementById("instantMessaging");
+ let otherInfoSection = abDocument.getElementById("otherInfo");
+ let selectedCardsSection = abDocument.getElementById("selectedCards");
+
+ openDirectory(googleBook);
+ Assert.equal(cardsList.view.rowCount, 1);
+ Assert.ok(BrowserTestUtils.is_hidden(detailsPane));
+
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(0), {}, abWindow);
+ await TestUtils.waitForCondition(() =>
+ BrowserTestUtils.is_visible(detailsPane)
+ );
+
+ // Header.
+ Assert.equal(viewContactName.textContent, "en\\c:oding test");
+ Assert.equal(viewPrimaryEmail.textContent, "");
+
+ // Action buttons.
+ await checkActionButtons();
+ Assert.ok(BrowserTestUtils.is_visible(editButton));
+
+ // Email section.
+ Assert.ok(BrowserTestUtils.is_hidden(emailAddressesSection));
+
+ // Phone numbers section.
+ Assert.ok(BrowserTestUtils.is_visible(phoneNumbersSection));
+ let items = phoneNumbersSection.querySelectorAll("li");
+ Assert.equal(items.length, 1);
+
+ Assert.equal(items[0].querySelector(".entry-type").textContent, "");
+ Assert.equal(items[0].querySelector(".entry-value").textContent, "01234567");
+
+ // Addresses section.
+ Assert.ok(BrowserTestUtils.is_hidden(addressesSection));
+
+ // Notes section.
+ Assert.ok(BrowserTestUtils.is_visible(notesSection));
+ Assert.equal(
+ notesSection.querySelector("div").textContent,
+ "notes:\nnotes;\nnotes,\nnotes\\"
+ );
+
+ // Websites section
+ Assert.ok(BrowserTestUtils.is_visible(websitesSection));
+ items = websitesSection.querySelectorAll("li");
+ Assert.equal(items.length, 1);
+ Assert.equal(
+ items[0].children[1].querySelector("a").href,
+ "https://host/url:url;url,url/url"
+ );
+ Assert.equal(
+ items[0].children[1].querySelector("a").textContent,
+ "host/url:url;url,url/url"
+ );
+ items[0].children[1].querySelector("a").scrollIntoView();
+ EventUtils.synthesizeMouseAtCenter(
+ items[0].children[1].querySelector("a"),
+ {},
+ abWindow
+ );
+ await TestUtils.waitForCondition(
+ () =>
+ mockExternalProtocolService.urlLoaded("https://host/url:url;url,url/url"),
+ "attempted to load website in a browser"
+ );
+
+ // Instant messaging section.
+ Assert.ok(BrowserTestUtils.is_hidden(imppSection));
+
+ // Other sections.
+ Assert.ok(BrowserTestUtils.is_visible(otherInfoSection));
+ items = otherInfoSection.querySelectorAll("li");
+ Assert.equal(items.length, 1);
+ Assert.equal(
+ items[0].children[0].dataset.l10nId,
+ "about-addressbook-entry-name-title"
+ );
+ Assert.equal(
+ items[0].children[1].textContent,
+ "title:title;title,title\\title\\:title\\;title\\,title\\\\"
+ );
+
+ Assert.ok(BrowserTestUtils.is_hidden(selectedCardsSection));
+
+ await closeAddressBookWindow();
+ await promiseDirectoryRemoved(googleBook.URI);
+});
+
+async function addAndDisplayCard(card) {
+ if (typeof card == "string") {
+ card = VCardUtils.vCardToAbCard(card);
+ }
+ card = personalBook.addCard(card);
+
+ let abWindow = await openAddressBookWindow();
+ let abDocument = abWindow.document;
+ let cardsList = abDocument.getElementById("cards");
+ let detailsPane = abDocument.getElementById("detailsPane");
+
+ let index = cardsList.view.getIndexForUID(card.UID);
+ EventUtils.synthesizeMouseAtCenter(
+ cardsList.getRowAtIndex(index),
+ {},
+ abWindow
+ );
+ await TestUtils.waitForCondition(() =>
+ BrowserTestUtils.is_visible(detailsPane)
+ );
+ return card;
+}
+
+async function checkActionButtons(
+ primaryEmail,
+ displayName,
+ searchString = primaryEmail
+) {
+ let tabmail = document.getElementById("tabmail");
+ let abWindow = getAddressBookWindow();
+ let abDocument = abWindow.document;
+
+ let writeButton = abDocument.getElementById("detailsWriteButton");
+ let eventButton = abDocument.getElementById("detailsEventButton");
+ let searchButton = abDocument.getElementById("detailsSearchButton");
+ let newListButton = abDocument.getElementById("detailsNewListButton");
+
+ if (primaryEmail) {
+ // Write.
+ Assert.ok(
+ BrowserTestUtils.is_visible(writeButton),
+ "write button is visible"
+ );
+
+ let composeWindowPromise = BrowserTestUtils.domWindowOpened();
+ EventUtils.synthesizeMouseAtCenter(writeButton, {}, abWindow);
+ await checkComposeWindow(
+ await composeWindowPromise,
+ `${displayName} <${primaryEmail}>`
+ );
+
+ // Search. Do this before the event test to stop a strange macOS failure.
+ Assert.ok(
+ BrowserTestUtils.is_visible(searchButton),
+ "search button is visible"
+ );
+
+ let searchTabPromise = BrowserTestUtils.waitForEvent(window, "TabOpen");
+ EventUtils.synthesizeMouseAtCenter(searchButton, {}, abWindow);
+ let {
+ detail: { tabInfo: searchTab },
+ } = await searchTabPromise;
+
+ let searchBox = tabmail.selectedTab.panel.querySelector(".searchBox");
+ Assert.equal(searchBox.value, searchString);
+
+ searchTabPromise = BrowserTestUtils.waitForEvent(window, "TabClose");
+ tabmail.closeTab(searchTab);
+ await searchTabPromise;
+
+ // Event.
+ Assert.ok(
+ BrowserTestUtils.is_visible(eventButton),
+ "event button is visible"
+ );
+
+ let eventWindowPromise = CalendarTestUtils.waitForEventDialog("edit");
+ EventUtils.synthesizeMouseAtCenter(eventButton, {}, abWindow);
+ let eventWindow = await eventWindowPromise;
+
+ let iframe = eventWindow.document.getElementById(
+ "calendar-item-panel-iframe"
+ );
+ let tabPanels = iframe.contentDocument.getElementById(
+ "event-grid-tabpanels"
+ );
+ let attendeesTabPanel = iframe.contentDocument.getElementById(
+ "event-grid-tabpanel-attendees"
+ );
+ Assert.equal(
+ tabPanels.selectedPanel,
+ attendeesTabPanel,
+ "attendees are displayed"
+ );
+ let attendeeNames = attendeesTabPanel.querySelectorAll(
+ ".attendee-list .attendee-name"
+ );
+ Assert.deepEqual(
+ Array.from(attendeeNames, a => a.textContent),
+ [`${displayName} <${primaryEmail}>`],
+ "attendees are correct"
+ );
+
+ eventWindowPromise = BrowserTestUtils.domWindowClosed(eventWindow);
+ BrowserTestUtils.promiseAlertDialog("extra1");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, eventWindow);
+ await eventWindowPromise;
+ Assert.report(false, undefined, undefined, "Item dialog closed");
+ } else {
+ Assert.ok(
+ BrowserTestUtils.is_hidden(writeButton),
+ "write button is hidden"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_hidden(eventButton),
+ "event button is hidden"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_hidden(searchButton),
+ "search button is hidden"
+ );
+ }
+
+ Assert.ok(
+ BrowserTestUtils.is_hidden(newListButton),
+ "new list button is hidden"
+ );
+}
diff --git a/comm/mail/components/addrbook/test/browser/browser_display_multiple.js b/comm/mail/components/addrbook/test/browser/browser_display_multiple.js
new file mode 100644
index 0000000000..02642f4408
--- /dev/null
+++ b/comm/mail/components/addrbook/test/browser/browser_display_multiple.js
@@ -0,0 +1,468 @@
+/* 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/. */
+
+var { CalendarTestUtils } = ChromeUtils.import(
+ "resource://testing-common/calendar/CalendarTestUtils.jsm"
+);
+
+var { VCardUtils } = ChromeUtils.import("resource:///modules/VCardUtils.jsm");
+
+add_setup(async function () {
+ let card1 = personalBook.addCard(createContact("victor", "test"));
+ personalBook.addCard(createContact("romeo", "test", undefined, ""));
+ let card3 = personalBook.addCard(createContact("oscar", "test"));
+ personalBook.addCard(createContact("mike", "test", undefined, ""));
+ const card5 = personalBook.addCard(createContact("xray", "test"));
+ const card6 = personalBook.addCard(createContact("yankee", "test"));
+ const card7 = personalBook.addCard(createContact("zulu", "test"));
+ let list1 = personalBook.addMailList(createMailingList("list 1"));
+ list1.addCard(card1);
+ list1.addCard(card3);
+ list1.addCard(card5);
+ list1.addCard(card6);
+ list1.addCard(card7);
+ let list2 = personalBook.addMailList(createMailingList("list 2"));
+ list2.addCard(card3);
+
+ MailServices.accounts.createLocalMailAccount();
+ let account = MailServices.accounts.accounts[0];
+ account.addIdentity(MailServices.accounts.createIdentity());
+
+ let calendar = CalendarTestUtils.createCalendar();
+
+ registerCleanupFunction(async () => {
+ MailServices.accounts.removeAccount(account, true);
+ CalendarTestUtils.removeCalendar(calendar);
+ });
+});
+
+add_task(async function testSelectMultiple() {
+ let abWindow = await openAddressBookWindow();
+ openDirectory(personalBook);
+
+ let abDocument = abWindow.document;
+ let cardsList = abDocument.getElementById("cards");
+ let detailsPane = abDocument.getElementById("detailsPane");
+
+ // In order; list 1, list 2, mike, oscar, romeo, victor, xray, yankee, zulu.
+ Assert.equal(cardsList.view.rowCount, 9);
+ Assert.ok(detailsPane.hidden);
+
+ // Select list 1 and check the list display.
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(0), {}, abWindow);
+ await checkHeader({ listName: "list 1" });
+ await checkActionButtons(
+ ["list 1 <list 1>"],
+ [],
+ [
+ "victor test <victor.test@invalid>",
+ "oscar test <oscar.test@invalid>",
+ "xray test <xray.test@invalid>",
+ "yankee test <yankee.test@invalid>",
+ "zulu test <zulu.test@invalid>",
+ ]
+ );
+ await checkList([
+ "oscar test",
+ "victor test",
+ "xray test",
+ "yankee test",
+ "zulu test",
+ ]);
+
+ // list 1 and list 2.
+ EventUtils.synthesizeMouseAtCenter(
+ cardsList.getRowAtIndex(1),
+ { shiftKey: true },
+ abWindow
+ );
+ await checkHeader({ selectionCount: 2, selectionType: "lists" });
+ await checkActionButtons(["list 1 <list 1>", "list 2 <list 2>"]);
+ await checkList(["list 1", "list 2"]);
+
+ // list 1 and mike (no address).
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(0), {}, abWindow);
+ EventUtils.synthesizeMouseAtCenter(
+ cardsList.getRowAtIndex(2),
+ { accelKey: true },
+ abWindow
+ );
+ await checkHeader({ selectionCount: 2, selectionType: "mixed" });
+ await checkActionButtons(["list 1 <list 1>"]);
+ await checkList(["list 1", "mike test"]);
+
+ // list 1 and oscar.
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(0), {}, abWindow);
+ EventUtils.synthesizeMouseAtCenter(
+ cardsList.getRowAtIndex(3),
+ { accelKey: true },
+ abWindow
+ );
+ await checkHeader({ selectionCount: 2, selectionType: "mixed" });
+ await checkActionButtons(
+ ["list 1 <list 1>"],
+ ["oscar test <oscar.test@invalid>"]
+ );
+ await checkList(["list 1", "oscar test"]);
+
+ // mike (no address) and oscar.
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(2), {}, abWindow);
+ EventUtils.synthesizeMouseAtCenter(
+ cardsList.getRowAtIndex(3),
+ { shiftKey: true },
+ abWindow
+ );
+ await checkHeader({ selectionCount: 2, selectionType: "contacts" });
+ await checkActionButtons([], ["oscar test <oscar.test@invalid>"]);
+ await checkList(["mike test", "oscar test"]);
+
+ // mike (no address), oscar, romeo (no address) and victor.
+ EventUtils.synthesizeMouseAtCenter(
+ cardsList.getRowAtIndex(5),
+ { shiftKey: true },
+ abWindow
+ );
+ await checkHeader({ selectionCount: 4, selectionType: "contacts" });
+ await checkActionButtons(
+ [],
+ ["oscar test <oscar.test@invalid>", "victor test <victor.test@invalid>"]
+ );
+ await checkList(["mike test", "oscar test", "romeo test", "victor test"]);
+
+ // mike and romeo (no addresses).
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(2), {}, abWindow);
+ EventUtils.synthesizeMouseAtCenter(
+ cardsList.getRowAtIndex(4),
+ { accelKey: true },
+ abWindow
+ );
+ await checkHeader({ selectionCount: 2, selectionType: "contacts" });
+ await checkActionButtons();
+ await checkList(["mike test", "romeo test"]);
+
+ // Everything.
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(0), {}, abWindow);
+ EventUtils.synthesizeMouseAtCenter(
+ cardsList.getRowAtIndex(5),
+ { shiftKey: true },
+ abWindow
+ );
+ await checkHeader({ selectionCount: 6, selectionType: "mixed" });
+ await checkActionButtons(
+ ["list 1 <list 1>", "list 2 <list 2>"],
+ ["oscar test <oscar.test@invalid>", "victor test <victor.test@invalid>"]
+ );
+ await checkList([
+ "list 1",
+ "list 2",
+ "mike test",
+ "oscar test",
+ "romeo test",
+ "victor test",
+ ]);
+
+ await closeAddressBookWindow();
+});
+
+add_task(async function testDeleteMultiple() {
+ const abWindow = await openAddressBookWindow();
+ const booksList = abWindow.booksList;
+
+ // Open mailing list list1.
+ booksList.getRowAtIndex(2).click();
+
+ const abDocument = abWindow.document;
+ const cardsList = abDocument.getElementById("cards");
+ const detailsPane = abDocument.getElementById("detailsPane");
+
+ // In order; oscar, victor, xray, yankee, zulu.
+ Assert.equal(cardsList.view.rowCount, 5);
+ Assert.ok(detailsPane.hidden);
+
+ // Select victor and yankee.
+ await TestUtils.waitForTick();
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(1), {}, abWindow);
+ EventUtils.synthesizeMouseAtCenter(
+ cardsList.getRowAtIndex(3),
+ { accelKey: true },
+ abWindow
+ );
+ await checkHeader({ selectionCount: 2, selectionType: "contacts" });
+ await checkList(["victor test", "yankee test"]);
+
+ // Delete victor and yankee.
+ let deletePromise = BrowserTestUtils.promiseAlertDialog("accept");
+ EventUtils.synthesizeKey("VK_DELETE", {}, window);
+ await deletePromise;
+ await TestUtils.topicObserved("addrbook-list-member-removed");
+ Assert.equal(cardsList.view.rowCount, 3);
+ Assert.ok(
+ detailsPane.hidden,
+ "The details pane should be cleared after removing two mailing list members."
+ );
+
+ // Select all contacts.
+ await TestUtils.waitForTick();
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(0), {}, abWindow);
+ EventUtils.synthesizeMouseAtCenter(
+ cardsList.getRowAtIndex(2),
+ { shiftKey: true },
+ abWindow
+ );
+ await checkHeader({ selectionCount: 3, selectionType: "contacts" });
+ await checkList(["oscar test", "xray test", "zulu test"]);
+
+ // Delete all contacts.
+ deletePromise = BrowserTestUtils.promiseAlertDialog("accept");
+ EventUtils.synthesizeKey("VK_DELETE", {}, window);
+ await deletePromise;
+ await TestUtils.topicObserved("addrbook-list-member-removed");
+ Assert.equal(cardsList.view.rowCount, 0);
+ Assert.ok(
+ detailsPane.hidden,
+ "The details pane should be cleared after removing all mailing list members."
+ );
+
+ // Open address book personalBook.
+ booksList.getRowAtIndex(1).click();
+
+ // In order; list 1, list 2, mike, oscar, romeo, victor, xray, yankee, zulu.
+ Assert.equal(cardsList.view.rowCount, 9);
+ Assert.ok(detailsPane.hidden);
+
+ // Select list 2 and victor.
+ await TestUtils.waitForTick();
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(1), {}, abWindow);
+ EventUtils.synthesizeMouseAtCenter(
+ cardsList.getRowAtIndex(5),
+ { accelKey: true },
+ abWindow
+ );
+ await checkHeader({ selectionCount: 2, selectionType: "mixed" });
+ await checkList(["list 2", "victor test"]);
+
+ // Delete list 2 and victor.
+ deletePromise = BrowserTestUtils.promiseAlertDialog("accept");
+ EventUtils.synthesizeKey("VK_DELETE", {}, window);
+ await deletePromise;
+ await TestUtils.topicObserved("addrbook-contact-deleted");
+ Assert.equal(cardsList.view.rowCount, 7);
+ Assert.ok(
+ detailsPane.hidden,
+ "The details pane should be cleared after deleting one list and one contact."
+ );
+
+ // Select all contacts.
+ await TestUtils.waitForTick();
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(0), {}, abWindow);
+ EventUtils.synthesizeMouseAtCenter(
+ cardsList.getRowAtIndex(6),
+ { shiftKey: true },
+ abWindow
+ );
+ await checkHeader({ selectionCount: 7, selectionType: "mixed" });
+ await checkList([
+ "list 1",
+ "mike test",
+ "oscar test",
+ "romeo test",
+ "xray test",
+ "yankee test",
+ "zulu test",
+ ]);
+
+ // Delete all contacts.
+ deletePromise = BrowserTestUtils.promiseAlertDialog("accept");
+ EventUtils.synthesizeKey("VK_DELETE", {}, window);
+ await deletePromise;
+ await TestUtils.topicObserved("addrbook-contact-deleted");
+ Assert.equal(cardsList.view.rowCount, 0);
+ Assert.ok(
+ detailsPane.hidden,
+ "The details pane should be cleared after removing all contacts."
+ );
+ await closeAddressBookWindow();
+});
+
+function checkHeader({ listName, selectionCount, selectionType } = {}) {
+ let abWindow = getAddressBookWindow();
+ let abDocument = abWindow.document;
+
+ let contactPhoto = abDocument.getElementById("viewContactPhoto");
+ let contactName = abDocument.getElementById("viewContactName");
+ let listHeader = abDocument.getElementById("viewListName");
+ let selectionHeader = abDocument.getElementById("viewSelectionCount");
+
+ Assert.ok(
+ BrowserTestUtils.is_hidden(contactPhoto),
+ "contact photo should be hidden"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_hidden(contactName),
+ "contact name should be hidden"
+ );
+ if (listName) {
+ Assert.ok(
+ BrowserTestUtils.is_visible(listHeader),
+ "list header should be visible"
+ );
+ Assert.equal(
+ listHeader.textContent,
+ listName,
+ "list header text is correct"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_hidden(selectionHeader),
+ "selection header should be hidden"
+ );
+ } else {
+ Assert.ok(
+ BrowserTestUtils.is_hidden(listHeader),
+ "list header should be hidden"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_visible(selectionHeader),
+ "selection header should be visible"
+ );
+ Assert.deepEqual(abDocument.l10n.getAttributes(selectionHeader), {
+ id: `about-addressbook-selection-${selectionType}-header2`,
+ args: {
+ count: selectionCount,
+ },
+ });
+ }
+}
+
+async function checkActionButtons(
+ listAddresses = [],
+ cardAddresses = [],
+ eventAddresses = cardAddresses
+) {
+ let abWindow = getAddressBookWindow();
+ let abDocument = abWindow.document;
+
+ let writeButton = abDocument.getElementById("detailsWriteButton");
+ let eventButton = abDocument.getElementById("detailsEventButton");
+ let searchButton = abDocument.getElementById("detailsSearchButton");
+ let newListButton = abDocument.getElementById("detailsNewListButton");
+
+ if (cardAddresses.length || listAddresses.length) {
+ // Write.
+ Assert.ok(
+ BrowserTestUtils.is_visible(writeButton),
+ "write button is visible"
+ );
+
+ let composeWindowPromise = BrowserTestUtils.domWindowOpened();
+ EventUtils.synthesizeMouseAtCenter(writeButton, {}, abWindow);
+ await checkComposeWindow(
+ await composeWindowPromise,
+ ...listAddresses,
+ ...cardAddresses
+ );
+ }
+
+ if (eventAddresses.length) {
+ // Event.
+ Assert.ok(
+ BrowserTestUtils.is_visible(eventButton),
+ "event button is visible"
+ );
+
+ let eventWindowPromise = CalendarTestUtils.waitForEventDialog("edit");
+ EventUtils.synthesizeMouseAtCenter(eventButton, {}, abWindow);
+ let eventWindow = await eventWindowPromise;
+
+ let iframe = eventWindow.document.getElementById(
+ "calendar-item-panel-iframe"
+ );
+ let tabPanels = iframe.contentDocument.getElementById(
+ "event-grid-tabpanels"
+ );
+ let attendeesTabPanel = iframe.contentDocument.getElementById(
+ "event-grid-tabpanel-attendees"
+ );
+ Assert.equal(
+ tabPanels.selectedPanel,
+ attendeesTabPanel,
+ "attendees are displayed"
+ );
+ let attendeeNames = attendeesTabPanel.querySelectorAll(
+ ".attendee-list .attendee-name"
+ );
+ Assert.deepEqual(
+ Array.from(attendeeNames, a => a.textContent),
+ eventAddresses,
+ "attendees are correct"
+ );
+
+ eventWindowPromise = BrowserTestUtils.domWindowClosed(eventWindow);
+ BrowserTestUtils.promiseAlertDialog("extra1");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, eventWindow);
+ await eventWindowPromise;
+ await new Promise(resolve => abWindow.setTimeout(resolve));
+ Assert.report(false, undefined, undefined, "Item dialog closed");
+ } else {
+ Assert.ok(
+ BrowserTestUtils.is_hidden(eventButton),
+ "event button is hidden"
+ );
+ }
+
+ if (cardAddresses.length) {
+ // New List.
+ Assert.ok(
+ BrowserTestUtils.is_visible(newListButton),
+ "new list button is visible"
+ );
+ let listWindowPromise = promiseLoadSubDialog(
+ "chrome://messenger/content/addressbook/abMailListDialog.xhtml"
+ );
+ EventUtils.synthesizeMouseAtCenter(newListButton, {}, abWindow);
+ let listWindow = await listWindowPromise;
+ let memberNames = listWindow.document.querySelectorAll(
+ ".textbox-addressingWidget"
+ );
+ Assert.deepEqual(
+ Array.from(memberNames, aw => aw.value),
+ [...cardAddresses, ""],
+ "list members are correct"
+ );
+
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, listWindow);
+ } else {
+ Assert.ok(
+ BrowserTestUtils.is_hidden(newListButton),
+ "new list button is hidden"
+ );
+ }
+
+ Assert.ok(
+ BrowserTestUtils.is_hidden(searchButton),
+ "search button is hidden"
+ );
+}
+
+function checkList(names) {
+ let abWindow = getAddressBookWindow();
+ let abDocument = abWindow.document;
+
+ let selectedCardsSection = abDocument.getElementById("selectedCards");
+ let otherSections = abDocument.querySelectorAll(
+ "#detailsBody > section:not(#detailsActions, #selectedCards)"
+ );
+
+ Assert.ok(BrowserTestUtils.is_visible(selectedCardsSection));
+ for (let section of otherSections) {
+ Assert.ok(BrowserTestUtils.is_hidden(section), `${section.id} is hidden`);
+ }
+
+ Assert.deepEqual(
+ Array.from(
+ selectedCardsSection.querySelectorAll("li .name"),
+ li => li.textContent
+ ),
+ names
+ );
+}
diff --git a/comm/mail/components/addrbook/test/browser/browser_drag_drop.js b/comm/mail/components/addrbook/test/browser/browser_drag_drop.js
new file mode 100644
index 0000000000..4f3c23aa5b
--- /dev/null
+++ b/comm/mail/components/addrbook/test/browser/browser_drag_drop.js
@@ -0,0 +1,417 @@
+/* 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/. */
+
+let dragService = Cc["@mozilla.org/widget/dragservice;1"].getService(
+ Ci.nsIDragService
+);
+
+function doDrag(sourceIndex, destIndex, modifiers, expectedEffect) {
+ let abWindow = getAddressBookWindow();
+ let booksList = abWindow.document.getElementById("books");
+ let cardsList = abWindow.document.getElementById("cards");
+
+ let destElement = abWindow.document.body;
+ if (destIndex !== null) {
+ destElement = booksList.getRowAtIndex(destIndex);
+ }
+
+ let [result, dataTransfer] = EventUtils.synthesizeDragOver(
+ cardsList.getRowAtIndex(sourceIndex),
+ destElement,
+ null,
+ null,
+ abWindow,
+ abWindow,
+ modifiers
+ );
+
+ Assert.equal(dataTransfer.effectAllowed, "all");
+ Assert.equal(dataTransfer.dropEffect, expectedEffect);
+
+ return [result, dataTransfer];
+}
+
+function doDragToBooksList(sourceIndex, destIndex, modifiers, expectedEffect) {
+ let abWindow = getAddressBookWindow();
+ let booksList = abWindow.document.getElementById("books");
+
+ dragService.startDragSessionForTests(Ci.nsIDragService.DRAGDROP_ACTION_NONE);
+
+ let [result, dataTransfer] = doDrag(
+ sourceIndex,
+ destIndex,
+ modifiers,
+ expectedEffect
+ );
+
+ EventUtils.synthesizeDropAfterDragOver(
+ result,
+ dataTransfer,
+ booksList.getRowAtIndex(destIndex),
+ abWindow,
+ modifiers
+ );
+
+ dragService.endDragSession(true);
+}
+
+async function doDragToComposeWindow(sourceIndices, expectedPills) {
+ let params = Cc[
+ "@mozilla.org/messengercompose/composeparams;1"
+ ].createInstance(Ci.nsIMsgComposeParams);
+ params.composeFields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+
+ let composeWindowPromise = BrowserTestUtils.domWindowOpened();
+ MailServices.compose.OpenComposeWindowWithParams(null, params);
+ let composeWindow = await composeWindowPromise;
+ await BrowserTestUtils.waitForEvent(composeWindow, "load");
+ let composeDocument = composeWindow.document;
+ let toAddrInput = composeDocument.getElementById("toAddrInput");
+ let toAddrRow = composeDocument.getElementById("addressRowTo");
+
+ let abWindow = getAddressBookWindow();
+ let cardsList = abWindow.document.getElementById("cards");
+
+ dragService.startDragSessionForTests(Ci.nsIDragService.DRAGDROP_ACTION_NONE);
+
+ cardsList.selectedIndices = sourceIndices;
+ let [result, dataTransfer] = EventUtils.synthesizeDragOver(
+ cardsList.getRowAtIndex(sourceIndices[0]),
+ toAddrInput,
+ null,
+ null,
+ abWindow,
+ composeWindow
+ );
+ EventUtils.synthesizeDropAfterDragOver(
+ result,
+ dataTransfer,
+ toAddrInput,
+ composeWindow
+ );
+
+ dragService.endDragSession(true);
+
+ let pills = toAddrRow.querySelectorAll("mail-address-pill");
+ Assert.equal(pills.length, expectedPills.length);
+ for (let i = 0; i < expectedPills.length; i++) {
+ Assert.equal(pills[i].label, expectedPills[i]);
+ }
+
+ let promptPromise = BrowserTestUtils.promiseAlertDialog("extra1");
+ composeWindow.goDoCommand("cmd_close");
+ await promptPromise;
+}
+
+function checkCardsInDirectory(directory, expectedCards = [], copiedCard) {
+ let actualCards = directory.childCards.slice();
+
+ for (let card of expectedCards) {
+ let index = actualCards.findIndex(c => c.UID == card.UID);
+ Assert.greaterOrEqual(index, 0);
+ actualCards.splice(index, 1);
+ }
+
+ if (copiedCard) {
+ Assert.equal(actualCards.length, 1);
+ Assert.equal(actualCards[0].firstName, copiedCard.firstName);
+ Assert.equal(actualCards[0].lastName, copiedCard.lastName);
+ Assert.equal(actualCards[0].primaryEmail, copiedCard.primaryEmail);
+ Assert.notEqual(actualCards[0].UID, copiedCard.UID);
+ } else {
+ Assert.equal(actualCards.length, 0);
+ }
+}
+
+add_task(async function test_drag() {
+ let sourceBook = createAddressBook("Source Book");
+
+ let contact1 = sourceBook.addCard(createContact("contact", "1"));
+ let contact2 = sourceBook.addCard(createContact("contact", "2"));
+ let contact3 = sourceBook.addCard(createContact("contact", "3"));
+
+ let abWindow = await openAddressBookWindow();
+ let cardsList = abWindow.document.getElementById("cards");
+
+ // Drag just contact1.
+
+ dragService.startDragSessionForTests(Ci.nsIDragService.DRAGDROP_ACTION_NONE);
+
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(0), {}, abWindow);
+ let [, dataTransfer] = doDrag(0, null, {}, "none");
+
+ let transferCards = dataTransfer.mozGetDataAt("moz/abcard-array", 0);
+ Assert.equal(transferCards.length, 1);
+ Assert.ok(transferCards[0].equals(contact1));
+
+ let transferUnicode = dataTransfer.getData("text/plain");
+ Assert.equal(transferUnicode, "contact 1 <contact.1@invalid>");
+
+ let transferVCard = dataTransfer.getData("text/vcard");
+ Assert.stringContains(transferVCard, `\r\nUID:${contact1.UID}\r\n`);
+
+ dragService.endDragSession(true);
+
+ // Drag contact2 without selecting it.
+
+ dragService.startDragSessionForTests(Ci.nsIDragService.DRAGDROP_ACTION_NONE);
+
+ [, dataTransfer] = doDrag(1, null, {}, "none");
+
+ transferCards = dataTransfer.mozGetDataAt("moz/abcard-array", 0);
+ Assert.equal(transferCards.length, 1);
+ Assert.ok(transferCards[0].equals(contact2));
+
+ transferUnicode = dataTransfer.getData("text/plain");
+ Assert.equal(transferUnicode, "contact 2 <contact.2@invalid>");
+
+ transferVCard = dataTransfer.getData("text/vcard");
+ Assert.stringContains(transferVCard, `\r\nUID:${contact2.UID}\r\n`);
+
+ dragService.endDragSession(true);
+
+ // Drag all contacts.
+
+ dragService.startDragSessionForTests(Ci.nsIDragService.DRAGDROP_ACTION_NONE);
+
+ EventUtils.synthesizeMouseAtCenter(
+ cardsList.getRowAtIndex(2),
+ { shiftKey: true },
+ abWindow
+ );
+ [, dataTransfer] = doDrag(0, null, {}, "none");
+
+ transferCards = dataTransfer.mozGetDataAt("moz/abcard-array", 0);
+ Assert.equal(transferCards.length, 3);
+ Assert.ok(transferCards[0].equals(contact1));
+ Assert.ok(transferCards[1].equals(contact2));
+ Assert.ok(transferCards[2].equals(contact3));
+
+ transferUnicode = dataTransfer.getData("text/plain");
+ Assert.equal(
+ transferUnicode,
+ "contact 1 <contact.1@invalid>,contact 2 <contact.2@invalid>,contact 3 <contact.3@invalid>"
+ );
+
+ transferVCard = dataTransfer.getData("text/vcard");
+ Assert.stringContains(transferVCard, `\r\nUID:${contact1.UID}\r\n`);
+
+ dragService.endDragSession(true);
+
+ await closeAddressBookWindow();
+
+ await promiseDirectoryRemoved(sourceBook.URI);
+});
+
+add_task(async function test_drop_on_books_list() {
+ let sourceBook = createAddressBook("Source Book");
+ let sourceList = sourceBook.addMailList(createMailingList("Source List"));
+ let destBook = createAddressBook("Destination Book");
+ let destList = destBook.addMailList(createMailingList("Destination List"));
+
+ let contact1 = sourceBook.addCard(createContact("contact", "1"));
+ let contact2 = sourceBook.addCard(createContact("contact", "2"));
+ let contact3 = sourceBook.addCard(createContact("contact", "3"));
+
+ let abWindow = await openAddressBookWindow();
+ let booksList = abWindow.document.getElementById("books");
+ let cardsList = abWindow.document.getElementById("cards");
+
+ checkCardsInDirectory(sourceBook, [contact1, contact2, contact3, sourceList]);
+ checkCardsInDirectory(sourceList);
+ checkCardsInDirectory(destBook, [destList]);
+ checkCardsInDirectory(destList);
+
+ Assert.equal(booksList.rowCount, 7);
+ openDirectory(sourceBook);
+
+ // Check drag effect set correctly for dragging a card.
+
+ Assert.equal(cardsList.view.rowCount, 4);
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(0), {}, abWindow);
+
+ dragService.startDragSessionForTests(Ci.nsIDragService.DRAGDROP_ACTION_NONE);
+
+ doDrag(0, 0, {}, "none"); // All Address Books
+ doDrag(0, 0, { ctrlKey: true }, "none");
+
+ doDrag(0, 1, {}, "move"); // Personal Address Book
+ doDrag(0, 1, { ctrlKey: true }, "copy");
+
+ doDrag(0, 2, {}, "move"); // Destination Book
+ doDrag(0, 2, { ctrlKey: true }, "copy");
+
+ doDrag(0, 3, {}, "none"); // Destination List
+ doDrag(0, 3, { ctrlKey: true }, "none");
+
+ doDrag(0, 4, {}, "none"); // Source Book
+ doDrag(0, 4, { ctrlKey: true }, "none");
+
+ doDrag(0, 5, {}, "link"); // Source List
+ doDrag(0, 5, { ctrlKey: true }, "link");
+
+ doDrag(0, 6, {}, "move"); // Collected Addresses
+ doDrag(0, 6, { ctrlKey: true }, "copy");
+
+ dragService.endDragSession(true);
+
+ // Check drag effect set correctly for dragging a list.
+
+ Assert.equal(cardsList.view.rowCount, 4);
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(3), {}, abWindow);
+
+ dragService.startDragSessionForTests(Ci.nsIDragService.DRAGDROP_ACTION_NONE);
+
+ doDrag(3, 0, {}, "none"); // All Address Books
+ doDrag(3, 0, { ctrlKey: true }, "none");
+
+ doDrag(3, 1, {}, "none"); // Personal Address Book
+ doDrag(3, 1, { ctrlKey: true }, "none");
+
+ doDrag(3, 2, {}, "none"); // Destination Book
+ doDrag(3, 2, { ctrlKey: true }, "none");
+
+ doDrag(3, 3, {}, "none"); // Destination List
+ doDrag(3, 3, { ctrlKey: true }, "none");
+
+ doDrag(3, 4, {}, "none"); // Source Book
+ doDrag(3, 4, { ctrlKey: true }, "none");
+
+ doDrag(3, 5, {}, "none"); // Source List
+ doDrag(3, 5, { ctrlKey: true }, "none");
+
+ doDrag(3, 6, {}, "none"); // Collected Addresses
+ doDrag(3, 6, { ctrlKey: true }, "none");
+
+ dragService.endDragSession(true);
+
+ // Drag contact1 into sourceList.
+
+ Assert.equal(cardsList.view.rowCount, 4);
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(0), {}, abWindow);
+
+ doDragToBooksList(0, 5, {}, "link");
+ checkCardsInDirectory(sourceBook, [contact1, contact2, contact3, sourceList]);
+ checkCardsInDirectory(sourceList, [contact1]);
+
+ // Drag contact1 into destList. Nothing should happen.
+
+ doDragToBooksList(0, 3, {}, "none");
+ checkCardsInDirectory(sourceBook, [contact1, contact2, contact3, sourceList]);
+ checkCardsInDirectory(destBook, [destList]);
+ checkCardsInDirectory(destList);
+
+ // Drag contact1 into destBook. It should be moved into destBook.
+
+ doDragToBooksList(0, 2, {}, "move");
+ checkCardsInDirectory(sourceBook, [contact2, contact3, sourceList]);
+ checkCardsInDirectory(sourceList);
+ checkCardsInDirectory(destBook, [contact1, destList]);
+
+ // Drag contact2 into destBook with Ctrl pressed.
+ // It should be copied into destBook.
+
+ Assert.equal(cardsList.view.rowCount, 3);
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(0), {}, abWindow);
+
+ doDragToBooksList(0, 2, { ctrlKey: true }, "copy");
+ checkCardsInDirectory(sourceBook, [contact2, contact3, sourceList]);
+ checkCardsInDirectory(destBook, [contact1, destList], contact2);
+ checkCardsInDirectory(destList);
+
+ // Delete the cards from destBook as it's confusing.
+
+ destBook.deleteCards(destBook.childCards.filter(c => !c.isMailList));
+ checkCardsInDirectory(destBook, [destList]);
+
+ // Drag contact2 and contact3 to destBook.
+
+ Assert.equal(cardsList.view.rowCount, 3);
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(0), {}, abWindow);
+ EventUtils.synthesizeMouseAtCenter(
+ cardsList.getRowAtIndex(1),
+ { shiftKey: true },
+ abWindow
+ );
+
+ doDragToBooksList(0, 2, {}, "move");
+ checkCardsInDirectory(sourceBook, [sourceList]);
+ checkCardsInDirectory(destBook, [contact2, contact3, destList]);
+
+ // Drag contact2 to the book it's already in. Nothing should happen.
+ // This test doesn't actually catch the bug it was written for, but maybe
+ // one day it will catch something.
+
+ openDirectory(destBook);
+ Assert.equal(cardsList.view.rowCount, 3);
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(0), {}, abWindow);
+ doDragToBooksList(0, 2, {}, "none");
+ checkCardsInDirectory(destBook, [contact2, contact3, destList]);
+
+ // Drag destList to the book it's already in. Nothing should happen.
+
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(2), {}, abWindow);
+ doDragToBooksList(2, 2, {}, "none");
+ checkCardsInDirectory(destBook, [contact2, contact3, destList]);
+
+ await closeAddressBookWindow();
+
+ await promiseDirectoryRemoved(sourceBook.URI);
+ await promiseDirectoryRemoved(destBook.URI);
+});
+
+add_task(async function test_drop_on_compose() {
+ MailServices.accounts.createLocalMailAccount();
+ let account = MailServices.accounts.accounts[0];
+ account.addIdentity(MailServices.accounts.createIdentity());
+
+ registerCleanupFunction(async () => {
+ MailServices.accounts.removeAccount(account, true);
+ });
+
+ let sourceBook = createAddressBook("Source Book");
+ let sourceList = sourceBook.addMailList(createMailingList("Source List"));
+
+ let contact1 = sourceBook.addCard(createContact("contact", "1"));
+ let contact2 = sourceBook.addCard(createContact("contact", "2"));
+ let contact3 = sourceBook.addCard(createContact("contact", "3"));
+ sourceList.addCard(contact1);
+ sourceList.addCard(contact2);
+ sourceList.addCard(contact3);
+
+ let abWindow = await openAddressBookWindow();
+ let cardsList = abWindow.document.getElementById("cards");
+ Assert.equal(cardsList.view.rowCount, 4);
+
+ // One contact.
+
+ await doDragToComposeWindow([0], ["contact 1 <contact.1@invalid>"]);
+
+ // Multiple contacts.
+
+ await doDragToComposeWindow(
+ [0, 1, 2],
+ [
+ "contact 1 <contact.1@invalid>",
+ "contact 2 <contact.2@invalid>",
+ "contact 3 <contact.3@invalid>",
+ ]
+ );
+
+ // A mailing list.
+
+ await doDragToComposeWindow([3], [`Source List <"Source List">`]);
+
+ // A mailing list and a contact.
+
+ await doDragToComposeWindow(
+ [3, 2],
+ ["contact 3 <contact.3@invalid>", `Source List <"Source List">`]
+ );
+
+ await closeAddressBookWindow();
+ await promiseDirectoryRemoved(sourceBook.URI);
+});
diff --git a/comm/mail/components/addrbook/test/browser/browser_edit_async.js b/comm/mail/components/addrbook/test/browser/browser_edit_async.js
new file mode 100644
index 0000000000..76588aee76
--- /dev/null
+++ b/comm/mail/components/addrbook/test/browser/browser_edit_async.js
@@ -0,0 +1,363 @@
+/* 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 { CardDAVDirectory } = ChromeUtils.import(
+ "resource:///modules/CardDAVDirectory.jsm"
+);
+const { CardDAVServer } = ChromeUtils.import(
+ "resource://testing-common/CardDAVServer.jsm"
+);
+
+let book;
+
+async function inEditingMode() {
+ let abWindow = getAddressBookWindow();
+ await TestUtils.waitForCondition(
+ () => abWindow.detailsPane.isEditing,
+ "entering editing mode"
+ );
+}
+
+async function notInEditingMode() {
+ let abWindow = getAddressBookWindow();
+ await TestUtils.waitForCondition(
+ () => !abWindow.detailsPane.isEditing,
+ "leaving editing mode"
+ );
+}
+
+add_setup(async function () {
+ CardDAVServer.open("alice", "alice");
+
+ book = createAddressBook(
+ "CardDAV Book",
+ Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE
+ );
+ book.setIntValue("carddav.syncinterval", 0);
+ book.setStringValue("carddav.url", CardDAVServer.url);
+ book.setStringValue("carddav.username", "alice");
+
+ let loginInfo = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
+ Ci.nsILoginInfo
+ );
+ loginInfo.init(CardDAVServer.origin, null, "test", "alice", "alice", "", "");
+ Services.logins.addLogin(loginInfo);
+});
+
+registerCleanupFunction(async function () {
+ await promiseDirectoryRemoved(book.URI);
+ CardDAVServer.close();
+ CardDAVServer.reset();
+ CardDAVServer.modifyCardOnPut = false;
+});
+
+/**
+ * Test the UI as we create/modify/delete a card and wait for responses from
+ * the server.
+ */
+add_task(async function testCreateCard() {
+ let abWindow = await openAddressBookWindow();
+ let abDocument = abWindow.document;
+
+ let createContactButton = abDocument.getElementById("toolbarCreateContact");
+ let bookRow = abWindow.booksList.getRowForUID(book.UID);
+ let searchInput = abDocument.getElementById("searchInput");
+ let editButton = abDocument.getElementById("editButton");
+ let saveEditButton = abDocument.getElementById("saveEditButton");
+ let deleteButton = abDocument.getElementById("detailsDeleteButton");
+
+ openDirectory(book);
+
+ // First, create a new contact.
+
+ EventUtils.synthesizeMouseAtCenter(createContactButton, {}, abWindow);
+ await inEditingMode();
+
+ abWindow.detailsPane.vCardEdit.displayName.value = "new contact";
+
+ // Saving the contact will get an immediate notification.
+ // Delay the server response so we can test the state of the UI.
+ let promise1 = TestUtils.topicObserved("addrbook-contact-created");
+ CardDAVServer.responseDelay = PromiseUtils.defer();
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await promise1;
+ await notInEditingMode();
+ Assert.ok(bookRow.classList.contains("requesting"));
+ Assert.equal(abDocument.activeElement, editButton);
+ Assert.ok(BrowserTestUtils.is_visible(editButton));
+
+ // Now allow the server to respond and check the UI state again.
+ let promise2 = TestUtils.topicObserved("addrbook-contact-updated");
+ CardDAVServer.responseDelay.resolve();
+ await promise2;
+ Assert.ok(!bookRow.classList.contains("requesting"));
+ Assert.equal(abDocument.activeElement, editButton);
+ Assert.ok(BrowserTestUtils.is_visible(editButton));
+
+ // Edit the contact.
+
+ EventUtils.synthesizeMouseAtCenter(editButton, {}, abWindow);
+ await inEditingMode();
+
+ abWindow.detailsPane.vCardEdit.displayName.value = "edited contact";
+
+ // Saving the contact will get an immediate notification.
+ // Delay the server response so we can test the state of the UI.
+ let promise3 = TestUtils.topicObserved("addrbook-contact-updated");
+ CardDAVServer.responseDelay = PromiseUtils.defer();
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await promise3;
+ await notInEditingMode();
+ Assert.ok(bookRow.classList.contains("requesting"));
+ Assert.equal(abDocument.activeElement, editButton);
+ Assert.ok(BrowserTestUtils.is_visible(editButton));
+
+ // Now allow the server to respond and check the UI state again.
+ let promise4 = TestUtils.topicObserved("addrbook-contact-updated");
+ CardDAVServer.responseDelay.resolve();
+ await promise4;
+ Assert.ok(!bookRow.classList.contains("requesting"));
+ Assert.equal(abDocument.activeElement, editButton);
+ Assert.ok(BrowserTestUtils.is_visible(editButton));
+
+ // Delete the contact.
+
+ EventUtils.synthesizeMouseAtCenter(editButton, {}, abWindow);
+ await inEditingMode();
+
+ // Saving the contact will get an immediate notification.
+ // Delay the server response so we can test the state of the UI.
+ let promise5 = TestUtils.topicObserved("addrbook-contact-deleted");
+ CardDAVServer.responseDelay = PromiseUtils.defer();
+ BrowserTestUtils.promiseAlertDialog("accept");
+ EventUtils.synthesizeMouseAtCenter(deleteButton, {}, abWindow);
+ await promise5;
+ await notInEditingMode();
+ Assert.ok(bookRow.classList.contains("requesting"));
+ Assert.equal(abDocument.activeElement, searchInput);
+ Assert.ok(BrowserTestUtils.is_hidden(editButton));
+
+ // Now allow the server to respond and check the UI state again.
+ CardDAVServer.responseDelay.resolve();
+ await TestUtils.waitForCondition(
+ () => !bookRow.classList.contains("requesting")
+ );
+
+ await closeAddressBookWindow();
+});
+
+/**
+ * Test the UI as we create a card and wait for responses from the server.
+ * In this test the server will assign the card a new UID, which means the
+ * client code has to do things differently, but the UI should behave as it
+ * did in the previous test.
+ */
+add_task(async function testCreateCardWithUIDChange() {
+ CardDAVServer.modifyCardOnPut = true;
+
+ let abWindow = await openAddressBookWindow();
+ let abDocument = abWindow.document;
+
+ let createContactButton = abDocument.getElementById("toolbarCreateContact");
+ let bookRow = abWindow.booksList.getRowForUID(book.UID);
+ let searchInput = abDocument.getElementById("searchInput");
+ let editButton = abDocument.getElementById("editButton");
+ let saveEditButton = abDocument.getElementById("saveEditButton");
+ let deleteButton = abDocument.getElementById("detailsDeleteButton");
+
+ openDirectory(book);
+
+ // First, create a new contact.
+
+ EventUtils.synthesizeMouseAtCenter(createContactButton, {}, abWindow);
+ await inEditingMode();
+
+ abWindow.detailsPane.vCardEdit.displayName.value = "new contact";
+
+ // Saving the contact will get an immediate notification.
+ // Delay the server response so we can test the state of the UI.
+ let promise1 = TestUtils.topicObserved("addrbook-contact-created");
+ CardDAVServer.responseDelay = PromiseUtils.defer();
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await promise1;
+ await notInEditingMode();
+ Assert.ok(bookRow.classList.contains("requesting"));
+ Assert.equal(abDocument.activeElement, editButton);
+ Assert.ok(BrowserTestUtils.is_visible(editButton));
+
+ let initialCard = abWindow.detailsPane.currentCard;
+ Assert.equal(initialCard.getProperty("_href", "RIGHT"), "RIGHT");
+
+ // Now allow the server to respond and check the UI state again.
+ let promise2 = TestUtils.topicObserved("addrbook-contact-created");
+ let promise3 = TestUtils.topicObserved("addrbook-contact-deleted");
+ CardDAVServer.responseDelay.resolve();
+ let [changedCard] = await promise2;
+ let [deletedCard] = await promise3;
+ Assert.ok(!bookRow.classList.contains("requesting"));
+ Assert.equal(abDocument.activeElement, editButton);
+ Assert.ok(BrowserTestUtils.is_visible(editButton));
+
+ Assert.equal(changedCard.UID, [...initialCard.UID].reverse().join(""));
+ Assert.equal(
+ changedCard.getProperty("_originalUID", "WRONG"),
+ initialCard.UID
+ );
+ Assert.equal(deletedCard.UID, initialCard.UID);
+
+ let displayedCard = abWindow.detailsPane.currentCard;
+ Assert.equal(displayedCard.directoryUID, book.UID);
+ Assert.notEqual(displayedCard.getProperty("_href", "WRONG"), "WRONG");
+ Assert.equal(displayedCard.UID, [...initialCard.UID].reverse().join(""));
+
+ // Delete the contact. This would fail if the UI hadn't been updated.
+
+ EventUtils.synthesizeMouseAtCenter(editButton, {}, abWindow);
+ await inEditingMode();
+
+ // Saving the contact will get an immediate notification.
+ // Delay the server response so we can test the state of the UI.
+ let promise4 = TestUtils.topicObserved("addrbook-contact-deleted");
+ CardDAVServer.responseDelay = PromiseUtils.defer();
+ BrowserTestUtils.promiseAlertDialog("accept");
+ EventUtils.synthesizeMouseAtCenter(deleteButton, {}, abWindow);
+ await promise4;
+ await notInEditingMode();
+ Assert.ok(bookRow.classList.contains("requesting"));
+ Assert.equal(abDocument.activeElement, searchInput);
+ Assert.ok(BrowserTestUtils.is_hidden(editButton));
+
+ // Now allow the server to respond and check the UI state again.
+ CardDAVServer.responseDelay.resolve();
+ await TestUtils.waitForCondition(
+ () => !bookRow.classList.contains("requesting")
+ );
+
+ await closeAddressBookWindow();
+ CardDAVServer.modifyCardOnPut = false;
+});
+
+/**
+ * Test that a modification to the card being edited causes a prompt to appear
+ * when saving the card.
+ */
+add_task(async function testModificationUpdatesUI() {
+ let card = personalBook.addCard(createContact("a", "person"));
+
+ let abWindow = await openAddressBookWindow();
+ let abDocument = abWindow.document;
+
+ let cardsList = abDocument.getElementById("cards");
+ let detailsPane = abDocument.getElementById("detailsPane");
+ let contactName = abDocument.getElementById("viewContactName");
+ let editButton = abDocument.getElementById("editButton");
+ let emailAddressesSection = abDocument.getElementById("emailAddresses");
+ let saveEditButton = abDocument.getElementById("saveEditButton");
+ let cancelEditButton = abDocument.getElementById("cancelEditButton");
+
+ openDirectory(personalBook);
+ Assert.equal(cardsList.view.rowCount, 1);
+
+ // Display a card.
+
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(0), {}, abWindow);
+ await TestUtils.waitForCondition(() =>
+ BrowserTestUtils.is_visible(detailsPane)
+ );
+
+ Assert.equal(contactName.textContent, "a person");
+ Assert.ok(BrowserTestUtils.is_visible(emailAddressesSection));
+ let items = emailAddressesSection.querySelectorAll("li");
+ Assert.equal(items.length, 1);
+ Assert.equal(items[0].querySelector("a").textContent, "a.person@invalid");
+
+ // Modify the card and check the display is updated.
+
+ let updatePromise = BrowserTestUtils.waitForMutationCondition(
+ detailsPane,
+ { childList: true, subtree: true },
+ () => true
+ );
+ card.vCardProperties.addValue("email", "person.a@lastfirst.invalid");
+ personalBook.modifyCard(card);
+
+ await updatePromise;
+ Assert.equal(contactName.textContent, "a person");
+ Assert.ok(BrowserTestUtils.is_visible(emailAddressesSection));
+ items = emailAddressesSection.querySelectorAll("li");
+ Assert.equal(items.length, 2);
+ Assert.equal(items[0].querySelector("a").textContent, "a.person@invalid");
+ Assert.equal(
+ items[1].querySelector("a").textContent,
+ "person.a@lastfirst.invalid"
+ );
+
+ // Enter edit mode. Clear one of the email addresses.
+
+ EventUtils.synthesizeMouseAtCenter(editButton, {}, abWindow);
+ await inEditingMode();
+ Assert.equal(abWindow.detailsPane.vCardEdit.displayName.value, "a person");
+ abDocument.querySelector(`#vcard-email tr input[type="email"]`).value = "";
+
+ // Modify the card. Nothing should happen at this point.
+
+ card.displayName = "a different person";
+ personalBook.modifyCard(card);
+
+ // Click to save.
+
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode();
+
+ [card] = personalBook.childCards;
+ Assert.equal(
+ card.displayName,
+ "a person",
+ "programmatic changes were overwritten"
+ );
+ Assert.deepEqual(
+ card.emailAddresses,
+ ["person.a@lastfirst.invalid"],
+ "UI changes were saved"
+ );
+
+ Assert.equal(contactName.textContent, "a person");
+ Assert.ok(BrowserTestUtils.is_visible(emailAddressesSection));
+ items = emailAddressesSection.querySelectorAll("li");
+ Assert.equal(items.length, 1);
+ Assert.equal(
+ items[0].querySelector("a").textContent,
+ "person.a@lastfirst.invalid"
+ );
+
+ // Enter edit mode again. Change the display name.
+
+ EventUtils.synthesizeMouseAtCenter(editButton, {}, abWindow);
+ await inEditingMode();
+ abWindow.detailsPane.vCardEdit.displayName.value = "a changed person";
+
+ // Modify the card. Nothing should happen at this point.
+
+ card.displayName = "a different person";
+ card.vCardProperties.addValue("email", "a.person@invalid");
+ personalBook.modifyCard(card);
+
+ // Click to cancel. The modified card should be shown.
+
+ EventUtils.synthesizeMouseAtCenter(cancelEditButton, {}, abWindow);
+ await notInEditingMode();
+
+ Assert.equal(contactName.textContent, "a different person");
+ Assert.ok(BrowserTestUtils.is_visible(emailAddressesSection));
+ items = emailAddressesSection.querySelectorAll("li");
+ Assert.equal(items.length, 2);
+ Assert.equal(
+ items[0].querySelector("a").textContent,
+ "person.a@lastfirst.invalid"
+ );
+ Assert.equal(items[1].querySelector("a").textContent, "a.person@invalid");
+
+ await closeAddressBookWindow();
+ personalBook.deleteCards(personalBook.childCards);
+});
diff --git a/comm/mail/components/addrbook/test/browser/browser_edit_card.js b/comm/mail/components/addrbook/test/browser/browser_edit_card.js
new file mode 100644
index 0000000000..27cabfa4d4
--- /dev/null
+++ b/comm/mail/components/addrbook/test/browser/browser_edit_card.js
@@ -0,0 +1,3517 @@
+/* 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/. */
+
+var { VCardUtils } = ChromeUtils.import("resource:///modules/VCardUtils.jsm");
+var { AddrBookCard } = ChromeUtils.import(
+ "resource:///modules/AddrBookCard.jsm"
+);
+
+requestLongerTimeout(2);
+
+async function inEditingMode() {
+ let abWindow = getAddressBookWindow();
+ let abDocument = abWindow.document;
+
+ await TestUtils.waitForCondition(
+ () => abWindow.detailsPane.isEditing,
+ "Waiting on entering editing mode"
+ );
+
+ Assert.ok(
+ BrowserTestUtils.is_visible(
+ abDocument.getElementById("detailsPaneBackdrop")
+ ),
+ "backdrop should be visible"
+ );
+ checkToolbarState(false);
+}
+
+/**
+ * Wait until we are no longer in editing mode.
+ *
+ * @param {Element} expectedFocus - The element that is expected to have focus
+ * after leaving editing.
+ */
+async function notInEditingMode(expectedFocus) {
+ let abWindow = getAddressBookWindow();
+ let abDocument = abWindow.document;
+
+ await TestUtils.waitForCondition(
+ () => !abWindow.detailsPane.isEditing,
+ "leaving editing mode"
+ );
+ await new Promise(resolve => abWindow.setTimeout(resolve));
+
+ Assert.ok(
+ BrowserTestUtils.is_hidden(
+ abDocument.getElementById("detailsPaneBackdrop")
+ ),
+ "backdrop should be hidden"
+ );
+ checkToolbarState(true);
+ Assert.equal(
+ abDocument.activeElement,
+ expectedFocus,
+ `Focus should be on #${expectedFocus.id}`
+ );
+}
+
+function getInput(entryName, addIfNeeded = false) {
+ let abWindow = getAddressBookWindow();
+ let abDocument = abWindow.document;
+
+ switch (entryName) {
+ case "DisplayName":
+ return abDocument.querySelector("vcard-fn #vCardDisplayName");
+ case "PreferDisplayName":
+ return abDocument.querySelector("vcard-fn #vCardPreferDisplayName");
+ case "NickName":
+ return abDocument.querySelector("vcard-nickname #vCardNickName");
+ case "Prefix":
+ let prefixInput = abDocument.querySelector("vcard-n #vcard-n-prefix");
+ if (addIfNeeded && BrowserTestUtils.is_hidden(prefixInput)) {
+ EventUtils.synthesizeMouseAtCenter(
+ abDocument.querySelector("vcard-n #n-list-component-prefix button"),
+ {},
+ abWindow
+ );
+ }
+ return prefixInput;
+ case "FirstName":
+ return abDocument.querySelector("vcard-n #vcard-n-firstname");
+ case "MiddleName":
+ let middleNameInput = abDocument.querySelector(
+ "vcard-n #vcard-n-middlename"
+ );
+ if (addIfNeeded && BrowserTestUtils.is_hidden(middleNameInput)) {
+ EventUtils.synthesizeMouseAtCenter(
+ abDocument.querySelector(
+ "vcard-n #n-list-component-middlename button"
+ ),
+ {},
+ abWindow
+ );
+ }
+ return middleNameInput;
+ case "LastName":
+ return abDocument.querySelector("vcard-n #vcard-n-lastname");
+ case "Suffix":
+ let suffixInput = abDocument.querySelector("vcard-n #vcard-n-suffix");
+ if (addIfNeeded && BrowserTestUtils.is_hidden(suffixInput)) {
+ EventUtils.synthesizeMouseAtCenter(
+ abDocument.querySelector("vcard-n #n-list-component-suffix button"),
+ {},
+ abWindow
+ );
+ }
+ return suffixInput;
+ case "PrimaryEmail":
+ if (
+ addIfNeeded &&
+ abDocument.getElementById("vcard-email").children.length < 1
+ ) {
+ let addButton = abDocument.getElementById("vcard-add-email");
+ addButton.scrollIntoView({ block: "nearest" });
+ EventUtils.synthesizeMouseAtCenter(addButton, {}, abWindow);
+ }
+ return abDocument.querySelector(
+ `#vcard-email tr:nth-child(1) input[type="email"]`
+ );
+ case "PrimaryEmailCheckbox":
+ return getInput("PrimaryEmail")
+ .closest(`tr`)
+ .querySelector(`input[type="checkbox"]`);
+ case "SecondEmail":
+ if (
+ addIfNeeded &&
+ abDocument.getElementById("vcard-email").children.length < 2
+ ) {
+ let addButton = abDocument.getElementById("vcard-add-email");
+ addButton.scrollIntoView({ block: "nearest" });
+ EventUtils.synthesizeMouseAtCenter(addButton, {}, abWindow);
+ }
+ return abDocument.querySelector(
+ `#vcard-email tr:nth-child(2) input[type="email"]`
+ );
+ case "SecondEmailCheckbox":
+ return getInput("SecondEmail")
+ .closest(`tr`)
+ .querySelector(`input[type="checkbox"]`);
+ }
+
+ return null;
+}
+
+function getFields(entryName, addIfNeeded = false, count) {
+ let abWindow = getAddressBookWindow();
+ let abDocument = abWindow.document;
+
+ let fieldsSelector;
+ let addButtonId;
+ let expectFocusSelector;
+ switch (entryName) {
+ case "email":
+ fieldsSelector = `#vcard-email tr`;
+ addButtonId = "vcard-add-email";
+ expectFocusSelector = "tr:last-of-type .vcard-type-selection";
+ break;
+ case "impp":
+ fieldsSelector = "vcard-impp";
+ addButtonId = "vcard-add-impp";
+ expectFocusSelector = "vcard-impp:last-of-type select";
+ break;
+ case "url":
+ fieldsSelector = "vcard-url";
+ addButtonId = "vcard-add-url";
+ expectFocusSelector = "vcard-url:last-of-type .vcard-type-selection";
+ break;
+ case "tel":
+ fieldsSelector = "vcard-tel";
+ addButtonId = "vcard-add-tel";
+ expectFocusSelector = "vcard-tel:last-of-type .vcard-type-selection";
+ break;
+ case "note":
+ fieldsSelector = "vcard-note";
+ addButtonId = "vcard-add-note";
+ expectFocusSelector = "vcard-note:last-of-type textarea";
+ break;
+ case "title":
+ fieldsSelector = "vcard-title";
+ addButtonId = "vcard-add-org";
+ expectFocusSelector = "vcard-title:last-of-type input";
+ break;
+ case "custom":
+ fieldsSelector = "vcard-custom";
+ addButtonId = "vcard-add-custom";
+ expectFocusSelector = "vcard-custom:last-of-type input";
+ break;
+ case "specialDate":
+ fieldsSelector = "vcard-special-date";
+ addButtonId = "vcard-add-bday-anniversary";
+ expectFocusSelector =
+ "vcard-special-date:last-of-type .vcard-type-selection";
+ break;
+ case "adr":
+ fieldsSelector = "vcard-adr";
+ addButtonId = "vcard-add-adr";
+ expectFocusSelector = "vcard-adr:last-of-type .vcard-type-selection";
+ break;
+ case "tz":
+ fieldsSelector = "vcard-tz";
+ addButtonId = "vcard-add-tz";
+ expectFocusSelector = "vcard-tz:last-of-type select";
+ break;
+ case "org":
+ fieldsSelector = "vcard-org";
+ addButtonId = "vcard-add-org";
+ expectFocusSelector = "#addr-book-edit-org input";
+ break;
+ case "role":
+ fieldsSelector = "vcard-role";
+ addButtonId = "vcard-add-org";
+ expectFocusSelector = "#addr-book-edit-org input";
+ break;
+ default:
+ throw new Error("entryName not found: " + entryName);
+ }
+ let fields = abDocument.querySelectorAll(fieldsSelector).length;
+ if (addIfNeeded && fields < count) {
+ let addButton = abDocument.getElementById(addButtonId);
+ for (let clickTimes = fields; clickTimes < count; clickTimes++) {
+ addButton.focus();
+ EventUtils.synthesizeKey("KEY_Enter", {}, abWindow);
+ let expectFocus = abDocument.querySelector(expectFocusSelector);
+ Assert.ok(
+ expectFocus,
+ `Expected focus element should now exist for ${entryName}`
+ );
+ Assert.ok(
+ BrowserTestUtils.is_visible(expectFocus),
+ `Expected focus element for ${entryName} should be visible`
+ );
+ Assert.equal(
+ expectFocus,
+ abDocument.activeElement,
+ `Expected focus element for ${entryName} should be active`
+ );
+ }
+ }
+ return abDocument.querySelectorAll(fieldsSelector);
+}
+
+function checkToolbarState(shouldBeEnabled) {
+ let abWindow = getAddressBookWindow();
+ let abDocument = abWindow.document;
+
+ for (let id of [
+ "toolbarCreateBook",
+ "toolbarCreateContact",
+ "toolbarCreateList",
+ "toolbarImport",
+ ]) {
+ Assert.equal(
+ abDocument.getElementById(id).disabled,
+ !shouldBeEnabled,
+ id + (!shouldBeEnabled ? " should not" : " should") + " be disabled"
+ );
+ }
+}
+
+function checkDisplayValues(expected) {
+ let abWindow = getAddressBookWindow();
+
+ for (let [key, values] of Object.entries(expected)) {
+ let section = abWindow.document.getElementById(key);
+ let items = Array.from(
+ section.querySelectorAll("li .entry-value"),
+ li => li.textContent
+ );
+ Assert.deepEqual(items, values);
+ }
+}
+
+function checkInputValues(expected) {
+ for (let [key, value] of Object.entries(expected)) {
+ let input = getInput(key, !!value);
+ if (!input) {
+ Assert.ok(!value, `${key} input exists to put a value in`);
+ continue;
+ }
+
+ Assert.ok(BrowserTestUtils.is_visible(input));
+ if (input.type == "checkbox") {
+ Assert.equal(input.checked, value, `${key} checked`);
+ } else {
+ Assert.equal(input.value, value, `${key} value`);
+ }
+ }
+}
+
+function checkVCardInputValues(expected) {
+ for (let [key, expectedEntries] of Object.entries(expected)) {
+ let fields = getFields(key, false, expectedEntries.length);
+
+ Assert.equal(
+ fields.length,
+ expectedEntries.length,
+ `${key} occurred ${fields.length} time(s) and ${expectedEntries.length} time(s) is expected.`
+ );
+
+ for (let [index, field] of fields.entries()) {
+ let expectedEntry = expectedEntries[index];
+ let valueField;
+ let typeField;
+ switch (key) {
+ case "email":
+ valueField = field.emailEl;
+ typeField = field.vCardType.selectEl;
+ break;
+ case "impp":
+ valueField = field.imppEl;
+ break;
+ case "url":
+ valueField = field.urlEl;
+ typeField = field.vCardType.selectEl;
+ break;
+ case "tel":
+ valueField = field.inputElement;
+ typeField = field.vCardType.selectEl;
+ break;
+ case "note":
+ valueField = field.textAreaEl;
+ break;
+ case "title":
+ valueField = field.titleEl;
+ break;
+ case "specialDate":
+ Assert.equal(
+ expectedEntry.value[0],
+ field.year.value,
+ `Year value of ${key} at position ${index}`
+ );
+ Assert.equal(
+ expectedEntry.value[1],
+ field.month.value,
+ `Month value of ${key} at position ${index}`
+ );
+ Assert.equal(
+ expectedEntry.value[2],
+ field.day.value,
+ `Day value of ${key} at position ${index}`
+ );
+ break;
+ case "adr":
+ typeField = field.vCardType.selectEl;
+ let addressValue = [
+ field.streetEl.value,
+ field.localityEl.value,
+ field.regionEl.value,
+ field.codeEl.value,
+ field.countryEl.value,
+ ];
+
+ Assert.deepEqual(
+ expectedEntry.value,
+ addressValue,
+ `Value of ${key} at position ${index}`
+ );
+ break;
+ case "tz":
+ valueField = field.selectEl;
+ break;
+ case "org":
+ let orgValue = [field.orgEl.value];
+ if (field.unitEl.value) {
+ orgValue.push(field.unitEl.value);
+ }
+ Assert.deepEqual(
+ expectedEntry.value,
+ orgValue,
+ `Value of ${key} at position ${index}`
+ );
+ break;
+ case "role":
+ valueField = field.roleEl;
+ break;
+ }
+
+ // Check the input value of the field.
+ if (valueField) {
+ Assert.equal(
+ expectedEntry.value,
+ valueField.value,
+ `Value of ${key} at position ${index}`
+ );
+ }
+
+ // Check the type of the field.
+ if (expectedEntry.type || typeField) {
+ Assert.equal(
+ expectedEntry.type || "",
+ typeField.value,
+ `Type of ${key} at position ${index}`
+ );
+ }
+ }
+ }
+}
+
+function checkCardValues(card, expected) {
+ for (let [key, value] of Object.entries(expected)) {
+ if (value) {
+ Assert.equal(
+ card.getProperty(key, "WRONG!"),
+ value,
+ `${key} has the right value`
+ );
+ } else {
+ Assert.equal(
+ card.getProperty(key, "RIGHT!"),
+ "RIGHT!",
+ `${key} has no value`
+ );
+ }
+ }
+}
+
+function checkVCardValues(card, expected) {
+ for (let [key, expectedEntries] of Object.entries(expected)) {
+ let cardValues = card.vCardProperties.getAllEntries(key);
+
+ Assert.equal(
+ expectedEntries.length,
+ cardValues.length,
+ `${key} is expected to occur ${expectedEntries.length} time(s) and ${cardValues.length} time(s) is found.`
+ );
+
+ for (let [index, entry] of cardValues.entries()) {
+ let expectedEntry = expectedEntries[index];
+
+ Assert.deepEqual(
+ expectedEntry.value,
+ entry.value,
+ `Value of ${key} at position ${index}`
+ );
+
+ if (entry.params.type || expectedEntry.type) {
+ Assert.equal(
+ expectedEntry.type,
+ entry.params.type,
+ `Type of ${key} at position ${index}`
+ );
+ }
+
+ if (entry.params.pref || expectedEntry.pref) {
+ Assert.equal(
+ expectedEntry.pref,
+ entry.params.pref,
+ `Pref of ${key} at position ${index}`
+ );
+ }
+ }
+ }
+}
+
+function setInputValues(changes) {
+ let abWindow = getAddressBookWindow();
+
+ for (let [key, value] of Object.entries(changes)) {
+ let input = getInput(key, !!value);
+ if (!input) {
+ Assert.ok(!value, `${key} input exists to put a value in`);
+ continue;
+ }
+
+ if (input.type == "checkbox") {
+ EventUtils.synthesizeMouseAtCenter(input, {}, abWindow);
+ Assert.equal(
+ input.checked,
+ value,
+ `${key} ${value ? "checked" : "unchecked"}`
+ );
+ } else {
+ input.select();
+ if (value) {
+ EventUtils.sendString(value);
+ } else {
+ EventUtils.synthesizeKey("VK_BACK_SPACE", {}, abWindow);
+ }
+ }
+ }
+ EventUtils.synthesizeKey("VK_TAB", {}, abWindow);
+}
+
+/**
+ * Uses EventUtils.synthesizeMouseAtCenter and XULPopup.activateItem to
+ * activate optionValue from the select element typeField.
+ *
+ * @param {HTMLSelectElement} typeField Select element.
+ * @param {string} optionValue The value attribute of the option element from
+ * typeField.
+ */
+async function activateTypeSelect(typeField, optionValue) {
+ let abWindow = getAddressBookWindow();
+ // Ensure that the select field is inside the viewport.
+ typeField.scrollIntoView({ block: "nearest" });
+ let shownPromise = BrowserTestUtils.waitForSelectPopupShown(window);
+ EventUtils.synthesizeMouseAtCenter(typeField, {}, abWindow);
+ let selectPopup = await shownPromise;
+
+ // Get the index of the optionValue from typeField
+ let index = Array.from(typeField.children).findIndex(
+ child => child.value === optionValue
+ );
+ Assert.ok(index >= 0, "Type in select field found");
+
+ // No change event is fired if the same option is activated.
+ if (index === typeField.selectedIndex) {
+ let popupHidden = BrowserTestUtils.waitForEvent(selectPopup, "popuphidden");
+ selectPopup.hidePopup();
+ await popupHidden;
+ return;
+ }
+
+ // The change event saves the vCard value.
+ let changeEvent = BrowserTestUtils.waitForEvent(typeField, "change");
+ selectPopup.activateItem(selectPopup.children[index]);
+ await changeEvent;
+}
+
+async function setVCardInputValues(changes) {
+ let abWindow = getAddressBookWindow();
+
+ for (let [key, entries] of Object.entries(changes)) {
+ let fields = getFields(key, true, entries.length);
+ // Somehow prevents an error on macOS when using <select> widgets that
+ // have just been added.
+ await new Promise(resolve => abWindow.setTimeout(resolve, 250));
+
+ for (let [index, field] of fields.entries()) {
+ let changeEntry = entries[index];
+ let valueField;
+ let typeField;
+ switch (key) {
+ case "email":
+ valueField = field.emailEl;
+ typeField = field.vCardType.selectEl;
+
+ if (
+ (field.checkboxEl.checked && changeEntry && !changeEntry.pref) ||
+ (!field.checkboxEl.checked &&
+ changeEntry &&
+ changeEntry.pref == "1")
+ ) {
+ field.checkboxEl.scrollIntoView({ block: "nearest" });
+ EventUtils.synthesizeMouseAtCenter(field.checkboxEl, {}, abWindow);
+ }
+ break;
+ case "impp":
+ valueField = field.imppEl;
+ break;
+ case "url":
+ valueField = field.urlEl;
+ typeField = field.vCardType.selectEl;
+ break;
+ case "tel":
+ valueField = field.inputElement;
+ typeField = field.vCardType.selectEl;
+ break;
+ case "note":
+ valueField = field.textAreaEl;
+ break;
+ case "specialDate":
+ if (changeEntry && changeEntry.value) {
+ field.month.value = changeEntry.value[1];
+ field.day.value = changeEntry.value[2];
+ field.year.value = changeEntry.value[0];
+ } else {
+ field.month.value = "";
+ field.day.value = "";
+ field.year.value = "";
+ }
+
+ if (changeEntry && changeEntry.key === "bday") {
+ field.selectEl.value = "bday";
+ } else {
+ field.selectEl.value = "anniversary";
+ }
+ break;
+ case "adr":
+ typeField = field.vCardType.selectEl;
+
+ for (let [index, input] of [
+ field.streetEl,
+ field.localityEl,
+ field.regionEl,
+ field.codeEl,
+ field.countryEl,
+ ].entries()) {
+ input.select();
+ if (
+ changeEntry &&
+ Array.isArray(changeEntry.value) &&
+ changeEntry.value[index]
+ ) {
+ EventUtils.sendString(changeEntry.value[index]);
+ } else {
+ EventUtils.synthesizeKey("VK_BACK_SPACE", {}, abWindow);
+ }
+ }
+ break;
+ case "tz":
+ if (changeEntry && changeEntry.value) {
+ field.selectEl.value = changeEntry.value;
+ } else {
+ field.selectEl.value = "";
+ }
+ break;
+ case "title":
+ valueField = field.titleEl;
+ break;
+ case "org":
+ for (let [index, input] of [field.orgEl, field.unitEl].entries()) {
+ input.select();
+ if (
+ changeEntry &&
+ Array.isArray(changeEntry.value) &&
+ changeEntry.value[index]
+ ) {
+ EventUtils.sendString(changeEntry.value[index]);
+ } else {
+ EventUtils.synthesizeKey("VK_BACK_SPACE", {}, abWindow);
+ }
+ }
+ break;
+ case "role":
+ valueField = field.roleEl;
+ break;
+ case "custom":
+ valueField = field.querySelector("vcard-custom:last-of-type input");
+ break;
+ }
+
+ if (valueField) {
+ valueField.select();
+ if (changeEntry && changeEntry.value) {
+ EventUtils.sendString(changeEntry.value);
+ } else {
+ EventUtils.synthesizeKey("VK_BACK_SPACE", {}, abWindow);
+ }
+ }
+
+ if (typeField && changeEntry && changeEntry.type) {
+ await activateTypeSelect(typeField, changeEntry.type);
+ } else if (typeField) {
+ await activateTypeSelect(typeField, "");
+ }
+ }
+ }
+ EventUtils.synthesizeKey("VK_TAB", {}, abWindow);
+}
+
+/**
+ * Open the contact at the given index in the #cards element.
+ *
+ * @param {number} index - The index of the contact to edit.
+ * @param {object} options - Options for how the contact is selected for
+ * editing.
+ * @param {boolean} options.useMouse - Whether to use mouse events to select the
+ * contact. Otherwise uses keyboard events.
+ * @param {boolean} options.useActivate - Whether to activate the contact for
+ * editing directly from the #cards list using "Enter" or double click.
+ * Otherwise uses the "Edit" button in the contact display.
+ */
+async function editContactAtIndex(index, options) {
+ let abWindow = getAddressBookWindow();
+ let abDocument = abWindow.document;
+ let cardsList = abDocument.getElementById("cards");
+ let detailsPane = abDocument.getElementById("detailsPane");
+ let editButton = abDocument.getElementById("editButton");
+
+ let selectHandler = {
+ seenEvent: null,
+ selectedAtEvent: null,
+
+ reset() {
+ this.seenEvent = null;
+ this.selectedAtEvent = null;
+ },
+ handleEvent(event) {
+ this.seenEvent = event;
+ this.selectedAtEvent = cardsList.selectedIndex;
+ },
+ };
+
+ if (!options.useMouse) {
+ cardsList.table.body.focus();
+ if (cardsList.currentIndex != index) {
+ selectHandler.reset();
+ cardsList.addEventListener("select", selectHandler, { once: true });
+ EventUtils.synthesizeKey("KEY_Home", {}, abWindow);
+ for (let i = 0; i < index; i++) {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, abWindow);
+ }
+ await TestUtils.waitForCondition(
+ () => selectHandler.seenEvent,
+ `'select' event should get fired`
+ );
+ }
+ }
+
+ if (options.useActivate) {
+ if (options.useMouse) {
+ EventUtils.synthesizeMouseAtCenter(
+ cardsList.getRowAtIndex(index),
+ { clickCount: 1 },
+ abWindow
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ cardsList.getRowAtIndex(index),
+ { clickCount: 2 },
+ abWindow
+ );
+ } else {
+ EventUtils.synthesizeKey("KEY_Enter", {}, abWindow);
+ }
+ } else {
+ if (options.useMouse) {
+ EventUtils.synthesizeMouseAtCenter(
+ cardsList.getRowAtIndex(index),
+ {},
+ abWindow
+ );
+ }
+
+ await TestUtils.waitForCondition(() =>
+ BrowserTestUtils.is_visible(detailsPane)
+ );
+
+ if (options.useMouse) {
+ EventUtils.synthesizeMouseAtCenter(editButton, {}, abWindow);
+ } else {
+ while (abDocument.activeElement != editButton) {
+ EventUtils.synthesizeKey("KEY_Tab", {}, abWindow);
+ }
+ EventUtils.synthesizeKey(" ", {}, abWindow);
+ }
+ }
+
+ await inEditingMode();
+}
+
+add_task(async function test_basic_edit() {
+ let book = createAddressBook("Test Book");
+ book.addCard(createContact("contact", "1"));
+
+ let abWindow = await openAddressBookWindow();
+ openDirectory(book);
+
+ let abDocument = abWindow.document;
+ let booksList = abDocument.getElementById("books");
+ let cardsList = abDocument.getElementById("cards");
+ let detailsPane = abDocument.getElementById("detailsPane");
+ let editButton = abDocument.getElementById("editButton");
+ let cancelEditButton = abDocument.getElementById("cancelEditButton");
+ let saveEditButton = abDocument.getElementById("saveEditButton");
+
+ let viewContactName = abDocument.getElementById("viewContactName");
+ let viewContactNickName = abDocument.getElementById("viewContactNickName");
+ let viewContactEmail = abDocument.getElementById("viewPrimaryEmail");
+ let editContactName = abDocument.getElementById("editContactHeadingName");
+ let editContactNickName = abDocument.getElementById(
+ "editContactHeadingNickName"
+ );
+ let editContactEmail = abDocument.getElementById("editContactHeadingEmail");
+
+ /**
+ * Assert that the heading has the expected text content and visibility.
+ *
+ * @param {Element} headingEl - The heading to test.
+ * @param {string} expect - The expected text content. If this is "", the
+ * heading is expected to be hidden as well.
+ */
+ function assertHeading(headingEl, expect) {
+ Assert.equal(
+ headingEl.textContent,
+ expect,
+ `Heading ${headingEl.id} content should match`
+ );
+ if (expect) {
+ Assert.ok(
+ BrowserTestUtils.is_visible(headingEl),
+ `Heading ${headingEl.id} should be visible`
+ );
+ } else {
+ Assert.ok(
+ BrowserTestUtils.is_hidden(headingEl),
+ `Heading ${headingEl.id} should be visible`
+ );
+ }
+ }
+
+ /**
+ * Assert the headings shown in the contact view page.
+ *
+ * @param {string} name - The expected name, or an empty string if none is
+ * expected.
+ * @param {string} nickname - The expected nickname, or an empty string if
+ * none is expected.
+ * @param {string} email - The expected email, or an empty string if none is
+ * expected.
+ */
+ function assertViewHeadings(name, nickname, email) {
+ assertHeading(viewContactName, name);
+ assertHeading(viewContactNickName, nickname);
+ assertHeading(viewContactEmail, email);
+ }
+
+ /**
+ * Assert the headings shown in the contact edit page.
+ *
+ * @param {string} name - The expected name, or an empty string if none is
+ * expected.
+ * @param {string} nickname - The expected nickname, or an empty string if
+ * none is expected.
+ * @param {string} email - The expected email, or an empty string if none is
+ * expected.
+ */
+ function assertEditHeadings(name, nickname, email) {
+ assertHeading(editContactName, name);
+ assertHeading(editContactNickName, nickname);
+ assertHeading(editContactEmail, email);
+ }
+
+ Assert.ok(detailsPane.hidden);
+ Assert.ok(!document.querySelector("vcard-n"));
+ Assert.ok(!abDocument.getElementById("vcard-email").children.length);
+
+ // Select a card in the list. Check the display in view mode.
+
+ Assert.equal(cardsList.view.rowCount, 1);
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(0), {}, abWindow);
+ await TestUtils.waitForCondition(() =>
+ BrowserTestUtils.is_visible(detailsPane)
+ );
+
+ assertViewHeadings("contact 1", "", "contact.1@invalid");
+
+ Assert.ok(BrowserTestUtils.is_visible(editButton));
+ Assert.ok(BrowserTestUtils.is_hidden(cancelEditButton));
+ Assert.ok(BrowserTestUtils.is_hidden(saveEditButton));
+
+ checkDisplayValues({
+ emailAddresses: ["contact.1@invalid"],
+ });
+
+ // Click to edit.
+
+ EventUtils.synthesizeMouseAtCenter(editButton, {}, abWindow);
+ await inEditingMode();
+
+ // Try to trigger the creation of a new contact while in edit mode.
+ EventUtils.synthesizeKey("n", { ctrlKey: true }, abWindow);
+
+ // Headings reflect initial values and shouldn't have changed.
+ assertEditHeadings("contact 1", "", "contact.1@invalid");
+
+ // Check that pressing Tab can't get us stuck on an element that shouldn't
+ // have focus.
+
+ abDocument.documentElement.focus();
+ Assert.equal(
+ abDocument.activeElement,
+ abDocument.documentElement,
+ "focus should be on the root element"
+ );
+ EventUtils.synthesizeKey("VK_TAB", {}, abWindow);
+ Assert.ok(
+ abDocument
+ .getElementById("editContactForm")
+ .contains(abDocument.activeElement),
+ "focus should be on the editing form"
+ );
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true }, abWindow);
+ Assert.equal(
+ abDocument.activeElement,
+ abDocument.documentElement,
+ "focus should be on the root element again"
+ );
+
+ // Check that clicking outside the form doesn't steal focus.
+
+ EventUtils.synthesizeMouseAtCenter(booksList, {}, abWindow);
+ Assert.equal(
+ abDocument.activeElement,
+ abDocument.body,
+ "focus should be on the body element"
+ );
+ EventUtils.synthesizeMouseAtCenter(cardsList, {}, abWindow);
+ Assert.equal(
+ abDocument.activeElement,
+ abDocument.body,
+ "focus should be on the body element still"
+ );
+
+ Assert.ok(BrowserTestUtils.is_hidden(editButton));
+ Assert.ok(BrowserTestUtils.is_visible(cancelEditButton));
+ Assert.ok(BrowserTestUtils.is_visible(saveEditButton));
+
+ checkInputValues({
+ FirstName: "contact",
+ LastName: "1",
+ DisplayName: "contact 1",
+ NickName: "",
+ PrimaryEmail: "contact.1@invalid",
+ SecondEmail: null,
+ });
+
+ // Make sure the header values reflect the fields values.
+ assertEditHeadings("contact 1", "", "contact.1@invalid");
+
+ // Make some changes but cancel them.
+
+ setInputValues({
+ LastName: "one",
+ DisplayName: "contact one",
+ NickName: "contact nickname",
+ PrimaryEmail: "contact.1.edited@invalid",
+ SecondEmail: "i@roman.invalid",
+ });
+
+ // Headings reflect new values.
+ assertEditHeadings(
+ "contact one",
+ "contact nickname",
+ "contact.1.edited@invalid"
+ );
+
+ // Change the preferred email to the secondary.
+ setInputValues({
+ SecondEmailCheckbox: true,
+ });
+ // The new email value should be reflected in the heading.
+ assertEditHeadings("contact one", "contact nickname", "i@roman.invalid");
+
+ let promptPromise = BrowserTestUtils.promiseAlertDialog("extra1");
+ EventUtils.synthesizeMouseAtCenter(cancelEditButton, {}, abWindow);
+ await promptPromise;
+ await new Promise(resolve => abWindow.setTimeout(resolve));
+ await notInEditingMode(editButton);
+ Assert.ok(BrowserTestUtils.is_visible(detailsPane));
+
+ // Heading reflects initial values.
+ assertViewHeadings("contact 1", "", "contact.1@invalid");
+
+ Assert.ok(BrowserTestUtils.is_visible(editButton));
+ Assert.ok(BrowserTestUtils.is_hidden(cancelEditButton));
+ Assert.ok(BrowserTestUtils.is_hidden(saveEditButton));
+
+ checkDisplayValues({
+ emailAddresses: ["contact.1@invalid"],
+ });
+ checkCardValues(book.childCards[0], {
+ FirstName: "contact",
+ LastName: "1",
+ DisplayName: "contact 1",
+ PrimaryEmail: "contact.1@invalid",
+ });
+
+ // Click to edit again. The changes should have been reversed.
+
+ EventUtils.synthesizeMouseAtCenter(editButton, {}, abWindow);
+ await inEditingMode();
+
+ Assert.ok(BrowserTestUtils.is_hidden(editButton));
+ Assert.ok(BrowserTestUtils.is_visible(cancelEditButton));
+ Assert.ok(BrowserTestUtils.is_visible(saveEditButton));
+
+ // Headings are restored.
+ assertEditHeadings("contact 1", "", "contact.1@invalid");
+
+ checkInputValues({
+ FirstName: "contact",
+ LastName: "1",
+ DisplayName: "contact 1",
+ PrimaryEmail: "contact.1@invalid",
+ SecondEmail: null,
+ });
+
+ // Make some changes again, and this time save them.
+
+ setInputValues({
+ LastName: "one",
+ DisplayName: "contact one",
+ NickName: "contact nickname",
+ SecondEmail: "i@roman.invalid",
+ });
+
+ assertEditHeadings("contact one", "contact nickname", "contact.1@invalid");
+
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode(editButton);
+ Assert.ok(BrowserTestUtils.is_visible(detailsPane));
+
+ // Headings show new values
+ assertViewHeadings("contact one", "contact nickname", "contact.1@invalid");
+
+ Assert.ok(BrowserTestUtils.is_visible(editButton));
+ Assert.ok(BrowserTestUtils.is_hidden(cancelEditButton));
+ Assert.ok(BrowserTestUtils.is_hidden(saveEditButton));
+
+ checkDisplayValues({
+ emailAddresses: ["contact.1@invalid", "i@roman.invalid"],
+ });
+ checkCardValues(book.childCards[0], {
+ FirstName: "contact",
+ LastName: "one",
+ DisplayName: "contact one",
+ PrimaryEmail: "contact.1@invalid",
+ SecondEmail: "i@roman.invalid",
+ });
+
+ // Click to edit again. The new values should be shown.
+
+ EventUtils.synthesizeMouseAtCenter(editButton, {}, abWindow);
+ await inEditingMode();
+
+ Assert.ok(BrowserTestUtils.is_hidden(editButton));
+ Assert.ok(BrowserTestUtils.is_visible(cancelEditButton));
+ Assert.ok(BrowserTestUtils.is_visible(saveEditButton));
+
+ checkInputValues({
+ FirstName: "contact",
+ LastName: "one",
+ DisplayName: "contact one",
+ PrimaryEmail: "contact.1@invalid",
+ SecondEmail: "i@roman.invalid",
+ });
+
+ // Cancel the edit by pressing the Escape key.
+
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, abWindow);
+ await notInEditingMode(editButton);
+
+ // Click to edit again. This time make some changes.
+
+ EventUtils.synthesizeMouseAtCenter(editButton, {}, abWindow);
+ await inEditingMode();
+
+ setInputValues({
+ FirstName: "person",
+ DisplayName: "person one",
+ });
+
+ // Cancel the edit by pressing the Escape key and cancel the prompt.
+
+ promptPromise = BrowserTestUtils.promiseAlertDialog("cancel");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, abWindow);
+ await promptPromise;
+ await new Promise(resolve => abWindow.setTimeout(resolve));
+ Assert.ok(
+ abWindow.detailsPane.isEditing,
+ "still editing after cancelling prompt"
+ );
+
+ // Cancel the edit by pressing the Escape key and accept the prompt.
+
+ promptPromise = BrowserTestUtils.promiseAlertDialog("accept");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, abWindow);
+ await promptPromise;
+ await notInEditingMode(editButton);
+ await new Promise(resolve => abWindow.setTimeout(resolve));
+
+ checkCardValues(book.childCards[0], {
+ FirstName: "person",
+ DisplayName: "person one",
+ });
+
+ // Click to edit again.
+
+ EventUtils.synthesizeMouseAtCenter(editButton, {}, abWindow);
+ await inEditingMode();
+
+ setInputValues({
+ LastName: "11",
+ DisplayName: "person 11",
+ SecondEmail: "xi@roman.invalid",
+ });
+
+ // Cancel the edit by pressing the Escape key and discard the changes.
+
+ promptPromise = BrowserTestUtils.promiseAlertDialog("extra1");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, abWindow);
+ await promptPromise;
+ await notInEditingMode(editButton);
+ await new Promise(resolve => abWindow.setTimeout(resolve));
+
+ checkCardValues(book.childCards[0], {
+ FirstName: "person",
+ DisplayName: "person one",
+ });
+
+ // Click to edit again.
+
+ EventUtils.synthesizeMouseAtCenter(editButton, {}, abWindow);
+ await inEditingMode();
+
+ // Make some changes again, and this time save them by pressing Enter.
+
+ setInputValues({
+ FirstName: "contact",
+ LastName: "1",
+ DisplayName: "contact 1",
+ NickName: "",
+ SecondEmail: null,
+ });
+
+ getInput("SecondEmail").focus();
+ EventUtils.synthesizeKey("VK_RETURN", {}, abWindow);
+ await notInEditingMode(editButton);
+
+ checkDisplayValues({
+ emailAddresses: ["contact.1@invalid"],
+ });
+ checkCardValues(book.childCards[0], {
+ FirstName: "contact",
+ LastName: "1",
+ DisplayName: "contact 1",
+ NickName: "",
+ PrimaryEmail: "contact.1@invalid",
+ SecondEmail: null,
+ });
+
+ await closeAddressBookWindow();
+ await promiseDirectoryRemoved(book.URI);
+});
+
+add_task(async function test_special_fields() {
+ Services.prefs.setStringPref("mail.addr_book.show_phonetic_fields", "true");
+
+ let abWindow = await openAddressBookWindow();
+ let abDocument = abWindow.document;
+ let createContactButton = abDocument.getElementById("toolbarCreateContact");
+
+ openDirectory(personalBook);
+ EventUtils.synthesizeMouseAtCenter(createContactButton, {}, abWindow);
+ await inEditingMode();
+
+ // The order of the FirstName and LastName fields can be reversed by L10n.
+ // This means they can be broken by L10n. Check that they're alright in the
+ // default configuration. We need to find a more robust way of doing this,
+ // but it is what it is for now.
+
+ let firstName = abDocument.getElementById("FirstName");
+ let lastName = abDocument.getElementById("LastName");
+ Assert.equal(
+ firstName.compareDocumentPosition(lastName),
+ Node.DOCUMENT_POSITION_FOLLOWING,
+ "LastName follows FirstName"
+ );
+
+ // The phonetic name fields should be visible, because the preference is set.
+ // They can also be broken by L10n.
+
+ let phoneticFirstName = abDocument.getElementById("PhoneticFirstName");
+ let phoneticLastName = abDocument.getElementById("PhoneticLastName");
+ Assert.ok(BrowserTestUtils.is_visible(phoneticFirstName));
+ Assert.ok(BrowserTestUtils.is_visible(phoneticLastName));
+ Assert.equal(
+ phoneticFirstName.compareDocumentPosition(phoneticLastName),
+ Node.DOCUMENT_POSITION_FOLLOWING,
+ "PhoneticLastName follows PhoneticFirstName"
+ );
+
+ await closeAddressBookWindow();
+
+ Services.prefs.setStringPref("mail.addr_book.show_phonetic_fields", "false");
+
+ abWindow = await openAddressBookWindow();
+ abDocument = abWindow.document;
+ createContactButton = abDocument.getElementById("toolbarCreateContact");
+
+ openDirectory(personalBook);
+ EventUtils.synthesizeMouseAtCenter(createContactButton, {}, abWindow);
+ await inEditingMode();
+
+ // The phonetic name fields should be visible, because the preference is set.
+ // They can also be broken by L10n.
+
+ phoneticFirstName = abDocument.getElementById("PhoneticFirstName");
+ phoneticLastName = abDocument.getElementById("PhoneticLastName");
+ Assert.ok(BrowserTestUtils.is_hidden(phoneticFirstName));
+ Assert.ok(BrowserTestUtils.is_hidden(phoneticLastName));
+
+ await closeAddressBookWindow();
+
+ Services.prefs.clearUserPref("mail.addr_book.show_phonetic_fields");
+}).skip(); // Phonetic fields not implemented.
+
+/**
+ * Test that the display name field is populated when it should be, and not
+ * when it shouldn't be.
+ */
+add_task(async function test_generate_display_name() {
+ Services.prefs.setBoolPref("mail.addr_book.displayName.autoGeneration", true);
+ Services.prefs.setStringPref(
+ "mail.addr_book.displayName.lastnamefirst",
+ "false"
+ );
+
+ let abWindow = await openAddressBookWindow();
+ let abDocument = abWindow.document;
+
+ let createContactButton = abDocument.getElementById("toolbarCreateContact");
+ let cardsList = abDocument.getElementById("cards");
+ let editButton = abDocument.getElementById("editButton");
+ let cancelEditButton = abDocument.getElementById("cancelEditButton");
+ let saveEditButton = abDocument.getElementById("saveEditButton");
+
+ openDirectory(personalBook);
+ EventUtils.synthesizeMouseAtCenter(createContactButton, {}, abWindow);
+ await inEditingMode();
+
+ checkInputValues({
+ FirstName: "",
+ LastName: "",
+ DisplayName: "",
+ PreferDisplayName: true,
+ });
+
+ // Try saving an empty contact.
+ let promptPromise = BrowserTestUtils.promiseAlertDialog(
+ "accept",
+ "chrome://global/content/commonDialog.xhtml"
+ );
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await promptPromise;
+ await new Promise(resolve => abWindow.setTimeout(resolve));
+ await inEditingMode();
+
+ // First name, no last name.
+ setInputValues({ FirstName: "first" });
+ checkInputValues({ DisplayName: "first" });
+
+ // Last name, no first name.
+ setInputValues({ FirstName: "", LastName: "last" });
+ checkInputValues({ DisplayName: "last" });
+
+ // Both names.
+ setInputValues({ FirstName: "first" });
+ checkInputValues({ DisplayName: "first last" });
+
+ // Modify the display name, it should not be overwritten.
+ setInputValues({ DisplayName: "don't touch me" });
+ setInputValues({ FirstName: "second" });
+ checkInputValues({ DisplayName: "don't touch me" });
+
+ // Clear the modified display name, it should still not be overwritten.
+ setInputValues({ DisplayName: "" });
+ setInputValues({ FirstName: "third" });
+ checkInputValues({ DisplayName: "" });
+
+ // Flip the order.
+ Services.prefs.setStringPref(
+ "mail.addr_book.displayName.lastnamefirst",
+ "true"
+ );
+ setInputValues({ FirstName: "fourth" });
+ checkInputValues({ DisplayName: "" });
+
+ // Turn off generation.
+ Services.prefs.setBoolPref(
+ "mail.addr_book.displayName.autoGeneration",
+ false
+ );
+ setInputValues({ FirstName: "fifth" });
+ checkInputValues({ DisplayName: "" });
+
+ setInputValues({ DisplayName: "last, fourth" });
+
+ // Save the card and check the values.
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode(editButton);
+ checkCardValues(personalBook.childCards[0], {
+ FirstName: "fifth",
+ LastName: "last",
+ DisplayName: "last, fourth",
+ });
+ Assert.ok(!abWindow.detailsPane.isDirty, "dirty flag is cleared");
+
+ // Reset the order and turn generation back on.
+ Services.prefs.setBoolPref("mail.addr_book.displayName.autoGeneration", true);
+ Services.prefs.setStringPref(
+ "mail.addr_book.displayName.lastnamefirst",
+ "false"
+ );
+
+ // Reload the card and check the values.
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(0), {}, abWindow);
+ EventUtils.synthesizeMouseAtCenter(editButton, {}, abWindow);
+ await inEditingMode();
+ checkInputValues({
+ FirstName: "fifth",
+ LastName: "last",
+ DisplayName: "last, fourth",
+ });
+
+ // Clear all required values.
+ setInputValues({
+ FirstName: "",
+ LastName: "",
+ DisplayName: "",
+ });
+
+ // Try saving the empty contact.
+ promptPromise = BrowserTestUtils.promiseAlertDialog(
+ "accept",
+ "chrome://global/content/commonDialog.xhtml"
+ );
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await promptPromise;
+ await new Promise(resolve => abWindow.setTimeout(resolve));
+ await inEditingMode();
+
+ // Close the edit without saving.
+ promptPromise = BrowserTestUtils.promiseAlertDialog("extra1");
+ EventUtils.synthesizeMouseAtCenter(cancelEditButton, {}, abWindow);
+ await promptPromise;
+ await new Promise(resolve => abWindow.setTimeout(resolve));
+ await notInEditingMode(editButton);
+
+ // Enter edit mode again. The values shouldn't have changed.
+ EventUtils.synthesizeKey("KEY_Enter", {}, abWindow);
+ await inEditingMode();
+ checkInputValues({
+ FirstName: "fifth",
+ LastName: "last",
+ DisplayName: "last, fourth",
+ });
+
+ // Check the saved name isn't overwritten.
+ setInputValues({ FirstName: "first" });
+ checkInputValues({ DisplayName: "last, fourth" });
+
+ promptPromise = BrowserTestUtils.promiseAlertDialog("extra1");
+ EventUtils.synthesizeMouseAtCenter(cancelEditButton, {}, abWindow);
+ await promptPromise;
+ await new Promise(resolve => abWindow.setTimeout(resolve));
+ await notInEditingMode(editButton);
+
+ await closeAddressBookWindow();
+ Services.prefs.clearUserPref("mail.addr_book.displayName.autoGeneration");
+ Services.prefs.clearUserPref("mail.addr_book.displayName.lastnamefirst");
+ personalBook.deleteCards(personalBook.childCards);
+});
+
+/**
+ * Test that the "prefer display name" checkbox is visible when it should be
+ * (in edit mode and only if there is a display name).
+ */
+add_task(async function test_prefer_display_name() {
+ let abWindow = await openAddressBookWindow();
+ let abDocument = abWindow.document;
+
+ let createContactButton = abDocument.getElementById("toolbarCreateContact");
+ let editButton = abDocument.getElementById("editButton");
+ let saveEditButton = abDocument.getElementById("saveEditButton");
+
+ // Make a new card. Check the default value is true.
+ // The display name shouldn't be affected by first and last name if the field
+ // is not empty.
+ openDirectory(personalBook);
+ EventUtils.synthesizeMouseAtCenter(createContactButton, {}, abWindow);
+
+ checkInputValues({ DisplayName: "", PreferDisplayName: true });
+
+ setInputValues({ DisplayName: "test" });
+ setInputValues({ FirstName: "first" });
+
+ checkInputValues({ DisplayName: "test" });
+
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode(editButton);
+
+ Assert.equal(personalBook.childCardCount, 1);
+ checkCardValues(personalBook.childCards[0], {
+ DisplayName: "test",
+ PreferDisplayName: "1",
+ });
+
+ // Edit the card. Check the UI matches the card value.
+ EventUtils.synthesizeMouseAtCenter(editButton, {}, abWindow);
+ await inEditingMode();
+
+ checkInputValues({ DisplayName: "test" });
+ checkInputValues({ FirstName: "first" });
+
+ // Change the card value.
+
+ let preferDisplayName = abDocument.querySelector(
+ "vcard-fn #vCardPreferDisplayName"
+ );
+ EventUtils.synthesizeMouseAtCenter(preferDisplayName, {}, abWindow);
+
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode(editButton);
+
+ Assert.equal(personalBook.childCardCount, 1);
+ checkCardValues(personalBook.childCards[0], {
+ DisplayName: "test",
+ PreferDisplayName: "0",
+ });
+
+ // Edit the card. Check the UI matches the card value.
+
+ preferDisplayName.checked = true; // Ensure it gets set.
+ EventUtils.synthesizeMouseAtCenter(editButton, {}, abWindow);
+ await inEditingMode();
+
+ // Clear the display name. The first and last name shouldn't affect it.
+ setInputValues({ DisplayName: "" });
+ checkInputValues({ FirstName: "first" });
+
+ setInputValues({ LastName: "last" });
+ checkInputValues({ DisplayName: "" });
+
+ await closeAddressBookWindow();
+ personalBook.deleteCards(personalBook.childCards);
+});
+
+/**
+ * Checks the state of the toolbar buttons is restored after editing.
+ */
+add_task(async function test_toolbar_state() {
+ personalBook.addCard(createContact("contact", "2"));
+ let abWindow = await openAddressBookWindow();
+ let abDocument = abWindow.document;
+
+ let cardsList = abDocument.getElementById("cards");
+ let editButton = abDocument.getElementById("editButton");
+ let cancelEditButton = abDocument.getElementById("cancelEditButton");
+ let saveEditButton = abDocument.getElementById("saveEditButton");
+
+ // In All Address Books, the "create card" and "create list" buttons should
+ // be disabled.
+
+ await openAllAddressBooks();
+ checkToolbarState(true);
+
+ // In other directories, all buttons should be enabled.
+
+ await openDirectory(personalBook);
+ checkToolbarState(true);
+
+ // Back to All Address Books.
+
+ await openAllAddressBooks();
+ checkToolbarState(true);
+
+ // Select a card, no change.
+
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(0), {}, abWindow);
+ checkToolbarState(true);
+
+ // Edit a card, all buttons disabled.
+
+ EventUtils.synthesizeMouseAtCenter(editButton, {}, abWindow);
+ await inEditingMode();
+
+ // Cancel editing, button states restored.
+
+ EventUtils.synthesizeMouseAtCenter(cancelEditButton, {}, abWindow);
+ await notInEditingMode(editButton);
+
+ // Edit a card again, all buttons disabled.
+
+ EventUtils.synthesizeKey(" ", {}, abWindow);
+ await inEditingMode();
+
+ // Cancel editing, button states restored.
+
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode(editButton);
+
+ await closeAddressBookWindow();
+ personalBook.deleteCards(personalBook.childCards);
+});
+
+add_task(async function test_delete_button() {
+ let abWindow = await openAddressBookWindow();
+ openDirectory(personalBook);
+
+ let abDocument = abWindow.document;
+ let searchInput = abDocument.getElementById("searchInput");
+ let cardsList = abDocument.getElementById("cards");
+ let detailsPane = abDocument.getElementById("detailsPane");
+
+ let createContactButton = abDocument.getElementById("toolbarCreateContact");
+ let editButton = abDocument.getElementById("editButton");
+ let deleteButton = abDocument.getElementById("detailsDeleteButton");
+ let saveEditButton = abDocument.getElementById("saveEditButton");
+
+ Assert.ok(BrowserTestUtils.is_hidden(detailsPane), "details pane is hidden");
+
+ // Create a new card. The delete button shouldn't be visible at this point.
+
+ EventUtils.synthesizeMouseAtCenter(createContactButton, {}, abWindow);
+ await inEditingMode();
+
+ Assert.ok(BrowserTestUtils.is_hidden(editButton));
+ Assert.ok(BrowserTestUtils.is_hidden(deleteButton));
+ Assert.ok(BrowserTestUtils.is_visible(saveEditButton));
+
+ setInputValues({
+ FirstName: "delete",
+ LastName: "me",
+ });
+
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode(editButton);
+
+ Assert.ok(BrowserTestUtils.is_visible(editButton));
+ Assert.ok(BrowserTestUtils.is_hidden(deleteButton));
+
+ Assert.equal(personalBook.childCardCount, 1, "contact was not deleted");
+ let contact = personalBook.childCards[0];
+
+ // Click to edit.
+
+ EventUtils.synthesizeMouseAtCenter(editButton, {}, abWindow);
+ await inEditingMode();
+
+ Assert.ok(BrowserTestUtils.is_hidden(editButton));
+ Assert.ok(BrowserTestUtils.is_visible(deleteButton));
+
+ // Click to delete, cancel the deletion.
+
+ let promptPromise = BrowserTestUtils.promiseAlertDialog("cancel");
+ EventUtils.synthesizeMouseAtCenter(deleteButton, {}, abWindow);
+ await promptPromise;
+ await new Promise(resolve => abWindow.setTimeout(resolve));
+
+ Assert.ok(abWindow.detailsPane.isEditing, "still in editing mode");
+ Assert.equal(personalBook.childCardCount, 1, "contact was not deleted");
+
+ // Click to delete, accept the deletion.
+
+ let deletionPromise = TestUtils.topicObserved("addrbook-contact-deleted");
+ promptPromise = BrowserTestUtils.promiseAlertDialog("accept");
+ EventUtils.synthesizeMouseAtCenter(deleteButton, {}, abWindow);
+ await promptPromise;
+ await notInEditingMode(searchInput);
+
+ let [subject, data] = await deletionPromise;
+ Assert.equal(subject.UID, contact.UID, "correct card was deleted");
+ Assert.equal(data, personalBook.UID, "card was deleted from correct place");
+ Assert.equal(personalBook.childCardCount, 0, "contact was deleted");
+ Assert.equal(
+ cardsList.view.directory.UID,
+ personalBook.UID,
+ "view didn't change"
+ );
+ await TestUtils.waitForCondition(() =>
+ BrowserTestUtils.is_hidden(detailsPane)
+ );
+
+ // Now let's delete a contact while viewing a list.
+
+ let listContact = createContact("delete", "me too");
+ let list = personalBook.addMailList(createMailingList("a list"));
+ list.addCard(listContact);
+ await new Promise(resolve => abWindow.setTimeout(resolve));
+
+ openDirectory(list);
+ Assert.equal(cardsList.view.rowCount, 1);
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(0), {}, abWindow);
+ await TestUtils.waitForCondition(() =>
+ BrowserTestUtils.is_visible(detailsPane)
+ );
+
+ Assert.ok(BrowserTestUtils.is_visible(editButton));
+ Assert.ok(BrowserTestUtils.is_hidden(deleteButton));
+
+ // Click to edit.
+
+ EventUtils.synthesizeMouseAtCenter(editButton, {}, abWindow);
+ await inEditingMode();
+
+ Assert.ok(BrowserTestUtils.is_hidden(editButton));
+ Assert.ok(BrowserTestUtils.is_visible(deleteButton));
+
+ // Click to delete, accept the deletion.
+ deletionPromise = TestUtils.topicObserved("addrbook-contact-deleted");
+ promptPromise = BrowserTestUtils.promiseAlertDialog("accept");
+ EventUtils.synthesizeMouseAtCenter(deleteButton, {}, abWindow);
+ await promptPromise;
+ await notInEditingMode(searchInput);
+
+ [subject, data] = await deletionPromise;
+ Assert.equal(subject.UID, listContact.UID, "correct card was deleted");
+ Assert.equal(data, personalBook.UID, "card was deleted from correct place");
+ Assert.equal(personalBook.childCardCount, 0, "contact was deleted");
+ Assert.equal(cardsList.view.directory.UID, list.UID, "view didn't change");
+ await TestUtils.waitForCondition(() =>
+ BrowserTestUtils.is_hidden(detailsPane)
+ );
+
+ personalBook.deleteDirectory(list);
+ await closeAddressBookWindow();
+});
+
+function checkNFieldState({ prefix, middlename, suffix }) {
+ let abWindow = getAddressBookWindow();
+ let abDocument = abWindow.document;
+
+ Assert.equal(abDocument.querySelectorAll("vcard-n").length, 1);
+
+ Assert.ok(
+ BrowserTestUtils.is_visible(abDocument.getElementById("vcard-n-firstname")),
+ "Firstname is always shown."
+ );
+
+ Assert.ok(
+ BrowserTestUtils.is_visible(abDocument.getElementById("vcard-n-lastname")),
+ "Lastname is always shown."
+ );
+
+ for (let [subValueName, inputId, buttonSelector, inputVisible] of [
+ ["prefix", "vcard-n-prefix", "#n-list-component-prefix button", prefix],
+ [
+ "middlename",
+ "vcard-n-middlename",
+ "#n-list-component-middlename button",
+ middlename,
+ ],
+ ["suffix", "vcard-n-suffix", "#n-list-component-suffix button", suffix],
+ ]) {
+ let inputEl = abDocument.getElementById(inputId);
+ Assert.ok(inputEl);
+ let buttonEl = abDocument.querySelector(buttonSelector);
+ Assert.ok(buttonEl);
+
+ if (inputVisible) {
+ Assert.ok(
+ BrowserTestUtils.is_visible(inputEl),
+ `${subValueName} input is shown with an initial value or a click on the button.`
+ );
+ Assert.ok(
+ BrowserTestUtils.is_hidden(buttonEl),
+ `${subValueName} button is hidden when the input is shown.`
+ );
+ } else {
+ Assert.ok(
+ BrowserTestUtils.is_hidden(inputEl),
+ `${subValueName} input is not shown initially.`
+ );
+ Assert.ok(
+ BrowserTestUtils.is_visible(buttonEl),
+ `${subValueName} button is shown when the input is hidden.`
+ );
+ }
+ }
+}
+
+/**
+ * Save repeatedly names of two contacts and ensure that no fields are leaking
+ * to another card.
+ */
+add_task(async function test_name_fields() {
+ let book = createAddressBook("Test Book N Field");
+ book.addCard(createContact("contact1", "lastname1"));
+ book.addCard(createContact("contact2", "lastname2"));
+
+ let abWindow = await openAddressBookWindow();
+ openDirectory(book);
+
+ let abDocument = abWindow.document;
+ let cardsList = abDocument.getElementById("cards");
+ let editButton = abDocument.getElementById("editButton");
+ let saveEditButton = abDocument.getElementById("saveEditButton");
+ let cancelEditButton = abDocument.getElementById("cancelEditButton");
+
+ // Edit contact1.
+ await editContactAtIndex(0, {});
+
+ // Check for the original values of contact1.
+ checkInputValues({ FirstName: "contact1", LastName: "lastname1" });
+
+ checkNFieldState({ prefix: false, middlename: false, suffix: false });
+
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode(editButton);
+
+ checkVCardValues(book.childCards[0], {
+ n: [{ value: ["lastname1", "contact1", "", "", ""] }],
+ });
+
+ // Edit contact1 set all n values.
+ await editContactAtIndex(0, { useMouse: true });
+
+ checkNFieldState({ prefix: false, middlename: false, suffix: false });
+
+ setInputValues({
+ Prefix: "prefix 1",
+ FirstName: "contact1 changed",
+ MiddleName: "middle name 1",
+ LastName: "lastname1 changed",
+ Suffix: "suffix 1",
+ });
+
+ checkNFieldState({ prefix: true, middlename: true, suffix: true });
+
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode(editButton);
+
+ checkVCardValues(book.childCards[0], {
+ n: [
+ {
+ value: [
+ "lastname1 changed",
+ "contact1 changed",
+ "middle name 1",
+ "prefix 1",
+ "suffix 1",
+ ],
+ },
+ ],
+ });
+
+ // Edit contact2.
+ await editContactAtIndex(1, {});
+
+ // Check for the original values of contact2 after saving contact1.
+ checkInputValues({ FirstName: "contact2", LastName: "lastname2" });
+
+ checkNFieldState({ prefix: false, middlename: false, suffix: false });
+
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode(editButton);
+
+ // Ensure that both vCardValues of contact1 and contact2 are correct.
+ checkVCardValues(book.childCards[0], {
+ n: [
+ {
+ value: [
+ "lastname1 changed",
+ "contact1 changed",
+ "middle name 1",
+ "prefix 1",
+ "suffix 1",
+ ],
+ },
+ ],
+ });
+
+ checkVCardValues(book.childCards[1], {
+ n: [{ value: ["lastname2", "contact2", "", "", ""] }],
+ });
+
+ // Edit contact1 and change the values to only firstname and lastname values
+ // to see that the button/input handling of the field is correct.
+ await editContactAtIndex(0, {});
+
+ checkInputValues({
+ Prefix: "prefix 1",
+ FirstName: "contact1 changed",
+ MiddleName: "middle name 1",
+ LastName: "lastname1 changed",
+ Suffix: "suffix 1",
+ });
+
+ checkNFieldState({ prefix: true, middlename: true, suffix: true });
+
+ setInputValues({
+ Prefix: "",
+ FirstName: "contact1 changed",
+ MiddleName: "",
+ LastName: "lastname1 changed",
+ Suffix: "",
+ });
+
+ // Fields are still visible until the contact is saved and edited again.
+ checkNFieldState({ prefix: true, middlename: true, suffix: true });
+
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode(editButton);
+
+ checkVCardValues(book.childCards[0], {
+ n: [{ value: ["lastname1 changed", "contact1 changed", "", "", ""] }],
+ });
+
+ checkVCardValues(book.childCards[1], {
+ n: [{ value: ["lastname2", "contact2", "", "", ""] }],
+ });
+
+ // Check in contact1 that prefix, middlename and suffix inputs are hidden
+ // again. Then remove the N last values and save.
+ await editContactAtIndex(0, { useMouse: true, useActivate: true });
+
+ checkInputValues({
+ FirstName: "contact1 changed",
+ LastName: "lastname1 changed",
+ });
+
+ checkNFieldState({ prefix: false, middlename: false, suffix: false });
+
+ // Let firstname and lastname empty for contact1.
+ setInputValues({
+ FirstName: "",
+ LastName: "",
+ });
+
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ // If useActivate is called, expect the focus to return to the cards list.
+ await notInEditingMode(cardsList.table.body);
+
+ checkVCardValues(book.childCards[0], {
+ n: [],
+ });
+
+ checkVCardValues(book.childCards[1], {
+ n: [{ value: ["lastname2", "contact2", "", "", ""] }],
+ });
+
+ // Edit contact2.
+ await editContactAtIndex(1, { useActivate: true });
+
+ checkInputValues({ FirstName: "contact2", LastName: "lastname2" });
+
+ checkNFieldState({ prefix: false, middlename: false, suffix: false });
+
+ setInputValues({
+ FirstName: "contact2 changed",
+ LastName: "lastname2 changed",
+ Suffix: "suffix 2",
+ });
+
+ checkNFieldState({ prefix: false, middlename: false, suffix: true });
+
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode(cardsList.table.body);
+
+ checkVCardValues(book.childCards[0], {
+ n: [],
+ });
+
+ checkVCardValues(book.childCards[1], {
+ n: [
+ { value: ["lastname2 changed", "contact2 changed", "", "", "suffix 2"] },
+ ],
+ });
+
+ // Edit contact1.
+ await editContactAtIndex(0, { useMouse: true, useActivate: true });
+
+ checkInputValues({ FirstName: "", LastName: "" });
+
+ checkNFieldState({ prefix: false, middlename: false, suffix: false });
+
+ setInputValues({
+ FirstName: "contact1",
+ MiddleName: "middle name 1",
+ LastName: "lastname1",
+ });
+
+ checkNFieldState({ prefix: false, middlename: true, suffix: false });
+
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode(cardsList.table.body);
+
+ checkVCardValues(book.childCards[0], {
+ n: [{ value: ["lastname1", "contact1", "middle name 1", "", ""] }],
+ });
+
+ checkVCardValues(book.childCards[1], {
+ n: [
+ { value: ["lastname2 changed", "contact2 changed", "", "", "suffix 2"] },
+ ],
+ });
+
+ // Now check when cancelling that no data is leaked between edits.
+ // Edit contact2 for this first.
+ await editContactAtIndex(1, { useActivate: true });
+
+ checkInputValues({
+ FirstName: "contact2 changed",
+ LastName: "lastname2 changed",
+ Suffix: "suffix 2",
+ });
+
+ checkNFieldState({ prefix: false, middlename: false, suffix: true });
+
+ setInputValues({
+ Prefix: "prefix 2",
+ FirstName: "contact2",
+ MiddleName: "middle name",
+ LastName: "lastname2",
+ Suffix: "suffix 2",
+ });
+
+ checkNFieldState({ prefix: true, middlename: true, suffix: true });
+
+ let promptPromise = BrowserTestUtils.promiseAlertDialog("extra1");
+ EventUtils.synthesizeMouseAtCenter(cancelEditButton, {}, abWindow);
+ await promptPromise;
+ await notInEditingMode(cardsList.table.body);
+
+ checkVCardValues(book.childCards[0], {
+ n: [{ value: ["lastname1", "contact1", "middle name 1", "", ""] }],
+ });
+
+ checkVCardValues(book.childCards[1], {
+ n: [
+ { value: ["lastname2 changed", "contact2 changed", "", "", "suffix 2"] },
+ ],
+ });
+
+ // Ensure that prefix, middlename and lastname are correctly shown after
+ // cancelling contact2. Then cancel contact2 again and look at contact1.
+ await editContactAtIndex(1, {});
+
+ checkInputValues({
+ FirstName: "contact2 changed",
+ LastName: "lastname2 changed",
+ Suffix: "suffix 2",
+ });
+
+ checkNFieldState({ prefix: false, middlename: false, suffix: true });
+
+ EventUtils.synthesizeMouseAtCenter(cancelEditButton, {}, abWindow);
+ await notInEditingMode(editButton);
+
+ checkVCardValues(book.childCards[0], {
+ n: [{ value: ["lastname1", "contact1", "middle name 1", "", ""] }],
+ });
+
+ checkVCardValues(book.childCards[1], {
+ n: [
+ { value: ["lastname2 changed", "contact2 changed", "", "", "suffix 2"] },
+ ],
+ });
+
+ // Ensure that a cancel from contact2 doesn't leak to contact1.
+ await editContactAtIndex(0, {});
+
+ checkNFieldState({ prefix: false, middlename: true, suffix: false });
+
+ checkInputValues({
+ FirstName: "contact1",
+ MiddleName: "middle name 1",
+ LastName: "lastname1",
+ });
+
+ await closeAddressBookWindow();
+ await promiseDirectoryRemoved(book.URI);
+});
+
+/**
+ * Checks if the default choice is visible or hidden.
+ * If the default choice is expected checks that at maximum one
+ * default email is ticked.
+ *
+ * @param {boolean} expectedDefaultChoiceVisible
+ * @param {number} expectedDefaultIndex
+ */
+async function checkDefaultEmailChoice(
+ expectedDefaultChoiceVisible,
+ expectedDefaultIndex
+) {
+ let abWindow = getAddressBookWindow();
+ let abDocument = abWindow.document;
+
+ let emailFields = abDocument.querySelectorAll(`#vcard-email tr`);
+
+ for (let [index, emailField] of emailFields.entries()) {
+ if (expectedDefaultChoiceVisible) {
+ await TestUtils.waitForCondition(
+ () => BrowserTestUtils.is_visible(emailField.checkboxEl),
+ `Email at index ${index} has a visible default email choice.`
+ );
+ } else {
+ await TestUtils.waitForCondition(
+ () => BrowserTestUtils.is_hidden(emailField.checkboxEl),
+ `Email at index ${index} has a hidden default email choice.`
+ );
+ }
+
+ // Default email checking of the field.
+ Assert.equal(
+ expectedDefaultIndex === index,
+ emailField.checkboxEl.checked,
+ `Pref of email at position ${index}`
+ );
+ }
+
+ // Check that at max one checkbox is ticked.
+ if (expectedDefaultChoiceVisible) {
+ let checked = Array.from(emailFields).filter(
+ emailField => emailField.checkboxEl.checked
+ );
+ Assert.ok(
+ checked.length <= 1,
+ "At maximum one email is ticked for the default email."
+ );
+ }
+}
+
+add_task(async function test_email_fields() {
+ let book = createAddressBook("Test Book Email Field");
+ book.addCard(createContact("contact1", "lastname1"));
+ book.addCard(createContact("contact2", "lastname2"));
+
+ let abWindow = await openAddressBookWindow();
+ openDirectory(book);
+
+ let abDocument = abWindow.document;
+ let cardsList = abDocument.getElementById("cards");
+ let editButton = abDocument.getElementById("editButton");
+ let saveEditButton = abDocument.getElementById("saveEditButton");
+ let cancelEditButton = abDocument.getElementById("cancelEditButton");
+
+ // Edit contact1.
+ await editContactAtIndex(0, { useActivate: true });
+
+ // Check for the original values of contact1.
+ checkVCardInputValues({
+ email: [{ value: "contact1.lastname1@invalid" }],
+ });
+
+ await checkDefaultEmailChoice(false, 0);
+
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ // Focus moves to cards list if we activate the edit directly from the list.
+ await notInEditingMode(cardsList.table.body);
+
+ checkVCardValues(book.childCards[0], {
+ email: [{ value: "contact1.lastname1@invalid", pref: "1" }],
+ });
+
+ // Edit contact1 set type.
+ await editContactAtIndex(0, { useMouse: true, useActivate: true });
+
+ await setVCardInputValues({
+ email: [{ value: "contact1.lastname1@invalid", type: "work" }],
+ });
+
+ await checkDefaultEmailChoice(false, 0);
+
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode(cardsList.table.body);
+
+ checkVCardValues(book.childCards[0], {
+ email: [{ value: "contact1.lastname1@invalid", type: "work", pref: "1" }],
+ });
+
+ // Check for the original values of contact2.
+ await editContactAtIndex(1, {});
+
+ checkVCardInputValues({
+ email: [{ value: "contact2.lastname2@invalid" }],
+ });
+
+ await checkDefaultEmailChoice(false, 0);
+
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode(editButton);
+
+ // Ensure that both vCardValues of contact1 and contact2 are correct.
+ checkVCardValues(book.childCards[0], {
+ email: [{ value: "contact1.lastname1@invalid", type: "work", pref: "1" }],
+ });
+
+ checkVCardValues(book.childCards[1], {
+ email: [{ value: "contact2.lastname2@invalid", pref: "1" }],
+ });
+
+ // Edit contact1 and add another email to see that the default email
+ // choosing is visible.
+ await editContactAtIndex(0, { useMouse: true });
+
+ checkVCardInputValues({
+ email: [{ value: "contact1.lastname1@invalid", type: "work" }],
+ });
+
+ await checkDefaultEmailChoice(false, 0);
+
+ await setVCardInputValues({
+ email: [
+ { value: "contact1.lastname1@invalid", pref: "1", type: "work" },
+ { value: "another.contact1@invalid", type: "home" },
+ ],
+ });
+
+ await checkDefaultEmailChoice(true, 0);
+
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode(editButton);
+
+ checkVCardValues(book.childCards[0], {
+ email: [
+ { value: "contact1.lastname1@invalid", pref: "1", type: "work" },
+ { value: "another.contact1@invalid", type: "home" },
+ ],
+ });
+
+ checkVCardValues(book.childCards[1], {
+ email: [{ value: "contact2.lastname2@invalid", pref: "1" }],
+ });
+
+ // Choose another default email in contact1.
+ await editContactAtIndex(0, { useMouse: true });
+
+ checkVCardInputValues({
+ email: [
+ { value: "contact1.lastname1@invalid", type: "work" },
+ { value: "another.contact1@invalid", type: "home" },
+ ],
+ });
+
+ await checkDefaultEmailChoice(true, 0);
+
+ await setVCardInputValues({
+ email: [
+ { value: "contact1.lastname1@invalid", type: "work" },
+ { value: "another.contact1@invalid", type: "home", pref: "1" },
+ ],
+ });
+
+ await checkDefaultEmailChoice(true, 1);
+
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode(editButton);
+
+ checkVCardValues(book.childCards[0], {
+ email: [
+ { value: "contact1.lastname1@invalid", type: "work" },
+ { value: "another.contact1@invalid", type: "home", pref: "1" },
+ ],
+ });
+
+ checkVCardValues(book.childCards[1], {
+ email: [{ value: "contact2.lastname2@invalid", pref: "1" }],
+ });
+
+ // Remove the first email from contact1.
+ await editContactAtIndex(0, {});
+
+ checkVCardInputValues({
+ email: [
+ { value: "contact1.lastname1@invalid", type: "work" },
+ { value: "another.contact1@invalid", type: "home" },
+ ],
+ });
+
+ await checkDefaultEmailChoice(true, 1);
+
+ await setVCardInputValues({
+ email: [{}, { value: "another.contact1@invalid", type: "home", pref: "1" }],
+ });
+
+ // The default email choosing is still visible until the contact is saved.
+ await checkDefaultEmailChoice(true, 1);
+
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode(editButton);
+
+ checkVCardValues(book.childCards[0], {
+ email: [{ value: "another.contact1@invalid", type: "home", pref: "1" }],
+ });
+
+ checkVCardValues(book.childCards[1], {
+ email: [{ value: "contact2.lastname2@invalid", pref: "1" }],
+ });
+
+ // Add multiple emails to contact2 and click each as the default email.
+ // The last default clicked email should be set as default email and
+ // only one should be selected.
+ await editContactAtIndex(1, {});
+
+ checkVCardInputValues({
+ email: [{ value: "contact2.lastname2@invalid" }],
+ });
+
+ await checkDefaultEmailChoice(false, 0);
+
+ await setVCardInputValues({
+ email: [
+ { value: "home.contact2@invalid", type: "home", pref: "1" },
+ { value: "work.contact2@invalid", type: "work", pref: "1" },
+ ],
+ });
+
+ await checkDefaultEmailChoice(true, 1);
+
+ await setVCardInputValues({
+ email: [
+ { value: "home.contact2@invalid", type: "home", pref: "1" },
+ { value: "work.contact2@invalid", type: "work", pref: "1" },
+ { value: "some.contact2@invalid" },
+ ],
+ });
+
+ await checkDefaultEmailChoice(true, 1);
+
+ await setVCardInputValues({
+ email: [
+ { value: "home.contact2@invalid", type: "home", pref: "1" },
+ { value: "work.contact2@invalid", type: "work", pref: "1" },
+ { value: "some.contact2@invalid", pref: "1" },
+ { value: "default.email.contact2@invalid", type: "home", pref: "1" },
+ ],
+ });
+
+ await checkDefaultEmailChoice(true, 3);
+
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode(editButton);
+
+ checkVCardValues(book.childCards[0], {
+ email: [{ value: "another.contact1@invalid", type: "home", pref: "1" }],
+ });
+
+ checkVCardValues(book.childCards[1], {
+ email: [
+ { value: "home.contact2@invalid", type: "home" },
+ { value: "work.contact2@invalid", type: "work" },
+ { value: "some.contact2@invalid" },
+ { value: "default.email.contact2@invalid", type: "home", pref: "1" },
+ ],
+ });
+
+ // Remove 3 emails from contact2.
+ await editContactAtIndex(1, { useActivate: true, useMouse: true });
+
+ checkVCardInputValues({
+ email: [
+ { value: "home.contact2@invalid", type: "home" },
+ { value: "work.contact2@invalid", type: "work" },
+ { value: "some.contact2@invalid" },
+ { value: "default.email.contact2@invalid", type: "home" },
+ ],
+ });
+
+ await checkDefaultEmailChoice(true, 3);
+
+ await setVCardInputValues({
+ email: [{ value: "home.contact2@invalid", type: "home" }],
+ });
+
+ // The default email choosing is still visible until the contact is saved.
+ // For this case the default email is left on an empty field which will be
+ // removed.
+ await checkDefaultEmailChoice(true, 3);
+
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode(cardsList.table.body);
+
+ checkVCardValues(book.childCards[0], {
+ email: [{ value: "another.contact1@invalid", type: "home", pref: "1" }],
+ });
+
+ checkVCardValues(book.childCards[1], {
+ email: [{ value: "home.contact2@invalid", type: "home", pref: "1" }],
+ });
+
+ // Now check when cancelling that no data is leaked between edits.
+ // Edit contact2 for this first.
+ await editContactAtIndex(1, { useActivate: true });
+
+ checkVCardInputValues({
+ email: [{ value: "home.contact2@invalid", type: "home" }],
+ });
+
+ await checkDefaultEmailChoice(false, 0);
+
+ await setVCardInputValues({
+ email: [
+ { value: "home.contact2@invalid", type: "home", pref: "1" },
+ { value: "work.contact2@invalid", type: "work", pref: "1" },
+ { value: "some.contact2@invalid", pref: "1" },
+ { value: "default.email.contact2@invalid", type: "home", pref: "1" },
+ ],
+ });
+
+ await checkDefaultEmailChoice(true, 3);
+
+ let promptPromise = BrowserTestUtils.promiseAlertDialog("extra1");
+ EventUtils.synthesizeMouseAtCenter(cancelEditButton, {}, abWindow);
+ await promptPromise;
+ await notInEditingMode(cardsList.table.body);
+
+ checkVCardValues(book.childCards[0], {
+ email: [{ value: "another.contact1@invalid", type: "home", pref: "1" }],
+ });
+
+ checkVCardValues(book.childCards[1], {
+ email: [{ value: "home.contact2@invalid", type: "home", pref: "1" }],
+ });
+
+ // Ensure that the default email choosing is not shown after
+ // cancelling contact2. Then cancel contact2 again and look at contact1.
+ await editContactAtIndex(1, { useMouse: true });
+
+ checkVCardInputValues({
+ email: [{ value: "home.contact2@invalid", type: "home" }],
+ });
+
+ await checkDefaultEmailChoice(false, 0);
+
+ EventUtils.synthesizeMouseAtCenter(cancelEditButton, {}, abWindow);
+ await notInEditingMode(editButton);
+
+ checkVCardValues(book.childCards[0], {
+ email: [{ value: "another.contact1@invalid", type: "home", pref: "1" }],
+ });
+
+ checkVCardValues(book.childCards[1], {
+ email: [{ value: "home.contact2@invalid", type: "home", pref: "1" }],
+ });
+
+ // Ensure that a cancel from contact2 doesn't leak to contact1.
+ await editContactAtIndex(0, {});
+
+ checkVCardInputValues({
+ email: [{ value: "another.contact1@invalid", type: "home" }],
+ });
+
+ await checkDefaultEmailChoice(false, 0);
+
+ await closeAddressBookWindow();
+ await promiseDirectoryRemoved(book.URI);
+});
+
+add_task(async function test_vCard_fields() {
+ let abWindow = await openAddressBookWindow();
+ let abDocument = abWindow.document;
+ let book = createAddressBook("Test Book VCard Fields");
+
+ let contact1 = createContact("contact1", "lastname");
+ book.addCard(contact1);
+ let contact2 = createContact("contact2", "lastname");
+ book.addCard(contact2);
+
+ openDirectory(book);
+
+ let cardsList = abDocument.getElementById("cards");
+ let searchInput = abDocument.getElementById("searchInput");
+ let editButton = abDocument.getElementById("editButton");
+ let cancelEditButton = abDocument.getElementById("cancelEditButton");
+ let saveEditButton = abDocument.getElementById("saveEditButton");
+
+ // Check that no field is initially shown with a new contact.
+ let createContactButton = abDocument.getElementById("toolbarCreateContact");
+ EventUtils.synthesizeMouseAtCenter(createContactButton, {}, abWindow);
+ await inEditingMode();
+
+ for (let [selector, label] of [
+ ["vcard-impp", "Chat accounts"],
+ ["vcard-url", "Websites"],
+ ["vcard-tel", "Phone numbers"],
+ ["vcard-note", "Notes"],
+ ["vcard-special-dates", "Special dates"],
+ ["vcard-adr", "Addresses"],
+ ["vcard-tz", "Time Zone"],
+ ["vcard-role", "Organizational properties"],
+ ["vcard-title", "Organizational properties"],
+ ["vcard-org", "Organizational properties"],
+ ]) {
+ Assert.equal(
+ abDocument.querySelectorAll(selector).length,
+ 0,
+ `${label} are not initially shown.`
+ );
+ }
+
+ // Cancel the new contact creation.
+ EventUtils.synthesizeMouseAtCenter(cancelEditButton, {}, abWindow);
+ await notInEditingMode(searchInput);
+
+ // Set values for contact1 with one entry for each field.
+ await editContactAtIndex(0, { useMouse: true, useActivate: true });
+
+ await setVCardInputValues({
+ impp: [{ value: "matrix:u/contact1:example.com" }],
+ url: [{ value: "https://www.example.com" }],
+ tel: [{ value: "+123456 789" }],
+ note: [{ value: "A note to this contact" }],
+ specialDate: [
+ { value: [2000, 3, 31], key: "bday" },
+ { value: [1980, 12, 15], key: "anniversary" },
+ ],
+ adr: [
+ {
+ value: ["123 Main Street", "Any Town", "CA", "91921-1234", "U.S.A"],
+ },
+ ],
+ tz: [{ value: "Africa/Abidjan" }],
+ role: [{ value: "Role" }],
+ title: [{ value: "Title" }],
+ org: [{ value: ["Example Inc.", "European Division"] }],
+ });
+
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode(cardsList.table.body);
+
+ checkVCardValues(book.childCards[0], {
+ impp: [{ value: "matrix:u/contact1:example.com" }],
+ url: [{ value: "https://www.example.com" }],
+ tel: [{ value: "+123456 789" }],
+ note: [{ value: "A note to this contact" }],
+ bday: [{ value: "2000-03-31" }],
+ anniversary: [{ value: "1980-12-15" }],
+ adr: [
+ {
+ value: [
+ "",
+ "",
+ "123 Main Street",
+ "Any Town",
+ "CA",
+ "91921-1234",
+ "U.S.A",
+ ],
+ },
+ ],
+ tz: [{ value: "Africa/Abidjan" }],
+ role: [{ value: "Role" }],
+ title: [{ value: "Title" }],
+ org: [{ value: ["Example Inc.", "European Division"] }],
+ });
+
+ checkVCardValues(book.childCards[1], {
+ impp: [],
+ url: [],
+ tel: [],
+ note: [],
+ bday: [],
+ anniversary: [],
+ adr: [],
+ tz: [],
+ role: [],
+ title: [],
+ org: [],
+ });
+
+ // Edit the same contact and set multiple fields.
+ EventUtils.synthesizeMouseAtCenter(editButton, {}, abWindow);
+ await inEditingMode();
+
+ checkVCardInputValues({
+ impp: [{ value: "matrix:u/contact1:example.com" }],
+ url: [{ value: "https://www.example.com" }],
+ tel: [{ value: "+123456 789" }],
+ note: [{ value: "A note to this contact" }],
+ specialDate: [
+ { value: [2000, 3, 31], key: "bday" },
+ { value: [1980, 12, 15], key: "anniversary" },
+ ],
+ adr: [
+ {
+ value: ["123 Main Street", "Any Town", "CA", "91921-1234", "U.S.A"],
+ },
+ ],
+ tz: [{ value: "Africa/Abidjan" }],
+ role: [{ value: "Role" }],
+ title: [{ value: "Title" }],
+ org: [{ value: ["Example Inc.", "European Division"] }],
+ });
+
+ await setVCardInputValues({
+ impp: [
+ { value: "matrix:u/contact1:example.com" },
+ { value: "irc://irc.example.com/contact1,isuser" },
+ { value: "xmpp:test@example.com" },
+ ],
+ url: [
+ { value: "https://example.com" },
+ { value: "https://hello", type: "home" },
+ { value: "https://www.example.invalid", type: "work" },
+ ],
+ tel: [
+ { value: "+123456 789", type: "home" },
+ { value: "809 77 666 8" },
+ { value: "+1113456789", type: "work" },
+ ],
+ note: [{ value: "Another note contact1\n\n\n" }],
+ specialDate: [
+ { value: [2000, 3, 31], key: "bday" },
+ { value: [1980, 12, 15], key: "anniversary" },
+ { value: [1960, 9, 17], key: "anniversary" },
+ { value: [2010, 7, 1], key: "anniversary" },
+ ],
+ adr: [
+ { value: ["123 Main Street", "", "", "", ""] },
+ { value: ["456 Side Street", "", "", "", ""], type: "home" },
+ { value: ["789 Side Street", "", "", "", ""], type: "work" },
+ ],
+ tz: [{ value: "Africa/Abidjan" }],
+ role: [{ value: "Role" }],
+ title: [{ value: "Title" }],
+ org: [{ value: ["Example Co.", "South American Division"] }],
+ });
+
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode(editButton);
+
+ checkVCardValues(book.childCards[0], {
+ impp: [
+ { value: "matrix:u/contact1:example.com" },
+ { value: "irc://irc.example.com/contact1,isuser" },
+ { value: "xmpp:test@example.com" },
+ ],
+ url: [
+ { value: "https://example.com" },
+ { value: "https://hello", type: "home" },
+ { value: "https://www.example.invalid", type: "work" },
+ ],
+ tel: [
+ { value: "+123456 789", type: "home" },
+ { value: "809 77 666 8" },
+ { value: "+1113456789", type: "work" },
+ ],
+ note: [{ value: "Another note contact1\n\n\n" }],
+ bday: [{ value: "2000-03-31" }],
+ anniversary: [
+ { value: "1980-12-15" },
+ { value: "1960-09-17" },
+ { value: "2010-07-01" },
+ ],
+ adr: [
+ { value: ["", "", "123 Main Street", "", "", "", ""] },
+ { value: ["", "", "456 Side Street", "", "", "", ""], type: "home" },
+ { value: ["", "", "789 Side Street", "", "", "", ""], type: "work" },
+ ],
+ tz: [{ value: "Africa/Abidjan" }],
+ role: [{ value: "Role" }],
+ title: [{ value: "Title" }],
+ org: [{ value: ["Example Co.", "South American Division"] }],
+ });
+
+ checkVCardValues(book.childCards[1], {
+ impp: [],
+ url: [],
+ tel: [],
+ note: [],
+ bday: [],
+ anniversary: [],
+ adr: [],
+ tz: [],
+ role: [],
+ title: [],
+ org: [],
+ });
+
+ // Switch from contact1 to contact2 and set some entries.
+ // Ensure that no fields from contact1 are leaked.
+ await editContactAtIndex(1, { useMouse: true });
+
+ checkVCardInputValues({
+ impp: [],
+ url: [],
+ tel: [],
+ note: [],
+ specialDate: [],
+ adr: [],
+ tz: [],
+ role: [],
+ title: [],
+ org: [],
+ });
+
+ await setVCardInputValues({
+ impp: [{ value: "invalid:example.com" }],
+ url: [{ value: "https://www.thunderbird.net" }],
+ tel: [{ value: "650-903-0800" }],
+ note: [{ value: "Another note\nfor contact 2" }],
+ specialDate: [
+ { value: [1966, 12, 15], key: "bday" },
+ { value: [1954, 9, 17], key: "anniversary" },
+ ],
+ adr: [{ value: ["123 Work Street", "", "", "", ""], type: "work" }],
+ tz: [{ value: "Africa/Accra" }],
+ role: [{ value: "Role contact 2" }],
+ title: [{ value: "Title contact 2" }],
+ org: [{ value: ["Organization contact 2"] }],
+ });
+
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode(editButton);
+
+ checkVCardValues(book.childCards[0], {
+ impp: [
+ { value: "matrix:u/contact1:example.com" },
+ { value: "irc://irc.example.com/contact1,isuser" },
+ { value: "xmpp:test@example.com" },
+ ],
+ url: [
+ { value: "https://example.com" },
+ { value: "https://hello", type: "home" },
+ { value: "https://www.example.invalid", type: "work" },
+ ],
+ tel: [
+ { value: "+123456 789", type: "home" },
+ { value: "809 77 666 8" },
+ { value: "+1113456789", type: "work" },
+ ],
+ note: [{ value: "Another note contact1\n\n\n" }],
+ bday: [{ value: "2000-03-31" }],
+ anniversary: [
+ { value: "1980-12-15" },
+ { value: "1960-09-17" },
+ { value: "2010-07-01" },
+ ],
+ adr: [
+ { value: ["", "", "123 Main Street", "", "", "", ""] },
+ { value: ["", "", "456 Side Street", "", "", "", ""], type: "home" },
+ { value: ["", "", "789 Side Street", "", "", "", ""], type: "work" },
+ ],
+ tz: [{ value: "Africa/Abidjan" }],
+ role: [{ value: "Role" }],
+ title: [{ value: "Title" }],
+ org: [{ value: ["Example Co.", "South American Division"] }],
+ });
+
+ checkVCardValues(book.childCards[1], {
+ impp: [{ value: "invalid:example.com" }],
+ url: [{ value: "https://www.thunderbird.net" }],
+ tel: [{ value: "650-903-0800" }],
+ note: [{ value: "Another note\nfor contact 2" }],
+ bday: [{ value: "1966-12-15" }],
+ anniversary: [{ value: "1954-09-17" }],
+ adr: [{ value: ["", "", "123 Work Street", "", "", "", ""], type: "work" }],
+ tz: [{ value: "Africa/Accra" }],
+ role: [{ value: "Role contact 2" }],
+ title: [{ value: "Title contact 2" }],
+ org: [{ value: "Organization contact 2" }],
+ });
+
+ // Ensure that no fields from contact2 are leaked to contact1.
+ // Check and remove all values from contact1.
+ await editContactAtIndex(0, {});
+
+ checkVCardInputValues({
+ impp: [
+ { value: "matrix:u/contact1:example.com" },
+ { value: "irc://irc.example.com/contact1,isuser" },
+ { value: "xmpp:test@example.com" },
+ ],
+ url: [
+ { value: "https://example.com" },
+ { value: "https://hello", type: "home" },
+ { value: "https://www.example.invalid", type: "work" },
+ ],
+ tel: [
+ { value: "+123456 789", type: "home" },
+ { value: "809 77 666 8" },
+ { value: "+1113456789", type: "work" },
+ ],
+ note: [{ value: "Another note contact1\n\n\n" }],
+ specialDate: [
+ { value: [2000, 3, 31], key: "bday" },
+ { value: [1980, 12, 15], key: "anniversary" },
+ { value: [1960, 9, 17], key: "anniversary" },
+ { value: [2010, 7, 1], key: "anniversary" },
+ ],
+ adr: [
+ { value: ["123 Main Street", "", "", "", ""] },
+ { value: ["456 Side Street", "", "", "", ""], type: "home" },
+ { value: ["789 Side Street", "", "", "", ""], type: "work" },
+ ],
+ tz: [{ value: "Africa/Abidjan" }],
+ role: [{ value: "Role" }],
+ title: [{ value: "Title" }],
+ org: [{ value: ["Example Co.", "South American Division"] }],
+ });
+
+ await setVCardInputValues({
+ impp: [{}, {}, {}],
+ url: [{}, {}, {}],
+ tel: [{}, {}, {}],
+ note: [{}],
+ specialDate: [{}, {}, {}, {}],
+ adr: [{}, {}, {}],
+ tz: [],
+ role: [],
+ title: [],
+ org: [],
+ });
+
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode(editButton);
+
+ checkVCardValues(book.childCards[0], {
+ impp: [],
+ url: [],
+ tel: [],
+ note: [],
+ bday: [],
+ anniversary: [],
+ adr: [],
+ tz: [],
+ role: [],
+ title: [],
+ org: [],
+ });
+
+ checkVCardValues(book.childCards[1], {
+ impp: [{ value: "invalid:example.com" }],
+ url: [{ value: "https://www.thunderbird.net" }],
+ tel: [{ value: "650-903-0800" }],
+ note: [{ value: "Another note\nfor contact 2" }],
+ bday: [{ value: "1966-12-15" }],
+ anniversary: [{ value: "1954-09-17" }],
+ adr: [{ value: ["", "", "123 Work Street", "", "", "", ""], type: "work" }],
+ tz: [{ value: "Africa/Accra" }],
+ role: [{ value: "Role contact 2" }],
+ title: [{ value: "Title contact 2" }],
+ org: [{ value: "Organization contact 2" }],
+ });
+
+ // Check contact2 make changes and cancel.
+ await editContactAtIndex(1, { useActivate: true });
+
+ checkVCardInputValues({
+ impp: [{ value: "invalid:example.com" }],
+ url: [{ value: "https://www.thunderbird.net" }],
+ tel: [{ value: "650-903-0800" }],
+ note: [{ value: "Another note\nfor contact 2" }],
+ specialDate: [
+ { value: [1966, 12, 15], key: "bday" },
+ { value: [1954, 9, 17], key: "anniversary" },
+ ],
+ adr: [{ value: ["123 Work Street", "", "", "", ""], type: "work" }],
+ tz: [{ value: "Africa/Accra" }],
+ role: [{ value: "Role contact 2" }],
+ title: [{ value: "Title contact 2" }],
+ org: [{ value: "Organization contact 2" }],
+ });
+
+ await setVCardInputValues({
+ impp: [{ value: "" }],
+ url: [
+ { value: "https://www.thunderbird.net" },
+ { value: "www.another.url", type: "work" },
+ ],
+ tel: [{ value: "650-903-0800" }, { value: "+123 456 789", type: "home" }],
+ note: [],
+ specialDate: [{}, { value: [1980, 12, 15], key: "anniversary" }],
+ adr: [],
+ tz: [],
+ role: [{ value: "Some Role contact 2" }],
+ title: [],
+ org: [{ value: "Some Organization" }],
+ });
+
+ // Cancel the changes.
+ let promptPromise = BrowserTestUtils.promiseAlertDialog("extra1");
+ EventUtils.synthesizeMouseAtCenter(cancelEditButton, {}, abWindow);
+ await promptPromise;
+ await notInEditingMode(cardsList.table.body);
+
+ checkVCardValues(book.childCards[0], {
+ impp: [],
+ url: [],
+ tel: [],
+ note: [],
+ bday: [],
+ anniversary: [],
+ adr: [],
+ tz: [],
+ role: [],
+ title: [],
+ org: [],
+ });
+
+ checkVCardValues(book.childCards[1], {
+ impp: [{ value: "invalid:example.com" }],
+ url: [{ value: "https://www.thunderbird.net" }],
+ tel: [{ value: "650-903-0800" }],
+ note: [{ value: "Another note\nfor contact 2" }],
+ bday: [{ value: "1966-12-15" }],
+ anniversary: [{ value: "1954-09-17" }],
+ adr: [{ value: ["", "", "123 Work Street", "", "", "", ""], type: "work" }],
+ tz: [{ value: "Africa/Accra" }],
+ role: [{ value: "Role contact 2" }],
+ title: [{ value: "Title contact 2" }],
+ org: [{ value: "Organization contact 2" }],
+ });
+
+ // Check that the cancel for contact2 worked cancel afterwards.
+ await editContactAtIndex(1, {});
+
+ checkVCardInputValues({
+ impp: [{ value: "invalid:example.com" }],
+ url: [{ value: "https://www.thunderbird.net" }],
+ tel: [{ value: "650-903-0800" }],
+ note: [{ value: "Another note\nfor contact 2" }],
+ specialDate: [
+ { value: [1966, 12, 15], key: "bday" },
+ { value: [1954, 9, 17], key: "anniversary" },
+ ],
+ adr: [{ value: ["123 Work Street", "", "", "", ""], type: "work" }],
+ tz: [{ value: "Africa/Accra" }],
+ role: [{ value: "Role contact 2" }],
+ title: [{ value: "Title contact 2" }],
+ org: [{ value: "Organization contact 2" }],
+ });
+
+ EventUtils.synthesizeMouseAtCenter(cancelEditButton, {}, abWindow);
+ await notInEditingMode(editButton);
+
+ checkVCardValues(book.childCards[0], {
+ impp: [],
+ url: [],
+ tel: [],
+ note: [],
+ bday: [],
+ anniversary: [],
+ adr: [],
+ tz: [],
+ role: [],
+ title: [],
+ org: [],
+ });
+
+ checkVCardValues(book.childCards[1], {
+ impp: [{ value: "invalid:example.com" }],
+ url: [{ value: "https://www.thunderbird.net" }],
+ tel: [{ value: "650-903-0800" }],
+ note: [{ value: "Another note\nfor contact 2" }],
+ bday: [{ value: "1966-12-15" }],
+ anniversary: [{ value: "1954-09-17" }],
+ adr: [{ value: ["", "", "123 Work Street", "", "", "", ""], type: "work" }],
+ tz: [{ value: "Africa/Accra" }],
+ role: [{ value: "Role contact 2" }],
+ title: [{ value: "Title contact 2" }],
+ org: [{ value: "Organization contact 2" }],
+ });
+
+ // Check that no values from contact2 are leaked to contact1 when cancelling.
+ await editContactAtIndex(0, {});
+
+ checkVCardInputValues({
+ impp: [],
+ url: [],
+ tel: [],
+ note: [],
+ specialDate: [],
+ adr: [],
+ tz: [],
+ role: [],
+ title: [],
+ org: [],
+ });
+
+ await closeAddressBookWindow();
+ await promiseDirectoryRemoved(book.URI);
+});
+
+add_task(async function test_vCard_minimal() {
+ let abWindow = await openAddressBookWindow();
+ let abDocument = abWindow.document;
+
+ let createContactButton = abDocument.getElementById("toolbarCreateContact");
+
+ openDirectory(personalBook);
+ EventUtils.synthesizeMouseAtCenter(createContactButton, {}, abWindow);
+ await inEditingMode();
+
+ checkInputValues({
+ FirstName: "",
+ LastName: "",
+ DisplayName: "",
+ PreferDisplayName: true,
+ });
+
+ let addOrgButton = abDocument.getElementById("vcard-add-org");
+ addOrgButton.scrollIntoView({ block: "nearest" });
+ EventUtils.synthesizeMouseAtCenter(addOrgButton, {}, abWindow);
+
+ Assert.ok(
+ BrowserTestUtils.is_visible(abDocument.querySelector("vcard-title")),
+ "Title should be visible"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_visible(abDocument.querySelector("vcard-role")),
+ "Role should be visible"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_visible(abDocument.querySelector("vcard-org")),
+ "Organization should be visible"
+ );
+
+ abDocument.querySelector("vcard-org input").value = "FBI";
+
+ let saveEditButton = abDocument.getElementById("saveEditButton");
+ let editButton = abDocument.getElementById("editButton");
+
+ // Should allow to save with only Organization filled.
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode(editButton);
+
+ checkVCardValues(personalBook.childCards[0], {
+ org: [{ value: "FBI" }],
+ });
+
+ await closeAddressBookWindow();
+ personalBook.deleteCards(personalBook.childCards);
+});
+
+/**
+ * Switches to different types to verify that all works accordingly.
+ */
+add_task(async function test_type_selection() {
+ let abWindow = await openAddressBookWindow();
+ let abDocument = abWindow.document;
+ let book = createAddressBook("Test Book Type Selection");
+
+ let contact1 = createContact("contact1", "lastname");
+ book.addCard(contact1);
+
+ openDirectory(book);
+
+ let editButton = abDocument.getElementById("editButton");
+ let saveEditButton = abDocument.getElementById("saveEditButton");
+
+ await editContactAtIndex(0, {});
+
+ await setVCardInputValues({
+ email: [
+ { value: "contact1@invalid" },
+ { value: "home.contact1@invalid", type: "home" },
+ { value: "work.contact1@invalid", type: "work" },
+ ],
+ url: [
+ { value: "https://none.example.com" },
+ { value: "https://home.example.com", type: "home" },
+ { value: "https://work.example.com", type: "work" },
+ ],
+ tel: [
+ { value: "+123456 789" },
+ { value: "809 HOME 77 666 8", type: "home" },
+ { value: "+111 WORK 3456789", type: "work" },
+ { value: "+123 CELL 456 789", type: "cell" },
+ { value: "809 FAX 77 666 8", type: "fax" },
+ { value: "+111 PAGER 3456789", type: "pager" },
+ ],
+ });
+
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode(editButton);
+
+ checkVCardValues(book.childCards[0], {
+ email: [
+ { value: "contact1@invalid", pref: "1" },
+ { value: "home.contact1@invalid", type: "home" },
+ { value: "work.contact1@invalid", type: "work" },
+ ],
+ url: [
+ { value: "https://none.example.com" },
+ { value: "https://home.example.com", type: "home" },
+ { value: "https://work.example.com", type: "work" },
+ ],
+ tel: [
+ { value: "+123456 789" },
+ { value: "809 HOME 77 666 8", type: "home" },
+ { value: "+111 WORK 3456789", type: "work" },
+ { value: "+123 CELL 456 789", type: "cell" },
+ { value: "809 FAX 77 666 8", type: "fax" },
+ { value: "+111 PAGER 3456789", type: "pager" },
+ ],
+ });
+
+ EventUtils.synthesizeMouseAtCenter(editButton, {}, abWindow);
+ await inEditingMode();
+
+ checkVCardInputValues({
+ email: [
+ { value: "contact1@invalid", pref: "1" },
+ { value: "home.contact1@invalid", type: "home" },
+ { value: "work.contact1@invalid", type: "work" },
+ ],
+ url: [
+ { value: "https://none.example.com" },
+ { value: "https://home.example.com", type: "home" },
+ { value: "https://work.example.com", type: "work" },
+ ],
+ tel: [
+ { value: "+123456 789" },
+ { value: "809 HOME 77 666 8", type: "home" },
+ { value: "+111 WORK 3456789", type: "work" },
+ { value: "+123 CELL 456 789", type: "cell" },
+ { value: "809 FAX 77 666 8", type: "fax" },
+ { value: "+111 PAGER 3456789", type: "pager" },
+ ],
+ });
+
+ await setVCardInputValues({
+ email: [
+ { value: "contact1@invalid", type: "work" },
+ { value: "home.contact1@invalid" },
+ { value: "work.contact1@invalid", type: "home" },
+ ],
+ url: [
+ { value: "https://none.example.com", type: "work" },
+ { value: "https://home.example.com" },
+ { value: "https://work.example.com", type: "home" },
+ ],
+ tel: [
+ { value: "+123456 789", type: "pager" },
+ { value: "809 HOME 77 666 8" },
+ { value: "+111 WORK 3456789", type: "home" },
+ { value: "+123 CELL 456 789" },
+ { value: "809 FAX 77 666 8", type: "fax" },
+ { value: "+111 PAGER 3456789", type: "cell" },
+ ],
+ });
+
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode(editButton);
+
+ checkVCardValues(book.childCards[0], {
+ email: [
+ { value: "contact1@invalid", type: "work", pref: "1" },
+ { value: "home.contact1@invalid" },
+ { value: "work.contact1@invalid", type: "home" },
+ ],
+ url: [
+ { value: "https://none.example.com", type: "work" },
+ { value: "https://home.example.com" },
+ { value: "https://work.example.com", type: "home" },
+ ],
+ tel: [
+ { value: "+123456 789", type: "pager" },
+ { value: "809 HOME 77 666 8" },
+ { value: "+111 WORK 3456789", type: "home" },
+ { value: "+123 CELL 456 789" },
+ { value: "809 FAX 77 666 8", type: "fax" },
+ { value: "+111 PAGER 3456789", type: "cell" },
+ ],
+ });
+
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode(editButton);
+
+ await closeAddressBookWindow();
+ await promiseDirectoryRemoved(book.URI);
+});
+
+/**
+ * Other vCard contacts are using uppercase types for the predefined spec
+ * labels. This tests our support for them for the edit of a contact.
+ */
+add_task(async function test_support_types_uppercase() {
+ let abWindow = await openAddressBookWindow();
+ let abDocument = abWindow.document;
+ let book = createAddressBook("Test Book Uppercase Type Support");
+
+ let editButton = abDocument.getElementById("editButton");
+ let saveEditButton = abDocument.getElementById("saveEditButton");
+
+ // Add a card with uppercase types.
+ book.addCard(
+ VCardUtils.vCardToAbCard(formatVCard`
+ BEGIN:VCARD
+ FN:contact 1
+ TEL:+123456 789
+ TEL;TYPE=HOME:809 HOME 77 666 8
+ TEL;TYPE=WORK:+111 WORK 3456789
+ TEL;TYPE=CELL:+123 CELL 456 789
+ TEL;TYPE=FAX:809 FAX 77 666 8
+ TEL;TYPE=PAGER:+111 PAGER 3456789
+ END:VCARD
+`)
+ );
+
+ openDirectory(book);
+
+ // First open the edit and check that the values are shown.
+ // Do not change anything.
+ await editContactAtIndex(0, {});
+
+ // The UI uses lowercase types but only changes them when the type is
+ // touched.
+ checkVCardInputValues({
+ tel: [
+ { value: "+123456 789" },
+ { value: "809 HOME 77 666 8", type: "home" },
+ { value: "+111 WORK 3456789", type: "work" },
+ { value: "+123 CELL 456 789", type: "cell" },
+ { value: "809 FAX 77 666 8", type: "fax" },
+ { value: "+111 PAGER 3456789", type: "pager" },
+ ],
+ });
+
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode(editButton);
+
+ // We haven't touched these values so they are not changed to lower case.
+ checkVCardValues(book.childCards[0], {
+ tel: [
+ { value: "+123456 789" },
+ { value: "809 HOME 77 666 8", type: "HOME" },
+ { value: "+111 WORK 3456789", type: "WORK" },
+ { value: "+123 CELL 456 789", type: "CELL" },
+ { value: "809 FAX 77 666 8", type: "FAX" },
+ { value: "+111 PAGER 3456789", type: "PAGER" },
+ ],
+ });
+
+ // Now make changes to the types.
+ EventUtils.synthesizeMouseAtCenter(editButton, {}, abWindow);
+ await inEditingMode();
+
+ checkVCardInputValues({
+ tel: [
+ { value: "+123456 789" },
+ { value: "809 HOME 77 666 8", type: "home" },
+ { value: "+111 WORK 3456789", type: "work" },
+ { value: "+123 CELL 456 789", type: "cell" },
+ { value: "809 FAX 77 666 8", type: "fax" },
+ { value: "+111 PAGER 3456789", type: "pager" },
+ ],
+ });
+
+ await setVCardInputValues({
+ tel: [
+ { value: "+123456 789", type: "home" },
+ { value: "809 HOME 77 666 8", type: "cell" },
+ { value: "+111 WORK 3456789", type: "pager" },
+ { value: "+123 CELL 456 789", type: "fax" },
+ { value: "809 FAX 77 666 8", type: "" },
+ { value: "+111 PAGER 3456789", type: "work" },
+ ],
+ });
+
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode(editButton);
+
+ // As we touched the type values they are now saved in lowercase.
+ // At this point it is up to the other vCard implementation to handle this.
+ checkVCardValues(book.childCards[0], {
+ tel: [
+ { value: "+123456 789", type: "home" },
+ { value: "809 HOME 77 666 8", type: "cell" },
+ { value: "+111 WORK 3456789", type: "pager" },
+ { value: "+123 CELL 456 789", type: "fax" },
+ { value: "809 FAX 77 666 8", type: "" },
+ { value: "+111 PAGER 3456789", type: "work" },
+ ],
+ });
+
+ await closeAddressBookWindow();
+ await promiseDirectoryRemoved(book.URI);
+});
+
+add_task(async function test_special_date_field() {
+ let abWindow = await openAddressBookWindow();
+ let abDocument = abWindow.document;
+
+ let createContactButton = abDocument.getElementById("toolbarCreateContact");
+
+ openDirectory(personalBook);
+ EventUtils.synthesizeMouseAtCenter(createContactButton, {}, abWindow);
+ await inEditingMode();
+
+ checkInputValues({
+ FirstName: "",
+ LastName: "",
+ DisplayName: "",
+ PreferDisplayName: true,
+ });
+
+ // Add data to the default values to allow saving.
+ setInputValues({
+ FirstName: "contact",
+ PrimaryEmail: "contact.1.edited@invalid",
+ });
+
+ let addSpecialDate = abDocument.getElementById("vcard-add-bday-anniversary");
+ addSpecialDate.scrollIntoView({ block: "nearest" });
+ EventUtils.synthesizeMouseAtCenter(addSpecialDate, {}, abWindow);
+
+ Assert.ok(
+ BrowserTestUtils.is_visible(abDocument.querySelector("vcard-special-date")),
+ "The special date field is visible."
+ );
+ // Somehow prevents an error on macOS when using <select> widgets that have
+ // just been added.
+ await new Promise(resolve => abWindow.setTimeout(resolve, 250));
+
+ let firstYear = abDocument.querySelector(
+ `vcard-special-date input[type="number"]`
+ );
+ Assert.ok(!firstYear.value, "year empty");
+ let firstMonth = abDocument.querySelector(
+ `vcard-special-date .vcard-month-select`
+ );
+ Assert.equal(firstMonth.value, "", "month should be on placeholder");
+ let firstDay = abDocument.querySelector(
+ `vcard-special-date .vcard-day-select`
+ );
+ Assert.equal(firstDay.value, "", "day should be on placeholder");
+ Assert.equal(firstDay.childNodes.length, 32, "all days should be possible");
+
+ // Set date to a leap year.
+ firstYear.value = 2004;
+
+ let shownPromise = BrowserTestUtils.waitForSelectPopupShown(window);
+ firstMonth.scrollIntoView({ block: "nearest" });
+ EventUtils.synthesizeMouseAtCenter(firstMonth, {}, abWindow);
+ let selectPopup = await shownPromise;
+
+ let changePromise = BrowserTestUtils.waitForEvent(firstMonth, "change");
+ selectPopup.activateItem(selectPopup.children[2]);
+ await changePromise;
+
+ await BrowserTestUtils.waitForCondition(
+ () => firstDay.childNodes.length == 30, // 29 days + empty option 0.
+ "day options filled with leap year"
+ );
+
+ // No leap year.
+ firstYear.select();
+ EventUtils.sendString("2003");
+ await BrowserTestUtils.waitForCondition(
+ () => firstDay.childNodes.length == 29, // 28 days + empty option 0.
+ "day options filled without leap year"
+ );
+
+ // Remove the field.
+ EventUtils.synthesizeMouseAtCenter(
+ abDocument.querySelector(`vcard-special-date .remove-property-button`),
+ {},
+ abWindow
+ );
+
+ Assert.ok(
+ !abDocument.querySelector("vcard-special-date"),
+ "The special date field was removed."
+ );
+
+ await closeAddressBookWindow();
+});
+
+/**
+ * Tests that custom properties (Custom1 etc.) are editable.
+ */
+add_task(async function testCustomProperties() {
+ let card = new AddrBookCard();
+ card._properties = new Map([
+ ["PopularityIndex", 0],
+ ["Custom2", "custom two"],
+ ["Custom4", "custom four"],
+ [
+ "_vCard",
+ formatVCard`
+ BEGIN:VCARD
+ FN:custom person
+ X-CUSTOM3:x-custom three
+ X-CUSTOM4:x-custom four
+ END:VCARD
+ `,
+ ],
+ ]);
+ card = personalBook.addCard(card);
+
+ let abWindow = await openAddressBookWindow();
+ let abDocument = abWindow.document;
+ let cardsList = abDocument.getElementById("cards");
+ let detailsPane = abDocument.getElementById("detailsPane");
+ let editButton = abDocument.getElementById("editButton");
+ let saveEditButton = abDocument.getElementById("saveEditButton");
+
+ let index = cardsList.view.getIndexForUID(card.UID);
+ EventUtils.synthesizeMouseAtCenter(
+ cardsList.getRowAtIndex(index),
+ {},
+ abWindow
+ );
+ await TestUtils.waitForCondition(() =>
+ BrowserTestUtils.is_visible(detailsPane)
+ );
+
+ EventUtils.synthesizeMouseAtCenter(editButton, {}, abWindow);
+ await inEditingMode();
+
+ let customField = getFields("custom")[0];
+ let inputs = customField.querySelectorAll("input");
+ Assert.equal(inputs.length, 4);
+ Assert.equal(inputs[0].value, "");
+ Assert.equal(inputs[1].value, "custom two");
+ Assert.equal(inputs[2].value, "x-custom three");
+ Assert.equal(inputs[3].value, "x-custom four");
+
+ inputs[0].value = "x-custom one";
+ inputs[1].value = "x-custom two";
+ inputs[3].value = "";
+
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode(editButton);
+
+ card = personalBook.childCards.find(c => c.UID == card.UID);
+ checkCardValues(card, {
+ Custom2: null,
+ Custom4: null,
+ });
+ checkVCardValues(card, {
+ "x-custom1": [{ value: "x-custom one" }],
+ "x-custom2": [{ value: "x-custom two" }],
+ "x-custom3": [{ value: "x-custom three" }],
+ "x-custom4": [],
+ });
+
+ await closeAddressBookWindow();
+ personalBook.deleteCards([card]);
+});
+
+/**
+ * Tests that we correctly fix Google's bad escaping of colons in values, and
+ * other characters in URI values.
+ */
+add_task(async function testGoogleEscaping() {
+ let googleBook = createAddressBook("Google Book");
+ googleBook.wrappedJSObject._isGoogleCardDAV = true;
+ googleBook.addCard(
+ VCardUtils.vCardToAbCard(formatVCard`
+ BEGIN:VCARD
+ VERSION:3.0
+ N:test;en\\\\c\\:oding;;;
+ FN:en\\\\c\\:oding test
+ TITLE:title\\:title\\;title\\,title\\\\title\\\\\\:title\\\\\\;title\\\\\\,title\\\\\\\\
+ TEL:tel\\:0123\\\\4567
+ EMAIL:test\\\\test@invalid
+ NOTE:notes\\:\\nnotes\\;\\nnotes\\,\\nnotes\\\\
+ URL:https\\://host/url\\:url\\;url\\,url\\\\url
+ END:VCARD
+ `)
+ );
+
+ let abWindow = await openAddressBookWindow();
+
+ let abDocument = abWindow.document;
+ let cardsList = abDocument.getElementById("cards");
+ let detailsPane = abDocument.getElementById("detailsPane");
+
+ openDirectory(googleBook);
+ Assert.equal(cardsList.view.rowCount, 1);
+ Assert.ok(BrowserTestUtils.is_hidden(detailsPane));
+ await editContactAtIndex(0, {});
+
+ checkInputValues({
+ FirstName: "en\\c:oding",
+ LastName: "test",
+ DisplayName: "en\\c:oding test",
+ });
+
+ checkVCardInputValues({
+ title: [
+ { value: "title:title;title,title\\title\\:title\\;title\\,title\\\\" },
+ ],
+ tel: [{ value: "tel:01234567" }],
+ email: [{ value: "test\\test@invalid" }],
+ note: [{ value: "notes:\nnotes;\nnotes,\nnotes\\" }],
+ url: [{ value: "https://host/url:url;url,url\\url" }],
+ });
+
+ await closeAddressBookWindow();
+ await promiseDirectoryRemoved(googleBook.URI);
+});
+
+/**
+ * Tests that contacts with nickname can be edited.
+ */
+add_task(async function testNickname() {
+ let book = createAddressBook("Nick");
+ book.addCard(
+ VCardUtils.vCardToAbCard(formatVCard`
+ BEGIN:VCARD
+ VERSION:4.0
+ EMAIL;PREF=1:jsmith@example.org
+ NICKNAME:Johnny
+ N:SMITH;JOHN;;;
+ END:VCARD
+ `)
+ );
+
+ let abWindow = await openAddressBookWindow();
+
+ let abDocument = abWindow.document;
+ let cardsList = abDocument.getElementById("cards");
+ let detailsPane = abDocument.getElementById("detailsPane");
+
+ openDirectory(book);
+ Assert.equal(cardsList.view.rowCount, 1);
+ Assert.ok(BrowserTestUtils.is_hidden(detailsPane));
+ await editContactAtIndex(0, {});
+
+ checkInputValues({
+ FirstName: "JOHN",
+ LastName: "SMITH",
+ NickName: "Johnny",
+ PrimaryEmail: "jsmith@example.org",
+ });
+
+ await closeAddressBookWindow();
+ await promiseDirectoryRemoved(book.URI);
+});
+
+add_task(async function test_remove_button() {
+ let abWindow = await openAddressBookWindow();
+ let abDocument = abWindow.document;
+
+ let book = createAddressBook("Test Book VCard Fields");
+ let contact1 = createContact("contact1", "lastname");
+ book.addCard(contact1);
+
+ openDirectory(book);
+
+ await editContactAtIndex(0, {});
+ let detailsPane = abDocument.getElementById("detailsPane");
+
+ let removeButtons = detailsPane.querySelectorAll(".remove-property-button");
+ Assert.equal(
+ removeButtons.length,
+ 2,
+ "Email and Organization Properties remove button is present."
+ );
+
+ Assert.ok(
+ BrowserTestUtils.is_visible(
+ abDocument
+ .getElementById("addr-book-edit-email")
+ .querySelector(".remove-property-button")
+ ),
+ "Email is present and remove button is visible."
+ );
+
+ Assert.ok(
+ BrowserTestUtils.is_hidden(
+ abDocument
+ .getElementById("addr-book-edit-org")
+ .querySelector(".remove-property-button")
+ ),
+ "Organization Properties are not filled and the remove button is not visible."
+ );
+
+ // Set a value for each field.
+ await setVCardInputValues({
+ impp: [{ value: "invalid:example.com" }],
+ url: [{ value: "https://www.thunderbird.net" }],
+ tel: [{ value: "650-903-0800" }],
+ note: [{ value: "Another note\nfor contact 2" }],
+ specialDate: [{ value: [1966, 12, 15], key: "bday" }],
+ adr: [{ value: ["123 Work Street", "", "", "", ""], type: "work" }],
+ tz: [{ value: "Africa/Accra" }],
+ role: [{ value: "Role contact 2" }],
+ title: [{ value: "Title contact 2" }],
+ org: [{ value: "Organization contact 2" }],
+ custom: [{ value: "foo" }],
+ });
+
+ let vCardEdit = detailsPane.querySelector("vcard-edit");
+
+ // Click the remove buttons and check that the properties are removed.
+
+ for (let [propertyName, fieldsetId, propertySelector, addButton] of [
+ ["adr", "addr-book-edit-address", "vcard-adr"],
+ ["impp", "addr-book-edit-impp", "vcard-impp"],
+ ["tel", "addr-book-edit-tel", "vcard-tel"],
+ ["url", "addr-book-edit-url", "vcard-url"],
+ ["email", "addr-book-edit-email", "#vcard-email tr"],
+ ["bday", "addr-book-edit-bday-anniversary", "vcard-special-date"],
+ ["tz", "addr-book-edit-tz", "vcard-tz", "vcard-add-tz"],
+ ["note", "addr-book-edit-note", "vcard-note", "vcard-add-note"],
+ ["org", "addr-book-edit-org", "vcard-org", "vcard-add-org"],
+ ["x-custom1", "addr-book-edit-custom", "vcard-custom", "vcard-add-custom"],
+ ]) {
+ Assert.ok(
+ vCardEdit.vCardProperties.getFirstEntry(propertyName),
+ `${propertyName} is present.`
+ );
+ let removeButton = abDocument
+ .getElementById(fieldsetId)
+ .querySelector(".remove-property-button");
+
+ removeButton.scrollIntoView({ block: "nearest" });
+ let removeEvent = BrowserTestUtils.waitForEvent(
+ vCardEdit,
+ "vcard-remove-property"
+ );
+ EventUtils.synthesizeMouseAtCenter(removeButton, {}, abWindow);
+ await removeEvent;
+
+ await Assert.ok(
+ !vCardEdit.vCardProperties.getFirstEntry(propertyName),
+ `${propertyName} is removed.`
+ );
+ Assert.equal(
+ vCardEdit.querySelectorAll(propertySelector).length,
+ 0,
+ `All elements representing ${propertyName} are removed.`
+ );
+
+ // For single entries the add button have to be visible again.
+ // Time Zone, Notes, Organizational Properties, Custom Properties
+ if (addButton) {
+ Assert.ok(
+ BrowserTestUtils.is_visible(abDocument.getElementById(addButton)),
+ `Add button for ${propertyName} is visible after remove.`
+ );
+ Assert.equal(
+ abDocument.activeElement.id,
+ addButton,
+ `The focus for ${propertyName} was moved to the add button.`
+ );
+ }
+ }
+
+ let saveEditButton = abDocument.getElementById("saveEditButton");
+ let editButton = abDocument.getElementById("editButton");
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode(editButton);
+
+ await closeAddressBookWindow();
+ await promiseDirectoryRemoved(book.URI);
+});
diff --git a/comm/mail/components/addrbook/test/browser/browser_edit_photo.js b/comm/mail/components/addrbook/test/browser/browser_edit_photo.js
new file mode 100644
index 0000000000..0b0da4771d
--- /dev/null
+++ b/comm/mail/components/addrbook/test/browser/browser_edit_photo.js
@@ -0,0 +1,866 @@
+/* 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 { CardDAVDirectory } = ChromeUtils.import(
+ "resource:///modules/CardDAVDirectory.jsm"
+);
+const { CardDAVServer } = ChromeUtils.import(
+ "resource://testing-common/CardDAVServer.jsm"
+);
+const { ICAL } = ChromeUtils.import("resource:///modules/calendar/Ical.jsm");
+
+const dragService = Cc["@mozilla.org/widget/dragservice;1"].getService(
+ Ci.nsIDragService
+);
+const profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile).path;
+
+async function inEditingMode() {
+ let abWindow = getAddressBookWindow();
+ await TestUtils.waitForCondition(
+ () => abWindow.detailsPane.isEditing,
+ "entering editing mode"
+ );
+}
+
+async function notInEditingMode() {
+ let abWindow = getAddressBookWindow();
+ await TestUtils.waitForCondition(
+ () => !abWindow.detailsPane.isEditing,
+ "leaving editing mode"
+ );
+}
+
+async function waitForDialogOpenState(state) {
+ let abWindow = getAddressBookWindow();
+ let dialog = abWindow.document.getElementById("photoDialog");
+ await TestUtils.waitForCondition(
+ () => dialog.open == state,
+ "waiting for photo dialog to change state"
+ );
+ await new Promise(resolve => abWindow.setTimeout(resolve));
+}
+
+async function waitForPreviewChange() {
+ let abWindow = getAddressBookWindow();
+ let preview = abWindow.document.querySelector("#photoDialog svg > image");
+ let oldValue = preview.getAttribute("href");
+ await BrowserTestUtils.waitForEvent(
+ preview,
+ "load",
+ false,
+ () => preview.getAttribute("href") != oldValue
+ );
+ await new Promise(resolve => abWindow.requestAnimationFrame(resolve));
+}
+
+async function waitForPhotoChange() {
+ let abWindow = getAddressBookWindow();
+ let photo = abWindow.document.querySelector("#photoButton .contact-photo");
+ let dialog = abWindow.document.getElementById("photoDialog");
+ let oldValue = photo.src;
+ await BrowserTestUtils.waitForMutationCondition(
+ photo,
+ { attributes: true },
+ () => photo.src != oldValue
+ );
+ await new Promise(resolve => abWindow.requestAnimationFrame(resolve));
+ Assert.ok(!dialog.open, "dialog was closed when photo changed");
+}
+
+function dropFile(target, path) {
+ let abWindow = getAddressBookWindow();
+ let file = new FileUtils.File(getTestFilePath(path));
+
+ let dataTransfer = new DataTransfer();
+ dataTransfer.dropEffect = "copy";
+ dataTransfer.mozSetDataAt("application/x-moz-file", file, 0);
+
+ dragService.startDragSessionForTests(Ci.nsIDragService.DRAGDROP_ACTION_COPY);
+ dragService.getCurrentSession().dataTransfer = dataTransfer;
+
+ EventUtils.synthesizeDragOver(
+ target,
+ target,
+ [{ type: "application/x-moz-file", data: file }],
+ "copy",
+ abWindow
+ );
+
+ // This make sure that the fake dataTransfer has still the expected drop
+ // effect after the synthesizeDragOver call.
+ dataTransfer.dropEffect = "copy";
+
+ EventUtils.synthesizeDropAfterDragOver(null, dataTransfer, target, abWindow, {
+ _domDispatchOnly: true,
+ });
+
+ dragService.endDragSession(true);
+}
+
+function checkDialogElements({
+ dropTargetClass = "",
+ svgVisible = false,
+ saveButtonVisible = false,
+ saveButtonDisabled = false,
+ discardButtonVisible = false,
+}) {
+ let abWindow = getAddressBookWindow();
+ let dialog = abWindow.document.getElementById("photoDialog");
+ let { saveButton, discardButton } = dialog;
+ let dropTarget = dialog.querySelector("#photoDropTarget");
+ let svg = dialog.querySelector("svg");
+ Assert.equal(
+ BrowserTestUtils.is_visible(dropTarget),
+ !!dropTargetClass,
+ "drop target visibility"
+ );
+ if (dropTargetClass) {
+ Assert.stringContains(
+ dropTarget.className,
+ dropTargetClass,
+ "drop target message"
+ );
+ }
+ Assert.equal(BrowserTestUtils.is_visible(svg), svgVisible, "SVG visibility");
+ Assert.equal(
+ BrowserTestUtils.is_visible(saveButton),
+ saveButtonVisible,
+ "save button visibility"
+ );
+ Assert.equal(
+ saveButton.disabled,
+ saveButtonDisabled,
+ "save button disabled state"
+ );
+ Assert.equal(
+ BrowserTestUtils.is_visible(discardButton),
+ discardButtonVisible,
+ "discard button visibility"
+ );
+}
+
+function getInput(entryName, addIfNeeded = false) {
+ let abWindow = getAddressBookWindow();
+ let abDocument = abWindow.document;
+
+ switch (entryName) {
+ case "DisplayName":
+ return abDocument.querySelector("vcard-fn #vCardDisplayName");
+ case "FirstName":
+ return abDocument.querySelector("vcard-n #vcard-n-firstname");
+ case "LastName":
+ return abDocument.querySelector("vcard-n #vcard-n-lastname");
+ case "PrimaryEmail":
+ if (
+ addIfNeeded &&
+ abDocument.getElementById("vcard-email").children.length < 1
+ ) {
+ EventUtils.synthesizeMouseAtCenter(
+ abDocument.getElementById("vcard-add-email"),
+ {},
+ abWindow
+ );
+ }
+ return abDocument.querySelector(
+ `#vcard-email tr:nth-child(1) input[type="email"]`
+ );
+ case "SecondEmail":
+ if (
+ addIfNeeded &&
+ abDocument.getElementById("vcard-email").children.length < 2
+ ) {
+ EventUtils.synthesizeMouseAtCenter(
+ abDocument.getElementById("vcard-add-email"),
+ {},
+ abWindow
+ );
+ }
+ return abDocument.querySelector(
+ `#vcard-email tr:nth-child(2) input[type="email"]`
+ );
+ }
+
+ return null;
+}
+
+function setInputValues(changes) {
+ let abWindow = getAddressBookWindow();
+
+ for (let [key, value] of Object.entries(changes)) {
+ let input = getInput(key, !!value);
+ if (!input) {
+ Assert.ok(!value, `${key} input exists to put a value in`);
+ continue;
+ }
+
+ input.select();
+ if (value) {
+ EventUtils.sendString(value);
+ } else {
+ EventUtils.synthesizeKey("VK_BACK_SPACE", {}, abWindow);
+ }
+ }
+ EventUtils.synthesizeKey("VK_TAB", {}, abWindow);
+}
+
+add_setup(async function () {
+ await openAddressBookWindow();
+ openDirectory(personalBook);
+});
+
+registerCleanupFunction(async function cleanUp() {
+ await closeAddressBookWindow();
+ personalBook.deleteCards(personalBook.childCards);
+ await CardDAVServer.close();
+});
+
+/** Create a new contact. We'll add a photo to this contact. */
+async function subtest_add_photo(book) {
+ let abWindow = getAddressBookWindow();
+ let abDocument = abWindow.document;
+
+ let createContactButton = abDocument.getElementById("toolbarCreateContact");
+ let saveEditButton = abDocument.getElementById("saveEditButton");
+ let photoButton = abDocument.getElementById("photoButton");
+ let editPhoto = photoButton.querySelector(".contact-photo");
+ let viewPhoto = abDocument.getElementById("viewContactPhoto");
+ let dialog = abWindow.document.getElementById("photoDialog");
+ let { saveButton } = dialog;
+
+ openDirectory(book);
+
+ EventUtils.synthesizeMouseAtCenter(createContactButton, {}, abWindow);
+ await inEditingMode();
+
+ // Open the photo dialog by clicking on the photo.
+
+ Assert.equal(
+ editPhoto.src,
+ "chrome://messenger/skin/icons/new/compact/user.svg",
+ "no photo shown"
+ );
+ EventUtils.synthesizeMouseAtCenter(photoButton, {}, abWindow);
+ await waitForDialogOpenState(true);
+
+ checkDialogElements({
+ dropTargetClass: "drop-target",
+ saveButtonVisible: true,
+ saveButtonDisabled: true,
+ });
+
+ // Drop a file on the photo dialog.
+
+ let previewChangePromise = waitForPreviewChange();
+ dropFile(dialog, "data/photo1.jpg");
+ await previewChangePromise;
+
+ checkDialogElements({
+ svgVisible: true,
+ saveButtonVisible: true,
+ });
+
+ // Accept the photo dialog.
+
+ let photoChangePromise = waitForPhotoChange();
+ EventUtils.synthesizeMouseAtCenter(saveButton, {}, abWindow);
+ await photoChangePromise;
+ Assert.notEqual(
+ editPhoto.src,
+ "chrome://messenger/skin/icons/new/compact/user.svg",
+ "a photo is shown"
+ );
+
+ // Save the contact.
+
+ let createdPromise = TestUtils.topicObserved("addrbook-contact-created");
+ setInputValues({
+ DisplayName: "Person with Photo 1",
+ });
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode();
+
+ // Photo shown in view.
+ Assert.notEqual(
+ viewPhoto.src,
+ "chrome://messenger/skin/icons/new/compact/user.svg",
+ "a photo is shown in contact view"
+ );
+
+ let [card, uid] = await createdPromise;
+ Assert.equal(uid, book.UID);
+ return card;
+}
+
+/** Create another new contact. This time we'll add a photo, but discard it. */
+async function subtest_dont_add_photo(book) {
+ let abWindow = getAddressBookWindow();
+ let abDocument = abWindow.document;
+
+ let createContactButton = abDocument.getElementById("toolbarCreateContact");
+ let saveEditButton = abDocument.getElementById("saveEditButton");
+ let photoButton = abDocument.getElementById("photoButton");
+ let editPhoto = photoButton.querySelector(".contact-photo");
+ let viewPhoto = abDocument.getElementById("viewContactPhoto");
+ let dialog = abWindow.document.getElementById("photoDialog");
+ let { saveButton, cancelButton, discardButton } = dialog;
+ let svg = dialog.querySelector("svg");
+
+ EventUtils.synthesizeMouseAtCenter(createContactButton, {}, abWindow);
+ await inEditingMode();
+
+ // Drop a file on the photo.
+
+ dropFile(photoButton, "data/photo2.jpg");
+ await waitForDialogOpenState(true);
+ await TestUtils.waitForCondition(() => BrowserTestUtils.is_visible(svg));
+
+ checkDialogElements({
+ svgVisible: true,
+ saveButtonVisible: true,
+ });
+
+ // Cancel the photo dialog.
+
+ EventUtils.synthesizeMouseAtCenter(cancelButton, {}, abWindow);
+ await waitForDialogOpenState(false);
+ Assert.equal(
+ editPhoto.src,
+ "chrome://messenger/skin/icons/new/compact/user.svg",
+ "no photo shown"
+ );
+
+ // Open the photo dialog by clicking on the photo.
+
+ EventUtils.synthesizeMouseAtCenter(photoButton, {}, abWindow);
+ await waitForDialogOpenState(true);
+
+ checkDialogElements({
+ dropTargetClass: "drop-target",
+ saveButtonVisible: true,
+ saveButtonDisabled: true,
+ });
+
+ // Drop a file on the photo dialog.
+
+ let previewChangePromise = waitForPreviewChange();
+ dropFile(dialog, "data/photo1.jpg");
+ await previewChangePromise;
+
+ checkDialogElements({
+ svgVisible: true,
+ saveButtonVisible: true,
+ });
+
+ // Drop another file on the photo dialog.
+
+ previewChangePromise = waitForPreviewChange();
+ dropFile(dialog, "data/photo2.jpg");
+ await previewChangePromise;
+
+ checkDialogElements({
+ svgVisible: true,
+ saveButtonVisible: true,
+ });
+
+ // Accept the photo dialog.
+
+ let photoChangePromise = waitForPhotoChange();
+ EventUtils.synthesizeMouseAtCenter(saveButton, {}, abWindow);
+ await photoChangePromise;
+ Assert.notEqual(
+ editPhoto.src,
+ "chrome://messenger/skin/icons/new/compact/user.svg",
+ "a photo is shown"
+ );
+
+ // Open the photo dialog by clicking on the photo.
+
+ EventUtils.synthesizeMouseAtCenter(photoButton, {}, abWindow);
+ await waitForDialogOpenState(true);
+
+ checkDialogElements({
+ svgVisible: true,
+ saveButtonVisible: true,
+ discardButtonVisible: true,
+ });
+
+ // Click to discard the photo.
+
+ photoChangePromise = waitForPhotoChange();
+ EventUtils.synthesizeMouseAtCenter(discardButton, {}, abWindow);
+ await photoChangePromise;
+
+ // Open the photo dialog by clicking on the photo.
+
+ EventUtils.synthesizeMouseAtCenter(photoButton, {}, abWindow);
+ await waitForDialogOpenState(true);
+
+ checkDialogElements({
+ dropTargetClass: "drop-target",
+ saveButtonVisible: true,
+ saveButtonDisabled: true,
+ });
+
+ EventUtils.synthesizeMouseAtCenter(cancelButton, {}, abWindow);
+ await waitForDialogOpenState(false);
+ Assert.equal(
+ editPhoto.src,
+ "chrome://messenger/skin/icons/new/compact/user.svg",
+ "no photo shown"
+ );
+
+ // Save the contact and check the photo was NOT saved.
+
+ let createdPromise = TestUtils.topicObserved("addrbook-contact-created");
+ setInputValues({
+ DisplayName: "Person with Photo 2",
+ });
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode();
+
+ Assert.equal(
+ viewPhoto.src,
+ "chrome://messenger/skin/icons/new/compact/user.svg",
+ "no photo shown in contact view"
+ );
+
+ let [card, uid] = await createdPromise;
+ Assert.equal(uid, book.UID);
+ return card;
+}
+
+/** Go back to the first contact and discard the photo. */
+async function subtest_discard_photo(book, checkPhotoCallback) {
+ let abWindow = getAddressBookWindow();
+ let abDocument = abWindow.document;
+
+ let cardsList = abDocument.getElementById("cards");
+ let editButton = abDocument.getElementById("editButton");
+ let saveEditButton = abDocument.getElementById("saveEditButton");
+ let photoButton = abDocument.getElementById("photoButton");
+ let editPhoto = photoButton.querySelector(".contact-photo");
+ let viewPhoto = abDocument.getElementById("viewContactPhoto");
+ let dialog = abWindow.document.getElementById("photoDialog");
+ let { discardButton } = dialog;
+
+ openDirectory(book);
+
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(0), {}, abWindow);
+ Assert.ok(
+ checkPhotoCallback(viewPhoto.src),
+ "saved photo shown in contact view"
+ );
+ EventUtils.synthesizeMouseAtCenter(editButton, {}, abWindow);
+ await inEditingMode();
+
+ // Open the photo dialog by clicking on the photo.
+
+ Assert.ok(
+ checkPhotoCallback(editPhoto.src),
+ "saved photo shown in edit view"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(photoButton, {}, abWindow);
+ await waitForDialogOpenState(true);
+
+ checkDialogElements({
+ svgVisible: true,
+ saveButtonVisible: true,
+ discardButtonVisible: true,
+ });
+
+ // Click to discard the photo.
+
+ let photoChangePromise = waitForPhotoChange();
+ EventUtils.synthesizeMouseAtCenter(discardButton, {}, abWindow);
+ await photoChangePromise;
+
+ // Save the contact and check the photo was removed.
+
+ let updatedPromise = TestUtils.topicObserved("addrbook-contact-updated");
+ EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
+ await notInEditingMode();
+ Assert.equal(
+ viewPhoto.src,
+ "chrome://messenger/skin/icons/new/compact/user.svg",
+ "photo no longer shown in contact view"
+ );
+
+ let [card, uid] = await updatedPromise;
+ Assert.equal(uid, book.UID);
+ return card;
+}
+
+/** Check that pasting URLs on photo widgets works. */
+async function subtest_paste_url() {
+ let abWindow = getAddressBookWindow();
+ let abDocument = abWindow.document;
+
+ let createContactButton = abDocument.getElementById("toolbarCreateContact");
+ let cancelEditButton = abDocument.getElementById("cancelEditButton");
+ let photoButton = abDocument.getElementById("photoButton");
+ let editPhoto = photoButton.querySelector(".contact-photo");
+ let dropTarget = abDocument.getElementById("photoDropTarget");
+
+ // Start a new contact and focus on the photo button.
+
+ EventUtils.synthesizeMouseAtCenter(createContactButton, {}, abWindow);
+ await inEditingMode();
+
+ Assert.equal(
+ editPhoto.src,
+ "chrome://messenger/skin/icons/new/compact/user.svg",
+ "no photo shown"
+ );
+
+ Assert.equal(abDocument.activeElement.id, "vcard-n-firstname");
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true }, abWindow);
+ // Focus is on name prefix button.
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true }, abWindow);
+ Assert.equal(
+ abDocument.activeElement,
+ photoButton,
+ "photo button is focused"
+ );
+
+ // Paste a URL.
+
+ let previewChangePromise = waitForPreviewChange();
+
+ let wrapper1 = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+ wrapper1.data =
+ "http://mochi.test:8888/browser/comm/mail/components/addrbook/test/browser/data/photo1.jpg";
+ let transfer1 = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+ transfer1.init(null);
+ transfer1.addDataFlavor("text/plain");
+ transfer1.setTransferData("text/plain", wrapper1);
+ Services.clipboard.setData(transfer1, null, Ci.nsIClipboard.kGlobalClipboard);
+ EventUtils.synthesizeKey("v", { accelKey: true }, abWindow);
+
+ await waitForDialogOpenState(true);
+ await previewChangePromise;
+ checkDialogElements({
+ svgVisible: true,
+ saveButtonVisible: true,
+ saveButtonDisabled: false,
+ });
+
+ // Close then reopen the dialog.
+
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, abWindow);
+ await waitForDialogOpenState(false);
+
+ EventUtils.synthesizeMouseAtCenter(photoButton, {}, abWindow);
+ await waitForDialogOpenState(true);
+ checkDialogElements({
+ dropTargetClass: "drop-target",
+ saveButtonVisible: true,
+ saveButtonDisabled: true,
+ });
+
+ // Paste a URL.
+
+ previewChangePromise = waitForPreviewChange();
+
+ let wrapper2 = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+ wrapper2.data =
+ "http://mochi.test:8888/browser/comm/mail/components/addrbook/test/browser/data/photo2.jpg";
+ let transfer2 = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+ transfer2.init(null);
+ transfer2.addDataFlavor("text/plain");
+ transfer2.setTransferData("text/plain", wrapper2);
+ Services.clipboard.setData(transfer2, null, Ci.nsIClipboard.kGlobalClipboard);
+ EventUtils.synthesizeKey("v", { accelKey: true }, abWindow);
+
+ await previewChangePromise;
+ checkDialogElements({
+ svgVisible: true,
+ saveButtonVisible: true,
+ saveButtonDisabled: false,
+ });
+
+ // Close then reopen the dialog.
+
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, abWindow);
+ await waitForDialogOpenState(false);
+
+ EventUtils.synthesizeMouseAtCenter(photoButton, {}, abWindow);
+ await waitForDialogOpenState(true);
+ checkDialogElements({
+ dropTargetClass: "drop-target",
+ saveButtonVisible: true,
+ saveButtonDisabled: true,
+ });
+
+ // Paste an invalid URL.
+
+ let wrapper3 = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+ wrapper3.data =
+ "http://mochi.test:8888/browser/comm/mail/components/addrbook/test/browser/data/fake.jpg";
+ let transfer3 = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+ transfer3.init(null);
+ transfer3.addDataFlavor("text/plain");
+ transfer3.setTransferData("text/plain", wrapper3);
+ Services.clipboard.setData(transfer3, null, Ci.nsIClipboard.kGlobalClipboard);
+ EventUtils.synthesizeKey("v", { accelKey: true }, abWindow);
+
+ await TestUtils.waitForCondition(() =>
+ dropTarget.classList.contains("drop-error")
+ );
+
+ checkDialogElements({
+ dropTargetClass: "drop-error",
+ saveButtonVisible: true,
+ saveButtonDisabled: true,
+ });
+
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, abWindow);
+ await waitForDialogOpenState(false);
+
+ EventUtils.synthesizeMouseAtCenter(cancelEditButton, {}, abWindow);
+ await notInEditingMode();
+}
+
+/** Test photo operations with a local address book. */
+add_task(async function test_local() {
+ // Create a new contact. We'll add a photo to this contact.
+
+ let card1 = await subtest_add_photo(personalBook);
+ let photo1Name = card1.getProperty("PhotoName", "");
+ Assert.ok(photo1Name, "PhotoName property saved on card");
+
+ let photo1Path = PathUtils.join(profileDir, "Photos", photo1Name);
+ let photo1File = new FileUtils.File(photo1Path);
+ Assert.ok(photo1File.exists(), "photo saved to disk");
+
+ let image = new Image();
+ let loadedPromise = BrowserTestUtils.waitForEvent(image, "load");
+ image.src = Services.io.newFileURI(photo1File).spec;
+ await loadedPromise;
+
+ Assert.equal(image.naturalWidth, 300, "photo saved at correct width");
+ Assert.equal(image.naturalHeight, 300, "photo saved at correct height");
+
+ // Create another new contact. This time we'll add a photo, but discard it.
+
+ let card2 = await subtest_dont_add_photo(personalBook);
+ Assert.equal(
+ card2.getProperty("PhotoName", "NO VALUE"),
+ "NO VALUE",
+ "PhotoName property not saved on card"
+ );
+
+ // Go back to the first contact and discard the photo.
+
+ let card3 = await subtest_discard_photo(personalBook, src =>
+ src.endsWith(photo1Name)
+ );
+ Assert.equal(
+ card3.getProperty("PhotoName", "NO VALUE"),
+ "NO VALUE",
+ "PhotoName property removed from card"
+ );
+ Assert.ok(
+ !new FileUtils.File(photo1Path).exists(),
+ "photo removed from disk"
+ );
+
+ // Check that pasting URLs on photo widgets works.
+
+ await subtest_paste_url(personalBook);
+});
+
+/**
+ * Test photo operations with a CardDAV address book and a server that only
+ * speaks vCard 3, i.e. Google.
+ */
+add_task(async function test_add_photo_carddav3() {
+ // Set up the server, address book and password.
+
+ CardDAVServer.open("alice", "alice");
+ CardDAVServer.mimicGoogle = true;
+
+ let book = createAddressBook(
+ "CardDAV Book",
+ Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE
+ );
+ book.setIntValue("carddav.syncinterval", 0);
+ book.setStringValue("carddav.url", CardDAVServer.url);
+ book.setStringValue("carddav.username", "alice");
+ book.setBoolValue("carddav.vcard3", true);
+ book.wrappedJSObject._isGoogleCardDAV = true;
+
+ let loginInfo = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
+ Ci.nsILoginInfo
+ );
+ loginInfo.init(CardDAVServer.origin, null, "test", "alice", "alice", "", "");
+ Services.logins.addLogin(loginInfo);
+
+ // Create a new contact. We'll add a photo to this contact.
+
+ // This notification fires when we retrieve the saved card from the server,
+ // which happens before subtest_add_photo finishes.
+ let updatedPromise = TestUtils.topicObserved("addrbook-contact-updated");
+ let card1 = await subtest_add_photo(book);
+ Assert.equal(
+ card1.getProperty("PhotoName", "RIGHT"),
+ "RIGHT",
+ "PhotoName property not saved on card"
+ );
+
+ // Check the card we sent.
+ let photoProp = card1.vCardProperties.getFirstEntry("photo");
+ Assert.ok(card1.vCardProperties.designSet === ICAL.design.vcard3);
+ Assert.ok(photoProp);
+ Assert.equal(photoProp.params.encoding, "B");
+ Assert.equal(photoProp.type, "binary");
+ Assert.ok(photoProp.value.startsWith("/9j/"));
+
+ // Check the card we received from the server. If the server didn't like it,
+ // the photo will be removed and this will fail.
+ let [card2] = await updatedPromise;
+ photoProp = card2.vCardProperties.getFirstEntry("photo");
+ Assert.ok(card2.vCardProperties.designSet === ICAL.design.vcard3);
+ Assert.ok(photoProp);
+ Assert.equal(photoProp.params.encoding, "B");
+ Assert.equal(photoProp.type, "binary");
+ Assert.ok(photoProp.value.startsWith("/9j/"));
+
+ // Check the card on the server.
+ Assert.equal(CardDAVServer.cards.size, 1);
+ let [serverCard] = [...CardDAVServer.cards.values()];
+ Assert.ok(
+ serverCard.vCard.includes("\nPHOTO;ENCODING=B:/9j/"),
+ "photo included in card on server"
+ );
+
+ // Discard the photo.
+
+ let card3 = await subtest_discard_photo(book, src =>
+ src.startsWith("data:image/jpeg;base64,/9j/")
+ );
+
+ // Check the card we sent.
+ Assert.equal(card3.vCardProperties.getFirstEntry("photo"), null);
+
+ // This notification is the second of two, and fires when we retrieve the
+ // saved card from the server, which doesn't happen before
+ // subtest_discard_photo finishes.
+ let [card4] = await TestUtils.topicObserved("addrbook-contact-updated");
+ Assert.equal(card4.vCardProperties.getFirstEntry("photo"), null);
+
+ // Check the card on the server.
+ Assert.equal(CardDAVServer.cards.size, 1);
+ [serverCard] = [...CardDAVServer.cards.values()];
+ Assert.ok(
+ !serverCard.vCard.includes("PHOTO:"),
+ "photo removed from card on server"
+ );
+
+ await promiseDirectoryRemoved(book.URI);
+ CardDAVServer.mimicGoogle = false;
+ CardDAVServer.close();
+ CardDAVServer.reset();
+});
+
+/**
+ * Test photo operations with a CardDAV address book and a server that can
+ * handle vCard 4.
+ */
+add_task(async function test_add_photo_carddav4() {
+ // Set up the server, address book and password.
+
+ CardDAVServer.open("bob", "bob");
+
+ let book = createAddressBook(
+ "CardDAV Book",
+ Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE
+ );
+ book.setIntValue("carddav.syncinterval", 0);
+ book.setStringValue("carddav.url", CardDAVServer.url);
+ book.setStringValue("carddav.username", "bob");
+
+ let loginInfo = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
+ Ci.nsILoginInfo
+ );
+ loginInfo.init(CardDAVServer.origin, null, "test", "bob", "bob", "", "");
+ Services.logins.addLogin(loginInfo);
+
+ // Create a new contact. We'll add a photo to this contact.
+
+ // This notification fires when we retrieve the saved card from the server,
+ // which happens before subtest_add_photo finishes.
+ let updatedPromise = TestUtils.topicObserved("addrbook-contact-updated");
+ let card1 = await subtest_add_photo(book);
+ Assert.equal(
+ card1.getProperty("PhotoName", "RIGHT"),
+ "RIGHT",
+ "PhotoName property not saved on card"
+ );
+
+ // Check the card we sent.
+ let photoProp = card1.vCardProperties.getFirstEntry("photo");
+ Assert.ok(card1.vCardProperties.designSet === ICAL.design.vcard);
+ Assert.ok(photoProp);
+ Assert.equal(photoProp.params.encoding, undefined);
+ Assert.equal(photoProp.type, "uri");
+ Assert.ok(photoProp.value.startsWith("data:image/jpeg;base64,/9j/"));
+
+ // Check the card we received from the server.
+ let [card2] = await updatedPromise;
+ photoProp = card2.vCardProperties.getFirstEntry("photo");
+ Assert.ok(card2.vCardProperties.designSet === ICAL.design.vcard);
+ Assert.ok(photoProp);
+ Assert.equal(photoProp.params.encoding, undefined);
+ Assert.equal(photoProp.type, "uri");
+ Assert.ok(photoProp.value.startsWith("data:image/jpeg;base64,/9j/"));
+
+ // Check the card on the server.
+ Assert.equal(CardDAVServer.cards.size, 1);
+ let [serverCard] = [...CardDAVServer.cards.values()];
+ Assert.ok(
+ serverCard.vCard.includes("\nPHOTO:data:image/jpeg;base64\\,/9j/"),
+ "photo included in card on server"
+ );
+
+ // Discard the photo.
+
+ let card3 = await subtest_discard_photo(book, src =>
+ src.startsWith("data:image/jpeg;base64,/9j/")
+ );
+
+ // Check the card we sent.
+ Assert.equal(card3.vCardProperties.getFirstEntry("photo"), null);
+
+ // This notification is the second of two, and fires when we retrieve the
+ // saved card from the server, which doesn't happen before
+ // subtest_discard_photo finishes.
+ let [card4] = await TestUtils.topicObserved("addrbook-contact-updated");
+ Assert.equal(card4.vCardProperties.getFirstEntry("photo"), null);
+
+ // Check the card on the server.
+ Assert.equal(CardDAVServer.cards.size, 1);
+ [serverCard] = [...CardDAVServer.cards.values()];
+ console.log(serverCard.vCard);
+ Assert.ok(
+ !serverCard.vCard.includes("PHOTO:"),
+ "photo removed from card on server"
+ );
+
+ await promiseDirectoryRemoved(book.URI);
+ CardDAVServer.close();
+ CardDAVServer.reset();
+});
diff --git a/comm/mail/components/addrbook/test/browser/browser_ldap_search.js b/comm/mail/components/addrbook/test/browser/browser_ldap_search.js
new file mode 100644
index 0000000000..6eb7322bb4
--- /dev/null
+++ b/comm/mail/components/addrbook/test/browser/browser_ldap_search.js
@@ -0,0 +1,180 @@
+/* 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 { LDAPServer } = ChromeUtils.import(
+ "resource://testing-common/LDAPServer.jsm"
+);
+
+const jsonFile =
+ "http://mochi.test:8888/browser/comm/mail/components/addrbook/test/browser/ldap_contacts.json";
+
+add_task(async () => {
+ function waitForCountChange(expectedCount) {
+ return new Promise(resolve => {
+ cardsList.addEventListener("rowcountchange", function onRowCountChange() {
+ console.log(cardsList.view.rowCount, expectedCount);
+ if (cardsList.view.rowCount == expectedCount) {
+ cardsList.removeEventListener("rowcountchange", onRowCountChange);
+ resolve();
+ }
+ });
+ });
+ }
+
+ // Set up some local people.
+
+ let cardsToRemove = [];
+ for (let name of ["daniel", "jonathan", "nathan"]) {
+ let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ card.displayName = name;
+
+ card = personalBook.addCard(card);
+ cardsToRemove.push(card);
+ }
+
+ // Set up the LDAP server.
+
+ LDAPServer.open();
+ let response = await fetch(jsonFile);
+ let ldapContacts = await response.json();
+
+ let bookPref = MailServices.ab.newAddressBook(
+ "Mochitest",
+ `ldap://localhost:${LDAPServer.port}/`,
+ 0
+ );
+ let book = MailServices.ab.getDirectoryFromId(bookPref);
+
+ let abWindow = await openAddressBookWindow();
+ let abDocument = abWindow.document;
+
+ let searchBox = abDocument.getElementById("searchInput");
+ let cardsList = abWindow.cardsPane.cardsList;
+ let noSearchResults = abDocument.getElementById("placeholderNoSearchResults");
+ let detailsPane = abDocument.getElementById("detailsPane");
+
+ // Search for some people in the LDAP directory.
+
+ openDirectory(book);
+ checkPlaceholders(["placeholderSearchOnly"]);
+
+ EventUtils.synthesizeMouseAtCenter(searchBox, {}, abWindow);
+ EventUtils.sendString("holmes", abWindow);
+
+ await LDAPServer.read(LDAPServer.BindRequest);
+ LDAPServer.writeBindResponse();
+ checkNamesListed();
+ checkPlaceholders(["placeholderSearching"]);
+
+ await LDAPServer.read(LDAPServer.SearchRequest);
+ LDAPServer.writeSearchResultEntry(ldapContacts.mycroft);
+ LDAPServer.writeSearchResultEntry(ldapContacts.sherlock);
+ LDAPServer.writeSearchResultDone();
+
+ Assert.ok(BrowserTestUtils.is_hidden(detailsPane));
+ await waitForCountChange(2);
+ checkNamesListed("Mycroft Holmes", "Sherlock Holmes");
+ checkPlaceholders();
+
+ // Check that displaying an LDAP card works without error.
+ EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(0), {}, abWindow);
+ await TestUtils.waitForCondition(() =>
+ BrowserTestUtils.is_visible(detailsPane)
+ );
+
+ EventUtils.synthesizeMouseAtCenter(searchBox, {}, abWindow);
+ EventUtils.synthesizeKey("a", { accelKey: true }, abWindow);
+ EventUtils.sendString("john", abWindow);
+
+ await LDAPServer.read(LDAPServer.BindRequest);
+ LDAPServer.writeBindResponse();
+ checkNamesListed();
+ checkPlaceholders(["placeholderSearching"]);
+
+ await LDAPServer.read(LDAPServer.SearchRequest);
+ LDAPServer.writeSearchResultEntry(ldapContacts.john);
+ LDAPServer.writeSearchResultDone();
+
+ await waitForCountChange(1);
+ checkNamesListed("John Watson");
+ checkPlaceholders();
+
+ // Now move back to the "All Address Books" view and search again.
+ // The search string is retained when switching books.
+
+ openAllAddressBooks();
+ checkNamesListed();
+ Assert.equal(searchBox.value, "john");
+
+ await LDAPServer.read(LDAPServer.BindRequest);
+ LDAPServer.writeBindResponse();
+ checkNamesListed();
+ checkPlaceholders(["placeholderSearching"]);
+
+ await LDAPServer.read(LDAPServer.SearchRequest);
+ LDAPServer.writeSearchResultEntry(ldapContacts.john);
+ LDAPServer.writeSearchResultDone();
+
+ await waitForCountChange(1);
+ checkNamesListed("John Watson");
+ checkPlaceholders();
+
+ EventUtils.synthesizeMouseAtCenter(searchBox, {}, abWindow);
+ EventUtils.synthesizeKey("a", { accelKey: true }, abWindow);
+ EventUtils.sendString("irene", abWindow);
+
+ await LDAPServer.read(LDAPServer.BindRequest);
+ LDAPServer.writeBindResponse();
+ checkNamesListed();
+ checkPlaceholders(["placeholderSearching"]);
+
+ await LDAPServer.read(LDAPServer.SearchRequest);
+ LDAPServer.writeSearchResultEntry(ldapContacts.irene);
+ LDAPServer.writeSearchResultDone();
+
+ await waitForCountChange(1);
+ checkNamesListed("Irene Adler");
+ checkPlaceholders();
+
+ EventUtils.synthesizeMouseAtCenter(searchBox, {}, abWindow);
+ EventUtils.synthesizeKey("a", { accelKey: true }, abWindow);
+ EventUtils.sendString("jo", abWindow);
+
+ await LDAPServer.read(LDAPServer.BindRequest);
+ LDAPServer.writeBindResponse();
+ checkNamesListed("jonathan");
+ checkPlaceholders();
+
+ await LDAPServer.read(LDAPServer.SearchRequest);
+ LDAPServer.writeSearchResultEntry(ldapContacts.john);
+ LDAPServer.writeSearchResultDone();
+
+ await waitForCountChange(2);
+ checkNamesListed("John Watson", "jonathan");
+ checkPlaceholders();
+
+ EventUtils.synthesizeMouseAtCenter(searchBox, {}, abWindow);
+ EventUtils.synthesizeKey("a", { accelKey: true }, abWindow);
+ EventUtils.sendString("mark", abWindow);
+
+ await LDAPServer.read(LDAPServer.BindRequest);
+ LDAPServer.writeBindResponse();
+ checkNamesListed();
+ checkPlaceholders(["placeholderSearching"]);
+
+ await LDAPServer.read(LDAPServer.SearchRequest);
+ LDAPServer.writeSearchResultDone();
+ await TestUtils.waitForCondition(() =>
+ BrowserTestUtils.is_visible(noSearchResults)
+ );
+ checkNamesListed();
+ checkPlaceholders(["placeholderNoSearchResults"]);
+
+ await closeAddressBookWindow();
+ personalBook.deleteCards(cardsToRemove);
+ await promiseDirectoryRemoved(book.URI);
+ LDAPServer.close();
+});
diff --git a/comm/mail/components/addrbook/test/browser/browser_mailing_lists.js b/comm/mail/components/addrbook/test/browser/browser_mailing_lists.js
new file mode 100644
index 0000000000..64d679ec13
--- /dev/null
+++ b/comm/mail/components/addrbook/test/browser/browser_mailing_lists.js
@@ -0,0 +1,474 @@
+/* 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, MailUtils */
+
+var { DisplayNameUtils } = ChromeUtils.import(
+ "resource:///modules/DisplayNameUtils.jsm"
+);
+var { mailTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MailTestUtils.jsm"
+);
+
+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 bookPrefName = MailServices.ab.newAddressBook(
+ inputs.abName,
+ null,
+ Ci.nsIAbManager.JS_DIRECTORY_TYPE
+ );
+ let addressBook = MailServices.ab.getDirectoryFromId(bookPrefName);
+
+ let abWindow = await openAddressBookWindow();
+
+ global = {
+ abWindow,
+ addressBook,
+ booksList: abWindow.booksList,
+ mailListUID: undefined,
+ };
+});
+
+/**
+ * Create a new mailing list with some addresses, in the new address book.
+ */
+add_task(async () => {
+ let mailingListWindowPromise = promiseLoadSubDialog(
+ "chrome://messenger/content/addressbook/abMailListDialog.xhtml"
+ ).then(async function (mlWindow) {
+ let mlDocument = mlWindow.document;
+ let mlDocElement = mlDocument.querySelector("dialog");
+
+ let listName = mlDocument.getElementById("ListName");
+ if (mlDocument.activeElement != listName) {
+ 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;
+
+ Assert.equal(
+ abPopup.label,
+ global.addressBook.dirName,
+ "the correct address book is selected in the menu"
+ );
+ Assert.equal(
+ abPopup.value,
+ global.addressBook.URI,
+ "the address book selected in the menu has the correct address book URI"
+ );
+ Assert.equal(listName.value, "", "no text in the list name field");
+ Assert.equal(listNickName.value, "", "no text in the list nickname field");
+ Assert.equal(listDescription.value, "", "no text in the description field");
+ Assert.equal(addressInput1.value, "", "no text in the addresses list");
+ Assert.equal(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();
+ });
+
+ // Select the address book.
+ openDirectory(global.addressBook);
+
+ // Open the new mailing list dialog, the callback above interacts with it.
+ EventUtils.synthesizeMouseAtCenter(
+ global.abWindow.document.getElementById("toolbarCreateList"),
+ { clickCount: 1 },
+ global.abWindow
+ );
+
+ await mailingListWindowPromise;
+
+ // Confirm that the mailing list and addresses were saved in the backend.
+
+ Assert.ok(
+ MailServices.ab.cardForEmailAddress(inputs.addresses[0]),
+ "address zero was saved"
+ );
+ Assert.ok(
+ MailServices.ab.cardForEmailAddress(inputs.addresses[1]),
+ "address one was saved"
+ );
+
+ let childCards = global.addressBook.childCards;
+
+ Assert.ok(
+ childCards.find(card => card.primaryEmail == inputs.addresses[0]),
+ "address zero was saved in the correct address book"
+ );
+ Assert.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;
+
+ Assert.ok(mailList, "mailing list was created");
+ Assert.ok(
+ global.addressBook.hasMailListWithName(inputs.mlName),
+ "mailing list was created in the correct address book"
+ );
+ Assert.equal(mailList.dirName, inputs.mlName, "mailing list name was saved");
+ Assert.equal(
+ mailList.listNickName,
+ inputs.nickName,
+ "mailing list nick name was saved"
+ );
+ Assert.equal(
+ mailList.description,
+ inputs.description,
+ "mailing list description was saved"
+ );
+
+ let listCards = mailList.childCards;
+ Assert.equal(listCards.length, 2, "two cards exist in the mailing list");
+ Assert.ok(
+ listCards[0].hasEmailAddress(inputs.addresses[0]),
+ "address zero was saved in the mailing list"
+ );
+ Assert.ok(
+ listCards[1].hasEmailAddress(inputs.addresses[1]),
+ "address one was saved in the mailing list"
+ );
+});
+
+/**
+ * Open the mailing list dialog and modify the mailing list.
+ */
+add_task(async () => {
+ let mailingListWindowPromise = promiseLoadSubDialog(
+ "chrome://messenger/content/addressbook/abEditListDialog.xhtml"
+ ).then(async function (mlWindow) {
+ let mlDocument = mlWindow.document;
+ let mlDocElement = mlDocument.querySelector("dialog");
+
+ if (!mlDocument.getElementById("addressCol1#3")) {
+ // 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"
+ );
+ }
+
+ if (mlDocument.activeElement.id != "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");
+
+ Assert.equal(
+ listName.value,
+ inputs.mlName,
+ "list name is displayed correctly"
+ );
+ Assert.equal(
+ listNickName.value,
+ inputs.nickName,
+ "list nickname is displayed correctly"
+ );
+ Assert.equal(
+ listDescription.value,
+ inputs.description,
+ "list description is displayed correctly"
+ );
+ Assert.equal(
+ addressInput1 && addressInput1.value,
+ getDisplayedAddress(inputs.addresses[0]),
+ "address zero is displayed correctly"
+ );
+ Assert.equal(
+ addressInput2 && addressInput2.value,
+ getDisplayedAddress(inputs.addresses[1]),
+ "address one is displayed correctly"
+ );
+
+ let textInputs = mlDocument.querySelectorAll(".textbox-addressingWidget");
+ Assert.equal(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 => {
+ id.focus();
+ EventUtils.sendKey("DOWN", mlWindow);
+ EventUtils.sendString(inputs.modification, mlWindow);
+ };
+ modifyField(listName);
+ modifyField(listNickName);
+ modifyField(listDescription);
+
+ mlDocElement.getButton("accept").click();
+ });
+
+ // Open the mailing list dialog, the callback above interacts with it.
+ global.booksList.selectedIndex = 3;
+ global.booksList.showPropertiesOfSelected();
+
+ await mailingListWindowPromise;
+
+ // Confirm that the mailing list and addresses were saved in the backend.
+
+ Assert.equal(
+ global.booksList.getRowAtIndex(3).querySelector("span").textContent,
+ inputs.mlName + inputs.modification,
+ `mailing list ("${
+ inputs.mlName + inputs.modification
+ }") is displayed in the address book list`
+ );
+
+ Assert.ok(
+ MailServices.ab.cardForEmailAddress(inputs.addresses[2]),
+ "address two was saved"
+ );
+ Assert.ok(
+ MailServices.ab.cardForEmailAddress(inputs.addresses[3]),
+ "address three was saved"
+ );
+
+ let childCards = global.addressBook.childCards;
+
+ Assert.ok(
+ childCards.find(card => card.primaryEmail == inputs.addresses[2]),
+ "address two was saved in the correct address book"
+ );
+ Assert.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
+ );
+
+ Assert.equal(
+ mailList && mailList.UID,
+ global.mailListUID,
+ "mailing list still exists"
+ );
+
+ Assert.ok(
+ global.addressBook.hasMailListWithName(inputs.mlName + inputs.modification),
+ "mailing list is still in the correct address book"
+ );
+ Assert.equal(
+ mailList.dirName,
+ inputs.mlName + inputs.modification,
+ "modified mailing list name was saved"
+ );
+ Assert.equal(
+ mailList.listNickName,
+ inputs.nickName + inputs.modification,
+ "modified mailing list nick name was saved"
+ );
+ Assert.equal(
+ mailList.description,
+ inputs.description + inputs.modification,
+ "modified mailing list description was saved"
+ );
+
+ let listCards = mailList.childCards;
+
+ Assert.equal(listCards.length, 3, "three cards exist in the mailing list");
+
+ Assert.ok(
+ listCards[0].hasEmailAddress(inputs.addresses[0]),
+ "address zero was saved in the mailing list (is still there)"
+ );
+ Assert.ok(
+ listCards[1].hasEmailAddress(inputs.addresses[2]),
+ "address two was saved in the mailing list"
+ );
+ Assert.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])
+ );
+
+ Assert.ok(!hasAddressOne, "address one was deleted from the mailing list");
+});
+
+/**
+ * Open the mailing list dialog and confirm the changes are displayed.
+ */
+add_task(async () => {
+ let mailingListWindowPromise = promiseLoadSubDialog(
+ "chrome://messenger/content/addressbook/abEditListDialog.xhtml"
+ ).then(async function (mailingListWindow) {
+ let mlDocument = mailingListWindow.document;
+ let mlDocElement = mlDocument.querySelector("dialog");
+
+ if (!mlDocument.getElementById("addressCol1#4")) {
+ // 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"
+ );
+ }
+
+ if (mlDocument.activeElement.id != "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");
+
+ Assert.equal(
+ listName.value,
+ inputs.mlName + inputs.modification,
+ "modified list name is displayed correctly"
+ );
+ Assert.equal(
+ listNickName.value,
+ inputs.nickName + inputs.modification,
+ "modified list nickname is displayed correctly"
+ );
+ Assert.equal(
+ listDescription.value,
+ inputs.description + inputs.modification,
+ "modified list description is displayed correctly"
+ );
+ Assert.equal(
+ addressInput1 && addressInput1.value,
+ getDisplayedAddress(inputs.addresses[0]),
+ "address zero is displayed correctly (is still there)"
+ );
+ Assert.equal(
+ addressInput2 && addressInput2.value,
+ getDisplayedAddress(inputs.addresses[2]),
+ "address two is displayed correctly"
+ );
+ Assert.equal(
+ addressInput3 && addressInput3.value,
+ getDisplayedAddress(inputs.addresses[3]),
+ "address three is displayed correctly"
+ );
+
+ let textInputs = mlDocument.querySelectorAll(".textbox-addressingWidget");
+ Assert.equal(textInputs.length, 4, "no extraneous addresses are displayed");
+
+ mlDocElement.getButton("cancel").click();
+ });
+
+ Assert.equal(
+ global.booksList.getRowAtIndex(3).querySelector("span").textContent,
+ inputs.mlName + inputs.modification,
+ `mailing list ("${
+ inputs.mlName + inputs.modification
+ }") is still displayed in the address book list`
+ );
+
+ // Open the mailing list dialog, the callback above interacts with it.
+ global.booksList.selectedIndex = 3;
+ global.booksList.showPropertiesOfSelected();
+
+ 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.xhtml"
+ );
+ let deletePromise = TestUtils.topicObserved("addrbook-directory-deleted");
+
+ Assert.equal(
+ global.booksList.getRowAtIndex(2).querySelector("span").textContent,
+ inputs.abName,
+ `address book ("${inputs.abName}") is displayed in the address book list`
+ );
+
+ global.booksList.focus();
+ global.booksList.selectedIndex = 2;
+ EventUtils.sendKey("DELETE", global.abWindow);
+
+ await Promise.all([mailingListWindowPromise, deletePromise]);
+
+ let addressBook = MailServices.ab.directories.find(
+ directory => directory.dirName == inputs.abName
+ );
+
+ Assert.ok(!addressBook, "address book was deleted");
+
+ closeAddressBookWindow();
+});
diff --git a/comm/mail/components/addrbook/test/browser/browser_open_actions.js b/comm/mail/components/addrbook/test/browser/browser_open_actions.js
new file mode 100644
index 0000000000..cb6f681ec8
--- /dev/null
+++ b/comm/mail/components/addrbook/test/browser/browser_open_actions.js
@@ -0,0 +1,157 @@
+/* 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/. */
+
+let tabmail = document.getElementById("tabmail");
+let writableBook, writableCard, readOnlyBook, readOnlyCard;
+
+add_setup(function () {
+ writableBook = createAddressBook("writable book");
+ writableCard = writableBook.addCard(createContact("writable", "card"));
+
+ readOnlyBook = createAddressBook("read-only book");
+ readOnlyCard = readOnlyBook.addCard(createContact("read-only", "card"));
+ readOnlyBook.setBoolValue("readOnly", true);
+
+ registerCleanupFunction(async function () {
+ await promiseDirectoryRemoved(writableBook.URI);
+ await promiseDirectoryRemoved(readOnlyBook.URI);
+ });
+});
+
+async function inEditingMode() {
+ let abWindow = getAddressBookWindow();
+ await TestUtils.waitForCondition(
+ () => abWindow.detailsPane.isEditing,
+ "entering editing mode"
+ );
+}
+
+async function notInEditingMode() {
+ let abWindow = getAddressBookWindow();
+ await TestUtils.waitForCondition(
+ () => !abWindow.detailsPane.isEditing,
+ "leaving editing mode"
+ );
+}
+
+/**
+ * Tests than a `toAddressBook` call with no argument opens the Address Book.
+ * Then call it again with the tab open and check that it doesn't reload.
+ */
+add_task(async function testNoAction() {
+ let abWindow1 = await window.toAddressBook();
+ Assert.equal(tabmail.tabInfo.length, 2);
+ Assert.equal(tabmail.currentTabInfo.mode.name, "addressBookTab");
+ await notInEditingMode();
+
+ let abWindow2 = await window.toAddressBook();
+ Assert.equal(tabmail.tabInfo.length, 2);
+ Assert.equal(tabmail.currentTabInfo.mode.name, "addressBookTab");
+ Assert.equal(
+ abWindow2.browsingContext.currentWindowGlobal.innerWindowId,
+ abWindow1.browsingContext.currentWindowGlobal.innerWindowId,
+ "address book page did not reload"
+ );
+ await notInEditingMode();
+
+ tabmail.selectTabByIndex(undefined, 1);
+ let abWindow3 = await window.toAddressBook();
+ Assert.equal(tabmail.tabInfo.length, 2);
+ Assert.equal(tabmail.currentTabInfo.mode.name, "addressBookTab");
+ Assert.equal(
+ abWindow3.browsingContext.currentWindowGlobal.innerWindowId,
+ abWindow1.browsingContext.currentWindowGlobal.innerWindowId,
+ "address book page did not reload"
+ );
+ await notInEditingMode();
+
+ await closeAddressBookWindow();
+ Assert.equal(tabmail.tabInfo.length, 1);
+});
+
+/**
+ * Tests than a call to toAddressBook with only a create action opens the
+ * Address Book. A new blank card should open in edit mode.
+ */
+add_task(async function testCreateBlank() {
+ await window.toAddressBook({ action: "create" });
+ await inEditingMode();
+ // TODO check blank
+ await closeAddressBookWindow();
+});
+
+/**
+ * Tests than a call to toAddressBook with a create action and an email
+ * address opens the Address Book. A new card with the email address should
+ * open in edit mode.
+ */
+add_task(async function testCreateWithAddress() {
+ await window.toAddressBook({ action: "create", address: "test@invalid" });
+ await inEditingMode();
+ // TODO check address matches
+ await closeAddressBookWindow();
+});
+
+/**
+ * Tests than a call to toAddressBook with a create action and a vCard opens
+ * the Address Book. A new card should open in edit mode.
+ */
+add_task(async function testCreateWithVCard() {
+ await window.toAddressBook({
+ action: "create",
+ vCard:
+ "BEGIN:VCARD\r\nFN:a test person\r\nN:person;test;;a;\r\nEND:VCARD\r\n",
+ });
+ await inEditingMode();
+ // TODO check card matches
+ await closeAddressBookWindow();
+});
+
+/**
+ * Tests than a call to toAddressBook with a display action opens the Address
+ * Book. The card should be displayed.
+ */
+add_task(async function testDisplayCard() {
+ await window.toAddressBook({ action: "display", card: writableCard });
+ checkDirectoryDisplayed(writableBook);
+ await notInEditingMode();
+
+ // let abWindow = getAddressBookWindow();
+ // let h1 = abWindow.document.querySelector("h1");
+ // Assert.equal(h1.textContent, "writable contact");
+
+ await closeAddressBookWindow();
+});
+
+/**
+ * Tests than a call to toAddressBook with an edit action and a writable card
+ * opens the Address Book. The card should open in edit mode.
+ */
+add_task(async function testEditCardWritable() {
+ await window.toAddressBook({ action: "edit", card: writableCard });
+ checkDirectoryDisplayed(writableBook);
+ await inEditingMode();
+
+ // let abWindow = getAddressBookWindow();
+ // let h1 = abWindow.document.querySelector("h1");
+ // Assert.equal(h1.textContent, "writable contact");
+
+ await closeAddressBookWindow();
+});
+
+/**
+ * Tests than a call to toAddressBook with an edit action and a read-only card
+ * opens the Address Book. The card should open in display mode.
+ */
+add_task(async function testEditCardReadOnly() {
+ await window.toAddressBook({ action: "edit", card: readOnlyCard });
+ checkDirectoryDisplayed(readOnlyBook);
+ await notInEditingMode();
+
+ // let abWindow = getAddressBookWindow();
+ // let h1 = abWindow.document.querySelector("h1");
+ // Assert.equal(h1.textContent, "read-only contact");
+
+ await closeAddressBookWindow();
+});
diff --git a/comm/mail/components/addrbook/test/browser/browser_search.js b/comm/mail/components/addrbook/test/browser/browser_search.js
new file mode 100644
index 0000000000..ab4f7a221f
--- /dev/null
+++ b/comm/mail/components/addrbook/test/browser/browser_search.js
@@ -0,0 +1,139 @@
+/* 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_task(async () => {
+ async function doSearch(searchString, ...expectedCards) {
+ let viewChangePromise = BrowserTestUtils.waitForEvent(
+ cardsList,
+ "viewchange"
+ );
+ EventUtils.synthesizeMouseAtCenter(searchBox, {}, abWindow);
+ if (searchString) {
+ EventUtils.synthesizeKey("a", { accelKey: true }, abWindow);
+ EventUtils.sendString(searchString, abWindow);
+ EventUtils.synthesizeKey("VK_RETURN", {}, abWindow);
+ } else {
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, abWindow);
+ }
+
+ await viewChangePromise;
+ checkCardsListed(...expectedCards);
+ checkPlaceholders(
+ expectedCards.length ? [] : ["placeholderNoSearchResults"]
+ );
+ }
+
+ let cards = {};
+ let cardsToRemove = {
+ personal: [],
+ history: [],
+ };
+ for (let name of ["daniel", "jonathan", "nathan"]) {
+ let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ card.displayName = name;
+
+ card = personalBook.addCard(card);
+ cards[name] = card;
+ cardsToRemove.personal.push(card);
+ }
+ for (let name of ["danielle", "katherine", "natalie", "susanah"]) {
+ let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ card.displayName = name;
+
+ card = historyBook.addCard(card);
+ cards[name] = card;
+ cardsToRemove.history.push(card);
+ }
+
+ let abWindow = await openAddressBookWindow();
+
+ registerCleanupFunction(() => {
+ abWindow.close();
+ personalBook.deleteCards(cardsToRemove.personal);
+ historyBook.deleteCards(cardsToRemove.history);
+ });
+
+ let abDocument = abWindow.document;
+ let searchBox = abDocument.getElementById("searchInput");
+ let cardsList = abWindow.cardsPane.cardsList;
+
+ Assert.equal(
+ abDocument.activeElement,
+ searchBox,
+ "search box was focused when the page loaded"
+ );
+
+ // All address books.
+
+ checkCardsListed(
+ cards.daniel,
+ cards.danielle,
+ cards.jonathan,
+ cards.katherine,
+ cards.natalie,
+ cards.nathan,
+ cards.susanah
+ );
+ checkPlaceholders();
+
+ // Personal address book.
+
+ openDirectory(personalBook);
+ checkCardsListed(cards.daniel, cards.jonathan, cards.nathan);
+ checkPlaceholders();
+
+ await doSearch("daniel", cards.daniel);
+ await doSearch("nathan", cards.jonathan, cards.nathan);
+
+ // History address book.
+
+ openDirectory(historyBook);
+ checkCardsListed();
+ checkPlaceholders(["placeholderNoSearchResults"]);
+
+ await doSearch(
+ null,
+ cards.danielle,
+ cards.katherine,
+ cards.natalie,
+ cards.susanah
+ );
+
+ await doSearch("daniel", cards.danielle);
+ await doSearch("nathan");
+
+ // All address books.
+
+ openAllAddressBooks();
+ checkCardsListed(cards.jonathan, cards.nathan);
+ checkPlaceholders();
+
+ await doSearch(
+ null,
+ cards.daniel,
+ cards.danielle,
+ cards.jonathan,
+ cards.katherine,
+ cards.natalie,
+ cards.nathan,
+ cards.susanah
+ );
+
+ await doSearch("daniel", cards.daniel, cards.danielle);
+ await doSearch("nathan", cards.jonathan, cards.nathan);
+ await doSearch(
+ null,
+ cards.daniel,
+ cards.danielle,
+ cards.jonathan,
+ cards.katherine,
+ cards.natalie,
+ cards.nathan,
+ cards.susanah
+ );
+});
diff --git a/comm/mail/components/addrbook/test/browser/browser_telemetry.js b/comm/mail/components/addrbook/test/browser/browser_telemetry.js
new file mode 100644
index 0000000000..36b73207c2
--- /dev/null
+++ b/comm/mail/components/addrbook/test/browser/browser_telemetry.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test telemetry related to address book.
+ */
+
+let { MailTelemetryForTests } = ChromeUtils.import(
+ "resource:///modules/MailGlue.jsm"
+);
+let { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+/**
+ * Test we're counting address books and contacts.
+ */
+add_task(async function test_address_book_count() {
+ Services.telemetry.clearScalars();
+
+ // Adding some address books and contracts.
+ let addrBook1 = createAddressBook("AB 1");
+ let addrBook2 = createAddressBook("AB 2");
+ let ldapBook = createAddressBook(
+ "LDAP Book",
+ Ci.nsIAbManager.LDAP_DIRECTORY_TYPE
+ );
+
+ let contact1 = createContact("test1", "example");
+ let contact2 = createContact("test2", "example");
+ let contact3 = createContact("test3", "example");
+ addrBook1.addCard(contact1);
+ addrBook2.addCard(contact2);
+ addrBook2.addCard(contact3);
+
+ // Run the probe.
+ MailTelemetryForTests.reportAddressBookTypes();
+
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", true);
+ Assert.equal(
+ scalars["tb.addressbook.addressbook_count"]["moz-abldapdirectory"],
+ 1,
+ "LDAP address book count must be correct"
+ );
+ Assert.equal(
+ scalars["tb.addressbook.addressbook_count"].jsaddrbook,
+ 4,
+ "JS address book count must be correct"
+ );
+ Assert.equal(
+ scalars["tb.addressbook.contact_count"].jsaddrbook,
+ 3,
+ "Contact count must be correct"
+ );
+
+ await promiseDirectoryRemoved(addrBook1.URI);
+ await promiseDirectoryRemoved(addrBook2.URI);
+ await promiseDirectoryRemoved(ldapBook.URI);
+});
diff --git a/comm/mail/components/addrbook/test/browser/data/addressbook.sjs b/comm/mail/components/addrbook/test/browser/data/addressbook.sjs
new file mode 100644
index 0000000000..bd28437261
--- /dev/null
+++ b/comm/mail/components/addrbook/test/browser/data/addressbook.sjs
@@ -0,0 +1,47 @@
+/* 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/. */
+
+function handleRequest(request, response) {
+ if (!request.hasHeader("Authorization")) {
+ response.setStatusLine("1.1", 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", `Basic realm="test"`);
+ return;
+ }
+
+ response.setStatusLine("1.1", 207, "Multi-Status");
+ response.setHeader("Content-Type", "text/xml", false);
+
+ // Request:
+ // <propfind>
+ // <prop>
+ // <resourcetype/>
+ // <getetag/>
+ // <getctag/>
+ // </prop>
+ // </propfind>
+
+ response.write(`<multistatus xmlns="DAV:"
+ xmlns:card="urn:ietf:params:xml:ns:carddav"
+ xmlns:cs="http://calendarserver.org/ns/">
+ <response>
+ <href>/browser/comm/mail/components/addrbook/test/browser/data/addressbook.sjs</href>
+ <propstat>
+ <prop>
+ <resourcetype>
+ <collection/>
+ <card:addressbook/>
+ </resourcetype>
+ <cs:getctag>0</cs:getctag>
+ </prop>
+ <status>HTTP/1.1 200 OK</status>
+ </propstat>
+ <propstat>
+ <prop>
+ <getetag/>
+ </prop>
+ <status>HTTP/1.1 404 Not Found</status>
+ </propstat>
+ </response>
+ </multistatus>`);
+}
diff --git a/comm/mail/components/addrbook/test/browser/data/addressbooks.sjs b/comm/mail/components/addrbook/test/browser/data/addressbooks.sjs
new file mode 100644
index 0000000000..0380dee3ab
--- /dev/null
+++ b/comm/mail/components/addrbook/test/browser/data/addressbooks.sjs
@@ -0,0 +1,62 @@
+/* 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/. */
+
+function handleRequest(request, response) {
+ if (!request.hasHeader("Authorization")) {
+ response.setStatusLine("1.1", 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", `Basic realm="test"`);
+ return;
+ }
+
+ response.setStatusLine("1.1", 207, "Multi-Status");
+ response.setHeader("Content-Type", "text/xml", false);
+
+ // Request:
+ // <propfind>
+ // <prop>
+ // <resourcetype/>
+ // <displayname/>
+ // <current-user-privilege-set/>
+ // </prop>
+ // </propfind>
+
+ response.write(`<multistatus xmlns="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav">
+ <response>
+ <href>/browser/comm/mail/components/addrbook/test/browser/data/addressbooks.sjs</href>
+ <propstat>
+ <prop>
+ <resourcetype>
+ <collection/>
+ </resourcetype>
+ <displayname>Things found by DNS</displayname>
+ </prop>
+ <status>HTTP/1.1 200 OK</status>
+ </propstat>
+ <propstat>
+ <prop>
+ <current-user-privilege-set/>
+ </prop>
+ <status>HTTP/1.1 404 Not Found</status>
+ </propstat>
+ </response>
+ <response>
+ <href>/browser/comm/mail/components/addrbook/test/browser/data/addressbook.sjs</href>
+ <propstat>
+ <prop>
+ <resourcetype>
+ <collection/>
+ <card:addressbook/>
+ </resourcetype>
+ <displayname>You found me!</displayname>
+ <current-user-privilege-set>
+ <privilege>
+ <all/>
+ </privilege>
+ </current-user-privilege-set>
+ </prop>
+ <status>HTTP/1.1 200 OK</status>
+ </propstat>
+ </response>
+ </multistatus>`);
+}
diff --git a/comm/mail/components/addrbook/test/browser/data/auth_headers.sjs b/comm/mail/components/addrbook/test/browser/data/auth_headers.sjs
new file mode 100644
index 0000000000..640d2acc54
--- /dev/null
+++ b/comm/mail/components/addrbook/test/browser/data/auth_headers.sjs
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Echoes request headers as JSON so a test can check what was sent.
+
+/* eslint-disable-next-line mozilla/reject-importGlobalProperties */
+Cu.importGlobalProperties(["URLSearchParams"]);
+
+function handleRequest(request, response) {
+ if (!request.hasHeader("Authorization")) {
+ response.setStatusLine("1.1", 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", `Basic realm="test"`);
+ return;
+ }
+
+ response.setHeader("Content-Type", "application/json", false);
+
+ let headers = {};
+ let enumerator = request.headers;
+ while (enumerator.hasMoreElements()) {
+ let header = enumerator.getNext().data;
+ headers[header.toLowerCase()] = request.getHeader(header);
+ }
+
+ response.write(JSON.stringify(headers));
+}
diff --git a/comm/mail/components/addrbook/test/browser/data/dns.sjs b/comm/mail/components/addrbook/test/browser/data/dns.sjs
new file mode 100644
index 0000000000..11121cce7c
--- /dev/null
+++ b/comm/mail/components/addrbook/test/browser/data/dns.sjs
@@ -0,0 +1,48 @@
+/* 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/. */
+
+function handleRequest(request, response) {
+ if (!request.hasHeader("Authorization")) {
+ response.setStatusLine("1.1", 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", `Basic realm="test"`);
+ return;
+ }
+
+ response.setStatusLine("1.1", 207, "Multi-Status");
+ response.setHeader("Content-Type", "text/xml", false);
+
+ // Request:
+ // <propfind>
+ // <prop>
+ // <resourcetype/>
+ // <displayname/>
+ // <current-user-principal/>
+ // <current-user-privilege-set/>
+ // </prop>
+ // </propfind>
+
+ response.write(`<multistatus xmlns="DAV:">
+ <response>
+ <href>/browser/comm/mail/components/addrbook/test/browser/data/dns.sjs</href>
+ <propstat>
+ <prop>
+ <resourcetype>
+ <collection/>
+ </resourcetype>
+ <current-user-principal>
+ <href>/browser/comm/mail/components/addrbook/test/browser/data/principal.sjs</href>
+ </current-user-principal>
+ </prop>
+ <status>HTTP/1.1 200 OK</status>
+ </propstat>
+ <propstat>
+ <prop>
+ <current-user-principal/>
+ <current-user-privilege-set/>
+ </prop>
+ <status>HTTP/1.1 404 Not Found</status>
+ </propstat>
+ </response>
+ </multistatus>`);
+}
diff --git a/comm/mail/components/addrbook/test/browser/data/photo1.jpg b/comm/mail/components/addrbook/test/browser/data/photo1.jpg
new file mode 100644
index 0000000000..35608787bf
--- /dev/null
+++ b/comm/mail/components/addrbook/test/browser/data/photo1.jpg
Binary files differ
diff --git a/comm/mail/components/addrbook/test/browser/data/photo2.jpg b/comm/mail/components/addrbook/test/browser/data/photo2.jpg
new file mode 100644
index 0000000000..41fd1e90fc
--- /dev/null
+++ b/comm/mail/components/addrbook/test/browser/data/photo2.jpg
Binary files differ
diff --git a/comm/mail/components/addrbook/test/browser/data/principal.sjs b/comm/mail/components/addrbook/test/browser/data/principal.sjs
new file mode 100644
index 0000000000..659cd3cd91
--- /dev/null
+++ b/comm/mail/components/addrbook/test/browser/data/principal.sjs
@@ -0,0 +1,38 @@
+/* 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/. */
+
+function handleRequest(request, response) {
+ if (!request.hasHeader("Authorization")) {
+ response.setStatusLine("1.1", 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", `Basic realm="test"`);
+ return;
+ }
+
+ response.setStatusLine("1.1", 207, "Multi-Status");
+ response.setHeader("Content-Type", "text/xml", false);
+
+ // Request:
+ // <propfind>
+ // <prop>
+ // <addressbook-home-set/>
+ // </prop>
+ // </propfind>
+
+ response.write(`<multistatus xmlns="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav">
+ <response>
+ <href>/browser/comm/mail/components/addrbook/test/browser/data/principal.sjs</href>
+ <propstat>
+ <prop>
+ <resourcetype>
+ <principal/>
+ </resourcetype>
+ <card:addressbook-home-set>
+ <href>/browser/comm/mail/components/addrbook/test/browser/data/addressbooks.sjs</href>
+ </card:addressbook-home-set>
+ </prop>
+ <status>HTTP/1.1 200 OK</status>
+ </propstat>
+ </response>
+ </multistatus>`);
+}
diff --git a/comm/mail/components/addrbook/test/browser/data/redirect_auto.sjs b/comm/mail/components/addrbook/test/browser/data/redirect_auto.sjs
new file mode 100644
index 0000000000..a9285c21d0
--- /dev/null
+++ b/comm/mail/components/addrbook/test/browser/data/redirect_auto.sjs
@@ -0,0 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Serves as the authorisation endpoint for OAuth2 testing.
+
+/* eslint-disable-next-line mozilla/reject-importGlobalProperties */
+Cu.importGlobalProperties(["URLSearchParams", "URL"]);
+
+function handleRequest(request, response) {
+ let params = new URLSearchParams(request.queryString);
+
+ if (request.method == "POST") {
+ response.setStatusLine(request.httpVersion, 303, "Redirected");
+ } else {
+ response.setStatusLine(request.httpVersion, 302, "Moved Temporarily");
+ }
+
+ let url = new URL(params.get("redirect_uri"));
+ url.searchParams.set("code", "success");
+ response.setHeader("Location", url.href);
+}
diff --git a/comm/mail/components/addrbook/test/browser/data/token.sjs b/comm/mail/components/addrbook/test/browser/data/token.sjs
new file mode 100644
index 0000000000..e070f8d55f
--- /dev/null
+++ b/comm/mail/components/addrbook/test/browser/data/token.sjs
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Serves as the token endpoint for OAuth2 testing.
+
+/* eslint-disable-next-line mozilla/reject-importGlobalProperties */
+Cu.importGlobalProperties(["URLSearchParams"]);
+
+function handleRequest(request, response) {
+ let stream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
+ Ci.nsIBinaryInputStream
+ );
+ stream.setInputStream(request.bodyInputStream);
+
+ let input = stream.readBytes(request.bodyInputStream.available());
+ let params = new URLSearchParams(input);
+
+ response.setHeader("Content-Type", "application/json", false);
+
+ if (params.get("refresh_token") == "expired_token") {
+ response.setStatusLine("1.1", 400, "Bad Request");
+ response.write(JSON.stringify({ error: "invalid_grant" }));
+ return;
+ }
+
+ let data = { access_token: "bobs_access_token" };
+
+ if (params.get("code") == "success") {
+ // Authorisation just happened, set a different access token so the test
+ // can detect it, and provide a refresh token.
+ data.access_token = "new_access_token";
+ data.refresh_token = "new_refresh_token";
+ }
+
+ response.write(JSON.stringify(data));
+}
diff --git a/comm/mail/components/addrbook/test/browser/head.js b/comm/mail/components/addrbook/test/browser/head.js
new file mode 100644
index 0000000000..37fc445410
--- /dev/null
+++ b/comm/mail/components/addrbook/test/browser/head.js
@@ -0,0 +1,445 @@
+/* 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/. */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+const personalBook = MailServices.ab.getDirectoryFromId("ldap_2.servers.pab");
+const historyBook = MailServices.ab.getDirectoryFromId(
+ "ldap_2.servers.history"
+);
+
+add_setup(async () => {
+ // Force the window to be full screen to avoid issues with buttons not being
+ // reachable. This is a temporary solution while we update the details pane
+ // UI to be properly responsive and wrap elements correctly.
+ window.fullScreen = true;
+});
+
+// We want to check that everything has been removed/reset, but if we register
+// a cleanup function here, it will run before any other cleanup function has
+// had a chance to run. Instead, when it runs register another cleanup
+// function which will run last.
+registerCleanupFunction(function () {
+ registerCleanupFunction(async function () {
+ Assert.equal(
+ MailServices.ab.directories.length,
+ 2,
+ "Only Personal ab and Collected Addresses should be left."
+ );
+ for (let directory of MailServices.ab.directories) {
+ if (
+ directory.dirPrefId == "ldap_2.servers.history" ||
+ directory.dirPrefId == "ldap_2.servers.pab"
+ ) {
+ Assert.equal(
+ directory.childCardCount,
+ 0,
+ `All contacts should have been removed from ${directory.dirName}`
+ );
+ if (directory.childCardCount) {
+ directory.deleteCards(directory.childCards);
+ }
+ } else {
+ await promiseDirectoryRemoved(directory.URI);
+ }
+ }
+ closeAddressBookWindow();
+
+ // TODO: convert this to UID.
+ Services.prefs.clearUserPref("mail.addr_book.view.startupURI");
+ Services.prefs.clearUserPref("mail.addr_book.view.startupURIisDefault");
+
+ // Some tests that open new windows don't return focus to the main window
+ // in a way that satisfies mochitest, and the test times out.
+ Services.focus.focusedWindow = window;
+ // Focus an element in the main window, then blur it again to avoid it
+ // hijacking keypresses.
+ let mainWindowElement = document.getElementById("button-appmenu");
+ mainWindowElement.focus();
+ mainWindowElement.blur();
+ // Reset the window to its default size.
+ window.fullScreen = false;
+ });
+});
+
+async function openAddressBookWindow() {
+ return new Promise(resolve => {
+ window.openTab("addressBookTab", {
+ onLoad(event, browser) {
+ resolve(browser.contentWindow);
+ },
+ });
+ });
+}
+
+function closeAddressBookWindow() {
+ let abTab = getAddressBookTab();
+ if (abTab) {
+ let tabmail = document.getElementById("tabmail");
+ tabmail.closeTab(abTab);
+ }
+}
+
+function getAddressBookTab() {
+ let tabmail = document.getElementById("tabmail");
+ return tabmail.tabInfo.find(
+ t => t.browser?.currentURI.spec == "about:addressbook"
+ );
+}
+
+function getAddressBookWindow() {
+ let tab = getAddressBookTab();
+ return tab?.browser.contentWindow;
+}
+
+async function openAllAddressBooks() {
+ let abWindow = getAddressBookWindow();
+ EventUtils.synthesizeMouseAtCenter(
+ abWindow.document.querySelector("#books > li"),
+ {},
+ abWindow
+ );
+ await new Promise(r => abWindow.setTimeout(r));
+}
+
+function openDirectory(directory) {
+ let abWindow = getAddressBookWindow();
+ let row = abWindow.booksList.getRowForUID(directory.UID);
+ EventUtils.synthesizeMouseAtCenter(row.querySelector("span"), {}, abWindow);
+}
+
+function createAddressBook(dirName, type = Ci.nsIAbManager.JS_DIRECTORY_TYPE) {
+ let prefName = MailServices.ab.newAddressBook(dirName, null, type);
+ return MailServices.ab.getDirectoryFromId(prefName);
+}
+
+async function createAddressBookWithUI(abName) {
+ let newAddressBookPromise = promiseLoadSubDialog(
+ "chrome://messenger/content/addressbook/abAddressBookNameDialog.xhtml"
+ );
+
+ let abWindow = getAddressBookWindow();
+ EventUtils.synthesizeMouseAtCenter(
+ abWindow.document.getElementById("toolbarCreateBook"),
+ {},
+ abWindow
+ );
+
+ let abNameDialog = await newAddressBookPromise;
+ EventUtils.sendString(abName, abNameDialog);
+ abNameDialog.document.querySelector("dialog").getButton("accept").click();
+
+ let addressBook = MailServices.ab.directories.find(
+ directory => directory.dirName == abName
+ );
+
+ Assert.ok(addressBook, "a new address book was created");
+
+ // At this point we need to wait for the UI to update.
+ await new Promise(r => abWindow.setTimeout(r));
+
+ return addressBook;
+}
+
+function createContact(firstName, lastName, displayName, primaryEmail) {
+ let contact = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ contact.displayName = displayName ?? `${firstName} ${lastName}`;
+ contact.firstName = firstName;
+ contact.lastName = lastName;
+ contact.primaryEmail =
+ primaryEmail ?? `${firstName}.${lastName}@invalid`.toLowerCase();
+ return contact;
+}
+
+function createMailingList(name) {
+ let list = Cc["@mozilla.org/addressbook/directoryproperty;1"].createInstance(
+ Ci.nsIAbDirectory
+ );
+ list.isMailList = true;
+ list.dirName = name;
+ return list;
+}
+
+async function createMailingListWithUI(mlParent, mlName) {
+ openDirectory(mlParent);
+
+ let newAddressBookPromise = promiseLoadSubDialog(
+ "chrome://messenger/content/addressbook/abMailListDialog.xhtml"
+ );
+
+ let abWindow = getAddressBookWindow();
+ EventUtils.synthesizeMouseAtCenter(
+ abWindow.document.getElementById("toolbarCreateList"),
+ {},
+ abWindow
+ );
+
+ let abListDialog = await newAddressBookPromise;
+ let abListDocument = abListDialog.document;
+ await new Promise(resolve => abListDialog.setTimeout(resolve));
+
+ abListDocument.getElementById("abPopup").value = mlParent.URI;
+ abListDocument.getElementById("ListName").value = mlName;
+ abListDocument.querySelector("dialog").getButton("accept").click();
+
+ let list = mlParent.childNodes.find(list => list.dirName == mlName);
+
+ Assert.ok(list, "a new list was created");
+
+ // At this point we need to wait for the UI to update.
+ await new Promise(r => abWindow.setTimeout(r));
+
+ return list;
+}
+
+function checkDirectoryDisplayed(directory) {
+ let abWindow = getAddressBookWindow();
+ let booksList = abWindow.document.getElementById("books");
+ let cardsList = abWindow.cardsPane.cardsList;
+
+ if (directory) {
+ Assert.equal(
+ booksList.selectedIndex,
+ booksList.getIndexForUID(directory.UID)
+ );
+ Assert.equal(cardsList.view.directory?.UID, directory.UID);
+ } else {
+ Assert.equal(booksList.selectedIndex, 0);
+ Assert.ok(!cardsList.view.directory);
+ }
+}
+
+function checkCardsListed(...expectedCards) {
+ checkNamesListed(
+ ...expectedCards.map(card =>
+ card.isMailList ? card.dirName : card.displayName
+ )
+ );
+
+ let abWindow = getAddressBookWindow();
+ let cardsList = abWindow.document.getElementById("cards");
+ for (let i = 0; i < expectedCards.length; i++) {
+ let row = cardsList.getRowAtIndex(i);
+ Assert.equal(
+ row.classList.contains("MailList"),
+ expectedCards[i].isMailList,
+ `row ${
+ expectedCards[i].isMailList ? "should" : "should not"
+ } be a mailing list row`
+ );
+ Assert.equal(
+ row.address.textContent,
+ expectedCards[i].primaryEmail ?? "",
+ "correct address should be displayed"
+ );
+ Assert.equal(
+ row.avatar.childElementCount,
+ 1,
+ "only one avatar image should be displayed"
+ );
+ }
+}
+
+function checkNamesListed(...expectedNames) {
+ let abWindow = getAddressBookWindow();
+ let cardsList = abWindow.document.getElementById("cards");
+ let expectedCount = expectedNames.length;
+
+ Assert.equal(
+ cardsList.view.rowCount,
+ expectedCount,
+ "Tree view has the right number of rows"
+ );
+
+ for (let i = 0; i < expectedCount; i++) {
+ Assert.equal(
+ cardsList.view.getCellText(i, { id: "GeneratedName" }),
+ expectedNames[i],
+ "view should give the correct name"
+ );
+ Assert.equal(
+ cardsList.getRowAtIndex(i).querySelector(".generatedname-column, .name")
+ .textContent,
+ expectedNames[i],
+ "correct name should be displayed"
+ );
+ }
+}
+
+function checkPlaceholders(expectedVisible = []) {
+ let abWindow = getAddressBookWindow();
+ let placeholder = abWindow.cardsPane.cardsList.placeholder;
+
+ if (!expectedVisible.length) {
+ Assert.ok(
+ BrowserTestUtils.is_hidden(placeholder),
+ "placeholders are hidden"
+ );
+ return;
+ }
+
+ for (let element of placeholder.children) {
+ let id = element.id;
+ if (expectedVisible.includes(id)) {
+ Assert.ok(BrowserTestUtils.is_visible(element), `${id} is visible`);
+ } else {
+ Assert.ok(BrowserTestUtils.is_hidden(element), `${id} is hidden`);
+ }
+ }
+}
+
+async function showSortMenu(name, value) {
+ let abWindow = getAddressBookWindow();
+ let abDocument = abWindow.document;
+
+ let displayButton = abDocument.getElementById("displayButton");
+ let sortContext = abDocument.getElementById("sortContext");
+ let shownPromise = BrowserTestUtils.waitForEvent(sortContext, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(displayButton, {}, abWindow);
+ await shownPromise;
+ let hiddenPromise = BrowserTestUtils.waitForEvent(sortContext, "popuphidden");
+ sortContext.activateItem(
+ sortContext.querySelector(`[name="${name}"][value="${value}"]`)
+ );
+ if (name == "toggle") {
+ sortContext.hidePopup();
+ }
+ await hiddenPromise;
+}
+
+async function showPickerMenu(name, value) {
+ let abWindow = getAddressBookWindow();
+ let cardsHeader = abWindow.cardsPane.table.header;
+ let pickerButton = cardsHeader.querySelector(
+ `th[is="tree-view-table-column-picker"] button`
+ );
+ let menupopup = cardsHeader.querySelector(
+ `th[is="tree-view-table-column-picker"] menupopup`
+ );
+ let shownPromise = BrowserTestUtils.waitForEvent(menupopup, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(pickerButton, {}, abWindow);
+ await shownPromise;
+ let hiddenPromise = BrowserTestUtils.waitForEvent(menupopup, "popuphidden");
+ menupopup.activateItem(
+ menupopup.querySelector(`[name="${name}"][value="${value}"]`)
+ );
+ if (name == "toggle") {
+ menupopup.hidePopup();
+ }
+ await hiddenPromise;
+}
+
+async function toggleLayout() {
+ let abWindow = getAddressBookWindow();
+ let abDocument = abWindow.document;
+
+ let displayButton = abDocument.getElementById("displayButton");
+ let sortContext = abDocument.getElementById("sortContext");
+ let shownPromise = BrowserTestUtils.waitForEvent(sortContext, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(displayButton, {}, abWindow);
+ await shownPromise;
+ let hiddenPromise = BrowserTestUtils.waitForEvent(sortContext, "popuphidden");
+ sortContext.activateItem(abDocument.getElementById("sortContextTableLayout"));
+ await hiddenPromise;
+}
+
+async function checkComposeWindow(composeWindow, ...expectedAddresses) {
+ await BrowserTestUtils.waitForEvent(composeWindow, "compose-editor-ready");
+ let composeDocument = composeWindow.document;
+ let toAddrRow = composeDocument.getElementById("addressRowTo");
+
+ let pills = toAddrRow.querySelectorAll("mail-address-pill");
+ Assert.equal(pills.length, expectedAddresses.length);
+ for (let i = 0; i < expectedAddresses.length; i++) {
+ Assert.equal(pills[i].label, expectedAddresses[i]);
+ }
+
+ await Promise.all([
+ BrowserTestUtils.closeWindow(composeWindow),
+ BrowserTestUtils.waitForEvent(window, "activate"),
+ ]);
+}
+
+function promiseDirectoryRemoved(uri) {
+ let removePromise = TestUtils.topicObserved("addrbook-directory-deleted");
+ MailServices.ab.deleteAddressBook(uri);
+ return removePromise;
+}
+
+function promiseLoadSubDialog(url) {
+ let abWindow = getAddressBookWindow();
+
+ return new Promise((resolve, reject) => {
+ abWindow.SubDialog._dialogStack.addEventListener(
+ "dialogopen",
+ function dialogopen(aEvent) {
+ if (
+ aEvent.detail.dialog._frame.contentWindow.location == "about:blank"
+ ) {
+ return;
+ }
+ abWindow.SubDialog._dialogStack.removeEventListener(
+ "dialogopen",
+ dialogopen
+ );
+
+ Assert.equal(
+ aEvent.detail.dialog._frame.contentWindow.location.toString(),
+ url,
+ "Check the proper URL is loaded"
+ );
+
+ // Check visibility
+ Assert.ok(
+ BrowserTestUtils.is_visible(
+ aEvent.detail.dialog._overlay,
+ "Overlay is visible"
+ )
+ );
+
+ // Check that stylesheets were injected
+ let expectedStyleSheetURLs =
+ aEvent.detail.dialog._injectedStyleSheets.slice(0);
+ for (let styleSheet of aEvent.detail.dialog._frame.contentDocument
+ .styleSheets) {
+ let i = expectedStyleSheetURLs.indexOf(styleSheet.href);
+ if (i >= 0) {
+ info("found " + styleSheet.href);
+ expectedStyleSheetURLs.splice(i, 1);
+ }
+ }
+ Assert.equal(
+ expectedStyleSheetURLs.length,
+ 0,
+ "All expectedStyleSheetURLs should have been found"
+ );
+
+ // Wait for the next event tick to make sure the remaining part of the
+ // testcase runs after the dialog gets ready for input.
+ executeSoon(() => resolve(aEvent.detail.dialog._frame.contentWindow));
+ }
+ );
+ });
+}
+
+function formatVCard(strings, ...values) {
+ let arr = [];
+ for (let str of strings) {
+ arr.push(str);
+ arr.push(values.shift());
+ }
+ let lines = arr.join("").split("\n");
+ let indent = lines[1].length - lines[1].trimLeft().length;
+ let outLines = [];
+ for (let line of lines) {
+ if (line.length > 0) {
+ outLines.push(line.substring(indent) + "\r\n");
+ }
+ }
+ return outLines.join("");
+}