diff options
Diffstat (limited to 'comm/mail/components/addrbook/test/browser/browser_edit_card.js')
-rw-r--r-- | comm/mail/components/addrbook/test/browser/browser_edit_card.js | 3517 |
1 files changed, 3517 insertions, 0 deletions
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); +}); |