diff options
Diffstat (limited to 'comm/calendar/test/browser/eventDialog')
15 files changed, 2537 insertions, 0 deletions
diff --git a/comm/calendar/test/browser/eventDialog/browser.ini b/comm/calendar/test/browser/eventDialog/browser.ini new file mode 100644 index 0000000000..85f569c0cc --- /dev/null +++ b/comm/calendar/test/browser/eventDialog/browser.ini @@ -0,0 +1,27 @@ +[default] +head = head.js +prefs = + calendar.item.promptDelete=false + calendar.timezone.local=UTC + calendar.timezone.useSystemTimezone=false + calendar.week.start=0 + mail.provider.suppress_dialog_on_startup=true + mail.spotlight.firstRunDone=true + mail.winsearch.firstRunDone=true + mailnews.start_page.override_url=about:blank + mailnews.start_page.url=about:blank +subsuite = thunderbird +support-files = data/** + +[browser_alarmDialog.js] +[browser_attachMenu.js] +[browser_attendeesDialog.js] +[browser_attendeesDialogAdd.js] +[browser_attendeesDialogNoEdit.js] +[browser_attendeesDialogRemove.js] +[browser_attendeesDialogUpdate.js] +[browser_eventDialog.js] +[browser_eventDialogDescriptionEditor.js] +[browser_eventDialogEditButton.js] +[browser_eventDialogModificationPrompt.js] +[browser_utf8.js] diff --git a/comm/calendar/test/browser/eventDialog/browser_alarmDialog.js b/comm/calendar/test/browser/eventDialog/browser_alarmDialog.js new file mode 100644 index 0000000000..0d6a07a3c4 --- /dev/null +++ b/comm/calendar/test/browser/eventDialog/browser_alarmDialog.js @@ -0,0 +1,88 @@ +/* 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 { saveAndCloseItemDialog, setData } = ChromeUtils.import( + "resource://testing-common/calendar/ItemEditingHelpers.jsm" +); + +var { dayView } = CalendarTestUtils; + +add_task(async function testAlarmDialog() { + let now = new Date(); + + const TITLE = "Event"; + + let calendar = CalendarTestUtils.createCalendar(); + registerCleanupFunction(() => { + CalendarTestUtils.removeCalendar(calendar); + }); + + await CalendarTestUtils.setCalendarView(window, "day"); + await CalendarTestUtils.goToDate( + window, + now.getUTCFullYear(), + now.getUTCMonth() + 1, + now.getUTCDate() + ); + await CalendarTestUtils.calendarViewForward(window, 1); + + let allDayHeader = dayView.getAllDayHeader(window); + Assert.ok(allDayHeader); + EventUtils.synthesizeMouseAtCenter(allDayHeader, {}, window); + + // Create a new all-day event tomorrow. + + // Prepare to dismiss the alarm. + let alarmPromise = BrowserTestUtils.promiseAlertDialog( + null, + "chrome://calendar/content/calendar-alarm-dialog.xhtml", + { + async callback(alarmWindow) { + await new Promise(resolve => alarmWindow.setTimeout(resolve, 500)); + + let dismissButton = alarmWindow.document.getElementById("alarm-dismiss-all-button"); + EventUtils.synthesizeMouseAtCenter(dismissButton, {}, alarmWindow); + }, + } + ); + let { dialogWindow, iframeWindow } = await CalendarTestUtils.editNewEvent(window); + await setData(dialogWindow, iframeWindow, { + allday: true, + reminder: "1day", + title: TITLE, + }); + + await saveAndCloseItemDialog(dialogWindow); + await alarmPromise; + + // Change the reminder duration, this resets the alarm. + let eventBox = await dayView.waitForAllDayItemAt(window, 1); + + // Prepare to snooze the alarm. + alarmPromise = BrowserTestUtils.promiseAlertDialog( + null, + "chrome://calendar/content/calendar-alarm-dialog.xhtml", + { + async callback(alarmWindow) { + await new Promise(resolve => alarmWindow.setTimeout(resolve, 500)); + + let snoozeAllButton = alarmWindow.document.getElementById("alarm-snooze-all-button"); + let popup = alarmWindow.document.querySelector("#alarm-snooze-all-popup"); + let menuitems = alarmWindow.document.querySelectorAll("#alarm-snooze-all-popup > menuitem"); + + let shownPromise = BrowserTestUtils.waitForEvent(snoozeAllButton, "popupshown"); + EventUtils.synthesizeMouseAtCenter(snoozeAllButton, {}, alarmWindow); + await shownPromise; + popup.activateItem(menuitems[5]); + }, + } + ); + + ({ dialogWindow, iframeWindow } = await CalendarTestUtils.editItem(window, eventBox)); + await setData(dialogWindow, iframeWindow, { reminder: "2days", title: TITLE }); + await saveAndCloseItemDialog(dialogWindow); + await alarmPromise; + + Assert.ok(true, "Test ran to completion"); +}); diff --git a/comm/calendar/test/browser/eventDialog/browser_attachMenu.js b/comm/calendar/test/browser/eventDialog/browser_attachMenu.js new file mode 100644 index 0000000000..2a0b2afc4c --- /dev/null +++ b/comm/calendar/test/browser/eventDialog/browser_attachMenu.js @@ -0,0 +1,266 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests for the attach menu in the event dialog window. + */ + +const { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); +const { cloudFileAccounts } = ChromeUtils.import("resource:///modules/cloudFileAccounts.jsm"); +const { MockFilePicker } = ChromeUtils.importESModule( + "resource://testing-common/MockFilePicker.sys.mjs" +); +var { saveAndCloseItemDialog, setData } = ChromeUtils.import( + "resource://testing-common/calendar/ItemEditingHelpers.jsm" +); + +// Remove the save prompt observer that head.js added. It's causing trouble here. +Services.ww.unregisterNotification(savePromptObserver); + +let calendar = CalendarTestUtils.createCalendar("Attachments"); +registerCleanupFunction(() => { + cal.manager.unregisterCalendar(calendar); + MockFilePicker.cleanup(); +}); + +async function getEventBox(selector) { + let itemBox; + await TestUtils.waitForCondition(() => { + itemBox = document.querySelector(selector); + return itemBox != null; + }, "calendar item did not appear in time"); + return itemBox; +} + +async function openEventFromBox(eventBox) { + if (Services.focus.activeWindow != window) { + await BrowserTestUtils.waitForEvent(window, "focus"); + } + let promise = CalendarTestUtils.waitForEventDialog(); + EventUtils.synthesizeMouseAtCenter(eventBox, { clickCount: 2 }); + return promise; +} + +/** + * Tests using the "Website" menu item attaches a link to the event. + */ +add_task(async function testAttachWebPage() { + let startDate = cal.createDateTime("20200101T000001Z"); + await CalendarTestUtils.setCalendarView(window, "month"); + window.goToDate(startDate); + + let { dialogWindow, iframeWindow, dialogDocument, iframeDocument } = + await CalendarTestUtils.editNewEvent(window); + + await setData(dialogWindow, iframeWindow, { + title: "Web Link Event", + startDate, + }); + + // Attach the url. + let attachButton = dialogWindow.document.querySelector("#button-url"); + Assert.ok(attachButton, "attach menu button found"); + + let menu = dialogDocument.querySelector("#button-attach-menupopup"); + let menuShowing = BrowserTestUtils.waitForEvent(menu, "popupshown"); + EventUtils.synthesizeMouseAtCenter(attachButton, {}, dialogWindow); + await menuShowing; + + let url = "https://thunderbird.net/"; + let urlPrompt = BrowserTestUtils.promiseAlertDialogOpen( + "", + "chrome://global/content/commonDialog.xhtml", + { + async callback(win) { + win.document.querySelector("#loginTextbox").value = url; + EventUtils.synthesizeKey("VK_RETURN", {}, win); + }, + } + ); + EventUtils.synthesizeMouseAtCenter( + dialogDocument.querySelector("#button-attach-url"), + {}, + dialogWindow + ); + await urlPrompt; + + // Now check that the url shows in the attachments list. + EventUtils.synthesizeMouseAtCenter( + iframeDocument.querySelector("#event-grid-tab-attachments"), + {}, + iframeWindow + ); + + let listBox = iframeDocument.querySelector("#attachment-link"); + await BrowserTestUtils.waitForCondition( + () => listBox.itemChildren.length == 1, + "attachment list did not show in time" + ); + + Assert.equal(listBox.itemChildren[0].tooltipText, url, "url included in attachments list"); + + // Save the new event. + await saveAndCloseItemDialog(dialogWindow); + + // Open the event to verify the attachment is shown in the summary dialog. + let summaryWin = await openEventFromBox(await getEventBox("calendar-month-day-box-item")); + let label = summaryWin.document.querySelector(`label[value="${url}"]`); + Assert.ok(label, "attachment label found on calendar summary dialog"); + await BrowserTestUtils.closeWindow(summaryWin); + + // Clean up. + let eventBox = await getEventBox("calendar-month-day-box-item"); + eventBox.focus(); + EventUtils.synthesizeKey("VK_DELETE", {}); +}); + +/** + * Tests selecting a provider from the attach menu works. + */ +add_task(async function testAttachProvider() { + let fileUrl = "https://path/to/mock/file.pdf"; + let iconURL = "chrome://messenger/content/extension.svg"; + let provider = { + type: "Mochitest", + displayName: "Mochitest", + iconURL, + initAccount(accountKey) { + return { + accountKey, + type: "Mochitest", + get displayName() { + return Services.prefs.getCharPref( + `mail.cloud_files.accounts.${this.accountKey}.displayName`, + "Mochitest Account" + ); + }, + iconURL, + configured: true, + managementURL: "", + uploadFile(window, aFile) { + return new Promise(resolve => + setTimeout(() => + resolve({ + id: 1, + path: aFile.path, + size: aFile.fileSize, + url: fileUrl, + // The uploadFile() function should return serviceIcon, serviceName + // and serviceUrl - either default or user defined values specified + // by the onFileUpload event. The item-edit dialog uses only the + // serviceIcon. + serviceIcon: "chrome://messenger/skin/icons/globe.svg", + }) + ) + ); + }, + }; + }, + }; + + cloudFileAccounts.registerProvider("Mochitest", provider); + cloudFileAccounts.createAccount("Mochitest"); + registerCleanupFunction(() => { + cloudFileAccounts.unregisterProvider("Mochitest"); + }); + + let file = new FileUtils.File(getTestFilePath("data/guests.txt")); + MockFilePicker.init(window); + MockFilePicker.setFiles([file]); + MockFilePicker.returnValue = MockFilePicker.returnOk; + + let startDate = cal.createDateTime("20200201T000001Z"); + await CalendarTestUtils.setCalendarView(window, "month"); + window.goToDate(startDate); + + let { dialogWindow, iframeWindow, dialogDocument, iframeDocument } = + await CalendarTestUtils.editNewEvent(window); + + await setData(dialogWindow, iframeWindow, { + title: "Provider Attachment Event", + startDate, + }); + + let attachButton = dialogDocument.querySelector("#button-url"); + Assert.ok(attachButton, "attach menu button found"); + + let menu = dialogDocument.querySelector("#button-attach-menupopup"); + let menuItem; + + await BrowserTestUtils.waitForCondition(() => { + menuItem = menu.querySelector("menuitem[label='File using Mochitest Account']"); + return menuItem; + }); + + Assert.ok(menuItem, "custom provider menuitem found"); + Assert.equal(menuItem.image, iconURL, "provider image src is provider image"); + + // Click on the "Attach" menu. + let menuShowing = BrowserTestUtils.waitForEvent(menu, "popupshown"); + EventUtils.synthesizeMouseAtCenter(attachButton, {}, dialogWindow); + await menuShowing; + + // Click on the menuitem to attach a file using our provider. + let menuHidden = BrowserTestUtils.waitForEvent(menu, "popuphidden"); + EventUtils.synthesizeMouseAtCenter(menuItem, {}, dialogWindow); + await menuHidden; + + // Check if the file dialog was "shown". MockFilePicker.open() is asynchronous + // but does not return a promise. + await BrowserTestUtils.waitForCondition( + () => MockFilePicker.shown, + "file picker was not shown in time" + ); + + // Click on the attachments tab of the event dialog. + EventUtils.synthesizeMouseAtCenter( + iframeDocument.querySelector("#event-grid-tab-attachments"), + {}, + iframeWindow + ); + + // Wait until the file we attached appears. + let listBox = iframeDocument.querySelector("#attachment-link"); + await BrowserTestUtils.waitForCondition( + () => listBox.itemChildren.length == 1, + "attachment list did not show in time" + ); + + let listItem = listBox.itemChildren[0]; + + // XXX: This property is set after an async operation. Unfortunately, that + // operation is not awaited on in its surrounding code so the assertion + // after this will occasionally fail if this is not done. + await BrowserTestUtils.waitForCondition( + () => listItem.attachCloudFileUpload, + "attachCloudFileUpload property not set on attachment listitem in time." + ); + + Assert.equal(listItem.attachCloudFileUpload.url, fileUrl, "upload attached to event"); + + let listItemImage = listItem.querySelector("img"); + Assert.equal( + listItemImage.src, + "chrome://messenger/skin/icons/globe.svg", + "attachment image is provider image" + ); + + // Save the new event. + dialogDocument.querySelector("#button-saveandclose").click(); + + // Open it and verify the attachment is shown. + let summaryWin = await openEventFromBox(await getEventBox("calendar-month-day-box-item")); + let label = summaryWin.document.querySelector(`label[value="${fileUrl}"]`); + Assert.ok(label, "attachment label found on calendar summary dialog"); + await BrowserTestUtils.closeWindow(summaryWin); + + if (Services.focus.activeWindow != window) { + await BrowserTestUtils.waitForEvent(window, "focus"); + } + + // Clean up. + let eventBox = await getEventBox("calendar-month-day-box-item"); + eventBox.focus(); + EventUtils.synthesizeKey("VK_DELETE", {}); +}); diff --git a/comm/calendar/test/browser/eventDialog/browser_attendeesDialog.js b/comm/calendar/test/browser/eventDialog/browser_attendeesDialog.js new file mode 100644 index 0000000000..f6e73f3957 --- /dev/null +++ b/comm/calendar/test/browser/eventDialog/browser_attendeesDialog.js @@ -0,0 +1,462 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* globals createEventWithDialog, openAttendeesWindow, closeAttendeesWindow */ + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); +var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm"); + +add_task(async () => { + let calendar = CalendarTestUtils.createCalendar("Mochitest", "memory"); + calendar.name = "Mochitest"; + calendar.setProperty("organizerId", "mailto:mochitest@example.com"); + + cal.freeBusyService.addProvider(freeBusyProvider); + + let book = MailServices.ab.getDirectoryFromId( + MailServices.ab.newAddressBook("Mochitest", null, 101) + ); + let contacts = {}; + for (let name of ["Charlie", "Juliet", "Mike", "Oscar", "Romeo", "Victor"]) { + let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(Ci.nsIAbCard); + card.firstName = name; + card.lastName = "Mochitest"; + card.displayName = `${name} Mochitest`; + card.primaryEmail = `${name.toLowerCase()}@example.com`; + contacts[name.toUpperCase()] = book.addCard(card); + } + let list = Cc["@mozilla.org/addressbook/directoryproperty;1"].createInstance(Ci.nsIAbDirectory); + list.isMailList = true; + list.dirName = "The Boys"; + list = book.addMailList(list); + list.addCard(contacts.MIKE); + list.addCard(contacts.OSCAR); + list.addCard(contacts.ROMEO); + list.addCard(contacts.VICTOR); + + let today = new Date(); + let times = { + ONE: new Date( + Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate() + 1, 13, 0, 0) + ), + TWO_THIRTY: new Date( + Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate() + 1, 14, 30, 0) + ), + THREE_THIRTY: new Date( + Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate() + 1, 15, 30, 0) + ), + FOUR: new Date( + Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate() + 1, 16, 0, 0) + ), + }; + + registerCleanupFunction(async () => { + CalendarTestUtils.removeCalendar(calendar); + cal.freeBusyService.removeProvider(freeBusyProvider); + MailServices.ab.deleteAddressBook(book.URI); + }); + + let eventWindow = await openEventWindow(calendar); + let eventDocument = eventWindow.document; + let iframeDocument = eventDocument.getElementById("calendar-item-panel-iframe").contentDocument; + + let eventStartTime = iframeDocument.getElementById("event-starttime"); + eventStartTime.value = times.ONE; + let eventEndTime = iframeDocument.getElementById("event-endtime"); + eventEndTime.value = times.THREE_THIRTY; + + async function checkAttendeesInAttendeesDialog(attendeesDocument, expectedAttendees) { + let attendeesList = attendeesDocument.getElementById("attendee-list"); + await TestUtils.waitForCondition( + () => attendeesList.childElementCount == expectedAttendees.length + 1, + "empty attendee input should have been added" + ); + + function getInputValueFromAttendeeRow(row) { + const input = row.querySelector("input"); + return input.value; + } + + Assert.deepEqual( + Array.from(attendeesList.children, getInputValueFromAttendeeRow), + [...expectedAttendees, ""], + "attendees list matches what was expected" + ); + Assert.equal( + attendeesDocument.activeElement, + attendeesList.children[expectedAttendees.length].querySelector("input"), + "empty attendee input should have focus" + ); + } + + async function checkFreeBusy(row, count) { + Assert.equal(row._freeBusyDiv.querySelectorAll(".pending").length, 1); + Assert.equal(row._freeBusyDiv.querySelectorAll(".busy").length, 0); + let responsePromise = BrowserTestUtils.waitForEvent(row, "freebusy-update-finished"); + freeBusyProvider.sendNextResponse(); + await responsePromise; + Assert.equal(row._freeBusyDiv.querySelectorAll(".pending").length, 0); + Assert.equal(row._freeBusyDiv.querySelectorAll(".busy").length, count); + } + + { + info("Opening for the first time"); + let attendeesWindow = await openAttendeesWindow(eventWindow); + let attendeesDocument = attendeesWindow.document; + let attendeesList = attendeesDocument.getElementById("attendee-list"); + + Assert.equal(attendeesWindow.arguments[0].calendar, calendar); + Assert.equal(attendeesWindow.arguments[0].organizer, null); + Assert.equal(calendar.getProperty("organizerId"), "mailto:mochitest@example.com"); + Assert.deepEqual(attendeesWindow.arguments[0].attendees, []); + + await new Promise(resolve => attendeesWindow.setTimeout(resolve)); + + let attendeesStartTime = attendeesDocument.getElementById("event-starttime"); + let attendeesEndTime = attendeesDocument.getElementById("event-endtime"); + Assert.equal(attendeesStartTime.value.toISOString(), times.ONE.toISOString()); + Assert.equal(attendeesEndTime.value.toISOString(), times.THREE_THIRTY.toISOString()); + + attendeesStartTime.value = times.TWO_THIRTY; + attendeesEndTime.value = times.FOUR; + + // Check free/busy of organizer. + + await checkAttendeesInAttendeesDialog(attendeesDocument, ["mochitest@example.com"]); + + let organizer = attendeesList.firstElementChild; + await checkFreeBusy(organizer, 5); + + // Add attendee. + + EventUtils.sendString("test@example.com", attendeesWindow); + EventUtils.synthesizeKey("VK_TAB", {}, attendeesWindow); + + await checkAttendeesInAttendeesDialog(attendeesDocument, [ + "mochitest@example.com", + "test@example.com", + ]); + await checkFreeBusy(attendeesList.children[1], 0); + + // Add another attendee, from the address book. + + let input = attendeesDocument.activeElement; + EventUtils.sendString("julie", attendeesWindow); + await new Promise(resolve => attendeesWindow.setTimeout(resolve, 1000)); + Assert.equal(input.value, "juliet Mochitest <juliet@example.com>"); + Assert.ok(input.popupElement.popupOpen); + Assert.equal(input.popupElement.richlistbox.childElementCount, 1); + Assert.equal(input.popupElement._currentIndex, 1); + EventUtils.synthesizeKey("VK_DOWN", {}, attendeesWindow); + Assert.equal(input.popupElement._currentIndex, 1); + EventUtils.synthesizeKey("VK_TAB", {}, attendeesWindow); + + await checkAttendeesInAttendeesDialog(attendeesDocument, [ + "mochitest@example.com", + "test@example.com", + "Juliet Mochitest <juliet@example.com>", + ]); + await checkFreeBusy(attendeesList.children[2], 1); + + // Add a mailing list which should expand. + + input = attendeesDocument.activeElement; + EventUtils.sendString("boys", attendeesWindow); + await new Promise(resolve => attendeesWindow.setTimeout(resolve, 1000)); + Assert.equal(input.value, "boys >> The Boys <The Boys>"); + Assert.ok(input.popupElement.popupOpen); + Assert.equal(input.popupElement.richlistbox.childElementCount, 1); + Assert.equal(input.popupElement._currentIndex, 1); + EventUtils.synthesizeKey("VK_DOWN", {}, attendeesWindow); + Assert.equal(input.popupElement._currentIndex, 1); + EventUtils.synthesizeKey("VK_TAB", {}, attendeesWindow); + + await checkAttendeesInAttendeesDialog(attendeesDocument, [ + "mochitest@example.com", + "test@example.com", + "Juliet Mochitest <juliet@example.com>", + "Mike Mochitest <mike@example.com>", + "Oscar Mochitest <oscar@example.com>", + "Romeo Mochitest <romeo@example.com>", + "Victor Mochitest <victor@example.com>", + ]); + await checkFreeBusy(attendeesList.children[3], 0); + await checkFreeBusy(attendeesList.children[4], 0); + await checkFreeBusy(attendeesList.children[5], 1); + await checkFreeBusy(attendeesList.children[6], 0); + + await closeAttendeesWindow(attendeesWindow); + await new Promise(resolve => eventWindow.setTimeout(resolve)); + } + + Assert.equal(eventStartTime.value.toISOString(), times.TWO_THIRTY.toISOString()); + Assert.equal(eventEndTime.value.toISOString(), times.FOUR.toISOString()); + + function checkAttendeesInEventDialog(organizer, expectedAttendees) { + Assert.equal(iframeDocument.getElementById("item-organizer-row").textContent, organizer); + + let attendeeItems = iframeDocument.querySelectorAll(".attendee-list .attendee-label"); + Assert.equal(attendeeItems.length, expectedAttendees.length); + for (let i = 0; i < expectedAttendees.length; i++) { + Assert.equal(attendeeItems[i].getAttribute("attendeeid"), expectedAttendees[i]); + } + } + + checkAttendeesInEventDialog("mochitest@example.com", [ + "mailto:mochitest@example.com", + "mailto:test@example.com", + "mailto:juliet@example.com", + "mailto:mike@example.com", + "mailto:oscar@example.com", + "mailto:romeo@example.com", + "mailto:victor@example.com", + ]); + + { + info("Opening for a second time"); + let attendeesWindow = await openAttendeesWindow(eventWindow); + let attendeesDocument = attendeesWindow.document; + let attendeesList = attendeesDocument.getElementById("attendee-list"); + + let attendeesStartTime = attendeesDocument.getElementById("event-starttime"); + let attendeesEndTime = attendeesDocument.getElementById("event-endtime"); + Assert.equal(attendeesStartTime.value.toISOString(), times.TWO_THIRTY.toISOString()); + Assert.equal(attendeesEndTime.value.toISOString(), times.FOUR.toISOString()); + + await checkAttendeesInAttendeesDialog(attendeesDocument, [ + "mochitest@example.com", + "test@example.com", + "Juliet Mochitest <juliet@example.com>", + "Mike Mochitest <mike@example.com>", + "Oscar Mochitest <oscar@example.com>", + "Romeo Mochitest <romeo@example.com>", + "Victor Mochitest <victor@example.com>", + ]); + + await checkFreeBusy(attendeesList.children[0], 5); + await checkFreeBusy(attendeesList.children[1], 0); + await checkFreeBusy(attendeesList.children[2], 1); + await checkFreeBusy(attendeesList.children[3], 0); + await checkFreeBusy(attendeesList.children[4], 0); + await checkFreeBusy(attendeesList.children[5], 1); + await checkFreeBusy(attendeesList.children[6], 0); + + await closeAttendeesWindow(attendeesWindow); + await new Promise(resolve => eventWindow.setTimeout(resolve)); + } + + Assert.equal(eventStartTime.value.toISOString(), times.TWO_THIRTY.toISOString()); + Assert.equal(eventEndTime.value.toISOString(), times.FOUR.toISOString()); + + checkAttendeesInEventDialog("mochitest@example.com", [ + "mailto:mochitest@example.com", + "mailto:test@example.com", + "mailto:juliet@example.com", + "mailto:mike@example.com", + "mailto:oscar@example.com", + "mailto:romeo@example.com", + "mailto:victor@example.com", + ]); + + iframeDocument.getElementById("notify-attendees-checkbox").checked = false; + await closeEventWindow(eventWindow); +}); + +add_task(async () => { + let calendar = CalendarTestUtils.createCalendar("Mochitest", "memory"); + calendar.setProperty("organizerId", "mailto:mochitest@example.com"); + + registerCleanupFunction(async () => { + CalendarTestUtils.removeCalendar(calendar); + }); + + let defaults = { + displayTimezone: true, + attendees: [], + organizer: null, + calendar, + onOk: () => {}, + }; + + async function testDays(startTime, endTime, expectedFirst, expectedLast) { + let attendeesWindow = await openAttendeesWindow({ ...defaults, startTime, endTime }); + let attendeesDocument = attendeesWindow.document; + + let days = attendeesDocument.querySelectorAll("calendar-day"); + Assert.equal(days.length, 16); + Assert.equal(days[0].date.icalString, expectedFirst); + Assert.equal(days[15].date.icalString, expectedLast); + + await closeAttendeesWindow(attendeesWindow); + } + + // With the management of the reduced days or not, the format of the dates is different according to the cases. + // In case of a reduced day, the day format will include the start hour of the day (defined by calendar.view.daystarthour). + // In the case of a full day, we keep the behavior similar to before. + + //Full day tests + await testDays( + cal.createDateTime("20100403T020000"), + cal.createDateTime("20100403T030000"), + "20100403", + "20100418" + ); + for (let i = -2; i < 0; i++) { + await testDays( + fromToday({ days: i, hours: 2 }), + fromToday({ days: i, hours: 3 }), + fromToday({ days: i }).icalString.substring(0, 8), + fromToday({ days: i + 15 }).icalString.substring(0, 8) + ); + } + for (let i = 0; i < 3; i++) { + await testDays( + fromToday({ days: i, hours: 2 }), + fromToday({ days: i, hours: 3 }), + fromToday({ days: 0 }).icalString.substring(0, 8), + fromToday({ days: 15 }).icalString.substring(0, 8) + ); + } + for (let i = 3; i < 5; i++) { + await testDays( + fromToday({ days: i, hours: 2 }), + fromToday({ days: i, hours: 3 }), + fromToday({ days: i - 2 }).icalString.substring(0, 8), + fromToday({ days: i + 13 }).icalString.substring(0, 8) + ); + } + await testDays( + cal.createDateTime("20300403T020000"), + cal.createDateTime("20300403T030000"), + "20300401", + "20300416" + ); + + // Reduced day tests + let dayStartHour = Services.prefs.getIntPref("calendar.view.daystarthour", 8).toString(); + if (dayStartHour.length == 1) { + dayStartHour = "0" + dayStartHour; + } + + await testDays( + cal.createDateTime("20100403T120000"), + cal.createDateTime("20100403T130000"), + "20100403T" + dayStartHour + "0000Z", + "20100418T" + dayStartHour + "0000Z" + ); + for (let i = -2; i < 0; i++) { + await testDays( + fromToday({ days: i, hours: 12 }), + fromToday({ days: i, hours: 13 }), + fromToday({ days: i }).icalString.substring(0, 8) + "T" + dayStartHour + "0000Z", + fromToday({ days: i + 15 }).icalString.substring(0, 8) + "T" + dayStartHour + "0000Z" + ); + } + for (let i = 0; i < 3; i++) { + await testDays( + fromToday({ days: i, hours: 12 }), + fromToday({ days: i, hours: 13 }), + fromToday({ days: 0 }).icalString.substring(0, 8) + "T" + dayStartHour + "0000Z", + fromToday({ days: 15 }).icalString.substring(0, 8) + "T" + dayStartHour + "0000Z" + ); + } + for (let i = 3; i < 5; i++) { + await testDays( + fromToday({ days: i, hours: 12 }), + fromToday({ days: i, hours: 13 }), + fromToday({ days: i - 2 }).icalString.substring(0, 8) + "T" + dayStartHour + "0000Z", + fromToday({ days: i + 13 }).icalString.substring(0, 8) + "T" + dayStartHour + "0000Z" + ); + } + await testDays( + cal.createDateTime("20300403T120000"), + cal.createDateTime("20300403T130000"), + "20300401T" + dayStartHour + "0000Z", + "20300416T" + dayStartHour + "0000Z" + ); +}); + +function openEventWindow(calendar) { + let eventWindowPromise = BrowserTestUtils.domWindowOpened(null, async win => { + await BrowserTestUtils.waitForEvent(win, "load"); + + let doc = win.document; + if (doc.documentURI == "chrome://calendar/content/calendar-event-dialog.xhtml") { + let iframe = doc.getElementById("calendar-item-panel-iframe"); + await BrowserTestUtils.waitForEvent(iframe.contentWindow, "load"); + return true; + } + return false; + }); + createEventWithDialog(calendar, null, null, "Event"); + return eventWindowPromise; +} + +async function closeEventWindow(eventWindow) { + let eventWindowPromise = BrowserTestUtils.domWindowClosed(eventWindow); + eventWindow.document.getElementById("button-saveandclose").click(); + await eventWindowPromise; + await new Promise(resolve => setTimeout(resolve)); +} + +function fromToday({ days = 0, hours = 0 }) { + if (!fromToday.today) { + fromToday.today = cal.dtz.now(); + fromToday.today.hour = fromToday.today.minute = fromToday.today.second = 0; + } + + let duration = cal.createDuration(); + duration.days = days; + duration.hours = hours; + + let value = fromToday.today.clone(); + value.addDuration(duration); + return value; +} + +var freeBusyProvider = { + pendingRequests: [], + sendNextResponse() { + let next = this.pendingRequests.shift(); + if (next) { + next(); + } + }, + getFreeBusyIntervals(aCalId, aStart, aEnd, aTypes, aListener) { + this.pendingRequests.push(() => { + info(`Sending free/busy response for ${aCalId}`); + if (aCalId in this.data) { + aListener.onResult( + null, + this.data[aCalId].map(([startDuration, duration]) => { + let start = fromToday(startDuration); + + let end = start.clone(); + end.addDuration(cal.createDuration(duration)); + + return new cal.provider.FreeBusyInterval( + aCalId, + Ci.calIFreeBusyInterval.BUSY, + start, + end + ); + }) + ); + } else { + aListener.onResult(null, []); + } + }); + }, + data: { + "mailto:mochitest@example.com": [ + [{ days: 1, hours: 4 }, "PT3H"], + [{ days: 1, hours: 8 }, "PT3H"], + [{ days: 1, hours: 12 }, "PT3H"], + [{ days: 1, hours: 16 }, "PT3H"], + [{ days: 2, hours: 4 }, "PT3H"], + ], + "mailto:juliet@example.com": [["P1DT9H", "PT8H"]], + "mailto:romeo@example.com": [["P1DT14H", "PT5H"]], + }, +}; diff --git a/comm/calendar/test/browser/eventDialog/browser_attendeesDialogAdd.js b/comm/calendar/test/browser/eventDialog/browser_attendeesDialogAdd.js new file mode 100644 index 0000000000..c1f2778118 --- /dev/null +++ b/comm/calendar/test/browser/eventDialog/browser_attendeesDialogAdd.js @@ -0,0 +1,248 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* globals openAttendeesWindow, closeAttendeesWindow, findAndEditMatchingRow */ + +const { CalEvent } = ChromeUtils.import("resource:///modules/CalEvent.jsm"); + +add_setup(async function () { + await CalendarTestUtils.setCalendarView(window, "day"); + CalendarTestUtils.goToDate(window, 2023, 2, 18); +}); + +add_task(async function testAddAttendeeToEventWithNone() { + const calendar = CalendarTestUtils.createCalendar(); + calendar.setProperty("organizerId", "mailto:foo@example.com"); + calendar.setProperty("organizerCN", "Foo Fooson"); + + // Create an event which currently has no attendees or organizer. + const event = await calendar.addItem( + new CalEvent(CalendarTestUtils.dedent` + BEGIN:VEVENT + SUMMARY:An event + DTSTART:20230218T100000Z + DTEND:20230218T110000Z + END:VEVENT + `) + ); + + // Remember event details so we can refetch it after editing. + const eventId = event.id; + const eventModified = event.lastModifiedTime; + + // Sanity check. + Assert.equal(event.organizer, null, "event should not have an organizer"); + Assert.equal(event.getAttendees().length, 0, "event should not have any attendees"); + + // Open our event for editing. + const { dialogWindow: eventWindow } = await CalendarTestUtils.dayView.editEventAt(window, 1); + const attendeesWindow = await openAttendeesWindow(eventWindow); + + // Set text in the empty row to create a new attendee. + findAndEditMatchingRow( + attendeesWindow, + "bar@example.com", + "there should an empty input", + value => value === "" + ); + + // Save and close the event. + await closeAttendeesWindow(attendeesWindow); + await CalendarTestUtils.items.saveAndCloseItemDialog(eventWindow); + + await TestUtils.waitForCondition(async () => { + const item = await calendar.getItem(eventId); + return item.lastModifiedTime != eventModified; + }); + + const editedEvent = await calendar.getItem(eventId); + + // Verify that the organizer was set on the event. + const organizer = editedEvent.organizer; + Assert.ok(organizer, "there should be an organizer for the event after editing"); + Assert.equal( + organizer.id, + "mailto:foo@example.com", + "organizer ID should match calendar property" + ); + Assert.equal(organizer.commonName, "Foo Fooson", "organizer name should match calendar property"); + + const attendees = editedEvent.getAttendees(); + Assert.equal(attendees.length, 2, "there should be two attendees of the event after editing"); + + // Verify that the organizer was added as an attendee. + const fooFooson = attendees.find(attendee => attendee.id == "mailto:foo@example.com"); + Assert.ok(fooFooson, "the organizer should have been added as an attendee"); + Assert.equal(fooFooson.commonName, "Foo Fooson", "attendee name should match organizer's"); + Assert.equal( + fooFooson.participationStatus, + "ACCEPTED", + "organizer attendee should have automatically accepted" + ); + Assert.equal(fooFooson.role, "REQ-PARTICIPANT", "organizer attendee should be required"); + + // Verify that the attendee we added to the list is represented on the event. + const barBarrington = attendees.find(attendee => attendee.id == "mailto:bar@example.com"); + Assert.ok(barBarrington, "an attendee should have the address bar@example.com"); + Assert.equal(barBarrington.commonName, null, "new attendee name should not be set"); + Assert.equal( + barBarrington.participationStatus, + "NEEDS-ACTION", + "new attendee should have default participation status" + ); + Assert.equal(barBarrington.role, "REQ-PARTICIPANT", "new attendee should have default role"); + + CalendarTestUtils.removeCalendar(calendar); +}); + +add_task(async function testAddAttendeeToEventWithoutOrganizerAsAttendee() { + const calendar = CalendarTestUtils.createCalendar(); + calendar.setProperty("organizerId", "mailto:foo@example.com"); + calendar.setProperty("organizerCN", "Foo Fooson"); + + // Create an event which has an organizer and attendees, but no attendee + // matching the organizer. + const event = await calendar.addItem( + new CalEvent(CalendarTestUtils.dedent` + BEGIN:VEVENT + SUMMARY:An event + DTSTART:20230218T100000Z + DTEND:20230218T110000Z + ORGANIZER;CN="Foo Fooson":mailto:foo@example.com + ATTENDEE;CN="Bar Barrington";PARTSTAT=DECLINED;ROLE=CHAIR:mailto:bar@examp + le.com + ATTENDEE;CN="Baz Luhrmann";PARTSTAT=NEEDS-ACTION;ROLE=OPT-PARTICIPANT;RSV + P=TRUE:mailto:baz@example.com + END:VEVENT + `) + ); + + // Remember event details so we can refetch it after editing. + const eventId = event.id; + const eventModified = event.lastModifiedTime; + + // Sanity check. Note that order of attendees is not significant and thus not + // guaranteed. + const organizer = event.organizer; + Assert.ok(organizer, "the organizer should be set"); + Assert.equal(organizer.id, "mailto:foo@example.com", "organizer ID should match"); + Assert.equal(organizer.commonName, "Foo Fooson", "organizer name should match"); + + const attendees = event.getAttendees(); + Assert.equal(attendees.length, 2, "there should be two attendees of the event"); + + const fooFooson = attendees.find(attendee => attendee.id == "mailto:foo@example.com"); + Assert.ok(!fooFooson, "there should be no attendee matching the organizer"); + + const barBarrington = attendees.find(attendee => attendee.id == "mailto:bar@example.com"); + Assert.ok(barBarrington, "an attendee should have the address bar@example.com"); + Assert.equal(barBarrington.commonName, "Bar Barrington", "attendee name should match"); + Assert.equal(barBarrington.participationStatus, "DECLINED", "attendee should have declined"); + Assert.equal(barBarrington.role, "CHAIR", "attendee should be the meeting chair"); + + const bazLuhrmann = attendees.find(attendee => attendee.id == "mailto:baz@example.com"); + Assert.ok(bazLuhrmann, "an attendee should have the address baz@example.com"); + Assert.equal(bazLuhrmann.commonName, "Baz Luhrmann", "attendee name should match"); + Assert.equal( + bazLuhrmann.participationStatus, + "NEEDS-ACTION", + "attendee should not have responded yet" + ); + Assert.equal(bazLuhrmann.role, "OPT-PARTICIPANT", "attendee should be optional"); + Assert.equal(bazLuhrmann.rsvp, "TRUE", "attendee should be expected to RSVP"); + + // Open our event for editing. + const { dialogWindow: eventWindow } = await CalendarTestUtils.dayView.editEventAt(window, 1); + const attendeesWindow = await openAttendeesWindow(eventWindow); + + // Verify that we don't display an attendee for the organizer if there is no + // attendee on the event for them. + const attendeeList = attendeesWindow.document.getElementById("attendee-list"); + const attendeeInput = Array.from(attendeeList.children) + .map(child => child.querySelector("input")) + .find(input => { + return input ? input.value.includes("foo@example.com") : false; + }); + Assert.ok(!attendeeInput, "there should be no row in the dialog for the organizer"); + + // Set text in the empty row to create a new attendee. + findAndEditMatchingRow( + attendeesWindow, + "Jim James <jim@example.com>", + "there should an empty input", + value => value === "" + ); + + // Save and close the event. + await closeAttendeesWindow(attendeesWindow); + await CalendarTestUtils.items.saveAndCloseItemDialog(eventWindow); + + await TestUtils.waitForCondition(async () => { + const item = await calendar.getItem(eventId); + return item.lastModifiedTime != eventModified; + }); + + const editedEvent = await calendar.getItem(eventId); + + // Verify that the organizer hasn't changed. + const editedOrganizer = editedEvent.organizer; + Assert.ok(editedOrganizer, "the organizer should still be set on the event after editing"); + Assert.equal( + editedOrganizer.id, + "mailto:foo@example.com", + "organizer ID should not have changed" + ); + Assert.equal(editedOrganizer.commonName, "Foo Fooson", "organizer name should not have changed"); + + const editedAttendees = editedEvent.getAttendees(); + Assert.equal( + editedAttendees.length, + 3, + "there should be three attendees of the event after editing" + ); + + // Verify that no attendee matching the organizer was added. + const editedFooFooson = editedAttendees.find(attendee => attendee.id == "mailto:foo@example.com"); + Assert.ok(!editedFooFooson, "there should still be no attendee matching the organizer"); + + // Verify that a new attendee was added. + const jimJames = editedAttendees.find(attendee => attendee.id == "mailto:jim@example.com"); + Assert.ok(jimJames, "an attendee should have the address jim@example.com"); + Assert.equal(jimJames.commonName, "Jim James", "new attendee name should be set"); + Assert.equal( + jimJames.participationStatus, + "NEEDS-ACTION", + "new attendee should have default participation status" + ); + Assert.equal(jimJames.role, "REQ-PARTICIPANT", "new attendee should have default role"); + + // Verify that the original first attendee's properties remain untouched. + const editedBarBarrington = editedAttendees.find( + attendee => attendee.id == "mailto:bar@example.com" + ); + Assert.ok(editedBarBarrington, "an attendee should have the address bar@example.com"); + Assert.equal(editedBarBarrington.commonName, "Bar Barrington", "attendee name should match"); + Assert.equal( + editedBarBarrington.participationStatus, + "DECLINED", + "attendee should have declined" + ); + Assert.equal(editedBarBarrington.role, "CHAIR", "attendee should be the meeting chair"); + + // Verify that the original second attendee's properties remain untouched. + const editedBazLuhrmann = editedAttendees.find( + attendee => attendee.id == "mailto:baz@example.com" + ); + Assert.ok(editedBazLuhrmann, "an attendee should have the address baz@example.com"); + Assert.equal(editedBazLuhrmann.commonName, "Baz Luhrmann", "attendee name should match"); + Assert.equal( + editedBazLuhrmann.participationStatus, + "NEEDS-ACTION", + "attendee should not have responded yet" + ); + Assert.equal(editedBazLuhrmann.role, "OPT-PARTICIPANT", "attendee should be optional"); + Assert.equal(editedBazLuhrmann.rsvp, "TRUE", "attendee should be expected to RSVP"); + + CalendarTestUtils.removeCalendar(calendar); +}); diff --git a/comm/calendar/test/browser/eventDialog/browser_attendeesDialogNoEdit.js b/comm/calendar/test/browser/eventDialog/browser_attendeesDialogNoEdit.js new file mode 100644 index 0000000000..a103173790 --- /dev/null +++ b/comm/calendar/test/browser/eventDialog/browser_attendeesDialogNoEdit.js @@ -0,0 +1,68 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* globals openAttendeesWindow, closeAttendeesWindow, findAndFocusMatchingRow */ + +const { CalEvent } = ChromeUtils.import("resource:///modules/CalEvent.jsm"); + +add_setup(async function () { + await CalendarTestUtils.setCalendarView(window, "day"); + CalendarTestUtils.goToDate(window, 2023, 2, 18); +}); + +add_task(async function testBackingOutWithNoAttendees() { + const calendar = CalendarTestUtils.createCalendar(); + calendar.setProperty("organizerId", "mailto:foo@example.com"); + calendar.setProperty("organizerCN", "Foo Fooson"); + + // Create an event which currently has no attendees or organizer. + const event = await calendar.addItem( + new CalEvent(CalendarTestUtils.dedent` + BEGIN:VEVENT + SUMMARY:An event + DTSTART:20230218T100000Z + DTEND:20230218T110000Z + END:VEVENT + `) + ); + + // Remember event details so we can refetch it after editing. + const eventId = event.id; + const eventModified = event.lastModifiedTime; + + // Sanity check. + Assert.equal(event.organizer, null, "event should not have an organizer"); + Assert.equal(event.getAttendees().length, 0, "event should not have any attendees"); + + // Open our event for editing. + const { dialogWindow: eventWindow } = await CalendarTestUtils.dayView.editEventAt(window, 1); + const attendeesWindow = await openAttendeesWindow(eventWindow); + + findAndFocusMatchingRow(attendeesWindow, "there should be a row matching the organizer", value => + value.includes(calendar.getProperty("organizerCN")) + ); + + // We changed our mind. Save and close the event. + await closeAttendeesWindow(attendeesWindow); + await CalendarTestUtils.items.saveAndCloseItemDialog(eventWindow); + + // The event is still counted as modified even with no changes. If this + // changes in the future, we'll just need to wait a reasonable time and fetch + // the event again. + await TestUtils.waitForCondition(async () => { + const item = await calendar.getItem(eventId); + return item.lastModifiedTime != eventModified; + }); + + const editedEvent = await calendar.getItem(eventId); + + // Verify that the organizer was set on the event. + const organizer = editedEvent.organizer; + Assert.ok(!organizer, "there should still be no organizer for the event"); + + const attendees = editedEvent.getAttendees(); + Assert.equal(attendees.length, 0, "there should still be no attendees of the event"); + + CalendarTestUtils.removeCalendar(calendar); +}); diff --git a/comm/calendar/test/browser/eventDialog/browser_attendeesDialogRemove.js b/comm/calendar/test/browser/eventDialog/browser_attendeesDialogRemove.js new file mode 100644 index 0000000000..7ad5a3cf68 --- /dev/null +++ b/comm/calendar/test/browser/eventDialog/browser_attendeesDialogRemove.js @@ -0,0 +1,147 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* globals openAttendeesWindow, closeAttendeesWindow, findAndEditMatchingRow */ + +const { CalEvent } = ChromeUtils.import("resource:///modules/CalEvent.jsm"); + +add_setup(async function () { + await CalendarTestUtils.setCalendarView(window, "day"); + CalendarTestUtils.goToDate(window, 2023, 2, 18); +}); + +add_task(async function testRemoveOrganizerAttendee() { + const calendar = CalendarTestUtils.createCalendar(); + calendar.setProperty("organizerId", "mailto:jim@example.com"); + calendar.setProperty("organizerCN", "Jim James"); + + // Create an event with several attendees, including one matching the current + // organizer. + const event = await calendar.addItem( + new CalEvent(CalendarTestUtils.dedent` + BEGIN:VEVENT + SUMMARY:An event + DTSTART:20230218T100000Z + DTEND:20230218T110000Z + ORGANIZER;CN="Foo Fooson":mailto:foo@example.com + ATTENDEE;CN="Foo Fooson";PARTSTAT=TENTATIVE;ROLE=REQ-PARTICIPANT:mailto:f + oo@example.com + ATTENDEE;CN="Bar Barrington";PARTSTAT=DECLINED;ROLE=CHAIR:mailto:bar@exam + ple.com + ATTENDEE;CN="Baz Luhrmann";PARTSTAT=NEEDS-ACTION;ROLE=OPT-PARTICIPANT;RSV + P=TRUE:mailto:baz@example.com + END:VEVENT + `) + ); + + // Remember event details so we can refetch it after editing. + const eventId = event.id; + const eventModified = event.lastModifiedTime; + + // Sanity check. Note that order of attendees is not significant and thus not + // guaranteed. + const organizer = event.organizer; + Assert.ok(organizer, "the organizer should be set"); + Assert.equal(organizer.id, "mailto:foo@example.com", "organizer ID should match"); + Assert.equal(organizer.commonName, "Foo Fooson", "organizer name should match"); + + const attendees = event.getAttendees(); + Assert.equal(attendees.length, 3, "there should be three attendees of the event"); + + const fooFooson = attendees.find(attendee => attendee.id == "mailto:foo@example.com"); + Assert.ok(fooFooson, "an attendee should match the organizer"); + Assert.equal(fooFooson.commonName, "Foo Fooson", "attendee name should match"); + Assert.equal(fooFooson.participationStatus, "TENTATIVE", "attendee should be marked tentative"); + Assert.equal(fooFooson.role, "REQ-PARTICIPANT", "attendee should be required"); + + const barBarrington = attendees.find(attendee => attendee.id == "mailto:bar@example.com"); + Assert.ok(barBarrington, "an attendee should have the address bar@example.com"); + Assert.equal(barBarrington.commonName, "Bar Barrington", "attendee name should match"); + Assert.equal(barBarrington.participationStatus, "DECLINED", "attendee should have declined"); + Assert.equal(barBarrington.role, "CHAIR", "attendee should be the meeting chair"); + + const bazLuhrmann = attendees.find(attendee => attendee.id == "mailto:baz@example.com"); + Assert.ok(bazLuhrmann, "an attendee should have the address baz@example.com"); + Assert.equal(bazLuhrmann.commonName, "Baz Luhrmann", "attendee name should match"); + Assert.equal( + bazLuhrmann.participationStatus, + "NEEDS-ACTION", + "attendee should not have responded yet" + ); + Assert.equal(bazLuhrmann.role, "OPT-PARTICIPANT", "attendee should be optional"); + Assert.equal(bazLuhrmann.rsvp, "TRUE", "attendee should be expected to RSVP"); + + // Open our event for editing. + const { dialogWindow: eventWindow } = await CalendarTestUtils.dayView.editEventAt(window, 1); + const attendeesWindow = await openAttendeesWindow(eventWindow); + + // Empty the row matching the organizer's attendee. + findAndEditMatchingRow( + attendeesWindow, + "", + "there should an input for attendee matching the organizer", + value => value.includes("foo@example.com") + ); + + // Save and close the event. + await closeAttendeesWindow(attendeesWindow); + await CalendarTestUtils.items.saveAndCloseItemDialog(eventWindow); + + await TestUtils.waitForCondition(async () => { + const item = await calendar.getItem(eventId); + return item.lastModifiedTime != eventModified; + }); + + const editedEvent = await calendar.getItem(eventId); + + // Verify that the organizer hasn't changed. + const editedOrganizer = editedEvent.organizer; + Assert.ok(editedOrganizer, "the organizer should still be set on the event after editing"); + Assert.equal( + editedOrganizer.id, + "mailto:foo@example.com", + "organizer ID should not have changed" + ); + Assert.equal(editedOrganizer.commonName, "Foo Fooson", "organizer name should not have changed"); + + const editedAttendees = editedEvent.getAttendees(); + Assert.equal( + editedAttendees.length, + 2, + "there should be two attendees of the event after editing" + ); + + // Verify that the attendee matching the organizer was removed. + const editedFooFooson = editedAttendees.find(attendee => attendee.id == "mailto:foo@example.com"); + Assert.ok(!editedFooFooson, "there should be no attendee matching the organizer after editing"); + + // Verify that the second attendee's properties remain untouched. + const editedBarBarrington = editedAttendees.find( + attendee => attendee.id == "mailto:bar@example.com" + ); + Assert.ok(editedBarBarrington, "an attendee should have the address bar@example.com"); + Assert.equal(editedBarBarrington.commonName, "Bar Barrington", "attendee name should match"); + Assert.equal( + editedBarBarrington.participationStatus, + "DECLINED", + "attendee should have declined" + ); + Assert.equal(editedBarBarrington.role, "CHAIR", "attendee should be the meeting chair"); + + // Verify that the final attendee's properties remain untouched. + const editedBazLuhrmann = editedAttendees.find( + attendee => attendee.id == "mailto:baz@example.com" + ); + Assert.ok(editedBazLuhrmann, "an attendee should have the address baz@example.com"); + Assert.equal(editedBazLuhrmann.commonName, "Baz Luhrmann", "attendee name should match"); + Assert.equal( + editedBazLuhrmann.participationStatus, + "NEEDS-ACTION", + "attendee should not have responded yet" + ); + Assert.equal(editedBazLuhrmann.role, "OPT-PARTICIPANT", "attendee should be optional"); + Assert.equal(editedBazLuhrmann.rsvp, "TRUE", "attendee should be expected to RSVP"); + + CalendarTestUtils.removeCalendar(calendar); +}); diff --git a/comm/calendar/test/browser/eventDialog/browser_attendeesDialogUpdate.js b/comm/calendar/test/browser/eventDialog/browser_attendeesDialogUpdate.js new file mode 100644 index 0000000000..b4e30344d0 --- /dev/null +++ b/comm/calendar/test/browser/eventDialog/browser_attendeesDialogUpdate.js @@ -0,0 +1,140 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* globals openAttendeesWindow, closeAttendeesWindow, findAndEditMatchingRow */ + +const { CalEvent } = ChromeUtils.import("resource:///modules/CalEvent.jsm"); + +add_setup(async function () { + await CalendarTestUtils.setCalendarView(window, "day"); + CalendarTestUtils.goToDate(window, 2023, 2, 18); +}); + +add_task(async function testUpdateAttendee() { + const calendar = CalendarTestUtils.createCalendar(); + calendar.setProperty("organizerId", "mailto:foo@example.com"); + + // Create an event with several attendees, all of which should have some + // non-default properties which aren't covered in the attendees dialog to + // ensure that we aren't throwing properties away when we close the dialog. + const event = await calendar.addItem( + new CalEvent(CalendarTestUtils.dedent` + BEGIN:VEVENT + SUMMARY:An event + DTSTART:20230218T100000Z + DTEND:20230218T110000Z + ORGANIZER;CN="Foo Fooson":mailto:foo@example.com + ATTENDEE;CN="Foo Fooson";PARTSTAT=TENTATIVE;ROLE=REQ-PARTICIPANT:mailto:f + oo@example.com + ATTENDEE;CN="Bar Barington";PARTSTAT=DECLINED;ROLE=CHAIR:mailto:bar@examp + le.com + ATTENDEE;CN="Baz Luhrmann";PARTSTAT=NEEDS-ACTION;ROLE=OPT-PARTICIPANT;RSV + P=TRUE:mailto:baz@example.com + END:VEVENT + `) + ); + + // Remember event details so we can refetch it after editing. + const eventId = event.id; + const eventModified = event.lastModifiedTime; + + // Sanity check. Note that order of attendees is not significant and thus not + // guaranteed. + const attendees = event.getAttendees(); + Assert.equal(attendees.length, 3, "there should be three attendees of the event"); + + const fooFooson = attendees.find(attendee => attendee.id == "mailto:foo@example.com"); + Assert.ok(fooFooson, "an attendee should have the address foo@example.com"); + Assert.equal(fooFooson.commonName, "Foo Fooson", "attendee name should match"); + Assert.equal(fooFooson.participationStatus, "TENTATIVE", "attendee should be marked tentative"); + Assert.equal(fooFooson.role, "REQ-PARTICIPANT", "attendee should be required"); + + const barBarrington = attendees.find(attendee => attendee.id == "mailto:bar@example.com"); + Assert.ok(barBarrington, "an attendee should have the address bar@example.com"); + Assert.equal(barBarrington.commonName, "Bar Barington", "attendee name should match"); + Assert.equal(barBarrington.participationStatus, "DECLINED", "attendee should have declined"); + Assert.equal(barBarrington.role, "CHAIR", "attendee should be the meeting chair"); + + const bazLuhrmann = attendees.find(attendee => attendee.id == "mailto:baz@example.com"); + Assert.ok(bazLuhrmann, "an attendee should have the address baz@example.com"); + Assert.equal(bazLuhrmann.commonName, "Baz Luhrmann", "attendee name should match"); + Assert.equal( + bazLuhrmann.participationStatus, + "NEEDS-ACTION", + "attendee should not have responded yet" + ); + Assert.equal(bazLuhrmann.role, "OPT-PARTICIPANT", "attendee should be optional"); + Assert.equal(bazLuhrmann.rsvp, "TRUE", "attendee should be expected to RSVP"); + + // Open our event for editing. + const { dialogWindow: eventWindow } = await CalendarTestUtils.dayView.editEventAt(window, 1); + const attendeesWindow = await openAttendeesWindow(eventWindow); + + // Edit the second attendee to correct their name. + findAndEditMatchingRow( + attendeesWindow, + "Bar Barrington <bar@example.com>", + "there should an input containing the provided email", + value => value.includes("bar@example.com") + ); + + // Save and close the event. + await closeAttendeesWindow(attendeesWindow); + await CalendarTestUtils.items.saveAndCloseItemDialog(eventWindow); + + await TestUtils.waitForCondition(async () => { + const item = await calendar.getItem(eventId); + return item.lastModifiedTime != eventModified; + }); + + const editedEvent = await calendar.getItem(eventId); + const editedAttendees = editedEvent.getAttendees(); + Assert.equal( + editedAttendees.length, + 3, + "there should be three attendees of the event after editing" + ); + + // Verify that the first attendee's properties have not been overwritten or + // lost. + const editedFooFooson = editedAttendees.find(attendee => attendee.id == "mailto:foo@example.com"); + Assert.ok(editedFooFooson, "an attendee should have the address foo@example.com"); + Assert.equal(editedFooFooson.commonName, "Foo Fooson", "attendee name should match"); + Assert.equal( + editedFooFooson.participationStatus, + "TENTATIVE", + "attendee should be marked tentative" + ); + Assert.equal(editedFooFooson.role, "REQ-PARTICIPANT", "attendee should be required"); + + // Verify that the second attendee's name has been changed and all other + // fields remain untouched. + const editedBarBarrington = editedAttendees.find( + attendee => attendee.id == "mailto:bar@example.com" + ); + Assert.ok(editedBarBarrington, "an attendee should have the address bar@example.com"); + Assert.equal(editedBarBarrington.commonName, "Bar Barrington", "attendee name should match"); + Assert.equal( + editedBarBarrington.participationStatus, + "DECLINED", + "attendee should have declined" + ); + Assert.equal(editedBarBarrington.role, "CHAIR", "attendee should be the meeting chair"); + + // Verify that the final attendee's properties remain untouched. + const editedBazLuhrmann = editedAttendees.find( + attendee => attendee.id == "mailto:baz@example.com" + ); + Assert.ok(editedBazLuhrmann, "an attendee should have the address baz@example.com"); + Assert.equal(editedBazLuhrmann.commonName, "Baz Luhrmann", "attendee name should match"); + Assert.equal( + editedBazLuhrmann.participationStatus, + "NEEDS-ACTION", + "attendee should not have responded yet" + ); + Assert.equal(editedBazLuhrmann.role, "OPT-PARTICIPANT", "attendee should be optional"); + Assert.equal(editedBazLuhrmann.rsvp, "TRUE", "attendee should be expected to RSVP"); + + CalendarTestUtils.removeCalendar(calendar); +}); diff --git a/comm/calendar/test/browser/eventDialog/browser_eventDialog.js b/comm/calendar/test/browser/eventDialog/browser_eventDialog.js new file mode 100644 index 0000000000..44d75d7169 --- /dev/null +++ b/comm/calendar/test/browser/eventDialog/browser_eventDialog.js @@ -0,0 +1,399 @@ +/* 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 { TIMEOUT_MODAL_DIALOG, checkMonthAlarmIcon, handleDeleteOccurrencePrompt } = + ChromeUtils.import("resource://testing-common/calendar/CalendarUtils.jsm"); +var { cancelItemDialog, formatTime, saveAndCloseItemDialog, setData } = ChromeUtils.import( + "resource://testing-common/calendar/ItemEditingHelpers.jsm" +); +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetters(this, { + CalEvent: "resource:///modules/CalEvent.jsm", +}); + +const EVENTTITLE = "Event"; +const EVENTLOCATION = "Location"; +const EVENTDESCRIPTION = "Event Description"; +const EVENTATTENDEE = "foo@example.com"; +const EVENTURL = "https://mozilla.org/"; +const EVENT_ORGANIZER_EMAIL = "pillow@example.com"; +var firstDay; + +var { dayView, monthView } = CalendarTestUtils; + +let calendar = CalendarTestUtils.createCalendar(); +// This is done so that calItemBase#isInvitation returns true. +calendar.setProperty("organizerId", `mailto:${EVENT_ORGANIZER_EMAIL}`); +registerCleanupFunction(() => { + CalendarTestUtils.removeCalendar(calendar); +}); + +add_task(async function testEventDialog() { + let now = new Date(); + + // Since from other tests we may be elsewhere, make sure we start today. + await CalendarTestUtils.setCalendarView(window, "day"); + await CalendarTestUtils.goToDate( + window, + now.getUTCFullYear(), + now.getUTCMonth() + 1, + now.getUTCDate() + ); + await CalendarTestUtils.calendarViewBackward(window, 1); + + // Open month view. + await CalendarTestUtils.setCalendarView(window, "month"); + firstDay = window.currentView().startDay; + dump(`First day in view is: ${firstDay.year}-${firstDay.month + 1}-${firstDay.day}\n`); + + // Setup start- & endTime. + // Next full hour except last hour of the day. + let hour = now.getUTCHours(); + let startHour = hour == 23 ? hour : (hour + 1) % 24; + + let nextHour = cal.dtz.now(); + nextHour.resetTo(firstDay.year, firstDay.month, firstDay.day, startHour, 0, 0, cal.dtz.UTC); + let startTime = formatTime(nextHour); + nextHour.resetTo( + firstDay.year, + firstDay.month, + firstDay.day, + (startHour + 1) % 24, + 0, + 0, + cal.dtz.UTC + ); + let endTime = formatTime(nextHour); + + // Create new event on first day in view. + EventUtils.synthesizeMouseAtCenter(monthView.getDayBox(window, 1, 1), {}, window); + + let { dialogWindow, iframeWindow, dialogDocument, iframeDocument } = + await CalendarTestUtils.editNewEvent(window); + + // First check all standard-values are set correctly. + let startPicker = iframeDocument.getElementById("event-starttime"); + Assert.equal(startPicker._timepicker._inputField.value, startTime); + + // Check selected calendar. + Assert.equal(iframeDocument.getElementById("item-calendar").value, "Test"); + + // Check standard title. + let defTitle = cal.l10n.getAnyString("calendar", "calendar", "newEvent"); + Assert.equal(iframeDocument.getElementById("item-title").placeholder, defTitle); + + // Prepare category. + let categories = cal.l10n.getAnyString("calendar", "categories", "categories2"); + // Pick 4th value in a comma-separated list. + let category = categories.split(",")[4]; + // Calculate date to repeat until. + let untildate = firstDay.clone(); + untildate.addDuration(cal.createDuration("P20D")); + + // Fill in the rest of the values. + await setData(dialogWindow, iframeWindow, { + title: EVENTTITLE, + location: EVENTLOCATION, + description: EVENTDESCRIPTION, + categories: [category], + repeat: "daily", + repeatuntil: untildate, + reminder: "5minutes", + privacy: "private", + attachment: { add: EVENTURL }, + attendees: { add: EVENTATTENDEE }, + }); + + // Verify attendee added. + EventUtils.synthesizeMouseAtCenter( + iframeDocument.getElementById("event-grid-tab-attendees"), + {}, + dialogWindow + ); + + let attendeesTab = iframeDocument.getElementById("event-grid-tabpanel-attendees"); + let attendeeNameElements = attendeesTab.querySelectorAll(".attendee-list .attendee-name"); + Assert.equal(attendeeNameElements.length, 2, "there should be two attendees after save"); + Assert.equal(attendeeNameElements[0].textContent, EVENT_ORGANIZER_EMAIL); + Assert.equal(attendeeNameElements[1].textContent, EVENTATTENDEE); + Assert.ok(!iframeDocument.getElementById("notify-attendees-checkbox").checked); + + // Verify private label visible. + await TestUtils.waitForCondition( + () => !dialogDocument.getElementById("status-privacy-private-box").hasAttribute("collapsed") + ); + dialogDocument.getElementById("event-privacy-menupopup").hidePopup(); + + // Add attachment and verify added. + EventUtils.synthesizeMouseAtCenter( + iframeDocument.getElementById("event-grid-tab-attachments"), + {}, + iframeWindow + ); + + let attachmentsTab = iframeDocument.getElementById("event-grid-tabpanel-attachments"); + Assert.equal(attachmentsTab.querySelectorAll("richlistitem").length, 1); + + let alarmPromise = BrowserTestUtils.promiseAlertDialog( + undefined, + "chrome://calendar/content/calendar-alarm-dialog.xhtml", + { + callback(alarmWindow) { + let dismissAllButton = alarmWindow.document.getElementById("alarm-dismiss-all-button"); + EventUtils.synthesizeMouseAtCenter(dismissAllButton, {}, alarmWindow); + }, + } + ); + + // save + await saveAndCloseItemDialog(dialogWindow); + + // Catch and dismiss alarm. + await alarmPromise; + + // Verify event and alarm icon visible until endDate (3 full rows) and check tooltip. + for (let row = 1; row <= 3; row++) { + for (let col = 1; col <= 7; col++) { + await monthView.waitForItemAt(window, row, col, 1); + checkMonthAlarmIcon(window, row, col); + checkTooltip(row, col, startTime, endTime); + } + } + Assert.ok(!monthView.getItemAt(window, 4, 1, 1)); + + // Delete and verify deleted 6th col in row 1. + EventUtils.synthesizeMouseAtCenter(monthView.getItemAt(window, 1, 6, 1), {}, window); + let elemToDelete = document.getElementById("month-view"); + await handleDeleteOccurrencePrompt(window, elemToDelete, false); + + await monthView.waitForNoItemAt(window, 1, 6, 1); + + // Verify all others still exist. + for (let col = 1; col <= 5; col++) { + Assert.ok(monthView.getItemAt(window, 1, col, 1)); + } + Assert.ok(monthView.getItemAt(window, 1, 7, 1)); + + for (let row = 2; row <= 3; row++) { + for (let col = 1; col <= 7; col++) { + Assert.ok(monthView.getItemAt(window, row, col, 1)); + } + } + + // Delete series by deleting last item in row 1 and confirming to delete all. + EventUtils.synthesizeMouseAtCenter(monthView.getItemAt(window, 1, 7, 1), {}, window); + elemToDelete = document.getElementById("month-view"); + await handleDeleteOccurrencePrompt(window, elemToDelete, true); + + // Verify all deleted. + await monthView.waitForNoItemAt(window, 1, 5, 1); + await monthView.waitForNoItemAt(window, 1, 6, 1); + await monthView.waitForNoItemAt(window, 1, 7, 1); + + for (let row = 2; row <= 3; row++) { + for (let col = 1; col <= 7; col++) { + await monthView.waitForNoItemAt(window, row, col, 1); + } + } +}); + +add_task(async function testOpenExistingEventDialog() { + let now = new Date(); + + await CalendarTestUtils.setCalendarView(window, "day"); + await CalendarTestUtils.goToDate( + window, + now.getUTCFullYear(), + now.getUTCMonth() + 1, + now.getUTCDate() + ); + + let createBox = dayView.getHourBoxAt(window, 8); + + // Create a new event. + let { dialogWindow, iframeWindow } = await CalendarTestUtils.editNewEvent(window, createBox); + await setData(dialogWindow, iframeWindow, { + title: EVENTTITLE, + location: EVENTLOCATION, + description: EVENTDESCRIPTION, + }); + await saveAndCloseItemDialog(dialogWindow); + + let eventBox = await dayView.waitForEventBoxAt(window, 1); + + // Open the event in the summary dialog, it will fail if otherwise. + let eventWin = await CalendarTestUtils.viewItem(window, eventBox); + Assert.equal( + eventWin.document.querySelector("calendar-item-summary .item-title").textContent, + EVENTTITLE + ); + Assert.equal( + eventWin.document.querySelector("calendar-item-summary .item-location").textContent, + EVENTLOCATION + ); + Assert.equal( + eventWin.document.querySelector("calendar-item-summary .item-description").contentDocument.body + .innerText, + EVENTDESCRIPTION + ); + EventUtils.synthesizeKey("VK_ESCAPE", {}, eventWin); + + eventBox.focus(); + EventUtils.synthesizeKey("VK_DELETE", {}, window); + await dayView.waitForNoEventBoxAt(window, 1); +}); + +add_task(async function testEventReminderDisplay() { + await CalendarTestUtils.setCalendarView(window, "day"); + await CalendarTestUtils.goToDate(window, 2020, 1, 1); + + let createBox = dayView.getHourBoxAt(window, 8); + + // Create an event without a reminder. + let { dialogWindow, iframeWindow } = await CalendarTestUtils.editNewEvent(window, createBox); + await setData(dialogWindow, iframeWindow, { + title: EVENTTITLE, + location: EVENTLOCATION, + description: EVENTDESCRIPTION, + }); + await saveAndCloseItemDialog(dialogWindow); + + let eventBox = await dayView.waitForEventBoxAt(window, 1); + + let eventWindow = await CalendarTestUtils.viewItem(window, eventBox); + let doc = eventWindow.document; + let row = doc.querySelector(".reminder-row"); + Assert.ok(row.hidden, "reminder dropdown is not displayed"); + EventUtils.synthesizeKey("VK_ESCAPE", {}, eventWindow); + + await CalendarTestUtils.goToDate(window, 2020, 2, 1); + createBox = dayView.getHourBoxAt(window, 8); + + // Create an event with a reminder. + ({ dialogWindow, iframeWindow } = await CalendarTestUtils.editNewEvent(window, createBox)); + await setData(dialogWindow, iframeWindow, { + title: EVENTTITLE, + location: EVENTLOCATION, + description: EVENTDESCRIPTION, + reminder: "1week", + }); + await saveAndCloseItemDialog(dialogWindow); + + eventBox = await dayView.waitForEventBoxAt(window, 1); + eventWindow = await CalendarTestUtils.viewItem(window, eventBox); + doc = eventWindow.document; + row = doc.querySelector(".reminder-row"); + + Assert.ok( + row.textContent.includes("7 days before"), + "the details are shown when a reminder is set" + ); + EventUtils.synthesizeKey("VK_ESCAPE", {}, eventWindow); + + // Create an invitation. + let icalString = + "BEGIN:VCALENDAR\r\n" + + "PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\r\n" + + "VERSION:2.0\r\n" + + "BEGIN:VEVENT\r\n" + + "CREATED:20200301T152601Z\r\n" + + "DTSTAMP:20200301T192729Z\r\n" + + "UID:x137e\r\n" + + "SUMMARY:Nap Time\r\n" + + "ORGANIZER;CN=Papa Bois:mailto:papabois@example.com\r\n" + + "ATTENDEE;RSVP=TRUE;CN=pillow@example.com;PARTSTAT=NEEDS-ACTION;CUTY\r\n" + + " PE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;X-NUM-GUESTS=0:mailto:pillow@example.com\r\n" + + "DTSTART:20200301T153000Z\r\n" + + "DTEND:20200301T163000Z\r\n" + + "DESCRIPTION:Slumber In Lumber\r\n" + + "SEQUENCE:0\r\n" + + "TRANSP:OPAQUE\r\n" + + "BEGIN:VALARM\r\n" + + "TRIGGER:-PT30M\r\n" + + "REPEAT:2\r\n" + + "DURATION:PT15M\r\n" + + "ACTION:DISPLAY\r\n" + + "END:VALARM\r\n" + + "END:VEVENT\r\n" + + "END:VCALENDAR\r\n"; + + let calendarEvent = await calendar.addItem(new CalEvent(icalString)); + await CalendarTestUtils.goToDate(window, 2020, 3, 1); + eventBox = await dayView.waitForEventBoxAt(window, 1); + + eventWindow = await CalendarTestUtils.viewItem(window, eventBox); + doc = eventWindow.document; + row = doc.querySelector(".reminder-row"); + + Assert.ok(!row.hidden, "reminder row is displayed"); + Assert.ok(row.querySelector("menulist") != null, "reminder dropdown is available"); + EventUtils.synthesizeKey("VK_ESCAPE", {}, eventWindow); + + // Delete directly, as using the UI causes a prompt to appear. + calendar.deleteItem(calendarEvent); + await dayView.waitForNoEventBoxAt(window, 1); +}); + +/** + * Test that using CTRL+Enter does not result in two events being created. + * This only happens in the dialog window. See bug 1668478. + */ +add_task(async function testCtrlEnterShortcut() { + await CalendarTestUtils.setCalendarView(window, "day"); + await CalendarTestUtils.goToDate(window, 2020, 9, 1); + + let createBox = dayView.getHourBoxAt(window, 8); + let { dialogWindow, iframeWindow } = await CalendarTestUtils.editNewEvent(window, createBox); + await setData(dialogWindow, iframeWindow, { + title: EVENTTITLE, + location: EVENTLOCATION, + description: EVENTDESCRIPTION, + }); + EventUtils.synthesizeKey("VK_RETURN", { ctrlKey: true }, dialogWindow); + + await CalendarTestUtils.setCalendarView(window, "month"); + + // Give the event boxes enough time to appear before checking for duplicates. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 2000)); + + let events = document.querySelectorAll("calendar-month-day-box-item"); + Assert.equal(events.length, 1, "event was created once"); + + if (Services.focus.activeWindow != window) { + await BrowserTestUtils.waitForEvent(window, "focus"); + } + + events[0].focus(); + EventUtils.synthesizeKey("VK_DELETE", {}, window); +}); + +function checkTooltip(row, col, startTime, endTime) { + let item = monthView.getItemAt(window, row, col, 1); + + let toolTipNode = document.getElementById("itemTooltip"); + toolTipNode.ownerGlobal.onMouseOverItem({ currentTarget: item }); + + function getDescription(index) { + return toolTipNode.querySelector( + `.tooltipHeaderTable > tr:nth-of-type(${index}) > .tooltipHeaderDescription` + ).textContent; + } + + // Check title. + Assert.equal(getDescription(1), EVENTTITLE); + + // Check date and time. + let dateTime = getDescription(3); + + let currDate = firstDay.clone(); + currDate.addDuration(cal.createDuration(`P${7 * (row - 1) + (col - 1)}D`)); + let startDate = cal.dtz.formatter.formatDate(currDate); + + Assert.ok(dateTime.includes(`${startDate} ${startTime} – `)); + + // This could be on the next day if it is 00:00. + Assert.ok(dateTime.endsWith(endTime)); +} diff --git a/comm/calendar/test/browser/eventDialog/browser_eventDialogDescriptionEditor.js b/comm/calendar/test/browser/eventDialog/browser_eventDialogDescriptionEditor.js new file mode 100644 index 0000000000..d838330e73 --- /dev/null +++ b/comm/calendar/test/browser/eventDialog/browser_eventDialogDescriptionEditor.js @@ -0,0 +1,154 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { CalEvent } = ChromeUtils.import("resource:///modules/CalEvent.jsm"); +const { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); + +add_setup(async function () { + await CalendarTestUtils.setCalendarView(window, "day"); + CalendarTestUtils.goToDate(window, 2023, 2, 18); +}); + +add_task(async function testPastePreformattedWithLinebreak() { + const calendar = CalendarTestUtils.createCalendar(); + + // Create an event which currently has no description. + const event = await calendar.addItem( + new CalEvent(CalendarTestUtils.dedent` + BEGIN:VEVENT + SUMMARY:An event + DTSTART:20230218T100000Z + DTEND:20230218T110000Z + END:VEVENT + `) + ); + + // Remember event details so we can refetch it after editing. + const eventId = event.id; + const eventModified = event.lastModifiedTime; + + // Sanity check. + Assert.equal(event.descriptionHTML, null, "event should not have an HTML description"); + Assert.equal(event.descriptionText, null, "event should not have a text description"); + + // Open our event for editing. + const { dialogWindow: eventWindow, iframeDocument } = await CalendarTestUtils.dayView.editEventAt( + window, + 1 + ); + + const editor = iframeDocument.getElementById("item-description"); + editor.focus(); + + const expectedHTML = + "<pre><code>This event is one which includes\nan explicit linebreak inside a pre tag.</code></pre>"; + + // Create a paste which includes HTML data, which the editor will recognize as + // HTML and paste with formatting by default. + const stringData = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); + stringData.data = expectedHTML; + + const transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable); + transferable.init(null); + transferable.addDataFlavor("text/html"); + transferable.setTransferData("text/html", stringData); + Services.clipboard.setData(transferable, null, Ci.nsIClipboard.kGlobalClipboard); + + // Paste. + EventUtils.synthesizeKey("v", { accelKey: true }, eventWindow); + + await CalendarTestUtils.items.saveAndCloseItemDialog(eventWindow); + + await TestUtils.waitForCondition(async () => { + const item = await calendar.getItem(eventId); + return item.lastModifiedTime != eventModified; + }); + + const editedEvent = await calendar.getItem(eventId); + + // Verify that the description has been set appropriately. There should be no + // change to the HTML, which is preformatted, and the text description should + // include a linebreak in the same place as the HTML. + Assert.equal(editedEvent.descriptionHTML, expectedHTML, "HTML description should match input"); + Assert.equal( + editedEvent.descriptionText, + "This event is one which includes\nan explicit linebreak inside a pre tag.", + "text description should include linebreak" + ); + + CalendarTestUtils.removeCalendar(calendar); +}); + +add_task(async function testTypeLongTextWithLinebreaks() { + const calendar = CalendarTestUtils.createCalendar(); + + // Create an event which currently has no description. + const event = await calendar.addItem( + new CalEvent(CalendarTestUtils.dedent` + BEGIN:VEVENT + SUMMARY:An event + DTSTART:20230218T100000Z + DTEND:20230218T110000Z + END:VEVENT + `) + ); + + // Remember event details so we can refetch it after editing. + const eventId = event.id; + const eventModified = event.lastModifiedTime; + + // Sanity check. + Assert.equal(event.descriptionHTML, null, "event should not have an HTML description"); + Assert.equal(event.descriptionText, null, "event should not have a text description"); + + // Open our event for editing. + const { + dialogWindow: eventWindow, + iframeDocument, + iframeWindow, + } = await CalendarTestUtils.dayView.editEventAt(window, 1); + + const editor = iframeDocument.getElementById("item-description"); + editor.focus(); + + // Insert text with several long lines and explicit linebreaks. + const firstLine = + "This event is pretty much just plain text, albeit it has some pretty long lines so that we can ensure that we don't accidentally wrap it during conversion."; + EventUtils.sendString(firstLine, iframeWindow); + EventUtils.sendKey("RETURN", iframeWindow); + + const secondLine = "This line follows immediately after a linebreak."; + EventUtils.sendString(secondLine, iframeWindow); + EventUtils.sendKey("RETURN", iframeWindow); + EventUtils.sendKey("RETURN", iframeWindow); + + const thirdLine = + "And one after a couple more linebreaks, for good measure. It might as well be a fairly long string as well, just so we're certain."; + EventUtils.sendString(thirdLine, iframeWindow); + + await CalendarTestUtils.items.saveAndCloseItemDialog(eventWindow); + + await TestUtils.waitForCondition(async () => { + const item = await calendar.getItem(eventId); + return item.lastModifiedTime != eventModified; + }); + + const editedEvent = await calendar.getItem(eventId); + + // Verify that the description has been set appropriately. The HTML should + // match the input and use <br> as a linebreak, while the text should not be + // wrapped and should use \n as a linebreak. + Assert.equal( + editedEvent.descriptionHTML, + `${firstLine}<br>${secondLine}<br><br>${thirdLine}`, + "HTML description should match input with <br> for linebreaks" + ); + Assert.equal( + editedEvent.descriptionText, + `${firstLine}\n${secondLine}\n\n${thirdLine}`, + "text description should match input with linebreaks" + ); + + CalendarTestUtils.removeCalendar(calendar); +}); diff --git a/comm/calendar/test/browser/eventDialog/browser_eventDialogEditButton.js b/comm/calendar/test/browser/eventDialog/browser_eventDialogEditButton.js new file mode 100644 index 0000000000..b7730444b2 --- /dev/null +++ b/comm/calendar/test/browser/eventDialog/browser_eventDialogEditButton.js @@ -0,0 +1,223 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests for the edit button displayed in the calendar summary dialog. + */ + +const { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetters(this, { + CalEvent: "resource:///modules/CalEvent.jsm", + CalRecurrenceInfo: "resource:///modules/CalRecurrenceInfo.jsm", +}); + +const calendar = CalendarTestUtils.createCalendar("Edit Button Test", "storage"); + +registerCleanupFunction(() => { + CalendarTestUtils.removeCalendar(calendar); +}); + +function createNonRecurringEvent() { + let event = new CalEvent(); + event.title = "Non-Recurring Event"; + event.startDate = cal.createDateTime("20191201T000001Z"); + return event; +} + +function createRecurringEvent() { + let event = new CalEvent(); + event.title = "Recurring Event"; + event.startDate = cal.createDateTime("20200101T000001Z"); + event.recurrenceInfo = new CalRecurrenceInfo(event); + event.recurrenceInfo.appendRecurrenceItem(cal.createRecurrenceRule("RRULE:FREQ=DAILY;COUNT=30")); + return event; +} + +/** + * Test the correct edit button is shown for a non-recurring event. + */ +add_task(async function testNonRecurringEvent() { + let event = await calendar.addItem(createNonRecurringEvent()); + await CalendarTestUtils.setCalendarView(window, "month"); + window.goToDate(event.startDate); + + let eventWindow = await CalendarTestUtils.monthView.viewItemAt(window, 1, 1, 1); + let editMenuButton = eventWindow.document.querySelector( + "#calendar-summary-dialog-edit-menu-button" + ); + + Assert.ok( + !BrowserTestUtils.is_visible(editMenuButton), + "edit dropdown is not visible for non-recurring event" + ); + + let editButton = eventWindow.document.querySelector("#calendar-summary-dialog-edit-button"); + + Assert.ok( + BrowserTestUtils.is_visible(editButton), + "edit button is visible for non-recurring event" + ); + await CalendarTestUtils.items.cancelItemDialog(eventWindow); + await calendar.deleteItem(event); +}); + +/** + * Test the edit button for a non-recurring event actual edits the event. + */ +add_task(async function testEditNonRecurringEvent() { + let event = await calendar.addItem(createNonRecurringEvent()); + await CalendarTestUtils.setCalendarView(window, "month"); + window.goToDate(event.startDate); + + let modificationPromise = new Promise(resolve => { + calendar.wrappedJSObject.addObserver({ + QueryInterface: ChromeUtils.generateQI(["calIObserver"]), + onModifyItem(aNewItem, aOldItem) { + calendar.wrappedJSObject.removeObserver(this); + resolve(); + }, + }); + }); + + let { dialogWindow, iframeDocument } = await CalendarTestUtils.monthView.editItemAt( + window, + 1, + 1, + 1 + ); + + let newTitle = "Edited Non-Recurring Event"; + iframeDocument.querySelector("#item-title").value = newTitle; + + await CalendarTestUtils.items.saveAndCloseItemDialog(dialogWindow); + await modificationPromise; + + let viewWindow = await CalendarTestUtils.monthView.viewItemAt(window, 1, 1, 1); + let actualTitle = viewWindow.document.querySelector( + "#calendar-item-summary .item-title" + ).textContent; + + Assert.equal(actualTitle, newTitle, "edit non-recurring event successful"); + await CalendarTestUtils.items.cancelItemDialog(viewWindow); + await calendar.deleteItem(event); +}); + +/** + * Tests the dropdown menu is displayed for a recurring event. + */ +add_task(async function testRecurringEvent() { + let event = await calendar.addItem(createRecurringEvent()); + await CalendarTestUtils.setCalendarView(window, "month"); + window.goToDate(event.startDate); + + let viewWindow = await CalendarTestUtils.monthView.viewItemAt(window, 1, 6, 1); + + Assert.ok( + !BrowserTestUtils.is_visible( + viewWindow.document.querySelector("#calendar-summary-dialog-edit-button") + ), + "non-recurring edit button is not visible for recurring event" + ); + Assert.ok( + BrowserTestUtils.is_visible( + viewWindow.document.querySelector("#calendar-summary-dialog-edit-menu-button") + ), + "edit dropdown is visible for recurring event" + ); + + await CalendarTestUtils.items.cancelItemDialog(viewWindow); + await calendar.deleteItem(event); +}); + +/** + * Tests the dropdown menu allows a single occurrence of a repeating event + * to be edited. + */ +add_task(async function testEditThisOccurrence() { + let event = createRecurringEvent(); + event = await calendar.addItem(event); + + await CalendarTestUtils.setCalendarView(window, "month"); + window.goToDate(event.startDate); + + let modificationPromise = new Promise(resolve => { + calendar.wrappedJSObject.addObserver({ + QueryInterface: ChromeUtils.generateQI(["calIObserver"]), + onModifyItem(aNewItem, aOldItem) { + calendar.wrappedJSObject.removeObserver(this); + resolve(); + }, + }); + }); + + let { dialogWindow, iframeDocument } = await CalendarTestUtils.monthView.editItemOccurrenceAt( + window, + 1, + 6, + 1 + ); + + let originalTitle = event.title; + let newTitle = "Edited This Occurrence"; + + iframeDocument.querySelector("#item-title").value = newTitle; + await CalendarTestUtils.items.saveAndCloseItemDialog(dialogWindow); + + await modificationPromise; + + let changedBox = await CalendarTestUtils.monthView.waitForItemAt(window, 1, 6, 1); + let eventBoxes = document.querySelectorAll("calendar-month-day-box-item"); + + for (let box of eventBoxes) { + if (box !== changedBox) { + Assert.equal( + box.item.title, + originalTitle, + '"Edit this occurrence" did not edit other occurrences' + ); + } else { + Assert.equal(box.item.title, newTitle, '"Edit this occurrence only" edited this occurrence.'); + } + } + await calendar.deleteItem(event); +}); + +/** + * Tests the dropdown menu allows all occurrences of a recurring event to be + * edited. + */ +add_task(async function testEditAllOccurrences() { + let event = await calendar.addItem(createRecurringEvent()); + + await CalendarTestUtils.setCalendarView(window, "month"); + window.goToDate(event.startDate); + + // Setup an observer so we can wait for the event boxes to be updated. + let boxesRefreshed = false; + let observer = new MutationObserver(() => (boxesRefreshed = true)); + observer.observe(document.querySelector("#month-view"), { + childList: true, + subtree: true, + }); + + let { dialogWindow, iframeDocument } = await CalendarTestUtils.monthView.editItemOccurrencesAt( + window, + 1, + 6, + 1 + ); + + let newTitle = "Edited All Occurrences"; + + iframeDocument.querySelector("#item-title").value = newTitle; + await CalendarTestUtils.items.saveAndCloseItemDialog(dialogWindow); + await TestUtils.waitForCondition(() => boxesRefreshed, "event boxes did not refresh in time"); + + let eventBoxes = document.querySelectorAll("calendar-month-day-box-item"); + for (let box of eventBoxes) { + Assert.equal(box.item.title, newTitle, '"Edit all occurrences" edited each occurrence'); + } + await calendar.deleteItem(event); +}); diff --git a/comm/calendar/test/browser/eventDialog/browser_eventDialogModificationPrompt.js b/comm/calendar/test/browser/eventDialog/browser_eventDialogModificationPrompt.js new file mode 100644 index 0000000000..b0f3282b24 --- /dev/null +++ b/comm/calendar/test/browser/eventDialog/browser_eventDialogModificationPrompt.js @@ -0,0 +1,160 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +requestLongerTimeout(2); + +var { cancelItemDialog, saveAndCloseItemDialog, setData } = ChromeUtils.import( + "resource://testing-common/calendar/ItemEditingHelpers.jsm" +); + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); + +var { data, newlines } = setupData(); + +var { dayView } = CalendarTestUtils; + +let calendar = CalendarTestUtils.createCalendar(); +// This is done so that calItemBase#isInvitation returns true. +calendar.setProperty("organizerId", "mailto:pillow@example.com"); +registerCleanupFunction(() => { + CalendarTestUtils.removeCalendar(calendar); +}); + +// Test that closing an event dialog with no changes does not prompt for save. +add_task(async function testEventDialogModificationPrompt() { + await CalendarTestUtils.setCalendarView(window, "day"); + await CalendarTestUtils.goToDate(window, 2009, 1, 1); + + let createbox = dayView.getHourBoxAt(window, 8); + + // Create new event. + let { dialogWindow, iframeWindow } = await CalendarTestUtils.editNewEvent(window, createbox); + let categories = cal.l10n.getAnyString("calendar", "categories", "categories2").split(","); + data[0].categories.push(categories[0]); + data[1].categories.push(categories[1], categories[2]); + + // Enter first set of data. + await setData(dialogWindow, iframeWindow, data[0]); + await saveAndCloseItemDialog(dialogWindow); + + let eventbox = await dayView.waitForEventBoxAt(window, 1); + + // Open, but change nothing. + ({ dialogWindow, iframeWindow } = await CalendarTestUtils.editItem(window, eventbox)); + // Escape the event window, there should be no prompt to save event. + cancelItemDialog(dialogWindow); + // Wait to see if the prompt appears. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 2000)); + + eventbox = await dayView.waitForEventBoxAt(window, 1); + // Open, change all values then revert the changes. + ({ dialogWindow, iframeWindow } = await CalendarTestUtils.editItem(window, eventbox)); + // Change all values. + await setData(dialogWindow, iframeWindow, data[1]); + + // Edit all values back to original. + await setData(dialogWindow, iframeWindow, data[0]); + + // Escape the event window, there should be no prompt to save event. + cancelItemDialog(dialogWindow); + // Wait to see if the prompt appears. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Delete event. + document.getElementById("day-view").focus(); + if (window.currentView().getSelectedItems().length == 0) { + EventUtils.synthesizeMouseAtCenter(eventbox, {}, window); + } + Assert.equal(eventbox.isEditing, false, "event is not being edited"); + EventUtils.synthesizeKey("VK_DELETE", {}, window); + await dayView.waitForNoEventBoxAt(window, 1); +}); + +add_task(async function testDescriptionWhitespace() { + for (let i = 0; i < newlines.length; i++) { + // test set i + let createbox = dayView.getHourBoxAt(window, 8); + let { dialogWindow, iframeWindow } = await CalendarTestUtils.editNewEvent(window, createbox); + await setData(dialogWindow, iframeWindow, newlines[i]); + await saveAndCloseItemDialog(dialogWindow); + + let eventbox = await dayView.waitForEventBoxAt(window, 1); + + // Open and close. + ({ dialogWindow, iframeWindow } = await CalendarTestUtils.editItem(window, eventbox)); + await setData(dialogWindow, iframeWindow, newlines[i]); + cancelItemDialog(dialogWindow); + // Wait to see if the prompt appears. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Delete it. + document.getElementById("day-view").focus(); + if (window.currentView().getSelectedItems().length == 0) { + EventUtils.synthesizeMouseAtCenter(eventbox, {}, window); + } + Assert.equal(eventbox.isEditing, false, "event is not being edited"); + EventUtils.synthesizeKey("VK_DELETE", {}, window); + await dayView.waitForNoEventBoxAt(window, 1); + } +}); + +function setupData() { + let date1 = cal.createDateTime("20090101T080000Z"); + let date2 = cal.createDateTime("20090102T090000Z"); + let date3 = cal.createDateTime("20090103T100000Z"); + return { + data: [ + { + title: "title1", + location: "location1", + description: "description1", + categories: [], + allday: false, + startdate: date1, + starttime: date1, + enddate: date2, + endtime: date2, + repeat: "none", + reminder: "none", + priority: "normal", + privacy: "public", + status: "confirmed", + freebusy: "busy", + timezonedisplay: true, + attachment: { add: "https://mozilla.org" }, + attendees: { add: "foo@bar.de,foo@bar.com" }, + }, + { + title: "title2", + location: "location2", + description: "description2", + categories: [], + allday: true, + startdate: date2, + starttime: date2, + enddate: date3, + endtime: date3, + repeat: "daily", + reminder: "5minutes", + priority: "high", + privacy: "private", + status: "tentative", + freebusy: "free", + timezonedisplay: false, + attachment: { remove: "mozilla.org" }, + attendees: { remove: "foo@bar.de,foo@bar.com" }, + }, + ], + newlines: [ + { title: "title", description: " test spaces " }, + { title: "title", description: "\ntest newline\n" }, + { title: "title", description: "\rtest \\r\r" }, + { title: "title", description: "\r\ntest \\r\\n\r\n" }, + { title: "title", description: "\ttest \\t\t" }, + ], + }; +} diff --git a/comm/calendar/test/browser/eventDialog/browser_utf8.js b/comm/calendar/test/browser/eventDialog/browser_utf8.js new file mode 100644 index 0000000000..5e9ff82d19 --- /dev/null +++ b/comm/calendar/test/browser/eventDialog/browser_utf8.js @@ -0,0 +1,56 @@ +/* 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 { cancelItemDialog, saveAndCloseItemDialog, setData } = ChromeUtils.import( + "resource://testing-common/calendar/ItemEditingHelpers.jsm" +); + +var UTF8STRING = " 💣 💥 ☣ "; + +add_task(async function testUTF8() { + let calendar = CalendarTestUtils.createCalendar(); + Services.prefs.setStringPref("calendar.categories.names", UTF8STRING); + + registerCleanupFunction(() => { + CalendarTestUtils.removeCalendar(calendar); + Services.prefs.clearUserPref("calendar.categories.names"); + }); + + await CalendarTestUtils.setCalendarView(window, "day"); + + // Create new event. + let eventBox = CalendarTestUtils.dayView.getHourBoxAt(window, 8); + let { dialogWindow, iframeWindow } = await CalendarTestUtils.editNewEvent(window, eventBox); + // Fill in name, location, description. + await setData(dialogWindow, iframeWindow, { + title: UTF8STRING, + location: UTF8STRING, + description: UTF8STRING, + categories: [UTF8STRING], + }); + await saveAndCloseItemDialog(dialogWindow); + + // open + let { dialogWindow: dlgWindow, iframeDocument } = await CalendarTestUtils.dayView.editEventAt( + window, + 1 + ); + // Check values. + Assert.equal(iframeDocument.getElementById("item-title").value, UTF8STRING); + Assert.equal(iframeDocument.getElementById("item-location").value, UTF8STRING); + // The trailing spaces confuse innerText, so we'll do this longhand + let editorEl = iframeDocument.getElementById("item-description"); + let editor = editorEl.getEditor(editorEl.contentWindow); + let description = editor.outputToString("text/plain", 0); + // The HTML editor makes the first character a NBSP instead of a space. + Assert.equal(description.replaceAll("\xA0", " "), UTF8STRING); + Assert.ok( + iframeDocument + .getElementById("item-categories") + .querySelector(`menuitem[label="${UTF8STRING}"][checked]`) + ); + + // Escape the event window. + cancelItemDialog(dlgWindow); +}); diff --git a/comm/calendar/test/browser/eventDialog/data/guests.txt b/comm/calendar/test/browser/eventDialog/data/guests.txt new file mode 100644 index 0000000000..e2959cf71e --- /dev/null +++ b/comm/calendar/test/browser/eventDialog/data/guests.txt @@ -0,0 +1,2 @@ +Nobody +No one diff --git a/comm/calendar/test/browser/eventDialog/head.js b/comm/calendar/test/browser/eventDialog/head.js new file mode 100644 index 0000000000..0646cd709c --- /dev/null +++ b/comm/calendar/test/browser/eventDialog/head.js @@ -0,0 +1,97 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +var { CalendarTestUtils } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarTestUtils.jsm" +); + +// If the "do you want to save the event?" prompt appears, the test failed. +// Listen for all windows opening, and if one is the save prompt, fail. +var savePromptObserver = { + async observe(win, topic) { + if (topic == "domwindowopened") { + await BrowserTestUtils.waitForEvent(win, "load"); + // Make sure this is a prompt window. + if (win.location.href == "chrome://global/content/commonDialog.xhtml") { + let doc = win.document; + // Adding attachments also shows a prompt, but we can tell which one + // this is by checking whether the textbox is visible. + if (doc.querySelector("#loginContainer").hasAttribute("hidden")) { + Assert.report(true, undefined, undefined, "Unexpected save prompt appeared"); + doc.querySelector("dialog").getButton("cancel").click(); + } + } + } + }, +}; +Services.ww.registerNotification(savePromptObserver); + +const calendarViewsInitialState = CalendarTestUtils.saveCalendarViewsState(window); + +registerCleanupFunction(async () => { + Services.ww.unregisterNotification(savePromptObserver); + await CalendarTestUtils.restoreCalendarViewsState(window, calendarViewsInitialState); +}); + +function openAttendeesWindow(eventWindowOrArgs) { + let attendeesWindowPromise = BrowserTestUtils.promiseAlertDialogOpen( + null, + "chrome://calendar/content/calendar-event-dialog-attendees.xhtml", + { + async callback(win) { + await new Promise(resolve => win.setTimeout(resolve)); + }, + } + ); + + if (Window.isInstance(eventWindowOrArgs)) { + EventUtils.synthesizeMouseAtCenter( + eventWindowOrArgs.document.getElementById("button-attendees"), + {}, + eventWindowOrArgs + ); + } else { + openDialog( + "chrome://calendar/content/calendar-event-dialog-attendees.xhtml", + "_blank", + "chrome,titlebar,resizable", + eventWindowOrArgs + ); + } + return attendeesWindowPromise; +} + +async function closeAttendeesWindow(attendeesWindow, buttonAction = "accept") { + let closedPromise = BrowserTestUtils.domWindowClosed(attendeesWindow); + let dialog = attendeesWindow.document.querySelector("dialog"); + dialog.getButton(buttonAction).click(); + await closedPromise; + + await new Promise(resolve => setTimeout(resolve)); +} + +function findAndFocusMatchingRow(attendeesWindow, message, matchFunction) { + // Get the attendee row for which the input matches. + const attendeeList = attendeesWindow.document.getElementById("attendee-list"); + const attendeeInput = Array.from(attendeeList.children) + .map(child => child.querySelector("input")) + .find(input => { + return input ? matchFunction(input.value) : false; + }); + Assert.ok(attendeeInput, message); + + attendeeInput.focus(); + + return attendeeInput; +} + +function findAndEditMatchingRow(attendeesWindow, newValue, message, matchFunction) { + // Get the attendee row we wish to edit. + const attendeeInput = findAndFocusMatchingRow(attendeesWindow, message, matchFunction); + + // Set the new value of the row. We set the input value directly due to issues + // experienced trying to use simulated keystrokes. + attendeeInput.value = newValue; + EventUtils.synthesizeKey("VK_RETURN", {}, attendeesWindow); +} |