summaryrefslogtreecommitdiffstats
path: root/comm/calendar/test/browser/eventDialog
diff options
context:
space:
mode:
Diffstat (limited to 'comm/calendar/test/browser/eventDialog')
-rw-r--r--comm/calendar/test/browser/eventDialog/browser.ini27
-rw-r--r--comm/calendar/test/browser/eventDialog/browser_alarmDialog.js88
-rw-r--r--comm/calendar/test/browser/eventDialog/browser_attachMenu.js266
-rw-r--r--comm/calendar/test/browser/eventDialog/browser_attendeesDialog.js462
-rw-r--r--comm/calendar/test/browser/eventDialog/browser_attendeesDialogAdd.js248
-rw-r--r--comm/calendar/test/browser/eventDialog/browser_attendeesDialogNoEdit.js68
-rw-r--r--comm/calendar/test/browser/eventDialog/browser_attendeesDialogRemove.js147
-rw-r--r--comm/calendar/test/browser/eventDialog/browser_attendeesDialogUpdate.js140
-rw-r--r--comm/calendar/test/browser/eventDialog/browser_eventDialog.js399
-rw-r--r--comm/calendar/test/browser/eventDialog/browser_eventDialogDescriptionEditor.js154
-rw-r--r--comm/calendar/test/browser/eventDialog/browser_eventDialogEditButton.js223
-rw-r--r--comm/calendar/test/browser/eventDialog/browser_eventDialogModificationPrompt.js160
-rw-r--r--comm/calendar/test/browser/eventDialog/browser_utf8.js56
-rw-r--r--comm/calendar/test/browser/eventDialog/data/guests.txt2
-rw-r--r--comm/calendar/test/browser/eventDialog/head.js97
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);
+}