/* 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/. */ /** * Common functions for the imip-bar tests. * * Note that these tests are heavily tied to the .eml files found in the data * folder. */ "use strict"; var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); var { CalItipDefaultEmailTransport } = ChromeUtils.import( "resource:///modules/CalItipEmailTransport.jsm" ); var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm"); var { FileTestUtils } = ChromeUtils.importESModule( "resource://testing-common/FileTestUtils.sys.mjs" ); var { CalendarTestUtils } = ChromeUtils.import( "resource://testing-common/calendar/CalendarTestUtils.jsm" ); registerCleanupFunction(async () => { // Some tests that open new windows don't return focus to the main window // in a way that satisfies mochitest, and the test times out. Services.focus.focusedWindow = window; document.body.focus(); }); class EmailTransport extends CalItipDefaultEmailTransport { sentItems = []; sentMsgs = []; getMsgSend() { let { sentMsgs } = this; return { sendMessageFile( userIdentity, accountKey, composeFields, messageFile, deleteSendFileOnCompletion, digest, deliverMode, msgToReplace, listener, statusFeedback, smtpPassword ) { sentMsgs.push({ userIdentity, accountKey, composeFields, messageFile, deleteSendFileOnCompletion, digest, deliverMode, msgToReplace, listener, statusFeedback, smtpPassword, }); }, }; } sendItems(recipients, itipItem, fromAttendee) { this.sentItems.push({ recipients, itipItem, fromAttendee }); return super.sendItems(recipients, itipItem, fromAttendee); } reset() { this.sentItems = []; this.sentMsgs = []; } } async function openMessageFromFile(file) { let fileURL = Services.io .newFileURI(file) .mutate() .setQuery("type=application/x-message-display") .finalize(); let winPromise = BrowserTestUtils.domWindowOpenedAndLoaded(); window.openDialog( "chrome://messenger/content/messageWindow.xhtml", "_blank", "all,chrome,dialog=no,status,toolbar", fileURL ); let win = await winPromise; await BrowserTestUtils.waitForEvent(win, "MsgLoaded"); await TestUtils.waitForCondition(() => Services.focus.activeWindow == win); return win; } /** * Opens an iMIP message file and waits for the imip-bar to appear. * * @param {nsIFile} file * @returns {Window} */ async function openImipMessage(file) { let win = await openMessageFromFile(file); let aboutMessage = win.document.getElementById("messageBrowser").contentWindow; let imipBar = aboutMessage.document.getElementById("imip-bar"); await TestUtils.waitForCondition(() => !imipBar.collapsed, "imip-bar shown"); if (Services.prefs.getBoolPref("calendar.itip.newInvitationDisplay")) { // CalInvitationDisplay.show() does some async activities before the panel is added. await TestUtils.waitForCondition( () => win.document .getElementById("messageBrowser") .contentDocument.querySelector("calendar-invitation-panel"), "calendar-invitation-panel shown" ); } return win; } /** * Clicks on one of the imip-bar action buttons. * * @param {Window} win * @param {string} id */ async function clickAction(win, id) { let aboutMessage = win.document.getElementById("messageBrowser").contentWindow; let action = aboutMessage.document.getElementById(id); await TestUtils.waitForCondition(() => !action.hidden, `button "#${id}" shown`); EventUtils.synthesizeMouseAtCenter(action, {}, aboutMessage); await TestUtils.waitForCondition(() => action.hidden, `button "#${id}" hidden`); } /** * Clicks on one of the imip-bar actions from a dropdown menu. * * @param {Window} win The window the imip message is opened in. * @param {string} buttonId The id of the containing the menu. * @param {string} actionId The id of the menu item to click. */ async function clickMenuAction(win, buttonId, actionId) { let aboutMessage = win.document.getElementById("messageBrowser").contentWindow; let actionButton = aboutMessage.document.getElementById(buttonId); await TestUtils.waitForCondition(() => !actionButton.hidden, `"${buttonId}" shown`); let actionMenu = actionButton.querySelector("menupopup"); let menuShown = BrowserTestUtils.waitForEvent(actionMenu, "popupshown"); EventUtils.synthesizeMouseAtCenter(actionButton.querySelector("dropmarker"), {}, aboutMessage); await menuShown; actionMenu.activateItem(aboutMessage.document.getElementById(actionId)); await TestUtils.waitForCondition(() => actionButton.hidden, `action menu "#${buttonId}" hidden`); } const unpromotedProps = ["location", "description", "sequence", "x-moz-received-dtstamp"]; /** * An object where the keys are paths/selectors and the values are the values * we expect to encounter. * * @typedef {object} Comparable */ /** * Compares the paths specified in the expected object against the provided * actual object. * * @param {object} actual This is expected to be a calIEvent or calIAttendee but * can also be an array of both etc. * @param {Comparable} expected */ function compareProperties(actual, expected, prefix = "") { Assert.equal(typeof actual, "object", `${prefix || "provided value"} is an object`); for (let [key, value] of Object.entries(expected)) { if (key.includes(".")) { let keys = key.split("."); let head = keys[0]; let tail = keys.slice(1).join("."); compareProperties(actual[head], { [tail]: value }, [prefix, head].filter(k => k).join(".")); continue; } let path = [prefix, key].filter(k => k).join("."); let actualValue = unpromotedProps.includes(key) ? actual.getProperty(key) : actual[key]; Assert.equal(actualValue, value, `property "${path}" is "${value}"`); } } /** * Compares the text contents of the selectors specified on the inviatation panel * to the expected value for each. * * @param {ShadowRoot} root The invitation panel's ShadowRoot instance. * @param {Comparable} expected */ function compareShownPanelValues(root, expected) { for (let [key, value] of Object.entries(expected)) { value = Array.isArray(value) ? value.join("") : value; Assert.equal( root.querySelector(key).textContent.trim(), value, `property "${key}" is "${value}"` ); } } /** * Clicks on one of the invitation panel action buttons. * * @param {Window} panel * @param {string} id * @param {boolean} sendResponse */ async function clickPanelAction(panel, id, sendResponse = true) { let promise = BrowserTestUtils.promiseAlertDialogOpen(sendResponse ? "accept" : "cancel"); let button = panel.shadowRoot.getElementById(id); EventUtils.synthesizeMouseAtCenter(button, {}, panel.ownerGlobal); await promise; await BrowserTestUtils.waitForEvent(panel.ownerGlobal, "onItipItemActionFinished"); } /** * Tests that an attempt to reply to the organizer of the event with the correct * details occurred. * * @param {EmailTransport} transport * @param {nsIdentity} identity * @param {string} partStat */ async function doReplyTest(transport, identity, partStat) { info("Verifying the attempt to send a response uses the correct data"); Assert.equal(transport.sentItems.length, 1, "itip subsystem attempted to send a response"); compareProperties(transport.sentItems[0], { "recipients.0.id": "mailto:sender@example.com", "itipItem.responseMethod": "REPLY", "fromAttendee.id": "mailto:receiver@example.com", "fromAttendee.participationStatus": partStat, }); // The itipItem is used to generate the iTIP data in the message body. info("Verifying the reply calItipItem attendee list"); let replyItem = transport.sentItems[0].itipItem.getItemList()[0]; let replyAttendees = replyItem.getAttendees(); Assert.equal(replyAttendees.length, 1, "reply has one attendee"); compareProperties(replyAttendees[0], { id: "mailto:receiver@example.com", participationStatus: partStat, }); info("Verifying the call to the message subsystem"); Assert.equal(transport.sentMsgs.length, 1, "transport sent 1 message"); compareProperties(transport.sentMsgs[0], { userIdentity: identity, "composeFields.from": "receiver@example.com", "composeFields.to": "Sender ", }); Assert.ok(transport.sentMsgs[0].messageFile.exists(), "message file was created"); } /** * @typedef {object} ImipBarActionTestConf * * @property {calICalendar} calendar The calendar used for the test. * @property {calIItipTranport} transport The transport used for the test. * @property {nsIIdentity} identity The identity expected to be used to * send the reply. * @property {boolean} isRecurring Indicates whether to treat the event as a * recurring event or not. * @property {string} partStat The participationStatus of the receiving user to * expect. * @property {boolean} noReply If true, do not expect an attempt to send a reply. * @property {boolean} noSend If true, expect the reply attempt to stop after the * user is prompted. * @property {boolean} isMajor For update tests indicates if the changes expected * are major or minor. */ /** * Test the properties of an event created from the imip-bar and optionally, the * attempt to send a reply. * * @param {ImipBarActionTestConf} conf * @param {calIEvent|calIEvent[]} item */ async function doImipBarActionTest(conf, event) { let { calendar, transport, identity, partStat, isRecurring, noReply, noSend } = conf; let events = [event]; let startDates = ["20220316T110000Z"]; let endDates = ["20220316T113000Z"]; if (isRecurring) { startDates = [...startDates, "20220317T110000Z", "20220318T110000Z"]; endDates = [...endDates, "20220317T113000Z", "20220318T113000Z"]; events = event.parentItem.recurrenceInfo.getOccurrences( cal.createDateTime("19700101"), cal.createDateTime("30000101"), Infinity ); Assert.equal(events.length, 3, "reccurring event has 3 occurrences"); } info("Verifying relevant properties of each event occurrence"); for (let [index, occurrence] of events.entries()) { compareProperties(occurrence, { id: "02e79b96", title: isRecurring ? "Repeat Event" : "Single Event", "calendar.name": calendar.name, ...(isRecurring ? { "recurrenceId.icalString": startDates[index] } : {}), "startDate.icalString": startDates[index], "endDate.icalString": endDates[index], description: "An event invitation.", location: "Somewhere", sequence: "0", "x-moz-received-dtstamp": "20220316T191602Z", "organizer.id": "mailto:sender@example.com", status: "CONFIRMED", }); // Alarms should be ignored. Assert.equal( occurrence.getAlarms().length, 0, `${isRecurring ? "occurrence" : "event"} has no reminders` ); info("Verifying attendee list and participation status"); let attendees = occurrence.getAttendees(); compareProperties(attendees, { "0.id": "mailto:sender@example.com", "0.participationStatus": "ACCEPTED", "1.participationStatus": partStat, "1.id": "mailto:receiver@example.com", "2.id": "mailto:other@example.com", "2.participationStatus": "NEEDS-ACTION", }); } if (noReply) { Assert.equal( transport.sentItems.length, 0, "itip subsystem did not attempt to send a response" ); } if (noReply || noSend) { Assert.equal(transport.sentMsgs.length, 0, "no call was made into the mail subsystem"); return; } await doReplyTest(transport, identity, partStat); } /** * Tests the recognition and application of a minor update to an existing event. * An update is considered minor if the SEQUENCE property has not changed but * the DTSTAMP has. * * @param {ImipBarActionTestConf} conf */ async function doMinorUpdateTest(conf) { let { transport, calendar, partStat, isRecurring } = conf; let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item.parentItem; let prevEventIcs = event.icalString; transport.reset(); let updatePath = isRecurring ? "data/repeat-update-minor.eml" : "data/update-minor.eml"; let win = await openImipMessage(new FileUtils.File(getTestFilePath(updatePath))); let aboutMessage = win.document.getElementById("messageBrowser").contentWindow; let updateButton = aboutMessage.document.getElementById("imipUpdateButton"); Assert.ok(!updateButton.hidden, `#${updateButton.id} button shown`); EventUtils.synthesizeMouseAtCenter(updateButton, {}, aboutMessage); await TestUtils.waitForCondition(async () => { event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item.parentItem; return event.icalString != prevEventIcs; }, "event updated"); await BrowserTestUtils.closeWindow(win); let events = [event]; let startDates = ["20220316T110000Z"]; let endDates = ["20220316T113000Z"]; if (isRecurring) { startDates = [...startDates, "20220317T110000Z", "20220318T110000Z"]; endDates = [...endDates, "20220317T113000Z", "20220318T113000Z"]; events = event.recurrenceInfo.getOccurrences( cal.createDateTime("19700101"), cal.createDateTime("30000101"), Infinity ); Assert.equal(events.length, 3, "reccurring event has 3 occurrences"); } info("Verifying relevant properties of each event occurrence"); for (let [index, occurrence] of events.entries()) { compareProperties(occurrence, { id: "02e79b96", title: "Updated Event", "calendar.name": calendar.name, ...(isRecurring ? { "recurrenceId.icalString": startDates[index] } : {}), "startDate.icalString": startDates[index], "endDate.icalString": endDates[index], description: "Updated description.", location: "Updated location", sequence: "0", "x-moz-received-dtstamp": "20220318T191602Z", "organizer.id": "mailto:sender@example.com", status: "CONFIRMED", }); // Note: It seems we do not keep the order of the attendees list for updates. let attendees = occurrence.getAttendees(); compareProperties(attendees, { "0.id": "mailto:sender@example.com", "0.participationStatus": "ACCEPTED", "1.id": "mailto:other@example.com", "1.participationStatus": "NEEDS-ACTION", "2.participationStatus": partStat, "2.id": "mailto:receiver@example.com", }); } Assert.equal(transport.sentItems.length, 0, "itip subsystem did not attempt to send a response"); Assert.equal(transport.sentMsgs.length, 0, "no call was made into the mail subsystem"); await calendar.deleteItem(event); } const actionIds = { single: { button: { ACCEPTED: "imipAcceptButton", TENTATIVE: "imipTentativeButton", DECLINED: "imipDeclineButton", }, noReply: { ACCEPTED: "imipAcceptButton_AcceptDontSend", TENTATIVE: "imipTentativeButton_TentativeDontSend", DECLINED: "imipDeclineButton_DeclineDontSend", }, }, recurring: { button: { ACCEPTED: "imipAcceptRecurrencesButton", TENTATIVE: "imipTentativeRecurrencesButton", DECLINED: "imipDeclineRecurrencesButton", }, noReply: { ACCEPTED: "imipAcceptRecurrencesButton_AcceptDontSend", TENTATIVE: "imipTentativeRecurrencesButton_TentativeDontSend", DECLINED: "imipDeclineRecurrencesButton_DeclineDontSend", }, }, }; /** * Tests the recognition and application of a major update to an existing event. * An update is considered major if the SEQUENCE property has changed. For major * updates, the imip-bar prompts the user to re-confirm their attendance. * * @param {ImipBarActionTestConf} conf */ async function doMajorUpdateTest(conf) { let { transport, identity, calendar, partStat, isRecurring, noReply } = conf; let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item.parentItem; let prevEventIcs = event.icalString; transport.reset(); let updatePath = isRecurring ? "data/repeat-update-major.eml" : "data/update-major.eml"; let win = await openImipMessage(new FileUtils.File(getTestFilePath(updatePath))); let actions = isRecurring ? actionIds.recurring : actionIds.single; if (noReply) { let { button, noReply } = actions; await clickMenuAction(win, button[partStat], noReply[partStat]); } else { await clickAction(win, actions.button[partStat]); } await TestUtils.waitForCondition(async () => { event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item.parentItem; return event.icalString != prevEventIcs; }, "event updated"); await BrowserTestUtils.closeWindow(win); if (noReply) { Assert.equal( transport.sentItems.length, 0, "itip subsystem did not attempt to send a response" ); Assert.equal(transport.sentMsgs.length, 0, "no call was made into the mail subsystem"); } else { await doReplyTest(transport, identity, partStat); } let events = [event]; let startDates = ["20220316T050000Z"]; let endDates = ["20220316T053000Z"]; if (isRecurring) { startDates = [...startDates, "20220317T050000Z", "20220318T050000Z"]; endDates = [...endDates, "20220317T053000Z", "20220318T053000Z"]; events = event.recurrenceInfo.getOccurrences( cal.createDateTime("19700101"), cal.createDateTime("30000101"), Infinity ); Assert.equal(events.length, 3, "reccurring event has 3 occurrences"); } for (let [index, occurrence] of events.entries()) { compareProperties(occurrence, { id: "02e79b96", title: isRecurring ? "Repeat Event" : "Single Event", "calendar.name": calendar.name, ...(isRecurring ? { "recurrenceId.icalString": startDates[index] } : {}), "startDate.icalString": startDates[index], "endDate.icalString": endDates[index], description: "An event invitation.", location: "Somewhere", sequence: "2", "x-moz-received-dtstamp": "20220316T191602Z", "organizer.id": "mailto:sender@example.com", status: "CONFIRMED", }); let attendees = occurrence.getAttendees(); compareProperties(attendees, { "0.id": "mailto:sender@example.com", "0.participationStatus": "ACCEPTED", "1.id": "mailto:other@example.com", "1.participationStatus": "NEEDS-ACTION", "2.participationStatus": partStat, "2.id": "mailto:receiver@example.com", }); } await calendar.deleteItem(event); } /** * Tests the recognition and application of a minor update exception to an * existing recurring event. * * @param {ImipBarActionTestConf} conf */ async function doMinorExceptionTest(conf) { let { transport, calendar, partStat } = conf; let recurrenceId = cal.createDateTime("20220317T110000Z"); let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item.parentItem; let originalProps = { id: "02e79b96", "recurrenceId.icalString": "20220317T110000Z", title: event.title, "calendar.name": calendar.name, "startDate.icalString": event.startDate.icalString, "endDate.icalString": event.endDate.icalString, description: event.getProperty("DESCRIPTION"), location: event.getProperty("LOCATION"), sequence: "0", "x-moz-received-dtstamp": event.getProperty("x-moz-received-dtstamp"), "organizer.id": "mailto:sender@example.com", status: "CONFIRMED", }; Assert.ok( !event.recurrenceInfo.getExceptionFor(recurrenceId), `no exception exists for ${recurrenceId}` ); transport.reset(); let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/exception-minor.eml"))); let aboutMessage = win.document.getElementById("messageBrowser").contentWindow; let updateButton = aboutMessage.document.getElementById("imipUpdateButton"); Assert.ok(!updateButton.hidden, `#${updateButton.id} button shown`); EventUtils.synthesizeMouseAtCenter(updateButton, {}, aboutMessage); let exception; await TestUtils.waitForCondition(async () => { event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item.parentItem; exception = event.recurrenceInfo.getExceptionFor(recurrenceId); return exception; }, "event exception applied"); await BrowserTestUtils.closeWindow(win); Assert.equal(transport.sentItems.length, 0, "itip subsystem did not attempt to send a response"); Assert.equal(transport.sentMsgs.length, 0, "no call was made into the mail subsystem"); info("Verifying relevant properties of the exception"); compareProperties(exception, { id: "02e79b96", "recurrenceId.icalString": "20220317T110000Z", title: "Exception title", "calendar.name": calendar.name, "startDate.icalString": "20220317T110000Z", "endDate.icalString": "20220317T113000Z", description: "Exception description", location: "Exception location", sequence: "0", "x-moz-received-dtstamp": "20220318T191602Z", "organizer.id": "mailto:sender@example.com", status: "CONFIRMED", }); compareProperties(exception.getAttendees(), { "0.id": "mailto:sender@example.com", "0.participationStatus": "ACCEPTED", "1.id": "mailto:other@example.com", "1.participationStatus": "NEEDS-ACTION", "2.id": "mailto:receiver@example.com", "2.participationStatus": partStat, }); let occurrences = event.recurrenceInfo.getOccurrences( cal.createDateTime("19700101"), cal.createDateTime("30000101"), Infinity ); Assert.equal(occurrences.length, 3, "reccurring event still has 3 occurrences"); info("Verifying relevant properties of the other occurrences"); let startDates = ["20220316T110000Z", "20220317T110000Z", "20220318T110000Z"]; let endDates = ["20220316T113000Z", "20220317T113000Z", "20220318T113000Z"]; for (let [index, occurrence] of occurrences.entries()) { if (occurrence.startDate.compare(recurrenceId) == 0) { continue; } compareProperties(occurrence, { ...originalProps, "recurrenceId.icalString": startDates[index], "startDate.icalString": startDates[index], "endDate.icalString": endDates[index], }); let attendees = occurrence.getAttendees(); compareProperties(attendees, { "0.id": "mailto:sender@example.com", "0.participationStatus": "ACCEPTED", "1.id": "mailto:receiver@example.com", "1.participationStatus": partStat, "2.id": "mailto:other@example.com", "2.participationStatus": "NEEDS-ACTION", }); } await calendar.deleteItem(event); } /** * Tests the recognition and application of a major update exception to an * existing recurring event. * * @param {ImipBarActionTestConf} conf */ async function doMajorExceptionTest(conf) { let { transport, identity, calendar, partStat, noReply } = conf; let recurrenceId = cal.createDateTime("20220317T110000Z"); let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item.parentItem; let originalProps = { id: "02e79b96", "recurrenceId.icalString": "20220317T110000Z", title: event.title, "calendar.name": calendar.name, "startDate.icalString": event.startDate.icalString, "endDate.icalString": event.endDate.icalString, description: event.getProperty("DESCRIPTION"), location: event.getProperty("LOCATION"), sequence: "0", "x-moz-received-dtstamp": event.getProperty("x-moz-received-dtstamp"), "organizer.id": "mailto:sender@example.com", status: "CONFIRMED", }; let originalPartStat = event .getAttendees() .find(att => att.id == "mailto:receiver@example.com").participationStatus; Assert.ok( !event.recurrenceInfo.getExceptionFor(recurrenceId), `no exception exists for ${recurrenceId}` ); transport.reset(); let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/exception-major.eml"))); if (noReply) { let { button, noReply } = actionIds.single; await clickMenuAction(win, button[partStat], noReply[partStat]); } else { await clickAction(win, actionIds.single.button[partStat]); } let exception; await TestUtils.waitForCondition(async () => { event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item.parentItem; exception = event.recurrenceInfo.getExceptionFor(recurrenceId); return exception; }, "event exception applied"); await BrowserTestUtils.closeWindow(win); if (noReply) { Assert.equal( transport.sentItems.length, 0, "itip subsystem did not attempt to send a response" ); Assert.equal(transport.sentMsgs.length, 0, "no call was made into the mail subsystem"); } else { await doReplyTest(transport, identity, partStat); } info("Verifying relevant properties of the exception"); compareProperties(exception, { ...originalProps, "startDate.icalString": "20220317T050000Z", "endDate.icalString": "20220317T053000Z", sequence: "2", }); compareProperties(exception.getAttendees(), { "0.id": "mailto:sender@example.com", "0.participationStatus": "ACCEPTED", "1.id": "mailto:other@example.com", "1.participationStatus": "NEEDS-ACTION", "2.id": "mailto:receiver@example.com", "2.participationStatus": partStat, }); let occurrences = event.recurrenceInfo.getOccurrences( cal.createDateTime("19700101"), cal.createDateTime("30000101"), Infinity ); Assert.equal(occurrences.length, 3, "reccurring event still has 3 occurrences"); info("Verifying relevant properties of the other occurrences"); let startDates = ["20220316T110000Z", "20220317T110000Z", "20220318T110000Z"]; let endDates = ["20220316T113000Z", "20220317T113000Z", "20220318T113000Z"]; for (let [index, occurrence] of occurrences.entries()) { if (occurrence.startDate.icalString == "20220317T050000Z") { continue; } compareProperties(occurrence, { ...originalProps, "recurrenceId.icalString": startDates[index], "startDate.icalString": startDates[index], "endDate.icalString": endDates[index], }); let attendees = occurrence.getAttendees(); compareProperties(attendees, { "0.id": "mailto:sender@example.com", "0.participationStatus": "ACCEPTED", "1.id": "mailto:receiver@example.com", "1.participationStatus": originalPartStat, "2.id": "mailto:other@example.com", "2.participationStatus": "NEEDS-ACTION", }); } await calendar.deleteItem(event); } /** * Test the properties of an event created from a minor or major exception where * we have not added the original event and optionally, the attempt to send a * reply. * * @param {ImipBarActionTestConf} conf */ async function doExceptionOnlyTest(conf) { let { calendar, transport, identity, partStat, noReply, isMajor } = conf; let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 5, 1)).item; // Exceptions are still created as recurring events. Assert.ok(event != event.parentItem, "event created is a recurring event"); let occurrences = event.parentItem.recurrenceInfo.getOccurrences( cal.createDateTime("10000101"), cal.createDateTime("30000101"), Infinity ); Assert.equal(occurrences.length, 1, "parent item only has one occurrence"); Assert.ok(occurrences[0] == event, "occurrence is the event exception"); info("Verifying relevant properties of the event"); compareProperties(event, { id: "02e79b96", title: isMajor ? event.title : "Exception title", "calendar.name": calendar.name, "recurrenceId.icalString": "20220317T110000Z", "startDate.icalString": isMajor ? "20220317T050000Z" : "20220317T110000Z", "endDate.icalString": isMajor ? "20220317T053000Z" : "20220317T113000Z", description: isMajor ? event.getProperty("DESCRIPTION") : "Exception description", location: isMajor ? event.getProperty("LOCATION") : "Exception location", sequence: isMajor ? "2" : "0", "x-moz-received-dtstamp": isMajor ? event.getProperty("x-moz-received-dtstamp") : "20220318T191602Z", "organizer.id": "mailto:sender@example.com", status: "CONFIRMED", }); // Alarms should be ignored. Assert.equal(event.getAlarms().length, 0, "event has no reminders"); info("Verifying attendee list and participation status"); let attendees = event.getAttendees(); compareProperties(attendees, { "0.id": "mailto:sender@example.com", "0.participationStatus": "ACCEPTED", "1.participationStatus": partStat, "1.id": "mailto:receiver@example.com", "2.id": "mailto:other@example.com", "2.participationStatus": "NEEDS-ACTION", }); if (noReply) { Assert.equal( transport.sentItems.length, 0, "itip subsystem did not attempt to send a response" ); Assert.equal(transport.sentMsgs.length, 0, "no call was made into the mail subsystem"); } else { await doReplyTest(transport, identity, partStat); } await calendar.deleteItem(event.parentItem); } /** * Tests the recognition and application of a cancellation to an existing event. * * @param {ImipBarActionTestConf} conf */ async function doCancelTest({ transport, calendar, isRecurring, event, recurrenceId }) { transport.reset(); let eventId = event.id; if (isRecurring) { // wait for the other occurrences to appear. await CalendarTestUtils.monthView.waitForItemAt(window, 3, 5, 1); await CalendarTestUtils.monthView.waitForItemAt(window, 3, 6, 1); } let cancellationPath = isRecurring ? "data/cancel-repeat-event.eml" : "data/cancel-single-event.eml"; let cancelMsgFile = new FileUtils.File(getTestFilePath(cancellationPath)); if (recurrenceId) { let srcTxt = await IOUtils.readUTF8(cancelMsgFile.path); srcTxt = srcTxt.replaceAll(/RRULE:.+/g, `RECURRENCE-ID:${recurrenceId}`); srcTxt = srcTxt.replaceAll(/SEQUENCE:.+/g, "SEQUENCE:3"); cancelMsgFile = FileTestUtils.getTempFile("cancel-occurrence.eml"); await IOUtils.writeUTF8(cancelMsgFile.path, srcTxt); } let win = await openImipMessage(cancelMsgFile); let aboutMessage = win.document.getElementById("messageBrowser").contentWindow; let deleteButton = aboutMessage.document.getElementById("imipDeleteButton"); Assert.ok(!deleteButton.hidden, `#${deleteButton.id} button shown`); EventUtils.synthesizeMouseAtCenter(deleteButton, {}, aboutMessage); if (isRecurring && recurrenceId) { // Expects a single occurrence to be cancelled. let occurrences; await TestUtils.waitForCondition(async () => { let { parentItem } = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item; occurrences = parentItem.recurrenceInfo.getOccurrences( cal.createDateTime("19700101"), cal.createDateTime("30000101"), Infinity ); return occurrences.length == 2; }, "occurrence was deleted"); Assert.ok( occurrences.every(occ => occ.recurrenceId && occ.recurrenceId != recurrenceId), `occurrence "${recurrenceId}" removed` ); Assert.ok(!!(await calendar.getItem(eventId)), "event was not deleted"); } else { await CalendarTestUtils.monthView.waitForNoItemAt(window, 3, 4, 1); if (isRecurring) { await CalendarTestUtils.monthView.waitForNoItemAt(window, 3, 5, 1); await CalendarTestUtils.monthView.waitForNoItemAt(window, 3, 6, 1); } await TestUtils.waitForCondition(async () => { let result = await calendar.getItem(eventId); return !result; }, "event was deleted"); } await BrowserTestUtils.closeWindow(win); Assert.equal(transport.sentItems.length, 0, "itip subsystem did not attempt to send a response"); Assert.equal(transport.sentMsgs.length, 0, "no call was made into the mail subsystem"); } /** * Tests processing of cancellations to exceptions to recurring events. * * @param {ImipBarActionTestConf} conf */ async function doCancelExceptionTest(conf) { let { partStat, recurrenceId, calendar } = conf; let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml")); let win = await openImipMessage(invite); await clickAction(win, actionIds.recurring.button[partStat]); let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item.parentItem; await BrowserTestUtils.closeWindow(win); let update = new FileUtils.File(getTestFilePath("data/exception-major.eml")); let updateWin = await openImipMessage(update); await clickAction(updateWin, actionIds.single.button[partStat]); let exception; await TestUtils.waitForCondition(async () => { event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item.parentItem; exception = event.recurrenceInfo.getExceptionFor(cal.createDateTime(recurrenceId)); return !!exception; }, "exception applied"); await BrowserTestUtils.closeWindow(updateWin); await doCancelTest({ ...conf, event }); await calendar.deleteItem(event); }