diff options
Diffstat (limited to 'comm/calendar/test/browser')
111 files changed, 17628 insertions, 0 deletions
diff --git a/comm/calendar/test/browser/browser.ini b/comm/calendar/test/browser/browser.ini new file mode 100644 index 0000000000..5eb780310b --- /dev/null +++ b/comm/calendar/test/browser/browser.ini @@ -0,0 +1,39 @@ +[default] +head = head.js +prefs = + calendar.debug.log=true + 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.oauth.loglevel=Debug + mailnews.start_page.override_url=about:blank + mailnews.start_page.url=about:blank + signon.rememberSignons=true +subsuite = thunderbird +support-files = data/** + +[browser_basicFunctionality.js] +[browser_calDAV_discovery.js] +[browser_calDAV_oAuth.js] +tags = oauth +[browser_calendarList.js] +[browser_calendarTelemetry.js] +[browser_calendarUnifinder.js] +[browser_dragEventItem.js] +[browser_eventDisplay_dayView.js] +[browser_eventDisplay_multiWeekView.js] +[browser_eventDisplay_weekView.js] +[browser_eventUndoRedo.js] +[browser_import.js] +[browser_localICS.js] +[browser_taskDelete.js] +[browser_taskUndoRedo.js] +[browser_tabs.js] +[browser_taskDisplay.js] +[browser_todayPane.js] +[browser_todayPane_dragAndDrop.js] +[browser_todayPane_visibility.js] diff --git a/comm/calendar/test/browser/browser_basicFunctionality.js b/comm/calendar/test/browser/browser_basicFunctionality.js new file mode 100644 index 0000000000..c8f0fd68b7 --- /dev/null +++ b/comm/calendar/test/browser/browser_basicFunctionality.js @@ -0,0 +1,78 @@ +/* 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 createCalendarUsingDialog */ + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); + +add_task(async function testBasicFunctionality() { + const calendarName = "Mochitest"; + + registerCleanupFunction(() => { + for (let calendar of cal.manager.getCalendars()) { + if (calendar.name == calendarName) { + cal.manager.removeCalendar(calendar); + } + } + Services.focus.focusedWindow = window; + }); + + Services.focus.focusedWindow = window; + + // Create test calendar. + await createCalendarUsingDialog(calendarName); + + // Check for minimonth, every month has a day 1. + Assert.ok( + document.querySelector("#calMinimonth .minimonth-cal-box td[aria-label='1']"), + "day 1 exists in the minimonth" + ); + + // Check for calendar list. + Assert.ok(document.querySelector("#calendar-list-pane"), "calendar list pane exists"); + Assert.ok(document.querySelector("#calendar-list"), "calendar list exists"); + + // Check for event search. + Assert.ok(document.querySelector("#bottom-events-box"), "event search box exists"); + + // There should be search field. + Assert.ok(document.querySelector("#unifinder-search-field"), "unifinded search field exists"); + + // Make sure the week view is the default selected view. + Assert.ok( + document + .querySelector(`.calview-toggle-item[aria-selected="true"]`) + .getAttribute("aria-controls") == "week-view", + "week-view toggle is the current default" + ); + + let dayViewButton = document.querySelector("#calTabDay"); + dayViewButton.click(); + Assert.ok(dayViewButton.getAttribute("aria-selected"), "day view button is selected"); + await CalendarTestUtils.ensureViewLoaded(window); + + // Day view should have 09:00 box. + let someTime = cal.createDateTime(); + someTime.resetTo(someTime.year, someTime.month, someTime.day, 9, 0, 0, someTime.timezone); + let label = cal.dtz.formatter.formatTime(someTime); + let labelEl = document.querySelectorAll("#day-view .multiday-timebar .multiday-hour-box")[9]; + Assert.ok(labelEl, "9th hour box should exist"); + Assert.equal(labelEl.textContent, label, "9th hour box should show the correct time"); + Assert.ok(CalendarTestUtils.dayView.getHourBoxAt(window, 9), "09:00 box exists"); + + // Open tasks view. + document.querySelector("#tasksButton").click(); + + // Should be possible to filter today's tasks. + Assert.ok(document.querySelector("#opt_today_filter"), "show today radio button exists"); + + // Check for task add button. + Assert.ok(document.querySelector("#calendar-add-task-button"), "task add button exists"); + + // Check for filtered tasks list. + Assert.ok( + document.querySelector("#calendar-task-tree .calendar-task-treechildren"), + "filtered tasks list exists" + ); +}); diff --git a/comm/calendar/test/browser/browser_calDAV_discovery.js b/comm/calendar/test/browser/browser_calDAV_discovery.js new file mode 100644 index 0000000000..217bf76f55 --- /dev/null +++ b/comm/calendar/test/browser/browser_calDAV_discovery.js @@ -0,0 +1,241 @@ +/* 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 { CalDAVServer } = ChromeUtils.import("resource://testing-common/calendar/CalDAVServer.jsm"); +var { DNS } = ChromeUtils.import("resource:///modules/DNS.jsm"); + +async function openWizard(...args) { + await CalendarTestUtils.openCalendarTab(window); + let wizardPromise = BrowserTestUtils.promiseAlertDialog( + undefined, + "chrome://calendar/content/calendar-creation.xhtml", + { + callback: wizardWindow => handleWizard(wizardWindow, ...args), + } + ); + EventUtils.synthesizeMouseAtCenter( + document.querySelector("#newCalendarSidebarButton"), + {}, + window + ); + return wizardPromise; +} + +async function handleWizard(wizardWindow, { username, url, password, expectedCalendars }) { + let wizardDocument = wizardWindow.document; + let acceptButton = wizardDocument.querySelector("dialog").getButton("accept"); + let cancelButton = wizardDocument.querySelector("dialog").getButton("cancel"); + + // Select calendar type. + + EventUtils.synthesizeMouseAtCenter( + wizardDocument.querySelector(`radio[value="network"]`), + {}, + wizardWindow + ); + EventUtils.synthesizeMouseAtCenter(acceptButton, {}, wizardWindow); + + // Network calendar settings. + + Assert.ok(acceptButton.disabled); + Assert.equal(wizardDocument.activeElement.id, "network-username-input"); + if (username) { + EventUtils.sendString(username, wizardWindow); + } + + if (username?.includes("@")) { + Assert.equal( + wizardDocument.getElementById("network-location-input").placeholder, + username.replace(/^.*@/, "") + ); + } + + EventUtils.synthesizeKey("VK_TAB", {}, wizardWindow); + Assert.equal(wizardDocument.activeElement.id, "network-location-input"); + if (url) { + EventUtils.sendString(url, wizardWindow); + } + + Assert.ok(!acceptButton.disabled); + + let promptPromise = handlePasswordPrompt(password); + EventUtils.synthesizeKey("VK_RETURN", {}, wizardWindow); + await promptPromise; + + // Select calendars. + + let list = wizardDocument.getElementById("network-calendar-list"); + await TestUtils.waitForCondition( + () => BrowserTestUtils.is_visible(list), + "waiting for calendar list to appear", + 200, + 100 + ); + + Assert.equal(list.childElementCount, expectedCalendars.length); + for (let i = 0; i < expectedCalendars.length; i++) { + let item = list.children[i]; + + Assert.equal(item.calendar.uri.spec, expectedCalendars[i].uri); + Assert.equal( + item.querySelector(".calendar-color").style.backgroundColor, + expectedCalendars[i].color + ); + Assert.equal(item.querySelector(".calendar-name").value, expectedCalendars[i].name); + + if (expectedCalendars[i].hasOwnProperty("readOnly")) { + Assert.equal( + item.calendar.readOnly, + expectedCalendars[i].readOnly, + `calendar read-only property is ${expectedCalendars[i].readOnly}` + ); + } + } + EventUtils.synthesizeMouseAtCenter(cancelButton, {}, wizardWindow); +} + +async function handlePasswordPrompt(password) { + return BrowserTestUtils.promiseAlertDialog(null, undefined, { + async callback(prompt) { + await new Promise(resolve => prompt.setTimeout(resolve)); + + prompt.document.getElementById("password1Textbox").value = password; + + let checkbox = prompt.document.getElementById("checkbox"); + Assert.greater(checkbox.getBoundingClientRect().width, 0); + Assert.ok(checkbox.checked); + + prompt.document.querySelector("dialog").getButton("accept").click(); + }, + }); +} + +/** + * Test that we correctly use DNS discovery. This uses the mochitest server + * (files in the data directory) instead of CalDAVServer because the latter + * can't speak HTTPS, and we only do DNS discovery for HTTPS. + */ +add_task(async function testDNS() { + var _srv = DNS.srv; + var _txt = DNS.txt; + DNS.srv = function (name) { + Assert.equal(name, "_caldavs._tcp.dnstest.invalid"); + return [{ prio: 0, weight: 0, host: "example.org", port: 443 }]; + }; + DNS.txt = function (name) { + Assert.equal(name, "_caldavs._tcp.dnstest.invalid"); + return [{ data: "path=/browser/comm/calendar/test/browser/data/dns.sjs" }]; + }; + + await openWizard({ + username: "carol@dnstest.invalid", + password: "carol", + expectedCalendars: [ + { + uri: "https://example.org/browser/comm/calendar/test/browser/data/calendar.sjs", + name: "You found me!", + color: "rgb(0, 128, 0)", + }, + { + uri: "https://example.org/browser/comm/calendar/test/browser/data/calendar2.sjs", + name: "Röda dagar", + color: "rgb(255, 0, 0)", + }, + ], + }); + + DNS.srv = _srv; + DNS.txt = _txt; +}); + +/** + * Test that the magic URL /.well-known/caldav works. + */ +add_task(async function testWellKnown() { + CalDAVServer.open("alice", "alice"); + + await openWizard({ + username: "alice", + url: CalDAVServer.origin, + password: "alice", + expectedCalendars: [ + { + uri: CalDAVServer.url, + name: "CalDAV Test", + color: "rgb(255, 128, 0)", + }, + ], + }); + + CalDAVServer.close(); +}); + +/** + * Tests calendars with only the "read" "current-user-privilege-set" are + * flagged read-only. + */ +add_task(async function testCalendarWithOnlyReadPriv() { + CalDAVServer.open("alice", "alice"); + CalDAVServer.privileges = "<d:privilege><d:read/></d:privilege>"; + await openWizard({ + username: "alice", + url: CalDAVServer.origin, + password: "alice", + expectedCalendars: [ + { + uri: CalDAVServer.url, + name: "CalDAV Test", + color: "rgb(255, 128, 0)", + readOnly: true, + }, + ], + }); + CalDAVServer.close(); +}); + +/** + * Tests calendars that return none of the expected values for "current-user-privilege-set" + * are flagged read-only. + */ +add_task(async function testCalendarWithoutPrivs() { + CalDAVServer.open("alice", "alice"); + CalDAVServer.privileges = ""; + await openWizard({ + username: "alice", + url: CalDAVServer.origin, + password: "alice", + expectedCalendars: [ + { + uri: CalDAVServer.url, + name: "CalDAV Test", + color: "rgb(255, 128, 0)", + readOnly: true, + }, + ], + }); + CalDAVServer.close(); +}); + +/** + * Tests calendars that return status 404 for "current-user-privilege-set" are + * not flagged read-only. + */ +add_task(async function testCalendarWithNoPrivSupport() { + CalDAVServer.open("alice", "alice"); + CalDAVServer.privileges = null; + await openWizard({ + username: "alice", + url: CalDAVServer.origin, + password: "alice", + expectedCalendars: [ + { + uri: CalDAVServer.url, + name: "CalDAV Test", + color: "rgb(255, 128, 0)", + readOnly: false, + }, + ], + }); + CalDAVServer.close(); +}); diff --git a/comm/calendar/test/browser/browser_calDAV_oAuth.js b/comm/calendar/test/browser/browser_calDAV_oAuth.js new file mode 100644 index 0000000000..4d9c733076 --- /dev/null +++ b/comm/calendar/test/browser/browser_calDAV_oAuth.js @@ -0,0 +1,201 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Creates calendars in various configurations (current and legacy) and performs +// requests in each of them to prove that OAuth2 authentication is working as expected. + +var { CalDavCalendar } = ChromeUtils.import("resource:///modules/CalDavCalendar.jsm"); +var { CalDavGenericRequest } = ChromeUtils.import("resource:///modules/caldav/CalDavRequest.jsm"); + +var LoginInfo = Components.Constructor( + "@mozilla.org/login-manager/loginInfo;1", + Ci.nsILoginInfo, + "init" +); + +// Ideal login info. This is what would be saved if you created a new calendar. +const ORIGIN = "oauth://mochi.test"; +const SCOPE = "test_scope"; +const USERNAME = "bob@test.invalid"; +const VALID_TOKEN = "bobs_refresh_token"; + +/** + * Set a string pref for the given calendar. + * + * @param {string} calendarId + * @param {string} key + * @param {string} value + */ +function setPref(calendarId, key, value) { + Services.prefs.setStringPref(`calendar.registry.${calendarId}.${key}`, value); +} + +/** + * Clear any existing saved logins and add the given ones. + * + * @param {string[][]} - Zero or more arrays consisting of origin, realm, username, and password. + */ +function setLogins(...logins) { + Services.logins.removeAllLogins(); + for (let [origin, realm, username, password] of logins) { + Services.logins.addLogin(new LoginInfo(origin, null, realm, username, password, "", "")); + } +} + +/** + * Create a calendar with the given id, perform a request, and check that the correct + * authorisation header was used. If the user is required to re-authenticate with the provider, + * check that the new token is stored in the right place. + * + * @param {string} calendarId - ID of the new calendar + * @param {string} [newTokenUsername] - If given, re-authentication must happen and the new token + * stored with this user name. + */ +async function subtest(calendarId, newTokenUsername) { + let calendar = new CalDavCalendar(); + calendar.id = calendarId; + + let request = new CalDavGenericRequest( + calendar.wrappedJSObject.session, + calendar, + "GET", + Services.io.newURI( + "http://mochi.test:8888/browser/comm/mail/components/addrbook/test/browser/data/auth_headers.sjs" + ) + ); + let response = await request.commit(); + let headers = JSON.parse(response.text); + + if (newTokenUsername) { + Assert.equal(headers.authorization, "Bearer new_access_token"); + + let logins = Services.logins + .findLogins(ORIGIN, null, SCOPE) + .filter(l => l.username == newTokenUsername); + Assert.equal(logins.length, 1); + Assert.equal(logins[0].username, newTokenUsername); + Assert.equal(logins[0].password, "new_refresh_token"); + } else { + Assert.equal(headers.authorization, "Bearer bobs_access_token"); + } + + Services.logins.removeAllLogins(); +} + +// Test making a request when there is no matching token stored. + +/** No token stored, no username or session ID set. */ +add_task(function testCalendarOAuth_id_none() { + let calendarId = "testCalendarOAuth_id_none"; + return subtest(calendarId, calendarId); +}); + +/** No token stored, session ID set. */ +add_task(function testCalendarOAuth_sessionId_none() { + let calendarId = "testCalendarOAuth_sessionId_none"; + setPref(calendarId, "sessionId", "test_session"); + return subtest(calendarId, "test_session"); +}); + +/** No token stored, username set. */ +add_task(function testCalendarOAuth_username_none() { + let calendarId = "testCalendarOAuth_username_none"; + setPref(calendarId, "username", USERNAME); + return subtest(calendarId, USERNAME); +}); + +// Test making a request when there IS a matching token, but the server rejects it. +// Currently a new token is not requested on failure. + +/** Expired token stored with calendar ID. */ +add_task(function testCalendarOAuth_id_expired() { + let calendarId = "testCalendarOAuth_id_expired"; + setLogins([`oauth:${calendarId}`, "Google CalDAV v2", calendarId, "expired_token"]); + return subtest(calendarId, calendarId); +}).skip(); // Broken. + +/** Expired token stored with session ID. */ +add_task(function testCalendarOAuth_sessionId_expired() { + let calendarId = "testCalendarOAuth_sessionId_expired"; + setPref(calendarId, "sessionId", "test_session"); + setLogins(["oauth:test_session", "Google CalDAV v2", "test_session", "expired_token"]); + return subtest(calendarId, "test_session"); +}).skip(); // Broken. + +/** Expired token stored with calendar ID, username set. */ +add_task(function testCalendarOAuth_username_expired() { + let calendarId = "testCalendarOAuth_username_expired"; + setPref(calendarId, "username", USERNAME); + setLogins([`oauth:${calendarId}`, "Google CalDAV v2", calendarId, "expired_token"]); + return subtest(calendarId, USERNAME); +}).skip(); // Broken. + +// Test making a request with a valid token, using Lightning's client ID and secret. + +/** Valid token stored with calendar ID. */ +add_task(function testCalendarOAuth_id_valid() { + let calendarId = "testCalendarOAuth_id_valid"; + setLogins([`oauth:${calendarId}`, "Google CalDAV v2", calendarId, VALID_TOKEN]); + return subtest(calendarId); +}); + +/** Valid token stored with session ID. */ +add_task(function testCalendarOAuth_sessionId_valid() { + let calendarId = "testCalendarOAuth_sessionId_valid"; + setPref(calendarId, "sessionId", "test_session"); + setLogins(["oauth:test_session", "Google CalDAV v2", "test_session", VALID_TOKEN]); + return subtest(calendarId); +}); + +/** Valid token stored with calendar ID, username set. */ +add_task(function testCalendarOAuth_username_valid() { + let calendarId = "testCalendarOAuth_username_valid"; + setPref(calendarId, "username", USERNAME); + setLogins([`oauth:${calendarId}`, "Google CalDAV v2", calendarId, VALID_TOKEN]); + return subtest(calendarId, USERNAME); +}); + +// Test making a request with a valid token, using Thunderbird's client ID and secret. + +/** Valid token stored with calendar ID. */ +add_task(function testCalendarOAuthTB_id_valid() { + let calendarId = "testCalendarOAuthTB_id_valid"; + setLogins([ORIGIN, SCOPE, calendarId, VALID_TOKEN]); + return subtest(calendarId); +}); + +/** Valid token stored with session ID. */ +add_task(function testCalendarOAuthTB_sessionId_valid() { + let calendarId = "testCalendarOAuthTB_sessionId_valid"; + setPref(calendarId, "sessionId", "test_session"); + setLogins([ORIGIN, SCOPE, "test_session", VALID_TOKEN]); + return subtest(calendarId); +}); + +/** Valid token stored with calendar ID, username set. */ +add_task(function testCalendarOAuthTB_username_valid() { + let calendarId = "testCalendarOAuthTB_username_valid"; + setPref(calendarId, "username", USERNAME); + setLogins([ORIGIN, SCOPE, calendarId, VALID_TOKEN]); + return subtest(calendarId, USERNAME); +}); + +/** Valid token stored with username, exact scope. */ +add_task(function testCalendarOAuthTB_username_validSingle() { + let calendarId = "testCalendarOAuthTB_username_validSingle"; + setPref(calendarId, "username", USERNAME); + setLogins( + [ORIGIN, SCOPE, USERNAME, VALID_TOKEN], + [ORIGIN, "other_scope", USERNAME, "other_refresh_token"] + ); + return subtest(calendarId); +}); + +/** Valid token stored with username, many scopes. */ +add_task(function testCalendarOAuthTB_username_validMultiple() { + let calendarId = "testCalendarOAuthTB_username_validMultiple"; + setPref(calendarId, "username", USERNAME); + setLogins([ORIGIN, "scope test_scope other_scope", USERNAME, VALID_TOKEN]); + return subtest(calendarId); +}); diff --git a/comm/calendar/test/browser/browser_calendarList.js b/comm/calendar/test/browser/browser_calendarList.js new file mode 100644 index 0000000000..b85ef5a56e --- /dev/null +++ b/comm/calendar/test/browser/browser_calendarList.js @@ -0,0 +1,341 @@ +/* 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/. */ + +async function calendarListContextMenu(target, menuItem) { + await new Promise(r => setTimeout(r)); + window.focus(); + await TestUtils.waitForCondition( + () => Services.focus.focusedWindow == window, + "waiting for window to be focused" + ); + + // The test frequently times out if we don't wait here. Unknown why. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, 250)); + + let contextMenu = document.getElementById("list-calendars-context-menu"); + let shownPromise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown"); + EventUtils.synthesizeMouseAtCenter(target, { type: "contextmenu" }); + await shownPromise; + + if (menuItem) { + let hiddenPromise = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden"); + contextMenu.activateItem(document.getElementById(menuItem)); + await hiddenPromise; + } +} + +async function withMockPromptService(response, callback) { + let realPrompt = Services.prompt; + Services.prompt = { + QueryInterface: ChromeUtils.generateQI(["nsIPromptService"]), + confirmEx: (unused1, unused2, text) => { + info(text); + return response; + }, + }; + await callback(); + Services.prompt = realPrompt; +} + +add_task(async () => { + function checkProperties(index, expected) { + let calendarList = document.getElementById("calendar-list"); + let item = calendarList.rows[index]; + let colorImage = item.querySelector(".calendar-color"); + for (let [key, expectedValue] of Object.entries(expected)) { + switch (key) { + case "id": + Assert.equal(item.getAttribute("calendar-id"), expectedValue); + break; + case "disabled": + Assert.equal(item.querySelector(".calendar-displayed").hidden, expectedValue); + break; + case "displayed": + Assert.equal(item.querySelector(".calendar-displayed").checked, expectedValue); + break; + case "color": + if (item.hasAttribute("calendar-disabled")) { + Assert.equal(getComputedStyle(colorImage).backgroundColor, "rgba(0, 0, 0, 0)"); + } else { + Assert.equal(getComputedStyle(colorImage).backgroundColor, expectedValue); + } + break; + case "name": + Assert.equal(item.querySelector(".calendar-name").textContent, expectedValue); + break; + } + } + } + + function checkDisplayed(...expected) { + let calendarList = document.getElementById("calendar-list"); + Assert.greater(calendarList.rowCount, Math.max(...expected)); + for (let i = 0; i < calendarList.rowCount; i++) { + Assert.equal( + calendarList.rows[i].querySelector(".calendar-displayed").checked, + expected.includes(i) + ); + } + } + + function checkSortOrder(...expected) { + let orderPref = Services.prefs.getStringPref("calendar.list.sortOrder", "wrong"); + Assert.notEqual(orderPref, "wrong", "sort order pref has a value"); + let order = orderPref.split(" "); + Assert.equal(order.length, expected.length, "sort order length"); + for (let i = 0; i < expected.length; i++) { + Assert.equal(order[i], calendars[expected[i]].id, "sort order ids"); + } + } + + let calendarList = document.getElementById("calendar-list"); + let contextMenu = document.getElementById("list-calendars-context-menu"); + let composite = cal.view.getCompositeCalendar(window); + + await CalendarTestUtils.openCalendarTab(window); + + // Check the default calendar. + let calendars = cal.manager.getCalendars(); + Assert.equal(calendars.length, 1); + Assert.equal(calendarList.rowCount, 1); + checkProperties(0, { + color: "rgb(168, 194, 225)", + name: "Home", + }); + checkSortOrder(0); + + // Test adding calendars. + + // Open and then cancel the 'create calendar' dialog, just to prove that the + // context menu works. + let dialogPromise = BrowserTestUtils.promiseAlertDialog( + "cancel", + "chrome://calendar/content/calendar-creation.xhtml" + ); + calendarListContextMenu(calendarList, "list-calendars-context-new"); + await dialogPromise; + + // Add some new calendars, check their properties. + for (let i = 1; i <= 3; i++) { + calendars[i] = CalendarTestUtils.createCalendar(`Mochitest ${i}`, "memory"); + } + + Assert.equal(cal.manager.getCalendars().length, 4); + Assert.equal(calendarList.rowCount, 4); + + for (let i = 1; i <= 3; i++) { + checkProperties(i, { + id: calendars[i].id, + displayed: true, + color: "rgb(168, 194, 225)", + name: `Mochitest ${i}`, + }); + } + checkSortOrder(0, 1, 2, 3); + + // Test the context menu. + + await new Promise(resolve => setTimeout(resolve)); + EventUtils.synthesizeMouseAtCenter(calendarList.rows[1], {}); + await new Promise(resolve => setTimeout(resolve)); + await calendarListContextMenu(calendarList.rows[1]); + await new Promise(resolve => setTimeout(resolve)); + Assert.equal( + document.getElementById("list-calendars-context-togglevisible").label, + "Hide Mochitest 1" + ); + Assert.equal( + document.getElementById("list-calendars-context-showonly").label, + "Show Only Mochitest 1" + ); + Assert.ok( + document.getElementById("list-calendar-context-reload").hidden, + "Local calendar should have reload menu showing" + ); + contextMenu.hidePopup(); + + Assert.equal(document.activeElement, calendarList); + Assert.equal(calendarList.rows[calendarList.selectedIndex], calendarList.rows[1]); + + // Test show/hide. + // TODO: Check events on calendars are hidden/shown. + + EventUtils.synthesizeMouseAtCenter(calendarList.rows[2].querySelector(".calendar-displayed"), {}); + Assert.equal(document.activeElement, calendarList); + Assert.equal(calendarList.rows[calendarList.selectedIndex], calendarList.rows[2]); + Assert.equal(composite.getCalendarById(calendars[2].id), null); + checkDisplayed(0, 1, 3); + + composite.removeCalendar(calendars[1]); + checkDisplayed(0, 3); + + await calendarListContextMenu(calendarList.rows[3], "list-calendars-context-togglevisible"); + checkDisplayed(0); + + EventUtils.synthesizeMouseAtCenter(calendarList.rows[2].querySelector(".calendar-displayed"), {}); + Assert.equal(composite.getCalendarById(calendars[2].id), calendars[2]); + Assert.equal(document.activeElement, calendarList); + Assert.equal(calendarList.rows[calendarList.selectedIndex], calendarList.rows[2]); + checkDisplayed(0, 2); + + composite.addCalendar(calendars[1]); + checkDisplayed(0, 1, 2); + + await calendarListContextMenu(calendarList.rows[3], "list-calendars-context-togglevisible"); + checkDisplayed(0, 1, 2, 3); + + await calendarListContextMenu(calendarList.rows[1], "list-calendars-context-showonly"); + checkDisplayed(1); + + await calendarListContextMenu(calendarList, "list-calendars-context-showall"); + checkDisplayed(0, 1, 2, 3); + + // Test editing calendars. + + dialogPromise = BrowserTestUtils.promiseAlertDialog( + null, + "chrome://calendar/content/calendar-properties-dialog.xhtml", + { + callback(win) { + let doc = win.document; + let nameElement = doc.getElementById("calendar-name"); + let colorElement = doc.getElementById("calendar-color"); + Assert.equal(nameElement.value, "Mochitest 1"); + Assert.equal(colorElement.value, "#a8c2e1"); + nameElement.value = "A New Calendar!"; + colorElement.value = "#009900"; + doc.querySelector("dialog").getButton("accept").click(); + }, + } + ); + EventUtils.synthesizeMouseAtCenter(calendarList.rows[1], { clickCount: 2 }); + await dialogPromise; + + Assert.equal(document.activeElement, calendarList); + Assert.equal(calendarList.rows[calendarList.selectedIndex], calendarList.rows[1]); + checkProperties(1, { + color: "rgb(0, 153, 0)", + name: "A New Calendar!", + }); + + dialogPromise = BrowserTestUtils.promiseAlertDialog( + null, + "chrome://calendar/content/calendar-properties-dialog.xhtml", + { + callback(win) { + let doc = win.document; + let nameElement = doc.getElementById("calendar-name"); + let colorElement = doc.getElementById("calendar-color"); + Assert.equal(nameElement.value, "A New Calendar!"); + Assert.equal(colorElement.value, "#009900"); + nameElement.value = "Mochitest 1"; + doc.querySelector("dialog").getButton("accept").click(); + }, + } + ); + calendarListContextMenu(calendarList.rows[1], "list-calendars-context-edit"); + await dialogPromise; + + Assert.equal(document.activeElement, calendarList); + Assert.equal(calendarList.rows[calendarList.selectedIndex], calendarList.rows[1]); + checkProperties(1, { + color: "rgb(0, 153, 0)", + name: "Mochitest 1", + }); + + dialogPromise = BrowserTestUtils.promiseAlertDialog( + null, + "chrome://calendar/content/calendar-properties-dialog.xhtml", + { + callback(win) { + let doc = win.document; + Assert.equal(doc.getElementById("calendar-name").value, "Mochitest 3"); + let enabledElement = doc.getElementById("calendar-enabled-checkbox"); + Assert.ok(enabledElement.checked); + enabledElement.checked = false; + doc.querySelector("dialog").getButton("accept").click(); + }, + } + ); + // We're clicking on an item that wasn't the selected one. Selection should be updated. + calendarListContextMenu(calendarList.rows[3], "list-calendars-context-edit"); + await dialogPromise; + + Assert.equal(document.activeElement, calendarList); + Assert.equal(calendarList.rows[calendarList.selectedIndex], calendarList.rows[3]); + checkProperties(3, { disabled: true }); + + calendars[3].setProperty("disabled", false); + checkProperties(3, { disabled: false }); + + // Test reordering calendars. + + let dragSession = Cc["@mozilla.org/widget/dragservice;1"].getService(Ci.nsIDragService); + dragSession.startDragSessionForTests(Ci.nsIDragService.DRAGDROP_ACTION_MOVE); + + await new Promise(resolve => window.setTimeout(resolve)); + + let [result, dataTransfer] = EventUtils.synthesizeDragOver( + calendarList.rows[3], + calendarList.rows[0], + undefined, + undefined, + undefined, + undefined, + { + screenY: calendarList.rows[0].getBoundingClientRect().top + 1, + } + ); + await new Promise(resolve => setTimeout(resolve)); + + EventUtils.synthesizeDropAfterDragOver(result, dataTransfer, calendarList.rows[0]); + EventUtils.sendDragEvent({ type: "dragend" }, calendarList.rows[0]); + dragSession.endDragSession(true); + await new Promise(resolve => setTimeout(resolve)); + + checkSortOrder(3, 0, 1, 2); + + Assert.equal(document.activeElement, calendarList); + Assert.equal(calendarList.rows[calendarList.selectedIndex], calendarList.rows[0]); + + // Test deleting calendars. + + // Delete a calendar by unregistering it. + CalendarTestUtils.removeCalendar(calendars[3]); + Assert.equal(cal.manager.getCalendars().length, 3); + Assert.equal(calendarList.rowCount, 3); + checkSortOrder(0, 1, 2); + + // Start to remove a calendar. Cancel the prompt. + EventUtils.synthesizeMouseAtCenter(calendarList.rows[1], {}); + await withMockPromptService(1, () => { + EventUtils.synthesizeKey("VK_DELETE"); + }); + Assert.equal(cal.manager.getCalendars().length, 3, "three calendars left in the manager"); + Assert.equal(calendarList.rowCount, 3, "three calendars left in the list"); + checkSortOrder(0, 1, 2); + + // Remove a calendar with the keyboard. + await withMockPromptService(0, () => { + EventUtils.synthesizeKey("VK_DELETE"); + }); + Assert.equal(cal.manager.getCalendars().length, 2, "two calendars left in the manager"); + Assert.equal(calendarList.rowCount, 2, "two calendars left in the list"); + checkSortOrder(0, 2); + + // Remove a calendar with the context menu. + await withMockPromptService(0, async () => { + EventUtils.synthesizeMouseAtCenter(calendarList.rows[1], {}); + await calendarListContextMenu(calendarList.rows[1], "list-calendars-context-delete"); + }); + + Assert.equal(cal.manager.getCalendars().length, 1, "one calendar left in the manager"); + Assert.equal(calendarList.rowCount, 1, "one calendar left in the list"); + checkSortOrder(0); + + Assert.equal(composite.defaultCalendar.id, calendars[0].id, "default calendar id check"); + Assert.equal(calendarList.rows[calendarList.selectedIndex], calendarList.rows[0]); + await CalendarTestUtils.closeCalendarTab(window); +}); diff --git a/comm/calendar/test/browser/browser_calendarTelemetry.js b/comm/calendar/test/browser/browser_calendarTelemetry.js new file mode 100644 index 0000000000..cf2dff7a55 --- /dev/null +++ b/comm/calendar/test/browser/browser_calendarTelemetry.js @@ -0,0 +1,119 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test telemetry related to calendar. + */ + +let { MailTelemetryForTests } = ChromeUtils.import("resource:///modules/MailGlue.jsm"); +let { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +/** + * Check that we're counting calendars and read only calendars. + */ +add_task(async function testCalendarCount() { + Services.telemetry.clearScalars(); + + let calendars = cal.manager.getCalendars(); + let homeCal = calendars.find(cal => cal.name == "Home"); + let readOnly = homeCal.readOnly; + homeCal.readOnly = true; + + for (let i = 1; i <= 3; i++) { + calendars[i] = CalendarTestUtils.createCalendar(`Mochitest ${i}`, "memory"); + if (i === 1 || i === 3) { + calendars[i].readOnly = true; + } + } + + await MailTelemetryForTests.reportCalendars(); + + let scalars = TelemetryTestUtils.getProcessScalars("parent", true); + Assert.equal( + scalars["tb.calendar.calendar_count"].memory, + 3, + "Count of calendars must be correct." + ); + Assert.equal( + scalars["tb.calendar.read_only_calendar_count"].memory, + 2, + "Count of readonly calendars must be correct." + ); + + Assert.ok( + !scalars["tb.calendar.calendar_count"].storage, + "'Home' calendar not included in count while disabled" + ); + + Assert.ok( + !scalars["tb.calendar.read_only_calendar_count"].storage, + "'Home' calendar not included in read-only count while disabled" + ); + + for (let i = 1; i <= 3; i++) { + CalendarTestUtils.removeCalendar(calendars[i]); + } + homeCal.readOnly = readOnly; +}); + +/** + * Ensure the "Home" calendar is not ignored if it has been used. + */ +add_task(async function testHomeCalendar() { + let calendar = cal.manager.getCalendars().find(cal => cal.name == "Home"); + let readOnly = calendar.readOnly; + let disabled = calendar.getProperty("disabled"); + + // Test when enabled with no events. + calendar.setProperty("disabled", false); + calendar.readOnly = true; + Services.telemetry.clearScalars(); + await MailTelemetryForTests.reportCalendars(); + + let scalars = TelemetryTestUtils.getProcessScalars("parent", true); + Assert.ok(!scalars["tb.calendar.calendar_count"], "'Home' calendar not counted when unused"); + Assert.ok( + !scalars["tb.calendar.read_only_calendar_count"], + "'Home' calendar not included in readonly count when unused" + ); + + // Now test with an event added to the calendar. + calendar.readOnly = false; + + let event = new CalEvent(); + event.id = "bacd"; + event.title = "Test"; + event.startDate = cal.dtz.now(); + event = await calendar.addItem(event); + + calendar.readOnly = true; + + await TestUtils.waitForCondition(async () => { + let result = await calendar.getItem("bacd"); + return result; + }, "item added to calendar"); + + Services.telemetry.clearScalars(); + await MailTelemetryForTests.reportCalendars(); + + scalars = TelemetryTestUtils.getProcessScalars("parent", true); + Assert.equal( + scalars["tb.calendar.calendar_count"].storage, + 1, + "'Home' calendar counted when there are items" + ); + Assert.equal( + scalars["tb.calendar.read_only_calendar_count"].storage, + 1, + "'Home' calendar included in read-only count when used" + ); + + calendar.readOnly = false; + await calendar.deleteItem(event); + calendar.readOnly = readOnly; + calendar.setProperty("disabled", disabled); +}); diff --git a/comm/calendar/test/browser/browser_calendarUnifinder.js b/comm/calendar/test/browser/browser_calendarUnifinder.js new file mode 100644 index 0000000000..7020b70f71 --- /dev/null +++ b/comm/calendar/test/browser/browser_calendarUnifinder.js @@ -0,0 +1,76 @@ +/* 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 { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); + +const { mailTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/MailTestUtils.jsm" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + CalEvent: "resource:///modules/CalEvent.jsm", + CalRecurrenceInfo: "resource:///modules/CalRecurrenceInfo.jsm", +}); + +/** + * Tests clicking on events opens in the summary dialog for both + * non-recurring and recurring events. + */ +add_task(async function testOpenEvent() { + let uri = Services.io.newURI("moz-memory-calendar://"); + let calendar = cal.manager.createCalendar("memory", uri); + + calendar.name = "Unifinder Test"; + cal.manager.registerCalendar(calendar); + registerCleanupFunction(() => cal.manager.removeCalendar(calendar)); + + let now = cal.dtz.now(); + + let noRepeatEvent = new CalEvent(); + noRepeatEvent.id = "no repeat event"; + noRepeatEvent.title = "No Repeat Event"; + noRepeatEvent.startDate = now; + noRepeatEvent.endDate = noRepeatEvent.startDate.clone(); + noRepeatEvent.endDate.hour++; + + let repeatEvent = new CalEvent(); + repeatEvent.id = "repeated event"; + repeatEvent.title = "Repeat Event"; + repeatEvent.startDate = now; + repeatEvent.endDate = noRepeatEvent.startDate.clone(); + repeatEvent.endDate.hour++; + repeatEvent.recurrenceInfo = new CalRecurrenceInfo(repeatEvent); + repeatEvent.recurrenceInfo.appendRecurrenceItem( + cal.createRecurrenceRule("RRULE:FREQ=DAILY;COUNT=30") + ); + + await CalendarTestUtils.openCalendarTab(window); + + if (window.isUnifinderHidden()) { + window.toggleUnifinder(); + + await BrowserTestUtils.waitForCondition( + () => window.isUnifinderHidden(), + "calendar unifinder is open" + ); + } + + for (let event of [noRepeatEvent, repeatEvent]) { + await calendar.addItem(event); + + let dialogWindowPromise = CalendarTestUtils.waitForEventDialog(); + let tree = document.querySelector("#unifinder-search-results-tree"); + mailTestUtils.treeClick(EventUtils, window, tree, 0, 1, { clickCount: 2 }); + + let dialogWindow = await dialogWindowPromise; + let docUri = dialogWindow.document.documentURI; + Assert.ok( + docUri === "chrome://calendar/content/calendar-summary-dialog.xhtml", + "event summary dialog did show" + ); + + await BrowserTestUtils.closeWindow(dialogWindow); + await calendar.deleteItem(event); + } +}); diff --git a/comm/calendar/test/browser/browser_dragEventItem.js b/comm/calendar/test/browser/browser_dragEventItem.js new file mode 100644 index 0000000000..204e429fdd --- /dev/null +++ b/comm/calendar/test/browser/browser_dragEventItem.js @@ -0,0 +1,414 @@ +/* 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/. */ + +/** + * Test dragging of events in the various calendar views. + */ +const { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetters(this, { + CalEvent: "resource:///modules/CalEvent.jsm", +}); + +const calendar = CalendarTestUtils.createCalendar("Drag Test", "memory"); +// Set a low number of hours to reduce pixel -> minute rounding errors. +Services.prefs.setIntPref("calendar.view.visiblehours", 3); + +registerCleanupFunction(() => { + CalendarTestUtils.removeCalendar(calendar); + Services.prefs.clearUserPref("calendar.view.visiblehours"); + // Reset the spaces toolbar to its default visible state. + window.gSpacesToolbar.toggleToolbar(false); +}); + +/** + * Ensures that the window is maximised after switching dates. + * + * @param {calIDateTime} date - A date to navigate the view to. + */ +async function resetView(date, view) { + window.goToDate(date); + + if (window.windowState != window.STATE_MAXIMIZED) { + // The multi-day views adjust scrolling dynamically when they detect a + // resize. Hook into the resize event and scroll after the adjustment. + let resizePromise = BrowserTestUtils.waitForEvent(window, "resize"); + window.maximize(); + await resizePromise; + } +} + +/** + * End the dragging of the event at the specified location. + * + * @param {number} day - The day to drop into. + * @param {number} hour - The starting hour to drop to. + * @param {number} topOffset - An offset to apply to the mouse position. + */ +function endDrag(day, hour, topOffset) { + let view = window.currentView(); + let hourElement; + if (view.id == "day-view") { + hourElement = CalendarTestUtils.dayView.getHourBoxAt(window, hour); + } else { + hourElement = CalendarTestUtils.weekView.getHourBoxAt(window, day, hour); + } + // We scroll to align the *end* of the hour element so we can avoid triggering + // the auto-scroll when we synthesize mousemove below. + // FIXME: Use and test auto scroll by holding mouseover at the view edges. + CalendarTestUtils.scrollViewToTarget(hourElement, false); + + let hourRect = hourElement.getBoundingClientRect(); + + // We drop the event with some offset from the starting edge of the desired + // hourElement. + // NOTE: This may mean that the drop point may not be above the hourElement. + // NOTE: We assume that the drop point is however still above the view. + // Currently event "move" events get cancelled if the pointer leaves the view. + let top = Math.round(hourRect.top + topOffset); + let left = Math.round(hourRect.left + hourRect.width / 2); + + EventUtils.synthesizeMouseAtPoint(left, top, { type: "mousemove", shiftKey: true }, window); + EventUtils.synthesizeMouseAtPoint(left, top, { type: "mouseup", shiftKey: true }, window); +} + +/** + * Simulates the dragging of an event box in a multi-day view to another + * column, horizontally. + * + * @param {MozCalendarEventBox} eventBox - The event to start moving. + * @param {number} day - The day to drop into. + * @param {number} hour - The starting hour to drop to. + */ +function simulateDragToColumn(eventBox, day, hour) { + // Scroll to align to the top of the view. + CalendarTestUtils.scrollViewToTarget(eventBox, true); + + let sourceRect = eventBox.getBoundingClientRect(); + // Start dragging from the center of the event box to avoid the gripbars. + // NOTE: We assume that the eventBox's center is in view. + let leftOffset = sourceRect.width / 2; + // We round the mouse position to try and reduce rounding errors when + // scrolling the view. + let sourceTop = Math.round(sourceRect.top + sourceRect.height / 2); + let sourceLeft = sourceRect.left + leftOffset; + // Keep track of the exact offset. + let topOffset = sourceTop - sourceRect.top; + + EventUtils.synthesizeMouseAtPoint( + sourceLeft, + sourceTop, + // Hold shift to avoid snapping. + { type: "mousedown", shiftKey: true }, + window + ); + EventUtils.synthesizeMouseAtPoint( + // We assume the location of the mouseout event does not matter, just as + // long as the event box receives it. + sourceLeft, + sourceTop, + { type: "mouseout", shiftKey: true }, + window + ); + + // End drag with the same offset from the starting edge. + endDrag(day, hour, topOffset); +} + +/** + * Simulates the dragging of an event box via one of the gripbars. + * + * @param {MozCalendarEventBox} eventBox - The event to resize. + * @param {"start"|"end"} - The side to grab. + * @param {number} day - The day to move into. + * @param {number} hour - The hour to move to. + */ +function simulateGripbarDrag(eventBox, side, day, hour) { + // Scroll the edge of the box into view. + CalendarTestUtils.scrollViewToTarget(eventBox, side == "start"); + + let gripbar = side == "start" ? eventBox.startGripbar : eventBox.endGripbar; + + let sourceRect = gripbar.getBoundingClientRect(); + let sourceTop = sourceRect.top + sourceRect.height / 2; + let sourceLeft = sourceRect.left + sourceRect.width / 2; + + // Hover to make the gripbar visible. + EventUtils.synthesizeMouseAtPoint(sourceLeft, sourceTop, { type: "mouseover" }, window); + EventUtils.synthesizeMouseAtPoint( + sourceLeft, + sourceTop, + // Hold shift to avoid snapping. + { type: "mousedown", shiftKey: true }, + window + ); + + // End the drag at the start of the hour. + endDrag(day, hour, 0); +} + +/** + * Tests dragging an event item updates the event in the month view. + */ +add_task(async function testMonthViewDragEventItem() { + let event = new CalEvent(); + event.id = "1"; + event.title = "Month View Event"; + event.startDate = cal.createDateTime("20210316T000000Z"); + event.endDate = cal.createDateTime("20210316T110000Z"); + + await CalendarTestUtils.setCalendarView(window, "month"); + await calendar.addItem(event); + await resetView(event.startDate); + + // Hide the spaces toolbar since it interferes with the calendar + window.gSpacesToolbar.toggleToolbar(true); + + let eventItem = await CalendarTestUtils.monthView.waitForItemAt(window, 3, 3, 1); + let dayBox = await CalendarTestUtils.monthView.getDayBox(window, 3, 2); + let dragSession = Cc["@mozilla.org/widget/dragservice;1"].getService(Ci.nsIDragService); + dragSession.startDragSessionForTests(Ci.nsIDragService.DRAGDROP_ACTION_MOVE); + + let [result, dataTransfer] = EventUtils.synthesizeDragOver( + eventItem, + dayBox, + undefined, + undefined, + eventItem.ownerGlobal, + dayBox.ownerGlobal + ); + EventUtils.synthesizeDropAfterDragOver(result, dataTransfer, dayBox); + dragSession.endDragSession(true); + + Assert.ok( + !CalendarTestUtils.monthView.getItemAt(window, 3, 3, 1), + "item removed from initial date" + ); + + eventItem = await CalendarTestUtils.monthView.waitForItemAt(window, 3, 2, 1); + Assert.ok(eventItem, "item moved to new date"); + + let { id, title, startDate, endDate } = eventItem.occurrence; + Assert.equal(id, event.id, "id is correct"); + Assert.equal(title, event.title, "title is correct"); + Assert.equal(startDate.icalString, "20210315T000000Z", "startDate is correct"); + Assert.equal(endDate.icalString, "20210315T110000Z", "endDate is correct"); + await calendar.deleteItem(eventItem.occurrence); +}); + +/** + * Tests dragging an event item updates the event in the multiweek view. + */ +add_task(async function testMultiWeekViewDragEventItem() { + let event = new CalEvent(); + event.id = "2"; + event.title = "Multiweek View Event"; + event.startDate = cal.createDateTime("20210316T000000Z"); + event.endDate = cal.createDateTime("20210316T110000Z"); + + await CalendarTestUtils.setCalendarView(window, "multiweek"); + await calendar.addItem(event); + await resetView(event.startDate); + + let eventItem = await CalendarTestUtils.multiweekView.waitForItemAt(window, 1, 3, 1); + let dayBox = await CalendarTestUtils.multiweekView.getDayBox(window, 1, 2); + let dragSession = Cc["@mozilla.org/widget/dragservice;1"].getService(Ci.nsIDragService); + dragSession.startDragSessionForTests(Ci.nsIDragService.DRAGDROP_ACTION_MOVE); + + let [result, dataTransfer] = EventUtils.synthesizeDragOver( + eventItem, + dayBox, + undefined, + undefined, + eventItem.ownerGlobal, + dayBox.ownerGlobal + ); + EventUtils.synthesizeDropAfterDragOver(result, dataTransfer, dayBox); + dragSession.endDragSession(true); + + Assert.ok( + !CalendarTestUtils.multiweekView.getItemAt(window, 1, 3, 1), + "item removed from initial date" + ); + + eventItem = await CalendarTestUtils.multiweekView.waitForItemAt(window, 1, 2, 1); + Assert.ok(eventItem, "item moved to new date"); + + let { id, title, startDate, endDate } = eventItem.occurrence; + Assert.equal(id, event.id, "id is correct"); + Assert.equal(title, event.title, "title is correct"); + Assert.equal(startDate.icalString, "20210315T000000Z", "startDate is correct"); + Assert.equal(endDate.icalString, "20210315T110000Z", "endDate is correct"); + await calendar.deleteItem(eventItem.occurrence); +}); + +/** + * Tests dragging an event box to the previous day updates the event in the + * week view. + */ +add_task(async function testWeekViewDragEventBoxToPreviousDay() { + let event = new CalEvent(); + event.id = "3"; + event.title = "Week View Previous Day"; + event.startDate = cal.createDateTime("20210316T020000Z"); + event.endDate = cal.createDateTime("20210316T030000Z"); + + await CalendarTestUtils.setCalendarView(window, "week"); + await calendar.addItem(event); + await resetView(event.startDate); + + let eventBox = await CalendarTestUtils.weekView.waitForEventBoxAt(window, 3, 1); + simulateDragToColumn(eventBox, 2, 2); + + eventBox = await CalendarTestUtils.weekView.waitForEventBoxAt(window, 2, 1); + await TestUtils.waitForCondition( + () => !CalendarTestUtils.weekView.getEventBoxAt(window, 3, 1), + "Old position is empty" + ); + + let { id, title, startDate, endDate } = eventBox.occurrence; + Assert.equal(id, event.id, "id is correct"); + Assert.equal(title, event.title, "title is correct"); + Assert.equal(startDate.icalString, "20210315T020000Z", "startDate is correct"); + Assert.equal(endDate.icalString, "20210315T030000Z", "endDate is correct"); + await calendar.deleteItem(eventBox.occurrence); +}); + +/** + * Tests dragging an event box to the following day updates the event in the + * week view. + */ +add_task(async function testWeekViewDragEventBoxToFollowingDay() { + let event = new CalEvent(); + event.id = "4"; + event.title = "Week View Following Day"; + event.startDate = cal.createDateTime("20210316T020000Z"); + event.endDate = cal.createDateTime("20210316T030000Z"); + + await CalendarTestUtils.setCalendarView(window, "week"); + await calendar.addItem(event); + await resetView(event.startDate); + + let eventBox = await CalendarTestUtils.weekView.waitForEventBoxAt(window, 3, 1); + simulateDragToColumn(eventBox, 4, 2); + + eventBox = await CalendarTestUtils.weekView.waitForEventBoxAt(window, 4, 1); + await TestUtils.waitForCondition( + () => !CalendarTestUtils.weekView.getEventBoxAt(window, 3, 1), + "Old position is empty" + ); + + let { id, title, startDate, endDate } = eventBox.occurrence; + Assert.equal(id, event.id, "id is correct"); + Assert.equal(title, event.title, "title is correct"); + Assert.equal(startDate.icalString, "20210317T020000Z", "startDate is correct"); + Assert.equal(endDate.icalString, "20210317T030000Z", "endDate is correct"); + await calendar.deleteItem(eventBox.occurrence); +}); + +/** + * Tests dragging the top of an event box updates the start time in the week + * view. + */ +add_task(async function testWeekViewDragEventBoxStartTime() { + let event = new CalEvent(); + event.id = "5"; + event.title = "Week View Start"; + event.startDate = cal.createDateTime("20210316T020000Z"); + event.endDate = cal.createDateTime("20210316T030000Z"); + + await CalendarTestUtils.setCalendarView(window, "week"); + await calendar.addItem(event); + await resetView(event.startDate); + + let eventBox = await CalendarTestUtils.weekView.waitForEventBoxAt(window, 3, 1); + simulateGripbarDrag(eventBox, "start", 3, 1); + eventBox = await CalendarTestUtils.weekView.waitForEventBoxAt(window, 3, 1); + + let { id, title, startDate, endDate } = eventBox.occurrence; + Assert.equal(id, event.id, "id is correct"); + Assert.equal(title, event.title, "title is correct"); + Assert.equal(startDate.icalString, "20210316T010000Z", "startDate was changed"); + Assert.equal(endDate.icalString, "20210316T030000Z", "endDate did not change"); + await calendar.deleteItem(eventBox.occurrence); +}); + +/** + * Tests dragging the end of an event box changes the time in the week view. + */ +add_task(async function testWeekViewDragEventBoxEndTime() { + let event = new CalEvent(); + event.id = "6"; + event.title = "Week View End"; + event.startDate = cal.createDateTime("20210316T020000Z"); + event.endDate = cal.createDateTime("20210316T030000Z"); + + await CalendarTestUtils.setCalendarView(window, "week"); + await calendar.addItem(event); + await resetView(event.startDate); + + let eventBox = await CalendarTestUtils.weekView.waitForEventBoxAt(window, 3, 1); + simulateGripbarDrag(eventBox, "end", 3, 6); + eventBox = await CalendarTestUtils.weekView.waitForEventBoxAt(window, 3, 1); + + let { id, title, startDate, endDate } = eventBox.occurrence; + Assert.equal(id, event.id, "id is correct"); + Assert.equal(title, event.title, "title is correct"); + Assert.equal(startDate.icalString, "20210316T020000Z", "startDate did not change"); + Assert.equal(endDate.icalString, "20210316T060000Z", "endDate was changed"); + await calendar.deleteItem(eventBox.occurrence); +}); + +/** + * Tests dragging the top of an event box changes the start time in the day view. + */ +add_task(async function testDayViewDragEventBoxStartTime() { + let event = new CalEvent(); + event.id = "7"; + event.title = "Day View Start"; + event.startDate = cal.createDateTime("20210316T020000Z"); + event.endDate = cal.createDateTime("20210316T030000Z"); + + await CalendarTestUtils.setCalendarView(window, "day"); + await calendar.addItem(event); + await resetView(event.startDate); + + let eventBox = await CalendarTestUtils.dayView.waitForEventBoxAt(window, 1); + simulateGripbarDrag(eventBox, "start", 1, 1); + eventBox = await CalendarTestUtils.dayView.waitForEventBoxAt(window, 1); + + let { id, title, startDate, endDate } = eventBox.occurrence; + Assert.equal(id, event.id, "id is correct"); + Assert.equal(title, event.title, "title is correct"); + Assert.equal(startDate.icalString, "20210316T010000Z", "startDate was changed"); + Assert.equal(endDate.icalString, "20210316T030000Z", "endDate did not change"); + await calendar.deleteItem(eventBox.occurrence); +}); + +/** + * Tests dragging the bottom of an event box changes the end time in the day + * view. + */ +add_task(async function testDayViewDragEventBoxEndTime() { + let event = new CalEvent(); + event.id = "8"; + event.title = "Day View End"; + event.startDate = cal.createDateTime("20210316T020000Z"); + event.endDate = cal.createDateTime("20210316T030000Z"); + + await CalendarTestUtils.setCalendarView(window, "day"); + await calendar.addItem(event); + await resetView(event.startDate); + + let eventBox = await CalendarTestUtils.dayView.waitForEventBoxAt(window, 1); + simulateGripbarDrag(eventBox, "end", 1, 4); + eventBox = await CalendarTestUtils.dayView.waitForEventBoxAt(window, 1); + + let { id, title, startDate, endDate } = eventBox.occurrence; + Assert.equal(id, event.id, "id is correct"); + Assert.equal(title, event.title, "title is correct"); + Assert.equal(startDate.icalString, "20210316T020000Z", "startDate did not change"); + Assert.equal(endDate.icalString, "20210316T040000Z", "endDate was changed"); + await calendar.deleteItem(eventBox.occurrence); +}); diff --git a/comm/calendar/test/browser/browser_eventDisplay_dayView.js b/comm/calendar/test/browser/browser_eventDisplay_dayView.js new file mode 100644 index 0000000000..5f0941cac4 --- /dev/null +++ b/comm/calendar/test/browser/browser_eventDisplay_dayView.js @@ -0,0 +1,133 @@ +/* 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/. */ + +XPCOMUtils.defineLazyModuleGetters(this, { + CalEvent: "resource:///modules/CalEvent.jsm", +}); + +var calendar = CalendarTestUtils.createCalendar(); +registerCleanupFunction(() => { + CalendarTestUtils.removeCalendar(calendar); +}); + +/** + * Create an event item in the calendar. + * + * @param {string} name - The name of the event. + * @param {string} start - The date time string for the start of the event. + * @param {string} end - The date time string for the end of the event. + * + * @returns {CalEvent} - The created event. + */ +async function createEvent(name, start, end) { + let event = new CalEvent(); + event.title = name; + event.startDate = cal.createDateTime(start); + event.endDate = cal.createDateTime(end); + return calendar.addItem(event); +} + +/** + * Assert that there is an event shown on the given date in the day-view. + * + * @param {object} date - The date to move to. + * @param {number} date.day - The day. + * @param {number} date.week - The week. + * @param {number} date.year - The year. + * @param {object} expect - Details about the expected event. + * @param {string} expect.name - The event name. + * @param {boolean} expect.startInView - Whether the event starts within the + * view on the given date. + * @param {boolean} expect.endInView - Whether the event ends within the view + * on the given date. + * @param {string} message - A message to use in assertions. + */ +async function assertDayEvent(date, expect, message) { + await CalendarTestUtils.goToDate(window, date.year, date.month, date.day); + let element = await CalendarTestUtils.dayView.waitForEventBoxAt(window, 1); + Assert.equal( + element.querySelector(".event-name-label").textContent, + expect.name, + `Event name should match: ${message}` + ); + await CalendarTestUtils.assertEventBoxDraggable( + element, + expect.startInView, + expect.endInView, + message + ); +} + +/** + * Test an event that occurs within one day, in the day view. + */ +add_task(async function testInsideDayView() { + let event = await createEvent("Test Event", "20190403T123400", "20190403T234500"); + await CalendarTestUtils.setCalendarView(window, "day"); + Assert.equal( + document.querySelectorAll("#day-view calendar-event-column").length, + 1, + "1 day column in the day view" + ); + + // This event is fully within this view. + await assertDayEvent( + { day: 3, month: 4, year: 2019 }, + { name: "Test Event", startInView: true, endInView: true }, + "Single day event" + ); + + await CalendarTestUtils.closeCalendarTab(window); + await calendar.deleteItem(event); +}); + +/** + * Test an event that starts and ends at midnight, in the day view. + */ +add_task(async function testMidnightDayView() { + let event = await createEvent("Test Event", "20190403T000000", "20190404T000000"); + await CalendarTestUtils.setCalendarView(window, "day"); + + // This event is fully within this view. + await assertDayEvent( + { day: 3, month: 4, year: 2019 }, + { name: "Test Event", startInView: true, endInView: true }, + "Single midnight event" + ); + + await CalendarTestUtils.closeCalendarTab(window); + await calendar.deleteItem(event); +}); + +/** + * Test an event that spans multiple days, in the day view. + */ +add_task(async function testOutsideDayView() { + let event = await createEvent("Test Event", "20190402T123400", "20190404T234500"); + await CalendarTestUtils.setCalendarView(window, "day"); + + // Go to the start of the event. The end of the event is beyond the current view. + await assertDayEvent( + { day: 2, month: 4, year: 2019 }, + { name: "Test Event", startInView: true, endInView: false }, + "First day" + ); + + // Go to the middle of the event. Both ends of the event are beyond the current view. + await assertDayEvent( + { day: 3, month: 4, year: 2019 }, + { name: "Test Event", startInView: false, endInView: false }, + "Middle day" + ); + + // Go to the end of the event. The start of the event is beyond the current view. + await assertDayEvent( + { day: 4, month: 4, year: 2019 }, + { name: "Test Event", startInView: false, endInView: true }, + "Last day" + ); + + await CalendarTestUtils.closeCalendarTab(window); + await calendar.deleteItem(event); +}); diff --git a/comm/calendar/test/browser/browser_eventDisplay_multiWeekView.js b/comm/calendar/test/browser/browser_eventDisplay_multiWeekView.js new file mode 100644 index 0000000000..91ddeec6ac --- /dev/null +++ b/comm/calendar/test/browser/browser_eventDisplay_multiWeekView.js @@ -0,0 +1,275 @@ +/* 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/. */ + +XPCOMUtils.defineLazyModuleGetters(this, { + CalEvent: "resource:///modules/CalEvent.jsm", +}); + +var calendar = CalendarTestUtils.createCalendar(); +registerCleanupFunction(() => { + CalendarTestUtils.removeCalendar(calendar); +}); + +/** + * Create an event item in the calendar. + * + * @param {string} name - The name of the event. + * @param {string} start - The date time string for the start of the event. + * @param {string} end - The date time string for the end of the event. + * + * @returns {Promise<CalEvent>} - The created event. + */ +function createEvent(name, start, end) { + let event = new CalEvent(); + event.title = name; + event.startDate = cal.createDateTime(start); + event.endDate = cal.createDateTime(end); + return calendar.addItem(event); +} + +/** + * Assert that there is a an event in the multiweek or month view between the + * expected range, and no events on the other days. + * + * @param {"multiweek"|"month"} viewName - The view to test. + * @param {number} numWeeks - The number of weeks shown in the view. + * @param {object} date - The date to move to. + * @param {number} date.day - The day. + * @param {number} date.week - The week. + * @param {number} date.year - The year. + * @param {object} expect - Details about the expected event. + * @param {string} expect.name - The event name. + * @param {number} expect.start - The day that the event should start in the + * week. Between 1 and 7. + * @param {number} expect.end - The day that the event should end in the week. + * @param {boolean} expect.startInView - Whether the event starts within the + * view on the given date. + * @param {boolean} expect.endInView - Whether the event ends within the view + * on the given date. + * @param {string} message - A message to use in assertions. + */ +async function assertMultiweekEvents(viewName, numWeeks, date, expect, message) { + await CalendarTestUtils.goToDate(window, date.year, date.month, date.day); + let view = + viewName == "multiweek" ? CalendarTestUtils.multiweekView : CalendarTestUtils.monthView; + + // start = (startWeek - 1) * 7 + startDay + let startWeek = Math.floor((expect.start - 1) / 7) + 1; + let startDay = ((expect.start - 1) % 7) + 1; + let endWeek = Math.floor((expect.end - 1) / 7) + 1; + let endDay = ((expect.end - 1) % 7) + 1; + for (let week = startWeek; week <= endWeek; week++) { + let start = week == startWeek ? startDay : 1; + let end = week == endWeek ? endDay : 7; + for (let day = start; day <= end; day++) { + let element = await view.waitForItemAt(window, week, day, 1); + Assert.equal( + element.querySelector(".event-name-label").textContent, + expect.name, + `Week ${week}, day ${day} event name should match: ${message}` + ); + let multidayIcon = element.querySelector(".item-type-icon"); + if (startDay == endDay && week == startWeek && day == startDay) { + Assert.equal(multidayIcon.src, "", `Week ${week}, day ${day} icon has no source`); + } else if (expect.startInView && week == startWeek && day == startDay) { + Assert.equal( + multidayIcon.src, + "chrome://calendar/skin/shared/event-start.svg", + `Week ${week}, day ${day} icon src shows event start: ${message}` + ); + } else if (expect.endInView && week == endWeek && day == endDay) { + Assert.equal( + multidayIcon.src, + "chrome://calendar/skin/shared/event-end.svg", + `Week ${week}, day ${day} icon src shows event end: ${message}` + ); + } else { + Assert.equal( + multidayIcon.src, + "chrome://calendar/skin/shared/event-continue.svg", + `Week ${week}, day ${day} icon src shows event continue: ${message}` + ); + } + } + } + Assert.equal( + numWeeks, + document.querySelectorAll(`#${viewName}-view .monthbody tr:not([hidden])`).length, + `Should show ${numWeeks} weeks in the view: ${message}` + ); + // Test no events loaded on the other days. + for (let week = 1; week <= numWeeks; week++) { + for (let day = 1; day <= 7; day++) { + if ( + (week > startWeek && week < endWeek) || + (week == startWeek && day >= startDay) || + (week == endWeek && day <= endDay) + ) { + continue; + } + Assert.ok( + !view.getItemAt(window, week, day, 1), + `Should be no events on day ${day}: ${message}` + ); + } + } +} + +/** + * Test an event that occurs fully within the multi-week view. + */ +add_task(async function testInsideMultiweekView() { + let event = await createEvent("Test Event", "20190402T123400", "20190419T234500"); + await CalendarTestUtils.setCalendarView(window, "multiweek"); + Assert.equal( + document.querySelectorAll("#multiweek-view tr:not([hidden]) calendar-month-day-box").length, + 28, + "28 days in the multiweek view" + ); + + await assertMultiweekEvents( + "multiweek", + 4, + { day: 1, month: 4, year: 2019 }, + { name: "Test Event", start: 3, end: 20, startInView: true, endInView: true }, + "3 week event in multiweek view" + ); + + await CalendarTestUtils.closeCalendarTab(window); + await calendar.deleteItem(event); +}); + +/** + * Test an event that starts and ends at midnight, in the multi-week view. + */ +add_task(async function testMidnightMultiweekView() { + // Event spans one day. + let event = await createEvent("Test Event", "20190402T000000", "20190403T000000"); + await CalendarTestUtils.setCalendarView(window, "multiweek"); + + await assertMultiweekEvents( + "multiweek", + 4, + { day: 1, month: 4, year: 2019 }, + { name: "Test Event", start: 3, end: 3, startInView: true, endInView: true }, + "one day midnight event in multiweek" + ); + + await CalendarTestUtils.closeCalendarTab(window); + await calendar.deleteItem(event); +}); + +/** + * Test an event that starts or ends outside the multi-week view. + */ +add_task(async function testOutsideMultiweekView() { + let event = await createEvent("Test Event", "20190402T123400", "20190507T234500"); + await CalendarTestUtils.setCalendarView(window, "multiweek"); + + await assertMultiweekEvents( + "multiweek", + 4, + { day: 11, month: 3, year: 2019 }, + { name: "Test Event", start: 24, end: 28, startInView: true, endInView: false }, + "First block in multiweek" + ); + + await assertMultiweekEvents( + "multiweek", + 4, + { day: 8, month: 4, year: 2019 }, + { name: "Test Event", start: 1, end: 28, startInView: false, endInView: false }, + "Middle block in multiweek" + ); + + await assertMultiweekEvents( + "multiweek", + 4, + { day: 29, month: 4, year: 2019 }, + { name: "Test Event", start: 1, end: 10, startInView: false, endInView: true }, + "End block in multiweek" + ); + + await CalendarTestUtils.closeCalendarTab(window); + await calendar.deleteItem(event); +}); + +/** + * Test an event that occurs within one month, in the month view. + */ +add_task(async function testInsideMonthView() { + let event = await createEvent("Test Event", "20190702T123400", "20190719T234500"); + await CalendarTestUtils.setCalendarView(window, "month"); + Assert.equal( + document.querySelectorAll("#month-view tr:not([hidden]) calendar-month-day-box").length, + 35, + "35 days in the month view" + ); + + await assertMultiweekEvents( + "month", + 5, + { day: 1, month: 7, year: 2019 }, + { name: "Test Event", start: 3, end: 20, startInView: true, endInView: true }, + "Event in single month" + ); + + await CalendarTestUtils.closeCalendarTab(window); + await calendar.deleteItem(event); +}); + +/** + * Test an event that starts and ends at midnight, in the month view. + */ +add_task(async function testMidnightMonthView() { + // Event spans three days. + let event = await createEvent("Test Event", "20190702T000000", "20190705T000000"); + await CalendarTestUtils.setCalendarView(window, "month"); + + await assertMultiweekEvents( + "month", + 5, + { day: 1, month: 7, year: 2019 }, + { name: "Test Event", start: 3, end: 5, startInView: true, endInView: true }, + "3 day midnight event in single month" + ); + + await CalendarTestUtils.closeCalendarTab(window); + await calendar.deleteItem(event); +}); + +/** + * Test an event that spans multiple months, in the month view. + */ +add_task(async function testOutsideMonthView() { + let event = await createEvent("Test Event", "20190320T123400", "20190507T234500"); + await CalendarTestUtils.setCalendarView(window, "month"); + + await assertMultiweekEvents( + "month", + 6, + { day: 1, month: 3, year: 2019 }, + { name: "Test Event", start: 25, end: 42, startInView: true, endInView: false }, + "First month" + ); + + await assertMultiweekEvents( + "month", + 5, + { day: 1, month: 4, year: 2019 }, + { name: "Test Event", start: 1, end: 35, startInView: false, endInView: false }, + "Middle month" + ); + + await assertMultiweekEvents( + "month", + 5, + { day: 1, month: 5, year: 2019 }, + { name: "Test Event", start: 1, end: 10, startInView: false, endInView: true }, + "End month" + ); + + await CalendarTestUtils.closeCalendarTab(window); + await calendar.deleteItem(event); +}); diff --git a/comm/calendar/test/browser/browser_eventDisplay_weekView.js b/comm/calendar/test/browser/browser_eventDisplay_weekView.js new file mode 100644 index 0000000000..806105c29a --- /dev/null +++ b/comm/calendar/test/browser/browser_eventDisplay_weekView.js @@ -0,0 +1,151 @@ +/* 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/. */ + +XPCOMUtils.defineLazyModuleGetters(this, { + CalEvent: "resource:///modules/CalEvent.jsm", +}); + +var calendar = CalendarTestUtils.createCalendar(); +registerCleanupFunction(() => { + CalendarTestUtils.removeCalendar(calendar); +}); + +/** + * Create an event item in the calendar. + * + * @param {string} name - The name of the event. + * @param {string} start - The date time string for the start of the event. + * @param {string} end - The date time string for the end of the event. + * + * @returns {CalEvent} - The created event. + */ +async function createEvent(name, start, end) { + let event = new CalEvent(); + event.title = name; + event.startDate = cal.createDateTime(start); + event.endDate = cal.createDateTime(end); + return calendar.addItem(event); +} + +/** + * Assert that there is a an event in the week-view between the expected range, + * and no events on the other days. + * + * @param {object} date - The date to move to. + * @param {number} date.day - The day. + * @param {number} date.week - The week. + * @param {number} date.year - The year. + * @param {object} expect - Details about the expected event. + * @param {string} expect.name - The event name. + * @param {number} expect.start - The day that the event should start in the + * week. Between 1 and 7. + * @param {number} expect.end - The day that the event should end in the week. + * @param {boolean} expect.startInView - Whether the event starts within the + * view on the given date. + * @param {boolean} expect.endInView - Whether the event ends within the view + * on the given date. + * @param {string} message - A message to use in assertions. + */ +async function assertWeekEvents(date, expect, message) { + await CalendarTestUtils.goToDate(window, date.year, date.month, date.day); + // First test for expected events since these can take a short while to load, + // and we don't want to test for the absence of an event before they show. + for (let day = expect.start; day <= expect.end; day++) { + let element = await CalendarTestUtils.weekView.waitForEventBoxAt(window, day, 1); + Assert.equal( + element.querySelector(".event-name-label").textContent, + expect.name, + `Day ${day} event name should match: ${message}` + ); + let icon = element.querySelector(".item-recurrence-icon"); + Assert.equal(icon.src, ""); + Assert.ok(icon.hidden); + await CalendarTestUtils.assertEventBoxDraggable( + element, + expect.startInView && day == expect.start, + expect.endInView && day == expect.end, + `Day ${day}: ${message}` + ); + } + // Test no events loaded on the other days. + for (let day = 1; day <= 7; day++) { + if (day >= expect.start && day <= expect.end) { + continue; + } + Assert.equal( + CalendarTestUtils.weekView.getEventBoxes(window, day).length, + 0, + `Should be no events on day ${day}: ${message}` + ); + } +} + +/** + * Test an event that occurs within one week, in the week view. + */ +add_task(async function testInsideWeekView() { + let event = await createEvent("Test Event", "20190101T123400", "20190103T234500"); + await CalendarTestUtils.setCalendarView(window, "week"); + Assert.equal( + document.querySelectorAll("#week-view calendar-event-column").length, + 7, + "7 day columns in the week view" + ); + + await assertWeekEvents( + { day: 1, month: 1, year: 2019 }, + { name: "Test Event", start: 3, end: 5, startInView: true, endInView: true }, + "Single week event" + ); + + await CalendarTestUtils.closeCalendarTab(window); + await calendar.deleteItem(event); +}); + +/** + * Test an event that starts and ends at midnight, in the week view. + */ +add_task(async function testMidnightWeekView() { + // Spans three days. + let event = await createEvent("Test Event", "20190101T000000", "20190104T000000"); + await CalendarTestUtils.setCalendarView(window, "week"); + + // Midnight-to-midnight event only spans one day even though the end time + // matches the starting time of the next day (midnight). + await assertWeekEvents( + { day: 1, month: 1, year: 2019 }, + { name: "Test Event", start: 3, end: 5, startInView: true, endInView: true }, + "Midnight week event" + ); + + await CalendarTestUtils.closeCalendarTab(window); + await calendar.deleteItem(event); +}); + +/** + * Test an event that spans multiple weeks, in the week view. + */ +add_task(async function testOutsideWeekView() { + let event = await createEvent("Test Event", "20190402T123400", "20190418T234500"); + await CalendarTestUtils.setCalendarView(window, "week"); + + await assertWeekEvents( + { day: 3, month: 4, year: 2019 }, + { name: "Test Event", start: 3, end: 7, startInView: true, endInView: false }, + "First week" + ); + await assertWeekEvents( + { day: 10, month: 4, year: 2019 }, + { name: "Test Event", start: 1, end: 7, startInView: false, endInView: false }, + "Middle week" + ); + await assertWeekEvents( + { day: 17, month: 4, year: 2019 }, + { name: "Test Event", start: 1, end: 5, startInView: false, endInView: true }, + "Last week" + ); + + await CalendarTestUtils.closeCalendarTab(window); + await calendar.deleteItem(event); +}); diff --git a/comm/calendar/test/browser/browser_eventUndoRedo.js b/comm/calendar/test/browser/browser_eventUndoRedo.js new file mode 100644 index 0000000000..34ea8d0523 --- /dev/null +++ b/comm/calendar/test/browser/browser_eventUndoRedo.js @@ -0,0 +1,260 @@ +/* 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/. */ + +"use strict"; + +/** + * Tests for ensuring the undo/redo options are enabled properly when + * manipulating events. + */ + +XPCOMUtils.defineLazyModuleGetters(this, { + CalEvent: "resource:///modules/CalEvent.jsm", + CalTransactionManager: "resource:///modules/CalTransactionManager.jsm", +}); + +const calendar = CalendarTestUtils.createCalendar("Undo Redo Test"); +const calTransManager = CalTransactionManager.getInstance(); + +/** + * Checks the value of the "disabled" property for items in either the "Edit" + * menu bar or the app menu. Display of the relevant menu is triggered first so + * the UI code can update the respective items. + * + * @param {XULElement} element - The menu item we want to check, if its id begins + * with "menu" then we assume it is in the menu + * bar, if "appmenu" then the app menu. + */ +async function isDisabled(element) { + let targetMenu = document.getElementById("menu_EditPopup"); + + let shownPromise = BrowserTestUtils.waitForEvent(targetMenu, "popupshown"); + EventUtils.synthesizeMouseAtCenter(document.getElementById("menu_Edit"), {}); + await shownPromise; + + let hiddenPromise = BrowserTestUtils.waitForEvent(targetMenu, "popuphidden"); + let status = element.disabled; + targetMenu.hidePopup(); + await hiddenPromise; + return status; +} + +async function clickItem(element) { + let targetMenu = document.getElementById("menu_EditPopup"); + + let shownPromise = BrowserTestUtils.waitForEvent(targetMenu, "popupshown"); + EventUtils.synthesizeMouseAtCenter(document.getElementById("menu_Edit"), {}); + await shownPromise; + + targetMenu.activateItem(element); +} + +/** + * Removes CalTransaction items from the CalTransactionManager stacks so other + * tests are unhindered. + */ +function clearTransactions() { + calTransManager.undoStack = []; + calTransManager.redoStack = []; +} + +/** + * Test the undo/redo functionality for event creation. + * + * @param {string} undoId - The id of the "undo" menu item. + * @param {string} redoId - The id of the "redo" menu item. + */ +async function testAddUndoRedoEvent(undoId, redoId) { + let undo = document.getElementById(undoId); + let redo = document.getElementById(redoId); + Assert.ok(await isDisabled(undo), `#${undoId} is disabled`); + Assert.ok(await isDisabled(redo), `#${redoId} is disabled`); + + let newBtn = document.getElementById("sidePanelNewEvent"); + let windowOpened = CalendarTestUtils.waitForEventDialog("edit"); + EventUtils.synthesizeMouseAtCenter(newBtn, {}); + + let win = await windowOpened; + let iframeWin = win.document.getElementById("calendar-item-panel-iframe").contentWindow; + await CalendarTestUtils.items.setData(win, iframeWin, { title: "A New Event" }); + await CalendarTestUtils.items.saveAndCloseItemDialog(win); + + let eventItem; + await TestUtils.waitForCondition(() => { + eventItem = document.querySelector("calendar-month-day-box-item"); + return eventItem; + }, "event not created in time"); + + Assert.ok(!(await isDisabled(undo)), `#${undoId} is enabled`); + Assert.ok(await isDisabled(redo), `#${redoId} is disabled`); + + // Test undo. + await clickItem(undo); + await TestUtils.waitForCondition(() => { + eventItem = document.querySelector("calendar-month-day-box-item"); + return !eventItem; + }, "undo did not remove item in time"); + + Assert.ok(!eventItem, `#${undoId} reverses item creation`); + + // Test redo. + await clickItem(redo); + await TestUtils.waitForCondition(() => { + eventItem = document.querySelector("calendar-month-day-box-item"); + return eventItem; + }, `${redoId} did not re-create item in time`); + Assert.ok(eventItem, `#${redoId} redos item creation`); + + await calendar.deleteItem(eventItem.item); + clearTransactions(); +} + +/** + * Test the undo/redo functionality for event modification. + * + * @param {string} undoId - The id of the "undo" menu item. + * @param {string} redoId - The id of the "redo" menu item. + */ +async function testModifyUndoRedoEvent(undoId, redoId) { + let undo = document.getElementById(undoId); + let redo = document.getElementById(redoId); + Assert.ok(await isDisabled(undo), `#${undoId} is disabled`); + Assert.ok(await isDisabled(redo), `#${redoId} is disabled`); + + let event = new CalEvent(); + event.title = "Modifiable Event"; + event.startDate = cal.dtz.now(); + await calendar.addItem(event); + window.goToDate(event.startDate); + + let eventItem; + await TestUtils.waitForCondition(() => { + eventItem = document.querySelector("calendar-month-day-box-item"); + return eventItem; + }, "event not created in time"); + + let { dialogWindow, iframeWindow } = await CalendarTestUtils.editItem(window, eventItem); + await CalendarTestUtils.items.setData(dialogWindow, iframeWindow, { + title: "Modified Event", + }); + await CalendarTestUtils.items.saveAndCloseItemDialog(dialogWindow); + + await TestUtils.waitForCondition(() => { + eventItem = document.querySelector("calendar-month-day-box-item"); + return eventItem && eventItem.item.title == "Modified Event"; + }, "event not modified in time"); + + Assert.ok(!(await isDisabled(undo)), `#${undoId} is enabled`); + Assert.ok(await isDisabled(redo), `#${redoId} is disabled`); + + // Test undo. + await clickItem(undo); + await TestUtils.waitForCondition(() => { + eventItem = document.querySelector("calendar-month-day-box-item"); + return eventItem && eventItem.item.title == "Modifiable Event"; + }, `#${undoId} did not un-modify event in time`); + + Assert.equal(eventItem.item.title, "Modifiable Event", `#${undoId} reverses item modification`); + + // Test redo. + await clickItem(redo); + await TestUtils.waitForCondition(() => { + eventItem = document.querySelector("calendar-month-day-box-item"); + return eventItem && eventItem.item.title == "Modified Event"; + }, `${redoId} did not re-modify item in time`); + + Assert.equal(eventItem.item.title, "Modified Event", `#${redoId} redos item modification`); + + clearTransactions(); + await calendar.deleteItem(eventItem.item); +} + +/** + * Test the undo/redo functionality for event deletion. + * + * @param {string} undoId - The id of the "undo" menu item. + * @param {string} redoId - The id of the "redo" menu item. + */ +async function testDeleteUndoRedo(undoId, redoId) { + let undo = document.getElementById(undoId); + let redo = document.getElementById(redoId); + Assert.ok(await isDisabled(undo), `#${undoId} is disabled`); + Assert.ok(await isDisabled(redo), `#${redoId} is disabled`); + + let event = new CalEvent(); + event.title = "Deletable Event"; + event.startDate = cal.dtz.now(); + await calendar.addItem(event); + window.goToDate(event.startDate); + + let eventItem; + await TestUtils.waitForCondition(() => { + eventItem = document.querySelector("calendar-month-day-box-item"); + return eventItem; + }, "event not created in time"); + + EventUtils.synthesizeMouseAtCenter(eventItem, {}); + EventUtils.synthesizeKey("VK_DELETE"); + + await TestUtils.waitForCondition(() => { + eventItem = document.querySelector("calendar-month-day-box-item"); + return !eventItem; + }, "event not deleted in time"); + + Assert.ok(!(await isDisabled(undo)), `#${undoId} is enabled`); + Assert.ok(await isDisabled(redo), `#${redoId} is disabled`); + + // Test undo. + await clickItem(undo); + await TestUtils.waitForCondition(() => { + eventItem = document.querySelector("calendar-month-day-box-item"); + return eventItem; + }, `#${undoId} did not add event in time`); + Assert.ok(eventItem, `#${undoId} reverses item deletion`); + + // Test redo. + await clickItem(redo); + await TestUtils.waitForCondition(() => { + eventItem = document.querySelector("calendar-month-day-box-item"); + return !eventItem; + }, "redo did not delete item in time"); + + Assert.ok(!eventItem, `#${redoId} redos item deletion`); + clearTransactions(); +} + +/** + * Ensure the menu bar is visible and navigate the calendar view to today. + */ +add_setup(async function () { + registerCleanupFunction(() => { + CalendarTestUtils.removeCalendar(calendar); + }); + + clearTransactions(); + document.getElementById("toolbar-menubar").setAttribute("autohide", null); + await CalendarTestUtils.setCalendarView(window, "month"); + window.goToDate(cal.dtz.now()); +}); + +/** + * Tests the menu bar's undo/redo after adding an event. + */ +add_task(async function testMenuBarAddEventUndoRedo() { + return testAddUndoRedoEvent("menu_undo", "menu_redo"); +}).__skipMe = AppConstants.platform == "macosx"; // Can't click menu bar on Mac. + +/** + * Tests the menu bar's undo/redo after modifying an event. + */ +add_task(async function testMenuBarModifyEventUndoRedo() { + return testModifyUndoRedoEvent("menu_undo", "menu_redo"); +}).__skipMe = AppConstants.platform == "macosx"; // Can't click menu bar on Mac. + +/** + * Tests the menu bar's undo/redo after deleting an event. + */ +add_task(async function testMenuBarDeleteEventUndoRedo() { + return testDeleteUndoRedo("menu_undo", "menu_redo"); +}).__skipMe = AppConstants.platform == "macosx"; // Can't click menu bar on Mac. diff --git a/comm/calendar/test/browser/browser_import.js b/comm/calendar/test/browser/browser_import.js new file mode 100644 index 0000000000..86e605802b --- /dev/null +++ b/comm/calendar/test/browser/browser_import.js @@ -0,0 +1,285 @@ +/* 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/. */ + +// This tests importing an ICS file. Rather than using the UI to trigger the +// import, loadEventsFromFile is called directly. + +/* globals loadEventsFromFile */ + +const { MockFilePicker } = ChromeUtils.importESModule( + "resource://testing-common/MockFilePicker.sys.mjs" +); +const ChromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIChromeRegistry); + +add_task(async () => { + await CalendarTestUtils.setCalendarView(window, "month"); + await CalendarTestUtils.goToDate(window, 2019, 1, 1); + + let chromeUrl = Services.io.newURI(getRootDirectory(gTestPath) + "data/import.ics"); + let fileUrl = ChromeRegistry.convertChromeURL(chromeUrl); + let file = fileUrl.QueryInterface(Ci.nsIFileURL).file; + + MockFilePicker.init(window); + MockFilePicker.setFiles([file]); + MockFilePicker.returnValue = MockFilePicker.returnCancel; + + let calendar = CalendarTestUtils.createCalendar(); + + registerCleanupFunction(() => { + CalendarTestUtils.removeCalendar(calendar); + MockFilePicker.cleanup(); + }); + + let cancelReturn = await loadEventsFromFile(); + ok(!cancelReturn, "loadEventsFromFile returns false on cancel"); + + // Prepare to test the import dialog. + MockFilePicker.returnValue = MockFilePicker.returnOK; + + let dialogWindowPromise = BrowserTestUtils.promiseAlertDialog( + null, + "chrome://calendar/content/calendar-ics-file-dialog.xhtml", + { + async callback(dialogWindow) { + let doc = dialogWindow.document; + let dialogElement = doc.querySelector("dialog"); + + let optionsPane = doc.getElementById("calendar-ics-file-dialog-options-pane"); + let progressPane = doc.getElementById("calendar-ics-file-dialog-progress-pane"); + let resultPane = doc.getElementById("calendar-ics-file-dialog-result-pane"); + + ok(!optionsPane.hidden); + ok(progressPane.hidden); + ok(resultPane.hidden); + + // Check the initial import dialog state. + let displayedPath = doc.querySelector("#calendar-ics-file-dialog-file-path").value; + let pathFragment = "browser/comm/calendar/test/browser/data/import.ics"; + if (Services.appinfo.OS == "WINNT") { + pathFragment = pathFragment.replace(/\//g, "\\"); + } + is( + displayedPath.substring(displayedPath.length - pathFragment.length), + pathFragment, + "the displayed ics file path is correct" + ); + + let calendarMenu = doc.querySelector("#calendar-ics-file-dialog-calendar-menu"); + // 0 is the Home calendar. + calendarMenu.selectedIndex = 1; + let calendarMenuItems = calendarMenu.querySelectorAll("menuitem"); + is(calendarMenu.value, "Test", "correct calendar name is selected"); + Assert.equal(calendarMenuItems.length, 1, "exactly one calendar is in the calendars menu"); + is(calendarMenuItems[0].selected, true, "calendar menu item is selected"); + + let items; + await TestUtils.waitForCondition(() => { + items = doc.querySelectorAll(".calendar-ics-file-dialog-item-frame"); + return items.length == 4; + }, "four calendar items are displayed"); + is( + items[0].querySelector(".item-title").textContent, + "Event One", + "event 1 title should be correct" + ); + is( + items[1].querySelector(".item-title").textContent, + "Event Two", + "event 2 title should be correct" + ); + is( + items[2].querySelector(".item-title").textContent, + "Event Three", + "event 3 title should be correct" + ); + is( + items[3].querySelector(".item-title").textContent, + "Event Four", + "event 4 title should be correct" + ); + is( + items[0].querySelector(".item-date-row-start-date").textContent, + cal.dtz.formatter.formatDateTime(cal.createDateTime("20190101T150000")), + "event 1 start date should be correct" + ); + is( + items[0].querySelector(".item-date-row-end-date").textContent, + cal.dtz.formatter.formatDateTime(cal.createDateTime("20190101T160000")), + "event 1 end date should be correct" + ); + is( + items[1].querySelector(".item-date-row-start-date").textContent, + cal.dtz.formatter.formatDateTime(cal.createDateTime("20190101T160000")), + "event 2 start date should be correct" + ); + is( + items[1].querySelector(".item-date-row-end-date").textContent, + cal.dtz.formatter.formatDateTime(cal.createDateTime("20190101T170000")), + "event 2 end date should be correct" + ); + is( + items[2].querySelector(".item-date-row-start-date").textContent, + cal.dtz.formatter.formatDateTime(cal.createDateTime("20190101T170000")), + "event 3 start date should be correct" + ); + is( + items[2].querySelector(".item-date-row-end-date").textContent, + cal.dtz.formatter.formatDateTime(cal.createDateTime("20190101T180000")), + "event 3 end date should be correct" + ); + is( + items[3].querySelector(".item-date-row-start-date").textContent, + cal.dtz.formatter.formatDateTime(cal.createDateTime("20190101T180000")), + "event 4 start date should be correct" + ); + is( + items[3].querySelector(".item-date-row-end-date").textContent, + cal.dtz.formatter.formatDateTime(cal.createDateTime("20190101T190000")), + "event 4 end date should be correct" + ); + + function check_displayed_titles(expectedTitles) { + let items = doc.querySelectorAll( + ".calendar-ics-file-dialog-item-frame:not([hidden]) > calendar-item-summary" + ); + Assert.deepEqual( + [...items].map(summary => summary.item.title), + expectedTitles + ); + } + + let filterInput = doc.getElementById("calendar-ics-file-dialog-search-input"); + async function check_filter(filterText, expectedTitles) { + let commandPromise = BrowserTestUtils.waitForEvent(filterInput, "command"); + + EventUtils.synthesizeMouseAtCenter(filterInput, {}, dialogWindow); + if (filterText) { + EventUtils.synthesizeKey("a", { accelKey: true }, dialogWindow); + EventUtils.sendString(filterText, dialogWindow); + } else { + EventUtils.synthesizeKey("VK_ESCAPE", {}, dialogWindow); + } + + await commandPromise; + + check_displayed_titles(expectedTitles); + } + + await check_filter("event", ["Event One", "Event Two", "Event Three", "Event Four"]); + await check_filter("four", ["Event Four"]); + await check_filter("ONE", ["Event One"]); + await check_filter(`"event t"`, ["Event Two", "Event Three"]); + await check_filter("", ["Event One", "Event Two", "Event Three", "Event Four"]); + + async function check_sort(order, expectedTitles) { + let sortButton = doc.getElementById("calendar-ics-file-dialog-sort-button"); + let shownPromise = BrowserTestUtils.waitForEvent(sortButton, "popupshown"); + EventUtils.synthesizeMouseAtCenter(sortButton, {}, dialogWindow); + await shownPromise; + let hiddenPromise = BrowserTestUtils.waitForEvent(sortButton, "popuphidden"); + EventUtils.synthesizeMouseAtCenter( + doc.getElementById(`calendar-ics-file-dialog-sort-${order}`), + {}, + dialogWindow + ); + await hiddenPromise; + + let items = doc.querySelectorAll("calendar-item-summary"); + is(items.length, 4, "four calendar items are displayed"); + Assert.deepEqual( + [...items].map(summary => summary.item.title), + expectedTitles + ); + } + + await check_sort("title-ascending", [ + "Event Four", + "Event One", + "Event Three", + "Event Two", + ]); + await check_sort("start-descending", [ + "Event Four", + "Event Three", + "Event Two", + "Event One", + ]); + await check_sort("title-descending", [ + "Event Two", + "Event Three", + "Event One", + "Event Four", + ]); + await check_sort("start-ascending", [ + "Event One", + "Event Two", + "Event Three", + "Event Four", + ]); + + items = doc.querySelectorAll(".calendar-ics-file-dialog-item-frame"); + + // Import just the first item, and check that the correct number of items remains. + let firstItemImportButton = items[0].querySelector( + ".calendar-ics-file-dialog-item-import-button" + ); + EventUtils.synthesizeMouseAtCenter(firstItemImportButton, { clickCount: 1 }, dialogWindow); + + await TestUtils.waitForCondition(() => { + let remainingItems = doc.querySelectorAll(".calendar-ics-file-dialog-item-frame"); + return remainingItems.length == 3; + }, "three items remain after importing the first item"); + check_displayed_titles(["Event Two", "Event Three", "Event Four"]); + + // Filter and import the shown items. + await check_filter("four", ["Event Four"]); + + dialogElement.getButton("accept").click(); + ok(optionsPane.hidden); + ok(!progressPane.hidden); + ok(resultPane.hidden); + + await TestUtils.waitForCondition(() => !optionsPane.hidden); + ok(progressPane.hidden); + ok(resultPane.hidden); + + is(filterInput.value, ""); + check_displayed_titles(["Event Two", "Event Three"]); + + // Click the accept button to import the remaining items. + dialogElement.getButton("accept").click(); + ok(optionsPane.hidden); + ok(!progressPane.hidden); + ok(resultPane.hidden); + + await TestUtils.waitForCondition(() => !resultPane.hidden); + ok(optionsPane.hidden); + ok(progressPane.hidden); + + let messageElement = doc.querySelector("#calendar-ics-file-dialog-result-message"); + is(messageElement.textContent, "Import complete.", "import success message appeared"); + + dialogElement.getButton("accept").click(); + }, + } + ); + + await loadEventsFromFile(); + await dialogWindowPromise; + + // Check that the items were actually successfully imported. + let result = await calendar.getItemsAsArray( + Ci.calICalendar.ITEM_FILTER_ALL_ITEMS, + 0, + cal.createDateTime("20190101T000000"), + cal.createDateTime("20190102T000000") + ); + is(result.length, 4, "all items that were imported were in fact imported"); + + await CalendarTestUtils.monthView.waitForItemAt(window, 1, 3, 4); + + for (let item of result) { + await calendar.deleteItem(item); + } +}); diff --git a/comm/calendar/test/browser/browser_localICS.js b/comm/calendar/test/browser/browser_localICS.js new file mode 100644 index 0000000000..43e0299937 --- /dev/null +++ b/comm/calendar/test/browser/browser_localICS.js @@ -0,0 +1,63 @@ +/* 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 createCalendarUsingDialog */ + +var { saveAndCloseItemDialog, setData } = ChromeUtils.import( + "resource://testing-common/calendar/ItemEditingHelpers.jsm" +); + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); + +const HOUR = 8; + +// Unique name needed as deleting a calendar only unsubscribes from it and +// if same file were used on next testrun then previously created event +// would show up. +var calendarName = String(Date.now()); +var calendarFile = Services.dirsvc.get("TmpD", Ci.nsIFile); +calendarFile.append(calendarName + ".ics"); + +add_task(async function testLocalICS() { + await CalendarTestUtils.setCalendarView(window, "day"); + await createCalendarUsingDialog(calendarName, { network: {} }); + + // Create new event. + let box = CalendarTestUtils.dayView.getHourBoxAt(window, HOUR); + let { dialogWindow, iframeWindow } = await CalendarTestUtils.editNewEvent(window, box); + await setData(dialogWindow, iframeWindow, { title: calendarName, calendar: calendarName }); + await saveAndCloseItemDialog(dialogWindow); + + // Assert presence in view. + await CalendarTestUtils.dayView.waitForEventBoxAt(window, 1); + + // Verify in file. + let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + let cstream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance( + Ci.nsIConverterInputStream + ); + + // Wait a moment until file is written. + await TestUtils.waitForCondition(() => calendarFile.exists()); + + // Read the calendar file and check for the summary. + fstream.init(calendarFile, -1, 0, 0); + cstream.init(fstream, "UTF-8", 0, 0); + + let str = {}; + cstream.readString(-1, str); + cstream.close(); + + Assert.ok(str.value.includes("SUMMARY:" + calendarName)); +}); + +registerCleanupFunction(() => { + for (let calendar of cal.manager.getCalendars()) { + if (calendar.name == calendarName) { + cal.manager.removeCalendar(calendar); + } + } +}); diff --git a/comm/calendar/test/browser/browser_tabs.js b/comm/calendar/test/browser/browser_tabs.js new file mode 100644 index 0000000000..950b98a68c --- /dev/null +++ b/comm/calendar/test/browser/browser_tabs.js @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +add_task(async () => { + // Test the calendar tab opens and closes. + await CalendarTestUtils.openCalendarTab(window); + await CalendarTestUtils.closeCalendarTab(window); + + // Test the tasks tab opens and closes. + await openTasksTab(); + await closeTasksTab(); + + // Test the calendar and tasks tabs at the same time. + await CalendarTestUtils.openCalendarTab(window); + await openTasksTab(); + await CalendarTestUtils.closeCalendarTab(window); + await closeTasksTab(); + + // Test calendar view selection. + await CalendarTestUtils.setCalendarView(window, "day"); + await CalendarTestUtils.setCalendarView(window, "week"); + await CalendarTestUtils.setCalendarView(window, "multiweek"); + await CalendarTestUtils.setCalendarView(window, "month"); + await CalendarTestUtils.closeCalendarTab(window); +}); diff --git a/comm/calendar/test/browser/browser_taskDelete.js b/comm/calendar/test/browser/browser_taskDelete.js new file mode 100644 index 0000000000..dcbf7ab057 --- /dev/null +++ b/comm/calendar/test/browser/browser_taskDelete.js @@ -0,0 +1,185 @@ +/* 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 deleting tasks in the task view. + */ +const { mailTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/MailTestUtils.jsm" +); +const { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetters(this, { + CalRecurrenceInfo: "resource:///modules/CalRecurrenceInfo.jsm", + CalTodo: "resource:///modules/CalTodo.jsm", +}); + +let calendar = CalendarTestUtils.createCalendar("Task Delete Test", "memory"); +registerCleanupFunction(() => CalendarTestUtils.removeCalendar(calendar)); + +/** + * Test ensures its possible to delete a task in the task view. Creates two task + * and deletes one. + */ +add_task(async function testTaskDeletion() { + let task1 = new CalTodo(); + task1.id = "1"; + task1.title = "Task 1"; + task1.entryDate = cal.createDateTime("20210126T000001Z"); + + let task2 = new CalTodo(); + task2.id = "2"; + task2.title = "Task 2"; + task2.entryDate = cal.createDateTime("20210127T000001Z"); + + await calendar.addItem(task1); + await calendar.addItem(task2); + await openTasksTab(); + + let tree = document.querySelector("#calendar-task-tree"); + let radio = document.querySelector("#opt_next7days_filter"); + let waitForRefresh = BrowserTestUtils.waitForEvent(tree, "refresh"); + EventUtils.synthesizeMouseAtCenter(radio, {}); + tree.refresh(); + + await waitForRefresh; + Assert.equal(tree.view.rowCount, 2, "2 tasks are displayed"); + + mailTestUtils.treeClick(EventUtils, window, tree, 0, 1, { clickCount: 1 }); + EventUtils.synthesizeKey("VK_DELETE"); + + // Try and trigger a reflow + tree.getBoundingClientRect(); + tree.invalidate(); + await new Promise(r => setTimeout(r)); + + await TestUtils.waitForCondition(() => { + tree = document.querySelector("#calendar-task-tree"); + return tree.view.rowCount == 1; + }, `task view displays ${tree.view.rowCount} tasks instead of 1`); + + let result = await calendar.getItem(task1.id); + Assert.ok(!result, "first task was deleted successfully"); + + result = await calendar.getItem(task2.id); + Assert.ok(result, "second task was not deleted"); + await calendar.deleteItem(task2); + await closeTasksTab(); +}); + +/** + * Test ensures it is possible to delete a recurring task from the task view. + * See bug 1688708. + */ +add_task(async function testRecurringTaskDeletion() { + let repeatTask = new CalTodo(); + repeatTask.id = "1"; + repeatTask.title = "Repeating Task"; + repeatTask.entryDate = cal.createDateTime("20210125T000001Z"); + repeatTask.recurrenceInfo = new CalRecurrenceInfo(repeatTask); + repeatTask.recurrenceInfo.appendRecurrenceItem( + cal.createRecurrenceRule("RRULE:FREQ=DAILY;COUNT=3") + ); + + let nonRepeatTask = new CalTodo(); + nonRepeatTask.id = "2"; + nonRepeatTask.title = "Non-Repeating Task"; + nonRepeatTask.entryDate = cal.createDateTime("20210126T000001Z"); + + repeatTask = await calendar.addItem(repeatTask); + nonRepeatTask = await calendar.addItem(nonRepeatTask); + + await openTasksTab(); + + let tree = document.querySelector("#calendar-task-tree"); + let radio = document.querySelector("#opt_next7days_filter"); + let waitForRefresh = BrowserTestUtils.waitForEvent(tree, "refresh"); + EventUtils.synthesizeMouseAtCenter(radio, {}); + tree.refresh(); + + await waitForRefresh; + Assert.equal(tree.view.rowCount, 4, "4 tasks are displayed"); + + // Delete a single occurrence. + let handleSingleDelete = BrowserTestUtils.promiseAlertDialog( + null, + "chrome://calendar/content/calendar-occurrence-prompt.xhtml", + { + async callback(win) { + let dialog = win.document.querySelector("dialog"); + let button = dialog.querySelector("#accept-occurrence-button"); + EventUtils.synthesizeMouseAtCenter(button, {}, win); + }, + } + ); + mailTestUtils.treeClick(EventUtils, window, tree, 1, 1, { clickCount: 1 }); + EventUtils.synthesizeKey("VK_DELETE"); + await handleSingleDelete; + + // Try and trigger a reflow + tree.getBoundingClientRect(); + tree.invalidate(); + await new Promise(r => setTimeout(r)); + + await TestUtils.waitForCondition(() => { + tree = document.querySelector("#calendar-task-tree"); + return tree.view.rowCount == 3; + }, `task view displays ${tree.view.rowCount} tasks instead of 3`); + + repeatTask = await calendar.getItem(repeatTask.id); + + Assert.equal( + repeatTask.recurrenceInfo.getOccurrences( + cal.createDateTime("20210126T000001Z"), + cal.createDateTime("20210126T000001Z"), + 10 + ).length, + 0, + "a single occurrence was deleted successfully" + ); + + Assert.equal( + repeatTask.recurrenceInfo.getOccurrences( + repeatTask.entryDate, + cal.createDateTime("20210131T000001Z"), + 10 + ).length, + 2, + "other occurrences were not removed" + ); + + // Delete all occurrences + let handleAllDelete = BrowserTestUtils.promiseAlertDialog( + null, + "chrome://calendar/content/calendar-occurrence-prompt.xhtml", + { + async callback(win) { + let dialog = win.document.querySelector("dialog"); + let button = dialog.querySelector("#accept-parent-button"); + EventUtils.synthesizeMouseAtCenter(button, {}, win); + }, + } + ); + + mailTestUtils.treeClick(EventUtils, window, tree, 1, 1, { clickCount: 1 }); + EventUtils.synthesizeKey("VK_DELETE"); + await handleAllDelete; + + // Try and trigger a reflow + tree.getBoundingClientRect(); + tree.invalidate(); + await new Promise(r => setTimeout(r)); + + await TestUtils.waitForCondition(() => { + tree = document.querySelector("#calendar-task-tree"); + return tree.view.rowCount == 1; + }, `task view displays ${tree.view.rowCount} tasks instead of 1`); + + repeatTask = await calendar.getItem(repeatTask.id); + Assert.ok(!repeatTask, "all occurrences were removed"); + + let result = await calendar.getItem(nonRepeatTask.id); + Assert.ok(result, "non-recurring task was not deleted"); + await closeTasksTab(); +}); diff --git a/comm/calendar/test/browser/browser_taskDisplay.js b/comm/calendar/test/browser/browser_taskDisplay.js new file mode 100644 index 0000000000..1ccd0dac3f --- /dev/null +++ b/comm/calendar/test/browser/browser_taskDisplay.js @@ -0,0 +1,274 @@ +/* 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/. */ + +XPCOMUtils.defineLazyModuleGetters(this, { + CalRecurrenceInfo: "resource:///modules/CalRecurrenceInfo.jsm", + CalTodo: "resource:///modules/CalTodo.jsm", +}); + +var calendar = CalendarTestUtils.createCalendar(); +registerCleanupFunction(() => { + CalendarTestUtils.removeCalendar(calendar); +}); + +let tree = document.getElementById("calendar-task-tree"); + +add_task(async () => { + async function createTask(title, attributes = {}) { + let task = new CalTodo(); + task.title = title; + for (let [key, value] of Object.entries(attributes)) { + task[key] = value; + } + return calendar.addItem(task); + } + + function treeRefresh() { + return BrowserTestUtils.waitForEvent(tree, "refresh"); + } + + async function setFilterGroup(name) { + info(`Setting filter to ${name}`); + let radio = document.getElementById(`opt_${name}_filter`); + EventUtils.synthesizeMouseAtCenter(radio, {}); + await treeRefresh(); + Assert.equal( + document.getElementById("calendar-task-tree").getAttribute("filterValue"), + radio.value, + "Filter group changed" + ); + } + + async function setFilterText(text) { + EventUtils.synthesizeMouseAtCenter(document.getElementById("task-text-filter-field"), {}); + EventUtils.sendString(text); + Assert.equal(document.getElementById("task-text-filter-field").value, text, "Filter text set"); + await treeRefresh(); + } + + async function clearFilterText() { + EventUtils.synthesizeMouseAtCenter(document.getElementById("task-text-filter-field"), {}); + EventUtils.synthesizeKey("VK_ESCAPE"); + Assert.equal( + document.getElementById("task-text-filter-field").value, + "", + "Filter text cleared" + ); + await treeRefresh(); + } + + async function checkVisibleTasks(...expectedTasks) { + function toPrettyString(task) { + if (task.recurrenceId) { + return `${task.title}#${task.recurrenceId}`; + } + return task.title; + } + tree.getBoundingClientRect(); // Try and trigger a reflow... + tree.invalidate(); + + // It seems that under certain conditions notifyOperationComplete() is + // called in CalStorageCalender.getItems before all the results have been + // retrieved. This results in the "refresh" event being fired prematurely in + // calendar-task-tree. After some investigation, the cause of this seems to + // be related to multiple calls of executeAsync() in CalStorageItemModel. + // getAdditionalDataForItemMap() not finishing before notifyOperationComplete() + // is called despite being awaited on. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, 500)); + + let actualTasks = []; + for (let i = 0; i < tree.view.rowCount; i++) { + actualTasks.push(tree.getTaskAtRow(i)); + } + info("Expected: " + expectedTasks.map(toPrettyString).join(", ")); + info("Actual: " + actualTasks.map(toPrettyString).join(", ")); + + Assert.equal(tree.view.rowCount, expectedTasks.length, "Correct number of tasks"); + await new Promise(r => setTimeout(r)); + + // Although the order of expectedTasks matches the observed behaviour when + // this test was written, order is NOT checked here. The order of the list + // is not well defined (particularly when changing the filter text). + for (let aTask of actualTasks) { + Assert.ok( + expectedTasks.some(eTask => eTask.hasSameIds(aTask)), + toPrettyString(aTask) + ); + } + } + + let today = cal.dtz.now(); + today.hour = today.minute = today.second = 0; + let yesterday = today.clone(); + yesterday.addDuration(cal.createDuration("-P1D")); + let tomorrow = today.clone(); + tomorrow.addDuration(cal.createDuration("P1D")); + let later = today.clone(); + later.addDuration(cal.createDuration("P2W")); + + let tasks = { + incomplete: await createTask("Incomplete"), + started30: await createTask("30% started", { percentComplete: 30 }), + started60: await createTask("60% started", { percentComplete: 60 }), + complete: await createTask("Complete", { isCompleted: true }), + overdue: await createTask("Overdue", { dueDate: yesterday }), + startsToday: await createTask("Starts today", { entryDate: today }), + startsTomorrow: await createTask("Starts tomorrow", { entryDate: tomorrow }), + startsLater: await createTask("Starts later", { entryDate: later }), + }; + + let repeatingTask = new CalTodo(); + repeatingTask.title = "Repeating"; + repeatingTask.entryDate = yesterday; + repeatingTask.recurrenceInfo = new CalRecurrenceInfo(repeatingTask); + repeatingTask.recurrenceInfo.appendRecurrenceItem( + cal.createRecurrenceRule("RRULE:FREQ=DAILY;COUNT=3") + ); + + let firstOccurrence = repeatingTask.recurrenceInfo.getOccurrenceFor(yesterday); + firstOccurrence.isCompleted = true; + firstOccurrence.completedDate = yesterday; + repeatingTask.recurrenceInfo.modifyException(firstOccurrence, true); + + repeatingTask = await calendar.addItem(repeatingTask); + + let occurrences = repeatingTask.recurrenceInfo.getOccurrences(yesterday, later, 10); + Assert.equal(occurrences.length, 3); + + await openTasksTab(); + + await setFilterGroup("all"); + await checkVisibleTasks( + tasks.incomplete, + tasks.started30, + tasks.started60, + tasks.complete, + tasks.overdue, + tasks.startsToday, + tasks.startsTomorrow, + tasks.startsLater, + repeatingTask + ); + + await setFilterGroup("open"); + await checkVisibleTasks( + tasks.incomplete, + tasks.started30, + tasks.started60, + tasks.overdue, + tasks.startsToday, + tasks.startsTomorrow, + tasks.startsLater, + occurrences[1], + occurrences[2] + ); + + await setFilterGroup("completed"); + await checkVisibleTasks(tasks.complete, occurrences[0]); + + await setFilterGroup("overdue"); + await checkVisibleTasks(tasks.overdue); + + await setFilterGroup("notstarted"); + await checkVisibleTasks(tasks.overdue, tasks.incomplete, tasks.startsToday, occurrences[1]); + + await setFilterGroup("next7days"); + await checkVisibleTasks( + tasks.overdue, + tasks.incomplete, + tasks.startsToday, + tasks.started30, + tasks.started60, + tasks.complete, + tasks.startsTomorrow, + occurrences[1], + occurrences[2] + ); + + await setFilterGroup("today"); + await checkVisibleTasks( + tasks.overdue, + tasks.incomplete, + tasks.startsToday, + tasks.started30, + tasks.started60, + tasks.complete, + occurrences[1] + ); + + await setFilterGroup("throughcurrent"); + await checkVisibleTasks( + tasks.overdue, + tasks.incomplete, + tasks.startsToday, + tasks.started30, + tasks.started60, + tasks.complete, + occurrences[1] + ); + + await setFilterText("No matches"); + await checkVisibleTasks(); + + await clearFilterText(); + await checkVisibleTasks( + tasks.incomplete, + tasks.started30, + tasks.started60, + tasks.complete, + tasks.overdue, + tasks.startsToday, + occurrences[1] + ); + + await setFilterText("StArTeD"); + await checkVisibleTasks(tasks.started30, tasks.started60); + + await setFilterGroup("today"); + Assert.equal(document.getElementById("task-text-filter-field").value, "StArTeD"); + await checkVisibleTasks(tasks.started30, tasks.started60); + + await setFilterGroup("next7days"); + Assert.equal(document.getElementById("task-text-filter-field").value, "StArTeD"); + await checkVisibleTasks(tasks.started30, tasks.started60); + + await setFilterGroup("notstarted"); + Assert.equal(document.getElementById("task-text-filter-field").value, "StArTeD"); + await checkVisibleTasks(); + + await setFilterGroup("overdue"); + Assert.equal(document.getElementById("task-text-filter-field").value, "StArTeD"); + await checkVisibleTasks(); + + await setFilterGroup("completed"); + Assert.equal(document.getElementById("task-text-filter-field").value, "StArTeD"); + await checkVisibleTasks(); + + await setFilterGroup("open"); + Assert.equal(document.getElementById("task-text-filter-field").value, "StArTeD"); + await checkVisibleTasks(tasks.started30, tasks.started60); + + await setFilterGroup("all"); + Assert.equal(document.getElementById("task-text-filter-field").value, "StArTeD"); + await checkVisibleTasks(tasks.started30, tasks.started60); + + await clearFilterText(); + await checkVisibleTasks( + tasks.started30, + tasks.started60, + tasks.incomplete, + tasks.complete, + tasks.overdue, + tasks.startsToday, + tasks.startsTomorrow, + tasks.startsLater, + repeatingTask + ); + + for (let task of Object.values(tasks)) { + await calendar.deleteItem(task); + } + await setFilterGroup("throughcurrent"); +}); diff --git a/comm/calendar/test/browser/browser_taskUndoRedo.js b/comm/calendar/test/browser/browser_taskUndoRedo.js new file mode 100644 index 0000000000..09cf9a8de2 --- /dev/null +++ b/comm/calendar/test/browser/browser_taskUndoRedo.js @@ -0,0 +1,244 @@ +/* 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/. */ + +"use strict"; + +/** + * Tests for ensuring the undo/redo options are enabled properly when + * manipulating tasks. + */ + +var { mailTestUtils } = ChromeUtils.import("resource://testing-common/mailnews/MailTestUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetters(this, { + CalTodo: "resource:///modules/CalTodo.jsm", + CalTransactionManager: "resource:///modules/CalTransactionManager.jsm", +}); + +const calendar = CalendarTestUtils.createCalendar("Undo Redo Test", "memory"); +const calTransManager = CalTransactionManager.getInstance(); + +/** + * Checks the value of the "disabled" property for items in either the "Edit" + * menu bar or the app menu. Display of the relevant menu is triggered first so + * the UI code can update the respective items. + * + * @param {XULElement} element - The menu item we want to check, if its id begins + * with "menu" then we assume it is in the menu + * bar, if "appmenu" then the app menu. + */ +async function isDisabled(element) { + let targetMenu = document.getElementById("menu_EditPopup"); + + let shownPromise = BrowserTestUtils.waitForEvent(targetMenu, "popupshown"); + EventUtils.synthesizeMouseAtCenter(document.getElementById("menu_Edit"), {}); + await shownPromise; + + let hiddenPromise = BrowserTestUtils.waitForEvent(targetMenu, "popuphidden"); + let status = element.disabled; + EventUtils.synthesizeKey("VK_ESCAPE"); + await hiddenPromise; + return status; +} + +/** + * Removes CalTransaction items from the CalTransactionManager stacks so other + * tests are unhindered. + */ +function clearTransactions() { + calTransManager.undoStack = []; + calTransManager.redoStack = []; +} + +/** + * Test the undo/redo functionality for task creation. + * + * @param {string} undoId - The id of the "undo" menu item. + * @param {string} redoId - The id of the "redo" menu item. + */ +async function taskAddUndoRedoTask(undoId, redoId) { + let undo = document.getElementById(undoId); + let redo = document.getElementById(redoId); + Assert.ok(await isDisabled(undo), `#${undoId} is disabled`); + Assert.ok(await isDisabled(redo), `#${redoId} is disabled`); + + let newBtn = document.getElementById("sidePanelNewTask"); + let windowPromise = CalendarTestUtils.waitForEventDialog("edit"); + EventUtils.synthesizeMouseAtCenter(newBtn, {}); + + let win = await windowPromise; + let iframeWin = win.document.getElementById("calendar-item-panel-iframe").contentWindow; + await CalendarTestUtils.items.setData(win, iframeWin, { title: "New Task" }); + await CalendarTestUtils.items.saveAndCloseItemDialog(win); + + let tree = document.querySelector("#calendar-task-tree"); + let refreshPromise = BrowserTestUtils.waitForEvent(tree, "refresh"); + tree.refresh(); + await refreshPromise; + + Assert.equal(tree.view.rowCount, 1); + Assert.ok(!(await isDisabled(undo)), `#${undoId} is enabled`); + Assert.ok(await isDisabled(redo), `#${redoId} is disabled`); + + // Test undo. + undo.doCommand(); + await TestUtils.waitForCondition( + () => tree.view.rowCount == 0, + `${undoId} did not remove task in time` + ); + Assert.equal(tree.view.rowCount, 0, `#${undoId} reverses task creation`); + + // Test redo. + redo.doCommand(); + await TestUtils.waitForCondition( + () => tree.view.rowCount == 1, + `${redoId} did not re-create task in time` + ); + + let task = tree.getTaskAtRow(0); + Assert.equal(task.title, "New Task", `#${redoId} redos task creation`); + await calendar.deleteItem(task); + clearTransactions(); +} + +/** + * Test the undo/redo functionality for task modification. + * + * @param {string} undoId - The id of the "undo" menu item. + * @param {string} redoId - The id of the "redo" menu item. + */ +async function testModifyUndoRedoTask(undoId, redoId) { + let undo = document.getElementById(undoId); + let redo = document.getElementById(redoId); + Assert.ok(await isDisabled(undo), `#${undoId} is disabled`); + Assert.ok(await isDisabled(redo), `#${redoId} is disabled`); + + let task = new CalTodo(); + task.title = "Modifiable Task"; + task.entryDate = cal.dtz.now(); + await calendar.addItem(task); + + let tree = document.querySelector("#calendar-task-tree"); + let refreshPromise = BrowserTestUtils.waitForEvent(tree, "refresh"); + tree.refresh(); + await refreshPromise; + + let windowPromise = CalendarTestUtils.waitForEventDialog("edit"); + mailTestUtils.treeClick(EventUtils, window, tree, 0, 1, { clickCount: 2 }); + + let win = await windowPromise; + let iframeWin = win.document.getElementById("calendar-item-panel-iframe").contentWindow; + await CalendarTestUtils.items.setData(win, iframeWin, { title: "Modified Task" }); + await CalendarTestUtils.items.saveAndCloseItemDialog(win); + + Assert.equal(tree.getTaskAtRow(0).title, "Modified Task"); + Assert.ok(!(await isDisabled(undo)), `#${undoId} is enabled`); + Assert.ok(await isDisabled(redo), `#${redoId} is disabled`); + + // Test undo. + undo.doCommand(); + refreshPromise = BrowserTestUtils.waitForEvent(tree, "refresh"); + tree.refresh(); + await refreshPromise; + Assert.equal( + tree.getTaskAtRow(0).title, + "Modifiable Task", + `#${undoId} reverses task modification` + ); + + // Test redo. + redo.doCommand(); + refreshPromise = BrowserTestUtils.waitForEvent(tree, "refresh"); + tree.refresh(); + await refreshPromise; + Assert.equal(tree.getTaskAtRow(0).title, "Modified Task", `#${redoId} redos task modification`); + + clearTransactions(); + await calendar.deleteItem(tree.getTaskAtRow(0)); +} + +/** + * Test the undo/redo functionality for task deletion. + * + * @param {string} undoId - The id of the "undo" menu item. + * @param {string} redoId - The id of the "redo" menu item. + */ +async function testDeleteUndoRedoTask(undoId, redoId) { + let undo = document.getElementById(undoId); + let redo = document.getElementById(redoId); + Assert.ok(await isDisabled(undo), `#${undoId} is disabled`); + Assert.ok(await isDisabled(redo), `#${redoId} is disabled`); + + let task = new CalTodo(); + task.title = "Deletable Task"; + task.startDate = cal.dtz.now(); + task.entryDate = cal.dtz.now(); + await calendar.addItem(task); + + let tree = document.querySelector("#calendar-task-tree"); + let refreshPromise = BrowserTestUtils.waitForEvent(tree, "refresh"); + tree.refresh(); + await refreshPromise; + Assert.equal(tree.view.rowCount, 1); + + mailTestUtils.treeClick(EventUtils, window, tree, 0, 1, { clickCount: 1 }); + EventUtils.synthesizeKey("VK_DELETE"); + await TestUtils.waitForCondition(() => tree.view.rowCount == 0, "task was not removed in time"); + + Assert.ok(!(await isDisabled(undo)), `#${undoId} is enabled`); + Assert.ok(await isDisabled(redo), `#${redoId} is disabled`); + + // Test undo. + undo.doCommand(); + tree.refresh(); + await TestUtils.waitForCondition( + () => tree.view.rowCount == 1, + "undo did not restore task in time" + ); + Assert.equal(tree.getTaskAtRow(0).title, "Deletable Task", `#${undoId} reverses item deletion`); + + // Test redo. + redo.doCommand(); + await TestUtils.waitForCondition( + () => tree.view.rowCount == 0, + `#${redoId} redo did not delete item in time` + ); + Assert.ok(!tree.getTaskAtRow(0), `#${redoId} redos item deletion`); + + clearTransactions(); +} + +/** + * Ensure the menu bar is visible and navigate to the task view. + */ +add_setup(async function () { + registerCleanupFunction(() => { + CalendarTestUtils.removeCalendar(calendar); + }); + + clearTransactions(); + document.getElementById("toolbar-menubar").setAttribute("autohide", null); + await openTasksTab(); +}); + +/** + * Tests the menu bar's undo/redo after adding an event. + */ +add_task(async function testMenuBarAddTaskUndoRedo() { + return taskAddUndoRedoTask("menu_undo", "menu_redo"); +}).__skipMe = AppConstants.platform == "macosx"; // Can't click menu bar on Mac. + +/** + * Tests the menu bar's undo/redo after modifying an event. + */ +add_task(async function testMenuBarModifyTaskUndoRedo() { + return testModifyUndoRedoTask("menu_undo", "menu_redo"); +}).__skipMe = AppConstants.platform == "macosx"; // Can't click menu bar on Mac. + +/** + * Tests the menu bar's undo/redo after deleting an event. + */ +add_task(async function testMenuBarDeleteTaskUndoRedo() { + return testDeleteUndoRedoTask("menu_undo", "menu_redo"); +}).__skipMe = AppConstants.platform == "macosx"; // Can't click menu bar on Mac. diff --git a/comm/calendar/test/browser/browser_todayPane.js b/comm/calendar/test/browser/browser_todayPane.js new file mode 100644 index 0000000000..8ad9141815 --- /dev/null +++ b/comm/calendar/test/browser/browser_todayPane.js @@ -0,0 +1,820 @@ +/* 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 TodayPane */ + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); +var { formatDate, formatTime } = ChromeUtils.import( + "resource://testing-common/calendar/ItemEditingHelpers.jsm" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + CalDateTime: "resource:///modules/CalDateTime.jsm", + CalEvent: "resource:///modules/CalEvent.jsm", + CalRecurrenceInfo: "resource:///modules/CalRecurrenceInfo.jsm", +}); + +let calendar = CalendarTestUtils.createCalendar(); +Services.prefs.setIntPref("calendar.agenda.days", 7); +registerCleanupFunction(() => { + CalendarTestUtils.removeCalendar(calendar); + Services.prefs.clearUserPref("calendar.agenda.days"); +}); + +let today = cal.dtz.now(); +let startHour = today.hour; +today.hour = today.minute = today.second = 0; + +let todayPanePanel = document.getElementById("today-pane-panel"); +let todayPaneStatusButton = document.getElementById("calendar-status-todaypane-button"); + +// Go to mail tab. +selectFolderTab(); + +// Verify today pane open. +if (todayPanePanel.hasAttribute("collapsed")) { + EventUtils.synthesizeMouseAtCenter(todayPaneStatusButton, {}); +} +Assert.ok(!todayPanePanel.hasAttribute("collapsed"), "Today Pane is open"); + +// Verify today pane's date. +Assert.equal(document.getElementById("datevalue-label").value, today.day, "Today Pane shows today"); + +async function addEvent(title, relativeStart, relativeEnd, isAllDay) { + let event = new CalEvent(); + event.id = cal.getUUID(); + event.title = title; + event.startDate = today.clone(); + event.startDate.addDuration(cal.createDuration(relativeStart)); + event.startDate.isDate = isAllDay; + event.endDate = today.clone(); + event.endDate.addDuration(cal.createDuration(relativeEnd)); + event.endDate.isDate = isAllDay; + return calendar.addItem(event); +} + +function checkEvent(row, { dateHeader, time, title, relative, overlap, classes = [] }) { + let dateHeaderElement = row.querySelector(".agenda-date-header"); + if (dateHeader) { + Assert.ok(BrowserTestUtils.is_visible(dateHeaderElement), "date header is visible"); + if (dateHeader instanceof CalDateTime || dateHeader instanceof Ci.calIDateTime) { + dateHeader = cal.dtz.formatter.formatDateLongWithoutYear(dateHeader); + } + Assert.equal(dateHeaderElement.textContent, dateHeader, "date header has correct value"); + } else { + Assert.ok(BrowserTestUtils.is_hidden(dateHeaderElement), "date header is hidden"); + } + + let calendarElement = row.querySelector(".agenda-listitem-calendar"); + let timeElement = row.querySelector(".agenda-listitem-time"); + if (time) { + Assert.ok(BrowserTestUtils.is_visible(calendarElement), "calendar is visible"); + Assert.ok(BrowserTestUtils.is_visible(timeElement), "time is visible"); + if (time instanceof CalDateTime || time instanceof Ci.calIDateTime) { + time = cal.dtz.formatter.formatTime(time); + } + Assert.equal(timeElement.textContent, time, "time has correct value"); + } else if (time === "") { + Assert.ok(BrowserTestUtils.is_visible(calendarElement), "calendar is visible"); + Assert.ok(BrowserTestUtils.is_hidden(timeElement), "time is hidden"); + } else { + Assert.ok(BrowserTestUtils.is_hidden(calendarElement), "calendar is hidden"); + Assert.ok(BrowserTestUtils.is_hidden(timeElement), "time is hidden"); + } + + let titleElement = row.querySelector(".agenda-listitem-title"); + Assert.ok(BrowserTestUtils.is_visible(titleElement), "title is visible"); + Assert.equal(titleElement.textContent, title, "title has correct value"); + + let relativeElement = row.querySelector(".agenda-listitem-relative"); + if (Array.isArray(relative)) { + Assert.ok(BrowserTestUtils.is_visible(relativeElement), "relative time is visible"); + Assert.report( + !relative.includes(relativeElement.textContent), + relative, + relativeElement.textContent, + "relative time is correct", + "includes" + ); + } else if (relative !== undefined) { + Assert.ok(BrowserTestUtils.is_hidden(relativeElement), "relative time is hidden"); + } + + let overlapElement = row.querySelector(".agenda-listitem-overlap"); + if (overlap) { + Assert.ok(BrowserTestUtils.is_visible(overlapElement), "overlap is visible"); + Assert.equal( + overlapElement.src, + `chrome://messenger/skin/icons/new/event-${overlap}.svg`, + "overlap has correct image" + ); + Assert.equal( + overlapElement.dataset.l10nId, + `calendar-editable-item-multiday-event-icon-${overlap}`, + "overlap has correct alt text" + ); + } else { + Assert.ok(BrowserTestUtils.is_hidden(overlapElement), "overlap is hidden"); + } + + for (let className of classes) { + Assert.ok(row.classList.contains(className), `row has ${className} class`); + } +} + +function checkEvents(...expectedEvents) { + Assert.equal(TodayPane.agenda.rowCount, expectedEvents.length, "expected number of rows shown"); + for (let i = 0; i < expectedEvents.length; i++) { + Assert.ok(TodayPane.agenda.rows[i].getAttribute("is"), "agenda-listitem"); + checkEvent(TodayPane.agenda.rows[i], expectedEvents[i]); + } +} + +add_task(async function testBasicAllDay() { + let todaysEvent = await addEvent("Today's Event", "P0D", "P1D", true); + checkEvents({ dateHeader: "Today", title: "Today's Event" }); + + let tomorrowsEvent = await addEvent("Tomorrow's Event", "P1D", "P2D", true); + checkEvents( + { dateHeader: "Today", title: "Today's Event" }, + { dateHeader: "Tomorrow", title: "Tomorrow's Event" } + ); + + let events = []; + for (let i = 2; i < 7; i++) { + events.push(await addEvent(`Event ${i + 1}`, `P${i}D`, `P${i + 1}D`, true)); + checkEvents( + { dateHeader: "Today", title: "Today's Event" }, + { dateHeader: "Tomorrow", title: "Tomorrow's Event" }, + ...events.map(e => { + return { dateHeader: e.startDate, title: e.title }; + }) + ); + } + + await calendar.deleteItem(todaysEvent); + checkEvents( + { dateHeader: "Tomorrow", title: "Tomorrow's Event" }, + ...events.map(e => { + return { dateHeader: e.startDate, title: e.title }; + }) + ); + await calendar.deleteItem(tomorrowsEvent); + checkEvents( + ...events.map(e => { + return { dateHeader: e.startDate, title: e.title }; + }) + ); + + while (events.length) { + await calendar.deleteItem(events.shift()); + checkEvents( + ...events.map(e => { + return { dateHeader: e.startDate, title: e.title }; + }) + ); + } +}); + +add_task(async function testBasic() { + let time = today.clone(); + time.hour = 23; + + let todaysEvent = await addEvent("Today's Event", "P0DT23H", "P1D"); + checkEvents({ dateHeader: "Today", time, title: "Today's Event" }); + + let tomorrowsEvent = await addEvent("Tomorrow's Event", "P1DT23H", "P2D"); + checkEvents( + { dateHeader: "Today", time, title: "Today's Event" }, + { dateHeader: "Tomorrow", time, title: "Tomorrow's Event" } + ); + + let events = []; + for (let i = 2; i < 7; i++) { + events.push(await addEvent(`Event ${i + 1}`, `P${i}DT23H`, `P${i + 1}D`)); + checkEvents( + { dateHeader: "Today", time, title: "Today's Event" }, + { dateHeader: "Tomorrow", time, title: "Tomorrow's Event" }, + ...events.map(e => { + return { dateHeader: e.startDate, time, title: e.title }; + }) + ); + } + + await calendar.deleteItem(todaysEvent); + checkEvents( + { dateHeader: "Tomorrow", time, title: "Tomorrow's Event" }, + ...events.map(e => { + return { dateHeader: e.startDate, time, title: e.title }; + }) + ); + await calendar.deleteItem(tomorrowsEvent); + checkEvents( + ...events.map(e => { + return { dateHeader: e.startDate, time, title: e.title }; + }) + ); + + while (events.length) { + await calendar.deleteItem(events.shift()); + checkEvents( + ...events.map(e => { + return { dateHeader: e.startDate, time, title: e.title }; + }) + ); + } +}); + +/** + * Adds and removes events in a different order from which they occur. + * This checks that the events are inserted in the right place, and that the + * date header is shown/hidden appropriately. + */ +add_task(async function testSortOrder() { + let afternoonEvent = await addEvent("Afternoon Event", "P1DT13H", "P1DT17H"); + checkEvents({ + dateHeader: "Tomorrow", + time: afternoonEvent.startDate, + title: "Afternoon Event", + }); + + let morningEvent = await addEvent("Morning Event", "P1DT8H", "P1DT12H"); + checkEvents( + { dateHeader: "Tomorrow", time: morningEvent.startDate, title: "Morning Event" }, + { time: afternoonEvent.startDate, title: "Afternoon Event" } + ); + + let allDayEvent = await addEvent("All Day Event", "P1D", "P2D", true); + checkEvents( + { dateHeader: "Tomorrow", title: "All Day Event" }, + { time: morningEvent.startDate, title: "Morning Event" }, + { time: afternoonEvent.startDate, title: "Afternoon Event" } + ); + + let eveningEvent = await addEvent("Evening Event", "P1DT18H", "P1DT22H"); + checkEvents( + { dateHeader: "Tomorrow", title: "All Day Event" }, + { time: morningEvent.startDate, title: "Morning Event" }, + { time: afternoonEvent.startDate, title: "Afternoon Event" }, + { time: eveningEvent.startDate, title: "Evening Event" } + ); + + await calendar.deleteItem(afternoonEvent); + checkEvents( + { dateHeader: "Tomorrow", title: "All Day Event" }, + { time: morningEvent.startDate, title: "Morning Event" }, + { time: eveningEvent.startDate, title: "Evening Event" } + ); + + await calendar.deleteItem(morningEvent); + checkEvents( + { dateHeader: "Tomorrow", title: "All Day Event" }, + { time: eveningEvent.startDate, title: "Evening Event" } + ); + + await calendar.deleteItem(allDayEvent); + checkEvents({ + dateHeader: "Tomorrow", + time: eveningEvent.startDate, + title: "Evening Event", + }); + + await calendar.deleteItem(eveningEvent); + checkEvents(); +}); + +/** + * Check events that begin and end on different days inside the date range. + * All-day events are still sorted ahead of non-all-day events. + */ +add_task(async function testOverlapInside() { + let allDayEvent = await addEvent("All Day Event", "P0D", "P2D", true); + checkEvents( + { dateHeader: "Today", title: "All Day Event", overlap: "start" }, + { dateHeader: "Tomorrow", title: "All Day Event", overlap: "end" } + ); + + let timedEvent = await addEvent("Timed Event", "P1H", "P1D23H"); + checkEvents( + { dateHeader: "Today", title: "All Day Event", overlap: "start" }, + { time: timedEvent.startDate, title: "Timed Event", overlap: "start" }, + { dateHeader: "Tomorrow", title: "All Day Event", overlap: "end" }, + { time: timedEvent.endDate, title: "Timed Event", overlap: "end" } + ); + + await calendar.deleteItem(allDayEvent); + await calendar.deleteItem(timedEvent); +}); + +/** + * Check events that begin and end on different days and that end at midnight. + * The list item for the end of the event should be the last one on the day + * before the end midnight, and its time label should display "24:00". + */ +add_task(async function testOverlapEndAtMidnight() { + // Start with an event that begins outside the displayed dates. + + let timedEvent = await addEvent("Timed Event", "-P1D", "P1D"); + // Ends an hour before `timedEvent` to prove the ordering is correct. + let duringEvent = await addEvent("During Event", "P22H", "P23H"); + // Starts at the same time as `timedEvent` ends to prove the ordering is correct. + let nextEvent = await addEvent("Next Event", "P1D", "P2D", true); + + checkEvents( + { dateHeader: "Today", time: duringEvent.startDate, title: "During Event" }, + { + // Should show "24:00" as the time and end today. + time: cal.dtz.formatter.formatTime(timedEvent.endDate, true), + title: "Timed Event", + overlap: "end", + }, + { dateHeader: "Tomorrow", title: "Next Event" } + ); + + // Move the event fully into the displayed range. + + let timedClone = timedEvent.clone(); + timedClone.startDate.day += 2; + timedClone.endDate.day += 2; + await calendar.modifyItem(timedClone, timedEvent); + + let duringClone = duringEvent.clone(); + duringClone.startDate.day += 2; + duringClone.endDate.day += 2; + await calendar.modifyItem(duringClone, duringEvent); + + let nextClone = nextEvent.clone(); + nextClone.startDate.day += 2; + nextClone.endDate.day += 2; + await calendar.modifyItem(nextClone, nextEvent); + + let realEndDate = today.clone(); + realEndDate.day += 2; + checkEvents( + { + dateHeader: "Tomorrow", + time: timedClone.startDate, + title: "Timed Event", + overlap: "start", + }, + { dateHeader: realEndDate, time: duringClone.startDate, title: "During Event" }, + { + // Should show "24:00" as the time and end on the day after tomorrow. + time: cal.dtz.formatter.formatTime(timedClone.endDate, true), + title: "Timed Event", + overlap: "end", + }, + { dateHeader: nextClone.startDate, title: "Next Event" } + ); + + await calendar.deleteItem(timedClone); + await calendar.deleteItem(duringClone); + await calendar.deleteItem(nextClone); +}); + +/** + * Check events that begin and/or end outside the date range. Events that have + * already started are listed as "Today", but still sorted by start time. + * All-day events are still sorted ahead of non-all-day events. + */ +add_task(async function testOverlapOutside() { + let before = await addEvent("Starts Before", "-P1D", "P1D", true); + checkEvents({ dateHeader: "Today", title: "Starts Before", overlap: "end" }); + + let after = await addEvent("Ends After", "P0D", "P9D", true); + checkEvents( + { dateHeader: "Today", title: "Starts Before", overlap: "end" }, + { title: "Ends After", overlap: "start" } + ); + + let both = await addEvent("Beyond Start and End", "-P2D", "P9D", true); + checkEvents( + { dateHeader: "Today", title: "Beyond Start and End", overlap: "continue" }, + { title: "Starts Before", overlap: "end" }, + { title: "Ends After", overlap: "start" } + ); + + // Change `before` to begin earlier than `both`. They should swap places. + + let startClone = before.clone(); + startClone.startDate.day -= 2; + await calendar.modifyItem(startClone, before); + checkEvents( + { dateHeader: "Today", title: "Starts Before", overlap: "end" }, + { title: "Beyond Start and End", overlap: "continue" }, + { title: "Ends After", overlap: "start" } + ); + + let beforeWithTime = await addEvent("Starts Before with time", "-PT5H", "PT15H"); + checkEvents( + { dateHeader: "Today", title: "Starts Before", overlap: "end" }, + { title: "Beyond Start and End", overlap: "continue" }, + { title: "Ends After", overlap: "start" }, + // This is the end of the event so the end time is used. + { time: beforeWithTime.endDate, title: "Starts Before with time", overlap: "end" } + ); + + let afterWithTime = await addEvent("Ends After with time", "PT6H", "P8DT12H"); + checkEvents( + { dateHeader: "Today", title: "Starts Before", overlap: "end" }, + { title: "Beyond Start and End", overlap: "continue" }, + { title: "Ends After", overlap: "start" }, + { time: afterWithTime.startDate, title: "Ends After with time", overlap: "start" }, + // This is the end of the event so the end time is used. + { time: beforeWithTime.endDate, title: "Starts Before with time", overlap: "end" } + ); + + let bothWithTime = await addEvent("Beyond Start and End with time", "-P2DT10H", "P9DT1H"); + checkEvents( + { dateHeader: "Today", title: "Starts Before", overlap: "end" }, + { title: "Beyond Start and End", overlap: "continue" }, + { title: "Ends After", overlap: "start" }, + { time: "", title: "Beyond Start and End with time", overlap: "continue" }, + { time: afterWithTime.startDate, title: "Ends After with time", overlap: "start" }, + // This is the end of the event so the end time is used. + { time: beforeWithTime.endDate, title: "Starts Before with time", overlap: "end" } + ); + + await calendar.deleteItem(before); + await calendar.deleteItem(after); + await calendar.deleteItem(both); + await calendar.deleteItem(beforeWithTime); + await calendar.deleteItem(afterWithTime); + await calendar.deleteItem(bothWithTime); +}); + +/** + * Checks that events that happened earlier today are marked as in the past, + * and events happening now are marked as such. + * + * This test may fail if run within a minute either side of midnight. + * + * It would be nice to test that as time passes events are changed + * appropriately, but that means waiting around for minutes and probably won't + * be very reliable, so we don't do that. + */ +add_task(async function testActive() { + let now = cal.dtz.now(); + + let pastEvent = await addEvent("Past Event", "PT0M", "PT1M"); + let presentEvent = await addEvent("Present Event", `PT${now.hour}H`, `PT${now.hour + 1}H`); + let futureEvent = await addEvent("Future Event", "PT23H59M", "PT24H"); + checkEvents( + { dateHeader: "Today", time: pastEvent.startDate, title: "Past Event" }, + { time: presentEvent.startDate, title: "Present Event" }, + { time: futureEvent.startDate, title: "Future Event" } + ); + + let [pastRow, presentRow, futureRow] = TodayPane.agenda.rows; + Assert.ok(pastRow.classList.contains("agenda-listitem-past"), "past event is marked past"); + Assert.ok(!pastRow.classList.contains("agenda-listitem-now"), "past event is not marked now"); + Assert.ok( + !presentRow.classList.contains("agenda-listitem-past"), + "present event is not marked past" + ); + Assert.ok(presentRow.classList.contains("agenda-listitem-now"), "present event is marked now"); + Assert.ok( + !futureRow.classList.contains("agenda-listitem-past"), + "future event is not marked past" + ); + Assert.ok(!futureRow.classList.contains("agenda-listitem-now"), "future event is not marked now"); + + await calendar.deleteItem(pastEvent); + await calendar.deleteItem(presentEvent); + await calendar.deleteItem(futureEvent); +}); + +/** + * Checks events in different time zones are displayed correctly. + */ +add_task(async function testOtherTimeZones() { + // Johannesburg is UTC+2. + let johannesburg = cal.timezoneService.getTimezone("Africa/Johannesburg"); + // Panama is UTC-5. + let panama = cal.timezoneService.getTimezone("America/Panama"); + + // All-day events are displayed on the day of the event, the time zone is ignored. + + let allDayEvent = new CalEvent(); + allDayEvent.id = cal.getUUID(); + allDayEvent.title = "All-day event in Johannesburg"; + allDayEvent.startDate = cal.createDateTime(); + allDayEvent.startDate.resetTo(today.year, today.month, today.day + 1, 0, 0, 0, johannesburg); + allDayEvent.startDate.isDate = true; + allDayEvent.endDate = cal.createDateTime(); + allDayEvent.endDate.resetTo(today.year, today.month, today.day + 2, 0, 0, 0, johannesburg); + allDayEvent.endDate.isDate = true; + allDayEvent = await calendar.addItem(allDayEvent); + + checkEvents({ + dateHeader: "Tomorrow", + title: "All-day event in Johannesburg", + }); + + await calendar.deleteItem(allDayEvent); + + // The event time must be displayed in the local time zone, and the event must be sorted correctly. + + let beforeEvent = await addEvent("Before", "P1DT5H", "P1DT6H"); + let afterEvent = await addEvent("After", "P1DT7H", "P1DT8H"); + + let timedEvent = new CalEvent(); + timedEvent.id = cal.getUUID(); + timedEvent.title = "Morning in Johannesburg"; + timedEvent.startDate = cal.createDateTime(); + timedEvent.startDate.resetTo(today.year, today.month, today.day + 1, 8, 0, 0, johannesburg); + timedEvent.endDate = cal.createDateTime(); + timedEvent.endDate.resetTo(today.year, today.month, today.day + 1, 12, 0, 0, johannesburg); + timedEvent = await calendar.addItem(timedEvent); + + checkEvents( + { + dateHeader: "Tomorrow", + time: beforeEvent.startDate, + title: "Before", + }, + { + time: cal.dtz.formatter.formatTime(cal.createDateTime("20000101T060000Z")), // The date used here is irrelevant. + title: "Morning in Johannesburg", + }, + { + time: afterEvent.startDate, + title: "After", + } + ); + Assert.stringContains( + TodayPane.agenda.rows[1].querySelector(".agenda-listitem-time").getAttribute("datetime"), + "T08:00:00+02:00" + ); + + await calendar.deleteItem(beforeEvent); + await calendar.deleteItem(afterEvent); + await calendar.deleteItem(timedEvent); + + // Events that cross midnight in the local time zone (but not in the event time zone) + // must have a start row and an end row. + + let overnightEvent = new CalEvent(); + overnightEvent.id = cal.getUUID(); + overnightEvent.title = "Evening in Panama"; + overnightEvent.startDate = cal.createDateTime(); + overnightEvent.startDate.resetTo(today.year, today.month, today.day, 17, 0, 0, panama); + overnightEvent.endDate = cal.createDateTime(); + overnightEvent.endDate.resetTo(today.year, today.month, today.day, 23, 0, 0, panama); + overnightEvent = await calendar.addItem(overnightEvent); + + checkEvents( + { + dateHeader: "Today", + time: cal.dtz.formatter.formatTime(cal.createDateTime("20000101T220000Z")), // The date used here is irrelevant. + title: "Evening in Panama", + overlap: "start", + }, + { + dateHeader: "Tomorrow", + time: cal.dtz.formatter.formatTime(cal.createDateTime("20000101T040000Z")), // The date used here is irrelevant. + title: "Evening in Panama", + overlap: "end", + } + ); + Assert.stringContains( + TodayPane.agenda.rows[0].querySelector(".agenda-listitem-time").getAttribute("datetime"), + "T17:00:00-05:00" + ); + Assert.stringContains( + TodayPane.agenda.rows[1].querySelector(".agenda-listitem-time").getAttribute("datetime"), + "T23:00:00-05:00" + ); + + await calendar.deleteItem(overnightEvent); +}); + +/** + * Checks events in different time zones are displayed correctly. + */ +add_task(async function testRelativeTime() { + let formatter = new Intl.RelativeTimeFormat(undefined, { style: "short" }); + let now = cal.dtz.now(); + now.second = 0; + info(`The time is now ${now}`); + + let testData = [ + { + name: "two hours ago", + start: "-PT1H55M", + expected: { + classes: ["agenda-listitem-past"], + }, + minHour: 2, + }, + { + name: "one hour ago", + start: "-PT1H5M", + expected: { + classes: ["agenda-listitem-past"], + }, + minHour: 2, + }, + { + name: "23 minutes ago", + start: "-PT23M", + expected: { + classes: ["agenda-listitem-past"], + }, + minHour: 1, + }, + { + name: "now", + start: "-PT5M", + expected: { + relative: ["now"], + classes: ["agenda-listitem-now"], + }, + minHour: 1, + maxHour: 22, + }, + { + name: "19 minutes ahead", + start: "PT19M", + expected: { + relative: [formatter.format(19, "minute"), formatter.format(18, "minute")], + }, + maxHour: 22, + }, + { + name: "one hour ahead", + start: "PT1H25M", + expected: { + relative: [formatter.format(85, "minute"), formatter.format(84, "minute")], + }, + maxHour: 21, + }, + { + name: "one and half hours ahead", + start: "PT1H35M", + expected: { + relative: [formatter.format(2, "hour")], + }, + maxHour: 21, + }, + { + name: "two hours ahead", + start: "PT1H49M", + expected: { + relative: [formatter.format(2, "hour")], + }, + maxHour: 21, + }, + ]; + + let events = []; + let expectedEvents = []; + for (let { name, start, expected, minHour, maxHour } of testData) { + if (minHour && now.hour < minHour) { + info(`Skipping ${name} because it's too early.`); + continue; + } + if (maxHour && now.hour > maxHour) { + info(`Skipping ${name} because it's too late.`); + continue; + } + + let event = new CalEvent(); + event.id = cal.getUUID(); + event.title = name; + event.startDate = now.clone(); + event.startDate.addDuration(cal.createDuration(start)); + event.endDate = event.startDate.clone(); + event.endDate.addDuration(cal.createDuration("PT10M")); + events.push(await calendar.addItem(event)); + + expectedEvents.push({ ...expected, title: name, time: event.startDate }); + } + + expectedEvents[0].dateHeader = "Today"; + checkEvents(...expectedEvents); + + for (let event of events) { + await calendar.deleteItem(event); + } +}); + +/** + * Tests the today pane opens events in the summary dialog for both + * non-recurring and recurring events. + */ +add_task(async function testOpenEvent() { + let noRepeatEvent = new CalEvent(); + noRepeatEvent.id = "no repeat event"; + noRepeatEvent.title = "No Repeat Event"; + noRepeatEvent.startDate = today.clone(); + noRepeatEvent.startDate.hour = startHour; + noRepeatEvent.endDate = noRepeatEvent.startDate.clone(); + noRepeatEvent.endDate.hour++; + + let repeatEvent = new CalEvent(); + repeatEvent.id = "repeated event"; + repeatEvent.title = "Repeated Event"; + repeatEvent.startDate = today.clone(); + repeatEvent.startDate.hour = startHour; + repeatEvent.endDate = noRepeatEvent.startDate.clone(); + repeatEvent.endDate.hour++; + repeatEvent.recurrenceInfo = new CalRecurrenceInfo(repeatEvent); + repeatEvent.recurrenceInfo.appendRecurrenceItem( + cal.createRecurrenceRule("RRULE:FREQ=DAILY;COUNT=5") + ); + + for (let event of [noRepeatEvent, repeatEvent]) { + let addedEvent = await calendar.addItem(event); + + if (event == noRepeatEvent) { + Assert.equal(TodayPane.agenda.rowCount, 1); + } else { + Assert.equal(TodayPane.agenda.rowCount, 5); + } + Assert.equal( + TodayPane.agenda.rows[0].querySelector(".agenda-listitem-title").textContent, + event.title, + "event title is correct" + ); + + let dialogWindowPromise = CalendarTestUtils.waitForEventDialog(); + EventUtils.synthesizeMouseAtCenter(TodayPane.agenda.rows[0], { clickCount: 2 }); + + let dialogWindow = await dialogWindowPromise; + let docUri = dialogWindow.document.documentURI; + Assert.ok( + docUri === "chrome://calendar/content/calendar-summary-dialog.xhtml", + "event summary dialog shown" + ); + + await BrowserTestUtils.closeWindow(dialogWindow); + await calendar.deleteItem(addedEvent); + } +}); + +/** + * Tests that the "New Event" button begins creating an event on the date + * selected in the Today Pane. + */ +add_task(async function testNewEvent() { + async function checkEventDialogDate() { + let dialogWindowPromise = CalendarTestUtils.waitForEventDialog("edit"); + EventUtils.synthesizeMouseAtCenter(newEventButton, {}, window); + await dialogWindowPromise.then(async function (dialogWindow) { + let iframe = dialogWindow.document.querySelector("#calendar-item-panel-iframe"); + let iframeDocument = iframe.contentDocument; + + let startDate = iframeDocument.getElementById("event-starttime"); + Assert.equal( + startDate._datepicker._inputField.value, + formatDate(expectedDate), + "date should match the expected date" + ); + Assert.equal( + startDate._timepicker._inputField.value, + formatTime(expectedDate), + "time should be the next hour after now" + ); + + await BrowserTestUtils.closeWindow(dialogWindow); + }); + } + + let newEventButton = document.getElementById("todaypane-new-event-button"); + + // Check today with the "day" view. + + TodayPane.displayMiniSection("miniday"); + EventUtils.synthesizeMouseAtCenter(document.getElementById("today-button"), {}, window); + + let expectedDate = cal.dtz.now(); + expectedDate.hour++; + expectedDate.minute = 0; + + await checkEventDialogDate(); + + // Check tomorrow with the "day" view. + + EventUtils.synthesizeMouseAtCenter(document.getElementById("next-day-button"), {}, window); + expectedDate.day++; + + await checkEventDialogDate(); + + // Check today with the "month" view; + + TodayPane.displayMiniSection("minimonth"); + let minimonth = document.getElementById("today-minimonth"); + minimonth.value = new Date(); + expectedDate.day--; + + await checkEventDialogDate(); + + // Check a date in the past with the "month" view; + + minimonth.value = new Date(Date.UTC(2018, 8, 1)); + expectedDate.resetTo(2018, 8, 1, expectedDate.hour, 0, 0, cal.dtz.UTC); + + await checkEventDialogDate(); +}).__skipMe = new Date().getUTCHours() == 23; diff --git a/comm/calendar/test/browser/browser_todayPane_dragAndDrop.js b/comm/calendar/test/browser/browser_todayPane_dragAndDrop.js new file mode 100644 index 0000000000..bac91ed60d --- /dev/null +++ b/comm/calendar/test/browser/browser_todayPane_dragAndDrop.js @@ -0,0 +1,262 @@ +/* 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 drag and drop on the today pane. + */ +const { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); +const { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm"); +const { + add_message_to_folder, + be_in_folder, + create_folder, + create_message, + inboxFolder, + select_click_row, +} = ChromeUtils.import("resource://testing-common/mozmill/FolderDisplayHelpers.jsm"); +const { SyntheticPartLeaf } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); + +const calendar = CalendarTestUtils.createCalendar("Mochitest", "memory"); +registerCleanupFunction(() => CalendarTestUtils.removeCalendar(calendar)); + +/** + * Ensures the today pane is visible for each test. + */ +async function ensureTodayPane() { + let todayPane = document.querySelector("#today-pane-panel"); + if (!todayPane.isVisible()) { + todayPane.setVisible(true, true, true); + } + + await TestUtils.waitForCondition(() => todayPane.isVisible(), "today pane not visible in time"); +} + +/** + * Tests dropping a message from the message pane on to the today pane brings + * up the new event dialog. + */ +add_task(async function testDropMozMessage() { + let folder = await create_folder("Mochitest"); + let subject = "The Grand Event"; + let body = "Parking is available."; + await be_in_folder(folder); + await add_message_to_folder([folder], create_message({ subject, body: { body } })); + select_click_row(0); + + let about3PaneTab = document.getElementById("tabmail").currentTabInfo; + let msg = about3PaneTab.message; + let msgStr = about3PaneTab.folder.getUriForMsg(msg); + let msgUrl = MailServices.messageServiceFromURI(msgStr).getUrlForUri(msgStr); + + // Setup a DataTransfer to mimic what ThreadPaneOnDragStart sends. + let dataTransfer = new DataTransfer(); + dataTransfer.mozSetDataAt("text/x-moz-message", msgStr, 0); + dataTransfer.mozSetDataAt("text/x-moz-url", msgUrl.spec, 0); + dataTransfer.mozSetDataAt( + "application/x-moz-file-promise-url", + msgUrl.spec + "?fileName=" + encodeURIComponent("message.eml"), + 0 + ); + dataTransfer.mozSetDataAt( + "application/x-moz-file-promise", + new window.messageFlavorDataProvider(), + 0 + ); + + let promise = CalendarTestUtils.waitForEventDialog("edit"); + await ensureTodayPane(); + document.querySelector("#agenda").dispatchEvent(new DragEvent("drop", { dataTransfer })); + + let eventWindow = await promise; + let iframe = eventWindow.document.querySelector("#calendar-item-panel-iframe"); + let iframeDoc = iframe.contentDocument; + + Assert.equal( + iframeDoc.querySelector("#item-title").value, + subject, + "the message subject was used as the event title" + ); + Assert.equal( + iframeDoc.querySelector("#item-description").contentDocument.body.innerText, + body, + "the message body was used as the event description" + ); + + await BrowserTestUtils.closeWindow(eventWindow); + await be_in_folder(inboxFolder); + folder.deleteSelf(null); +}); + +/** + * Tests dropping an entry from the address book adds the address as an attendee + * to a new event when dropped on the today pane. + */ +add_task(async function testMozAddressDrop() { + let vcard = CalendarTestUtils.dedent` + BEGIN:VCARD + VERSION:4.0 + EMAIL;PREF=1:person@example.com + FN:Some Person + N:Some;Person;;; + UID:d5f9113d-5ede-4a5c-ba8e-0f2345369993 + END:VCARD + `; + + let address = "Some Person <person@example.com>"; + + // Setup a DataTransfer to mimic what the address book sends. + let dataTransfer = new DataTransfer(); + dataTransfer.setData("moz/abcard", "0"); + dataTransfer.setData("text/x-moz-address", address); + dataTransfer.setData("text/plain", address); + dataTransfer.setData("text/vcard", decodeURIComponent(vcard)); + dataTransfer.setData("application/x-moz-file-promise-dest-filename", "person.vcf"); + dataTransfer.setData("application/x-moz-file-promise-url", "data:text/vcard," + vcard); + dataTransfer.setData("application/x-moz-file-promise", window.abFlavorDataProvider); + + let promise = CalendarTestUtils.waitForEventDialog("edit"); + await ensureTodayPane(); + document.querySelector("#agenda").dispatchEvent(new DragEvent("drop", { dataTransfer })); + + let eventWindow = await promise; + let iframe = eventWindow.document.querySelector("#calendar-item-panel-iframe"); + let iframeWin = iframe.cotnentWindow; + let iframeDoc = iframe.contentDocument; + + // Verify the address was added as an attendee. + EventUtils.synthesizeMouseAtCenter( + iframeDoc.querySelector("#event-grid-tab-attendees"), + {}, + iframeWin + ); + + let box = iframeDoc.querySelector('[attendeeid="mailto:person@example.com"]'); + Assert.ok(box, "address included as an attendee to the new event"); + await BrowserTestUtils.closeWindow(eventWindow); +}); + +/** + * Tests dropping plain text that is actually ics data format is picked up by + * the today pane. + */ +add_task(async function testPlainTextICSDrop() { + let event = CalendarTestUtils.dedent` + BEGIN:VCALENDAR + BEGIN:VEVENT + SUMMARY:An Event + DESCRIPTION:Parking is not available. + DTSTART:20210325T110000Z + DTEND:20210325T120000Z + UID:916bd967-35ac-40f6-8cd5-487739c9d245 + END:VEVENT + END:VCALENDAR + `; + + // Setup a DataTransfer to mimic what the address book sends. + let dataTransfer = new DataTransfer(); + dataTransfer.setData("text/plain", event); + + let promise = CalendarTestUtils.waitForEventDialog("edit"); + await ensureTodayPane(); + document.querySelector("#agenda").dispatchEvent(new DragEvent("drop", { dataTransfer })); + + let eventWindow = await promise; + let iframe = eventWindow.document.querySelector("#calendar-item-panel-iframe"); + let iframeDoc = iframe.contentDocument; + Assert.equal(iframeDoc.querySelector("#item-title").value, "An Event"); + + let startTime = iframeDoc.querySelector("#event-starttime"); + Assert.equal( + startTime._datepicker._inputBoxValue, + cal.dtz.formatter.formatDateShort(cal.createDateTime("20210325T110000Z")) + ); + + let endTime = iframeDoc.querySelector("#event-endtime"); + Assert.equal( + endTime._datepicker._inputBoxValue, + cal.dtz.formatter.formatDateShort(cal.createDateTime("20210325T120000Z")) + ); + + Assert.equal( + iframeDoc.querySelector("#item-description").contentDocument.body.innerText, + "Parking is not available." + ); + await BrowserTestUtils.closeWindow(eventWindow); +}); + +/** + * Tests dropping a file with an ics extension on the today pane is parsed as an + * ics file. + */ +add_task(async function testICSFileDrop() { + let file = await File.createFromFileName(getTestFilePath("data/event.ics")); + let dataTransfer = new DataTransfer(); + dataTransfer.items.add(file); + + let promise = CalendarTestUtils.waitForEventDialog("edit"); + await ensureTodayPane(); + + // For some reason, dataTransfer.items.add() results in a mozItemCount of 2 + // instead of one. Call onExternalDrop directly to get around that. + window.calendarCalendarButtonDNDObserver.onExternalDrop(dataTransfer); + + let eventWindow = await promise; + let iframe = eventWindow.document.querySelector("#calendar-item-panel-iframe"); + let iframeDoc = iframe.contentDocument; + + Assert.equal(iframeDoc.querySelector("#item-title").value, "An Event"); + + let startTime = iframeDoc.querySelector("#event-starttime"); + Assert.equal( + startTime._datepicker._inputBoxValue, + cal.dtz.formatter.formatDateShort(cal.createDateTime("20210325T110000Z")) + ); + + let endTime = iframeDoc.querySelector("#event-endtime"); + Assert.equal( + endTime._datepicker._inputBoxValue, + cal.dtz.formatter.formatDateShort(cal.createDateTime("20210325T120000Z")) + ); + + Assert.equal( + iframeDoc.querySelector("#item-description").contentDocument.body.innerText, + "Parking is not available." + ); + await BrowserTestUtils.closeWindow(eventWindow); +}); + +/** + * Tests dropping any other file on the today pane ends up as an attachment + * to a new event. + */ +add_task(async function testOtherFileDrop() { + let file = await File.createFromNsIFile( + new FileUtils.File(getTestFilePath("data/attachment.png")) + ); + let dataTransfer = new DataTransfer(); + dataTransfer.setData("image/png", file); + dataTransfer.items.add(file); + + let promise = CalendarTestUtils.waitForEventDialog("edit"); + await ensureTodayPane(); + document.querySelector("#agenda").dispatchEvent(new DragEvent("drop", { dataTransfer })); + + let eventWindow = await promise; + let iframe = eventWindow.document.querySelector("#calendar-item-panel-iframe"); + let iframeWin = iframe.contentWindow; + let iframeDoc = iframe.contentDocument; + + EventUtils.synthesizeMouseAtCenter( + iframeDoc.querySelector("#event-grid-tab-attachments"), + {}, + iframeWin + ); + + let listBox = iframeDoc.querySelector("#attachment-link"); + let listItem = listBox.itemChildren[0]; + Assert.equal(listItem.querySelector("label").value, "attachment.png"); + await BrowserTestUtils.closeWindow(eventWindow); +}); diff --git a/comm/calendar/test/browser/browser_todayPane_visibility.js b/comm/calendar/test/browser/browser_todayPane_visibility.js new file mode 100644 index 0000000000..d2176218ed --- /dev/null +++ b/comm/calendar/test/browser/browser_todayPane_visibility.js @@ -0,0 +1,167 @@ +/* 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 openAddonsTab, openChatTab, openNewCalendarEventTab, + * openNewCalendarTaskTab, openPreferencesTab, openTasksTab, + * selectCalendarEventTab, selectCalendarTaskTab, selectFolderTab, + * toAddressBook */ + +// Test that today pane is visible/collapsed correctly for various tab types. +// In all cases today pane should not be visible in preferences or addons tab. +// Also test that the today pane button is visible/hidden for various tab types. +add_task(async () => { + let calendar = CalendarTestUtils.createCalendar(); + registerCleanupFunction(() => { + CalendarTestUtils.removeCalendar(calendar); + }); + + const todayPane = document.getElementById("today-pane-panel"); + const todayPaneButton = document.getElementById("calendar-status-todaypane-button"); + + let eventTabPanelId, taskTabPanelId; + + async function clickTodayPaneButton() { + // The today pane button will be hidden for certain tabs (e.g. preferences), and then + // the user won't be able to click it, so we shouldn't be able to here either. + if (BrowserTestUtils.is_visible(todayPaneButton)) { + EventUtils.synthesizeMouseAtCenter(todayPaneButton, { clickCount: 1 }); + } + await new Promise(resolve => setTimeout(resolve)); + } + + /** + * Tests whether the today pane is only open in certain tabs. + * + * @param {string[]} tabsWhereVisible - Array of tab mode names for tabs where + * the today pane should be visible. + */ + async function checkTodayPaneVisibility(tabsWhereVisible) { + function check(tabModeName) { + let shouldBeVisible = tabsWhereVisible.includes(tabModeName); + is( + BrowserTestUtils.is_visible(todayPane), + shouldBeVisible, + `today pane is ${shouldBeVisible ? "visible" : "collapsed"} in ${tabModeName} tab` + ); + } + + await selectFolderTab(); + check("folder"); + await CalendarTestUtils.openCalendarTab(window); + check("calendar"); + await openTasksTab(); + check("tasks"); + await openChatTab(); + check("chat"); + await selectCalendarEventTab(eventTabPanelId); + check("calendarEvent"); + await selectCalendarTaskTab(taskTabPanelId); + check("calendarTask"); + await toAddressBook(); + check("addressBookTab"); + await openPreferencesTab(); + check("preferencesTab"); + await openAddonsTab(); + check("contentTab"); + } + + // Show today pane in folder (mail) tab, but not in other tabs. + await selectFolderTab(); + if (!BrowserTestUtils.is_visible(todayPane)) { + await clickTodayPaneButton(); + } + await CalendarTestUtils.openCalendarTab(window); + if (BrowserTestUtils.is_visible(todayPane)) { + await clickTodayPaneButton(); + } + await openTasksTab(); + if (BrowserTestUtils.is_visible(todayPane)) { + await clickTodayPaneButton(); + } + await openChatTab(); + if (BrowserTestUtils.is_visible(todayPane)) { + await clickTodayPaneButton(); + } + eventTabPanelId = await openNewCalendarEventTab(); + if (BrowserTestUtils.is_visible(todayPane)) { + await clickTodayPaneButton(); + } + taskTabPanelId = await openNewCalendarTaskTab(); + if (BrowserTestUtils.is_visible(todayPane)) { + await clickTodayPaneButton(); + } + + await checkTodayPaneVisibility(["folder"]); + + // Show today pane in calendar tab, but not in other tabs. + // Hide it in folder tab. + await selectFolderTab(); + await clickTodayPaneButton(); + // Show it in calendar tab. + await CalendarTestUtils.openCalendarTab(window); + await clickTodayPaneButton(); + + await checkTodayPaneVisibility(["calendar"]); + + // Show today pane in tasks tab, but not in other tabs. + // Hide it in calendar tab. + await CalendarTestUtils.openCalendarTab(window); + await clickTodayPaneButton(); + // Show it in tasks tab. + await openTasksTab(); + await clickTodayPaneButton(); + + await checkTodayPaneVisibility(["tasks"]); + + // Show today pane in chat tab, but not in other tabs. + // Hide it in tasks tab. + await openTasksTab(); + await clickTodayPaneButton(); + // Show it in chat tab. + await openChatTab(); + await clickTodayPaneButton(); + + await checkTodayPaneVisibility(["chat"]); + + // Show today pane in calendar event tab, but not in other tabs. + // Hide it in chat tab. + await openChatTab(); + await clickTodayPaneButton(); + // Show it in calendar event tab. + await selectCalendarEventTab(eventTabPanelId); + await clickTodayPaneButton(); + + await checkTodayPaneVisibility(["calendarEvent"]); + + // Show today pane in calendar task tab, but not in other tabs. + // Hide it in calendar event tab. + await selectCalendarEventTab(eventTabPanelId); + await clickTodayPaneButton(); + // Show it in calendar task tab. + await selectCalendarTaskTab(taskTabPanelId); + await clickTodayPaneButton(); + + await checkTodayPaneVisibility(["calendarTask"]); + + // Check the visibility of the today pane button. + const button = document.getElementById("calendar-status-todaypane-button"); + await selectFolderTab(); + ok(BrowserTestUtils.is_visible(button), "today pane button is visible in folder tab"); + await CalendarTestUtils.openCalendarTab(window); + ok(BrowserTestUtils.is_visible(button), "today pane button is visible in calendar tab"); + await openTasksTab(); + ok(BrowserTestUtils.is_visible(button), "today pane button is visible in tasks tab"); + await openChatTab(); + ok(BrowserTestUtils.is_visible(button), "today pane button is visible in chat tab"); + await selectCalendarEventTab(eventTabPanelId); + ok(BrowserTestUtils.is_visible(button), "today pane button is visible in event tab"); + await selectCalendarTaskTab(taskTabPanelId); + ok(BrowserTestUtils.is_visible(button), "today pane button is visible in task tab"); + await toAddressBook(); + is(BrowserTestUtils.is_visible(button), false, "today pane button is hidden in address book tab"); + await openPreferencesTab(); + is(BrowserTestUtils.is_visible(button), false, "today pane button is hidden in preferences tab"); + await openAddonsTab(); + is(BrowserTestUtils.is_visible(button), false, "today pane button is hidden in addons tab"); +}); diff --git a/comm/calendar/test/browser/contextMenu/browser.ini b/comm/calendar/test/browser/contextMenu/browser.ini new file mode 100644 index 0000000000..b6590e849c --- /dev/null +++ b/comm/calendar/test/browser/contextMenu/browser.ini @@ -0,0 +1,14 @@ +[default] +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 + +[browser_edit.js] diff --git a/comm/calendar/test/browser/contextMenu/browser_edit.js b/comm/calendar/test/browser/contextMenu/browser_edit.js new file mode 100644 index 0000000000..672055709f --- /dev/null +++ b/comm/calendar/test/browser/contextMenu/browser_edit.js @@ -0,0 +1,187 @@ +/* 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 { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); +const { CalendarTestUtils } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarTestUtils.jsm" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + CalEvent: "resource:///modules/CalEvent.jsm", +}); + +/** + * Grabs a calendar-month-day-box-item from the view using an attribute CSS + * selector. Only works when the calendar is in month view. + */ +async function getDayBoxItem(attrSelector) { + let itemBox; + await TestUtils.waitForCondition(() => { + itemBox = document.querySelector( + `calendar-month-day-box[${attrSelector}] calendar-month-day-box-item` + ); + return itemBox != null; + }, "calendar item did not appear in time"); + return itemBox; +} + +/** + * Switches to the view to the calendar. + */ +add_setup(function () { + return CalendarTestUtils.setCalendarView(window, "month"); +}); + +/** + * Tests the "Edit" menu item is available and opens up the event dialog. + */ +add_task(async function testEditEditableItem() { + let calendar = CalendarTestUtils.createCalendar("Editable", "memory"); + registerCleanupFunction(() => CalendarTestUtils.removeCalendar(calendar)); + + let title = "Editable Event"; + let event = new CalEvent(); + event.title = title; + event.startDate = cal.createDateTime("20200101T000001Z"); + + await calendar.addItem(event); + window.goToDate(event.startDate); + + let menu = document.querySelector("#calendar-item-context-menu"); + let editMenu = document.querySelector("#calendar-item-context-menu-modify-menuitem"); + let popupPromise = BrowserTestUtils.waitForEvent(menu, "popupshown"); + + EventUtils.synthesizeMouseAtCenter(await getDayBoxItem('day="1"'), { type: "contextmenu" }); + await popupPromise; + Assert.ok(!editMenu.disabled, 'context menu "Edit" item is not disabled for editable event'); + + let editDialogPromise = BrowserTestUtils.domWindowOpened(null, async win => { + await BrowserTestUtils.waitForEvent(win, "load"); + + let doc = win.document; + Assert.ok( + doc.documentURI == "chrome://calendar/content/calendar-event-dialog.xhtml", + "editing event dialog opened" + ); + + let iframe = doc.querySelector("#calendar-item-panel-iframe"); + await BrowserTestUtils.waitForEvent(iframe.contentWindow, "load"); + + let iframeDoc = iframe.contentDocument; + Assert.ok( + (iframeDoc.querySelector("#item-title").value = title), + 'context menu "Edit" item opens the editing dialog' + ); + doc.querySelector("dialog").acceptDialog(); + return true; + }); + + menu.activateItem(editMenu); + await editDialogPromise; +}); + +/** + * Tests that the "Edit" menu item is disabled for events we are not allowed to + * modify. + */ +add_task(async function testEditNonEditableItem() { + let calendar = CalendarTestUtils.createCalendar("Non-Editable", "memory"); + registerCleanupFunction(() => CalendarTestUtils.removeCalendar(calendar)); + + let event = new CalEvent(); + let acl = { + QueryInterface: ChromeUtils.generateQI(["calIItemACLEntry"]), + userCanModify: false, + userCanRespond: true, + userCanViewAll: true, + userCanViewDateAndTime: true, + calendarEntry: { + hasAccessControl: true, + userIsOwner: false, + }, + }; + event.title = "Read Only Event"; + event.startDate = cal.createDateTime("20200102T000001Z"); + event.mACLEntry = acl; + + await calendar.addItem(event); + window.goToDate(event.startDate); + + let menu = document.querySelector("#calendar-item-context-menu"); + let editMenu = document.querySelector("#calendar-item-context-menu-modify-menuitem"); + let popupPromise = BrowserTestUtils.waitForEvent(menu, "popupshowing"); + + EventUtils.synthesizeMouseAtCenter(await getDayBoxItem('day="2"'), { type: "contextmenu" }); + await popupPromise; + Assert.ok(editMenu.disabled, 'context menu "Edit" item is disabled for non-editable event'); + menu.hidePopup(); +}); + +/** + * Tests that the "Edit" menu item is disabled when the event is an invitation. + */ +add_task(async function testInvitation() { + let calendar = CalendarTestUtils.createCalendar("Invitation", "memory"); + calendar.setProperty("organizerId", "mailto:attendee@example.com"); + registerCleanupFunction(() => CalendarTestUtils.removeCalendar(calendar)); + + let icalString = CalendarTestUtils.dedent` + BEGIN:VEVENT + CREATED:20200103T152601Z + DTSTAMP:20200103T192729Z + UID:x131e + SUMMARY:Invitation + ORGANIZER;CN=Org:mailto:organizer@example.com + ATTENDEE;RSVP=TRUE;CN=attendee@example.com;PARTSTAT=NEEDS-ACTION;CUTY + PE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;X-NUM-GUESTS=0:mailto:attendee@example.com + DTSTART:20200103T153000Z + DTEND:20200103T163000Z + DESCRIPTION:Just a Test + SEQUENCE:0 + TRANSP:OPAQUE + END:VEVENT + `; + + let invitation = new CalEvent(icalString); + await calendar.addItem(invitation); + window.goToDate(invitation.startDate); + + let menu = document.querySelector("#calendar-item-context-menu"); + let editMenu = document.querySelector("#calendar-item-context-menu-modify-menuitem"); + let popupPromise = BrowserTestUtils.waitForEvent(menu, "popupshowing"); + + EventUtils.synthesizeMouseAtCenter(await getDayBoxItem('day="3"'), { type: "contextmenu" }); + await popupPromise; + Assert.ok(editMenu.disabled, 'context menu "Edit" item is disabled for invitations'); + menu.hidePopup(); +}); + +/** + * Tests that the "Edit" menu item is disabled when the calendar is read-only. + */ +add_task(async function testCalendarReadOnly() { + let calendar = CalendarTestUtils.createCalendar("ReadOnly", "memory"); + registerCleanupFunction(() => CalendarTestUtils.removeCalendar(calendar)); + + let event = new CalEvent(); + event.title = "ReadOnly Event"; + event.startDate = cal.createDateTime("20200104T000001Z"); + + await calendar.addItem(event); + calendar.setProperty("readOnly", true); + window.goToDate(event.startDate); + + let menu = document.querySelector("#calendar-item-context-menu"); + let editMenu = document.querySelector("#calendar-item-context-menu-modify-menuitem"); + let popupPromise = BrowserTestUtils.waitForEvent(menu, "popupshowing"); + + EventUtils.synthesizeMouseAtCenter(await getDayBoxItem('day="4"'), { type: "contextmenu" }); + await popupPromise; + Assert.ok(editMenu.disabled, 'context menu "Edit" item is disabled when calendar is read-only'); + menu.hidePopup(); +}); + +registerCleanupFunction(() => { + return CalendarTestUtils.closeCalendarTab(window); +}); diff --git a/comm/calendar/test/browser/data/attachment.png b/comm/calendar/test/browser/data/attachment.png Binary files differnew file mode 100644 index 0000000000..30caecab7b --- /dev/null +++ b/comm/calendar/test/browser/data/attachment.png diff --git a/comm/calendar/test/browser/data/calendars.sjs b/comm/calendar/test/browser/data/calendars.sjs new file mode 100644 index 0000000000..f1175f1903 --- /dev/null +++ b/comm/calendar/test/browser/data/calendars.sjs @@ -0,0 +1,126 @@ +/* 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/. */ + +Cu.importGlobalProperties(["TextEncoder"]); + +function handleRequest(request, response) { + if (!request.hasHeader("Authorization")) { + response.setStatusLine("1.1", 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", `Basic realm="test"`); + return; + } + + response.setStatusLine("1.1", 207, "Multi-Status"); + response.setHeader("Content-Type", "text/xml; charset=utf-8", false); + + // Request: + // <propfind> + // <prop> + // <resourcetype/> + // <displayname/> + // <current-user-privilege-set/> + // <calendar-color/> + // </prop> + // </propfind> + + let res = `<multistatus xmlns="DAV:" + xmlns:A="http://apple.com/ns/ical/" + xmlns:C="urn:ietf:params:xml:ns:caldav" + xmlns:CS="http://calendarserver.org/ns/"> + <response> + <href>/browser/comm/calendar/test/browser/data/calendars.sjs</href> + <propstat> + <prop> + <resourcetype> + <collection/> + </resourcetype> + <displayname>Things found by DNS</displayname> + </prop> + <status>HTTP/1.1 200 OK</status> + </propstat> + <propstat> + <prop> + <current-user-privilege-set/> + <A:calendar-color/> + </prop> + <status>HTTP/1.1 404 Not Found</status> + </propstat> + </response> + <response> + <href>/browser/comm/calendar/test/browser/data/calendar.sjs</href> + <propstat> + <prop> + <resourcetype> + <collection/> + <C:calendar/> + <CS:shared/> + </resourcetype> + <displayname>You found me!</displayname> + <A:calendar-color>#008000</A:calendar-color> + </prop> + <status>HTTP/1.1 200 OK</status> + </propstat> + <propstat> + <prop> + <current-user-privilege-set/> + </prop> + <status>HTTP/1.1 404 Not Found</status> + </propstat> + </response> + <response> + <href>/browser/comm/calendar/test/browser/data/calendar2.sjs</href> + <propstat> + <prop> + <resourcetype> + <collection/> + <C:calendar/> + <CS:shared/> + </resourcetype> + <displayname>Röda dagar</displayname> + <A:calendar-color>#ff0000</A:calendar-color> + <current-user-privilege-set> + <privilege> + <read/> + </privilege> + <privilege> + <C:read-free-busy/> + </privilege> + <privilege> + <read-current-user-privilege-set/> + </privilege> + <privilege> + <write/> + </privilege> + <privilege> + <write-content/> + </privilege> + <privilege> + <write-properties/> + </privilege> + <privilege> + <bind/> + </privilege> + <privilege> + <unbind/> + </privilege> + </current-user-privilege-set> + </prop> + <status>HTTP/1.1 200 OK</status> + </propstat> + <propstat> + <prop> + <current-user-privilege-set/> + </prop> + <status>HTTP/1.1 404 Not Found</status> + </propstat> + </response> + </multistatus>`; + + let bytes = new TextEncoder().encode(res); + let str = ""; + for (let i = 0; i < bytes.length; i += 65536) { + str += String.fromCharCode.apply(null, bytes.subarray(i, i + 65536)); + } + response.write(str); +} diff --git a/comm/calendar/test/browser/data/dns.sjs b/comm/calendar/test/browser/data/dns.sjs new file mode 100644 index 0000000000..85b8777233 --- /dev/null +++ b/comm/calendar/test/browser/data/dns.sjs @@ -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/. */ + +function handleRequest(request, response) { + if (!request.hasHeader("Authorization")) { + response.setStatusLine("1.1", 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", `Basic realm="test"`); + return; + } + + response.setStatusLine("1.1", 207, "Multi-Status"); + response.setHeader("Content-Type", "text/xml", false); + + // Request: + // <propfind> + // <prop> + // <resourcetype/> + // <owner/> + // <displayname/> + // <current-user-principal/> + // <current-user-privilege-set/> + // <calendar-color/> + // <calendar-home-set/> + // </prop> + // </propfind> + + response.write(`<multistatus xmlns="DAV:" + xmlns:A="http://apple.com/ns/ical/" + xmlns:C="urn:ietf:params:xml:ns:caldav"> + <response> + <href>/browser/comm/calendar/test/browser/data/dns.sjs</href> + <propstat> + <prop> + <resourcetype> + <collection/> + </resourcetype> + <current-user-principal> + <href>/browser/comm/calendar/test/browser/data/principal.sjs</href> + </current-user-principal> + </prop> + <status>HTTP/1.1 200 OK</status> + </propstat> + <propstat> + <prop> + <owner/> + <displayname/> + <current-user-privilege-set/> + <A:calendar-color/> + <C:calendar-home-set/> + </prop> + <status>HTTP/1.1 404 Not Found</status> + </propstat> + </response> + </multistatus>`); +} diff --git a/comm/calendar/test/browser/data/event.ics b/comm/calendar/test/browser/data/event.ics new file mode 100644 index 0000000000..3ee7fd4495 --- /dev/null +++ b/comm/calendar/test/browser/data/event.ics @@ -0,0 +1,10 @@ +BEGIN:VCALENDAR
+PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+VERSION:2.0
+BEGIN:VEVENT
+SUMMARY:An Event
+DESCRIPTION:Parking is not available.
+DTSTART:20210325T110000Z
+DTEND:20210325T120000Z
+END:VEVENT
+END:VCALENDAR
diff --git a/comm/calendar/test/browser/data/import.ics b/comm/calendar/test/browser/data/import.ics new file mode 100644 index 0000000000..b6e7a965d7 --- /dev/null +++ b/comm/calendar/test/browser/data/import.ics @@ -0,0 +1,24 @@ +BEGIN:VCALENDAR
+PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+VERSION:2.0
+BEGIN:VEVENT
+SUMMARY:Event One
+DTSTART:20190101T150000
+DTEND:20190101T160000
+END:VEVENT
+BEGIN:VEVENT
+SUMMARY:Event Four
+DTSTART:20190101T180000
+DTEND:20190101T190000
+END:VEVENT
+BEGIN:VEVENT
+SUMMARY:Event Three
+DTSTART:20190101T170000
+DTEND:20190101T180000
+END:VEVENT
+BEGIN:VEVENT
+SUMMARY:Event Two
+DTSTART:20190101T160000
+DTEND:20190101T170000
+END:VEVENT
+END:VCALENDAR
diff --git a/comm/calendar/test/browser/data/principal.sjs b/comm/calendar/test/browser/data/principal.sjs new file mode 100644 index 0000000000..4cebd9660e --- /dev/null +++ b/comm/calendar/test/browser/data/principal.sjs @@ -0,0 +1,39 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +function handleRequest(request, response) { + if (!request.hasHeader("Authorization")) { + response.setStatusLine("1.1", 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", `Basic realm="test"`); + return; + } + + response.setStatusLine("1.1", 207, "Multi-Status"); + response.setHeader("Content-Type", "text/xml", false); + + // Request: + // <propfind> + // <prop> + // <resourcetype/> + // <calendar-home-set/> + // </prop> + // </propfind> + + response.write(`<multistatus xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav"> + <response> + <href>/browser/comm/calendar/test/browser/data/principal.sjs</href> + <propstat> + <prop> + <resourcetype> + <principal/> + </resourcetype> + <C:calendar-home-set> + <href>/browser/comm/calendar/test/browser/data/calendars.sjs</href> + </C:calendar-home-set> + </prop> + <status>HTTP/1.1 200 OK</status> + </propstat> + </response> + </multistatus>`); +} 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); +} diff --git a/comm/calendar/test/browser/head.js b/comm/calendar/test/browser/head.js new file mode 100644 index 0000000000..f76cc85754 --- /dev/null +++ b/comm/calendar/test/browser/head.js @@ -0,0 +1,374 @@ +/* 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/. */ + +/* import-globals-from ../../base/content/calendar-views-utils.js */ + +/* globals openOptionsDialog, openAddonsMgr */ + +const { CalendarTestUtils } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarTestUtils.jsm" +); + +async function openTasksTab() { + let tabmail = document.getElementById("tabmail"); + let tasksMode = tabmail.tabModes.tasks; + + if (tasksMode.tabs.length == 1) { + tabmail.selectedTab = tasksMode.tabs[0]; + } else { + let tasksTabButton = document.getElementById("tasksButton"); + EventUtils.synthesizeMouseAtCenter(tasksTabButton, { clickCount: 1 }); + } + + is(tasksMode.tabs.length, 1, "tasks tab is open"); + is(tabmail.selectedTab, tasksMode.tabs[0], "tasks tab is selected"); + + await new Promise(resolve => setTimeout(resolve)); +} + +async function closeTasksTab() { + let tabmail = document.getElementById("tabmail"); + let tasksMode = tabmail.tabModes.tasks; + + if (tasksMode.tabs.length == 1) { + tabmail.closeTab(tasksMode.tabs[0]); + } + + is(tasksMode.tabs.length, 0, "tasks tab is not open"); + + await new Promise(resolve => setTimeout(resolve)); +} + +/** + * Currently there's always a folder tab open, hence "select" not "open". + */ +async function selectFolderTab() { + const tabmail = document.getElementById("tabmail"); + const folderMode = tabmail.tabModes.mail3PaneTab; + + tabmail.selectedTab = folderMode.tabs[0]; + + is(folderMode.tabs.length > 0, true, "at least one folder tab is open"); + is(tabmail.selectedTab, folderMode.tabs[0], "a folder tab is selected"); + + await new Promise(resolve => setTimeout(resolve)); +} + +async function openChatTab() { + let tabmail = document.getElementById("tabmail"); + let chatMode = tabmail.tabModes.chat; + + if (chatMode.tabs.length == 1) { + tabmail.selectedTab = chatMode.tabs[0]; + } else { + window.showChatTab(); + } + + is(chatMode.tabs.length, 1, "chat tab is open"); + is(tabmail.selectedTab, chatMode.tabs[0], "chat tab is selected"); + + await new Promise(resolve => setTimeout(resolve)); +} + +async function closeChatTab() { + let tabmail = document.getElementById("tabmail"); + let chatMode = tabmail.tabModes.chat; + + if (chatMode.tabs.length == 1) { + tabmail.closeTab(chatMode.tabs[0]); + } + + is(chatMode.tabs.length, 0, "chat tab is not open"); + + await new Promise(resolve => setTimeout(resolve)); +} + +/** + * Opens a new calendar event or task tab. + * + * @param {string} tabMode - Mode of the new tab, either `calendarEvent` or `calendarTask`. + * @returns {string} - The id of the new tab's panel element. + */ +async function _openNewCalendarItemTab(tabMode) { + let tabmail = document.getElementById("tabmail"); + let itemTabs = tabmail.tabModes[tabMode].tabs; + let previousTabCount = itemTabs.length; + + Services.prefs.setBoolPref("calendar.item.editInTab", true); + let buttonId = "sidePanelNewEvent"; + if (tabMode == "calendarTask") { + await openTasksTab(); + buttonId = "sidePanelNewTask"; + } else { + await CalendarTestUtils.openCalendarTab(window); + } + + let newItemButton = document.getElementById(buttonId); + EventUtils.synthesizeMouseAtCenter(newItemButton, { clickCount: 1 }); + + let newTab = itemTabs[itemTabs.length - 1]; + + is(itemTabs.length, previousTabCount + 1, `new ${tabMode} tab is open`); + is(tabmail.selectedTab, newTab, `new ${tabMode} tab is selected`); + + await BrowserTestUtils.browserLoaded(newTab.iframe); + await new Promise(resolve => setTimeout(resolve)); + return newTab.panel.id; +} + +let openNewCalendarEventTab = _openNewCalendarItemTab.bind(null, "calendarEvent"); +let openNewCalendarTaskTab = _openNewCalendarItemTab.bind(null, "calendarTask"); + +/** + * Selects an existing (open) calendar event or task tab. + * + * @param {string} tabMode - The tab mode, either `calendarEvent` or `calendarTask`. + * @param {string} panelId - The id of the tab's panel element. + */ +async function _selectCalendarItemTab(tabMode, panelId) { + let tabmail = document.getElementById("tabmail"); + let itemTabs = tabmail.tabModes[tabMode].tabs; + let tabToSelect = itemTabs.find(tab => tab.panel.id == panelId); + + ok(tabToSelect, `${tabMode} tab is open`); + + tabmail.selectedTab = tabToSelect; + + is(tabmail.selectedTab, tabToSelect, `${tabMode} tab is selected`); + + await new Promise(resolve => setTimeout(resolve)); +} + +let selectCalendarEventTab = _selectCalendarItemTab.bind(null, "calendarEvent"); +let selectCalendarTaskTab = _selectCalendarItemTab.bind(null, "calendarTask"); + +/** + * Closes a calendar event or task tab. + * + * @param {string} tabMode - The tab mode, either `calendarEvent` or `calendarTask`. + * @param {string} panelId - The id of the panel of the tab to close. + */ +async function _closeCalendarItemTab(tabMode, panelId) { + let tabmail = document.getElementById("tabmail"); + let itemTabs = tabmail.tabModes[tabMode].tabs; + let previousTabCount = itemTabs.length; + let itemTab = itemTabs.find(tab => tab.panel.id == panelId); + + if (itemTab) { + // Tab does not immediately close, so wait for it. + let tabClosedPromise = new Promise(resolve => { + itemTab.tabNode.addEventListener("TabClose", resolve, { once: true }); + }); + tabmail.closeTab(itemTab); + await tabClosedPromise; + } + + is(itemTabs.length, previousTabCount - 1, `${tabMode} tab was closed`); + + await new Promise(resolve => setTimeout(resolve)); +} + +let closeCalendarEventTab = _closeCalendarItemTab.bind(null, "calendarEvent"); +let closeCalendarTaskTab = _closeCalendarItemTab.bind(null, "calendarTask"); + +async function openPreferencesTab() { + const tabmail = document.getElementById("tabmail"); + const prefsMode = tabmail.tabModes.preferencesTab; + + if (prefsMode.tabs.length == 1) { + tabmail.selectedTab = prefsMode.tabs[0]; + } else { + openOptionsDialog(); + } + + is(prefsMode.tabs.length, 1, "preferences tab is open"); + is(tabmail.selectedTab, prefsMode.tabs[0], "preferences tab is selected"); + + await new Promise(resolve => setTimeout(resolve)); +} + +async function closeAddressBookTab() { + let tabmail = document.getElementById("tabmail"); + let abMode = tabmail.tabModes.addressBookTab; + + if (abMode.tabs.length == 1) { + tabmail.closeTab(abMode.tabs[0]); + } + + is(abMode.tabs.length, 0, "address book tab is not open"); + + await new Promise(resolve => setTimeout(resolve)); +} + +async function closePreferencesTab() { + let tabmail = document.getElementById("tabmail"); + let prefsMode = tabmail.tabModes.preferencesTab; + + if (prefsMode.tabs.length == 1) { + tabmail.closeTab(prefsMode.tabs[0]); + } + + is(prefsMode.tabs.length, 0, "preferences tab is not open"); + + await new Promise(resolve => setTimeout(resolve)); +} + +async function openAddonsTab() { + const tabmail = document.getElementById("tabmail"); + const contentMode = tabmail.tabModes.contentTab; + + if (contentMode.tabs.length == 1) { + tabmail.selectedTab = contentMode.tabs[0]; + } else { + openAddonsMgr("addons://list/extension"); + } + + is(contentMode.tabs.length, 1, "addons tab is open"); + is(tabmail.selectedTab, contentMode.tabs[0], "addons tab is selected"); + + await new Promise(resolve => setTimeout(resolve)); +} + +async function closeAddonsTab() { + let tabmail = document.getElementById("tabmail"); + let contentMode = tabmail.tabModes.contentTab; + + if (contentMode.tabs.length == 1) { + tabmail.closeTab(contentMode.tabs[0]); + } + + is(contentMode.tabs.length, 0, "addons tab is not open"); + + await new Promise(resolve => setTimeout(resolve)); +} + +/** + * Create a calendar using the "Create New Calendar" dialog. + * + * @param {string} name - Name for the new calendar. + * @param {object} [data] - Data to enter into the dialog. + * @param {boolean} [data.showReminders] - False to disable reminders. + * @param {string} [data.email] - An email address. + * @param {object} [data.network] - Data for network calendars. + * @param {string} [data.network.location] - A URI (leave undefined for local ICS file). + * @param {boolean} [data.network.offline] - False to disable the cache. + */ +async function createCalendarUsingDialog(name, data = {}) { + /** + * Callback function to interact with the dialog. + * + * @param {nsIDOMWindow} win - The dialog window. + */ + async function useDialog(win) { + let doc = win.document; + let dialogElement = doc.querySelector("dialog"); + let acceptButton = dialogElement.getButton("accept"); + + if (data.network) { + // Choose network calendar type. + doc.querySelector("#calendar-type [value='network']").click(); + acceptButton.click(); + + // Enter a location. + if (data.network.location == undefined) { + let calendarFile = Services.dirsvc.get("TmpD", Ci.nsIFile); + calendarFile.append(name + ".ics"); + let fileURI = Services.io.newFileURI(calendarFile); + data.network.location = fileURI.prePath + fileURI.pathQueryRef; + } + EventUtils.synthesizeMouseAtCenter(doc.querySelector("#network-location-input"), {}, win); + EventUtils.sendString(data.network.location, win); + + // Choose offline support. + if (data.network.offline == undefined) { + data.network.offline = true; + } + let offlineCheckbox = doc.querySelector("#network-cache-checkbox"); + if (!offlineCheckbox.checked) { + EventUtils.synthesizeMouseAtCenter(offlineCheckbox, {}, win); + } + acceptButton.click(); + + // Set up an observer to wait for calendar(s) to be found, before + // clicking the accept button to subscribe to the calendar(s). + let observer = new MutationObserver(mutationList => { + mutationList.forEach(async mutation => { + if (mutation.type === "childList") { + acceptButton.click(); + } + }); + }); + observer.observe(doc.querySelector("#network-calendar-list"), { childList: true }); + } else { + // Choose local calendar type. + doc.querySelector("#calendar-type [value='local']").click(); + acceptButton.click(); + + // Set calendar name. + // Setting the value does not activate the accept button on all platforms, + // so we need to type something in case the field is empty. + let nameInput = doc.querySelector("#local-calendar-name-input"); + if (nameInput.value == "") { + EventUtils.synthesizeMouseAtCenter(nameInput, {}, win); + EventUtils.sendString(name, win); + } + + // Set reminder option. + if (data.showReminders == undefined) { + data.showReminders = true; + } + let localFireAlarmsCheckbox = doc.querySelector("#local-fire-alarms-checkbox"); + if (localFireAlarmsCheckbox.checked != data.showReminders) { + EventUtils.synthesizeMouseAtCenter(localFireAlarmsCheckbox, {}, win); + } + + // Set email account. + if (data.email == undefined) { + data.email = "none"; + } + let emailIdentityMenulist = doc.querySelector("#email-identity-menulist"); + EventUtils.synthesizeMouseAtCenter(emailIdentityMenulist, {}, win); + emailIdentityMenulist.querySelector("menuitem[value='none']").click(); + + // Create the calendar. + acceptButton.click(); + } + } + + let dialogWindowPromise = BrowserTestUtils.promiseAlertDialog( + null, + "chrome://calendar/content/calendar-creation.xhtml", + { callback: useDialog } + ); + // Open the "create new calendar" dialog. + CalendarTestUtils.openCalendarTab(window); + // This double-click must be inside the calendar list but below the list items. + EventUtils.synthesizeMouseAtCenter(document.querySelector("#calendar-list"), { clickCount: 2 }); + return dialogWindowPromise; +} + +const calendarViewsInitialState = CalendarTestUtils.saveCalendarViewsState(window); + +registerCleanupFunction(async () => { + await CalendarTestUtils.restoreCalendarViewsState(window, calendarViewsInitialState); + await closeTasksTab(); + await closeChatTab(); + await closeAddressBookTab(); + await closePreferencesTab(); + await closeAddonsTab(); + + // Close any event or task tabs that are open. + let tabmail = document.getElementById("tabmail"); + let eventTabPanelIds = tabmail.tabModes.calendarEvent.tabs.map(tab => tab.panel.id); + let taskTabPanelIds = tabmail.tabModes.calendarTask.tabs.map(tab => tab.panel.id); + for (let id of eventTabPanelIds) { + await closeCalendarEventTab(id); + } + for (let id of taskTabPanelIds) { + await closeCalendarTaskTab(id); + } + Services.prefs.setBoolPref("calendar.item.editInTab", false); + + Assert.equal(tabmail.tabInfo.length, 1, "all tabs closed"); +}); diff --git a/comm/calendar/test/browser/invitations/browser.ini b/comm/calendar/test/browser/invitations/browser.ini new file mode 100644 index 0000000000..7c7aa6af46 --- /dev/null +++ b/comm/calendar/test/browser/invitations/browser.ini @@ -0,0 +1,31 @@ +[default] +head = ../head.js 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_attachedPublishEvent.js] +[browser_icsAttachment.js] +skip-if = os == 'win' +[browser_identityPrompt.js] +[browser_imipBar.js] +[browser_imipBarCancel.js] +[browser_imipBarEmail.js] +[browser_imipBarExceptionCancel.js] +[browser_imipBarExceptionOnly.js] +[browser_imipBarExceptions.js] +[browser_imipBarRepeat.js] +[browser_imipBarRepeatCancel.js] +[browser_imipBarRepeatUpdates.js] +[browser_imipBarUpdates.js] +[browser_invitationDisplayNew.js] +[browser_unsupportedFreq.js] diff --git a/comm/calendar/test/browser/invitations/browser_attachedPublishEvent.js b/comm/calendar/test/browser/invitations/browser_attachedPublishEvent.js new file mode 100644 index 0000000000..af121a8032 --- /dev/null +++ b/comm/calendar/test/browser/invitations/browser_attachedPublishEvent.js @@ -0,0 +1,72 @@ +/* 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/. */ + +/** + * Test that attached events - NOT invites - works properly. + * These are attached VCALENDARs that have METHOD:PUBLISH. + */ +"use strict"; + +var { CalendarTestUtils } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarTestUtils.jsm" +); + +var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm"); + +var gCalendar; + +/** + * Initialize account, identity and calendar. + */ +add_setup(async function () { + let receiverAcct = MailServices.accounts.createAccount(); + receiverAcct.incomingServer = MailServices.accounts.createIncomingServer( + "receiver", + "example.com", + "imap" + ); + let receiverIdentity = MailServices.accounts.createIdentity(); + receiverIdentity.email = "john.doe@example.com"; + receiverAcct.addIdentity(receiverIdentity); + gCalendar = CalendarTestUtils.createCalendar("EventTestCal"); + + registerCleanupFunction(() => { + CalendarTestUtils.removeCalendar(gCalendar); + MailServices.accounts.removeAccount(receiverAcct, true); + }); +}); + +/** + * Test that opening a message containing an event with iTIP method "PUBLISH" + * shows the correct UI. + * The party crashing dialog should not show. + */ +add_task(async function test_event_from_eml() { + let file = new FileUtils.File(getTestFilePath("data/message-non-invite.eml")); + + let win = await openMessageFromFile(file); + let aboutMessage = win.document.getElementById("messageBrowser").contentWindow; + let imipBar = aboutMessage.document.getElementById("imip-bar"); + + await TestUtils.waitForCondition(() => !imipBar.collapsed); + info("Ok, iMIP bar is showing"); + + let imipAddButton = aboutMessage.document.getElementById("imipAddButton"); + Assert.ok(!imipAddButton.hidden, "Add button should show"); + + EventUtils.synthesizeMouseAtCenter(imipAddButton, {}, aboutMessage); + + // Make sure the event got added, without showing the party crashing dialog. + await TestUtils.waitForCondition(async () => { + let event = await gCalendar.getItem("1e5fd4e6-bc52-439c-ac76-40da54f57c77@secure.example.com"); + return event; + }); + + await TestUtils.waitForCondition(() => imipAddButton.hidden, "Add button should hide"); + + let imipDetailsButton = aboutMessage.document.getElementById("imipDetailsButton"); + Assert.ok(!imipDetailsButton.hidden, "Details button should show"); + + await BrowserTestUtils.closeWindow(win); +}); diff --git a/comm/calendar/test/browser/invitations/browser_icsAttachment.js b/comm/calendar/test/browser/invitations/browser_icsAttachment.js new file mode 100644 index 0000000000..11bde9144d --- /dev/null +++ b/comm/calendar/test/browser/invitations/browser_icsAttachment.js @@ -0,0 +1,71 @@ +/* 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/. */ + +/** + * Test TB can be set as default calendar app. + */ + +/** + * Set TB as default calendar app. + */ +add_setup(function () { + let shellSvc = Cc["@mozilla.org/mail/shell-service;1"].getService(Ci.nsIShellService); + shellSvc.setDefaultClient(false, shellSvc.CALENDAR); + ok(shellSvc.isDefaultClient(false, shellSvc.CALENDAR), "setDefaultClient works"); +}); + +/** + * Test when opening an ics attachment, TB should be shown as an option. + */ +add_task(async function test_ics_attachment() { + let file = new FileUtils.File(getTestFilePath("data/message-containing-event.eml")); + let msgWindow = await openMessageFromFile(file); + let aboutMessage = msgWindow.document.getElementById("messageBrowser").contentWindow; + let promise = BrowserTestUtils.promiseAlertDialog( + null, + "chrome://mozapps/content/downloads/unknownContentType.xhtml", + { + async callback(dialogWindow) { + ok(true, "unknownContentType dialog opened"); + let dialogElement = dialogWindow.document.querySelector("dialog"); + let acceptButton = dialogElement.getButton("accept"); + return new Promise(resolve => { + let observer = new MutationObserver(mutationList => { + mutationList.forEach(async mutation => { + if (mutation.attributeName == "disabled" && !acceptButton.disabled) { + is(acceptButton.disabled, false, "Accept button enabled"); + if (AppConstants.platform != "macosx") { + let bundle = Services.strings.createBundle( + "chrome://branding/locale/brand.properties" + ); + let name = bundle.GetStringFromName("brandShortName"); + // macOS requires extra step in Finder to set TB as default calendar app. + ok( + dialogWindow.document.getElementById("openHandler").label.includes(name), + `${name} is the default calendar app` + ); + } + + // Should really click acceptButton and test + // calender-ics-file-dialog is opened. But on local, a new TB + // instance is started and this test will fail. + dialogElement.getButton("cancel").click(); + resolve(); + } + }); + }); + observer.observe(acceptButton, { attributes: true }); + }); + }, + } + ); + EventUtils.synthesizeMouseAtCenter( + aboutMessage.document.getElementById("attachmentName"), + {}, + aboutMessage + ); + await promise; + + await BrowserTestUtils.closeWindow(msgWindow); +}); diff --git a/comm/calendar/test/browser/invitations/browser_identityPrompt.js b/comm/calendar/test/browser/invitations/browser_identityPrompt.js new file mode 100644 index 0000000000..e2d6fe3115 --- /dev/null +++ b/comm/calendar/test/browser/invitations/browser_identityPrompt.js @@ -0,0 +1,144 @@ +/* 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 calender-itip-identity dialog. + */ + +"use strict"; + +var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm"); +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); + +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); +var { CalendarTestUtils } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarTestUtils.jsm" +); + +let receiverAcct; +let receiverIdentity; +let gInbox; +let calendar; + +registerCleanupFunction(() => { + CalendarTestUtils.removeCalendar(calendar); + MailServices.accounts.removeIncomingServer(receiverAcct.incomingServer, true); + MailServices.accounts.removeAccount(receiverAcct); +}); + +/** + * Initialize account, identity and calendar. + */ +add_setup(async function () { + if (MailServices.accounts.accounts.length == 0) { + MailServices.accounts.createLocalMailAccount(); + } + + let rootFolder = MailServices.accounts.localFoldersServer.rootFolder; + if (!rootFolder.containsChildNamed("Inbox")) { + rootFolder.createSubfolder("Inbox", null); + } + gInbox = rootFolder.getChildNamed("Inbox"); + + receiverAcct = MailServices.accounts.createAccount(); + receiverAcct.incomingServer = MailServices.accounts.createIncomingServer( + "receiver", + "example.com", + "imap" + ); + receiverIdentity = MailServices.accounts.createIdentity(); + receiverIdentity.email = "receiver@example.com"; + receiverAcct.addIdentity(receiverIdentity); + + calendar = CalendarTestUtils.createCalendar("Test"); + + let copyListener = new PromiseTestUtils.PromiseCopyListener(); + MailServices.copy.copyFileMessage( + new FileUtils.File(getTestFilePath("data/meet-meeting-invite.eml")), + gInbox, + null, + false, + 0, + "", + copyListener, + null + ); + await copyListener.promise; +}); + +/** + * Tests that the identity prompt shows when accepting an invitation to an + * event with an identity no calendar is configured to use. + */ +add_task(async function testInvitationIdentityPrompt() { + let tabmail = document.getElementById("tabmail"); + let about3Pane = tabmail.currentAbout3Pane; + about3Pane.displayFolder(gInbox.URI); + about3Pane.threadTree.selectedIndex = 0; + + let dialogPromise = BrowserTestUtils.promiseAlertDialog( + null, + "chrome://calendar/content/calendar-itip-identity-dialog.xhtml", + { + async callback(win) { + // Select the identity we want to use. + let menulist = win.document.getElementById("identity-menu"); + for (let i = 0; i < menulist.itemCount; i++) { + let target = menulist.getItemAtIndex(i); + if (target.value == receiverIdentity.fullAddress) { + menulist.selectedIndex = i; + } + } + + win.document.querySelector("dialog").getButton("accept").click(); + }, + } + ); + + // Override this function to intercept the attempt to send the email out. + let sendItemsArgs = []; + let getImipTransport = cal.itip.getImipTransport; + cal.itip.getImipTransport = () => ({ + scheme: "mailto", + type: "email", + sendItems(receipientArray, item, sender) { + sendItemsArgs = [receipientArray, item, sender]; + return true; + }, + }); + + let aboutMessage = tabmail.currentAboutMessage; + let acceptButton = aboutMessage.document.getElementById("imipAcceptButton"); + await TestUtils.waitForCondition( + () => BrowserTestUtils.is_visible(acceptButton), + "waiting for accept button to become visible" + ); + EventUtils.synthesizeMouseAtCenter(acceptButton, {}, aboutMessage); + await dialogPromise; + + let event; + await TestUtils.waitForCondition(async () => { + event = await calendar.getItem("65m17hsdolmotv3kvmrtg40ont@google.com"); + return event && sendItemsArgs.length; + }); + + // Restore this function. + cal.itip.getImipTransport = getImipTransport; + + let id = `mailto:${receiverIdentity.email}`; + Assert.ok(event, "event was added to the calendar successfully"); + Assert.ok(event.getAttendeeById(id), "selected identity was added to the attendee list"); + Assert.equal( + event.getProperty("X-MOZ-INVITED-ATTENDEE"), + id, + "X-MOZ-INVITED-ATTENDEE is set to the selected identity" + ); + + let [recipientArray, , sender] = sendItemsArgs; + Assert.equal(recipientArray.length, 1, "one recipient for the reply"); + Assert.equal(recipientArray[0].id, "mailto:example@gmail.com", "recipient is event organizer"); + Assert.equal(sender.id, id, "sender is the identity selected"); +}); diff --git a/comm/calendar/test/browser/invitations/browser_imipBar.js b/comm/calendar/test/browser/invitations/browser_imipBar.js new file mode 100644 index 0000000000..c9a21a6d2b --- /dev/null +++ b/comm/calendar/test/browser/invitations/browser_imipBar.js @@ -0,0 +1,199 @@ +/* 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 receiving event invitations via the imip-bar. + */ +"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 { CalendarTestUtils } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarTestUtils.jsm" +); + +let identity; +let calendar; +let transport; + +/** + * Initialize account, identity and calendar. + */ +add_setup(async function () { + let account = MailServices.accounts.createAccount(); + account.incomingServer = MailServices.accounts.createIncomingServer( + "receiver", + "example.com", + "imap" + ); + identity = MailServices.accounts.createIdentity(); + identity.email = "receiver@example.com"; + account.addIdentity(identity); + + await CalendarTestUtils.setCalendarView(window, "month"); + window.goToDate(cal.createDateTime("20220316T191602Z")); + + calendar = CalendarTestUtils.createCalendar("Test"); + transport = new EmailTransport(account, identity); + + let getImipTransport = cal.itip.getImipTransport; + cal.itip.getImipTransport = () => transport; + + let deleteMgr = Cc["@mozilla.org/calendar/deleted-items-manager;1"].getService( + Ci.calIDeletedItems + ).wrappedJSObject; + let markDeleted = deleteMgr.markDeleted; + deleteMgr.markDeleted = () => {}; + + registerCleanupFunction(() => { + MailServices.accounts.removeAccount(account, true); + cal.itip.getImipTransport = getImipTransport; + deleteMgr.markDeleted = markDeleted; + CalendarTestUtils.removeCalendar(calendar); + }); +}); + +/** + * Tests accepting an invitation and sending a response. + */ +add_task(async function testAcceptWithResponse() { + transport.reset(); + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/single-event.eml"))); + await clickAction(win, "imipAcceptButton"); + + let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item; + await doImipBarActionTest( + { + calendar, + transport, + identity, + partStat: "ACCEPTED", + }, + event + ); + + await calendar.deleteItem(event); + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Tests tentatively accepting an invitation and sending a response. + */ +add_task(async function testTentativeWithResponse() { + transport.reset(); + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/single-event.eml"))); + await clickAction(win, "imipTentativeButton"); + + let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item; + await doImipBarActionTest( + { + calendar, + transport, + identity, + partStat: "TENTATIVE", + }, + event + ); + + await calendar.deleteItem(event); + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Tests declining an invitation and sending a response. + */ +add_task(async function testDeclineWithResponse() { + transport.reset(); + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/single-event.eml"))); + await clickAction(win, "imipDeclineButton"); + + let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item; + await doImipBarActionTest( + { + calendar, + transport, + identity, + partStat: "DECLINED", + }, + event + ); + + await calendar.deleteItem(event); + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Tests accepting an invitation without sending a response. + */ +add_task(async function testAcceptWithoutResponse() { + transport.reset(); + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/single-event.eml"))); + await clickMenuAction(win, "imipAcceptButton", "imipAcceptButton_AcceptDontSend"); + + let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item; + await doImipBarActionTest( + { + calendar, + transport, + identity, + partStat: "ACCEPTED", + noReply: true, + }, + event + ); + await calendar.deleteItem(event); + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Tests tentatively accepting an invitation without sending a response. + */ +add_task(async function testTentativeWithoutResponse() { + transport.reset(); + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/single-event.eml"))); + await clickMenuAction(win, "imipTentativeButton", "imipTentativeButton_TentativeDontSend"); + + let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item; + await doImipBarActionTest( + { + calendar, + transport, + identity, + partStat: "TENTATIVE", + noReply: true, + }, + event + ); + + await calendar.deleteItem(event); + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Tests declining an invitation without sending a response. + */ +add_task(async function testDeclineWithoutResponse() { + transport.reset(); + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/single-event.eml"))); + await clickMenuAction(win, "imipDeclineButton", "imipDeclineButton_DeclineDontSend"); + + let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item; + await doImipBarActionTest( + { + calendar, + transport, + identity, + partStat: "DECLINED", + noReply: true, + }, + event + ); + + await calendar.deleteItem(event); + await BrowserTestUtils.closeWindow(win); +}); diff --git a/comm/calendar/test/browser/invitations/browser_imipBarCancel.js b/comm/calendar/test/browser/invitations/browser_imipBarCancel.js new file mode 100644 index 0000000000..3cde7d4656 --- /dev/null +++ b/comm/calendar/test/browser/invitations/browser_imipBarCancel.js @@ -0,0 +1,129 @@ +/* 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 processing cancellations via the imip-bar. + */ + +"use strict"; + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); +var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm"); + +var { CalendarTestUtils } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarTestUtils.jsm" +); + +let identity; +let calendar; +let transport; + +/** + * Initialize account, identity and calendar. + */ +add_setup(async function () { + let account = MailServices.accounts.createAccount(); + account.incomingServer = MailServices.accounts.createIncomingServer( + "receiver", + "example.com", + "imap" + ); + identity = MailServices.accounts.createIdentity(); + identity.email = "receiver@example.com"; + account.addIdentity(identity); + + await CalendarTestUtils.setCalendarView(window, "month"); + window.goToDate(cal.createDateTime("20220316T191602Z")); + + calendar = CalendarTestUtils.createCalendar("Test"); + transport = new EmailTransport(account, identity); + + let getImipTransport = cal.itip.getImipTransport; + cal.itip.getImipTransport = () => transport; + + let deleteMgr = Cc["@mozilla.org/calendar/deleted-items-manager;1"].getService( + Ci.calIDeletedItems + ).wrappedJSObject; + let markDeleted = deleteMgr.markDeleted; + deleteMgr.markDeleted = () => {}; + + registerCleanupFunction(() => { + MailServices.accounts.removeAccount(account, true); + cal.itip.getImipTransport = getImipTransport; + deleteMgr.markDeleted = markDeleted; + CalendarTestUtils.removeCalendar(calendar); + }); +}); + +/** + * Tests accepting a cancellation to an already accepted event. + */ +add_task(async function testCancelAccepted() { + transport.reset(); + let invite = new FileUtils.File(getTestFilePath("data/single-event.eml")); + let win = await openImipMessage(invite); + await clickAction(win, "imipAcceptButton"); + + let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item; + await BrowserTestUtils.closeWindow(win); + await doCancelTest({ + transport, + calendar, + event, + }); +}); + +/** + * Tests accepting a cancellation to tentatively accepted event. + */ +add_task(async function testCancelTentative() { + transport.reset(); + let invite = new FileUtils.File(getTestFilePath("data/single-event.eml")); + let win = await openImipMessage(invite); + await clickAction(win, "imipTentativeButton"); + + let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item; + await BrowserTestUtils.closeWindow(win); + await doCancelTest({ + transport, + calendar, + event, + }); +}); + +/** + * Tests accepting a cancellation to an already declined event. + */ +add_task(async function testCancelDeclined() { + transport.reset(); + let invite = new FileUtils.File(getTestFilePath("data/single-event.eml")); + let win = await openImipMessage(invite); + await clickAction(win, "imipDeclineButton"); + + let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item; + await BrowserTestUtils.closeWindow(win); + await doCancelTest({ + transport, + calendar, + event, + }); +}); + +/** + * Tests the handling of a cancellation when the event was not processed + * previously. + */ +add_task(async function testUnprocessedCancel() { + transport.reset(); + let invite = new FileUtils.File(getTestFilePath("data/cancel-single-event.eml")); + let win = await openImipMessage(invite); + + // There should be no buttons present because there is no action to take. + // Note: the imip-bar message "This message contains an event that has already been processed" is + // misleading. + for (let button of [...win.document.querySelectorAll("#imip-view-toolbar > toolbarbutton")]) { + Assert.ok(button.hidden, `${button.id} is hidden`); + } + await BrowserTestUtils.closeWindow(win); +}); diff --git a/comm/calendar/test/browser/invitations/browser_imipBarEmail.js b/comm/calendar/test/browser/invitations/browser_imipBarEmail.js new file mode 100644 index 0000000000..a3816b65dd --- /dev/null +++ b/comm/calendar/test/browser/invitations/browser_imipBarEmail.js @@ -0,0 +1,168 @@ +/* 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/. */ + +/** + * Test that the IMIP bar behaves properly for eml files with invites. + */ + +/* eslint-disable @microsoft/sdl/no-insecure-url */ + +function getFileFromChromeURL(leafName) { + let ChromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIChromeRegistry); + + let url = Services.io.newURI(getRootDirectory(gTestPath) + leafName); + info(url.spec); + let fileURL = ChromeRegistry.convertChromeURL(url).QueryInterface(Ci.nsIFileURL); + return fileURL.file; +} + +/** + * Test that when opening a message containing a Teams meeting invite + * works as it should. + */ +add_task(async function test_event_from_eml() { + let file = getFileFromChromeURL("data/teams-meeting-invite.eml"); + + let msgWindow = await openMessageFromFile(file); + let aboutMessage = msgWindow.document.getElementById("messageBrowser").contentWindow; + + await TestUtils.waitForCondition( + () => !aboutMessage.document.getElementById("imip-bar").collapsed + ); + info("Ok, iMIP bar is showing"); + + // The contentDocument has both the imipHTMLDetails HTML part generated by us, + // and the regular HTML part generated by the sender (the server). + let links = [ + ...msgWindow.content.document.getElementById("imipHTMLDetails").querySelectorAll("a"), + ]; + + Assert.equal(links.length, 3, "The 3 links should show"); + + // Check the links and their text + Assert.equal( + links[0].href, + "https://teams.microsoft.com/l/meetup-join/19%3ameeting_MGU5NmI2ZGYtOWZmOC00Y2ZmLWJlOTItNjUxNjA5YjUyYTYy%40thread.v2/0?context=%7b%22Tid%22%3a%222fd0c1c5-28e1-40c4-9f0d-a0363ca80a3c%22%2c%22Oid%22%3a%2214464d09-ceb8-458c-a61c-717f1e5c66c5%22%7d", + "link0 href" + ); + Assert.equal( + links[0].textContent, + "<https://teams.microsoft.com/l/meetup-join/19%3ameeting_MGU5NmI2ZGYtOWZmOC00Y2ZmLWJlOTItNjUxNjA5YjUyYTYy%40thread.v2/0?context=%7b%22Tid%22%3a%222fd0c1c5-28e1-40c4-9f0d-a0363ca80a3c%22%2c%22Oid%22%3a%2214464d09-ceb8-458c-a61c-717f1e5c66c5%22%7d>", + "link0 textContent" + ); + + Assert.equal(links[1].href, "https://aka.ms/JoinTeamsMeeting", "link1 href"); + Assert.equal(links[1].textContent, "<https://aka.ms/JoinTeamsMeeting>", "link1 textContent"); + + Assert.equal( + links[2].href, + "https://teams.microsoft.com/meetingOptions/?organizerId=14464d09-ceb8-458c-a61c-717f1e5c66c5&tenantId=2fd0c1c5-28e1-40c4-9f0d-a0363ca80a3c&threadId=19_meeting_MGU5NmI2ZGYtOWZmOC00Y2ZmLWJlOTItNjUxNjA5YjUyYTYy@thread.v2&messageId=0&language=fi-FI", + "link2 href" + ); + Assert.equal( + links[2].textContent, + "<https://teams.microsoft.com/meetingOptions/?organizerId=14464d09-ceb8-458c-a61c-717f1e5c66c5&tenantId=2fd0c1c5-28e1-40c4-9f0d-a0363ca80a3c&threadId=19_meeting_MGU5NmI2ZGYtOWZmOC00Y2ZmLWJlOTItNjUxNjA5YjUyYTYy@thread.v2&messageId=0&language=fi-FI>", + "link2 textContent" + ); + + await BrowserTestUtils.closeWindow(msgWindow); + + Assert.ok(true, "test_event_from_eml test ran to completion"); +}); + +/** + * Test that when opening a message containing a Meet meeting invite + * works as it should. + */ +add_task(async function test_event_from_eml() { + let file = getFileFromChromeURL("data/meet-meeting-invite.eml"); + + let msgWindow = await openMessageFromFile(file); + let aboutMessage = msgWindow.document.getElementById("messageBrowser").contentWindow; + + await TestUtils.waitForCondition( + () => !aboutMessage.document.getElementById("imip-bar").collapsed + ); + info("Ok, iMIP bar is showing"); + + // The contentDocument has both the imipHTMLDetails HTML part generated by us, + // and the regular HTML part generated by the sender (the server). + let links = [ + ...msgWindow.content.document.getElementById("imipHTMLDetails").querySelectorAll("a"), + ]; + + Assert.equal(links.length, 4, "The 4 links should show"); + + // Check the links and their text + Assert.equal(links[0].href, "mailto:foo@example.com", "link0 href"); + Assert.equal(links[0].textContent, "<foo@example.com>", "link0 textContent"); + + Assert.equal(links[1].href, "http://example.com/?foo=bar", "link1 href"); + Assert.equal(links[1].textContent, "http://example.com?foo=bar", "link1 textContent"); + + Assert.equal(links[2].href, "https://meet.google.com/pyb-ndcu-hhc", "link1 href"); + Assert.equal(links[2].textContent, "https://meet.google.com/pyb-ndcu-hhc", "link1 textContent"); + + Assert.equal( + links[3].href, + "https://calendar.google.com/calendar/event?action=VIEW&eid=NjVtMTdoc2RvbG1vdHYza3ZtcnRnNDBvbnQgbWFnbnVzLm1lbGluQGh1dC5maQ&tok=MjEjYmVydGF0aGVib3RAZ21haWwuY29tZTg2NGFjYmNjYWE1MjVlZWJmY2UzYmRmMDAyNWU0MDkzNDAxZjRhZg&ctz=Europe%2FHelsinki&hl=sv&es=1", + "link2 href" + ); + Assert.equal( + links[3].textContent, + "https://calendar.google.com/calendar/event?action=VIEW&eid=NjVtMTdoc2RvbG1vdHYza3ZtcnRnNDBvbnQgbWFnbnVzLm1lbGluQGh1dC5maQ&tok=MjEjYmVydGF0aGVib3RAZ21haWwuY29tZTg2NGFjYmNjYWE1MjVlZWJmY2UzYmRmMDAyNWU0MDkzNDAxZjRhZg&ctz=Europe%2FHelsinki&hl=sv&es=1", + "link2 textContent" + ); + + await BrowserTestUtils.closeWindow(msgWindow); + + Assert.ok(true, "test_event_from_eml test ran to completion"); +}); + +/** + * Test that when opening a message containing an outlook invite with "empty" + * content works as it should. + */ +add_task(async function test_outlook_event_from_eml() { + let file = getFileFromChromeURL("data/outlook-test-invite.eml"); + + let msgWindow = await openMessageFromFile(file); + let aboutMessage = msgWindow.document.getElementById("messageBrowser").contentWindow; + + await TestUtils.waitForCondition( + () => !aboutMessage.document.getElementById("imip-bar").collapsed + ); + info("Ok, iMIP bar is showing"); + + let details = msgWindow.content.document.getElementById("imipHTMLDetails"); + + Assert.equal( + details.getAttribute("open"), + "open", + "Details should be expanded when the message doesn't include good details" + ); + + await BrowserTestUtils.closeWindow(msgWindow); + + Assert.ok(true, "test_outlook_event_from_eml test ran to completion"); +}); + +/** + * Test that when opening a message containing an event, the IMIP bar shows. + */ +add_task(async function test_event_from_eml() { + let file = getFileFromChromeURL("data/message-containing-event.eml"); + + let msgWindow = await openMessageFromFile(file); + let aboutMessage = msgWindow.document.getElementById("messageBrowser").contentWindow; + + await TestUtils.waitForCondition( + () => !aboutMessage.document.getElementById("imip-bar").collapsed + ); + info("Ok, iMIP bar is showing"); + + await BrowserTestUtils.closeWindow(msgWindow); + + Assert.ok(true, "test_event_from_eml test ran to completion"); +}); diff --git a/comm/calendar/test/browser/invitations/browser_imipBarExceptionCancel.js b/comm/calendar/test/browser/invitations/browser_imipBarExceptionCancel.js new file mode 100644 index 0000000000..7800e742ca --- /dev/null +++ b/comm/calendar/test/browser/invitations/browser_imipBarExceptionCancel.js @@ -0,0 +1,137 @@ +/* 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 processing cancellations to recurring event exceptions. + */ + +"use strict"; + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); +var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm"); + +var { CalendarTestUtils } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarTestUtils.jsm" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + CalEvent: "resource:///modules/CalEvent.jsm", +}); + +let identity; +let calendar; +let transport; + +/** + * Initialize account, identity and calendar. + */ +add_setup(async function () { + requestLongerTimeout(3); + let account = MailServices.accounts.createAccount(); + account.incomingServer = MailServices.accounts.createIncomingServer( + "receiver", + "example.com", + "imap" + ); + identity = MailServices.accounts.createIdentity(); + identity.email = "receiver@example.com"; + account.addIdentity(identity); + + await CalendarTestUtils.setCalendarView(window, "month"); + window.goToDate(cal.createDateTime("20220316T191602Z")); + + calendar = CalendarTestUtils.createCalendar("Test"); + transport = new EmailTransport(account, identity); + let getImipTransport = cal.itip.getImipTransport; + cal.itip.getImipTransport = () => transport; + + let deleteMgr = Cc["@mozilla.org/calendar/deleted-items-manager;1"].getService( + Ci.calIDeletedItems + ).wrappedJSObject; + + let markDeleted = deleteMgr.markDeleted; + deleteMgr.markDeleted = () => {}; + + registerCleanupFunction(() => { + MailServices.accounts.removeAccount(account, true); + cal.itip.getImipTransport = getImipTransport; + deleteMgr.markDeleted = markDeleted; + CalendarTestUtils.removeCalendar(calendar); + }); +}); + +/** + * Tests cancelling an exception works. + */ +add_task(async function testCancelException() { + for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) { + await doCancelExceptionTest({ + calendar, + transport, + identity, + partStat, + recurrenceId: "20220317T110000Z", + isRecurring: true, + }); + } +}); + +/** + * Tests cancelling an event with only an exception processed works. + */ +add_task(async function testCancelExceptionOnly() { + for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) { + let win = await openImipMessage( + new FileUtils.File(getTestFilePath("data/exception-major.eml")) + ); + await clickAction(win, actionIds.single.button[partStat]); + + let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 5, 1)).item; + await BrowserTestUtils.closeWindow(win); + await doCancelTest({ + calendar, + event, + transport, + identity, + }); + } +}); + +/** + * Tests processing a cancellation for a recurring event works when only an + * exception was processed previously. + */ +add_task(async function testCancelSeriesWithExceptionOnly() { + for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) { + let win = await openImipMessage( + new FileUtils.File(getTestFilePath("data/exception-major.eml")) + ); + await clickMenuAction( + win, + actionIds.single.button[partStat], + actionIds.single.noReply[partStat] + ); + + let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 5, 1)).item; + await BrowserTestUtils.closeWindow(win); + + let cancel = new FileUtils.File(getTestFilePath("data/cancel-repeat-event.eml")); + let cancelWin = await openImipMessage(cancel); + let aboutMessage = cancelWin.document.getElementById("messageBrowser").contentWindow; + + let deleteButton = aboutMessage.document.getElementById("imipDeleteButton"); + Assert.ok(!deleteButton.hidden, `#${deleteButton.id} button shown`); + EventUtils.synthesizeMouseAtCenter(deleteButton, {}, aboutMessage); + await BrowserTestUtils.closeWindow(cancelWin); + await CalendarTestUtils.monthView.waitForNoItemAt(window, 3, 5, 1); + Assert.ok(!(await calendar.getItem(event.id)), "event was deleted"); + + 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"); + } +}); diff --git a/comm/calendar/test/browser/invitations/browser_imipBarExceptionOnly.js b/comm/calendar/test/browser/invitations/browser_imipBarExceptionOnly.js new file mode 100644 index 0000000000..88ad0b3c41 --- /dev/null +++ b/comm/calendar/test/browser/invitations/browser_imipBarExceptionOnly.js @@ -0,0 +1,262 @@ +/* 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 receiving an invitation exception but the original event was not + * processed first. + */ +"use strict"; + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); +var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm"); + +var { CalendarTestUtils } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarTestUtils.jsm" +); + +let identity; +let calendar; +let transport; + +/** + * Initialize account, identity and calendar. + */ +add_setup(async function () { + requestLongerTimeout(5); + let account = MailServices.accounts.createAccount(); + account.incomingServer = MailServices.accounts.createIncomingServer( + "receiver", + "example.com", + "imap" + ); + identity = MailServices.accounts.createIdentity(); + identity.email = "receiver@example.com"; + account.addIdentity(identity); + + await CalendarTestUtils.setCalendarView(window, "month"); + window.goToDate(cal.createDateTime("20220316T191602Z")); + + calendar = CalendarTestUtils.createCalendar("Test"); + transport = new EmailTransport(account, identity); + + let getImipTransport = cal.itip.getImipTransport; + cal.itip.getImipTransport = () => transport; + + let deleteMgr = Cc["@mozilla.org/calendar/deleted-items-manager;1"].getService( + Ci.calIDeletedItems + ).wrappedJSObject; + let markDeleted = deleteMgr.markDeleted; + deleteMgr.markDeleted = () => {}; + + registerCleanupFunction(() => { + MailServices.accounts.removeAccount(account, true); + cal.itip.getImipTransport = getImipTransport; + deleteMgr.markDeleted = markDeleted; + CalendarTestUtils.removeCalendar(calendar); + }); +}); + +/** + * Tests accepting a minor exception and sending a response. + */ +add_task(async function testMinorAcceptWithResponse() { + transport.reset(); + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/exception-minor.eml"))); + await clickAction(win, "imipAcceptButton"); + await doExceptionOnlyTest({ + calendar, + transport, + identity, + partStat: "ACCEPTED", + }); + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Tests tentatively accepting a minor exception and sending a response. + */ +add_task(async function testMinorTentativeWithResponse() { + transport.reset(); + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/exception-minor.eml"))); + await clickAction(win, "imipTentativeButton"); + await doExceptionOnlyTest({ + calendar, + transport, + identity, + partStat: "TENTATIVE", + }); + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Tests declining a minor exception and sending a response. + */ +add_task(async function testMinorDeclineWithResponse() { + transport.reset(); + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/exception-minor.eml"))); + await clickAction(win, "imipDeclineButton"); + await doExceptionOnlyTest({ + calendar, + transport, + identity, + partStat: "DECLINED", + }); + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Tests accepting a minor exception without sending a response. + */ +add_task(async function testMinorAcceptWithoutResponse() { + transport.reset(); + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/exception-minor.eml"))); + await clickMenuAction(win, "imipAcceptButton", "imipAcceptButton_AcceptDontSend"); + await doExceptionOnlyTest({ + calendar, + transport, + identity, + partStat: "ACCEPTED", + noReply: true, + }); + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Tests tentatively accepting a minor exception without sending a response. + */ +add_task(async function testMinorTentativeWithoutResponse() { + transport.reset(); + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/exception-minor.eml"))); + await clickMenuAction(win, "imipTentativeButton", "imipTentativeButton_TentativeDontSend"); + await doExceptionOnlyTest({ + calendar, + transport, + identity, + partStat: "TENTATIVE", + noReply: true, + }); + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Tests declining a minor exception without sending a response. + */ +add_task(async function testMinorDeclineWithoutResponse() { + transport.reset(); + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/exception-minor.eml"))); + await clickMenuAction(win, "imipDeclineButton", "imipDeclineButton_DeclineDontSend"); + await doExceptionOnlyTest({ + calendar, + transport, + identity, + partStat: "DECLINED", + noReply: true, + }); + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Tests accepting a major exception and sending a response. + */ +add_task(async function testMajorAcceptWithResponse() { + transport.reset(); + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/exception-major.eml"))); + await clickAction(win, "imipAcceptButton"); + await doExceptionOnlyTest({ + calendar, + transport, + identity, + partStat: "ACCEPTED", + isMajor: true, + }); + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Tests tentatively accepting a major exception and sending a response. + */ +add_task(async function testMajorTentativeWithResponse() { + transport.reset(); + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/exception-major.eml"))); + await clickAction(win, "imipTentativeButton"); + await doExceptionOnlyTest({ + calendar, + transport, + identity, + partStat: "TENTATIVE", + isMajor: true, + }); + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Tests declining a major exception and sending a response. + */ +add_task(async function testMajorDeclineWithResponse() { + transport.reset(); + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/exception-major.eml"))); + await clickAction(win, "imipDeclineButton"); + await doExceptionOnlyTest({ + calendar, + transport, + identity, + partStat: "DECLINED", + isMajor: true, + }); + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Tests accepting a major exception without sending a response. + */ +add_task(async function testMajorAcceptWithoutResponse() { + transport.reset(); + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/exception-major.eml"))); + await clickMenuAction(win, "imipAcceptButton", "imipAcceptButton_AcceptDontSend"); + await doExceptionOnlyTest({ + calendar, + transport, + identity, + partStat: "ACCEPTED", + noReply: true, + isMajor: true, + }); + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Tests tentatively accepting a major exception without sending a response. + */ +add_task(async function testMajorTentativeWithoutResponse() { + transport.reset(); + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/exception-major.eml"))); + await clickMenuAction(win, "imipTentativeButton", "imipTentativeButton_TentativeDontSend"); + await doExceptionOnlyTest({ + calendar, + transport, + identity, + partStat: "TENTATIVE", + noReply: true, + isMajor: true, + }); + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Tests declining a major exception without sending a response. + */ +add_task(async function testMajorDeclineWithoutResponse() { + transport.reset(); + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/exception-major.eml"))); + await clickMenuAction(win, "imipDeclineButton", "imipDeclineButton_DeclineDontSend"); + await doExceptionOnlyTest({ + calendar, + transport, + identity, + partStat: "DECLINED", + noReply: true, + isMajor: true, + }); + await BrowserTestUtils.closeWindow(win); +}); diff --git a/comm/calendar/test/browser/invitations/browser_imipBarExceptions.js b/comm/calendar/test/browser/invitations/browser_imipBarExceptions.js new file mode 100644 index 0000000000..2cdf18ed59 --- /dev/null +++ b/comm/calendar/test/browser/invitations/browser_imipBarExceptions.js @@ -0,0 +1,288 @@ +/* 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 handling exceptions to recurring event invitations via the imip-bar. + */ + +"use strict"; + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); +var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm"); + +var { CalendarTestUtils } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarTestUtils.jsm" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + CalEvent: "resource:///modules/CalEvent.jsm", +}); + +let identity; +let calendar; +let transport; + +/** + * Initialize account, identity and calendar. + */ +add_setup(async function () { + requestLongerTimeout(5); + let account = MailServices.accounts.createAccount(); + account.incomingServer = MailServices.accounts.createIncomingServer( + "receiver", + "example.com", + "imap" + ); + identity = MailServices.accounts.createIdentity(); + identity.email = "receiver@example.com"; + account.addIdentity(identity); + + await CalendarTestUtils.setCalendarView(window, "month"); + window.goToDate(cal.createDateTime("20220316T191602Z")); + + calendar = CalendarTestUtils.createCalendar("Test"); + transport = new EmailTransport(account, identity); + let getImipTransport = cal.itip.getImipTransport; + cal.itip.getImipTransport = () => transport; + + let deleteMgr = Cc["@mozilla.org/calendar/deleted-items-manager;1"].getService( + Ci.calIDeletedItems + ).wrappedJSObject; + + let markDeleted = deleteMgr.markDeleted; + deleteMgr.markDeleted = () => {}; + + registerCleanupFunction(() => { + MailServices.accounts.removeAccount(account, true); + cal.itip.getImipTransport = getImipTransport; + deleteMgr.markDeleted = markDeleted; + CalendarTestUtils.removeCalendar(calendar); + }); +}); + +/** + * Tests a minor update exception to an already accepted recurring event. + */ +add_task(async function testMinorUpdateExceptionToAccepted() { + transport.reset(); + let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml")); + let win = await openImipMessage(invite); + await clickAction(win, "imipAcceptRecurrencesButton"); + + await BrowserTestUtils.closeWindow(win); + await doMinorExceptionTest({ + transport, + calendar, + partStat: "ACCEPTED", + }); +}); + +/** + * Tests a minor update exception to an already tentatively accepted recurring + * event. + */ +add_task(async function testMinorUpdateExceptionToTentative() { + transport.reset(); + let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml")); + let win = await openImipMessage(invite); + await clickAction(win, "imipTentativeRecurrencesButton"); + + await BrowserTestUtils.closeWindow(win); + await doMinorExceptionTest({ + transport, + calendar, + partStat: "TENTATIVE", + }); +}); + +/** + * Tests a minor update exception to an already declined recurring declined + * event. + */ +add_task(async function testMinorUpdateExceptionToDeclined() { + transport.reset(); + let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml")); + let win = await openImipMessage(invite); + await clickAction(win, "imipDeclineRecurrencesButton"); + + await BrowserTestUtils.closeWindow(win); + await doMinorExceptionTest({ + transport, + calendar, + partStat: "DECLINED", + }); +}); + +/** + * Tests a major update exception to an already accepted event. + */ +add_task(async function testMajorExceptionToAcceptedWithResponse() { + for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) { + transport.reset(); + let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml")); + let win = await openImipMessage(invite); + await clickAction(win, "imipAcceptRecurrencesButton"); + + await BrowserTestUtils.closeWindow(win); + await doMajorExceptionTest({ + transport, + identity, + calendar, + partStat, + }); + } +}); + +/** + * Tests a major update exception to an already tentatively accepted event. + */ +add_task(async function testMajorExceptionToTentativeWithResponse() { + for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) { + transport.reset(); + let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml")); + let win = await openImipMessage(invite); + await clickAction(win, "imipTentativeRecurrencesButton"); + + await BrowserTestUtils.closeWindow(win); + await doMajorExceptionTest({ + transport, + identity, + calendar, + partStat, + }); + } +}); + +/** + * Tests a major update exception to an already declined event. + */ +add_task(async function testMajorExceptionToDeclinedWithResponse() { + for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) { + transport.reset(); + let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml")); + let win = await openImipMessage(invite); + await clickAction(win, "imipDeclineRecurrencesButton"); + + await BrowserTestUtils.closeWindow(win); + await doMajorExceptionTest({ + transport, + identity, + calendar, + isRecurring: true, + partStat, + }); + } +}); + +/** + * Tests a major update exception to an already accepted event without sending + * a reply. + */ +add_task(async function testMajorExecptionToAcceptedWithoutResponse() { + for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) { + transport.reset(); + let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml")); + let win = await openImipMessage(invite); + await clickMenuAction( + win, + "imipAcceptRecurrencesButton", + "imipAcceptRecurrencesButton_AcceptDontSend" + ); + + await BrowserTestUtils.closeWindow(win); + await doMajorExceptionTest({ + transport, + calendar, + isRecurring: true, + partStat, + noReply: true, + }); + } +}); + +/** + * Tests a major update exception to an already tentatively accepted event + * without sending a reply. + */ +add_task(async function testMajorUpdateToTentativeWithoutResponse() { + for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) { + transport.reset(); + let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml")); + let win = await openImipMessage(invite); + await clickMenuAction( + win, + "imipTentativeRecurrencesButton", + "imipTentativeRecurrencesButton_TentativeDontSend" + ); + + await BrowserTestUtils.closeWindow(win); + await doMajorExceptionTest({ + transport, + calendar, + isRecurring: true, + partStat, + noReply: true, + }); + } +}); + +/** + * Tests a major update exception to a declined event without sending a reply. + */ +add_task(async function testMajorUpdateToDeclinedWithoutResponse() { + for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) { + transport.reset(); + let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml")); + let win = await openImipMessage(invite); + await clickMenuAction( + win, + "imipDeclineRecurrencesButton", + "imipDeclineRecurrencesButton_DeclineDontSend" + ); + + await BrowserTestUtils.closeWindow(win); + await doMajorExceptionTest({ + transport, + calendar, + isRecurring: true, + partStat, + noReply: true, + }); + } +}); + +/** + * Tests a major update exception to an event where the participation status + * is still "NEEDS-ACTION". Here we want to ensure action is only taken on the + * target exception date and not the other dates. + */ +add_task(async function testMajorUpdateToNeedsAction() { + for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) { + transport.reset(); + + // Extract the event from the .eml file and manually add it to the calendar. + let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml")); + let srcText = await IOUtils.readUTF8(invite.path); + let ics = srcText.match( + /--00000000000080f3da05db4aef59[\S\s]+--00000000000080f3da05db4aef59/g + )[0]; + ics = ics.split("--00000000000080f3da05db4aef59").join(""); + ics = ics.replaceAll(/Content-(Type|Transfer-Encoding)?: .*/g, ""); + + let event = new CalEvent(ics); + + // This will not be set because we manually added the event. + event.setProperty("x-moz-received-dtstamp", "20220316T191602Z"); + + await calendar.addItem(event); + await CalendarTestUtils.monthView.waitForItemAt(window, 3, 5, 1).item; + await doMajorExceptionTest({ + transport, + identity, + calendar, + isRecurring: true, + partStat, + }); + } +}); diff --git a/comm/calendar/test/browser/invitations/browser_imipBarRepeat.js b/comm/calendar/test/browser/invitations/browser_imipBarRepeat.js new file mode 100644 index 0000000000..c14ff2c0a5 --- /dev/null +++ b/comm/calendar/test/browser/invitations/browser_imipBarRepeat.js @@ -0,0 +1,218 @@ +/* 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 receiving recurring event invitations via the imip-bar. + */ +"use strict"; + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); +var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm"); + +var { CalendarTestUtils } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarTestUtils.jsm" +); + +let identity; +let calendar; +let transport; + +/** + * Initialize account, identity and calendar. + */ +add_setup(async function () { + let account = MailServices.accounts.createAccount(); + account.incomingServer = MailServices.accounts.createIncomingServer( + "receiver", + "example.com", + "imap" + ); + identity = MailServices.accounts.createIdentity(); + identity.email = "receiver@example.com"; + account.addIdentity(identity); + + await CalendarTestUtils.setCalendarView(window, "month"); + window.goToDate(cal.createDateTime("20220316T191602Z")); + + calendar = CalendarTestUtils.createCalendar("Test"); + transport = new EmailTransport(account, identity); + + let getImipTransport = cal.itip.getImipTransport; + cal.itip.getImipTransport = () => transport; + + let deleteMgr = Cc["@mozilla.org/calendar/deleted-items-manager;1"].getService( + Ci.calIDeletedItems + ).wrappedJSObject; + let markDeleted = deleteMgr.markDeleted; + deleteMgr.markDeleted = () => {}; + + registerCleanupFunction(() => { + MailServices.accounts.removeAccount(account, true); + cal.itip.getImipTransport = getImipTransport; + deleteMgr.markDeleted = markDeleted; + CalendarTestUtils.removeCalendar(calendar); + }); +}); + +/** + * Tests accepting an invitation to a recurring event and sending a response. + */ +add_task(async function testAcceptRecurringWithResponse() { + transport.reset(); + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/repeat-event.eml"))); + await clickAction(win, "imipAcceptRecurrencesButton"); + + let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item; + await doImipBarActionTest( + { + calendar, + transport, + identity, + isRecurring: true, + partStat: "ACCEPTED", + }, + event + ); + + await calendar.deleteItem(event.parentItem); + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Tests tentatively accepting an invitation to a recurring event and sending a + * response. + */ +add_task(async function testTentativeRecurringWithResponse() { + transport.reset(); + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/repeat-event.eml"))); + await clickAction(win, "imipTentativeRecurrencesButton"); + + let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item; + await doImipBarActionTest( + { + calendar, + transport, + identity, + isRecurring: true, + partStat: "TENTATIVE", + }, + event + ); + + await calendar.deleteItem(event.parentItem); + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Tests declining an invitation to a recurring event and sending a response. + */ +add_task(async function testDeclineRecurringWithResponse() { + transport.reset(); + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/repeat-event.eml"))); + await clickAction(win, "imipDeclineRecurrencesButton"); + + let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item; + + await doImipBarActionTest( + { + calendar, + transport, + identity, + isRecurring: true, + partStat: "DECLINED", + }, + event + ); + + await calendar.deleteItem(event.parentItem); + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Tests accepting an invitation to a recurring event without sending a response. + */ +add_task(async function testAcceptRecurringWithoutResponse() { + transport.reset(); + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/repeat-event.eml"))); + await clickMenuAction( + win, + "imipAcceptRecurrencesButton", + "imipAcceptRecurrencesButton_AcceptDontSend" + ); + + let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item; + await doImipBarActionTest( + { + calendar, + transport, + identity, + isRecurring: true, + partStat: "ACCEPTED", + noReply: true, + }, + event + ); + + await calendar.deleteItem(event.parentItem); + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Tests tentatively accepting an invitation to a recurring event without sending + * a response. + */ +add_task(async function testTentativeRecurringWithoutResponse() { + transport.reset(); + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/repeat-event.eml"))); + await clickMenuAction( + win, + "imipTentativeRecurrencesButton", + "imipTentativeRecurrencesButton_TentativeDontSend" + ); + + let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item; + await doImipBarActionTest( + { + calendar, + transport, + identity, + isRecurring: true, + partStat: "TENTATIVE", + noReply: true, + }, + event + ); + + await calendar.deleteItem(event.parentItem); + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Tests declining an invitation to a recurring event without sending a response. + */ +add_task(async function testDeclineRecurrencesWithoutResponse() { + transport.reset(); + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/repeat-event.eml"))); + await clickMenuAction( + win, + "imipDeclineRecurrencesButton", + "imipDeclineRecurrencesButton_DeclineDontSend" + ); + + let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item; + await doImipBarActionTest( + { + calendar, + transport, + identity, + isRecurring: true, + partStat: "DECLINED", + noReply: true, + }, + event + ); + + await calendar.deleteItem(event.parentItem); + await BrowserTestUtils.closeWindow(win); +}); diff --git a/comm/calendar/test/browser/invitations/browser_imipBarRepeatCancel.js b/comm/calendar/test/browser/invitations/browser_imipBarRepeatCancel.js new file mode 100644 index 0000000000..1ab50cc739 --- /dev/null +++ b/comm/calendar/test/browser/invitations/browser_imipBarRepeatCancel.js @@ -0,0 +1,186 @@ +/* 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 processing cancellations to recurring invitations via the imip-bar. + */ +"use strict"; + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); +var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm"); + +var { CalendarTestUtils } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarTestUtils.jsm" +); + +let identity; +let calendar; +let transport; + +/** + * Initialize account, identity and calendar. + */ +add_setup(async function () { + requestLongerTimeout(5); + let account = MailServices.accounts.createAccount(); + account.incomingServer = MailServices.accounts.createIncomingServer( + "receiver", + "example.com", + "imap" + ); + identity = MailServices.accounts.createIdentity(); + identity.email = "receiver@example.com"; + account.addIdentity(identity); + + await CalendarTestUtils.setCalendarView(window, "month"); + window.goToDate(cal.createDateTime("20220316T191602Z")); + + calendar = CalendarTestUtils.createCalendar("Test"); + transport = new EmailTransport(account, identity); + + let getImipTransport = cal.itip.getImipTransport; + cal.itip.getImipTransport = () => transport; + + let deleteMgr = Cc["@mozilla.org/calendar/deleted-items-manager;1"].getService( + Ci.calIDeletedItems + ).wrappedJSObject; + let markDeleted = deleteMgr.markDeleted; + deleteMgr.markDeleted = () => {}; + + registerCleanupFunction(() => { + MailServices.accounts.removeAccount(account, true); + cal.itip.getImipTransport = getImipTransport; + deleteMgr.markDeleted = markDeleted; + CalendarTestUtils.removeCalendar(calendar); + }); +}); + +/** + * Tests accepting a cancellation to an already accepted recurring event. + */ +add_task(async function testCancelAcceptedRecurring() { + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/repeat-event.eml"))); + await clickAction(win, "imipAcceptRecurrencesButton"); + + let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item; + await BrowserTestUtils.closeWindow(win); + await doCancelTest({ + calendar, + event, + transport, + isRecurring: true, + }); +}); + +/** + * Tests accepting a cancellation to an already tentatively accepted event. + */ +add_task(async function testCancelTentativeRecurring() { + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/repeat-event.eml"))); + await clickAction(win, "imipTentativeRecurrencesButton"); + + let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item; + await BrowserTestUtils.closeWindow(win); + await doCancelTest({ + calendar, + event, + transport, + identity, + isRecurring: true, + }); +}); + +/** + * Tests accepting a cancellation to an already declined recurring event. + */ +add_task(async function testCancelDeclinedRecurring() { + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/repeat-event.eml"))); + await clickAction(win, "imipDeclineRecurrencesButton"); + + let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item; + await BrowserTestUtils.closeWindow(win); + await doCancelTest({ + calendar, + event, + transport, + identity, + isRecurring: true, + }); +}); + +/** + * Tests accepting a cancellation to a single occurrence of an already accepted + * recurring event. + */ +add_task(async function testCancelAcceptedOccurrence() { + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/repeat-event.eml"))); + await clickAction(win, "imipAcceptRecurrencesButton"); + + let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item; + await BrowserTestUtils.closeWindow(win); + await doCancelTest({ + calendar, + event, + transport, + isRecurring: true, + recurrenceId: "20220317T110000Z", + }); + await calendar.deleteItem(event.parentItem); +}); + +/** + * Tests accepting a cancellation to a single occurrence of an already tentatively + * accepted event. + */ +add_task(async function testCancelTentativeOccurrence() { + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/repeat-event.eml"))); + await clickAction(win, "imipTentativeRecurrencesButton"); + + let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item; + await BrowserTestUtils.closeWindow(win); + await doCancelTest({ + calendar, + event, + transport, + identity, + isRecurring: true, + recurrenceId: "20220317T110000Z", + }); + await calendar.deleteItem(event.parentItem); +}); + +/** + * Tests accepting a cancellation to a single occurrence of an already declined + * recurring event. + */ +add_task(async function testCancelDeclinedOccurrence() { + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/repeat-event.eml"))); + await clickAction(win, "imipDeclineRecurrencesButton"); + + let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item; + await BrowserTestUtils.closeWindow(win); + await doCancelTest({ + calendar, + event, + transport, + identity, + isRecurring: true, + recurrenceId: "20220317T110000Z", + }); + await calendar.deleteItem(event.parentItem); +}); + +/** + * Tests the handling of a cancellation when the event was not processed + * previously. + */ +add_task(async function testUnprocessedCancel() { + transport.reset(); + let invite = new FileUtils.File(getTestFilePath("data/cancel-repeat-event.eml")); + let win = await openImipMessage(invite); + for (let button of [...win.document.querySelectorAll("#imip-view-toolbar > toolbarbutton")]) { + Assert.ok(button.hidden, `${button.id} is hidden`); + } + await BrowserTestUtils.closeWindow(win); +}); diff --git a/comm/calendar/test/browser/invitations/browser_imipBarRepeatUpdates.js b/comm/calendar/test/browser/invitations/browser_imipBarRepeatUpdates.js new file mode 100644 index 0000000000..7f0d16f627 --- /dev/null +++ b/comm/calendar/test/browser/invitations/browser_imipBarRepeatUpdates.js @@ -0,0 +1,247 @@ +/* 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 receiving minor and major updates to recurring event invitations + * via the imip-bar. + */ + +"use strict"; + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); +var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm"); + +var { CalendarTestUtils } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarTestUtils.jsm" +); + +let identity; +let calendar; +let transport; + +/** + * Initialize account, identity and calendar. + */ +add_setup(async function () { + requestLongerTimeout(5); + let account = MailServices.accounts.createAccount(); + account.incomingServer = MailServices.accounts.createIncomingServer( + "receiver", + "example.com", + "imap" + ); + identity = MailServices.accounts.createIdentity(); + identity.email = "receiver@example.com"; + account.addIdentity(identity); + + await CalendarTestUtils.setCalendarView(window, "month"); + window.goToDate(cal.createDateTime("20220316T191602Z")); + + calendar = CalendarTestUtils.createCalendar("Test"); + transport = new EmailTransport(account, identity); + let getImipTransport = cal.itip.getImipTransport; + cal.itip.getImipTransport = () => transport; + + let deleteMgr = Cc["@mozilla.org/calendar/deleted-items-manager;1"].getService( + Ci.calIDeletedItems + ).wrappedJSObject; + let markDeleted = deleteMgr.markDeleted; + deleteMgr.markDeleted = () => {}; + + registerCleanupFunction(() => { + MailServices.accounts.removeAccount(account, true); + cal.itip.getImipTransport = getImipTransport; + deleteMgr.markDeleted = markDeleted; + CalendarTestUtils.removeCalendar(calendar); + }); +}); + +/** + * Tests a minor update to an already accepted event. + */ +add_task(async function testMinorUpdateToAccepted() { + transport.reset(); + let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml")); + let win = await openImipMessage(invite); + await clickAction(win, "imipAcceptRecurrencesButton"); + + await BrowserTestUtils.closeWindow(win); + await doMinorUpdateTest({ + transport, + calendar, + isRecurring: true, + partStat: "ACCEPTED", + }); +}); + +/** + * Tests a minor update to an already tentatively accepted event. + */ +add_task(async function testMinorUpdateToTentative() { + transport.reset(); + let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml")); + let win = await openImipMessage(invite); + await clickAction(win, "imipTentativeRecurrencesButton"); + + await BrowserTestUtils.closeWindow(win); + await doMinorUpdateTest({ + transport, + calendar, + isRecurring: true, + partStat: "TENTATIVE", + }); +}); + +/** + * Tests a minor update to an already declined event. + */ +add_task(async function testMinorUpdateToDeclined() { + transport.reset(); + let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml")); + let win = await openImipMessage(invite); + await clickAction(win, "imipDeclineRecurrencesButton"); + + await BrowserTestUtils.closeWindow(win); + await doMinorUpdateTest({ transport, calendar, isRecurring: true, invite, partStat: "DECLINED" }); +}); + +/** + * Tests a major update to an already accepted event. + */ +add_task(async function testMajorUpdateToAcceptedWithResponse() { + for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) { + transport.reset(); + let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml")); + let win = await openImipMessage(invite); + await clickAction(win, "imipAcceptRecurrencesButton"); + + await BrowserTestUtils.closeWindow(win); + await doMajorUpdateTest({ + transport, + identity, + calendar, + isRecurring: true, + partStat, + }); + } +}); + +/** + * Tests a major update to an already tentatively accepted event. + */ +add_task(async function testMajorUpdateToTentativeWithResponse() { + for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) { + transport.reset(); + let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml")); + let win = await openImipMessage(invite); + await clickAction(win, "imipTentativeRecurrencesButton"); + + await BrowserTestUtils.closeWindow(win); + await doMajorUpdateTest({ + transport, + identity, + calendar, + isRecurring: true, + partStat, + }); + } +}); + +/** + * Tests a major update to an already declined event. + */ +add_task(async function testMajorUpdateToDeclinedWithResponse() { + for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) { + transport.reset(); + let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml")); + let win = await openImipMessage(invite); + await clickAction(win, "imipDeclineRecurrencesButton"); + + await BrowserTestUtils.closeWindow(win); + await doMajorUpdateTest({ + transport, + identity, + calendar, + isRecurring: true, + partStat, + }); + } +}); + +/** + * Tests a major update to an already accepted event without replying to the + * update. + */ +add_task(async function testMajorUpdateToAcceptedWithoutResponse() { + for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) { + transport.reset(); + let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml")); + let win = await openImipMessage(invite); + await clickMenuAction( + win, + "imipAcceptRecurrencesButton", + "imipAcceptRecurrencesButton_AcceptDontSend" + ); + + await BrowserTestUtils.closeWindow(win); + await doMajorUpdateTest({ + transport, + calendar, + isRecurring: true, + partStat, + noReply: true, + }); + } +}); + +/** + * Tests a major update to an already tentatively accepted event without replying + * to the update. + */ +add_task(async function testMajorUpdateToTentativeWithoutResponse() { + for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) { + transport.reset(); + let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml")); + let win = await openImipMessage(invite); + await clickMenuAction( + win, + "imipTentativeRecurrencesButton", + "imipTentativeRecurrencesButton_TentativeDontSend" + ); + + await BrowserTestUtils.closeWindow(win); + await doMajorUpdateTest({ + transport, + calendar, + isRecurring: true, + partStat, + noReply: true, + }); + } +}); + +/** + * Tests a major update to an already declined event. + */ +add_task(async function testMajorUpdateToDeclinedWithoutResponse() { + for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) { + transport.reset(); + let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml")); + let win = await openImipMessage(invite); + await clickMenuAction( + win, + "imipDeclineRecurrencesButton", + "imipDeclineRecurrencesButton_DeclineDontSend" + ); + + await BrowserTestUtils.closeWindow(win); + await doMajorUpdateTest({ + transport, + calendar, + isRecurring: true, + partStat, + noReply: true, + }); + } +}); diff --git a/comm/calendar/test/browser/invitations/browser_imipBarUpdates.js b/comm/calendar/test/browser/invitations/browser_imipBarUpdates.js new file mode 100644 index 0000000000..d0f5018e89 --- /dev/null +++ b/comm/calendar/test/browser/invitations/browser_imipBarUpdates.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 receiving minor and major updates to invitations via the imip-bar. + */ + +"use strict"; + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); +var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm"); + +var { CalendarTestUtils } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarTestUtils.jsm" +); + +let identity; +let calendar; +let transport; + +/** + * Initialize account, identity and calendar. + */ +add_setup(async function () { + requestLongerTimeout(5); + let account = MailServices.accounts.createAccount(); + account.incomingServer = MailServices.accounts.createIncomingServer( + "receiver", + "example.com", + "imap" + ); + identity = MailServices.accounts.createIdentity(); + identity.email = "receiver@example.com"; + account.addIdentity(identity); + + await CalendarTestUtils.setCalendarView(window, "month"); + window.goToDate(cal.createDateTime("20220316T191602Z")); + + calendar = CalendarTestUtils.createCalendar("Test"); + transport = new EmailTransport(account, identity); + + let getImipTransport = cal.itip.getImipTransport; + cal.itip.getImipTransport = () => transport; + + let deleteMgr = Cc["@mozilla.org/calendar/deleted-items-manager;1"].getService( + Ci.calIDeletedItems + ).wrappedJSObject; + let markDeleted = deleteMgr.markDeleted; + deleteMgr.markDeleted = () => {}; + + registerCleanupFunction(() => { + MailServices.accounts.removeAccount(account, true); + cal.itip.getImipTransport = getImipTransport; + deleteMgr.markDeleted = markDeleted; + CalendarTestUtils.removeCalendar(calendar); + }); +}); + +/** + * Tests a minor update to an already accepted event. + */ +add_task(async function testMinorUpdateToAccepted() { + transport.reset(); + let invite = new FileUtils.File(getTestFilePath("data/single-event.eml")); + let win = await openImipMessage(invite); + await clickAction(win, "imipAcceptButton"); + + await BrowserTestUtils.closeWindow(win); + await doMinorUpdateTest({ + transport, + calendar, + partStat: "ACCEPTED", + }); +}); + +/** + * Tests a minor update to an already tentatively accepted event. + */ +add_task(async function testMinorUpdateToTentative() { + transport.reset(); + let invite = new FileUtils.File(getTestFilePath("data/single-event.eml")); + let win = await openImipMessage(invite); + await clickAction(win, "imipTentativeButton"); + + await BrowserTestUtils.closeWindow(win); + await doMinorUpdateTest({ transport, calendar, invite, partStat: "TENTATIVE" }); +}); + +/** + * Tests a minor update to an already declined event. + */ +add_task(async function testMinorUpdateToDeclined() { + transport.reset(); + let invite = new FileUtils.File(getTestFilePath("data/single-event.eml")); + let win = await openImipMessage(invite); + await clickAction(win, "imipDeclineButton"); + + await BrowserTestUtils.closeWindow(win); + await doMinorUpdateTest({ transport, calendar, invite, partStat: "DECLINED" }); +}); + +/** + * Tests a major update to an already accepted event. + */ +add_task(async function testMajorUpdateToAcceptedWithResponse() { + for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) { + transport.reset(); + let invite = new FileUtils.File(getTestFilePath("data/single-event.eml")); + let win = await openImipMessage(invite); + await clickAction(win, "imipAcceptButton"); + + await BrowserTestUtils.closeWindow(win); + await doMajorUpdateTest({ + transport, + identity, + calendar, + partStat, + }); + } +}); + +/** + * Tests a major update to an already tentatively accepted event. + */ +add_task(async function testMajorUpdateToTentativeWithResponse() { + for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) { + transport.reset(); + let invite = new FileUtils.File(getTestFilePath("data/single-event.eml")); + let win = await openImipMessage(invite); + await clickAction(win, "imipTentativeButton"); + + await BrowserTestUtils.closeWindow(win); + await doMajorUpdateTest({ + transport, + identity, + calendar, + partStat, + }); + } +}); + +/** + * Tests a major update to an already declined event. + */ +add_task(async function testMajorUpdateToDeclinedWithResponse() { + for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) { + transport.reset(); + let invite = new FileUtils.File(getTestFilePath("data/single-event.eml")); + let win = await openImipMessage(invite); + await clickAction(win, "imipDeclineButton"); + + await BrowserTestUtils.closeWindow(win); + await doMajorUpdateTest({ + transport, + identity, + calendar, + partStat, + }); + } +}); + +/** + * Tests a major update to an already accepted event without replying to the + * update. + */ +add_task(async function testMajorUpdateToAcceptedWithoutResponse() { + for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) { + transport.reset(); + let invite = new FileUtils.File(getTestFilePath("data/single-event.eml")); + let win = await openImipMessage(invite); + await clickAction(win, "imipAcceptButton"); + + await BrowserTestUtils.closeWindow(win); + await doMajorUpdateTest({ + transport, + calendar, + partStat, + noReply: true, + }); + } +}); + +/** + * Tests a major update to an already tentatively accepted event without replying + * to the update. + */ +add_task(async function testMajorUpdateToTentativeWithoutResponse() { + for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) { + transport.reset(); + let invite = new FileUtils.File(getTestFilePath("data/single-event.eml")); + let win = await openImipMessage(invite); + await clickAction(win, "imipTentativeButton"); + + await BrowserTestUtils.closeWindow(win); + await doMajorUpdateTest({ + transport, + calendar, + partStat, + noReply: true, + }); + } +}); + +/** + * Tests a major update to an already declined event. + */ +add_task(async function testMajorUpdateToDeclinedWithoutResponse() { + for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) { + transport.reset(); + let invite = new FileUtils.File(getTestFilePath("data/single-event.eml")); + let win = await openImipMessage(invite); + await clickAction(win, "imipDeclineButton"); + + await BrowserTestUtils.closeWindow(win); + await doMajorUpdateTest({ + transport, + calendar, + partStat, + noReply: true, + }); + } +}); diff --git a/comm/calendar/test/browser/invitations/browser_invitationDisplayNew.js b/comm/calendar/test/browser/invitations/browser_invitationDisplayNew.js new file mode 100644 index 0000000000..a7b3f833de --- /dev/null +++ b/comm/calendar/test/browser/invitations/browser_invitationDisplayNew.js @@ -0,0 +1,257 @@ +/* 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 invitation panel display with new events. + */ +"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 { CalendarTestUtils } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarTestUtils.jsm" +); + +let identity; +let calendar; +let transport; + +/** + * Initialize account, identity and calendar. + */ +add_setup(async function () { + let account = MailServices.accounts.createAccount(); + account.incomingServer = MailServices.accounts.createIncomingServer( + "receiver", + "example.com", + "imap" + ); + identity = MailServices.accounts.createIdentity(); + identity.email = "receiver@example.com"; + account.addIdentity(identity); + + await CalendarTestUtils.setCalendarView(window, "month"); + window.goToDate(cal.createDateTime("20220316T191602Z")); + + calendar = CalendarTestUtils.createCalendar("Test"); + transport = new EmailTransport(account, identity); + + let getImipTransport = cal.itip.getImipTransport; + cal.itip.getImipTransport = () => transport; + + let deleteMgr = Cc["@mozilla.org/calendar/deleted-items-manager;1"].getService( + Ci.calIDeletedItems + ).wrappedJSObject; + let markDeleted = deleteMgr.markDeleted; + deleteMgr.markDeleted = () => {}; + + Services.prefs.setBoolPref("calendar.itip.newInvitationDisplay", true); + registerCleanupFunction(() => { + MailServices.accounts.removeAccount(account, true); + cal.itip.getImipTransport = getImipTransport; + deleteMgr.markDeleted = markDeleted; + CalendarTestUtils.removeCalendar(calendar); + Services.prefs.setBoolPref("calendar.itip.newInvitationDisplay", false); + }); +}); + +/** + * Tests the invitation panel shows the correct data when loaded with a new + * invitation. + */ +add_task(async function testShowPanelData() { + transport.reset(); + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/single-event.eml"))); + let panel = win.document + .getElementById("messageBrowser") + .contentDocument.querySelector("calendar-invitation-panel"); + + if (panel.ownerDocument.hasPendingL10nMutations) { + await BrowserTestUtils.waitForEvent(panel.ownerDocument, "L10nMutationsFinished"); + } + + let notification = panel.shadowRoot.querySelector("notification-message"); + compareShownPanelValues(notification.shadowRoot, { + ".notification-message": "You have been invited to this event.", + ".notification-button-container > button": "More", + }); + + compareShownPanelValues(panel.shadowRoot, { + "#title": "Single Event", + "#location": "Somewhere", + "#partStatTotal": "3 participants", + '[data-l10n-id="calendar-invitation-panel-partstat-accepted"]': "1 yes", + '[data-l10n-id="calendar-invitation-panel-partstat-needs-action"]': "2 pending", + "#attendees li:nth-of-type(1)": "Sender <sender@example.com>", + "#attendees li:nth-of-type(2)": "Receiver <receiver@example.com>", + "#attendees li:nth-of-type(3)": "Other <other@example.com>", + "#description": "An event invitation.", + }); + + Assert.ok(!panel.shadowRoot.querySelector("#actionButtons").hidden, "action buttons shown"); + for (let indicator of [ + ...panel.shadowRoot.querySelectorAll("calendar-invitation-change-indicator"), + ]) { + Assert.ok(indicator.hidden, `${indicator.id} is hidden`); + } + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Tests accepting an invitation and sending a response. + */ +add_task(async function testAcceptWithResponse() { + transport.reset(); + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/single-event.eml"))); + let panel = win.document + .getElementById("messageBrowser") + .contentDocument.querySelector("calendar-invitation-panel"); + + await clickPanelAction(panel, "acceptButton"); + let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item; + await doImipBarActionTest( + { + calendar, + transport, + identity, + partStat: "ACCEPTED", + }, + event + ); + await calendar.deleteItem(event); + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Tests tentatively accepting an invitation and sending a response. + */ +add_task(async function testTentativeWithResponse() { + transport.reset(); + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/single-event.eml"))); + let panel = win.document + .getElementById("messageBrowser") + .contentDocument.querySelector("calendar-invitation-panel"); + + await clickPanelAction(panel, "tentativeButton"); + let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item; + await doImipBarActionTest( + { + calendar, + transport, + identity, + partStat: "TENTATIVE", + }, + event + ); + + await calendar.deleteItem(event); + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Tests declining an invitation and sending a response. + */ +add_task(async function testDeclineWithResponse() { + transport.reset(); + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/single-event.eml"))); + let panel = win.document + .getElementById("messageBrowser") + .contentDocument.querySelector("calendar-invitation-panel"); + + await clickPanelAction(panel, "declineButton"); + let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item; + await doImipBarActionTest( + { + calendar, + transport, + identity, + partStat: "DECLINED", + }, + event + ); + await calendar.deleteItem(event); + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Tests accepting an invitation without sending a response. + */ +add_task(async function testAcceptWithoutResponse() { + transport.reset(); + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/single-event.eml"))); + let panel = win.document + .getElementById("messageBrowser") + .contentDocument.querySelector("calendar-invitation-panel"); + + await clickPanelAction(panel, "acceptButton", false); + let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item; + await doImipBarActionTest( + { + calendar, + transport, + identity, + partStat: "ACCEPTED", + noSend: true, + }, + event + ); + await calendar.deleteItem(event); + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Tests tentatively accepting an invitation without sending a response. + */ +add_task(async function testTentativeWithoutResponse() { + transport.reset(); + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/single-event.eml"))); + let panel = win.document + .getElementById("messageBrowser") + .contentDocument.querySelector("calendar-invitation-panel"); + + await clickPanelAction(panel, "tentativeButton", false); + let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item; + await doImipBarActionTest( + { + calendar, + transport, + identity, + partStat: "TENTATIVE", + noSend: true, + }, + event + ); + await calendar.deleteItem(event); + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Tests declining an invitation without sending a response. + */ +add_task(async function testDeclineWithoutResponse() { + transport.reset(); + let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/single-event.eml"))); + let panel = win.document + .getElementById("messageBrowser") + .contentDocument.querySelector("calendar-invitation-panel"); + + await clickPanelAction(panel, "declineButton", false); + let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item; + await doImipBarActionTest( + { + calendar, + transport, + identity, + partStat: "DECLINED", + noSend: true, + }, + event + ); + await calendar.deleteItem(event); + await BrowserTestUtils.closeWindow(win); +}); diff --git a/comm/calendar/test/browser/invitations/browser_unsupportedFreq.js b/comm/calendar/test/browser/invitations/browser_unsupportedFreq.js new file mode 100644 index 0000000000..2d05ed66dc --- /dev/null +++ b/comm/calendar/test/browser/invitations/browser_unsupportedFreq.js @@ -0,0 +1,107 @@ +/* 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 ensuring the application does not hang after processing an + * unsupported FREQ value. + */ +"use strict"; + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); +var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm"); + +var { CalendarTestUtils } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarTestUtils.jsm" +); + +let calendar; + +/** + * Initialize account, identity and calendar. + */ +add_setup(async function () { + let account = MailServices.accounts.createAccount(); + account.incomingServer = MailServices.accounts.createIncomingServer( + "receiver", + "example.com", + "imap" + ); + + let identity = MailServices.accounts.createIdentity(); + identity.email = "receiver@example.com"; + account.addIdentity(identity); + + await CalendarTestUtils.setCalendarView(window, "month"); + window.goToDate(cal.createDateTime("20220316T191602Z")); + + calendar = CalendarTestUtils.createCalendar("Test"); + registerCleanupFunction(() => { + MailServices.accounts.removeAccount(account, true); + CalendarTestUtils.removeCalendar(calendar); + }); +}); + +/** + * Runs the test using the provided FREQ value. + * + * @param {string} freq Either "SECONDLY" or "MINUTELY" + */ +async function doFreqTest(freq) { + let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml")); + let srcText = await IOUtils.readUTF8(invite.path); + let tmpFile = FileTestUtils.getTempFile(`${freq}.eml`); + + srcText = srcText.replace(/RRULE:.*/g, `RRULE:FREQ=${freq}`); + srcText = srcText.replace(/UID:.*/g, `UID:${freq}`); + await IOUtils.writeUTF8(tmpFile.path, srcText); + + let win = await openImipMessage(tmpFile); + await clickMenuAction( + win, + "imipAcceptRecurrencesButton", + "imipAcceptRecurrencesButton_AcceptDontSend" + ); + + // Give the view time to refresh and create any occurrences. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 5000)); + await BrowserTestUtils.closeWindow(win); + + let dayBoxItems = document.querySelectorAll("calendar-month-day-box-item"); + Assert.equal(dayBoxItems.length, 1, "only one occurrence displayed"); + + let [dayBox] = dayBoxItems; + let { item } = dayBox; + Assert.equal(item.title, "Repeat Event"); + Assert.equal(item.startDate.icalString, "20220316T110000Z"); + + let summaryDialog = await CalendarTestUtils.viewItem(window, dayBox); + Assert.equal( + summaryDialog.document.querySelector(".repeat-details").textContent, + "Repeat details unknown", + "repeat details not shown" + ); + + await BrowserTestUtils.closeWindow(summaryDialog); + await calendar.deleteItem(item.parentItem); + await TestUtils.waitForCondition( + () => document.querySelectorAll("calendar-month-day-box-item").length == 0 + ); +} + +/** + * Tests accepting an invitation using the FREQ=SECONDLY value does not render + * the application unusable. + */ +add_task(async function testSecondly() { + return doFreqTest("SECONDLY"); +}); + +/** + * Tests accepting an invitation using the FREQ=MINUTELY value does not render + * the application unusable. + */ +add_task(async function testMinutely() { + return doFreqTest("MINUTELY"); +}); diff --git a/comm/calendar/test/browser/invitations/data/cancel-repeat-event.eml b/comm/calendar/test/browser/invitations/data/cancel-repeat-event.eml new file mode 100644 index 0000000000..03f298525b --- /dev/null +++ b/comm/calendar/test/browser/invitations/data/cancel-repeat-event.eml @@ -0,0 +1,49 @@ +MIME-Version: 1.0
+Date: Mon, 28 Mar 2022 17:49:35 +0000
+Subject: Invitation: Repeat Event @ Daily from 2pm to 3pm 3 times (AST) (receiver@example.com)
+From: sender@example.com
+To: receiver@example.com
+Content-Type: multipart/mixed; boundary="00000000000080f3db05db4aef5b"
+
+--00000000000080f3db05db4aef5b
+Content-Type: multipart/alternative; boundary="00000000000080f3da05db4aef59"
+
+--00000000000080f3da05db4aef59
+Content-Type: text/calendar; charset="UTF-8"; method=CANCEL
+Content-Transfer-Encoding: 7bit
+
+BEGIN:VCALENDAR
+METHOD:CANCEL
+BEGIN:VEVENT
+DTSTART:20220316T110000Z
+DTEND:20220316T113000Z
+RRULE:FREQ=DAILY;WKST=SU;COUNT=3;INTERVAL=1
+DTSTAMP:20220316T191602Z
+UID:02e79b96
+ORGANIZER;CN=Sender;
+ EMAIL=sender@example.com:mailto:sender@example.com
+ATTENDEE;CN=Sender;
+ EMAIL=sender@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=ACCEPTED:mailto:sender@example.com
+ATTENDEE;CN=Receiver;EMAIL=receiver@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=NEEDS-ACTION:mailto:receiver@example.com
+ATTENDEE;CN=Other;EMAIL=other@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=NEEDS-ACTION:mailto:other@example.com
+CREATED:20220328T174934Z
+LAST-MODIFIED:20220328T174934Z
+LOCATION:Somewhere
+SEQUENCE:1
+STATUS:CANCELLED
+SUMMARY:Repeat Event
+DESCRIPTION:An event invitation.
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+TRIGGER:-P1D
+DESCRIPTION:This is an event reminder
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+
+--00000000000080f3da05db4aef59--
+--00000000000080f3db05db4aef5b--
diff --git a/comm/calendar/test/browser/invitations/data/cancel-single-event.eml b/comm/calendar/test/browser/invitations/data/cancel-single-event.eml new file mode 100644 index 0000000000..afb4edb99d --- /dev/null +++ b/comm/calendar/test/browser/invitations/data/cancel-single-event.eml @@ -0,0 +1,78 @@ +MIME-Version: 1.0 +Content-Transfer-Encoding: binary +Content-Type: multipart/mixed; boundary="_----------=_1647458162153312762582" +Date: Wed, 16 Mar 2022 15:16:02 -0400 +To: receiver@example.com +Subject: Cancellation: Single Event @ Wed, Mar 16 2022 11:00 AST +From: Sender <sender@example.com> + +This is a multi-part message in MIME format. + +--_----------=_1647458162153312762582 +MIME-Version: 1.0 +Content-Transfer-Encoding: binary +Content-Type: multipart/alternative; boundary="_----------=_1647458162153312762583" +Date: Wed, 16 Mar 2022 15:16:02 -0400 + +This is a multi-part message in MIME format. + +--_----------=_1647458162153312762583 +MIME-Version: 1.0 +Content-Disposition: inline +Content-Length: 227 +Content-Transfer-Encoding: binary +Content-Type: text/plain; charset="utf-8" +Date: Wed, 16 Mar 2022 15:16:02 -0400 + +Single Event + +When: + Wed, Mar 16 2022 + 11:00 - 12:00 AST +Where: + Somewhere + +--_----------=_1647458162153312762583 +MIME-Version: 1.0 +Content-Disposition: inline +Content-Transfer-Encoding: quoted-printable +Content-Type: text/calendar; charset="utf-8"; method=CANCEL +Date: Wed, 16 Mar 2022 15:16:02 -0400 + +BEGIN:VCALENDAR +VERSION:2.0 +METHOD:CANCEL +CALSCALE:GREGORIAN +BEGIN:VEVENT +UID:02e79b96 +SEQUENCE:1 +DTSTAMP:20220317T191602Z +CREATED:20220316T191532Z +DTSTART:20220316T110000Z +DTEND:20220316T113000Z +DURATION:PT1H +PRIORITY:0 +SUMMARY:Single Event +DESCRIPTION:An event invitation. +LOCATION:Somewhere +STATUS:CANCELLED +TRANSP:OPAQUE +CLASS:PUBLIC +ORGANIZER;CN=3DSender; + EMAIL=3Dsender@example.com:mailto:sender@example.com +ATTENDEE;CN=3DSender; + EMAIL=3Dsender@example.com;CUTYPE=3DINDIVIDUAL; + PARTSTAT=3DACCEPTED:mailto:sender@example.com +ATTENDEE;CN=Receiver;EMAIL=3Dreceiver@example.com;CUTYPE=3DINDIVIDUAL; + PARTSTAT=3DNEEDS-ACTION:mailto:receiver@example.com +ATTENDEE;CN=Other;EMAIL=other@example.com;CUTYPE=3DINDIVIDUAL; + PARTSTAT=3DNEEDS-ACTION:mailto:other@example.com +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-P1D +DESCRIPTION:This is an event reminder +END:VALARM +END:VEVENT +END:VCALENDAR + +--_----------=_1647458162153312762583-- diff --git a/comm/calendar/test/browser/invitations/data/exception-major.eml b/comm/calendar/test/browser/invitations/data/exception-major.eml new file mode 100644 index 0000000000..07f48e64bd --- /dev/null +++ b/comm/calendar/test/browser/invitations/data/exception-major.eml @@ -0,0 +1,49 @@ +MIME-Version: 1.0
+Date: Mon, 28 Mar 2022 17:49:35 +0000
+Subject: Exception Major
+From: sender@example.com
+To: receiver@example.com
+Content-Type: multipart/mixed; boundary="00000000000080f3db05db4aef5b"
+
+--00000000000080f3db05db4aef5b
+Content-Type: multipart/alternative; boundary="00000000000080f3da05db4aef59"
+
+--00000000000080f3da05db4aef59
+Content-Type: text/calendar; charset="UTF-8"; method=REQUEST
+Content-Transfer-Encoding: 7bit
+
+BEGIN:VCALENDAR
+METHOD:REQUEST
+BEGIN:VEVENT
+DTSTART:20220317T050000Z
+DTEND:20220317T053000Z
+RECURRENCE-ID:20220317T110000Z
+DTSTAMP:20220316T191602Z
+UID:02e79b96
+ORGANIZER;CN=Sender;
+ EMAIL=sender@example.com:mailto:sender@example.com
+ATTENDEE;CN=Sender;
+ EMAIL=sender@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=ACCEPTED;RSVP=FALSE:mailto:sender@example.com
+ATTENDEE;CN=Receiver;EMAIL=receiver@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:receiver@example.com
+ATTENDEE;CN=Other;EMAIL=other@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:other@example.com
+CREATED:20220328T174934Z
+LAST-MODIFIED:20220328T174934Z
+LOCATION:Somewhere
+SEQUENCE:2
+STATUS:CONFIRMED
+SUMMARY:Repeat Event
+DESCRIPTION:An event invitation.
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+TRIGGER:-P1D
+DESCRIPTION:This is an event reminder
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+
+--00000000000080f3da05db4aef59--
+--00000000000080f3db05db4aef5b--
diff --git a/comm/calendar/test/browser/invitations/data/exception-minor.eml b/comm/calendar/test/browser/invitations/data/exception-minor.eml new file mode 100644 index 0000000000..7cc38d29d3 --- /dev/null +++ b/comm/calendar/test/browser/invitations/data/exception-minor.eml @@ -0,0 +1,49 @@ +MIME-Version: 1.0
+Date: Mon, 28 Mar 2022 17:49:35 +0000
+Subject: Exception Minor
+From: sender@example.com
+To: receiver@example.com
+Content-Type: multipart/mixed; boundary="00000000000080f3db05db4aef5b"
+
+--00000000000080f3db05db4aef5b
+Content-Type: multipart/alternative; boundary="00000000000080f3da05db4aef59"
+
+--00000000000080f3da05db4aef59
+Content-Type: text/calendar; charset="UTF-8"; method=REQUEST
+Content-Transfer-Encoding: 7bit
+
+BEGIN:VCALENDAR
+METHOD:REQUEST
+BEGIN:VEVENT
+DTSTART:20220317T110000Z
+DTEND:20220317T113000Z
+RECURRENCE-ID:20220317T110000Z
+DTSTAMP:20220318T191602Z
+UID:02e79b96
+ORGANIZER;CN=Sender;
+ EMAIL=sender@example.com:mailto:sender@example.com
+ATTENDEE;CN=Sender;
+ EMAIL=sender@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=ACCEPTED;RSVP=FALSE:mailto:sender@example.com
+ATTENDEE;CN=Receiver;EMAIL=receiver@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:receiver@example.com
+ATTENDEE;CN=Other;EMAIL=other@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:other@example.com
+CREATED:20220328T174934Z
+LAST-MODIFIED:20220328T174934Z
+LOCATION:Exception location
+SEQUENCE:0
+STATUS:CONFIRMED
+SUMMARY:Exception title
+DESCRIPTION:Exception description
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+TRIGGER:-P1D
+DESCRIPTION:Exception description.
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+
+--00000000000080f3da05db4aef59--
+--00000000000080f3db05db4aef5b--
diff --git a/comm/calendar/test/browser/invitations/data/meet-meeting-invite.eml b/comm/calendar/test/browser/invitations/data/meet-meeting-invite.eml new file mode 100644 index 0000000000..8587cd803a --- /dev/null +++ b/comm/calendar/test/browser/invitations/data/meet-meeting-invite.eml @@ -0,0 +1,384 @@ +Sender: Google Kalender <calendar-notification@google.com>
+Message-ID: <0000000000008c6d7005be1c767c@google.com>
+Date: Mon, 22 Mar 2021 09:12:20 +0000
+Subject: Meet invite (HTML)
+From: example@gmail.com
+To: homer@example.com
+Content-Type: multipart/mixed; boundary="0000000000008c6d6205be1c767e"
+Return-Path: example@gmail.com
+MIME-Version: 1.0
+
+--0000000000008c6d6205be1c767e
+Content-Type: multipart/alternative; boundary="0000000000008c6d6005be1c767c"
+
+--0000000000008c6d6005be1c767c
+Content-Type: text/plain; charset="UTF-8"; format=flowed; delsp=yes
+Content-Transfer-Encoding: base64
+
+RHUgaGFyIGJsaXZpdCBpbmJqdWRlbiB0aWxsIGbDtmxqYW5kZSBow6RuZGVsc2UuCgpUaXRlbDog
+TWVlZWVldCBtZSBIVE1MClRoaXMgaXMgYSB0ZXN0LiBCb2xkLiBJdGFsaWMuJm5ic3A7V2lsbCBk
+aXNjdXNzIGFkZHJlc3MgZm9yIGVtYWlsICAKJmx0O2Zvb0BleGFtcGxlLmNvbSZndDsgYW5kIGh0
+dHA6Ly9leGFtcGxlLmNvbT9mb289YmFyLgpOw6RyOiBtw6VuIGRlbiAyMiBtYXJzIDIwMjEgMTE6
+MzBhbSDigJMgMTI6MzBwbSDDlnN0ZXVyb3BlaXNrIHRpZCAtIEhlbHNpbmdmb3JzCgpBbnNsdXRu
+aW5nc2luZm86IEFuc2x1dCB0aWxsIEdvb2dsZSBNZWV0Cmh0dHBzOi8vbWVldC5nb29nbGUuY29t
+L3B5Yi1uZGN1LWhoYz9ocz0yMjQKCkthbGVuZGVyOiBob21lckBleGFtcGxlLmNvbQpWZW06CiAg
+ICAgKiBleGFtcGxlQGdtYWlsLmNvbeKAkyBvcmdhbmlzYXTDtnIKICAgICAqIGhvbWVyQGV4YW1w
+bGUuY29tCgpJbmZvcm1hdGlvbiBvbSBow6RuZGVsc2VuOiAgCmh0dHBzOi8vY2FsZW5kYXIuZ29v
+Z2xlLmNvbS9jYWxlbmRhci9ldmVudD9hY3Rpb249VklFVyZlaWQ9TmpWdE1UZG9jMlJ2YkcxdmRI
+WXphM1p0Y25Sbk5EQnZiblFnYldGbmJuVnpMbTFsYkdsdVFHaDFkQzVtYVEmdG9rPU1qRWpZbVZ5
+ZEdGMGFHVmliM1JBWjIxaGFXd3VZMjl0WlRnMk5HRmpZbU5qWVdFMU1qVmxaV0ptWTJVelltUm1N
+REF5TldVME1Ea3pOREF4WmpSaFpnJmN0ej1FdXJvcGUlMkZIZWxzaW5raSZobD1zdiZlcz0wCgpJ
+bmJqdWRhbiBmcsOlbiBHb29nbGUgS2FsZW5kZXI6IGh0dHBzOi8vY2FsZW5kYXIuZ29vZ2xlLmNv
+bS9jYWxlbmRhci8KCkRldHRhIGUtcG9zdG1lZGRlbGFuZGUgaGFyIHNraWNrYXRzIHRpbGwga29u
+dG90IGhvbWVyQGV4YW1wbGUuY29tICAKZWZ0ZXJzb20gZHUgw6RyIGRlbHRhZ2FyZSB2aWQgZGVu
+bmEgaMOkbmRlbHNlLgoKT20gZHUgaW50ZSB2aWxsIGbDpSB1cHBkYXRlcmluZ2FyIG9tIGRlbm5h
+IGjDpG5kZWxzZSBpIGZyYW10aWRlbiBrYW4gZHUgdGFja2EgIApuZWogdGlsbCBkZW5uYSBow6Ru
+ZGVsc2UuIER1IGthbiDDpHZlbiByZWdpc3RyZXJhIGRpZyBmw7ZyIGF0dCBmw6UgZXR0ICAKR29v
+Z2xlLWtvbnRvIHDDpSBodHRwczovL2NhbGVuZGFyLmdvb2dsZS5jb20vY2FsZW5kYXIvIG9jaCBr
+b250cm9sbGVyYSAgCmF2aXNlcmluZ3NpbnN0w6RsbG5pbmdhcm5hIGbDtnIgaGVsYSBrYWxlbmRl
+cm4uCgpPbSBkdSB2aWRhcmViZWZvcmRyYXIgZGVuIGjDpHIgaW5ianVkYW4ga2FuIGRldCBnw7Zy
+YSBkZXQgbcO2amxpZ3QgZsO2ciBhbGxhICAKbW90dGFnYXJlIGF0dCBza2lja2EgZXR0IHN2YXIg
+dGlsbCBvcmdhbmlzYXTDtnJlbiBvY2ggbMOkZ2dhcyB0aWxsIHDDpSAgCmfDpHN0bGlzdGFuLCBi
+anVkYSBpbiBhbmRyYSBvYXZzZXR0IGRlcmFzIGVnZW4gaW5ianVkbmluZ3NzdGF0dXMgZWxsZXIg
+IAptb2RpZmllcmEgZGl0dCBPU0EuIEzDpHMgbWVyIHDDpSAgCmh0dHBzOi8vc3VwcG9ydC5nb29n
+bGUuY29tL2NhbGVuZGFyL2Fuc3dlci8zNzEzNSNmb3J3YXJkaW5nCg==
+--0000000000008c6d6005be1c767c
+Content-Type: text/html; charset="UTF-8"
+Content-Transfer-Encoding: quoted-printable
+
+<html>
+<head>
+<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Dutf-8">
+</head>
+<body>
+<span itemscope=3D"" itemtype=3D"http://schema.org/InformAction"><span styl=
+e=3D"display:none" itemprop=3D"about" itemscope=3D"" itemtype=3D"http://sch=
+ema.org/Person">
+<meta itemprop=3D"description" content=3D"Inbjudan fr=C3=A5n example@gm=
+ail.com">
+</span><span itemprop=3D"object" itemscope=3D"" itemtype=3D"http://schema.o=
+rg/Event">
+<div style=3D"">
+<table cellspacing=3D"0" cellpadding=3D"8" border=3D"0" summary=3D"" style=
+=3D"width:100%;font-family:Arial,Sans-serif;border:1px Solid #ccc;border-wi=
+dth:1px 2px 2px 1px;background-color:#fff;">
+<tbody>
+<tr>
+<td>
+<meta itemprop=3D"eventStatus" content=3D"http://schema.org/EventScheduled"=
+>
+<h4 style=3D"padding:6px 0;margin:0 0 4px 0;font-family:Arial,Sans-serif;fo=
+nt-size:13px;line-height:1.4;border:1px Solid #fff;background:#fff;color:#0=
+90;font-weight:normal">
+<strong>Du har blivit inbjuden till f=C3=B6ljande h=C3=A4ndelse.</strong></=
+h4>
+<div style=3D"padding:2px"><span itemprop=3D"publisher" itemscope=3D"" item=
+type=3D"http://schema.org/Organization">
+<meta itemprop=3D"name" content=3D"Google Calendar">
+</span>
+<meta itemprop=3D"eventId/googleCalendar" content=3D"65m17hsdolmotv3kvmrtg4=
+0ont">
+<h3 style=3D"padding:0 0 6px 0;margin:0;font-family:Arial,Sans-serif;font-s=
+ize:16px;font-weight:bold;color:#222">
+<span itemprop=3D"name">Meeeeet me HTML</span></h3>
+<table style=3D"display:inline-table" cellpadding=3D"0" cellspacing=3D"0" b=
+order=3D"0" summary=3D"Uppgifter om h=C3=A4ndelse">
+<tbody>
+<tr>
+<td style=3D"padding:0 1em 10px 0;font-family:Arial,Sans-serif;font-size:13=
+px;color:#888;white-space:nowrap;width:90px" valign=3D"top">
+<div><i style=3D"font-style:normal">N=C3=A4r</i></div>
+</td>
+<td style=3D"padding-bottom:10px;font-family:Arial,Sans-serif;font-size:13p=
+x;color:#222" valign=3D"top">
+<div style=3D"text-indent:-1px"><time itemprop=3D"startDate" datetime=3D"20=
+210322T093000Z"></time><time itemprop=3D"endDate" datetime=3D"20210322T1030=
+00Z"></time>m=C3=A5n den 22 mars 2021 11:30am =E2=80=93 12:30pm
+<span style=3D"color:#888">=C3=96steuropeisk tid - Helsingfors</span></div>
+</td>
+</tr>
+<tr>
+<td style=3D"padding:0 1em 4px 0;font-family:Arial,Sans-serif;font-size:13p=
+x;color:#888;white-space:nowrap;width:90px" valign=3D"top">
+<div><i style=3D"font-style:normal">Anslutningsinfo</i></div>
+</td>
+<td style=3D"padding-bottom:4px;font-family:Arial,Sans-serif;font-size:13px=
+;color:#222" valign=3D"top">
+<div style=3D"text-indent:-1px">Anslut till Google Meet</div>
+</td>
+</tr>
+<tr>
+<td style=3D"padding:0 1em 10px 0;font-family:Arial,Sans-serif;font-size:13=
+px;color:#888;white-space:nowrap;width:90px">
+</td>
+<td style=3D"padding-bottom:10px;font-family:Arial,Sans-serif;font-size:13p=
+x;color:#222" valign=3D"top">
+<div style=3D"text-indent:-1px">
+<div style=3D"text-indent:-1px"><span itemprop=3D"potentialaction" itemscop=
+e=3D"" itemtype=3D"http://schema.org/JoinAction"><span itemprop=3D"name" co=
+ntent=3D"meet.google.com/pyb-ndcu-hhc"><span itemprop=3D"target" itemscope=
+=3D"" itemtype=3D"http://schema.org/EntryPoint"><span itemprop=3D"url" cont=
+ent=3D"https://meet.google.com/pyb-ndcu-hhc?hs=3D224"><span itemprop=3D"htt=
+pMethod" content=3D"GET"><a href=3D"https://meet.google.com/pyb-ndcu-hhc?hs=
+=3D224" style=3D"color:#20c;white-space:nowrap" target=3D"_blank">meet.goog=
+le.com/pyb-ndcu-hhc</a></span></span></span></span></span>
+</div>
+</div>
+</td>
+</tr>
+<tr>
+<td style=3D"padding:0 1em 10px 0;font-family:Arial,Sans-serif;font-size:13=
+px;color:#888;white-space:nowrap;width:90px" valign=3D"top">
+<div><i style=3D"font-style:normal">Kalender</i></div>
+</td>
+<td style=3D"padding-bottom:10px;font-family:Arial,Sans-serif;font-size:13p=
+x;color:#222" valign=3D"top">
+<div style=3D"text-indent:-1px">homer@example.com</div>
+</td>
+</tr>
+<tr>
+<td style=3D"padding:0 1em 10px 0;font-family:Arial,Sans-serif;font-size:13=
+px;color:#888;white-space:nowrap;width:90px" valign=3D"top">
+<div><i style=3D"font-style:normal">Vem</i></div>
+</td>
+<td style=3D"padding-bottom:10px;font-family:Arial,Sans-serif;font-size:13p=
+x;color:#222" valign=3D"top">
+<table cellspacing=3D"0" cellpadding=3D"0">
+<tbody>
+<tr>
+<td style=3D"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px=
+;color:#222;width:10px">
+<div style=3D"text-indent:-1px"><span style=3D"font-family:Courier New,mono=
+space">=E2=80=A2</span></div>
+</td>
+<td style=3D"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px=
+;color:#222">
+<div style=3D"text-indent:-1px">
+<div>
+<div style=3D"margin:0 0 0.3em 0"><span itemprop=3D"attendee" itemscope=3D"=
+" itemtype=3D"http://schema.org/Person"><span itemprop=3D"name" class=3D"no=
+translate">example@gmail.com</span>
+<meta itemprop=3D"email" content=3D"example@gmail.com">
+</span><span itemprop=3D"organizer" itemscope=3D"" itemtype=3D"http://schem=
+a.org/Person">
+<meta itemprop=3D"name" content=3D"example@gmail.com">
+<meta itemprop=3D"email" content=3D"example@gmail.com">
+</span><span style=3D"font-size:11px;color:#888">=E2=80=93 organisat=C3=B6r=
+</span></div>
+</div>
+</div>
+</td>
+</tr>
+<tr>
+<td style=3D"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px=
+;color:#222;width:10px">
+<div style=3D"text-indent:-1px"><span style=3D"font-family:Courier New,mono=
+space">=E2=80=A2</span></div>
+</td>
+<td style=3D"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px=
+;color:#222">
+<div style=3D"text-indent:-1px">
+<div>
+<div style=3D"margin:0 0 0.3em 0"><span itemprop=3D"attendee" itemscope=3D"=
+" itemtype=3D"http://schema.org/Person"><span itemprop=3D"name" class=3D"no=
+translate">homer@example.com</span>
+<meta itemprop=3D"email" content=3D"homer@example.com">
+</span></div>
+</div>
+</div>
+</td>
+</tr>
+</tbody>
+</table>
+</td>
+</tr>
+</tbody>
+</table>
+<div style=3D"float:right;font-weight:bold;font-size:13px"><a href=3D"https=
+://calendar.google.com/calendar/event?action=3DVIEW&eid=3DNjVtMTdoc2Rvb=
+G1vdHYza3ZtcnRnNDBvbnQgbWFnbnVzLm1lbGluQGh1dC5maQ&tok=3DMjEjYmVydGF0aGV=
+ib3RAZ21haWwuY29tZTg2NGFjYmNjYWE1MjVlZWJmY2UzYmRmMDAyNWU0MDkzNDAxZjRhZg&=
+;ctz=3DEurope%2FHelsinki&hl=3Dsv&es=3D0" style=3D"color:#20c;white-=
+space:nowrap" itemprop=3D"url">mer
+ information =C2=BB</a><br>
+</div>
+<div style=3D"padding-bottom:15px;font-family:Arial,Sans-serif;font-size:13=
+px;color:#222;white-space:pre-wrap!important;white-space:-moz-pre-wrap!impo=
+rtant;white-space:-pre-wrap!important;white-space:-o-pre-wrap!important;whi=
+te-space:pre;word-wrap:break-word">
+<span>This is a test. <b>Bold</b>. <i>Italic</i>. <br>
+<br>
+Will discuss address for email <<a href=3D"mailto:foo@example.com" targe=
+t=3D"_blank">foo@example.com</a>> and
+<a href=3D"https://www.google.com/url?q=3Dhttp%3A%2F%2Fexample.com%3Ffoo%3D=
+bar&sa=3DD&ust=3D1616836340813000&usg=3DAOvVaw04gjO0O3Bf1tJs9vs=
+BMj3x" target=3D"_blank">
+http://example.com?foo=3Dbar</a>.</span>
+<meta itemprop=3D"description" content=3D"This is a test. Bold. Italic.&nbs=
+p;Will discuss address for email <foo@example.com> and http://example=
+.com?foo=3Dbar.">
+</div>
+</div>
+<p style=3D"color:#222;font-size:13px;margin:0"><span style=3D"color:#888">=
+Ska du delta (homer@example.com)?
+</span><wbr><strong><span itemprop=3D"potentialaction" itemscope=3D"" itemt=
+ype=3D"http://schema.org/RsvpAction">
+<meta itemprop=3D"attendance" content=3D"http://schema.org/RsvpAttendance/Y=
+es">
+<span itemprop=3D"handler" itemscope=3D"" itemtype=3D"http://schema.org/Htt=
+pActionHandler"><link itemprop=3D"method" href=3D"http://schema.org/HttpReq=
+uestMethod/GET"><a href=3D"https://calendar.google.com/calendar/event?actio=
+n=3DRESPOND&eid=3DNjVtMTdoc2RvbG1vdHYza3ZtcnRnNDBvbnQgbWFnbnVzLm1lbGluQ=
+Gh1dC5maQ&rst=3D1&tok=3DMjEjYmVydGF0aGVib3RAZ21haWwuY29tZTg2NGFjYmN=
+jYWE1MjVlZWJmY2UzYmRmMDAyNWU0MDkzNDAxZjRhZg&ctz=3DEurope%2FHelsinki&=
+;hl=3Dsv&es=3D0" style=3D"color:#20c;white-space:nowrap" itemprop=3D"ur=
+l">Ja</a></span></span><span style=3D"margin:0 0.4em;font-weight:normal">
+ - </span><span itemprop=3D"potentialaction" itemscope=3D"" itemtype=3D"htt=
+p://schema.org/RsvpAction">
+<meta itemprop=3D"attendance" content=3D"http://schema.org/RsvpAttendance/M=
+aybe">
+<span itemprop=3D"handler" itemscope=3D"" itemtype=3D"http://schema.org/Htt=
+pActionHandler"><link itemprop=3D"method" href=3D"http://schema.org/HttpReq=
+uestMethod/GET"><a href=3D"https://calendar.google.com/calendar/event?actio=
+n=3DRESPOND&eid=3DNjVtMTdoc2RvbG1vdHYza3ZtcnRnNDBvbnQgbWFnbnVzLm1lbGluQ=
+Gh1dC5maQ&rst=3D3&tok=3DMjEjYmVydGF0aGVib3RAZ21haWwuY29tZTg2NGFjYmN=
+jYWE1MjVlZWJmY2UzYmRmMDAyNWU0MDkzNDAxZjRhZg&ctz=3DEurope%2FHelsinki&=
+;hl=3Dsv&es=3D0" style=3D"color:#20c;white-space:nowrap" itemprop=3D"ur=
+l">Kanske</a></span></span><span style=3D"margin:0 0.4em;font-weight:normal=
+">
+ - </span><span itemprop=3D"potentialaction" itemscope=3D"" itemtype=3D"htt=
+p://schema.org/RsvpAction">
+<meta itemprop=3D"attendance" content=3D"http://schema.org/RsvpAttendance/N=
+o">
+<span itemprop=3D"handler" itemscope=3D"" itemtype=3D"http://schema.org/Htt=
+pActionHandler"><link itemprop=3D"method" href=3D"http://schema.org/HttpReq=
+uestMethod/GET"><a href=3D"https://calendar.google.com/calendar/event?actio=
+n=3DRESPOND&eid=3DNjVtMTdoc2RvbG1vdHYza3ZtcnRnNDBvbnQgbWFnbnVzLm1lbGluQ=
+Gh1dC5maQ&rst=3D2&tok=3DMjEjYmVydGF0aGVib3RAZ21haWwuY29tZTg2NGFjYmN=
+jYWE1MjVlZWJmY2UzYmRmMDAyNWU0MDkzNDAxZjRhZg&ctz=3DEurope%2FHelsinki&=
+;hl=3Dsv&es=3D0" style=3D"color:#20c;white-space:nowrap" itemprop=3D"ur=
+l">Nej</a></span></span></strong>
+<wbr><a href=3D"https://calendar.google.com/calendar/event?action=3DVIEW&am=
+p;eid=3DNjVtMTdoc2RvbG1vdHYza3ZtcnRnNDBvbnQgbWFnbnVzLm1lbGluQGh1dC5maQ&=
+tok=3DMjEjYmVydGF0aGVib3RAZ21haWwuY29tZTg2NGFjYmNjYWE1MjVlZWJmY2UzYmRmMDAyN=
+WU0MDkzNDAxZjRhZg&ctz=3DEurope%2FHelsinki&hl=3Dsv&es=3D0" style=
+=3D"color:#20c;white-space:nowrap" itemprop=3D"url">fler
+ alternativ =C2=BB</a></p>
+</td>
+</tr>
+<tr>
+<td style=3D"background-color:#f6f6f6;color:#888;border-top:1px Solid #ccc;=
+font-family:Arial,Sans-serif;font-size:11px">
+<p>Inbjudan fr=C3=A5n <a href=3D"https://calendar.google.com/calendar/" tar=
+get=3D"_blank" style=3D"">
+Google Kalender</a></p>
+<p>Detta e-postmeddelande har skickats till kontot homer@example.com efte=
+rsom du =C3=A4r deltagare vid denna h=C3=A4ndelse.</p>
+<p>Om du inte vill f=C3=A5 uppdateringar om denna h=C3=A4ndelse i framtiden=
+ kan du tacka nej till denna h=C3=A4ndelse. Du kan =C3=A4ven registrera dig=
+ f=C3=B6r att f=C3=A5 ett Google-konto p=C3=A5 https://calendar.google.com/=
+calendar/ och kontrollera aviseringsinst=C3=A4llningarna f=C3=B6r hela kale=
+ndern.</p>
+<p>Om du vidarebefordrar den h=C3=A4r inbjudan kan det g=C3=B6ra det m=C3=
+=B6jligt f=C3=B6r alla mottagare att skicka ett svar till organisat=C3=B6re=
+n och l=C3=A4ggas till p=C3=A5 g=C3=A4stlistan, bjuda in andra oavsett dera=
+s egen inbjudningsstatus eller modifiera ditt OSA.
+<a href=3D"https://support.google.com/calendar/answer/37135#forwarding">L=
+=C3=A4s mer</a>.</p>
+</td>
+</tr>
+</tbody>
+</table>
+</div>
+</span></span>
+</body>
+</html>
+
+--0000000000008c6d6005be1c767c
+Content-Type: text/calendar; charset="UTF-8"; method=REQUEST
+Content-Transfer-Encoding: quoted-printable
+
+BEGIN:VCALENDAR
+PRODID:-//Google Inc//Google Calendar 70.9054//EN
+VERSION:2.0
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VEVENT
+DTSTART:20210322T093000Z
+DTEND:20210322T103000Z
+DTSTAMP:20210322T091220Z
+ORGANIZER;CN=3Dexample@gmail.com:mailto:example@gmail.com
+UID:65m17hsdolmotv3kvmrtg40ont@google.com
+ATTENDEE;CUTYPE=3DINDIVIDUAL;ROLE=3DREQ-PARTICIPANT;PARTSTAT=3DACCEPTED;RSV=
+P=3DTRUE
+ ;CN=3Dexample@gmail.com;X-NUM-GUESTS=3D0:mailto:example@gmail.com
+ATTENDEE;CUTYPE=3DINDIVIDUAL;ROLE=3DREQ-PARTICIPANT;PARTSTAT=3DNEEDS-ACTION=
+;RSVP=3D
+ TRUE;CN=3Dhomer@example.com;X-NUM-GUESTS=3D0:mailto:homer@example.com
+X-MICROSOFT-CDO-OWNERAPPTID:-410050292
+CREATED:20210322T091220Z
+DESCRIPTION:This is a test. <b>Bold</b>. <i>Italic</i>. \;<br><br>Will=
+=20
+ discuss address for email <\;foo@example.com>\; and http://example.com=
+?
+ foo=3Dbar.\n\n-::~:~::~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:=
+~:~
+ :~:~:~:~:~:~:~:~::~:~::-\n=C3=84ndra inte det h=C3=A4r avsnittet i beskriv=
+ningen.\n\n
+ Den h=C3=A4r h=C3=A4ndelsen har ett videosamtal.\nG=C3=A5 med: https://mee=
+t.google.com/pyb
+ -ndcu-hhc\n\nVisa din h=C3=A4ndelse p=C3=A5 https://calendar.google.com/ca=
+lendar/even
+ t?action=3DVIEW&eid=3DNjVtMTdoc2RvbG1vdHYza3ZtcnRnNDBvbnQgbWFnbnVzLm1lbGlu=
+QGh1d
+ C5maQ&tok=3DMjEjYmVydGF0aGVib3RAZ21haWwuY29tZTg2NGFjYmNjYWE1MjVlZWJmY2UzYm=
+RmM
+ DAyNWU0MDkzNDAxZjRhZg&ctz=3DEurope%2FHelsinki&hl=3Dsv&es=3D1.\n-::~:~::~:~=
+:~:~:~:
+ ~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~::~:~::-
+LAST-MODIFIED:20210322T091220Z
+LOCATION:
+SEQUENCE:0
+STATUS:CONFIRMED
+SUMMARY:Meeeeet me HTML
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+
+--0000000000008c6d6005be1c767c--
+
+--0000000000008c6d6205be1c767e
+Content-Type: application/ics; name="invite.ics"
+Content-Disposition: attachment; filename="invite.ics"
+Content-Transfer-Encoding: base64
+
+QkVHSU46VkNBTEVOREFSClBST0RJRDotLy9Hb29nbGUgSW5jLy9Hb29nbGUgQ2FsZW5kYXIgNzAu
+OTA1NC8vRU4KVkVSU0lPTjoyLjAKQ0FMU0NBTEU6R1JFR09SSUFOCk1FVEhPRDpSRVFVRVNUCkJF
+R0lOOlZFVkVOVApEVFNUQVJUOjIwMjEwMzIyVDA5MzAwMFoKRFRFTkQ6MjAyMTAzMjJUMTAzMDAw
+WgpEVFNUQU1QOjIwMjEwMzIyVDA5MTIyMFoKT1JHQU5JWkVSO0NOPWV4YW1wbGVAZ21haWwuY29t
+Om1haWx0bzpleGFtcGxlQGdtYWlsLmNvbQpVSUQ6NjVtMTdoc2RvbG1vdHYza3ZtcnRnNDBvbnRA
+Z29vZ2xlLmNvbQpBVFRFTkRFRTtDVVRZUEU9SU5ESVZJRFVBTDtST0xFPVJFUS1QQVJUSUNJUEFO
+VDtQQVJUU1RBVD1BQ0NFUFRFRDtSU1ZQPVRSVUUKIDtDTj1leGFtcGxlQGdtYWlsLmNvbTtYLU5V
+TS1HVUVTVFM9MDptYWlsdG86ZXhhbXBsZUBnbWFpbC5jb20KQVRURU5ERUU7Q1VUWVBFPUlORElW
+SURVQUw7Uk9MRT1SRVEtUEFSVElDSVBBTlQ7UEFSVFNUQVQ9TkVFRFMtQUNUSU9OO1JTVlA9CiBU
+UlVFO0NOPWhvbWVyQGV4YW1wbGUuY29tO1gtTlVNLUdVRVNUUz0wOm1haWx0bzpob21lckBleGFt
+cGxlLmNvbQpYLU1JQ1JPU09GVC1DRE8tT1dORVJBUFBUSUQ6LTQxMDA1MDI5MgpDUkVBVEVEOjIw
+MjEwMzIyVDA5MTIyMFoKREVTQ1JJUFRJT046VGhpcyBpcyBhIHRlc3QuIDxiPkJvbGQ8L2I+LiA8
+aT5JdGFsaWM8L2k+LiZuYnNwXDs8YnI+PGJyPldpbGwgCiBkaXNjdXNzIGFkZHJlc3MgZm9yIGVt
+YWlsICZsdFw7Zm9vQGV4YW1wbGUuY29tJmd0XDsgYW5kIGh0dHA6Ly9leGFtcGxlLmNvbT8KIGZv
+bz1iYXIuXG5cbi06On46fjo6fjp+On46fjp+On46fjp+On46fjp+On46fjp+On46fjp+On46fjp+
+On46fjp+On46fjp+On46fgogOn46fjp+On46fjp+On46fjo6fjp+OjotXG7DhG5kcmEgaW50ZSBk
+ZXQgaMOkciBhdnNuaXR0ZXQgaSBiZXNrcml2bmluZ2VuLlxuXG4KIERlbiBow6RyIGjDpG5kZWxz
+ZW4gaGFyIGV0dCB2aWRlb3NhbXRhbC5cbkfDpSBtZWQ6IGh0dHBzOi8vbWVldC5nb29nbGUuY29t
+L3B5YgogLW5kY3UtaGhjXG5cblZpc2EgZGluIGjDpG5kZWxzZSBww6UgaHR0cHM6Ly9jYWxlbmRh
+ci5nb29nbGUuY29tL2NhbGVuZGFyL2V2ZW4KIHQ/YWN0aW9uPVZJRVcmZWlkPU5qVnRNVGRvYzJS
+dmJHMXZkSFl6YTNadGNuUm5OREJ2Ym5RZ2JXRm5iblZ6TG0xbGJHbHVRR2gxZAogQzVtYVEmdG9r
+PU1qRWpZbVZ5ZEdGMGFHVmliM1JBWjIxaGFXd3VZMjl0WlRnMk5HRmpZbU5qWVdFMU1qVmxaV0pt
+WTJVelltUm1NCiBEQXlOV1UwTURrek5EQXhaalJoWmcmY3R6PUV1cm9wZSUyRkhlbHNpbmtpJmhs
+PXN2JmVzPTEuXG4tOjp+On46On46fjp+On46fjoKIH46fjp+On46fjp+On46fjp+On46fjp+On46
+fjp+On46fjp+On46fjp+On46fjp+On46fjp+On46fjp+On46On46fjo6LQpMQVNULU1PRElGSUVE
+OjIwMjEwMzIyVDA5MTIyMFoKTE9DQVRJT046ClNFUVVFTkNFOjAKU1RBVFVTOkNPTkZJUk1FRApT
+VU1NQVJZOk1lZWVlZXQgbWUgSFRNTApUUkFOU1A6T1BBUVVFCkVORDpWRVZFTlQKRU5EOlZDQUxF
+TkRBUgo=
+
+--0000000000008c6d6205be1c767e--
diff --git a/comm/calendar/test/browser/invitations/data/message-containing-event.eml b/comm/calendar/test/browser/invitations/data/message-containing-event.eml new file mode 100644 index 0000000000..d27c2976db --- /dev/null +++ b/comm/calendar/test/browser/invitations/data/message-containing-event.eml @@ -0,0 +1,44 @@ +From: ExampleStore <noreply@example.com> +Date: Wed, 24 Aug 2016 16:40:06 -0400 +Subject: ExampleStore - booking 01.09.2016 @ 09.25 - 09.50 +Content-Type: multipart/mixed; + boundary="_=aspNetEmail=_51bed191ceac49f7a22392ea84b6ef35" +To: <foo@example.com> +Message-ID: <df0f52ae-d3dc-4b89-bc3e-67fc4f6e8552@example.com> +MIME-Version: 1.0 + +--_=aspNetEmail=_51bed191ceac49f7a22392ea84b6ef35 +Content-Type: multipart/alternative; + boundary="_=ALT_=aspNetEmail=_51bed191ceac49f7a22392ea84b6ef35" + +--_=ALT_=aspNetEmail=_51bed191ceac49f7a22392ea84b6ef35 +Content-Type: text/plain; charset="UTF-8" + +Remember your booking @ 09.25 + +--_=ALT_=aspNetEmail=_51bed191ceac49f7a22392ea84b6ef35 +Content-Type: text/html; charset="UTF-8" + +<html> +<body> +<p>You have a booking for 9.25</p> +</body> +</html> + +--_=ALT_=aspNetEmail=_51bed191ceac49f7a22392ea84b6ef35-- + +--_=aspNetEmail=_51bed191ceac49f7a22392ea84b6ef35 +Content-Type: TeXt/CaLeNdAr; method=PUBLISH; charset=UTF-8 +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="booking.ics" + +QkVHSU46VkNBTEVOREFSDQpWRVJTSU9OOjIuMA0KTUVUSE9EOlBVQkxJU0gNClBST0RJRDpleGFt +cGxlLmNvbQ0KQkVHSU46VkVWRU5UDQpEVFNUQVJUOjIwMTYwOTAxVDA2MjUwMFoNCkRURU5EOjIw +MTYwOTAxVDA2NTAwMFoNCkRUU1RBTVA6MjAxNjA4MjRUMjA0MDAwWg0KVUlEOjIwMTYwODI0VDIw +NDAwMFotNTY4ODYwMjgwQGV4YW1wbGUuY29tDQpTVU1NQVJZOkhhaXJjdXQNCk9SR0FOSVpFUjpt +YWlsdG86c29tZW9uZUBleGFtcGxlLmNvbQ0KREVTQ1JJUFRJT046SGFpcmN1dCtzdHlsaW5nDQpM +T0NBVElPTjpTb21ld2hlcmUNClRSQU5TUDpPUEFRVUUNClNFUVVFTkNFOjANCkNMQVNTOlBVQkxJ +Qw0KQkVHSU46VkFMQVJNDQpUUklHR0VSOi1QVDYwTQ0KQUNUSU9OOkFVRElPDQpERVNDUklQVElP +TjpSZW1pbmRlcg0KRU5EOlZBTEFSTQ0KRU5EOlZFVkVOVA0KRU5EOlZDQUxFTkRBUg== + +--_=aspNetEmail=_51bed191ceac49f7a22392ea84b6ef35-- diff --git a/comm/calendar/test/browser/invitations/data/message-non-invite.eml b/comm/calendar/test/browser/invitations/data/message-non-invite.eml new file mode 100644 index 0000000000..cf391f445a --- /dev/null +++ b/comm/calendar/test/browser/invitations/data/message-non-invite.eml @@ -0,0 +1,115 @@ +Date: Sun, 28 Nov 2021 21:39:31 +0000 +From: Jane <noreply@example.com> +To: <john.doe@example.com> +Message-ID: <1074020157.32201638135571450.JavaMail.root@hki-example-prod-app-004> +Subject: We're having a party - you're NOT invited +Content-Type: multipart/mixed; + boundary="----=_Part_6440_2094089067.1638135571440" +MIME-Version: 1.0 + +------=_Part_6440_2094089067.1638135571440 +Content-Type: multipart/related; + boundary="----=_Part_6441_499243807.1638135571440" + +------=_Part_6441_499243807.1638135571440 +Content-Type: text/plain; charset="UTF-8" + +Hey, we're having a party! You're not invited ;) + +------=_Part_6441_499243807.1638135571440-- + +------=_Part_6440_2094089067.1638135571440 +Content-Type: text/calendar; charset="utf-8"; name="event.ics" +Content-Transfer-Encoding: quoted-printable +Content-Disposition: attachment; filename="event.ics" + +BEGIN:VCALENDAR +PRODID:-//EXAMPLE:COM//iCal4j 1.0.5.2//EN +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +BEGIN:VTIMEZONE +TZID:Europe/Helsinki +TZURL:http://tzurl.org/zoneinfo/Europe/Helsinki +X-LIC-LOCATION:Europe/Helsinki +BEGIN:DAYLIGHT +TZOFFSETFROM:+0200 +TZOFFSETTO:+0300 +TZNAME:EEST +DTSTART:19830327T030000 +RRULE:FREQ=3DYEARLY;BYMONTH=3D3;BYDAY=3D-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0300 +TZOFFSETTO:+0200 +TZNAME:EET +DTSTART:19961027T040000 +RRULE:FREQ=3DYEARLY;BYMONTH=3D10;BYDAY=3D-1SU +END:STANDARD +BEGIN:STANDARD +TZOFFSETFROM:+013952 +TZOFFSETTO:+013952 +TZNAME:HMT +DTSTART:18780531T000000 +RDATE:18780531T000000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETFROM:+013952 +TZOFFSETTO:+0200 +TZNAME:EET +DTSTART:19210501T000000 +RDATE:19210501T000000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETFROM:+0200 +TZOFFSETTO:+0300 +TZNAME:EEST +DTSTART:19420403T000000 +RDATE:19420403T000000 +RDATE:19810329T020000 +RDATE:19820328T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0300 +TZOFFSETTO:+0200 +TZNAME:EET +DTSTART:19421003T000000 +RDATE:19421003T000000 +RDATE:19810927T030000 +RDATE:19820926T030000 +RDATE:19830925T040000 +RDATE:19840930T040000 +RDATE:19850929T040000 +RDATE:19860928T040000 +RDATE:19870927T040000 +RDATE:19880925T040000 +RDATE:19890924T040000 +RDATE:19900930T040000 +RDATE:19910929T040000 +RDATE:19920927T040000 +RDATE:19930926T040000 +RDATE:19940925T040000 +RDATE:19950924T040000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0200 +TZNAME:EET +DTSTART:19830101T000000 +RDATE:19830101T000000 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTAMP:20211128T213931Z +DTSTART;TZID=3DEurope/Helsinki:20211129T105500 +DTEND;TZID=3DEurope/Helsinki:20211129T110000 +SUMMARY:Party at John's house\, Helsinki +ORGANIZER;CN=3DJANE:mailto:noreply@example.com +UID:1e5fd4e6-bc52-439c-ac76-40da54f57c77@secure.example.com +SEQUENCE:3 +STATUS:CONFIRMED +LAST-MODIFIED:20211128T213931Z +END:VEVENT +END:VCALENDAR + +------=_Part_6440_2094089067.1638135571440-- diff --git a/comm/calendar/test/browser/invitations/data/outlook-test-invite.eml b/comm/calendar/test/browser/invitations/data/outlook-test-invite.eml new file mode 100644 index 0000000000..de07a9b873 --- /dev/null +++ b/comm/calendar/test/browser/invitations/data/outlook-test-invite.eml @@ -0,0 +1,102 @@ +From: Marge <marge@example.org>
+To: Homer <homer@example.org>
+Subject: Testaus
+Thread-Topic: Testaus
+Thread-Index: AdfdgAAehFDfsPyXTommZqRYgeMqiQAABo4Q
+Date: Fri, 19 Nov 2021 20:00:31 +0000
+Message-ID: <HE1P190MB0540579ABA18FCE320901B31E39C9@HE1P190MB0540.EURP190.PROD.OUTLOOK.COM>
+Accept-Language: en-US
+Content-Language: en-US
+Content-Type: multipart/alternative;
+ boundary="_000_HE1P190MB0540579ABA18FCE320901B31E39C9HE1P190MB0540EURP_"
+MIME-Version: 1.0
+
+--_000_HE1P190MB0540579ABA18FCE320901B31E39C9HE1P190MB0540EURP_
+Content-Type: text/plain; charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+
+
+--_000_HE1P190MB0540579ABA18FCE320901B31E39C9HE1P190MB0540EURP_
+Content-Type: text/html; charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+<html xmlns:v=3D"urn:schemas-microsoft-com:vml" xmlns:o=3D"urn:schemas-micr=
+osoft-com:office:office" xmlns:w=3D"urn:schemas-microsoft-com:office:word" =
+xmlns:m=3D"http://schemas.microsoft.com/office/2004/12/omml" xmlns=3D"http:=
+//www.w3.org/TR/REC-html40"><head>
+<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Diso-8859-=
+1">
+<meta name=3D"Generator" content=3D"Microsoft Word 15 (filtered medium)">
+<style><!--
+/* Font Definitions */
+@font-face
+ {font-family:"Cambria Math";
+ panose-1:2 4 5 3 5 4 6 3 2 4;}
+@font-face
+ {font-family:Calibri;
+ panose-1:2 15 5 2 2 2 4 3 2 4;}
+/* Style Definitions */
+p.MsoNormal, li.MsoNormal, div.MsoNormal
+ {margin:0cm;
+ font-size:11.0pt;
+ font-family:"Calibri",sans-serif;
+ mso-fareast-language:EN-US;}
+span.EmailStyle18
+ {mso-style-type:personal-compose;
+ font-family:"Calibri",sans-serif;
+ color:windowtext;}
+.MsoChpDefault
+ {mso-style-type:export-only;
+ font-size:10.0pt;}
+@page WordSection1
+ {size:612.0pt 792.0pt;
+ margin:70.85pt 2.0cm 70.85pt 2.0cm;}
+div.WordSection1
+ {page:WordSection1;}
+--></style><!--[if gte mso 9]><xml>
+<o:shapedefaults v:ext=3D"edit" spidmax=3D"1026" />
+</xml><![endif]--><!--[if gte mso 9]><xml>
+<o:shapelayout v:ext=3D"edit">
+<o:idmap v:ext=3D"edit" data=3D"1" />
+</o:shapelayout></xml><![endif]-->
+</head>
+<body lang=3D"FI" link=3D"#0563C1" vlink=3D"#954F72" style=3D"word-wrap:bre=
+ak-word">
+<div class=3D"WordSection1">
+<p class=3D"MsoNormal"><o:p> </o:p></p>
+</div>
+</body>
+</html>
+
+--_000_HE1P190MB0540579ABA18FCE320901B31E39C9HE1P190MB0540EURP_
+Content-Type: text/calendar; charset="utf-8"; method=REQUEST
+Content-Transfer-Encoding: base64
+
+QkVHSU46VkNBTEVOREFSCk1FVEhPRDpSRVFVRVNUClBST0RJRDpNaWNyb3NvZnQgRXhjaGFuZ2Ug
+U2VydmVyIDIwMTAKVkVSU0lPTjoyLjAKQkVHSU46VlRJTUVaT05FClRaSUQ6RkxFIFN0YW5kYXJk
+IFRpbWUKQkVHSU46U1RBTkRBUkQKRFRTVEFSVDoxNjAxMDEwMVQwNDAwMDAKVFpPRkZTRVRGUk9N
+OiswMzAwClRaT0ZGU0VUVE86KzAyMDAKUlJVTEU6RlJFUT1ZRUFSTFk7SU5URVJWQUw9MTtCWURB
+WT0tMVNVO0JZTU9OVEg9MTAKRU5EOlNUQU5EQVJECkJFR0lOOkRBWUxJR0hUCkRUU1RBUlQ6MTYw
+MTAxMDFUMDMwMDAwClRaT0ZGU0VURlJPTTorMDIwMApUWk9GRlNFVFRPOiswMzAwClJSVUxFOkZS
+RVE9WUVBUkxZO0lOVEVSVkFMPTE7QllEQVk9LTFTVTtCWU1PTlRIPTMKRU5EOkRBWUxJR0hUCkVO
+RDpWVElNRVpPTkUKQkVHSU46VkVWRU5UCk9SR0FOSVpFUjtDTj1NYXJnZTptYWlsdG86bWFyZ2VA
+ZXhhbXBsZS5vcmcKQVRURU5ERUU7Uk9MRT1SRVEtUEFSVElDSVBBTlQ7UEFSVFNUQVQ9TkVFRFMt
+QUNUSU9OO1JTVlA9VFJVRTtDTj1Ib20KIGVyOm1haWx0bzpob21lckBleGFtcGxlLm9yZwpERVND
+UklQVElPTjtMQU5HVUFHRT1lbi1VUzpcbgpVSUQ6MDMwMDAwMDA4MjAwRTAwMDc0QzVCNzEwMUE4
+MkUwMDgwMDAwMDAwMDYwQjYwOEQwOTBEREQ3MDEwMDAwMDAwMDAwMDAwMDAKIDAxMDAwMDAwMDRC
+RTBDRkZBNTRCQ0Y2NEU5NTZFMzQxNDMzNjJDM0MwClNVTU1BUlk7TEFOR1VBR0U9ZW4tVVM6VGVz
+dGF1cwpEVFNUQVJUO1RaSUQ9RkxFIFN0YW5kYXJkIFRpbWU6MjAyMTExMjdUMDkwMDAwCkRURU5E
+O1RaSUQ9RkxFIFN0YW5kYXJkIFRpbWU6MjAyMTExMjdUMDkzMDAwCkNMQVNTOlBVQkxJQwpQUklP
+UklUWTo1CkRUU1RBTVA6MjAyMTExMTlUMjAwMDI5WgpUUkFOU1A6T1BBUVVFClNUQVRVUzpDT05G
+SVJNRUQKU0VRVUVOQ0U6MApMT0NBVElPTjtMQU5HVUFHRT1lbi1VUzoKWC1NSUNST1NPRlQtQ0RP
+LUFQUFQtU0VRVUVOQ0U6MApYLU1JQ1JPU09GVC1DRE8tT1dORVJBUFBUSUQ6LTcyMDEyODAyNwpY
+LU1JQ1JPU09GVC1DRE8tQlVTWVNUQVRVUzpURU5UQVRJVkUKWC1NSUNST1NPRlQtQ0RPLUlOVEVO
+REVEU1RBVFVTOkJVU1kKWC1NSUNST1NPRlQtQ0RPLUFMTERBWUVWRU5UOkZBTFNFClgtTUlDUk9T
+T0ZULUNETy1JTVBPUlRBTkNFOjEKWC1NSUNST1NPRlQtQ0RPLUlOU1RUWVBFOjAKWC1NSUNST1NP
+RlQtRE9OT1RGT1JXQVJETUVFVElORzpGQUxTRQpYLU1JQ1JPU09GVC1ESVNBTExPVy1DT1VOVEVS
+OkZBTFNFClgtTUlDUk9TT0ZULUxPQ0FUSU9OUzpbXQpCRUdJTjpWQUxBUk0KREVTQ1JJUFRJT046
+UkVNSU5ERVIKVFJJR0dFUjtSRUxBVEVEPVNUQVJUOi1QVDE1TQpBQ1RJT046RElTUExBWQpFTkQ6
+VkFMQVJNCkVORDpWRVZFTlQKRU5EOlZDQUxFTkRBUgo=
+
+--_000_HE1P190MB0540579ABA18FCE320901B31E39C9HE1P190MB0540EURP_--
diff --git a/comm/calendar/test/browser/invitations/data/repeat-event.eml b/comm/calendar/test/browser/invitations/data/repeat-event.eml new file mode 100644 index 0000000000..9247e6575b --- /dev/null +++ b/comm/calendar/test/browser/invitations/data/repeat-event.eml @@ -0,0 +1,49 @@ +MIME-Version: 1.0
+Date: Mon, 28 Mar 2022 17:49:35 +0000
+Subject: Invitation: Repeat Event @ Daily from 2pm to 3pm 3 times (AST) (receiver@example.com)
+From: sender@example.com
+To: receiver@example.com
+Content-Type: multipart/mixed; boundary="00000000000080f3db05db4aef5b"
+
+--00000000000080f3db05db4aef5b
+Content-Type: multipart/alternative; boundary="00000000000080f3da05db4aef59"
+
+--00000000000080f3da05db4aef59
+Content-Type: text/calendar; charset="UTF-8"; method=REQUEST
+Content-Transfer-Encoding: 7bit
+
+BEGIN:VCALENDAR
+METHOD:REQUEST
+BEGIN:VEVENT
+DTSTART:20220316T110000Z
+DTEND:20220316T113000Z
+RRULE:FREQ=DAILY;WKST=SU;COUNT=3;INTERVAL=1
+DTSTAMP:20220316T191602Z
+UID:02e79b96
+ORGANIZER;CN=Sender;
+ EMAIL=sender@example.com:mailto:sender@example.com
+ATTENDEE;CN=Sender;
+ EMAIL=sender@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=ACCEPTED;RSVP=FALSE:mailto:sender@example.com
+ATTENDEE;CN=Receiver;EMAIL=receiver@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:receiver@example.com
+ATTENDEE;CN=Other;EMAIL=other@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:other@example.com
+CREATED:20220328T174934Z
+LAST-MODIFIED:20220328T174934Z
+LOCATION:Somewhere
+SEQUENCE:0
+STATUS:CONFIRMED
+SUMMARY:Repeat Event
+DESCRIPTION:An event invitation.
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+TRIGGER:-P1D
+DESCRIPTION:This is an event reminder
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+
+--00000000000080f3da05db4aef59--
+--00000000000080f3db05db4aef5b--
diff --git a/comm/calendar/test/browser/invitations/data/repeat-update-major.eml b/comm/calendar/test/browser/invitations/data/repeat-update-major.eml new file mode 100644 index 0000000000..61fe9f5022 --- /dev/null +++ b/comm/calendar/test/browser/invitations/data/repeat-update-major.eml @@ -0,0 +1,49 @@ +MIME-Version: 1.0
+Date: Mon, 28 Mar 2022 17:49:35 +0000
+Subject: Repeat Update Major
+From: sender@example.com
+To: receiver@example.com
+Content-Type: multipart/mixed; boundary="00000000000080f3db05db4aef5b"
+
+--00000000000080f3db05db4aef5b
+Content-Type: multipart/alternative; boundary="00000000000080f3da05db4aef59"
+
+--00000000000080f3da05db4aef59
+Content-Type: text/calendar; charset="UTF-8"; method=REQUEST
+Content-Transfer-Encoding: 7bit
+
+BEGIN:VCALENDAR
+METHOD:REQUEST
+BEGIN:VEVENT
+DTSTART:20220316T050000Z
+DTEND:20220316T053000Z
+RRULE:FREQ=DAILY;WKST=SU;COUNT=3;INTERVAL=1
+DTSTAMP:20220316T191602Z
+UID:02e79b96
+ORGANIZER;CN=Sender;
+ EMAIL=sender@example.com:mailto:sender@example.com
+ATTENDEE;CN=Sender;
+ EMAIL=sender@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=ACCEPTED;RSVP=FALSE:mailto:sender@example.com
+ATTENDEE;CN=Receiver;EMAIL=receiver@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:receiver@example.com
+ATTENDEE;CN=Other;EMAIL=other@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:other@example.com
+CREATED:20220328T174934Z
+LAST-MODIFIED:20220328T174934Z
+LOCATION:Somewhere
+SEQUENCE:2
+STATUS:CONFIRMED
+SUMMARY:Repeat Event
+DESCRIPTION:An event invitation.
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+TRIGGER:-P1D
+DESCRIPTION:This is an event reminder
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+
+--00000000000080f3da05db4aef59--
+--00000000000080f3db05db4aef5b--
diff --git a/comm/calendar/test/browser/invitations/data/repeat-update-minor.eml b/comm/calendar/test/browser/invitations/data/repeat-update-minor.eml new file mode 100644 index 0000000000..a6ad357553 --- /dev/null +++ b/comm/calendar/test/browser/invitations/data/repeat-update-minor.eml @@ -0,0 +1,49 @@ +MIME-Version: 1.0
+Date: Mon, 28 Mar 2022 17:49:35 +0000
+Subject: Invitation: Repeat Update Minor
+From: sender@example.com
+To: receiver@example.com
+Content-Type: multipart/mixed; boundary="00000000000080f3db05db4aef5b"
+
+--00000000000080f3db05db4aef5b
+Content-Type: multipart/alternative; boundary="00000000000080f3da05db4aef59"
+
+--00000000000080f3da05db4aef59
+Content-Type: text/calendar; charset="UTF-8"; method=REQUEST
+Content-Transfer-Encoding: 7bit
+
+BEGIN:VCALENDAR
+METHOD:REQUEST
+BEGIN:VEVENT
+DTSTART:20220316T110000Z
+DTEND:20220316T113000Z
+RRULE:FREQ=DAILY;WKST=SU;COUNT=3;INTERVAL=1
+DTSTAMP:20220318T191602Z
+UID:02e79b96
+ORGANIZER;CN=Sender;
+ EMAIL=sender@example.com:mailto:sender@example.com
+ATTENDEE;CN=Sender;
+ EMAIL=sender@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=ACCEPTED;RSVP=FALSE:mailto:sender@example.com
+ATTENDEE;CN=Receiver;EMAIL=receiver@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:receiver@example.com
+ATTENDEE;CN=Other;EMAIL=other@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:other@example.com
+CREATED:20220328T174934Z
+LAST-MODIFIED:20220328T174934Z
+LOCATION:Updated location
+SEQUENCE:0
+STATUS:CONFIRMED
+SUMMARY:Updated Event
+DESCRIPTION:Updated description.
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+TRIGGER:-P1D
+DESCRIPTION:Updated description.
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+
+--00000000000080f3da05db4aef59--
+--00000000000080f3db05db4aef5b--
diff --git a/comm/calendar/test/browser/invitations/data/single-event.eml b/comm/calendar/test/browser/invitations/data/single-event.eml new file mode 100644 index 0000000000..14418c2c79 --- /dev/null +++ b/comm/calendar/test/browser/invitations/data/single-event.eml @@ -0,0 +1,78 @@ +MIME-Version: 1.0 +Content-Transfer-Encoding: binary +Content-Type: multipart/mixed; boundary="_----------=_1647458162153312762582" +Date: Wed, 16 Mar 2022 15:16:02 -0400 +To: receiver@example.com +Subject: Invitation: Single Event @ Wed, Mar 16 2022 11:00 AST +From: Sender <sender@example.com> + +This is a multi-part message in MIME format. + +--_----------=_1647458162153312762582 +MIME-Version: 1.0 +Content-Transfer-Encoding: binary +Content-Type: multipart/alternative; boundary="_----------=_1647458162153312762583" +Date: Wed, 16 Mar 2022 15:16:02 -0400 + +This is a multi-part message in MIME format. + +--_----------=_1647458162153312762583 +MIME-Version: 1.0 +Content-Disposition: inline +Content-Length: 227 +Content-Transfer-Encoding: binary +Content-Type: text/plain; charset="utf-8" +Date: Wed, 16 Mar 2022 15:16:02 -0400 + +Single Event + +When: + Wed, Mar 16 2022 + 11:00 - 12:00 AST +Where: + Somewhere + +--_----------=_1647458162153312762583 +MIME-Version: 1.0 +Content-Disposition: inline +Content-Transfer-Encoding: quoted-printable +Content-Type: text/calendar; charset="utf-8"; method="REQUEST" +Date: Wed, 16 Mar 2022 15:16:02 -0400 + +BEGIN:VCALENDAR +VERSION:2.0 +METHOD:REQUEST +CALSCALE:GREGORIAN +BEGIN:VEVENT +UID:02e79b96 +SEQUENCE:0 +DTSTAMP:20220316T191602Z +CREATED:20220316T191532Z +DTSTART:20220316T110000Z +DTEND:20220316T113000Z +DURATION:PT1H +PRIORITY:0 +SUMMARY:Single Event +DESCRIPTION:An event invitation. +LOCATION:Somewhere +STATUS:CONFIRMED +TRANSP:OPAQUE +CLASS:PUBLIC +ORGANIZER;CN=3DSender; + EMAIL=3Dsender@example.com:mailto:sender@example.com +ATTENDEE;CN=3DSender; + EMAIL=3Dsender@example.com;CUTYPE=3DINDIVIDUAL; + PARTSTAT=3DACCEPTED;RSVP=3DFALSE:mailto:sender@example.com +ATTENDEE;CN=Receiver;EMAIL=3Dreceiver@example.com;CUTYPE=3DINDIVIDUAL; + PARTSTAT=3DNEEDS-ACTION;RSVP=3DTRUE:mailto:receiver@example.com +ATTENDEE;CN=Other;EMAIL=other@example.com;CUTYPE=3DINDIVIDUAL; + PARTSTAT=3DNEEDS-ACTION;RSVP=3DTRUE:mailto:other@example.com +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-P1D +DESCRIPTION:This is an event reminder +END:VALARM +END:VEVENT +END:VCALENDAR + +--_----------=_1647458162153312762583-- diff --git a/comm/calendar/test/browser/invitations/data/teams-meeting-invite.eml b/comm/calendar/test/browser/invitations/data/teams-meeting-invite.eml new file mode 100644 index 0000000000..777486ec87 --- /dev/null +++ b/comm/calendar/test/browser/invitations/data/teams-meeting-invite.eml @@ -0,0 +1,167 @@ +From: Marge <marge@example.com>
+To: bart@example.com, homer@example.com
+Subject: Teams meeting
+Thread-Topic: Teams meeting
+Thread-Index: AdbIy2RnFnEYrGmq80aB3RiaEcOS6w==
+Date: Wed, 2 Dec 2020 16:52:34 +0000
+Message-ID: <HE1PR0802MB228346BE1576FEAB8A7F32328EF30@HE1PR0802MB2283.eurprd08.prod.outlook.com>
+Accept-Language: fi-FI, en-US
+Content-Language: en-US
+X-MS-Has-Attach:
+X-MS-TNEF-Correlator:
+Content-Type: multipart/alternative;
+ boundary="_000_HE1PR0802MB228346BE1576FEAB8A7F32328EF30HE1PR0802MB2283_"
+X-Spam-Flag: No
+Return-Path: marge@example.com
+MIME-Version: 1.0
+
+--_000_HE1PR0802MB228346BE1576FEAB8A7F32328EF30HE1PR0802MB2283_
+Content-Type: text/plain; charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+
+
+
+___________________________________________________________________________=
+_____
+Microsoft Teams -kokous
+Liity tietokoneella tai mobiilisovelluksella
+Liity kokoukseen napsauttamalla t=E4t=E4<https://teams.microsoft.com/l/meet=
+up-join/19%3ameeting_MHU5NmI5ZGAtOWZmOC00Y2ZmLWJlOTItNjUxNjA5YjUyYTYy%40thr=
+ead.v2/0?context=3D%7b%33Tid%22%3a%222fd0c1c5-28e1-40c4-9f0d-a0363ca80a3c%2=
+2%2c%22Oid%22%3a%2214464d09-ceb8-458c-a61c-717f1e5c66c5%22%7d>
+Lis=E4tietoja<https://aka.ms/JoinTeamsMeeting> | Kokousasetukset<https://te=
+ams.microsoft.com/meetingOptions/?organizerId=3D14464d09-ceb8-458c-a61c-717=
+f1e5c66c5&tenantId=3D2fd0c1c5-28e1-40c4-9f0d-a0363ca80a3c&threadId=3D19_mee=
+ting_MHU5NmI5ZGAtOWZmOC00Y2ZmLWJlOTItNjUxNjA5YjUyYTYy@thread.v2&messageId=
+=3D0&language=3Dfi-FI>
+___________________________________________________________________________=
+_____
+
+--_000_HE1PR0802MB228346BE1576FEAB8A7F32328EF30HE1PR0802MB2283_
+Content-Type: text/html; charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+<html>
+<head>
+<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Diso-8859-=
+1">
+</head>
+<body>
+<div><br>
+<br>
+<br>
+<div style=3D"width:100%;height: 20px;"><span style=3D"white-space:nowrap;c=
+olor:#5F5F5F;opacity:.36;">________________________________________________=
+________________________________</span>
+</div>
+<div class=3D"me-email-text" style=3D"color:#252424;font-family:'Segoe UI',=
+'Helvetica Neue',Helvetica,Arial,sans-serif;">
+<div style=3D"margin-top: 24px; margin-bottom: 20px;"><span style=3D"font-s=
+ize: 24px; color:#252424">Microsoft Teams -kokous</span>
+</div>
+<div style=3D"margin-bottom: 20px;">
+<div style=3D"margin-top: 0px; margin-bottom: 0px; font-weight: bold"><span=
+ style=3D"font-size: 14px; color:#252424">Liity tietokoneella tai mobiiliso=
+velluksella</span>
+</div>
+<a class=3D"me-email-headline" style=3D"font-size: 14px;font-family:'Segoe =
+UI Semibold','Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif;text-de=
+coration: underline;color: #6264a7;" href=3D"https://teams.microsoft.com/l/=
+meetup-join/19%3ameeting_MHU5NmI5ZGAtOWZmOC00Y2ZmLWJlOTItNjUxNjA5YjUyYTYy%4=
+0thread.v2/0?context=3D%7b%33Tid%22%3a%222fd0c1c5-28e1-40c4-9f0d-a0363ca80a=
+3c%22%2c%22Oid%22%3a%2214464d09-ceb8-458c-a61c-717f1e5c66c5%22%7d" target=
+=3D"_blank" rel=3D"noreferrer noopener">Liity
+ kokoukseen napsauttamalla t=E4t=E4</a> </div>
+<div style=3D"margin-bottom: 24px;margin-top: 20px;"><a class=3D"me-email-l=
+ink" style=3D"font-size: 14px;text-decoration: underline;color: #6264a7;fon=
+t-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif;" target=3D=
+"_blank" href=3D"https://aka.ms/JoinTeamsMeeting" rel=3D"noreferrer noopene=
+r">Lis=E4tietoja</a>
+ | <a class=3D"me-email-link" style=3D"font-size: 14px;text-decoration: und=
+erline;color: #6264a7;font-family:'Segoe UI','Helvetica Neue',Helvetica,Ari=
+al,sans-serif;" target=3D"_blank" href=3D"https://teams.microsoft.com/meeti=
+ngOptions/?organizerId=3D14464d09-ceb8-458c-a61c-717f1e5c66c5&tenantId=
+=3D2fd0c1c5-28e1-40c4-9f0d-a0363ca80a3c&threadId=3D19_meeting_MGU5NmI2Z=
+GYtOWZmOC00Y2ZmLWJlOTItNjUxNjA5YjUyYTYy@thread.v2&messageId=3D0&lan=
+guage=3Dfi-FI" rel=3D"noreferrer noopener">
+Kokousasetukset</a> </div>
+</div>
+<div style=3D"font-size: 14px; margin-bottom: 4px;font-family:'Segoe UI','H=
+elvetica Neue',Helvetica,Arial,sans-serif;">
+</div>
+<div style=3D"font-size: 12px;"></div>
+<div style=3D"width:100%;height: 20px;"><span style=3D"white-space:nowrap;c=
+olor:#5F5F5F;opacity:.36;">________________________________________________=
+________________________________</span>
+</div>
+</div>
+</body>
+</html>
+
+--_000_HE1PR0802MB228346BE1576FEAB8A7F32328EF30HE1PR0802MB2283_
+Content-Type: text/calendar; charset="utf-8"; method=REQUEST
+Content-Transfer-Encoding: base64
+
+QkVHSU46VkNBTEVOREFSCk1FVEhPRDpSRVFVRVNUClBST0RJRDpNaWNyb3NvZnQgRXhjaGFuZ2Ug
+U2VydmVyIDIwMTAKVkVSU0lPTjoyLjAKQkVHSU46VlRJTUVaT05FClRaSUQ6RkxFIFN0YW5kYXJk
+IFRpbWUKQkVHSU46U1RBTkRBUkQKRFRTVEFSVDoxNjAxMDEwMVQwNDAwMDAKVFpPRkZTRVRGUk9N
+OiswMzAwClRaT0ZGU0VUVE86KzAyMDAKUlJVTEU6RlJFUT1ZRUFSTFk7SU5URVJWQUw9MTtCWURB
+WT0tMVNVO0JZTU9OVEg9MTAKRU5EOlNUQU5EQVJECkJFR0lOOkRBWUxJR0hUCkRUU1RBUlQ6MTYw
+MTAxMDFUMDMwMDAwClRaT0ZGU0VURlJPTTorMDIwMApUWk9GRlNFVFRPOiswMzAwClJSVUxFOkZS
+RVE9WUVBUkxZO0lOVEVSVkFMPTE7QllEQVk9LTFTVTtCWU1PTlRIPTMKRU5EOkRBWUxJR0hUCkVO
+RDpWVElNRVpPTkUKQkVHSU46VkVWRU5UCk9SR0FOSVpFUjtDTj1NYXJnZTptYWlsdG86bWFyZ2VA
+ZXhhbXBsZS5jb20KQVRURU5ERUU7Uk9MRT1SRVEtUEFSVElDSVBBTlQ7UEFSVFNUQVQ9TkVFRFMt
+QUNUSU9OO1JTVlA9VFJVRTtDTj1iYXJ0QGUKIGV4YW1wbGUuY29tOm1haWx0bzpiYXJ0QGV4YW1w
+bGUuY29tCkFUVEVOREVFO1JPTEU9UkVRLVBBUlRJQ0lQQU5UO1BBUlRTVEFUPU5FRURTLUFDVElP
+TjtSU1ZQPVRSVUU7Q049aG9tZXJAZXgKIGFtcGxlLmNvbTptYWlsdG86aG9tZXJAZXhhbXBsZS5j
+b20KCkRFU0NSSVBUSU9OO0xBTkdVQUdFPWVuLVVTOlxuXG5cbl9fX19fX19fX19fX19fX19fX19f
+X19fX19fX19fX19fX19fX19fX19fXwogX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19f
+X19fX19cbk1pY3Jvc29mdCBUZWFtcyAta29rb3VzXG5MaWl0eSB0aWUKIHRva29uZWVsbGEgdGFp
+IG1vYmlpbGlzb3ZlbGx1a3NlbGxhXG5MaWl0eSBrb2tvdWtzZWVuIG5hcHNhdXR0YW1hbGxhIHTD
+pHQKIMOkPGh0dHBzOi8vdGVhbXMubWljcm9zb2Z0LmNvbS9sL21lZXR1cC1qb2luLzE5JTNhbWVl
+dGluZ19NR1U1Tm1JMlpHWXRPV1ptCiBPQzAwWTJabUxXSmxPVEl0TmpVeE5qQTVZalV5WVRZeSU0
+MHRocmVhZC52Mi8wP2NvbnRleHQ9JTdiJTIyVGlkJTIyJTNhJTIyMgogZmQwYzFjNS0yOGUxLTQw
+YzQtOWYwZC1hMDM2M2NhODBhM2MlMjIlMmMlMjJPaWQlMjIlM2ElMjIxNDQ2NGQwOS1jZWI4LTQ1
+OGMKIC1hNjFjLTcxN2YxZTVjNjZjNSUyMiU3ZD5cbkxpc8OkdGlldG9qYTxodHRwczovL2FrYS5t
+cy9Kb2luVGVhbXNNZWV0aW5nPiB8CiAgS29rb3VzYXNldHVrc2V0PGh0dHBzOi8vdGVhbXMubWlj
+cm9zb2Z0LmNvbS9tZWV0aW5nT3B0aW9ucy8/b3JnYW5pemVySWQ9MQogNDQ2NGQwOS1jZWI4LTQ1
+OGMtYTYxYy03MTdmMWU1YzY2YzUmdGVuYW50SWQ9MmZkMGMxYzUtMjhlMS00MGM0LTlmMGQtYTAz
+NjMKIGNhODBhM2MmdGhyZWFkSWQ9MTlfbWVldGluZ19NR1U1Tm1JMlpHWXRPV1ptT0MwMFkyWm1M
+V0psT1RJdE5qVXhOakE1WWpVeVlUCiBZeUB0aHJlYWQudjImbWVzc2FnZUlkPTAmbGFuZ3VhZ2U9
+ZmktRkk+XG5fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fXwogX19fX19fX19fX19fX19f
+X19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fXG4KVUlEOjA1NjAwMDAwODIwMEUwMDA3
+NEM1QjcxMDFBODJFMDA4MDAwMDAwMDAxQUY4NEM2NENCQzhENjAxMDAwMDAwMDAwMDAwMDAwCiAw
+MTAwMDAwMDA0MDNCNUFDMTBBMEVCNDQ0QTk0N0QyQjQ5OUE0Qjk4QwpTVU1NQVJZO0xBTkdVQUdF
+PWVuLVVTOlRlYW1zIG1lZXRpbmcKRFRTVEFSVDtUWklEPUZMRSBTdGFuZGFyZCBUaW1lOjIwMjAx
+MjAyVDE5MDAwMApEVEVORDtUWklEPUZMRSBTdGFuZGFyZCBUaW1lOjIwMjAxMjAyVDIxMDAwMApD
+TEFTUzpQVUJMSUMKUFJJT1JJVFk6NQpEVFNUQU1QOjIwMjAxMjAyVDE2NTEzNFoKVFJBTlNQOk9Q
+QVFVRQpTVEFUVVM6Q09ORklSTUVEClNFUVVFTkNFOjAKTE9DQVRJT047TEFOR1VBR0U9ZW4tVVM6
+ClgtTUlDUk9TT0ZULUNETy1BUFBULVNFUVVFTkNFOjAKWC1NSUNST1NPRlQtQ0RPLU9XTkVSQVBQ
+VElEOjIxMTg5MTUzNTQKWC1NSUNST1NPRlQtQ0RPLUJVU1lTVEFUVVM6VEVOVEFUSVZFClgtTUlD
+Uk9TT0ZULUNETy1JTlRFTkRFRFNUQVRVUzpCVVNZClgtTUlDUk9TT0ZULUNETy1BTExEQVlFVkVO
+VDpGQUxTRQpYLU1JQ1JPU09GVC1DRE8tSU1QT1JUQU5DRToxClgtTUlDUk9TT0ZULUNETy1JTlNU
+VFlQRTowClgtTUlDUk9TT0ZULVNLWVBFVEVBTVNNRUVUSU5HVVJMOmh0dHBzOi8vdGVhbXMubWlj
+cm9zb2Z0LmNvbS9sL21lZXR1cC1qb2luLwogMTklM2FtZWV0aW5nX01HVTVObUkyWkdZdE9XWm1P
+QzAwWTJabUxXSmxPVEl0TmpVeE5qQTVZalV5WVRZeSU0MHRocmVhZC52Mi8KIDA/Y29udGV4dD0l
+N2IlMjJUaWQlMjIlM2ElMjIyZmQwYzFjNS0xOGUxLTQwYzQtOWYwZC1hMDM2M2NhODBhM2MlMjIl
+MmMlMjJPCiBpZCUyMiUzYSUyMjE0NDY0ZDA5LWNlYjgtNDU4Yy1hNjFjLTcxN2YxZTVjNjZjNSUy
+MiU3ZApYLU1JQ1JPU09GVC1TQ0hFRFVMSU5HU0VSVklDRVVQREFURVVSTDpodHRwczovL3NjaGVk
+dWxlci50ZWFtcy5taWNyb3NvZnQuY28KIG0vdGVhbXMvMmZkMGMxYzUtMjhlMS00MGM0LTlmMGQt
+YWIzNjNjYTgwYTNjLzE0NDY0ZDA5LWNlYjgtNDU4Yy1hNjFjLTcxN2YxCiBlNWM2NmM1LzE5X21l
+ZXRpbmdfTUdVNU5tSTJaR1l0T1dabU9DMDBZMlptTFdKbE9USXROalV4TmpBNVlqVXlZVFl5QHRo
+cmVhZAogLnYyLzAKWC1NSUNST1NPRlQtU0tZUEVURUFNU1BST1BFUlRJRVM6eyJjaWQiOiIxOTpt
+ZWV0aW5nX01HVTVObUkyWkdZdE9XWm1PQzAwWTJaCiBtTFdKbE9USXROalV4TmpBNVlqVXlZVFl5
+QHRocmVhZC52MiJcLCJyaWQiOjBcLCJtaWQiOjBcLCJ1aWQiOm51bGxcLCJwcml2YQogdGUiOnRy
+dWVcLCJ0eXBlIjowfQpYLU1JQ1JPU09GVC1PTkxJTkVNRUVUSU5HQ09ORkxJTks6Y29uZjpzaXA6
+bWFyZ2VAZXhhbXBsZS5jb21cO2dydXVcO29wCiBhcXVlPWFwcDpjb25mOmZvY3VzOmlkOnRlYW1z
+OjI6MCExOTptZWV0aW5nX01HVTVNbUkyWkdZdE9XWm1PQzAwWTJabUxXSmxPVAogSXROalV4TmpB
+NVlqVXlZVFl5LXRocmVhZC52MiExNDQ2NGQwOWNlYjg0NThjYTYxYzcxN2YxZTVjNjZjNSEyZmQw
+YzFjNTI4ZTEKIDQwYzQ5ZjBkYTAzNjNjYTgwYTNjClgtTUlDUk9TT0ZULU9OTElORU1FRVRJTkdJ
+TkZPUk1BVElPTjp7Ik9ubGluZU1lZXRpbmdDaGFubmVsSWQiOm51bGxcLCJPbmxpbgogZU1lZXRp
+bmdQcm92aWRlciI6M30KWC1NSUNST1NPRlQtRE9OT1RGT1JXQVJETUVFVElORzpGQUxTRQpYLU1J
+Q1JPU09GVC1ESVNBTExPVy1DT1VOVEVSOkZBTFNFClgtTUlDUk9TT0ZULUxPQ0FUSU9OUzpbXQpC
+RUdJTjpWQUxBUk0KREVTQ1JJUFRJT046UkVNSU5ERVIKVFJJR0dFUjtSRUxBVEVEPVNUQVJUOi1Q
+VDE1TQpBQ1RJT046RElTUExBWQpFTkQ6VkFMQVJNCkVORDpWRVZFTlQKRU5EOlZDQUxFTkRBUgo=
+
+--_000_HE1PR0802MB228346BE1576FEAB8A7F32328EF30HE1PR0802MB2283_--
diff --git a/comm/calendar/test/browser/invitations/data/update-major.eml b/comm/calendar/test/browser/invitations/data/update-major.eml new file mode 100644 index 0000000000..04754798b2 --- /dev/null +++ b/comm/calendar/test/browser/invitations/data/update-major.eml @@ -0,0 +1,78 @@ +MIME-Version: 1.0 +Content-Transfer-Encoding: binary +Content-Type: multipart/mixed; boundary="_----------=_1647458162153312762582" +Date: Wed, 16 Mar 2022 15:16:02 -0400 +To: receiver@example.com +Subject: Update Major +From: Sender <sender@example.com> + +This is a multi-part message in MIME format. + +--_----------=_1647458162153312762582 +MIME-Version: 1.0 +Content-Transfer-Encoding: binary +Content-Type: multipart/alternative; boundary="_----------=_1647458162153312762583" +Date: Wed, 16 Mar 2022 15:16:02 -0400 + +This is a multi-part message in MIME format. + +--_----------=_1647458162153312762583 +MIME-Version: 1.0 +Content-Disposition: inline +Content-Length: 227 +Content-Transfer-Encoding: binary +Content-Type: text/plain; charset="utf-8" +Date: Wed, 16 Mar 2022 15:16:02 -0400 + +Single Event + +When: + Wed, Mar 16 2022 + 11:00 - 12:00 AST +Where: + Somewhere + +--_----------=_1647458162153312762583 +MIME-Version: 1.0 +Content-Disposition: inline +Content-Transfer-Encoding: quoted-printable +Content-Type: text/calendar; charset="utf-8"; method="REQUEST" +Date: Wed, 16 Mar 2022 15:16:02 -0400 + +BEGIN:VCALENDAR +VERSION:2.0 +METHOD:REQUEST +CALSCALE:GREGORIAN +BEGIN:VEVENT +UID:02e79b96 +SEQUENCE:2 +DTSTAMP:20220316T191602Z +CREATED:20220316T191532Z +DTSTART:20220316T050000Z +DTEND:20220316T053000Z +DURATION:PT1H +PRIORITY:0 +SUMMARY:Single Event +DESCRIPTION:An event invitation. +LOCATION:Somewhere +STATUS:CONFIRMED +TRANSP:OPAQUE +CLASS:PUBLIC +ORGANIZER;CN=3DSender; + EMAIL=3Dsender@example.com:mailto:sender@example.com +ATTENDEE;CN=3DSender; + EMAIL=3Dsender@example.com;CUTYPE=3DINDIVIDUAL; + PARTSTAT=3DACCEPTED;RSVP=3DFALSE:mailto:sender@example.com +ATTENDEE;CN=Receiver;EMAIL=3Dreceiver@example.com;CUTYPE=3DINDIVIDUAL; + PARTSTAT=3DNEEDS-ACTION;RSVP=3DTRUE:mailto:receiver@example.com +ATTENDEE;CN=Other;EMAIL=other@example.com;CUTYPE=3DINDIVIDUAL; + PARTSTAT=3DNEEDS-ACTION;RSVP=3DTRUE:mailto:other@example.com +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-P1D +DESCRIPTION:This is an event reminder +END:VALARM +END:VEVENT +END:VCALENDAR + +--_----------=_1647458162153312762583-- diff --git a/comm/calendar/test/browser/invitations/data/update-minor.eml b/comm/calendar/test/browser/invitations/data/update-minor.eml new file mode 100644 index 0000000000..afeb8e9ba0 --- /dev/null +++ b/comm/calendar/test/browser/invitations/data/update-minor.eml @@ -0,0 +1,78 @@ +MIME-Version: 1.0 +Content-Transfer-Encoding: binary +Content-Type: multipart/mixed; boundary="_----------=_1647458162153312762582" +Date: Wed, 16 Mar 2022 15:16:02 -0400 +To: receiver@example.com +Subject: Update Minor +From: Sender <sender@example.com> + +This is a multi-part message in MIME format. + +--_----------=_1647458162153312762582 +MIME-Version: 1.0 +Content-Transfer-Encoding: binary +Content-Type: multipart/alternative; boundary="_----------=_1647458162153312762583" +Date: Wed, 16 Mar 2022 15:16:02 -0400 + +This is a multi-part message in MIME format. + +--_----------=_1647458162153312762583 +MIME-Version: 1.0 +Content-Disposition: inline +Content-Length: 227 +Content-Transfer-Encoding: binary +Content-Type: text/plain; charset="utf-8" +Date: Wed, 16 Mar 2022 15:16:02 -0400 + +Single Event + +When: + Wed, Mar 16 2022 + 11:00 - 12:00 AST +Where: + Somewhere + +--_----------=_1647458162153312762583 +MIME-Version: 1.0 +Content-Disposition: inline +Content-Transfer-Encoding: quoted-printable +Content-Type: text/calendar; charset="utf-8"; method="REQUEST" +Date: Wed, 16 Mar 2022 15:16:02 -0400 + +BEGIN:VCALENDAR +VERSION:2.0 +METHOD:REQUEST +CALSCALE:GREGORIAN +BEGIN:VEVENT +UID:02e79b96 +SEQUENCE:0 +DTSTAMP:20220318T191602Z +CREATED:20220316T191532Z +DTSTART:20220316T110000Z +DTEND:20220316T113000Z +DURATION:PT1H +PRIORITY:0 +SUMMARY:Updated Event +DESCRIPTION:Updated description. +LOCATION:Updated location +STATUS:CONFIRMED +TRANSP:OPAQUE +CLASS:PUBLIC +ORGANIZER;CN=3DSender; + EMAIL=3Dsender@example.com:mailto:sender@example.com +ATTENDEE;CN=3DSender; + EMAIL=3Dsender@example.com;CUTYPE=3DINDIVIDUAL; + PARTSTAT=3DACCEPTED;RSVP=3DFALSE:mailto:sender@example.com +ATTENDEE;CN=Receiver;EMAIL=3Dreceiver@example.com;CUTYPE=3DINDIVIDUAL; + PARTSTAT=3DNEEDS-ACTION;RSVP=3DTRUE:mailto:receiver@example.com +ATTENDEE;CN=Other;EMAIL=other@example.com;CUTYPE=3DINDIVIDUAL; + PARTSTAT=3DNEEDS-ACTION;RSVP=3DTRUE:mailto:other@example.com +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-P1D +DESCRIPTION:Updated description. +END:VALARM +END:VEVENT +END:VCALENDAR + +--_----------=_1647458162153312762583-- diff --git a/comm/calendar/test/browser/invitations/head.js b/comm/calendar/test/browser/invitations/head.js new file mode 100644 index 0000000000..24835c3021 --- /dev/null +++ b/comm/calendar/test/browser/invitations/head.js @@ -0,0 +1,942 @@ +/* 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 <toolbarbutton> 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 <sender@example.com>", + }); + 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); +} diff --git a/comm/calendar/test/browser/preferences/browser.ini b/comm/calendar/test/browser/preferences/browser.ini new file mode 100644 index 0000000000..a06213b220 --- /dev/null +++ b/comm/calendar/test/browser/preferences/browser.ini @@ -0,0 +1,16 @@ +[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 + +[browser_alarmDefaultValue.js] +[browser_categoryColors.js] diff --git a/comm/calendar/test/browser/preferences/browser_alarmDefaultValue.js b/comm/calendar/test/browser/preferences/browser_alarmDefaultValue.js new file mode 100644 index 0000000000..b1cde7cf11 --- /dev/null +++ b/comm/calendar/test/browser/preferences/browser_alarmDefaultValue.js @@ -0,0 +1,176 @@ +/* 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/. */ + +/** + * Test default alarm settings for events and tasks + */ + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); + +var { CalendarTestUtils } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarTestUtils.jsm" +); +var { cancelItemDialog } = ChromeUtils.import( + "resource://testing-common/calendar/ItemEditingHelpers.jsm" +); + +const DEFVALUE = 43; + +add_task(async function testDefaultAlarms() { + let calendar = CalendarTestUtils.createCalendar("Mochitest", "memory"); + calendar.setProperty("calendar-main-default", true); + registerCleanupFunction(async () => { + CalendarTestUtils.removeCalendar(calendar); + }); + + let localeUnitString = cal.l10n.getCalString("unitDays"); + let unitString = PluralForm.get(DEFVALUE, localeUnitString).replace("#1", DEFVALUE); + let alarmString = (...args) => cal.l10n.getString("calendar-alarms", ...args); + let originStringEvent = alarmString("reminderCustomOriginBeginBeforeEvent"); + let originStringTask = alarmString("reminderCustomOriginBeginBeforeTask"); + let expectedEventReminder = alarmString("reminderCustomTitle", [unitString, originStringEvent]); + let expectedTaskReminder = alarmString("reminderCustomTitle", [unitString, originStringTask]); + + // Configure the preferences. + let { prefsWindow, prefsDocument } = await openNewPrefsTab("paneCalendar", "defaultsnoozelength"); + await handlePrefTab(prefsWindow, prefsDocument); + + // Create New Event. + await CalendarTestUtils.openCalendarTab(window); + + let { dialogWindow, iframeWindow, iframeDocument } = await CalendarTestUtils.editNewEvent(window); + + Assert.equal(iframeDocument.querySelector(".item-alarm").value, "custom"); + let reminderDetails = iframeDocument.querySelector(".reminder-single-alarms-label"); + Assert.equal(reminderDetails.value, expectedEventReminder); + + let reminderDialogPromise = BrowserTestUtils.promiseAlertDialog( + null, + "chrome://calendar/content/calendar-event-dialog-reminder.xhtml", + { callback: handleReminderDialog } + ); + EventUtils.synthesizeMouseAtCenter(reminderDetails, {}, iframeWindow); + await reminderDialogPromise; + + let promptPromise = BrowserTestUtils.promiseAlertDialog("extra1"); + cancelItemDialog(dialogWindow); + await promptPromise; + + // Create New Task. + await openTasksTab(); + ({ dialogWindow, iframeWindow, iframeDocument } = await CalendarTestUtils.editNewTask(window)); + + Assert.equal(iframeDocument.querySelector(".item-alarm").value, "custom"); + reminderDetails = iframeDocument.querySelector(".reminder-single-alarms-label"); + Assert.equal(reminderDetails.value, expectedTaskReminder); + + reminderDialogPromise = BrowserTestUtils.promiseAlertDialog( + null, + "chrome://calendar/content/calendar-event-dialog-reminder.xhtml", + { callback: handleReminderDialog } + ); + EventUtils.synthesizeMouseAtCenter(reminderDetails, {}, iframeWindow); + await reminderDialogPromise; + + promptPromise = BrowserTestUtils.promiseAlertDialog("extra1"); + cancelItemDialog(dialogWindow); + await promptPromise; +}); + +async function handlePrefTab(prefsWindow, prefsDocument) { + function menuList(id, value) { + let list = prefsDocument.getElementById(id); + list.scrollIntoView(); + list.click(); + list.querySelector(`menuitem[value="${value}"]`).click(); + } + // Turn on alarms for events and tasks. + menuList("eventdefalarm", "1"); + menuList("tododefalarm", "1"); + + // Selects "days" as a unit. + menuList("tododefalarmunit", "days"); + menuList("eventdefalarmunit", "days"); + + function text(id, value) { + let input = prefsDocument.getElementById(id); + input.scrollIntoView(); + EventUtils.synthesizeMouse(input, 5, 5, {}, prefsWindow); + Assert.equal(prefsDocument.activeElement, input); + EventUtils.synthesizeKey("a", { accelKey: true }, prefsWindow); + EventUtils.sendString(value, prefsWindow); + } + // Sets default alarm length for events to DEFVALUE. + text("eventdefalarmlen", DEFVALUE.toString()); + text("tododefalarmlen", DEFVALUE.toString()); + + Assert.equal(Services.prefs.getIntPref("calendar.alarms.onforevents"), 1); + Assert.equal(Services.prefs.getIntPref("calendar.alarms.eventalarmlen"), DEFVALUE); + Assert.equal(Services.prefs.getStringPref("calendar.alarms.eventalarmunit"), "days"); + Assert.equal(Services.prefs.getIntPref("calendar.alarms.onfortodos"), 1); + Assert.equal(Services.prefs.getIntPref("calendar.alarms.todoalarmlen"), DEFVALUE); + Assert.equal(Services.prefs.getStringPref("calendar.alarms.todoalarmunit"), "days"); +} + +async function handleReminderDialog(remindersWindow) { + await new Promise(remindersWindow.setTimeout); + let remindersDocument = remindersWindow.document; + + let listbox = remindersDocument.getElementById("reminder-listbox"); + Assert.equal(listbox.selectedCount, 1); + Assert.equal(listbox.selectedItem.reminder.offset.days, DEFVALUE); + + EventUtils.synthesizeMouseAtCenter( + remindersDocument.getElementById("reminder-new-button"), + {}, + remindersWindow + ); + Assert.equal(listbox.itemCount, 2); + Assert.equal(listbox.selectedCount, 1); + Assert.equal(listbox.selectedItem.reminder.offset.days, DEFVALUE); + + function text(id, value) { + let input = remindersDocument.getElementById(id); + EventUtils.synthesizeMouse(input, 5, 5, {}, remindersWindow); + Assert.equal(remindersDocument.activeElement, input); + EventUtils.synthesizeKey("a", { accelKey: true }, remindersWindow); + EventUtils.sendString(value, remindersWindow); + } + text("reminder-length", "20"); + Assert.equal(listbox.selectedItem.reminder.offset.days, 20); + + EventUtils.synthesizeMouseAtCenter(listbox, {}, remindersWindow); + EventUtils.synthesizeKey("VK_UP", {}, remindersWindow); + Assert.equal(listbox.selectedIndex, 0); + + Assert.equal(listbox.selectedItem.reminder.offset.days, DEFVALUE); + + remindersDocument.querySelector("dialog").getButton("accept").click(); +} + +async function openTasksTab() { + let tabmail = document.getElementById("tabmail"); + let tasksMode = tabmail.tabModes.tasks; + + if (tasksMode.tabs.length == 1) { + tabmail.selectedTab = tasksMode.tabs[0]; + } else { + let tasksTabButton = document.getElementById("tasksButton"); + EventUtils.synthesizeMouseAtCenter(tasksTabButton, { clickCount: 1 }); + } + + is(tasksMode.tabs.length, 1, "tasks tab is open"); + is(tabmail.selectedTab, tasksMode.tabs[0], "tasks tab is selected"); + + await new Promise(resolve => setTimeout(resolve)); +} + +registerCleanupFunction(function () { + Services.prefs.clearUserPref("calendar.alarms.onforevents"); + Services.prefs.clearUserPref("calendar.alarms.eventalarmlen"); + Services.prefs.clearUserPref("calendar.alarms.eventalarmunit"); + Services.prefs.clearUserPref("calendar.alarms.onfortodos"); + Services.prefs.clearUserPref("calendar.alarms.todoalarmlen"); + Services.prefs.clearUserPref("calendar.alarms.todoalarmunit"); +}); diff --git a/comm/calendar/test/browser/preferences/browser_categoryColors.js b/comm/calendar/test/browser/preferences/browser_categoryColors.js new file mode 100644 index 0000000000..29ad21ce13 --- /dev/null +++ b/comm/calendar/test/browser/preferences/browser_categoryColors.js @@ -0,0 +1,90 @@ +/* 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 { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); + +var { CalendarTestUtils } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarTestUtils.jsm" +); + +add_task(async function testCategoryColors() { + let calendar = CalendarTestUtils.createCalendar("Mochitest", "memory"); + + registerCleanupFunction(async () => { + CalendarTestUtils.removeCalendar(calendar); + }); + + let { prefsWindow, prefsDocument } = await openNewPrefsTab("paneCalendar", "categorieslist"); + + let listBox = prefsDocument.getElementById("categorieslist"); + Assert.equal(listBox.itemChildren.length, 22); + + for (let item of listBox.itemChildren) { + info(`${item.firstElementChild.value}: ${item.lastElementChild.style.backgroundColor}`); + Assert.ok(item.lastElementChild.style.backgroundColor); + } + + // Edit the name and colour of a built-in category. + + let subDialogPromise = BrowserTestUtils.waitForEvent( + prefsWindow.gSubDialog._dialogStack, + "dialogopen" + ); + + EventUtils.synthesizeMouse(listBox, 5, 5, {}, prefsWindow); + Assert.equal(listBox.selectedIndex, 0); + EventUtils.synthesizeMouseAtCenter(prefsDocument.getElementById("editCButton"), {}, prefsWindow); + + await subDialogPromise; + + let subDialogBrowser = prefsWindow.gSubDialog._topDialog._frame; + let subDialogDocument = subDialogBrowser.contentDocument; + subDialogDocument.getElementById("categoryName").value = "ZZZ Mochitest"; + subDialogDocument.getElementById("categoryColor").value = "#00CC00"; + subDialogDocument.body.firstElementChild.getButton("accept").click(); + + let listItem = listBox.itemChildren[listBox.itemCount - 1]; + Assert.equal(listBox.selectedItem, listItem); + Assert.equal(listItem.firstElementChild.value, "ZZZ Mochitest"); + Assert.equal(listItem.lastElementChild.style.backgroundColor, "rgb(0, 204, 0)"); + Assert.equal(Services.prefs.getCharPref("calendar.category.color.zzz_mochitest"), "#00cc00"); + + // Remove the colour of a built-in category. + + subDialogPromise = BrowserTestUtils.waitForEvent( + prefsWindow.gSubDialog._dialogStack, + "dialogopen" + ); + + EventUtils.synthesizeMouse(listBox, 5, 5, {}, prefsWindow); + EventUtils.synthesizeKey("VK_HOME", {}, prefsWindow); + Assert.equal(listBox.selectedIndex, 0); + let itemName = listBox.itemChildren[0].firstElementChild.value; + EventUtils.synthesizeMouseAtCenter(prefsDocument.getElementById("editCButton"), {}, prefsWindow); + + await subDialogPromise; + + subDialogBrowser = prefsWindow.gSubDialog._topDialog._frame; + await new Promise(subDialogBrowser.contentWindow.setTimeout); + subDialogDocument = subDialogBrowser.contentDocument; + subDialogDocument.getElementById("useColor").checked = false; + subDialogDocument.body.firstElementChild.getButton("accept").click(); + + listItem = listBox.itemChildren[0]; + Assert.equal(listBox.selectedItem, listItem); + Assert.equal(listItem.firstElementChild.value, itemName); + Assert.equal(listItem.lastElementChild.style.backgroundColor, ""); + Assert.equal(Services.prefs.getCharPref(`calendar.category.color.${itemName.toLowerCase()}`), ""); + + // Remove the added category. + + EventUtils.synthesizeMouse(listBox, 5, 5, {}, prefsWindow); + EventUtils.synthesizeKey("VK_END", {}, prefsWindow); + Assert.equal(listBox.selectedIndex, listBox.itemCount - 1); + EventUtils.synthesizeMouseAtCenter( + prefsDocument.getElementById("deleteCButton"), + {}, + prefsWindow + ); +}); diff --git a/comm/calendar/test/browser/preferences/head.js b/comm/calendar/test/browser/preferences/head.js new file mode 100644 index 0000000000..8d6d5d3ab5 --- /dev/null +++ b/comm/calendar/test/browser/preferences/head.js @@ -0,0 +1,64 @@ +/* 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 openPreferencesTab */ + +async function openNewPrefsTab(paneID, scrollPaneTo, otherArgs) { + let tabmail = document.getElementById("tabmail"); + let prefsTabMode = tabmail.tabModes.preferencesTab; + + Assert.equal(prefsTabMode.tabs.length, 0, "Prefs tab is not open"); + + let prefsDocument = await new Promise(resolve => { + Services.obs.addObserver(function documentLoaded(subject) { + if (subject.URL.startsWith("about:preferences")) { + Services.obs.removeObserver(documentLoaded, "chrome-document-loaded"); + resolve(subject); + } + }, "chrome-document-loaded"); + openPreferencesTab(paneID, scrollPaneTo, otherArgs); + }); + Assert.ok(prefsDocument.URL.startsWith("about:preferences"), "Prefs tab is open"); + + prefsDocument = prefsTabMode.tabs[0].browser.contentDocument; + let prefsWindow = prefsDocument.ownerGlobal; + prefsWindow.resizeTo(screen.availWidth, screen.availHeight); + if (paneID) { + await new Promise(resolve => prefsWindow.setTimeout(resolve)); + Assert.equal(prefsWindow.gLastCategory.category, paneID, `Selected pane is ${paneID}`); + } else { + // If we don't wait here for other scripts to run, they + // could be in a bad state if our test closes the tab. + await new Promise(resolve => prefsWindow.setTimeout(resolve)); + } + + registerCleanupOnce(); + + await new Promise(resolve => prefsWindow.setTimeout(resolve)); + if (scrollPaneTo) { + Assert.greater( + prefsDocument.getElementById("preferencesContainer").scrollTop, + 0, + "Prefs page did scroll when it was supposed to" + ); + } + return { prefsDocument, prefsWindow }; +} + +function registerCleanupOnce() { + if (registerCleanupOnce.alreadyRegistered) { + return; + } + registerCleanupFunction(closePrefsTab); + registerCleanupOnce.alreadyRegistered = true; +} + +async function closePrefsTab() { + info("Closing prefs tab"); + let tabmail = document.getElementById("tabmail"); + let prefsTab = tabmail.tabModes.preferencesTab.tabs[0]; + if (prefsTab) { + tabmail.closeTab(prefsTab); + } +} diff --git a/comm/calendar/test/browser/providers/browser.ini b/comm/calendar/test/browser/providers/browser.ini new file mode 100644 index 0000000000..84d6133696 --- /dev/null +++ b/comm/calendar/test/browser/providers/browser.ini @@ -0,0 +1,21 @@ +[default] +head = head.js +prefs = + calendar.item.promptDelete=false + calendar.debug.log=true + calendar.debug.log.verbose=true + 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 + +[browser_caldavCalendar_cached.js] +[browser_caldavCalendar_uncached.js] +[browser_icsCalendar_cached.js] +[browser_icsCalendar_uncached.js] +[browser_storageCalendar.js] diff --git a/comm/calendar/test/browser/providers/browser_caldavCalendar_cached.js b/comm/calendar/test/browser/providers/browser_caldavCalendar_cached.js new file mode 100644 index 0000000000..5b725e4d54 --- /dev/null +++ b/comm/calendar/test/browser/providers/browser_caldavCalendar_cached.js @@ -0,0 +1,64 @@ +/* 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 { CalDAVServer } = ChromeUtils.import("resource://testing-common/calendar/CalDAVServer.jsm"); + +CalDAVServer.open("bob", "bob"); +if (!Services.logins.findLogins(CalDAVServer.origin, null, "test").length) { + // Save a username and password to the login manager. + let loginInfo = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo); + loginInfo.init(CalDAVServer.origin, null, "test", "bob", "bob", "", ""); + Services.logins.addLogin(loginInfo); +} + +let calendar; +add_setup(async function () { + calendarObserver._onLoadPromise = PromiseUtils.defer(); + calendar = createCalendar("caldav", CalDAVServer.url, true); + await calendarObserver._onLoadPromise.promise; + info("calendar set-up complete"); + + registerCleanupFunction(async () => { + // This test has issues cleaning up, and it breaks all the subsequent tests. + await new Promise(r => setTimeout(r, 1000)); // eslint-disable-line mozilla/no-arbitrary-setTimeout + await CalDAVServer.close(); + Services.logins.removeAllLogins(); + removeCalendar(calendar); + }); +}); + +async function promiseIdle() { + await TestUtils.waitForCondition(() => !calendar.wrappedJSObject.mPendingSync); + await fetch(`${CalDAVServer.origin}/ping`); +} + +add_task(async function testAlarms() { + calendarObserver._batchRequired = true; + await runTestAlarms(calendar); + + // Be sure the calendar has finished deleting the event. + await promiseIdle(); +}); + +add_task(async function testSyncChanges() { + await syncChangesTest.setUp(); + + await CalDAVServer.putItemInternal( + "ad0850e5-8020-4599-86a4-86c90af4e2cd.ics", + syncChangesTest.part1Item + ); + await syncChangesTest.runPart1(); + + await CalDAVServer.putItemInternal( + "ad0850e5-8020-4599-86a4-86c90af4e2cd.ics", + syncChangesTest.part2Item + ); + await syncChangesTest.runPart2(); + + CalDAVServer.deleteItemInternal("ad0850e5-8020-4599-86a4-86c90af4e2cd.ics"); + await syncChangesTest.runPart3(); + + // Be sure the calendar has finished all requests. + await promiseIdle(); +}); diff --git a/comm/calendar/test/browser/providers/browser_caldavCalendar_uncached.js b/comm/calendar/test/browser/providers/browser_caldavCalendar_uncached.js new file mode 100644 index 0000000000..7489ae4e09 --- /dev/null +++ b/comm/calendar/test/browser/providers/browser_caldavCalendar_uncached.js @@ -0,0 +1,61 @@ +/* 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 { CalDAVServer } = ChromeUtils.import("resource://testing-common/calendar/CalDAVServer.jsm"); + +CalDAVServer.open("bob", "bob"); +if (!Services.logins.findLogins(CalDAVServer.origin, null, "test").length) { + // Save a username and password to the login manager. + let loginInfo = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo); + loginInfo.init(CalDAVServer.origin, null, "test", "bob", "bob", "", ""); + Services.logins.addLogin(loginInfo); +} + +let calendar; +add_setup(async function () { + calendarObserver._onLoadPromise = PromiseUtils.defer(); + calendar = createCalendar("caldav", CalDAVServer.url, false); + await calendarObserver._onLoadPromise.promise; + info("calendar set-up complete"); + + registerCleanupFunction(async () => { + await CalDAVServer.close(); + Services.logins.removeAllLogins(); + removeCalendar(calendar); + }); +}); + +async function promiseIdle() { + await fetch(`${CalDAVServer.origin}/ping`); +} + +add_task(async function testAlarms() { + calendarObserver._batchRequired = true; + await runTestAlarms(calendar); + + // Be sure the calendar has finished deleting the event. + await promiseIdle(); +}); + +add_task(async function testSyncChanges() { + await syncChangesTest.setUp(); + + await CalDAVServer.putItemInternal( + "ad0850e5-8020-4599-86a4-86c90af4e2cd.ics", + syncChangesTest.part1Item + ); + await syncChangesTest.runPart1(); + + await CalDAVServer.putItemInternal( + "ad0850e5-8020-4599-86a4-86c90af4e2cd.ics", + syncChangesTest.part2Item + ); + await syncChangesTest.runPart2(); + + CalDAVServer.deleteItemInternal("ad0850e5-8020-4599-86a4-86c90af4e2cd.ics"); + await syncChangesTest.runPart3(); + + // Be sure the calendar has finished all requests. + await promiseIdle(); +}); diff --git a/comm/calendar/test/browser/providers/browser_icsCalendar_cached.js b/comm/calendar/test/browser/providers/browser_icsCalendar_cached.js new file mode 100644 index 0000000000..ba788be5b9 --- /dev/null +++ b/comm/calendar/test/browser/providers/browser_icsCalendar_cached.js @@ -0,0 +1,73 @@ +/* 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 { ICSServer } = ChromeUtils.import("resource://testing-common/calendar/ICSServer.jsm"); + +ICSServer.open("bob", "bob"); +if (!Services.logins.findLogins(ICSServer.origin, null, "test").length) { + // Save a username and password to the login manager. + let loginInfo = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo); + loginInfo.init(ICSServer.origin, null, "test", "bob", "bob", "", ""); + Services.logins.addLogin(loginInfo); +} + +let calendar; +add_setup(async function () { + // TODO: item notifications from a cached ICS calendar occur outside of batches. + // This isn't fatal but it shouldn't happen. Side-effects include alarms firing + // twice - once from onAddItem then again at onLoad. + // + // Remove the next line when this is fixed. + calendarObserver._batchRequired = false; + + calendarObserver._onLoadPromise = PromiseUtils.defer(); + calendar = createCalendar("ics", ICSServer.url, true); + await calendarObserver._onLoadPromise.promise; + info("calendar set-up complete"); + + registerCleanupFunction(async () => { + await ICSServer.close(); + Services.logins.removeAllLogins(); + removeCalendar(calendar); + }); +}); + +async function promiseIdle() { + await TestUtils.waitForCondition( + () => + calendar.wrappedJSObject.mUncachedCalendar.wrappedJSObject._queue.length == 0 && + calendar.wrappedJSObject.mUncachedCalendar.wrappedJSObject._isLocked === false + ); + await fetch(`${ICSServer.origin}/ping`); +} + +add_task(async function testAlarms() { + // Remove the next line when fixed. + calendarObserver._batchRequired = false; + await runTestAlarms(calendar); + + // Be sure the calendar has finished deleting the event. + await promiseIdle(); +}).skip(); // Broken. + +add_task(async function testSyncChanges() { + await syncChangesTest.setUp(); + + await ICSServer.putICSInternal(syncChangesTest.part1Item); + await syncChangesTest.runPart1(); + + await ICSServer.putICSInternal(syncChangesTest.part2Item); + await syncChangesTest.runPart2(); + + await ICSServer.putICSInternal( + CalendarTestUtils.dedent` + BEGIN:VCALENDAR + END:VCALENDAR + ` + ); + await syncChangesTest.runPart3(); + + // Be sure the calendar has finished deleting the event. + await promiseIdle(); +}); diff --git a/comm/calendar/test/browser/providers/browser_icsCalendar_uncached.js b/comm/calendar/test/browser/providers/browser_icsCalendar_uncached.js new file mode 100644 index 0000000000..ef25408dce --- /dev/null +++ b/comm/calendar/test/browser/providers/browser_icsCalendar_uncached.js @@ -0,0 +1,64 @@ +/* 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 { ICSServer } = ChromeUtils.import("resource://testing-common/calendar/ICSServer.jsm"); + +ICSServer.open("bob", "bob"); +if (!Services.logins.findLogins(ICSServer.origin, null, "test").length) { + // Save a username and password to the login manager. + let loginInfo = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo); + loginInfo.init(ICSServer.origin, null, "test", "bob", "bob", "", ""); + Services.logins.addLogin(loginInfo); +} + +let calendar; +add_setup(async function () { + calendarObserver._onLoadPromise = PromiseUtils.defer(); + calendar = createCalendar("ics", ICSServer.url, false); + await calendarObserver._onLoadPromise.promise; + info("calendar set-up complete"); + + registerCleanupFunction(async () => { + await ICSServer.close(); + Services.logins.removeAllLogins(); + removeCalendar(calendar); + }); +}); + +async function promiseIdle() { + await TestUtils.waitForCondition( + () => + calendar.wrappedJSObject._queue.length == 0 && calendar.wrappedJSObject._isLocked === false + ); + await fetch(`${ICSServer.origin}/ping`); +} + +add_task(async function testAlarms() { + calendarObserver._batchRequired = true; + await runTestAlarms(calendar); + + // Be sure the calendar has finished deleting the event. + await promiseIdle(); +}); + +add_task(async function testSyncChanges() { + await syncChangesTest.setUp(); + + await ICSServer.putICSInternal(syncChangesTest.part1Item); + await syncChangesTest.runPart1(); + + await ICSServer.putICSInternal(syncChangesTest.part2Item); + await syncChangesTest.runPart2(); + + await ICSServer.putICSInternal( + CalendarTestUtils.dedent` + BEGIN:VCALENDAR + END:VCALENDAR + ` + ); + await syncChangesTest.runPart3(); + + // Be sure the calendar has finished all requests. + await promiseIdle(); +}); diff --git a/comm/calendar/test/browser/providers/browser_storageCalendar.js b/comm/calendar/test/browser/providers/browser_storageCalendar.js new file mode 100644 index 0000000000..1a9eb6a30c --- /dev/null +++ b/comm/calendar/test/browser/providers/browser_storageCalendar.js @@ -0,0 +1,13 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +let calendar = createCalendar("storage", "moz-storage-calendar://"); +registerCleanupFunction(() => { + removeCalendar(calendar); +}); + +add_task(function testAlarms() { + calendarObserver._batchRequired = false; + return runTestAlarms(calendar); +}); diff --git a/comm/calendar/test/browser/providers/head.js b/comm/calendar/test/browser/providers/head.js new file mode 100644 index 0000000000..bf58302131 --- /dev/null +++ b/comm/calendar/test/browser/providers/head.js @@ -0,0 +1,402 @@ +/* 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/. */ + +SimpleTest.requestCompleteLog(); + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); +var { CalendarTestUtils } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarTestUtils.jsm" +); +var { handleDeleteOccurrencePrompt } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarUtils.jsm" +); + +var { saveAndCloseItemDialog, setData } = ChromeUtils.import( + "resource://testing-common/calendar/ItemEditingHelpers.jsm" +); + +let calendarObserver = { + QueryInterface: ChromeUtils.generateQI(["calIObserver"]), + + /* calIObserver */ + + _batchCount: 0, + _batchRequired: true, + onStartBatch(calendar) { + info(`onStartBatch ${calendar?.id} ${++this._batchCount}`); + Assert.equal( + calendar, + this._expectedCalendar, + "onStartBatch should occur on the expected calendar" + ); + }, + onEndBatch(calendar) { + info(`onEndBatch ${calendar?.id} ${this._batchCount--}`); + Assert.equal( + calendar, + this._expectedCalendar, + "onEndBatch should occur on the expected calendar" + ); + }, + onLoad(calendar) { + info(`onLoad ${calendar.id}`); + Assert.equal(calendar, this._expectedCalendar, "onLoad should occur on the expected calendar"); + if (this._onLoadPromise) { + this._onLoadPromise.resolve(); + } + }, + onAddItem(item) { + info(`onAddItem ${item.calendar.id} ${item.id}`); + if (this._batchRequired) { + Assert.equal(this._batchCount, 1, "onAddItem must occur in a batch"); + } + }, + onModifyItem(newItem, oldItem) { + info(`onModifyItem ${newItem.calendar.id} ${newItem.id}`); + if (this._batchRequired) { + Assert.equal(this._batchCount, 1, "onModifyItem must occur in a batch"); + } + }, + onDeleteItem(deletedItem) { + info(`onDeleteItem ${deletedItem.calendar.id} ${deletedItem.id}`); + }, + onError(calendar, errNo, message) {}, + onPropertyChanged(calendar, name, value, oldValue) {}, + onPropertyDeleting(calendar, name) {}, +}; + +/** + * Create and register a calendar. + * + * @param {string} type - The calendar provider to use. + * @param {string} url - URL of the server. + * @param {boolean} useCache - Should this calendar have offline storage? + * @returns {calICalendar} + */ +function createCalendar(type, url, useCache) { + let calendar = cal.manager.createCalendar(type, Services.io.newURI(url)); + calendar.name = type + (useCache ? " with cache" : " without cache"); + calendar.id = cal.getUUID(); + calendar.setProperty("cache.enabled", useCache); + calendar.setProperty("calendar-main-default", true); + + cal.manager.registerCalendar(calendar); + calendar = cal.manager.getCalendarById(calendar.id); + calendarObserver._expectedCalendar = calendar; + calendar.addObserver(calendarObserver); + + info(`Created calendar ${calendar.id}`); + return calendar; +} + +/** + * Unregister a calendar. + * + * @param {calICalendar} calendar + */ +function removeCalendar(calendar) { + calendar.removeObserver(calendarObserver); + cal.manager.removeCalendar(calendar); +} + +let alarmService = Cc["@mozilla.org/calendar/alarm-service;1"].getService(Ci.calIAlarmService); + +let alarmObserver = { + QueryInterface: ChromeUtils.generateQI(["calIAlarmServiceObserver"]), + + /* calIAlarmServiceObserver */ + + _alarmCount: 0, + onAlarm(item, alarm) { + info("onAlarm"); + this._alarmCount++; + }, + onRemoveAlarmsByItem(item) {}, + onRemoveAlarmsByCalendar(calendar) {}, + onAlarmsLoaded(calendar) {}, +}; +alarmService.addObserver(alarmObserver); +registerCleanupFunction(async () => { + alarmService.removeObserver(alarmObserver); +}); + +/** + * Tests the creation, firing, dismissal, modification and deletion of an event with an alarm. + * Also checks that the number of events in the unifinder is correct at each stage. + * + * Passing this test requires the active calendar to fire notifications in the correct sequence. + */ +async function runTestAlarms() { + let today = cal.dtz.now(); + let start = today.clone(); + start.day++; + start.hour = start.minute = start.second = 0; + let end = start.clone(); + end.hour++; + let repeatUntil = start.clone(); + repeatUntil.day += 15; + + await CalendarTestUtils.setCalendarView(window, "multiweek"); + await CalendarTestUtils.goToToday(window); + Assert.equal(window.unifinderTreeView.rowCount, 0, "unifinder event count"); + + alarmObserver._alarmCount = 0; + + let alarmDialogPromise = BrowserTestUtils.promiseAlertDialog( + undefined, + "chrome://calendar/content/calendar-alarm-dialog.xhtml", + { + async callback(alarmWindow) { + info("Alarm dialog opened"); + let alarmDocument = alarmWindow.document; + + let list = alarmDocument.getElementById("alarm-richlist"); + let items = list.querySelectorAll(`richlistitem[is="calendar-alarm-widget-richlistitem"]`); + await TestUtils.waitForCondition(() => items.length); + Assert.equal(items.length, 1); + + await new Promise(resolve => alarmWindow.setTimeout(resolve, 500)); + + let dismissButton = alarmDocument.querySelector("#alarm-dismiss-all-button"); + EventUtils.synthesizeMouseAtCenter(dismissButton, {}, alarmWindow); + }, + } + ); + let { dialogWindow, iframeWindow } = await CalendarTestUtils.editNewEvent(window); + await setData(dialogWindow, iframeWindow, { + title: "test event", + startdate: start, + starttime: start, + enddate: end, + endtime: end, + reminder: "2days", + repeat: "weekly", + }); + + await saveAndCloseItemDialog(dialogWindow); + await alarmDialogPromise; + info("Alarm dialog closed"); + + await new Promise(r => setTimeout(r, 2000)); + Assert.equal(window.unifinderTreeView.rowCount, 1, "there should be one event in the unifinder"); + + Assert.equal( + [...Services.wm.getEnumerator("Calendar:AlarmWindow")].length, + 0, + "alarm dialog did not reappear" + ); + Assert.equal(alarmObserver._alarmCount, 1, "only one alarm"); + alarmObserver._alarmCount = 0; + + let eventBox = await CalendarTestUtils.multiweekView.waitForItemAt( + window, + start.weekday == 0 ? 2 : 1, // Sunday's event is next week. + start.weekday + 1, + 1 + ); + Assert.ok(!!eventBox.item.parentItem.alarmLastAck); + + ({ dialogWindow, iframeWindow } = await CalendarTestUtils.editItemOccurrences(window, eventBox)); + await setData(dialogWindow, iframeWindow, { + title: "modified test event", + repeat: "weekly", + repeatuntil: repeatUntil, + }); + + await saveAndCloseItemDialog(dialogWindow); + + Assert.equal(window.unifinderTreeView.rowCount, 1, "there should be one event in the unifinder"); + + Services.focus.focusedWindow = window; + + await new Promise(resolve => setTimeout(resolve, 2000)); + Assert.equal( + [...Services.wm.getEnumerator("Calendar:AlarmWindow")].length, + 0, + "alarm dialog should not reappear" + ); + Assert.equal(alarmObserver._alarmCount, 0, "there should not be any remaining alarms"); + alarmObserver._alarmCount = 0; + + eventBox = await CalendarTestUtils.multiweekView.waitForItemAt( + window, + start.weekday == 0 ? 2 : 1, // Sunday's event is next week. + start.weekday + 1, + 1 + ); + Assert.ok(!!eventBox.item.parentItem.alarmLastAck); + + EventUtils.synthesizeMouseAtCenter(eventBox, {}, window); + eventBox.focus(); + window.calendarController.onSelectionChanged({ detail: window.currentView().getSelectedItems() }); + await handleDeleteOccurrencePrompt(window, window.currentView(), true); + + await CalendarTestUtils.multiweekView.waitForNoItemAt( + window, + start.weekday == 0 ? 2 : 1, // Sunday's event is next week. + start.weekday + 1, + 1 + ); + Assert.equal(window.unifinderTreeView.rowCount, 0, "there should be no events in the unifinder"); +} + +const syncItem1Name = "holy cow, a new item!"; +const syncItem2Name = "a changed item"; + +let syncChangesTest = { + async setUp() { + await CalendarTestUtils.openCalendarTab(window); + + if (document.getElementById("today-pane-panel").collapsed) { + EventUtils.synthesizeMouseAtCenter( + document.getElementById("calendar-status-todaypane-button"), + {} + ); + } + + if (document.getElementById("agenda-panel").collapsed) { + EventUtils.synthesizeMouseAtCenter(document.getElementById("today-pane-cycler-next"), {}); + } + }, + + get part1Item() { + let today = cal.dtz.now(); + let start = today.clone(); + start.day += 9 - start.weekday; + start.hour = 13; + start.minute = start.second = 0; + let end = start.clone(); + end.hour++; + + return CalendarTestUtils.dedent` + BEGIN:VCALENDAR + BEGIN:VEVENT + UID:ad0850e5-8020-4599-86a4-86c90af4e2cd + SUMMARY:${syncItem1Name} + DTSTART:${start.icalString} + DTEND:${end.icalString} + END:VEVENT + END:VCALENDAR + `; + }, + + async runPart1() { + await CalendarTestUtils.setCalendarView(window, "multiweek"); + await CalendarTestUtils.goToToday(window); + + // Sanity check that we have not already synchronized and that there is no + // existing item. + Assert.ok( + !CalendarTestUtils.multiweekView.getItemAt(window, 2, 3, 1), + "there should be no existing item in the calendar" + ); + + // Synchronize. + EventUtils.synthesizeMouseAtCenter(document.getElementById("refreshCalendar"), {}); + + // Verify that the item we added appears in the calendar view. + let item = await CalendarTestUtils.multiweekView.waitForItemAt(window, 2, 3, 1); + Assert.equal(item.item.title, syncItem1Name, "view should include newly-added item"); + + // Verify that the today pane updates and shows the item we added. + await TestUtils.waitForCondition(() => window.TodayPane.agenda.rowCount == 1); + Assert.equal( + getTodayPaneItemTitle(0), + syncItem1Name, + "today pane should include newly-added item" + ); + Assert.ok( + !window.TodayPane.agenda.rows[0].nextElementSibling, + "there should be no additional items in the today pane" + ); + }, + + get part2Item() { + let today = cal.dtz.now(); + let start = today.clone(); + start.day += 10 - start.weekday; + start.hour = 9; + start.minute = start.second = 0; + let end = start.clone(); + end.hour++; + + return CalendarTestUtils.dedent` + BEGIN:VCALENDAR + BEGIN:VEVENT + UID:ad0850e5-8020-4599-86a4-86c90af4e2cd + SUMMARY:${syncItem2Name} + DTSTART:${start.icalString} + DTEND:${end.icalString} + END:VEVENT + END:VCALENDAR + `; + }, + + async runPart2() { + // Sanity check that we have not already synchronized and that there is no + // existing item. + Assert.ok( + !CalendarTestUtils.multiweekView.getItemAt(window, 2, 4, 1), + "there should be no existing item on the specified day" + ); + + // Synchronize. + EventUtils.synthesizeMouseAtCenter(document.getElementById("refreshCalendar"), {}); + + // Verify that the item has updated in the calendar view. + await CalendarTestUtils.multiweekView.waitForNoItemAt(window, 2, 3, 1); + let item = await CalendarTestUtils.multiweekView.waitForItemAt(window, 2, 4, 1); + Assert.equal(item.item.title, syncItem2Name, "view should show updated item"); + + // Verify that the today pane updates and shows the updated item. + await TestUtils.waitForCondition( + () => window.TodayPane.agenda.rowCount == 1 && getTodayPaneItemTitle(0) != syncItem1Name + ); + Assert.equal(getTodayPaneItemTitle(0), syncItem2Name, "today pane should show updated item"); + Assert.ok( + !window.TodayPane.agenda.rows[0].nextElementSibling, + "there should be no additional items in the today pane" + ); + }, + + async runPart3() { + // Synchronize via the calendar context menu. + await calendarListContextMenu( + document.querySelector("#calendar-list > li:nth-child(2)"), + "list-calendar-context-reload" + ); + + // Verify that the item is removed from the calendar view. + await CalendarTestUtils.multiweekView.waitForNoItemAt(window, 2, 3, 1); + await CalendarTestUtils.multiweekView.waitForNoItemAt(window, 2, 4, 1); + + // Verify that the item is removed from the today pane. + await TestUtils.waitForCondition(() => window.TodayPane.agenda.rowCount == 0); + }, +}; + +function getTodayPaneItemTitle(idx) { + const row = window.TodayPane.agenda.rows[idx]; + return row.querySelector(".agenda-listitem-title").textContent; +} + +async function calendarListContextMenu(target, menuItem) { + await new Promise(r => setTimeout(r)); + window.focus(); + await TestUtils.waitForCondition( + () => Services.focus.focusedWindow == window, + "waiting for window to be focused" + ); + + let contextMenu = document.getElementById("list-calendars-context-menu"); + let shownPromise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown"); + EventUtils.synthesizeMouseAtCenter(target, { type: "contextmenu" }); + await shownPromise; + + if (menuItem) { + let hiddenPromise = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden"); + contextMenu.activateItem(document.getElementById(menuItem)); + await hiddenPromise; + } +} diff --git a/comm/calendar/test/browser/recurrence/browser.ini b/comm/calendar/test/browser/recurrence/browser.ini new file mode 100644 index 0000000000..633ed27dde --- /dev/null +++ b/comm/calendar/test/browser/recurrence/browser.ini @@ -0,0 +1,23 @@ +[default] +dupe-manifest = +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 +tags = recurrence + +[browser_annual.js] +[browser_biweekly.js] +[browser_daily.js] +[browser_lastDayOfMonth.js] +[browser_recurrenceNavigation.js] +[browser_weeklyN.js] +[browser_weeklyUntil.js] +[browser_weeklyWithException.js] diff --git a/comm/calendar/test/browser/recurrence/browser_annual.js b/comm/calendar/test/browser/recurrence/browser_annual.js new file mode 100644 index 0000000000..54c7198f05 --- /dev/null +++ b/comm/calendar/test/browser/recurrence/browser_annual.js @@ -0,0 +1,69 @@ +/* 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 { handleDeleteOccurrencePrompt } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarUtils.jsm" +); + +var { saveAndCloseItemDialog, setData } = ChromeUtils.import( + "resource://testing-common/calendar/ItemEditingHelpers.jsm" +); + +var { dayView, weekView, multiweekView, monthView } = CalendarTestUtils; + +const STARTYEAR = 1950; +const EPOCH = 1970; + +add_task(async function testAnnualRecurrence() { + let calendar = CalendarTestUtils.createCalendar(); + registerCleanupFunction(() => { + CalendarTestUtils.removeCalendar(calendar); + }); + + await CalendarTestUtils.setCalendarView(window, "day"); + await CalendarTestUtils.goToDate(window, STARTYEAR, 1, 1); + + // Create yearly recurring all-day event. + let eventBox = dayView.getAllDayHeader(window); + let { dialogWindow, iframeWindow } = await CalendarTestUtils.editNewEvent(window, eventBox); + await setData(dialogWindow, iframeWindow, { title: "Event", repeat: "yearly" }); + await saveAndCloseItemDialog(dialogWindow); + await TestUtils.waitForCondition( + () => CalendarTestUtils.dayView.getAllDayItemAt(window, 1), + "recurring all-day event created" + ); + + let checkYears = [STARTYEAR, STARTYEAR + 1, EPOCH - 1, EPOCH, EPOCH + 1]; + for (let year of checkYears) { + await CalendarTestUtils.goToDate(window, year, 1, 1); + let date = new Date(Date.UTC(year, 0, 1)); + let column = date.getUTCDay() + 1; + + // day view + await CalendarTestUtils.setCalendarView(window, "day"); + await dayView.waitForAllDayItemAt(window, 1); + + // week view + await CalendarTestUtils.setCalendarView(window, "week"); + await weekView.waitForAllDayItemAt(window, column, 1); + + // multiweek view + await CalendarTestUtils.setCalendarView(window, "multiweek"); + await multiweekView.waitForItemAt(window, 1, column, 1); + + // month view + await CalendarTestUtils.setCalendarView(window, "month"); + await monthView.waitForItemAt(window, 1, column, 1); + } + + // Delete event. + await CalendarTestUtils.goToDate(window, checkYears[0], 1, 1); + await CalendarTestUtils.setCalendarView(window, "day"); + const box = await dayView.waitForAllDayItemAt(window, 1); + EventUtils.synthesizeMouseAtCenter(box, {}, window); + await handleDeleteOccurrencePrompt(window, box, true); + await TestUtils.waitForCondition(() => !dayView.getAllDayItemAt(window, 1), "No all-day events"); + + Assert.ok(true, "Test ran to completion"); +}); diff --git a/comm/calendar/test/browser/recurrence/browser_biweekly.js b/comm/calendar/test/browser/recurrence/browser_biweekly.js new file mode 100644 index 0000000000..6889822375 --- /dev/null +++ b/comm/calendar/test/browser/recurrence/browser_biweekly.js @@ -0,0 +1,85 @@ +/* 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 { handleDeleteOccurrencePrompt } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarUtils.jsm" +); + +var { saveAndCloseItemDialog, setData } = ChromeUtils.import( + "resource://testing-common/calendar/ItemEditingHelpers.jsm" +); + +var { dayView, weekView, multiweekView, monthView } = CalendarTestUtils; + +const HOUR = 8; + +add_task(async function testBiweeklyRecurrence() { + let calendar = CalendarTestUtils.createCalendar(); + registerCleanupFunction(() => { + CalendarTestUtils.removeCalendar(calendar); + }); + + await CalendarTestUtils.setCalendarView(window, "day"); + await CalendarTestUtils.goToDate(window, 2009, 1, 31); + + // Create biweekly event. + let eventBox = dayView.getHourBoxAt(window, HOUR); + let { dialogWindow, iframeWindow } = await CalendarTestUtils.editNewEvent(window, eventBox); + await setData(dialogWindow, iframeWindow, { title: "Event", repeat: "bi.weekly" }); + await saveAndCloseItemDialog(dialogWindow); + + // Check day view. + await CalendarTestUtils.setCalendarView(window, "day"); + for (let i = 0; i < 4; i++) { + await dayView.waitForEventBoxAt(window, 1); + await CalendarTestUtils.calendarViewForward(window, 14); + } + + // Check week view. + await CalendarTestUtils.setCalendarView(window, "week"); + await CalendarTestUtils.goToDate(window, 2009, 1, 31); + + for (let i = 0; i < 4; i++) { + await weekView.waitForEventBoxAt(window, 7, 1); + await CalendarTestUtils.calendarViewForward(window, 2); + } + + // Check multiweek view. + await CalendarTestUtils.setCalendarView(window, "multiweek"); + await CalendarTestUtils.goToDate(window, 2009, 1, 31); + + // Always two occurrences in view, 1st and 3rd or 2nd and 4th week. + for (let i = 0; i < 5; i++) { + await multiweekView.waitForItemAt(window, (i % 2) + 1, 7, 1); + Assert.ok(multiweekView.getItemAt(window, (i % 2) + 3, 7, 1)); + await CalendarTestUtils.calendarViewForward(window, 1); + } + + // Check month view. + await CalendarTestUtils.setCalendarView(window, "month"); + await CalendarTestUtils.goToDate(window, 2009, 1, 31); + + // January + await monthView.waitForItemAt(window, 5, 7, 1); + await CalendarTestUtils.calendarViewForward(window, 1); + + // February + await monthView.waitForItemAt(window, 2, 7, 1); + Assert.ok(monthView.getItemAt(window, 4, 7, 1)); + await CalendarTestUtils.calendarViewForward(window, 1); + + // March + await monthView.waitForItemAt(window, 2, 7, 1); + + let box = monthView.getItemAt(window, 4, 7, 1); + Assert.ok(box); + + // Delete event. + EventUtils.synthesizeMouseAtCenter(box, {}, window); + await handleDeleteOccurrencePrompt(window, box, true); + + await monthView.waitForNoItemAt(window, 4, 7, 1); + + Assert.ok(true, "Test ran to completion"); +}); diff --git a/comm/calendar/test/browser/recurrence/browser_daily.js b/comm/calendar/test/browser/recurrence/browser_daily.js new file mode 100644 index 0000000000..42ffc4c8db --- /dev/null +++ b/comm/calendar/test/browser/recurrence/browser_daily.js @@ -0,0 +1,162 @@ +/* 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 { handleDeleteOccurrencePrompt } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarUtils.jsm" +); + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); +var { saveAndCloseItemDialog, setData } = ChromeUtils.import( + "resource://testing-common/calendar/ItemEditingHelpers.jsm" +); + +var { + calendarViewBackward, + calendarViewForward, + setCalendarView, + dayView, + weekView, + multiweekView, + monthView, +} = CalendarTestUtils; + +const HOUR = 8; +const TITLE = "Event"; + +add_task(async function testDailyRecurrence() { + let calendar = CalendarTestUtils.createCalendar(); + registerCleanupFunction(() => { + CalendarTestUtils.removeCalendar(calendar); + }); + + await setCalendarView(window, "day"); + await CalendarTestUtils.goToDate(window, 2009, 1, 1); + + // Create daily event. + let eventBox = dayView.getHourBoxAt(window, HOUR); + let { dialogWindow, iframeWindow } = await CalendarTestUtils.editNewEvent(window, eventBox); + await setData(dialogWindow, iframeWindow, { + title: TITLE, + repeat: "daily", + repeatuntil: cal.createDateTime("20090320T000000Z"), + }); + await saveAndCloseItemDialog(dialogWindow); + + // Check day view for 7 days. + for (let day = 1; day <= 7; day++) { + await dayView.waitForEventBoxAt(window, 1); + await calendarViewForward(window, 1); + } + + // Check week view for 2 weeks. + await setCalendarView(window, "week"); + await CalendarTestUtils.goToDate(window, 2009, 1, 1); + + for (let day = 5; day <= 7; day++) { + await weekView.waitForEventBoxAt(window, day, 1); + } + + await calendarViewForward(window, 1); + + for (let day = 1; day <= 7; day++) { + await weekView.waitForEventBoxAt(window, day, 1); + } + + // Check multiweek view for 4 weeks. + await setCalendarView(window, "multiweek"); + await CalendarTestUtils.goToDate(window, 2009, 1, 1); + + for (let day = 5; day <= 7; day++) { + await multiweekView.waitForItemAt(window, 1, day, 1); + } + + for (let week = 2; week <= 4; week++) { + for (let day = 1; day <= 7; day++) { + await multiweekView.waitForItemAt(window, week, day, 1); + } + } + // Check month view for all 5 weeks. + await setCalendarView(window, "month"); + await CalendarTestUtils.goToDate(window, 2009, 1, 1); + + for (let day = 5; day <= 7; day++) { + await monthView.waitForItemAt(window, 1, day, 1); + } + + for (let week = 2; week <= 5; week++) { + for (let day = 1; day <= 7; day++) { + await monthView.waitForItemAt(window, week, day, 1); + } + } + + // Delete 3rd January occurrence. + let saturday = await monthView.waitForItemAt(window, 1, 7, 1); + EventUtils.synthesizeMouseAtCenter(saturday, {}, window); + await handleDeleteOccurrencePrompt(window, saturday, false); + + // Verify in all views. + await monthView.waitForNoItemAt(window, 1, 7, 1); + + await setCalendarView(window, "multiweek"); + Assert.ok(!multiweekView.getItemAt(window, 1, 7, 1)); + + await setCalendarView(window, "week"); + Assert.ok(!weekView.getEventBoxAt(window, 7, 1)); + + await setCalendarView(window, "day"); + Assert.ok(!dayView.getEventBoxAt(window, 1)); + + // Go to previous day to edit event to occur only on weekdays. + await calendarViewBackward(window, 1); + + ({ dialogWindow, iframeWindow } = await dayView.editEventOccurrencesAt(window, 1)); + await setData(dialogWindow, iframeWindow, { repeat: "every.weekday" }); + await saveAndCloseItemDialog(dialogWindow); + + // Check day view for 7 days. + let dates = [ + [2009, 1, 3], + [2009, 1, 4], + ]; + for (let [y, m, d] of dates) { + await CalendarTestUtils.goToDate(window, y, m, d); + Assert.ok(!dayView.getEventBoxAt(window, 1)); + } + + // Check week view for 2 weeks. + await setCalendarView(window, "week"); + await CalendarTestUtils.goToDate(window, 2009, 1, 1); + + for (let i = 0; i <= 1; i++) { + await weekView.waitForNoEventBoxAt(window, 1, 1); + Assert.ok(!weekView.getEventBoxAt(window, 7, 1)); + await calendarViewForward(window, 1); + } + + // Check multiweek view for 4 weeks. + await setCalendarView(window, "multiweek"); + await CalendarTestUtils.goToDate(window, 2009, 1, 1); + + for (let i = 1; i <= 4; i++) { + await multiweekView.waitForNoItemAt(window, i, 1, 1); + Assert.ok(!multiweekView.getItemAt(window, i, 7, 1)); + } + + // Check month view for all 5 weeks. + await setCalendarView(window, "month"); + await CalendarTestUtils.goToDate(window, 2009, 1, 1); + + for (let i = 1; i <= 5; i++) { + await monthView.waitForNoItemAt(window, i, 1, 1); + Assert.ok(!monthView.getItemAt(window, i, 7, 1)); + } + + // Delete event. + let day = monthView.getItemAt(window, 1, 5, 1); + EventUtils.synthesizeMouseAtCenter(day, {}, window); + await handleDeleteOccurrencePrompt(window, day, true); + await monthView.waitForNoItemAt(window, 1, 5, 1); + + Assert.ok(true, "Test ran to completion"); +}); diff --git a/comm/calendar/test/browser/recurrence/browser_lastDayOfMonth.js b/comm/calendar/test/browser/recurrence/browser_lastDayOfMonth.js new file mode 100644 index 0000000000..bc7e01556a --- /dev/null +++ b/comm/calendar/test/browser/recurrence/browser_lastDayOfMonth.js @@ -0,0 +1,112 @@ +/* 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 { handleDeleteOccurrencePrompt } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarUtils.jsm" +); + +var { menulistSelect } = ChromeUtils.import( + "resource://testing-common/calendar/ItemEditingHelpers.jsm" +); +var { saveAndCloseItemDialog, setData } = ChromeUtils.import( + "resource://testing-common/calendar/ItemEditingHelpers.jsm" +); + +var { setCalendarView, dayView, weekView, multiweekView, monthView } = CalendarTestUtils; + +const HOUR = 8; + +add_task(async function testLastDayOfMonthRecurrence() { + let calendar = CalendarTestUtils.createCalendar(); + registerCleanupFunction(() => { + CalendarTestUtils.removeCalendar(calendar); + }); + + await setCalendarView(window, "day"); + await CalendarTestUtils.goToDate(window, 2008, 1, 31); // Start with a leap year. + + // Create monthly recurring event. + let eventBox = dayView.getHourBoxAt(window, HOUR); + let { dialogWindow, iframeWindow } = await CalendarTestUtils.editNewEvent(window, eventBox); + await setData(dialogWindow, iframeWindow, { title: "Event", repeat: setRecurrence }); + await saveAndCloseItemDialog(dialogWindow); + + // data tuple: [year, month, day, row in month view] + // note: Month starts here with 1 for January. + let checkingData = [ + [2008, 1, 31, 5], + [2008, 2, 29, 5], + [2008, 3, 31, 6], + [2008, 4, 30, 5], + [2008, 5, 31, 5], + [2008, 6, 30, 5], + [2008, 7, 31, 5], + [2008, 8, 31, 6], + [2008, 9, 30, 5], + [2008, 10, 31, 5], + [2008, 11, 30, 6], + [2008, 12, 31, 5], + [2009, 1, 31, 5], + [2009, 2, 28, 4], + [2009, 3, 31, 5], + ]; + // Check all dates. + for (let [y, m, d, correctRow] of checkingData) { + let date = new Date(Date.UTC(y, m - 1, d)); + let column = date.getUTCDay() + 1; + + await CalendarTestUtils.goToDate(window, y, m, d); + + // day view + await setCalendarView(window, "day"); + await dayView.waitForEventBoxAt(window, 1); + + // week view + await setCalendarView(window, "week"); + await weekView.waitForEventBoxAt(window, column, 1); + + // multiweek view + await setCalendarView(window, "multiweek"); + await multiweekView.waitForItemAt(window, 1, column, 1); + + // month view + await setCalendarView(window, "month"); + await monthView.waitForItemAt(window, correctRow, column, 1); + } + + // Delete event. + await CalendarTestUtils.goToDate( + window, + checkingData[0][0], + checkingData[0][1], + checkingData[0][2] + ); + await setCalendarView(window, "day"); + let box = await dayView.waitForEventBoxAt(window, 1); + EventUtils.synthesizeMouseAtCenter(box, {}, window); + await handleDeleteOccurrencePrompt(window, box, true); + await dayView.waitForNoEventBoxAt(window, 1); + + Assert.ok(true, "Test ran to completion"); +}); + +async function setRecurrence(recurrenceWindow) { + let recurrenceDocument = recurrenceWindow.document; + // monthly + await menulistSelect(recurrenceDocument.getElementById("period-list"), "2"); + + // last day of month + EventUtils.synthesizeMouseAtCenter( + recurrenceDocument.getElementById("montly-period-relative-date-radio"), + {}, + recurrenceWindow + ); + await menulistSelect(recurrenceDocument.getElementById("monthly-ordinal"), "-1"); + await menulistSelect(recurrenceDocument.getElementById("monthly-weekday"), "-1"); + + let button = recurrenceDocument.querySelector("dialog").getButton("accept"); + button.scrollIntoView(); + // Close dialog. + EventUtils.synthesizeMouseAtCenter(button, {}, recurrenceWindow); +} diff --git a/comm/calendar/test/browser/recurrence/browser_recurrenceNavigation.js b/comm/calendar/test/browser/recurrence/browser_recurrenceNavigation.js new file mode 100644 index 0000000000..8dfe7287ce --- /dev/null +++ b/comm/calendar/test/browser/recurrence/browser_recurrenceNavigation.js @@ -0,0 +1,138 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); + +const calendar = CalendarTestUtils.createCalendar("Minimonths", "memory"); + +registerCleanupFunction(() => { + CalendarTestUtils.removeCalendar(calendar); +}); + +add_task(async function testRecurrenceNavigation() { + await CalendarTestUtils.setCalendarView(window, "month"); + + let eventDate = cal.createDateTime("20200201T000001Z"); + window.goToDate(eventDate); + + let newEventBtn = document.querySelector("#sidePanelNewEvent"); + let getEventWin = CalendarTestUtils.waitForEventDialog("edit"); + EventUtils.synthesizeMouseAtCenter(newEventBtn, {}); + + let eventWin = await getEventWin; + let iframe = eventWin.document.querySelector("iframe"); + + let getRepeatWin = BrowserTestUtils.promiseAlertDialogOpen( + "", + "chrome://calendar/content/calendar-event-dialog-recurrence.xhtml", + { + async callback(win) { + let container = await TestUtils.waitForCondition(() => { + return win.document.querySelector("#recurrencePreviewContainer"); + }, `The recurrence container exists`); + + let initialMonth = await TestUtils.waitForCondition(() => { + return container.querySelector(`calendar-minimonth[month="1"][year="2020"]`); + }, `Initial month exists`); + Assert.ok(!initialMonth.hidden, `Initial month is visible on load`); + + let nextButton = container.querySelector("#recurrenceNext"); + Assert.ok(nextButton, `Next button exists`); + nextButton.scrollIntoView(); + EventUtils.synthesizeMouseAtCenter(nextButton, {}, win); + + let nextMonth = container.querySelector(`calendar-minimonth[month="2"][year="2020"]`); + Assert.ok(nextMonth, `Next month exists`); + Assert.ok(!nextMonth.hidden, `Next month is visible`); + + let previousButton = container.querySelector("#recurrencePrevious"); + Assert.ok(previousButton, `Previous button exists`); + previousButton.scrollIntoView(); + EventUtils.synthesizeMouseAtCenter(previousButton, {}, win); + Assert.ok(!initialMonth.hidden, `Previous month is visible after using previous button`); + + // Check that future dates display + nextButton.scrollIntoView(); + for (let index = 0; index < 5; index++) { + EventUtils.synthesizeMouseAtCenter(nextButton, {}, win); + } + + let futureMonth = await TestUtils.waitForCondition(() => { + return container.querySelector(`calendar-minimonth[month="6"][year="2020"]`); + }, `Future month exist`); + Assert.ok(!futureMonth.hidden, `Future month is visible after using next button`); + + // Ensure the number of minimonths shown is the amount we expect. + let defaultMinimonthCount = "3"; + let actualVisibleMinimonthCount = container.querySelectorAll( + `calendar-minimonth:not([hidden])` + ).length; + Assert.equal( + defaultMinimonthCount, + actualVisibleMinimonthCount, + `Default minimonth visible count matches actual: ${actualVisibleMinimonthCount}` + ); + + // Go back 5 times; we should go back to the initial month. + for (let index = 0; index < 5; index++) { + EventUtils.synthesizeMouseAtCenter(previousButton, {}, win); + } + Assert.ok(!initialMonth.hidden, `Initial month is visible`); + + // Close window at end of tests for this item + await BrowserTestUtils.closeWindow(win); + }, + } + ); + + let repeatMenu = iframe.contentDocument.querySelector("#item-repeat"); + repeatMenu.value = "custom"; + repeatMenu.doCommand(); + await getRepeatWin; + + await BrowserTestUtils.closeWindow(eventWin); +}); + +add_task(async function testRecurrenceCreationOfMonths() { + await CalendarTestUtils.setCalendarView(window, "month"); + + let eventDate = cal.createDateTime("20200101T000001Z"); + window.goToDate(eventDate); + + let newEventBtn = document.querySelector("#sidePanelNewEvent"); + let getEventWin = CalendarTestUtils.waitForEventDialog("edit"); + EventUtils.synthesizeMouseAtCenter(newEventBtn, {}); + + let eventWin = await getEventWin; + let iframe = eventWin.document.querySelector("iframe"); + + let getRepeatWin = BrowserTestUtils.promiseAlertDialogOpen( + "", + "chrome://calendar/content/calendar-event-dialog-recurrence.xhtml", + { + async callback(win) { + let container = win.document.querySelector("#recurrencePreviewContainer"); + let nextButton = container.querySelector("#recurrenceNext"); + nextButton.scrollIntoView(); + for (let index = 0; index < 10; index++) { + EventUtils.synthesizeMouseAtCenter(nextButton, {}, win); + } + + let futureMonth = container.querySelector(`calendar-minimonth[month="10"][year="2020"]`); + Assert.ok(futureMonth, `Dynamically created future month exists`); + Assert.ok(!futureMonth.hidden, `Dynamically created future month is visible`); + + // Close window at end of tests for this item + await BrowserTestUtils.closeWindow(win); + }, + } + ); + + let repeatMenu = iframe.contentDocument.querySelector("#item-repeat"); + repeatMenu.value = "custom"; + repeatMenu.doCommand(); + await getRepeatWin; + + await BrowserTestUtils.closeWindow(eventWin); +}); diff --git a/comm/calendar/test/browser/recurrence/browser_rotated.ini b/comm/calendar/test/browser/recurrence/browser_rotated.ini new file mode 100644 index 0000000000..2385ed9324 --- /dev/null +++ b/comm/calendar/test/browser/recurrence/browser_rotated.ini @@ -0,0 +1,24 @@ +[default] +head = head.js +dupe-manifest = +prefs = + calendar.item.promptDelete=false + calendar.test.rotateViews=true + 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 +tags = recurrence-rotated + +[browser_annual.js] +[browser_biweekly.js] +[browser_daily.js] +[browser_lastDayOfMonth.js] +[browser_weeklyN.js] +[browser_weeklyUntil.js] +[browser_weeklyWithException.js] diff --git a/comm/calendar/test/browser/recurrence/browser_weeklyN.js b/comm/calendar/test/browser/recurrence/browser_weeklyN.js new file mode 100644 index 0000000000..e32ab470f1 --- /dev/null +++ b/comm/calendar/test/browser/recurrence/browser_weeklyN.js @@ -0,0 +1,268 @@ +/* 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 { handleDeleteOccurrencePrompt } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarUtils.jsm" +); + +var { menulistSelect, saveAndCloseItemDialog, setData } = ChromeUtils.import( + "resource://testing-common/calendar/ItemEditingHelpers.jsm" +); + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); + +var { dayView, weekView, multiweekView, monthView } = CalendarTestUtils; + +const HOUR = 8; + +/* + * This test is intended to verify that events recurring on a weekly basis are + * correctly created and displayed. The event should recur on multiple days in + * the week, skip days, and be limited to a certain number of recurrences in + * order to verify that these parameters are respected. Deletion should delete + * all event occurrences when appropriate. + */ +add_task(async function testWeeklyNRecurrence() { + async function setRecurrence(recurrenceWindow) { + let recurrenceDocument = recurrenceWindow.document; + + // Select weekly recurrence + await menulistSelect(recurrenceDocument.getElementById("period-list"), "1"); + + let monLabel = cal.l10n.getDateFmtString("day.2.Mmm"); + let tueLabel = cal.l10n.getDateFmtString("day.3.Mmm"); + let wedLabel = cal.l10n.getDateFmtString("day.4.Mmm"); + let friLabel = cal.l10n.getDateFmtString("day.6.Mmm"); + let satLabel = cal.l10n.getDateFmtString("day.7.Mmm"); + + let dayPicker = recurrenceDocument.getElementById("daypicker-weekday"); + + // Selected date is a Monday, so it should already be selected + Assert.ok( + dayPicker.querySelector(`[label="${monLabel}"]`).checked, + "Monday should already be selected" + ); + + // Select Tuesday, Wednesday, Friday, and Saturday as additional days for + // event occurrences + EventUtils.synthesizeMouseAtCenter( + dayPicker.querySelector(`[label="${tueLabel}"]`), + {}, + recurrenceWindow + ); + EventUtils.synthesizeMouseAtCenter( + dayPicker.querySelector(`[label="${wedLabel}"]`), + {}, + recurrenceWindow + ); + EventUtils.synthesizeMouseAtCenter( + dayPicker.querySelector(`[label="${friLabel}"]`), + {}, + recurrenceWindow + ); + EventUtils.synthesizeMouseAtCenter( + dayPicker.querySelector(`[label="${satLabel}"]`), + {}, + recurrenceWindow + ); + + // Create a total of four events + EventUtils.synthesizeMouseAtCenter( + recurrenceDocument.getElementById("recurrence-range-for"), + {}, + recurrenceWindow + ); + recurrenceDocument.getElementById("repeat-ntimes-count").value = "4"; + + let button = recurrenceDocument.querySelector("dialog").getButton("accept"); + button.scrollIntoView(); + // Close dialog + EventUtils.synthesizeMouseAtCenter(button, {}, recurrenceWindow); + } + + let calendar = CalendarTestUtils.createCalendar(); + registerCleanupFunction(() => { + CalendarTestUtils.removeCalendar(calendar); + }); + + await CalendarTestUtils.setCalendarView(window, "day"); + await CalendarTestUtils.goToDate(window, 2009, 1, 5); + + // Create event recurring on a weekly basis + let eventBox = dayView.getHourBoxAt(window, HOUR); + let { dialogWindow, iframeWindow } = await CalendarTestUtils.editNewEvent(window, eventBox); + await setData(dialogWindow, iframeWindow, { title: "Event", repeat: setRecurrence }); + await saveAndCloseItemDialog(dialogWindow); + + // Verify in the day view that events were created for Monday through Wednesday + for (let i = 0; i < 3; i++) { + await dayView.waitForEventBoxAt(window, 1); + await CalendarTestUtils.calendarViewForward(window, 1); + } + + // No event should have been created on Thursday because it was not selected + await dayView.waitForNoEventBoxAt(window, 1); + await CalendarTestUtils.calendarViewForward(window, 1); + + // An event should have been created for Friday because it was selected + await dayView.waitForEventBoxAt(window, 1); + await CalendarTestUtils.calendarViewForward(window, 1); + + // No event should have been created on Saturday due to four event limit + await dayView.waitForNoEventBoxAt(window, 1); + + // Validate event creation and lack of Saturday event in week view + await CalendarTestUtils.setCalendarView(window, "week"); + + for (let i = 2; i < 5; i++) { + await weekView.waitForEventBoxAt(window, i, 1); + } + + // No event Thursday or Saturday, event on Friday + await weekView.waitForNoEventBoxAt(window, 5, 1); + await weekView.waitForEventBoxAt(window, 6, 1); + await weekView.waitForNoEventBoxAt(window, 7, 1); + + // Validate event creation and lack of Saturday event in multiweek view + await CalendarTestUtils.setCalendarView(window, "multiweek"); + + for (let i = 2; i < 5; i++) { + await multiweekView.waitForItemAt(window, 1, i, 1); + } + + // No event Thursday or Saturday, event on Friday + await multiweekView.waitForNoItemAt(window, 1, 5, 1); + await multiweekView.waitForItemAt(window, 1, 6, 1); + await multiweekView.waitForNoItemAt(window, 1, 7, 1); + + // Validate event creation and lack of Saturday event in month view + await CalendarTestUtils.setCalendarView(window, "month"); + + for (let i = 2; i < 5; i++) { + // This should be the second week in the month + await monthView.waitForItemAt(window, 2, i, 1); + } + + // No event Thursday or Saturday, event on Friday + await monthView.waitForNoItemAt(window, 2, 5, 1); + await monthView.waitForItemAt(window, 2, 6, 1); + await monthView.waitForNoItemAt(window, 2, 7, 1); + + // Delete event + let box = await monthView.waitForItemAt(window, 2, 2, 1); + EventUtils.synthesizeMouseAtCenter(box, {}, window); + await handleDeleteOccurrencePrompt(window, box, true); + + // All occurrences should have been deleted + for (let i = 2; i < 5; i++) { + await monthView.waitForNoItemAt(window, 2, i, 1); + } + + await monthView.waitForNoItemAt(window, 2, 6, 1); +}); + +/* + * This test is intended to catch instances in which we aren't correctly setting + * the week start value of recurrences. For example, if the user has set their + * week to start on Saturday, then creates a recurring event running every other + * Saturday, Sunday, and Monday, they expect to see events on the initial + * Saturday, Sunday, Monday, skip a week, repeat. However, week start defaults + * to Monday, so if it is not correctly set, they would see events on the + * initial Saturday and Sunday, nothing on Monday, but an event on the following + * Monday. + */ +add_task(async function testRecurrenceAcrossWeekStart() { + // Sanity check that we're not testing against a default value + const initialWeekStart = Services.prefs.getIntPref("calendar.week.start", 0); + Assert.notEqual(initialWeekStart, 6, "week start should not be Saturday"); + + // Set week start to Saturday + Services.prefs.setIntPref("calendar.week.start", 6); + registerCleanupFunction(() => { + Services.prefs.setIntPref("calendar.week.start", initialWeekStart); + }); + + async function setRecurrence(recurrenceWindow) { + let recurrenceDocument = recurrenceWindow.document; + + // Select weekly recurrence + await menulistSelect(recurrenceDocument.getElementById("period-list"), "1"); + + // Recur every two weeks + recurrenceDocument.getElementById("weekly-weeks").value = "2"; + + let satLabel = cal.l10n.getDateFmtString("day.7.Mmm"); + let sunLabel = cal.l10n.getDateFmtString("day.1.Mmm"); + let monLabel = cal.l10n.getDateFmtString("day.2.Mmm"); + + let dayPicker = recurrenceDocument.getElementById("daypicker-weekday"); + + // Selected date is a Saturday, so it should already be selected + Assert.ok( + dayPicker.querySelector(`[label="${satLabel}"]`).checked, + "Saturday should already be checked" + ); + + // Select Sunday and Monday as additional days for event occurrences + EventUtils.synthesizeMouseAtCenter( + dayPicker.querySelector(`[label="${sunLabel}"]`), + {}, + recurrenceWindow + ); + EventUtils.synthesizeMouseAtCenter( + dayPicker.querySelector(`[label="${monLabel}"]`), + {}, + recurrenceWindow + ); + + // Create a total of six events + EventUtils.synthesizeMouseAtCenter( + recurrenceDocument.getElementById("recurrence-range-for"), + {}, + recurrenceWindow + ); + recurrenceDocument.getElementById("repeat-ntimes-count").value = "6"; + + let button = recurrenceDocument.querySelector("dialog").getButton("accept"); + button.scrollIntoView(); + // Close dialog + EventUtils.synthesizeMouseAtCenter(button, {}, recurrenceWindow); + } + + let calendar = CalendarTestUtils.createCalendar(); + registerCleanupFunction(() => { + CalendarTestUtils.removeCalendar(calendar); + }); + + await CalendarTestUtils.setCalendarView(window, "day"); + await CalendarTestUtils.goToDate(window, 2022, 10, 15); + + // Create event recurring every other week + let eventBox = dayView.getHourBoxAt(window, HOUR); + let { dialogWindow, iframeWindow } = await CalendarTestUtils.editNewEvent(window, eventBox); + await setData(dialogWindow, iframeWindow, { title: "Event", repeat: setRecurrence }); + await saveAndCloseItemDialog(dialogWindow); + + // Open week view + await CalendarTestUtils.setCalendarView(window, "week"); + + // Verify events created on Saturday, Sunday, Monday of first week + for (let i = 1; i < 4; i++) { + await weekView.waitForEventBoxAt(window, i, 1); + } + + // Verify no events created on Saturday, Sunday, Monday of second week + await CalendarTestUtils.calendarViewForward(window, 1); + + for (let i = 1; i < 4; i++) { + await weekView.waitForNoEventBoxAt(window, i, 1); + } + + // Verify events created on Saturday, Sunday, Monday of third week + await CalendarTestUtils.calendarViewForward(window, 1); + + for (let i = 1; i < 4; i++) { + await weekView.waitForEventBoxAt(window, i, 1); + } +}); diff --git a/comm/calendar/test/browser/recurrence/browser_weeklyUntil.js b/comm/calendar/test/browser/recurrence/browser_weeklyUntil.js new file mode 100644 index 0000000000..c9780e9428 --- /dev/null +++ b/comm/calendar/test/browser/recurrence/browser_weeklyUntil.js @@ -0,0 +1,175 @@ +/* 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 { handleDeleteOccurrencePrompt } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarUtils.jsm" +); + +var { formatDate, menulistSelect, saveAndCloseItemDialog, setData } = ChromeUtils.import( + "resource://testing-common/calendar/ItemEditingHelpers.jsm" +); + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); + +var { dayView, weekView, multiweekView, monthView } = CalendarTestUtils; + +const ENDDATE = cal.createDateTime("20090126T000000Z"); // Last Monday in month. +const HOUR = 8; + +add_task(async function testWeeklyUntilRecurrence() { + let calendar = CalendarTestUtils.createCalendar(); + registerCleanupFunction(() => { + CalendarTestUtils.removeCalendar(calendar); + }); + + await CalendarTestUtils.setCalendarView(window, "day"); + await CalendarTestUtils.goToDate(window, 2009, 1, 5); // Monday + + // Create weekly recurring event. + let eventBox = dayView.getHourBoxAt(window, HOUR); + let { dialogWindow, iframeWindow } = await CalendarTestUtils.editNewEvent(window, eventBox); + await setData(dialogWindow, iframeWindow, { title: "Event", repeat: setRecurrence }); + await saveAndCloseItemDialog(dialogWindow); + + // Check day view. + for (let week = 0; week < 3; week++) { + // Monday + await dayView.waitForEventBoxAt(window, 1); + await CalendarTestUtils.calendarViewForward(window, 2); + + // Wednesday + await dayView.waitForEventBoxAt(window, 1); + await CalendarTestUtils.calendarViewForward(window, 2); + + // Friday + await dayView.waitForEventBoxAt(window, 1); + await CalendarTestUtils.calendarViewForward(window, 3); + } + + // Monday, last occurrence + await dayView.waitForEventBoxAt(window, 1); + await CalendarTestUtils.calendarViewForward(window, 2); + + // Wednesday + await dayView.waitForNoEventBoxAt(window, 1); + + // Check week view. + await CalendarTestUtils.setCalendarView(window, "week"); + await CalendarTestUtils.goToDate(window, 2009, 1, 5); + for (let week = 0; week < 3; week++) { + // Monday + await weekView.waitForEventBoxAt(window, 2, 1); + + // Wednesday + await weekView.waitForEventBoxAt(window, 4, 1); + + // Friday + await weekView.waitForEventBoxAt(window, 6, 1); + + await CalendarTestUtils.calendarViewForward(window, 1); + } + + // Monday, last occurrence + await weekView.waitForEventBoxAt(window, 2, 1); + // Wednesday + await weekView.waitForNoEventBoxAt(window, 4, 1); + + // Check multiweek view. + await CalendarTestUtils.setCalendarView(window, "multiweek"); + await CalendarTestUtils.goToDate(window, 2009, 1, 5); + for (let week = 1; week < 4; week++) { + // Monday + await multiweekView.waitForItemAt(window, week, 2, 1); + // Wednesday + await multiweekView.waitForItemAt(window, week, 4, 1); + // Friday + await multiweekView.waitForItemAt(window, week, 6, 1); + } + + // Monday, last occurrence + await multiweekView.waitForItemAt(window, 4, 2, 1); + + // Wednesday + await multiweekView.waitForNoItemAt(window, 4, 4, 1); + + // Check month view. + await CalendarTestUtils.setCalendarView(window, "month"); + await CalendarTestUtils.goToDate(window, 2009, 1, 5); + // starts on week 2 in month-view + for (let week = 2; week < 5; week++) { + // Monday + await monthView.waitForItemAt(window, week, 2, 1); + // Wednesday + await monthView.waitForItemAt(window, week, 4, 1); + // Friday + await monthView.waitForItemAt(window, week, 6, 1); + } + + // Monday, last occurrence + await monthView.waitForItemAt(window, 5, 2, 1); + + // Wednesday + await monthView.waitForNoItemAt(window, 5, 4, 1); + + // Delete event. + let box = monthView.getItemAt(window, 2, 2, 1); + EventUtils.synthesizeMouseAtCenter(box, {}, window); + await handleDeleteOccurrencePrompt(window, box, true); + await monthView.waitForNoItemAt(window, 2, 2, 1); + + Assert.ok(true, "Test ran to completion"); +}); + +async function setRecurrence(recurrenceWindow) { + let recurrenceDocument = recurrenceWindow.document; + + // weekly + await menulistSelect(recurrenceDocument.getElementById("period-list"), "1"); + + let mon = cal.l10n.getDateFmtString("day.2.Mmm"); + let wed = cal.l10n.getDateFmtString("day.4.Mmm"); + let fri = cal.l10n.getDateFmtString("day.6.Mmm"); + + let dayPicker = recurrenceDocument.getElementById("daypicker-weekday"); + + // Starting from Monday so it should be checked. + Assert.ok(dayPicker.querySelector(`[label="${mon}"]`).checked, "mon checked"); + // Check Wednesday and Friday too. + EventUtils.synthesizeMouseAtCenter( + dayPicker.querySelector(`[label="${wed}"]`), + {}, + recurrenceWindow + ); + EventUtils.synthesizeMouseAtCenter( + dayPicker.querySelector(`[label="${fri}"]`), + {}, + recurrenceWindow + ); + + // Set until date. + EventUtils.synthesizeMouseAtCenter( + recurrenceDocument.getElementById("recurrence-range-until"), + {}, + recurrenceWindow + ); + + // Delete previous date. + let untilInput = recurrenceDocument.getElementById("repeat-until-date"); + untilInput.focus(); + EventUtils.synthesizeKey("a", { accelKey: true }, recurrenceWindow); + untilInput.focus(); + EventUtils.synthesizeKey("VK_DELETE", {}, recurrenceWindow); + + let endDateString = formatDate(ENDDATE); + EventUtils.sendString(endDateString, recurrenceWindow); + + // Move focus to ensure the date is selected. + untilInput.focus(); + EventUtils.synthesizeKey("VK_TAB", {}, recurrenceWindow); + + let button = recurrenceDocument.querySelector("dialog").getButton("accept"); + button.scrollIntoView(); + // Close dialog. + EventUtils.synthesizeMouseAtCenter(button, {}, recurrenceWindow); +} diff --git a/comm/calendar/test/browser/recurrence/browser_weeklyWithException.js b/comm/calendar/test/browser/recurrence/browser_weeklyWithException.js new file mode 100644 index 0000000000..fbe007ea45 --- /dev/null +++ b/comm/calendar/test/browser/recurrence/browser_weeklyWithException.js @@ -0,0 +1,264 @@ +/* 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 { handleDeleteOccurrencePrompt } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarUtils.jsm" +); + +var { menulistSelect, saveAndCloseItemDialog, setData } = ChromeUtils.import( + "resource://testing-common/calendar/ItemEditingHelpers.jsm" +); + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); + +var { dayView, weekView, multiweekView, monthView } = CalendarTestUtils; + +const HOUR = 8; +const STARTDATE = cal.createDateTime("20090106T000000Z"); +const TITLE = "Event"; + +add_task(async function testWeeklyWithExceptionRecurrence() { + let calendar = CalendarTestUtils.createCalendar(); + registerCleanupFunction(() => { + CalendarTestUtils.removeCalendar(calendar); + }); + + await CalendarTestUtils.setCalendarView(window, "day"); + await CalendarTestUtils.goToDate(window, 2009, 1, 5); + + // Create weekly recurring event. + let eventBox = dayView.getHourBoxAt(window, HOUR); + let { dialogWindow, iframeWindow } = await CalendarTestUtils.editNewEvent(window, eventBox); + await setData(dialogWindow, iframeWindow, { title: TITLE, repeat: setRecurrence }); + await saveAndCloseItemDialog(dialogWindow); + + let eventItem = await dayView.waitForEventBoxAt(window, 1); + let icon = eventItem.querySelector(".item-recurrence-icon"); + Assert.equal(icon.src, "chrome://messenger/skin/icons/new/recurrence.svg"); + Assert.ok(!icon.hidden); + + // Move 5th January occurrence to 6th January. + ({ dialogWindow, iframeWindow } = await dayView.editEventOccurrenceAt(window, 1)); + await setData(dialogWindow, iframeWindow, { + title: TITLE, + startdate: STARTDATE, + enddate: STARTDATE, + }); + await saveAndCloseItemDialog(dialogWindow); + + await CalendarTestUtils.goToDate(window, 2009, 1, 6); + eventItem = await dayView.waitForEventBoxAt(window, 1); + icon = eventItem.querySelector(".item-recurrence-icon"); + Assert.equal(icon.src, "chrome://messenger/skin/icons/new/recurrence-exception.svg"); + + // Change recurrence rule. + await CalendarTestUtils.goToDate(window, 2009, 1, 7); + ({ dialogWindow, iframeWindow } = await dayView.editEventOccurrencesAt(window, 1)); + await setData(dialogWindow, iframeWindow, { + title: "Event", + repeat: changeRecurrence, + }); + await saveAndCloseItemDialog(dialogWindow); + + // Check two weeks. + // day view + await CalendarTestUtils.setCalendarView(window, "day"); + + await CalendarTestUtils.goToDate(window, 2009, 1, 5); + await dayView.waitForNoEventBoxAt(window, 1); + + await CalendarTestUtils.calendarViewForward(window, 1); + + // Assert exactly two. + Assert.ok(!!(await dayView.waitForEventBoxAt(window, 1))); + Assert.ok(!!(await dayView.waitForEventBoxAt(window, 2))); + + await CalendarTestUtils.calendarViewForward(window, 1); + await dayView.waitForEventBoxAt(window, 1); + await CalendarTestUtils.calendarViewForward(window, 1); + await dayView.waitForNoEventBoxAt(window, 1); + await CalendarTestUtils.calendarViewForward(window, 1); + await dayView.waitForEventBoxAt(window, 1); + await CalendarTestUtils.calendarViewForward(window, 1); + await dayView.waitForNoEventBoxAt(window, 1); + await CalendarTestUtils.calendarViewForward(window, 1); + await dayView.waitForNoEventBoxAt(window, 1); + + // next week + await CalendarTestUtils.calendarViewForward(window, 1); + await dayView.waitForEventBoxAt(window, 1); + await CalendarTestUtils.calendarViewForward(window, 1); + await dayView.waitForEventBoxAt(window, 1); + await CalendarTestUtils.calendarViewForward(window, 1); + await dayView.waitForEventBoxAt(window, 1); + await CalendarTestUtils.calendarViewForward(window, 1); + await dayView.waitForNoEventBoxAt(window, 1); + await CalendarTestUtils.calendarViewForward(window, 1); + await dayView.waitForEventBoxAt(window, 1); + await CalendarTestUtils.calendarViewForward(window, 1); + await dayView.waitForNoEventBoxAt(window, 1); + + // week view + await CalendarTestUtils.setCalendarView(window, "week"); + await CalendarTestUtils.goToDate(window, 2009, 1, 5); + + // Assert exactly two on Tuesday. + Assert.ok(!!(await weekView.waitForEventBoxAt(window, 3, 1))); + Assert.ok(!!(await weekView.waitForEventBoxAt(window, 3, 2))); + + // Wait for the last occurrence because this appears last. + eventItem = await weekView.waitForEventBoxAt(window, 6, 1); + icon = eventItem.querySelector(".item-recurrence-icon"); + Assert.equal(icon.src, "chrome://messenger/skin/icons/new/recurrence.svg"); + Assert.ok(!icon.hidden); + + Assert.ok(!weekView.getEventBoxAt(window, 1, 1)); + Assert.ok(!weekView.getEventBoxAt(window, 2, 1)); + Assert.ok(!!weekView.getEventBoxAt(window, 4, 1)); + Assert.ok(!weekView.getEventBoxAt(window, 5, 1)); + Assert.ok(!weekView.getEventBoxAt(window, 7, 1)); + + await CalendarTestUtils.calendarViewForward(window, 1); + await weekView.waitForEventBoxAt(window, 6, 1); + Assert.ok(!weekView.getEventBoxAt(window, 1, 1)); + Assert.ok(!!weekView.getEventBoxAt(window, 2, 1)); + Assert.ok(!!weekView.getEventBoxAt(window, 3, 1)); + Assert.ok(!!weekView.getEventBoxAt(window, 4, 1)); + Assert.ok(!weekView.getEventBoxAt(window, 5, 1)); + Assert.ok(!weekView.getEventBoxAt(window, 7, 1)); + + // multiweek view + await CalendarTestUtils.setCalendarView(window, "multiweek"); + await CalendarTestUtils.goToDate(window, 2009, 1, 5); + // Wait for the first items, then check the ones not to be present. + // Assert exactly two. + await multiweekView.waitForItemAt(window, 1, 3, 1, 1); + Assert.ok(multiweekView.getItemAt(window, 1, 3, 2, 1)); + Assert.ok(!multiweekView.getItemAt(window, 1, 3, 3, 1)); + // Then check no item on the 5th. + Assert.ok(!multiweekView.getItemAt(window, 1, 2, 1)); + Assert.ok(multiweekView.getItemAt(window, 1, 4, 1)); + Assert.ok(!multiweekView.getItemAt(window, 1, 5, 1)); + Assert.ok(multiweekView.getItemAt(window, 1, 6, 1)); + Assert.ok(!multiweekView.getItemAt(window, 1, 7, 1)); + + Assert.ok(!multiweekView.getItemAt(window, 2, 1, 1)); + Assert.ok(multiweekView.getItemAt(window, 2, 2, 1)); + Assert.ok(multiweekView.getItemAt(window, 2, 3, 1)); + Assert.ok(multiweekView.getItemAt(window, 2, 4, 1)); + Assert.ok(!multiweekView.getItemAt(window, 2, 5, 1)); + Assert.ok(multiweekView.getItemAt(window, 2, 6, 1)); + Assert.ok(!multiweekView.getItemAt(window, 2, 7, 1)); + + eventItem = multiweekView.getItemAt(window, 2, 4, 1); + icon = eventItem.querySelector(".item-recurrence-icon"); + Assert.equal(icon.src, "chrome://messenger/skin/icons/new/recurrence.svg"); + Assert.ok(!icon.hidden); + + // month view + await CalendarTestUtils.setCalendarView(window, "month"); + // Wait for the first items, then check the ones not to be present. + // Assert exactly two. + // start on the second week + await monthView.waitForItemAt(window, 2, 3, 1); + Assert.ok(monthView.getItemAt(window, 2, 3, 2)); + Assert.ok(!monthView.getItemAt(window, 2, 3, 3)); + // Then check no item on the 5th. + Assert.ok(!monthView.getItemAt(window, 2, 2, 1)); + Assert.ok(monthView.getItemAt(window, 2, 4, 1)); + Assert.ok(!monthView.getItemAt(window, 2, 5, 1)); + Assert.ok(monthView.getItemAt(window, 2, 6, 1)); + Assert.ok(!monthView.getItemAt(window, 2, 7, 1)); + + Assert.ok(!monthView.getItemAt(window, 3, 1, 1)); + Assert.ok(monthView.getItemAt(window, 3, 2, 1)); + Assert.ok(monthView.getItemAt(window, 3, 3, 1)); + Assert.ok(monthView.getItemAt(window, 3, 4, 1)); + Assert.ok(!monthView.getItemAt(window, 3, 5, 1)); + Assert.ok(monthView.getItemAt(window, 3, 6, 1)); + Assert.ok(!monthView.getItemAt(window, 3, 7, 1)); + + eventItem = monthView.getItemAt(window, 3, 6, 1); + icon = eventItem.querySelector(".item-recurrence-icon"); + Assert.equal(icon.src, "chrome://messenger/skin/icons/new/recurrence.svg"); + Assert.ok(!icon.hidden); + + // Delete event. + await CalendarTestUtils.setCalendarView(window, "day"); + await CalendarTestUtils.goToDate(window, 2009, 1, 12); + eventBox = await dayView.waitForEventBoxAt(window, 1); + EventUtils.synthesizeMouseAtCenter(eventBox, {}, window); + await handleDeleteOccurrencePrompt(window, eventBox, true); + await dayView.waitForNoEventBoxAt(window, 1); + + Assert.ok(true, "Test ran to completion"); +}); + +async function setRecurrence(recurrenceWindow) { + let recurrenceDocument = recurrenceWindow.document; + + // weekly + await menulistSelect(recurrenceDocument.getElementById("period-list"), "1"); + + let mon = cal.l10n.getDateFmtString("day.2.Mmm"); + let wed = cal.l10n.getDateFmtString("day.4.Mmm"); + let fri = cal.l10n.getDateFmtString("day.6.Mmm"); + + let dayPicker = recurrenceDocument.getElementById("daypicker-weekday"); + + // Starting from Monday so it should be checked. + Assert.ok(dayPicker.querySelector(`[label="${mon}"]`).checked, "mon checked"); + + // Check Wednesday and Friday too. + EventUtils.synthesizeMouseAtCenter( + dayPicker.querySelector(`[label="${wed}"]`), + {}, + recurrenceWindow + ); + Assert.ok(dayPicker.querySelector(`[label="${wed}"]`).checked, "wed checked"); + EventUtils.synthesizeMouseAtCenter( + dayPicker.querySelector(`[label="${fri}"]`), + {}, + recurrenceWindow + ); + Assert.ok(dayPicker.querySelector(`[label="${fri}"]`).checked, "fri checked"); + + let button = recurrenceDocument.querySelector("dialog").getButton("accept"); + button.scrollIntoView(); + // Close dialog. + EventUtils.synthesizeMouseAtCenter(button, {}, recurrenceWindow); +} + +async function changeRecurrence(recurrenceWindow) { + let recurrenceDocument = recurrenceWindow.document; + + // weekly + await menulistSelect(recurrenceDocument.getElementById("period-list"), "1"); + + let mon = cal.l10n.getDateFmtString("day.2.Mmm"); + let tue = cal.l10n.getDateFmtString("day.3.Mmm"); + let wed = cal.l10n.getDateFmtString("day.4.Mmm"); + let fri = cal.l10n.getDateFmtString("day.6.Mmm"); + + let dayPicker = recurrenceDocument.getElementById("daypicker-weekday"); + + // Check old rule. + // Starting from Monday so it should be checked. + Assert.ok(dayPicker.querySelector(`[label="${mon}"]`).checked, "mon checked"); + Assert.ok(dayPicker.querySelector(`[label="${wed}"]`).checked, "wed checked"); + Assert.ok(dayPicker.querySelector(`[label="${fri}"]`).checked, "fri checked"); + + // Check Tuesday. + EventUtils.synthesizeMouseAtCenter( + dayPicker.querySelector(`[label="${tue}"]`), + {}, + recurrenceWindow + ); + Assert.ok(dayPicker.querySelector(`[label="${tue}"]`).checked, "tue checked"); + + let button = recurrenceDocument.querySelector("dialog").getButton("accept"); + button.scrollIntoView(); + // Close dialog. + EventUtils.synthesizeMouseAtCenter(button, {}, recurrenceWindow); +} diff --git a/comm/calendar/test/browser/recurrence/head.js b/comm/calendar/test/browser/recurrence/head.js new file mode 100644 index 0000000000..efcee250b6 --- /dev/null +++ b/comm/calendar/test/browser/recurrence/head.js @@ -0,0 +1,26 @@ +/* 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/. */ + +// The tests in this folder frequently take too long. Give them more time. +requestLongerTimeout(2); + +var { CalendarTestUtils } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarTestUtils.jsm" +); + +/* globals toggleOrientation */ + +let isRotated = + document.getElementById("calendar_toggle_orientation_command").getAttribute("checked") == "true"; +let shouldBeRotated = Services.prefs.getBoolPref("calendar.test.rotateViews", false); + +if (isRotated != shouldBeRotated) { + toggleOrientation(); +} + +const calendarViewsInitialState = CalendarTestUtils.saveCalendarViewsState(window); + +registerCleanupFunction(async () => { + await CalendarTestUtils.restoreCalendarViewsState(window, calendarViewsInitialState); +}); diff --git a/comm/calendar/test/browser/timezones/browser.ini b/comm/calendar/test/browser/timezones/browser.ini new file mode 100644 index 0000000000..2fe2a0b4ea --- /dev/null +++ b/comm/calendar/test/browser/timezones/browser.ini @@ -0,0 +1,17 @@ +[default] +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_minimonth.js] +[browser_timezones.js] +skip-if = debug # Takes way too long, bug 1746973. diff --git a/comm/calendar/test/browser/timezones/browser_minimonth.js b/comm/calendar/test/browser/timezones/browser_minimonth.js new file mode 100644 index 0000000000..eba6bb2485 --- /dev/null +++ b/comm/calendar/test/browser/timezones/browser_minimonth.js @@ -0,0 +1,215 @@ +/* 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 the minimonth widget in a range of time zones. It will fail if the + * widget loses time zone awareness. + */ + +/* eslint-disable no-restricted-syntax */ + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); +var { CalendarTestUtils } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarTestUtils.jsm" +); + +add_setup(async function () { + await CalendarTestUtils.openCalendarTab(window); +}); + +registerCleanupFunction(async function () { + await CalendarTestUtils.closeCalendarTab(window); + Services.prefs.setStringPref("calendar.timezone.local", "UTC"); +}); + +async function subtest() { + let zone = cal.dtz.defaultTimezone; + info(`Running test in ${zone.tzid}`); + + // Set the minimonth to display August 2016. + let minimonth = document.getElementById("calMinimonth"); + minimonth.showMonth(new Date(2016, 7, 15)); + + Assert.deepEqual( + [...minimonth.dayBoxes.keys()], + [ + "2016-07-31", + "2016-08-01", + "2016-08-02", + "2016-08-03", + "2016-08-04", + "2016-08-05", + "2016-08-06", + "2016-08-07", + "2016-08-08", + "2016-08-09", + "2016-08-10", + "2016-08-11", + "2016-08-12", + "2016-08-13", + "2016-08-14", + "2016-08-15", + "2016-08-16", + "2016-08-17", + "2016-08-18", + "2016-08-19", + "2016-08-20", + "2016-08-21", + "2016-08-22", + "2016-08-23", + "2016-08-24", + "2016-08-25", + "2016-08-26", + "2016-08-27", + "2016-08-28", + "2016-08-29", + "2016-08-30", + "2016-08-31", + "2016-09-01", + "2016-09-02", + "2016-09-03", + "2016-09-04", + "2016-09-05", + "2016-09-06", + "2016-09-07", + "2016-09-08", + "2016-09-09", + "2016-09-10", + ], + "day boxes are stored with the correct keys" + ); + + function check(date, row, column) { + if (date instanceof Date) { + info(date); + } else { + info(`${date} ${date.timezone.tzid}`); + } + if (row && column) { + Assert.equal(minimonth.getBoxForDate(date), minimonth.mCalBox.rows[row].cells[column]); + } else { + Assert.equal(minimonth.getBoxForDate(date), null); + } + } + + let dateWithZone = cal.createDateTime(); + + // Dates without timezones or the local timezone. + + // All of these represent the 1st of August. + check(new Date(2016, 7, 1), 1, 2); + check(new Date(2016, 7, 1, 9, 0, 0), 1, 2); + check(new Date(2016, 7, 1, 22, 0, 0), 1, 2); + + check(cal.createDateTime("20160801"), 1, 2); + check(cal.createDateTime("20160801T030000"), 1, 2); + check(cal.createDateTime("20160801T210000"), 1, 2); + + dateWithZone.resetTo(2016, 7, 1, 3, 0, 0, zone); + check(dateWithZone, 1, 2); + dateWithZone.resetTo(2016, 7, 1, 21, 0, 0, zone); + check(dateWithZone, 1, 2); + + // All of these represent the 31st of August. + check(new Date(2016, 7, 31), 5, 4); + check(new Date(2016, 7, 31, 9, 0, 0), 5, 4); + check(new Date(2016, 7, 31, 22, 0, 0), 5, 4); + + check(cal.createDateTime("20160831"), 5, 4); + check(cal.createDateTime("20160831T030000"), 5, 4); + check(cal.createDateTime("20160831T210000"), 5, 4); + + dateWithZone.resetTo(2016, 7, 31, 3, 0, 0, zone); + check(dateWithZone, 5, 4); + dateWithZone.resetTo(2016, 7, 31, 21, 0, 0, zone); + check(dateWithZone, 5, 4); + + // August a year earlier shouldn't be displayed. + check(new Date(2015, 7, 15)); + check(cal.createDateTime("20150815")); + dateWithZone.resetTo(2015, 7, 15, 0, 0, 0, zone); + check(dateWithZone); + + // The Saturday of the previous week shouldn't be displayed. + check(new Date(2016, 6, 30)); + check(cal.createDateTime("20160730")); + dateWithZone.resetTo(2016, 6, 30, 0, 0, 0, zone); + check(dateWithZone); + + // The Sunday of the next week shouldn't be displayed. + check(new Date(2016, 8, 11)); + check(cal.createDateTime("20160911")); + dateWithZone.resetTo(2016, 8, 11, 0, 0, 0, zone); + check(dateWithZone); + + // August a year later shouldn't be displayed. + check(new Date(2017, 7, 15)); + check(cal.createDateTime("20170815")); + dateWithZone.resetTo(2017, 7, 15, 0, 0, 0, zone); + check(dateWithZone); + + // UTC dates. + + check(cal.createDateTime("20160801T030000Z"), 1, zone.tzid == "America/Vancouver" ? 1 : 2); + check(cal.createDateTime("20160801T210000Z"), 1, zone.tzid == "Pacific/Auckland" ? 3 : 2); + + check(cal.createDateTime("20160831T030000Z"), 5, zone.tzid == "America/Vancouver" ? 3 : 4); + check(cal.createDateTime("20160831T210000Z"), 5, zone.tzid == "Pacific/Auckland" ? 5 : 4); + + // Dates in different zones. + + let auckland = cal.timezoneService.getTimezone("Pacific/Auckland"); + let vancouver = cal.timezoneService.getTimezone("America/Vancouver"); + + // Early in Auckland is the previous day everywhere else. + dateWithZone.resetTo(2016, 7, 15, 3, 0, 0, auckland); + check(dateWithZone, 3, zone.tzid == "Pacific/Auckland" ? 2 : 1); + + // Late in Auckland is the same day everywhere. + dateWithZone.resetTo(2016, 7, 15, 21, 0, 0, auckland); + check(dateWithZone, 3, 2); + + // Early in Vancouver is the same day everywhere. + dateWithZone.resetTo(2016, 7, 15, 3, 0, 0, vancouver); + check(dateWithZone, 3, 2); + + // Late in Vancouver is the next day everywhere else. + dateWithZone.resetTo(2016, 7, 15, 21, 0, 0, vancouver); + check(dateWithZone, 3, zone.tzid == "America/Vancouver" ? 2 : 3); + + // Reset the minimonth to a different month. + minimonth.showMonth(new Date(2016, 9, 15)); +} + +/** + * Run the test at UTC+12. + */ +add_task(async function auckland() { + Services.prefs.setStringPref("calendar.timezone.local", "Pacific/Auckland"); + await subtest(); +}); + +/** + * Run the test at UTC+2. + */ +add_task(async function berlin() { + Services.prefs.setStringPref("calendar.timezone.local", "Europe/Berlin"); + await subtest(); +}); + +/** + * Run the test at UTC. + */ +add_task(async function utc() { + Services.prefs.setStringPref("calendar.timezone.local", "UTC"); + await subtest(); +}); + +/** + * Run the test at UTC-7. + */ +add_task(async function vancouver() { + Services.prefs.setStringPref("calendar.timezone.local", "America/Vancouver"); + await subtest(); +}); diff --git a/comm/calendar/test/browser/timezones/browser_timezones.js b/comm/calendar/test/browser/timezones/browser_timezones.js new file mode 100644 index 0000000000..41ce97027a --- /dev/null +++ b/comm/calendar/test/browser/timezones/browser_timezones.js @@ -0,0 +1,867 @@ +/* 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(3); + +var { findEventsInNode } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarUtils.jsm" +); +var { saveAndCloseItemDialog, setData } = ChromeUtils.import( + "resource://testing-common/calendar/ItemEditingHelpers.jsm" +); +var { CalendarTestUtils } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarTestUtils.jsm" +); + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); + +var DATES = [ + [2009, 1, 1], + [2009, 4, 2], + [2009, 4, 16], + [2009, 4, 30], + [2009, 7, 2], + [2009, 10, 15], + [2009, 10, 29], + [2009, 11, 5], +]; + +var TIMEZONES = [ + "America/St_Johns", + "America/Caracas", // standard time UTC-4:30 from 2007 to 2016 + "America/Phoenix", + "America/Los_Angeles", + "America/Buenos_Aires", // standard time UTC-3, DST UTC-4 from October 2008 to March 2009 + "Europe/Paris", + "Asia/Katmandu", + "Australia/Adelaide", +]; + +const calendarViewsInitialState = CalendarTestUtils.saveCalendarViewsState(window); + +add_setup(async () => { + registerCleanupFunction(async () => { + await CalendarTestUtils.restoreCalendarViewsState(window, calendarViewsInitialState); + Services.prefs.setStringPref("calendar.timezone.local", "UTC"); + }); + + let calendar = CalendarTestUtils.createCalendar(); + registerCleanupFunction(() => { + CalendarTestUtils.removeCalendar(calendar); + }); + + await CalendarTestUtils.setCalendarView(window, "day"); + await CalendarTestUtils.goToDate(window, 2009, 1, 1); + + // Create weekly recurring events in all TIMEZONES. + let times = [ + [4, 30], + [5, 0], + [3, 0], + [3, 0], + [9, 0], + [14, 0], + [19, 45], + [1, 30], + ]; + let time = cal.createDateTime(); + for (let i = 0; i < TIMEZONES.length; i++) { + let eventBox = CalendarTestUtils.dayView.getHourBoxAt(window, i + 11); + let { dialogWindow, iframeWindow } = await CalendarTestUtils.editNewEvent(window, eventBox); + time.hour = times[i][0]; + time.minute = times[i][1]; + + // Set event data. + await setData(dialogWindow, iframeWindow, { + title: TIMEZONES[i], + repeat: "weekly", + repeatuntil: cal.createDateTime("20091231T000000Z"), + starttime: time, + timezone: TIMEZONES[i], + }); + + await saveAndCloseItemDialog(dialogWindow); + } +}); + +add_task(async function testTimezones3_checkStJohns() { + Services.prefs.setStringPref("calendar.timezone.local", "America/St_Johns"); + let times = [ + [ + [4, 30], + [6, 0], + [6, 30], + [7, 30], + [7, 30], + [9, 30], + [10, 30], + [11, 30], + ], + [ + [4, 30], + [7, 0], + [7, 30], + [7, 30], + [9, 30], + [9, 30], + [11, 30], + [12, 30], + ], + [ + [4, 30], + [7, 0], + [7, 30], + [7, 30], + [9, 30], + [9, 30], + [11, 30], + [13, 30], + ], + [ + [4, 30], + [7, 0], + [7, 30], + [7, 30], + [9, 30], + [9, 30], + [11, 30], + [13, 30], + ], + [ + [4, 30], + [7, 0], + [7, 30], + [7, 30], + [9, 30], + [9, 30], + [11, 30], + [13, 30], + ], + [ + [4, 30], + [7, 0], + [7, 30], + [7, 30], + [9, 30], + [9, 30], + [11, 30], + [12, 30], + ], + [ + [4, 30], + [7, 0], + [7, 30], + [7, 30], + [9, 30], + [10, 30], + [11, 30], + [12, 30], + ], + [ + [4, 30], + [6, 0], + [6, 30], + [7, 30], + [8, 30], + [9, 30], + [10, 30], + [11, 30], + ], + ]; + EventUtils.synthesizeMouseAtCenter(document.getElementById("calendarButton"), {}, window); + await CalendarTestUtils.setCalendarView(window, "day"); + await CalendarTestUtils.goToDate(window, 2009, 1, 1); + + await verify(DATES, TIMEZONES, times); +}); + +add_task(async function testTimezones4_checkCaracas() { + Services.prefs.setStringPref("calendar.timezone.local", "America/Caracas"); + let times = [ + [ + [3, 30], + [5, 0], + [5, 30], + [6, 30], + [6, 30], + [8, 30], + [9, 30], + [10, 30], + ], + [ + [2, 30], + [5, 0], + [5, 30], + [5, 30], + [7, 30], + [7, 30], + [9, 30], + [10, 30], + ], + [ + [2, 30], + [5, 0], + [5, 30], + [5, 30], + [7, 30], + [7, 30], + [9, 30], + [11, 30], + ], + [ + [2, 30], + [5, 0], + [5, 30], + [5, 30], + [7, 30], + [7, 30], + [9, 30], + [11, 30], + ], + [ + [2, 30], + [5, 0], + [5, 30], + [5, 30], + [7, 30], + [7, 30], + [9, 30], + [11, 30], + ], + [ + [2, 30], + [5, 0], + [5, 30], + [5, 30], + [7, 30], + [7, 30], + [9, 30], + [10, 30], + ], + [ + [2, 30], + [5, 0], + [5, 30], + [5, 30], + [7, 30], + [8, 30], + [9, 30], + [10, 30], + ], + [ + [3, 30], + [5, 0], + [5, 30], + [6, 30], + [7, 30], + [8, 30], + [9, 30], + [10, 30], + ], + ]; + EventUtils.synthesizeMouseAtCenter(document.getElementById("calendarButton"), {}, window); + await CalendarTestUtils.setCalendarView(window, "day"); + await CalendarTestUtils.goToDate(window, 2009, 1, 1); + + await verify(DATES, TIMEZONES, times); +}); + +add_task(async function testTimezones5_checkPhoenix() { + Services.prefs.setStringPref("calendar.timezone.local", "America/Phoenix"); + let times = [ + [ + [1, 0], + [2, 30], + [3, 0], + [4, 0], + [4, 0], + [6, 0], + [7, 0], + [8, 0], + ], + [ + [0, 0], + [2, 30], + [3, 0], + [3, 0], + [5, 0], + [5, 0], + [7, 0], + [8, 0], + ], + [ + [0, 0], + [2, 30], + [3, 0], + [3, 0], + [5, 0], + [5, 0], + [7, 0], + [9, 0], + ], + [ + [0, 0], + [2, 30], + [3, 0], + [3, 0], + [5, 0], + [5, 0], + [7, 0], + [9, 0], + ], + [ + [0, 0], + [2, 30], + [3, 0], + [3, 0], + [5, 0], + [5, 0], + [7, 0], + [9, 0], + ], + [ + [0, 0], + [2, 30], + [3, 0], + [3, 0], + [5, 0], + [5, 0], + [7, 0], + [8, 0], + ], + [ + [0, 0], + [2, 30], + [3, 0], + [3, 0], + [5, 0], + [6, 0], + [7, 0], + [8, 0], + ], + [ + [1, 0], + [2, 30], + [3, 0], + [4, 0], + [5, 0], + [6, 0], + [7, 0], + [8, 0], + ], + ]; + EventUtils.synthesizeMouseAtCenter(document.getElementById("calendarButton"), {}, window); + await CalendarTestUtils.setCalendarView(window, "day"); + await CalendarTestUtils.goToDate(window, 2009, 1, 1); + + await verify(DATES, TIMEZONES, times); +}); + +add_task(async function testTimezones6_checkLosAngeles() { + Services.prefs.setStringPref("calendar.timezone.local", "America/Los_Angeles"); + let times = [ + [ + [0, 0], + [1, 30], + [2, 0], + [3, 0], + [3, 0], + [5, 0], + [6, 0], + [7, 0], + ], + [ + [0, 0], + [2, 30], + [3, 0], + [3, 0], + [5, 0], + [5, 0], + [7, 0], + [8, 0], + ], + [ + [0, 0], + [2, 30], + [3, 0], + [3, 0], + [5, 0], + [5, 0], + [7, 0], + [9, 0], + ], + [ + [0, 0], + [2, 30], + [3, 0], + [3, 0], + [5, 0], + [5, 0], + [7, 0], + [9, 0], + ], + [ + [0, 0], + [2, 30], + [3, 0], + [3, 0], + [5, 0], + [5, 0], + [7, 0], + [9, 0], + ], + [ + [0, 0], + [2, 30], + [3, 0], + [3, 0], + [5, 0], + [5, 0], + [7, 0], + [8, 0], + ], + [ + [0, 0], + [2, 30], + [3, 0], + [3, 0], + [5, 0], + [6, 0], + [7, 0], + [8, 0], + ], + [ + [0, 0], + [1, 30], + [2, 0], + [3, 0], + [4, 0], + [5, 0], + [6, 0], + [7, 0], + ], + ]; + EventUtils.synthesizeMouseAtCenter(document.getElementById("calendarButton"), {}, window); + await CalendarTestUtils.setCalendarView(window, "day"); + await CalendarTestUtils.goToDate(window, 2009, 1, 1); + + await verify(DATES, TIMEZONES, times); +}); + +add_task(async function testTimezones7_checkBuenosAires() { + Services.prefs.setStringPref("calendar.timezone.local", "America/Argentina/Buenos_Aires"); + let times = [ + [ + [6, 0], + [7, 30], + [8, 0], + [9, 0], + [9, 0], + [11, 0], + [12, 0], + [13, 0], + ], + [ + [4, 0], + [6, 30], + [7, 0], + [7, 0], + [9, 0], + [9, 0], + [11, 0], + [12, 0], + ], + [ + [4, 0], + [6, 30], + [7, 0], + [7, 0], + [9, 0], + [9, 0], + [11, 0], + [13, 0], + ], + [ + [4, 0], + [6, 30], + [7, 0], + [7, 0], + [9, 0], + [9, 0], + [11, 0], + [13, 0], + ], + [ + [4, 0], + [6, 30], + [7, 0], + [7, 0], + [9, 0], + [9, 0], + [11, 0], + [13, 0], + ], + [ + [4, 0], + [6, 30], + [7, 0], + [7, 0], + [9, 0], + [9, 0], + [11, 0], + [12, 0], + ], + [ + [4, 0], + [6, 30], + [7, 0], + [7, 0], + [9, 0], + [10, 0], + [11, 0], + [12, 0], + ], + [ + [5, 0], + [6, 30], + [7, 0], + [8, 0], + [9, 0], + [10, 0], + [11, 0], + [12, 0], + ], + ]; + EventUtils.synthesizeMouseAtCenter(document.getElementById("calendarButton"), {}, window); + await CalendarTestUtils.setCalendarView(window, "day"); + await CalendarTestUtils.goToDate(window, 2009, 1, 1); + + await verify(DATES, TIMEZONES, times); +}); + +add_task(async function testTimezones8_checkParis() { + Services.prefs.setStringPref("calendar.timezone.local", "Europe/Paris"); + let times = [ + [ + [9, 0], + [10, 30], + [11, 0], + [12, 0], + [12, 0], + [14, 0], + [15, 0], + [16, 0], + ], + [ + [9, 0], + [11, 30], + [12, 0], + [12, 0], + [14, 0], + [14, 0], + [16, 0], + [17, 0], + ], + [ + [9, 0], + [11, 30], + [12, 0], + [12, 0], + [14, 0], + [14, 0], + [16, 0], + [18, 0], + ], + [ + [9, 0], + [11, 30], + [12, 0], + [12, 0], + [14, 0], + [14, 0], + [16, 0], + [18, 0], + ], + [ + [9, 0], + [11, 30], + [12, 0], + [12, 0], + [14, 0], + [14, 0], + [16, 0], + [18, 0], + ], + [ + [9, 0], + [11, 30], + [12, 0], + [12, 0], + [14, 0], + [14, 0], + [16, 0], + [17, 0], + ], + [ + [8, 0], + [10, 30], + [11, 0], + [11, 0], + [13, 0], + [14, 0], + [15, 0], + [16, 0], + ], + [ + [9, 0], + [10, 30], + [11, 0], + [12, 0], + [13, 0], + [14, 0], + [15, 0], + [16, 0], + ], + ]; + EventUtils.synthesizeMouseAtCenter(document.getElementById("calendarButton"), {}, window); + await CalendarTestUtils.setCalendarView(window, "day"); + await CalendarTestUtils.goToDate(window, 2009, 1, 1); + + await verify(DATES, TIMEZONES, times); +}); + +add_task(async function testTimezones9_checkKathmandu() { + Services.prefs.setStringPref("calendar.timezone.local", "Asia/Kathmandu"); + let times = [ + [ + [13, 45], + [15, 15], + [15, 45], + [16, 45], + [16, 45], + [18, 45], + [19, 45], + [20, 45], + ], + [ + [12, 45], + [15, 15], + [15, 45], + [15, 45], + [17, 45], + [17, 45], + [19, 45], + [20, 45], + ], + [ + [12, 45], + [15, 15], + [15, 45], + [15, 45], + [17, 45], + [17, 45], + [19, 45], + [21, 45], + ], + [ + [12, 45], + [15, 15], + [15, 45], + [15, 45], + [17, 45], + [17, 45], + [19, 45], + [21, 45], + ], + [ + [12, 45], + [15, 15], + [15, 45], + [15, 45], + [17, 45], + [17, 45], + [19, 45], + [21, 45], + ], + [ + [12, 45], + [15, 15], + [15, 45], + [15, 45], + [17, 45], + [17, 45], + [19, 45], + [20, 45], + ], + [ + [12, 45], + [15, 15], + [15, 45], + [15, 45], + [17, 45], + [18, 45], + [19, 45], + [20, 45], + ], + [ + [13, 45], + [15, 15], + [15, 45], + [16, 45], + [17, 45], + [18, 45], + [19, 45], + [20, 45], + ], + ]; + EventUtils.synthesizeMouseAtCenter(document.getElementById("calendarButton"), {}, window); + await CalendarTestUtils.setCalendarView(window, "day"); + await CalendarTestUtils.goToDate(window, 2009, 1, 1); + + await verify(DATES, TIMEZONES, times); +}); + +add_task(async function testTimezones10_checkAdelaide() { + Services.prefs.setStringPref("calendar.timezone.local", "Australia/Adelaide"); + let times = [ + [ + [18, 30], + [20, 0], + [20, 30], + [21, 30], + [21, 30], + [23, 30], + [0, 30, +1], + [1, 30, +1], + ], + [ + [17, 30], + [20, 0], + [20, 30], + [20, 30], + [22, 30], + [22, 30], + [0, 30, +1], + [1, 30, +1], + ], + [ + [16, 30], + [19, 0], + [19, 30], + [19, 30], + [21, 30], + [21, 30], + [23, 30], + [1, 30, +1], + ], + [ + [16, 30], + [19, 0], + [19, 30], + [19, 30], + [21, 30], + [21, 30], + [23, 30], + [1, 30, +1], + ], + [ + [16, 30], + [19, 0], + [19, 30], + [19, 30], + [21, 30], + [21, 30], + [23, 30], + [1, 30, +1], + ], + [ + [17, 30], + [20, 0], + [20, 30], + [20, 30], + [22, 30], + [22, 30], + [0, 30, +1], + [1, 30, +1], + ], + [ + [17, 30], + [20, 0], + [20, 30], + [20, 30], + [22, 30], + [23, 30], + [0, 30, +1], + [1, 30, +1], + ], + [ + [18, 30], + [20, 0], + [20, 30], + [21, 30], + [22, 30], + [23, 30], + [0, 30, +1], + [1, 30, +1], + ], + ]; + EventUtils.synthesizeMouseAtCenter(document.getElementById("calendarButton"), {}, window); + await CalendarTestUtils.setCalendarView(window, "day"); + await CalendarTestUtils.goToDate(window, 2009, 1, 1); + + await verify(DATES, TIMEZONES, times); +}); + +async function verify(dates, timezones, times) { + function* datetimes() { + for (let idx = 0; idx < dates.length; idx++) { + yield [dates[idx][0], dates[idx][1], dates[idx][2], times[idx]]; + } + } + let allowedDifference = 3; + + for (let [selectedYear, selectedMonth, selectedDay, selectedTime] of datetimes()) { + info(`Verifying on day ${selectedDay}, month ${selectedMonth}, year ${selectedYear}`); + await CalendarTestUtils.goToDate(window, selectedYear, selectedMonth, selectedDay); + + // Find event with timezone tz. + for (let tzIdx = 0; tzIdx < timezones.length; tzIdx++) { + let [hour, minutes, day] = selectedTime[tzIdx]; + info( + `Verifying at ${hour} hours, ${minutes} minutes (offset: ${day || "none"}) ` + + `in timezone "${timezones[tzIdx]}"` + ); + + // following day + if (day == 1) { + await CalendarTestUtils.calendarViewForward(window, 1); + } else if (day == -1) { + await CalendarTestUtils.calendarViewBackward(window, 1); + } + + let hourRect = CalendarTestUtils.dayView.getHourBoxAt(window, hour).getBoundingClientRect(); + let timeY = hourRect.y + hourRect.height * (minutes / 60); + + // Wait for at least one event box to exist. + await CalendarTestUtils.dayView.waitForEventBoxAt(window, 1); + + let eventPositions = Array.from(CalendarTestUtils.dayView.getEventBoxes(window)) + .filter(node => node.mOccurrence.title == timezones[tzIdx]) + .map(node => node.getBoundingClientRect().y); + + dump(`Looking for event at ${timeY}: found ${eventPositions.join(", ")}\n`); + + if (day == 1) { + await CalendarTestUtils.calendarViewBackward(window, 1); + } else if (day == -1) { + await CalendarTestUtils.calendarViewForward(window, 1); + } + + Assert.ok( + eventPositions.some(pos => Math.abs(timeY - pos) < allowedDifference), + `There should be an event box that starts at ${hour} hours, ${minutes} minutes` + ); + } + } +} diff --git a/comm/calendar/test/browser/views/browser.ini b/comm/calendar/test/browser/views/browser.ini new file mode 100644 index 0000000000..0aae8af9d0 --- /dev/null +++ b/comm/calendar/test/browser/views/browser.ini @@ -0,0 +1,32 @@ +[default] +head = head.js +prefs = + calendar.item.promptDelete=false + calendar.timezone.local=UTC + calendar.timezone.useSystemTimezone=false + # Default start of the week Thursday to make sure calendar isn't relying on + # built-in assumptions of week start. + calendar.week.start=4 + # Default Sunday to "not a weekend" and Wednesday to "weekend" to make sure + # calendar isn't relying on built-in assumptions of work days. + calendar.week.d0sundaysoff=false + calendar.week.d3wednesdaysoff=true + # Default work hours to be from 3:00 to 12:00 to make sure calendar isn't + # relying on built-in assumptions of work hours. + calendar.view.daystarthour=3 + calendar.view.dayendhour=12 + calendar.view.visiblehours=3 + 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 + +[browser_dayView.js] +[browser_monthView.js] +[browser_multiweekView.js] +[browser_propertyChanges.js] +[browser_taskView.js] +[browser_viewSwitch.js] +[browser_weekView.js] diff --git a/comm/calendar/test/browser/views/browser_dayView.js b/comm/calendar/test/browser/views/browser_dayView.js new file mode 100644 index 0000000000..ba68a85eea --- /dev/null +++ b/comm/calendar/test/browser/views/browser_dayView.js @@ -0,0 +1,185 @@ +/* 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 { formatDate, formatTime, saveAndCloseItemDialog, setData } = ChromeUtils.import( + "resource://testing-common/calendar/ItemEditingHelpers.jsm" +); + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); + +const TITLE1 = "Day View Event"; +const TITLE2 = "Day View Event Changed"; +const DESC = "Day View Event Description"; + +add_setup(async function () { + let calendar = CalendarTestUtils.createCalendar(); + registerCleanupFunction(() => { + CalendarTestUtils.removeCalendar(calendar); + }); + + await CalendarTestUtils.setCalendarView(window, "day"); +}); + +add_task(async function testDayView() { + await CalendarTestUtils.goToDate(window, 2009, 1, 1); + + let dayView = document.getElementById("day-view"); + // Verify date in view. + await TestUtils.waitForCondition( + () => dayView.dayColumns[0]?.date.icalString == "20090101", + "Inspecting the date" + ); + + // Create event at 8 AM. + let eventBox = CalendarTestUtils.dayView.getHourBoxAt(window, 8); + let { dialogWindow, iframeWindow, iframeDocument } = await CalendarTestUtils.editNewEvent( + window, + eventBox + ); + + // Check that the start time is correct. + let someDate = cal.createDateTime(); + someDate.resetTo(2009, 0, 1, 8, 0, 0, cal.dtz.UTC); + + let startPicker = iframeDocument.getElementById("event-starttime"); + Assert.equal(startPicker._datepicker._inputField.value, formatDate(someDate)); + Assert.equal(startPicker._timepicker._inputField.value, formatTime(someDate)); + + // Fill in title, description and calendar. + await setData(dialogWindow, iframeWindow, { + title: TITLE1, + description: DESC, + calendar: "Test", + }); + + await saveAndCloseItemDialog(dialogWindow); + + // If it was created successfully, it can be opened. + ({ dialogWindow, iframeWindow } = await CalendarTestUtils.dayView.editEventAt(window, 1)); + await setData(dialogWindow, iframeWindow, { title: TITLE2 }); + await saveAndCloseItemDialog(dialogWindow); + + // Check if name was saved. + await TestUtils.waitForCondition(() => { + eventBox = CalendarTestUtils.dayView.getEventBoxAt(window, 1); + if (!eventBox) { + return false; + } + + let eventName = eventBox.querySelector(".event-name-label"); + return eventName.textContent == TITLE2; + }, "event was modified in the view"); + + // Delete event + EventUtils.synthesizeMouseAtCenter(eventBox, {}, window); + eventBox.focus(); + EventUtils.synthesizeKey("VK_DELETE", {}, window); + await CalendarTestUtils.dayView.waitForNoEventBoxAt(window, 1); + + Assert.ok(true, "Test ran to completion"); +}); + +add_task(async function testDayViewDateLabel() { + await CalendarTestUtils.goToDate(window, 2022, 4, 13); + + let heading = CalendarTestUtils.dayView.getColumnHeading(window); + let labelSpan = heading.querySelector("span:not([hidden])"); + + Assert.equal( + labelSpan.textContent, + "Wednesday Apr 13", + "the date label should contain the displayed date in a human-readable string" + ); +}); + +add_task(async function testDayViewCurrentDayHighlight() { + // Sanity check that this date (which should be in the past) is not today's + // date. + let today = new Date(); + Assert.ok(today.getUTCFullYear() != 2022 || today.getUTCMonth() != 3 || today.getUTCDate() != 13); + + // When displaying days which are not the current day, there should be no + // highlight. + await CalendarTestUtils.goToDate(window, 2022, 4, 13); + + let container = CalendarTestUtils.dayView.getColumnContainer(window); + Assert.ok( + !container.classList.contains("day-column-today"), + "the displayed date should not be highlighted if it is not the current day" + ); + + // When displaying the current day, it should be highlighted. + await CalendarTestUtils.goToToday(window); + + container = CalendarTestUtils.dayView.getColumnContainer(window); + Assert.ok( + container.classList.contains("day-column-today"), + "the displayed date should be highlighted if it is the current day" + ); +}); + +add_task(async function testDayViewWorkDayHighlight() { + // The test configuration sets Sunday to be a work day, so it should not have + // the weekend background. + await CalendarTestUtils.goToDate(window, 2022, 4, 10); + + let container = CalendarTestUtils.dayView.getColumnContainer(window); + Assert.ok( + !container.classList.contains("day-column-weekend"), + "the displayed date should not be highlighted if it is a work day" + ); + + await CalendarTestUtils.goToDate(window, 2022, 4, 13); + + container = CalendarTestUtils.dayView.getColumnContainer(window); + Assert.ok( + container.classList.contains("day-column-weekend"), + "the displayed date should be highlighted if it is not a work day" + ); +}); + +add_task(async function testDayViewNavbar() { + await CalendarTestUtils.goToDate(window, 2022, 4, 13); + + let intervalDescription = CalendarTestUtils.getNavBarIntervalDescription(window); + Assert.equal( + intervalDescription.textContent, + "Wednesday, April 13, 2022", + "interval description should contain a description of the displayed date" + ); + + // Note that the value 14 here tests calculation of the calendar week based on + // the starting day of the week; if the calculation built in an assumption of + // Sunday or Monday as the starting day of the week, we would get 15 here. + let calendarWeek = CalendarTestUtils.getNavBarCalendarWeekBox(window); + Assert.equal( + calendarWeek.textContent, + "CW: 14", + "calendar week label should contain an indicator of which week contains displayed date" + ); +}); + +add_task(async function testDayViewTodayButton() { + // Though this code is cribbed from the CalendarTestUtils, it should be + // duplicated in case the utility implementation changes. + let todayButton = CalendarTestUtils.getNavBarTodayButton(window); + + EventUtils.synthesizeMouseAtCenter(todayButton, {}, window); + await CalendarTestUtils.ensureViewLoaded(window); + + let displayedDate = CalendarTestUtils.dayView.getEventColumn(window).date; + + let today = new Date(); + Assert.equal( + displayedDate.year, + today.getUTCFullYear(), + "year of displayed date should be this year" + ); + Assert.equal( + displayedDate.month, + today.getUTCMonth(), + "month of displayed date should be this month" + ); + Assert.equal(displayedDate.day, today.getUTCDate(), "day of displayed date should be today"); +}); diff --git a/comm/calendar/test/browser/views/browser_monthView.js b/comm/calendar/test/browser/views/browser_monthView.js new file mode 100644 index 0000000000..f3a385a3f5 --- /dev/null +++ b/comm/calendar/test/browser/views/browser_monthView.js @@ -0,0 +1,86 @@ +/* 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 { formatDate, formatTime, saveAndCloseItemDialog, setData } = ChromeUtils.import( + "resource://testing-common/calendar/ItemEditingHelpers.jsm" +); + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); + +const TITLE1 = "Month View Event"; +const TITLE2 = "Month View Event Changed"; +const DESC = "Month View Event Description"; + +add_task(async function testMonthView() { + let calendar = CalendarTestUtils.createCalendar(); + registerCleanupFunction(() => { + CalendarTestUtils.removeCalendar(calendar); + }); + + await CalendarTestUtils.setCalendarView(window, "month"); + await CalendarTestUtils.goToDate(window, 2009, 1, 1); + + // Verify date. + await TestUtils.waitForCondition(() => { + let dateLabel = document.querySelector( + '#month-view td[selected="true"] > calendar-month-day-box' + ); + return dateLabel && dateLabel.mDate.icalString == "20090101"; + }, "Inspecting the date"); + + // Create event. + // Thursday of 2009-01-05 should be the selected box in the first row with default settings. + let hour = new Date().getUTCHours(); // Remember time at click. + let eventBox = CalendarTestUtils.monthView.getDayBox(window, 1, 5); + let { dialogWindow, iframeWindow, iframeDocument } = await CalendarTestUtils.editNewEvent( + window, + eventBox + ); + + // Check that the start time is correct. + // Next full hour except last hour hour of the day. + let nextHour = hour == 23 ? hour : (hour + 1) % 24; + let someDate = cal.dtz.now(); + someDate.resetTo(2009, 0, 5, nextHour, 0, 0, cal.dtz.UTC); + + let startPicker = iframeDocument.getElementById("event-starttime"); + Assert.equal(startPicker._datepicker._inputField.value, formatDate(someDate)); + Assert.equal(startPicker._timepicker._inputField.value, formatTime(someDate)); + + // Fill in title, description and calendar. + await setData(dialogWindow, iframeWindow, { + title: TITLE1, + description: DESC, + calendar: "Test", + }); + + await saveAndCloseItemDialog(dialogWindow); + + // If it was created successfully, it can be opened. + ({ dialogWindow, iframeWindow } = await CalendarTestUtils.monthView.editItemAt(window, 1, 5, 1)); + // Change title and save changes. + await setData(dialogWindow, iframeWindow, { title: TITLE2 }); + await saveAndCloseItemDialog(dialogWindow); + + // Check if name was saved. + let eventName; + await TestUtils.waitForCondition(() => { + eventBox = CalendarTestUtils.monthView.getItemAt(window, 1, 5, 1); + if (!eventBox) { + return false; + } + eventName = eventBox.querySelector(".event-name-label").textContent; + return eventName == TITLE2; + }, "event name did not update in time"); + + Assert.equal(eventName, TITLE2); + + // Delete event. + EventUtils.synthesizeMouseAtCenter(eventBox, {}, window); + eventBox.focus(); + EventUtils.synthesizeKey("VK_DELETE", {}, window); + await CalendarTestUtils.monthView.waitForNoItemAt(window, 1, 5, 1); + + Assert.ok(true, "Test ran to completion"); +}); diff --git a/comm/calendar/test/browser/views/browser_multiweekView.js b/comm/calendar/test/browser/views/browser_multiweekView.js new file mode 100644 index 0000000000..feb8fcd3ec --- /dev/null +++ b/comm/calendar/test/browser/views/browser_multiweekView.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 { formatDate, formatTime, saveAndCloseItemDialog, setData } = ChromeUtils.import( + "resource://testing-common/calendar/ItemEditingHelpers.jsm" +); + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); + +const TITLE1 = "Multiweek View Event"; +const TITLE2 = "Multiweek View Event Changed"; +const DESC = "Multiweek View Event Description"; + +add_task(async function () { + let calendar = CalendarTestUtils.createCalendar(); + registerCleanupFunction(() => { + CalendarTestUtils.removeCalendar(calendar); + }); + + await CalendarTestUtils.setCalendarView(window, "multiweek"); + await CalendarTestUtils.goToDate(window, 2009, 1, 1); + + // Verify date. + await TestUtils.waitForCondition(() => { + let dateLabel = document.querySelector( + '#multiweek-view td[selected="true"] > calendar-month-day-box' + ); + return dateLabel && dateLabel.mDate.icalString == "20090101"; + }, "Inspecting the date"); + + // Create event. + // Thursday of 2009-01-05 should be the selected box in the first row with default settings. + let hour = new Date().getUTCHours(); // Remember time at click. + let eventBox = CalendarTestUtils.multiweekView.getDayBox(window, 1, 5); + let { dialogWindow, iframeWindow, iframeDocument } = await CalendarTestUtils.editNewEvent( + window, + eventBox + ); + + // Check that the start time is correct. + // Next full hour except last hour hour of the day. + let nextHour = hour == 23 ? hour : (hour + 1) % 24; + let someDate = cal.dtz.now(); + someDate.resetTo(2009, 0, 5, nextHour, 0, 0, cal.dtz.UTC); + + let startPicker = iframeDocument.getElementById("event-starttime"); + Assert.equal(startPicker._datepicker._inputField.value, formatDate(someDate)); + Assert.equal(startPicker._timepicker._inputField.value, formatTime(someDate)); + + // Fill in title, description and calendar. + await setData(dialogWindow, iframeWindow, { + title: TITLE1, + description: DESC, + calendar: "Test", + }); + + await saveAndCloseItemDialog(dialogWindow); + + // If it was created successfully, it can be opened. + ({ dialogWindow, iframeWindow } = await CalendarTestUtils.multiweekView.editItemAt( + window, + 1, + 5, + 1 + )); + // Change title and save changes. + await setData(dialogWindow, iframeWindow, { title: TITLE2 }); + await saveAndCloseItemDialog(dialogWindow); + + // Check if name was saved. + await TestUtils.waitForCondition(() => { + eventBox = CalendarTestUtils.multiweekView.getItemAt(window, 1, 5, 1); + if (eventBox === null) { + return false; + } + let eventName = eventBox.querySelector(".event-name-label"); + return eventName && eventName.textContent == TITLE2; + }, "Wait for the new title"); + + // Delete event. + EventUtils.synthesizeMouseAtCenter(eventBox, {}, window); + eventBox.focus(); + EventUtils.synthesizeKey("VK_DELETE", {}, window); + await CalendarTestUtils.multiweekView.waitForNoItemAt(window, 1, 5, 1); + + Assert.ok(true, "Test ran to completion"); +}); diff --git a/comm/calendar/test/browser/views/browser_propertyChanges.js b/comm/calendar/test/browser/views/browser_propertyChanges.js new file mode 100644 index 0000000000..79848a0e73 --- /dev/null +++ b/comm/calendar/test/browser/views/browser_propertyChanges.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/. */ + +/** Tests that changes in a calendar's properties are reflected in the current view. */ + +const { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); +const { CalEvent } = ChromeUtils.import("resource:///modules/CalEvent.jsm"); + +let composite = cal.view.getCompositeCalendar(window); + +// This is the calendar we're going to change the properties of. +let thisCalendar = CalendarTestUtils.createCalendar("This Calendar", "memory"); +thisCalendar.setProperty("color", "#ffee22"); + +// This calendar isn't going to change, and we'll check it doesn't. +let notThisCalendar = CalendarTestUtils.createCalendar("Not This Calendar", "memory"); +notThisCalendar.setProperty("color", "#dd3333"); + +add_setup(async function () { + let { dedent } = CalendarTestUtils; + await thisCalendar.addItem( + new CalEvent(dedent` + BEGIN:VEVENT + SUMMARY:This Event 1 + DTSTART;VALUE=DATE:20160205 + DTEND;VALUE=DATE:20160206 + END:VEVENT + `) + ); + await thisCalendar.addItem( + new CalEvent(dedent` + BEGIN:VEVENT + SUMMARY:This Event 2 + DTSTART:20160205T130000Z + DTEND:20160205T150000Z + END:VEVENT + `) + ); + await thisCalendar.addItem( + new CalEvent(dedent` + BEGIN:VEVENT + SUMMARY:This Event 3 + DTSTART;VALUE=DATE:20160208 + DTEND;VALUE=DATE:20160209 + RRULE:FREQ=DAILY;INTERVAL=2;COUNT=3 + END:VEVENT + `) + ); + + await notThisCalendar.addItem( + new CalEvent(dedent` + BEGIN:VEVENT + SUMMARY:Not This Event 1 + DTSTART;VALUE=DATE:20160205 + DTEND;VALUE=DATE:20160207 + END:VEVENT + `) + ); + await notThisCalendar.addItem( + new CalEvent(dedent` + BEGIN:VEVENT + SUMMARY:Not This Event 2 + DTSTART:20160205T140000Z + DTEND:20160205T170000Z + END:VEVENT + `) + ); +}); + +/** + * Assert whether the given event box is draggable (editable). + * + * @param {MozCalendarEventBox} eventBox - The event box to test. + * @param {boolean} draggable - Whether we expect it to be draggable. + * @param {string} message - A message for assertions. + */ +async function assertCanDrag(eventBox, draggable, message) { + // Hover to see if the drag gripbars appear. + let enterPromise = BrowserTestUtils.waitForEvent(eventBox, "mouseenter"); + EventUtils.synthesizeMouseAtCenter(eventBox, { type: "mouseover" }, window); + await enterPromise; + Assert.equal( + BrowserTestUtils.is_visible(eventBox.startGripbar), + draggable, + `Start gripbar should be ${draggable ? "visible" : "hidden"} on hover: ${message}` + ); + Assert.equal( + BrowserTestUtils.is_visible(eventBox.endGripbar), + draggable, + `End gripbar should be ${draggable ? "visible" : "hidden"} on hover: ${message}` + ); +} + +/** + * Assert whether the given event element is editable. + * + * @param {Element} eventElement - The event element to test. + * @param {boolean} editable - Whether we expect it to be editable. + * @param {string} message - A message for assertions. + */ +async function assertEditable(eventElement, editable, message) { + // FIXME: Have more ways to test if an event is editable (e.g. test the + // context menu) + if (eventElement.matches("calendar-event-box")) { + await CalendarTestUtils.assertEventBoxDraggable(eventElement, editable, editable, message); + } +} + +async function subTest(viewName, boxSelector, thisBoxCount, notThisBoxCount) { + async function makeChangeWithReload(changeFunction) { + await changeFunction(); + await CalendarTestUtils.ensureViewLoaded(window); + } + + async function checkBoxItems(expectedCount, checkFunction) { + await TestUtils.waitForCondition( + () => view.querySelectorAll(boxSelector).length == expectedCount, + "waiting for the correct number of boxes to be displayed" + ); + let boxItems = view.querySelectorAll(boxSelector); + + if (!checkFunction) { + return; + } + + for (let boxItem of boxItems) { + // TODO: why is it named `item` in some places and `occurrence` elsewhere? + let isThisCalendar = + (boxItem.item && boxItem.item.calendar == thisCalendar) || + boxItem.occurrence.calendar == thisCalendar; + await checkFunction(boxItem, isThisCalendar); + } + } + + let view = document.getElementById(`${viewName}-view`); + + await CalendarTestUtils.setCalendarView(window, viewName); + await CalendarTestUtils.goToDate(window, 2016, 2, 5); + + info("Check initial state."); + + await checkBoxItems(thisBoxCount + notThisBoxCount, async (boxItem, isThisCalendar) => { + let style = getComputedStyle(boxItem); + + if (isThisCalendar) { + Assert.equal(style.backgroundColor, "rgb(255, 238, 34)", "item background correct"); + Assert.equal(style.color, "rgb(34, 34, 34)", "item foreground correct"); + } else { + Assert.equal( + style.backgroundColor, + "rgb(221, 51, 51)", + "item background correct (not target calendar)" + ); + Assert.equal( + style.color, + "rgb(255, 255, 255)", + "item foreground correct (not target calendar)" + ); + } + await assertEditable(boxItem, true, "Initial event"); + }); + + info("Change color."); + + thisCalendar.setProperty("color", "#16a765"); + await checkBoxItems(thisBoxCount + notThisBoxCount, async (boxItem, isThisCalendar) => { + let style = getComputedStyle(boxItem); + + if (isThisCalendar) { + Assert.equal(style.backgroundColor, "rgb(22, 167, 101)", "item background correct"); + Assert.equal(style.color, "rgb(255, 255, 255)", "item foreground correct"); + } else { + Assert.equal( + style.backgroundColor, + "rgb(221, 51, 51)", + "item background correct (not target calendar)" + ); + Assert.equal( + style.color, + "rgb(255, 255, 255)", + "item foreground correct (not target calendar)" + ); + } + }); + + info("Reset color."); + thisCalendar.setProperty("color", "#ffee22"); + + info("Disable."); + + thisCalendar.setProperty("disabled", true); + await checkBoxItems(notThisBoxCount); + + info("Enable."); + + await makeChangeWithReload(() => thisCalendar.setProperty("disabled", false)); + await checkBoxItems(thisBoxCount + notThisBoxCount); + + info("Hide."); + + composite.removeCalendar(thisCalendar); + await checkBoxItems(notThisBoxCount); + + info("Show."); + + await makeChangeWithReload(() => composite.addCalendar(thisCalendar)); + await checkBoxItems(thisBoxCount + notThisBoxCount); + + info("Set read-only."); + + await makeChangeWithReload(() => thisCalendar.setProperty("readOnly", true)); + await checkBoxItems(thisBoxCount + notThisBoxCount, async (boxItem, isThisCalendar) => { + if (isThisCalendar) { + await assertEditable(boxItem, false, "In readonly calendar"); + } else { + await assertEditable(boxItem, true, "In non-readonly calendar"); + } + }); + + info("Clear read-only."); + + await makeChangeWithReload(() => thisCalendar.setProperty("readOnly", false)); + await checkBoxItems(thisBoxCount + notThisBoxCount, async boxItem => { + await assertEditable(boxItem, true, "In non-readonly calendar after clearing"); + }); +} + +add_task(async function testMonthView() { + await subTest("month", "calendar-month-day-box-item", 5, 3); +}); + +add_task(async function testMultiWeekView() { + await subTest("multiweek", "calendar-month-day-box-item", 5, 3); +}); + +add_task(async function testWeekView() { + await subTest("week", "calendar-editable-item, .multiday-events-list calendar-event-box", 4, 3); +}); + +add_task(async function testDayView() { + await subTest("day", "calendar-editable-item, .multiday-events-list calendar-event-box", 2, 2); +}); + +registerCleanupFunction(async () => { + CalendarTestUtils.removeCalendar(thisCalendar); + CalendarTestUtils.removeCalendar(notThisCalendar); +}); diff --git a/comm/calendar/test/browser/views/browser_taskView.js b/comm/calendar/test/browser/views/browser_taskView.js new file mode 100644 index 0000000000..c049c9668f --- /dev/null +++ b/comm/calendar/test/browser/views/browser_taskView.js @@ -0,0 +1,148 @@ +/* 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 { MID_SLEEP, execEventDialogCallback } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarUtils.jsm" +); +var { saveAndCloseItemDialog, setData } = ChromeUtils.import( + "resource://testing-common/calendar/ItemEditingHelpers.jsm" +); + +const TITLE = "Task"; +const DESCRIPTION = "1. Do A\n2. Do B"; +const PERCENTCOMPLETE = "50"; + +add_task(async function () { + let calendar = CalendarTestUtils.createCalendar(); + registerCleanupFunction(() => { + CalendarTestUtils.removeCalendar(calendar); + }); + + // Open task view. + EventUtils.synthesizeMouseAtCenter(document.getElementById("tasksButton"), {}, window); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, MID_SLEEP)); + + // Make sure that testing calendar is selected. + let calList = document.querySelector(`#calendar-list > [calendar-id="${calendar.id}"]`); + Assert.ok(calList); + EventUtils.synthesizeMouseAtCenter(calList, {}, window); + + let taskTreeNode = document.getElementById("calendar-task-tree"); + Assert.equal(taskTreeNode.mTaskArray.length, 0); + + // Add task. + let taskInput = document.getElementById("view-task-edit-field"); + taskInput.focus(); + EventUtils.sendString(TITLE, window); + EventUtils.synthesizeKey("VK_RETURN", {}, window); + + // Verify added. + await TestUtils.waitForCondition( + () => taskTreeNode.mTaskArray.length == 1, + "Added Task did not appear" + ); + + // Last added task is automatically selected so verify detail window data. + Assert.equal(document.getElementById("calendar-task-details-title").textContent, TITLE); + + // Open added task + // Double-click on completion checkbox is ignored as opening action, so don't + // click at immediate left where the checkbox is located. + let eventWindowPromise = CalendarTestUtils.waitForEventDialog("edit"); + let treeChildren = document.querySelector("#calendar-task-tree .calendar-task-treechildren"); + Assert.ok(treeChildren); + EventUtils.synthesizeMouse(treeChildren, 50, 0, { clickCount: 2 }, window); + + await eventWindowPromise; + await execEventDialogCallback(async (taskWindow, iframeWindow) => { + // Verify calendar. + Assert.equal(iframeWindow.document.getElementById("item-calendar").value, "Test"); + + await setData(taskWindow, iframeWindow, { + status: "needs-action", + percent: PERCENTCOMPLETE, + description: DESCRIPTION, + }); + + await saveAndCloseItemDialog(taskWindow); + }); + + Assert.less(taskTreeNode.mTaskArray.length, 2, "Should not have added task"); + Assert.greater(taskTreeNode.mTaskArray.length, 0, "Should not have removed task"); + + // Verify description and status in details pane. + await TestUtils.waitForCondition(() => { + let desc = document.getElementById("calendar-task-details-description"); + return desc && desc.contentDocument.body.innerText == DESCRIPTION; + }, "Calendar task description"); + Assert.equal(document.getElementById("calendar-task-details-status").textContent, "Needs Action"); + + // This is a hack. + taskTreeNode.getTaskAtRow(0).calendar.setProperty("capabilities.priority.supported", true); + + // Set high priority and verify it in detail pane. + EventUtils.synthesizeMouseAtCenter(document.getElementById("task-actions-priority"), {}, window); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, MID_SLEEP)); + + let priorityMenu = document.querySelector( + "#task-actions-priority-menupopup > .priority-1-menuitem" + ); + Assert.ok(priorityMenu); + EventUtils.synthesizeMouseAtCenter(priorityMenu, {}, window); + await TestUtils.waitForCondition( + () => !document.getElementById("calendar-task-details-priority-high").hidden, + "#calendar-task-details-priority-high did not show" + ); + + // Verify that tooltip shows status, priority and percent complete. + let toolTipNode = document.getElementById("taskTreeTooltip"); + toolTipNode.ownerGlobal.showToolTip(toolTipNode, taskTreeNode.getTaskAtRow(0)); + + function getTooltipDescription(index) { + return toolTipNode.querySelector( + `.tooltipHeaderTable > tr:nth-of-type(${index}) > .tooltipHeaderDescription` + ).textContent; + } + + // Name + Assert.equal(getTooltipDescription(1), TITLE); + // Calendar + Assert.equal(getTooltipDescription(2), "Test"); + // Priority + Assert.equal(getTooltipDescription(3), "High"); + // Status + Assert.equal(getTooltipDescription(4), "Needs Action"); + // Complete + Assert.equal(getTooltipDescription(5), PERCENTCOMPLETE + "%"); + + // Mark completed, verify. + EventUtils.synthesizeMouseAtCenter( + document.getElementById("task-actions-markcompleted"), + {}, + window + ); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, MID_SLEEP)); + + toolTipNode.ownerGlobal.showToolTip(toolTipNode, taskTreeNode.getTaskAtRow(0)); + Assert.equal(getTooltipDescription(4), "Completed"); + + // Delete task and verify. + EventUtils.synthesizeMouseAtCenter( + document.getElementById("calendar-delete-task-button"), + {}, + window + ); + await TestUtils.waitForCondition( + () => taskTreeNode.mTaskArray.length == 0, + "Task did not delete" + ); + + let tabmail = document.getElementById("tabmail"); + tabmail.closeTab(tabmail.currentTabInfo); + + Assert.ok(true, "Test ran to completion"); +}); diff --git a/comm/calendar/test/browser/views/browser_viewSwitch.js b/comm/calendar/test/browser/views/browser_viewSwitch.js new file mode 100644 index 0000000000..e730f3d797 --- /dev/null +++ b/comm/calendar/test/browser/views/browser_viewSwitch.js @@ -0,0 +1,138 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests that the time indicator is restarted and scroll position is restored + * when switching tabs or views. + */ + +/** + * Wait until the view's timebar shows the given number of visible hours. + * + * @param {CalendarMultidayBaseView} view - The calendar view. + * @param {number} numHours - The expected number of visible hours. + * + * @returns {Promise} - Promise that resolves when the timebar has numHours + * visible hours. + */ +function waitForVisibleHours(view, numHours) { + // The timebar is the only scrollable child in its column (the others are + // sticky), so the difference between the scroll area's scrollTopMax and the + // timebar's clientHeight should give us the visible height. + return TestUtils.waitForCondition(() => { + let timebarHeight = view.timebar.clientHeight; + let visiblePx = timebarHeight - view.grid.scrollTopMax; + let expectPx = (numHours / 24) * timebarHeight; + // Allow up to 3px difference to accommodate accumulated integer rounding + // errors (e.g. clientHeight is a rounded integer, whilst client rectangles + // and expectPx are floating). + return Math.abs(visiblePx - expectPx) < 3; + }, `${view.id} should have ${numHours} hours visible`); +} + +/** + * Wait until the view's timebar's first visible hour is the given hour. + * + * @param {CalendarMultidayBaseView} view - The calendar view. + * @param {number} hour - The expected first visible hour. + * + * @returns {Promise} - Promise that resolves when the timebar has the given + * first visible hour. + */ +function waitForFirstVisibleHour(view, hour) { + return TestUtils.waitForCondition(() => { + let expectPx = (hour / 24) * view.timebar.clientHeight; + let actualPx = view.grid.scrollTop; + return Math.abs(actualPx - expectPx) < 3; + }, `${view.id} first visible hour should be ${hour}`); +} + +/** + * Perform a scroll on the view by one hour. + * + * @param {CalendarMultidayBaseView} view - The calendar view to scroll. + * @param {boolean} scrollDown - Whether to scroll down, otherwise scrolls up. + */ +async function doScroll(view, scrollDown) { + let scrollPromise = BrowserTestUtils.waitForEvent(view.grid, "scroll"); + let viewRect = view.getBoundingClientRect(); + EventUtils.synthesizeWheel( + view.grid, + viewRect.width / 2, + viewRect.height / 2, + { deltaY: scrollDown ? 1 : -1, deltaMode: WheelEvent.DOM_DELTA_LINE }, + window + ); + await scrollPromise; +} + +add_task(async function () { + let expectedVisibleHours = 3; + let expectedStartHour = 3; + + let tabmail = document.getElementById("tabmail"); + Assert.equal(tabmail.tabInfo.length, 1); + + Assert.equal(Services.prefs.getIntPref("calendar.view.daystarthour"), expectedStartHour); + Assert.equal(Services.prefs.getIntPref("calendar.view.dayendhour"), 12); + Assert.equal(Services.prefs.getIntPref("calendar.view.visiblehours"), expectedVisibleHours); + + // Open the day view, check the display matches the prefs. + + await CalendarTestUtils.setCalendarView(window, "day"); + + let dayView = document.getElementById("day-view"); + + await waitForFirstVisibleHour(dayView, expectedStartHour); + await waitForVisibleHours(dayView, expectedVisibleHours); + + // Scroll down 3 hours. We'll check this scroll position later. + await doScroll(dayView, true); + await waitForFirstVisibleHour(dayView, expectedStartHour + 1); + + await doScroll(dayView, true); + await doScroll(dayView, true); + await waitForFirstVisibleHour(dayView, expectedStartHour + 3); + await waitForVisibleHours(dayView, expectedVisibleHours); + + // Open the week view, check the display matches the prefs. + + await CalendarTestUtils.setCalendarView(window, "week"); + + let weekView = document.getElementById("week-view"); + + await waitForFirstVisibleHour(weekView, expectedStartHour); + await waitForVisibleHours(weekView, expectedVisibleHours); + + // Scroll up 1 hour. + await doScroll(weekView, false); + await waitForFirstVisibleHour(weekView, expectedStartHour - 1); + await waitForVisibleHours(weekView, expectedVisibleHours); + + // Go back to the day view, check the timer and scroll position. + + await CalendarTestUtils.setCalendarView(window, "day"); + + await waitForFirstVisibleHour(dayView, expectedStartHour + 3); + await waitForVisibleHours(dayView, expectedVisibleHours); + + // Switch away from the calendar tab. + + tabmail.switchToTab(0); + + // Switch back to the calendar tab. Check scroll position. + + tabmail.switchToTab(1); + Assert.equal(window.currentView().id, "day-view"); + + await waitForFirstVisibleHour(dayView, expectedStartHour + 3); + await waitForVisibleHours(dayView, expectedVisibleHours); + + // Go back to the week view. Check scroll position. + + await CalendarTestUtils.setCalendarView(window, "week"); + + await waitForFirstVisibleHour(weekView, expectedStartHour - 1); + await waitForVisibleHours(weekView, expectedVisibleHours); +}); diff --git a/comm/calendar/test/browser/views/browser_weekView.js b/comm/calendar/test/browser/views/browser_weekView.js new file mode 100644 index 0000000000..0835da2f23 --- /dev/null +++ b/comm/calendar/test/browser/views/browser_weekView.js @@ -0,0 +1,81 @@ +/* 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 { formatDate, formatTime, saveAndCloseItemDialog, setData } = ChromeUtils.import( + "resource://testing-common/calendar/ItemEditingHelpers.jsm" +); + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); + +var TITLE1 = "Week View Event"; +var TITLE2 = "Week View Event Changed"; +var DESC = "Week View Event Description"; + +add_task(async function testWeekView() { + let calendar = CalendarTestUtils.createCalendar(); + registerCleanupFunction(() => { + CalendarTestUtils.removeCalendar(calendar); + }); + + await CalendarTestUtils.setCalendarView(window, "week"); + await CalendarTestUtils.goToDate(window, 2009, 1, 1); + + // Verify date. + await TestUtils.waitForCondition(() => { + let dateLabel = document.querySelector("#week-view .day-column-selected calendar-event-column"); + return dateLabel?.date.icalString == "20090101"; + }, "Date is selected"); + + // Create event at 8 AM. + // Thursday of 2009-01-05 is 4th with default settings. + let eventBox = CalendarTestUtils.weekView.getHourBoxAt(window, 5, 8); + let { dialogWindow, iframeWindow, iframeDocument } = await CalendarTestUtils.editNewEvent( + window, + eventBox + ); + + // Check that the start time is correct. + let someDate = cal.createDateTime(); + someDate.resetTo(2009, 0, 5, 8, 0, 0, cal.dtz.UTC); + + let startPicker = iframeDocument.getElementById("event-starttime"); + Assert.equal(startPicker._datepicker._inputField.value, formatDate(someDate)); + Assert.equal(startPicker._timepicker._inputField.value, formatTime(someDate)); + + // Fill in title, description and calendar. + await setData(dialogWindow, iframeWindow, { + title: TITLE1, + description: DESC, + calendar: "Test", + }); + + await saveAndCloseItemDialog(dialogWindow); + + // If it was created successfully, it can be opened. + ({ dialogWindow, iframeWindow } = await CalendarTestUtils.weekView.editEventAt(window, 5, 1)); + // Change title and save changes. + await setData(dialogWindow, iframeWindow, { title: TITLE2 }); + await saveAndCloseItemDialog(dialogWindow); + + // Check if name was saved. + let eventName; + await TestUtils.waitForCondition(() => { + eventBox = CalendarTestUtils.weekView.getEventBoxAt(window, 5, 1); + if (!eventBox) { + return false; + } + eventName = eventBox.querySelector(".event-name-label").textContent; + return eventName == TITLE2; + }, "event name did not update in time"); + + Assert.equal(eventName, TITLE2); + + // Delete event. + EventUtils.synthesizeMouseAtCenter(eventBox, {}, window); + eventBox.focus(); + EventUtils.synthesizeKey("VK_DELETE", {}, window); + await CalendarTestUtils.weekView.waitForNoEventBoxAt(window, 5, 1); + + Assert.ok(true, "Test ran to completion"); +}); diff --git a/comm/calendar/test/browser/views/head.js b/comm/calendar/test/browser/views/head.js new file mode 100644 index 0000000000..c0f924d9b5 --- /dev/null +++ b/comm/calendar/test/browser/views/head.js @@ -0,0 +1,13 @@ +/* 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 { CalendarTestUtils } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarTestUtils.jsm" +); + +const calendarViewsInitialState = CalendarTestUtils.saveCalendarViewsState(window); + +registerCleanupFunction(async () => { + await CalendarTestUtils.restoreCalendarViewsState(window, calendarViewsInitialState); +}); |