diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/calendar/test/CalendarTestUtils.jsm | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/calendar/test/CalendarTestUtils.jsm')
-rw-r--r-- | comm/calendar/test/CalendarTestUtils.jsm | 1203 |
1 files changed, 1203 insertions, 0 deletions
diff --git a/comm/calendar/test/CalendarTestUtils.jsm b/comm/calendar/test/CalendarTestUtils.jsm new file mode 100644 index 0000000000..42e587f540 --- /dev/null +++ b/comm/calendar/test/CalendarTestUtils.jsm @@ -0,0 +1,1203 @@ +/* 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"; + +const EXPORTED_SYMBOLS = ["CalendarTestUtils"]; + +const EventUtils = ChromeUtils.import("resource://testing-common/mozmill/EventUtils.jsm"); +const { BrowserTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/BrowserTestUtils.sys.mjs" +); +const { TestUtils } = ChromeUtils.importESModule("resource://testing-common/TestUtils.sys.mjs"); +const { Assert } = ChromeUtils.importESModule("resource://testing-common/Assert.sys.mjs"); +const { cancelItemDialog, saveAndCloseItemDialog, setData } = ChromeUtils.import( + "resource://testing-common/calendar/ItemEditingHelpers.jsm" +); + +const { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); + +async function clickAndWait(win, button) { + EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, win); + await new Promise(resolve => win.setTimeout(resolve)); +} + +/** + * @typedef EditItemAtResult + * @property {Window} dialogWindow - The window of the dialog. + * @property {Document} dialogDocument - The document of the dialog window. + * @property {Window} iframeWindow - The contentWindow property of the embedded + * iframe. + * @property {Document} iframeDocument - The contentDocument of the embedded + * iframe. + */ + +/** + * Helper class for testing the day view of the calendar. + */ +class CalendarDayViewTestUtils { + #helper = new CalendarWeekViewTestUtils("#day-view"); + + /** + * Provides the column container element for the displayed day. + * + * @param {Window} win - The window the calendar is displayed in. + * + * @returns {HTMLElement} - The column container element. + */ + getColumnContainer(win) { + return this.#helper.getColumnContainer(win, 1); + } + + /** + * Provides the element containing the formatted date for the displayed day. + * + * @param {Window} win - The window the calendar is displayed in. + * + * @returns {HTMLElement} - The column heading container. + */ + getColumnHeading(win) { + return this.#helper.getColumnHeading(win, 1); + } + + /** + * Provides the calendar-event-column for the day displayed. + * + * @param {Window} win - The window the calendar is displayed in. + * + * @returns {MozCalendarEventColumn} - The column. + */ + getEventColumn(win) { + return this.#helper.getEventColumn(win, 1); + } + + /** + * Provides the calendar-event-box elements for the day. + * + * @param {Window} win - The window the calendar is displayed in. + * + * @returns {MozCalendarEventBox[]} - The event boxes. + */ + getEventBoxes(win) { + return this.#helper.getEventBoxes(win, 1); + } + + /** + * Provides the calendar-event-box at "index" located in the event column for + * the day displayed. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} index - Indicates which event box to select. + * + * @returns {MozCalendarEventBox|undefined} - The event box, if it exists. + */ + getEventBoxAt(win, index) { + return this.#helper.getEventBoxAt(win, 1, index); + } + + /** + * Provides the .multiday-hour-box element for the specified hour. This + * element can be double clicked to create a new event at that hour. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} hour - Must be between 0-23. + * + * @returns {XULElement} - The hour box. + */ + getHourBoxAt(win, hour) { + return this.#helper.getHourBoxAt(win, 1, hour); + } + + /** + * Provides the all-day header, which can be double clicked to create a new + * all-day event. + * + * @param {Window} win - The window the calendar is displayed in. + * + * @returns {CalendarHeaderContainer} - The all-day header. + */ + getAllDayHeader(win) { + return this.#helper.getAllDayHeader(win, 1); + } + + /** + * Provides the all-day calendar-editable-item located at index for the + * current day. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} index - Indicates which item to select (1-based). + * + * @returns {MozCalendarEditableItem|undefined} - The all-day item, if it + * exists. + */ + getAllDayItemAt(win, index) { + return this.#helper.getAllDayItemAt(win, 1, index); + } + + /** + * Waits for the calendar-event-box at "index", located in the event + * column for the day displayed to appear. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} index - Indicates which item to select (1-based). + * + * @returns {Promise<MozCalendarEventBox>} - The event box. + */ + async waitForEventBoxAt(win, index) { + return this.#helper.waitForEventBoxAt(win, 1, index); + } + + /** + * Waits for the calendar-event-box at "index", located in the event column + * for the current day to disappear. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} index - Indicates the event box (1-based). + */ + async waitForNoEventBoxAt(win, index) { + return this.#helper.waitForNoEventBoxAt(win, 1, index); + } + + /** + * Wait for the all-day calendar-editable-item for the day. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} index - Indicates which item to select (1-based). + * + * @returns {Promise<MozCalendarEditableItem>} - The all-day item. + */ + async waitForAllDayItemAt(win, index) { + return this.#helper.waitForAllDayItemAt(win, 1, index); + } + + /** + * Opens the event dialog for viewing for the event box located at the + * specified index. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} index - Indicates which event to select. + * + * @returns {Promise<Window>} - The summary event dialog window. + */ + async viewEventAt(win, index) { + return this.#helper.viewEventAt(win, 1, index); + } + + /** + * Opens the event dialog for editing for the event box located at the + * specified index. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} index - Indicates which event to select. + * + * @returns {Promise<EditItemAtResult>} + */ + async editEventAt(win, index) { + return this.#helper.editEventAt(win, 1, index); + } + + /** + * Opens the event dialog for editing for a single occurrence of the event + * box located at the specified index. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} index - Indicates which event box to select. + * + * @returns {Promise<EditItemAtResult>} + */ + async editEventOccurrenceAt(win, index) { + return this.#helper.editEventOccurrenceAt(win, 1, index); + } + + /** + * Opens the event dialog for editing all occurrences of the event box + * located at the specified index. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} index - Indicates which event box to select. + * + * @returns {Promise<EditItemAtResult>} + */ + async editEventOccurrencesAt(win, index) { + return this.#helper.editEventOccurrencesAt(win, 1, index); + } +} + +/** + * Helper class for testing the week view of the calendar. + */ +class CalendarWeekViewTestUtils { + constructor(rootSelector = "#week-view") { + this.rootSelector = rootSelector; + } + + /** + * Provides the column container element for the day specified. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} day - Must be between 1-7. + * + * @throws If the day parameter is out of range. + * @returns {HTMLElement} - The column container element. + */ + getColumnContainer(win, day) { + if (day < 1 || day > 7) { + throw new Error( + `Invalid parameter to #getColumnContainer(): expected day=1-7, got day=${day}.` + ); + } + + let containers = win.document.documentElement.querySelectorAll( + `${this.rootSelector} .day-column-container` + ); + return containers[day - 1]; + } + + /** + * Provides the element containing the formatted date for the day specified. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} day - Must be between 1-7. + * + * @throws If the day parameter is out of range. + * @returns {HTMLElement} - The column heading container element. + */ + getColumnHeading(win, day) { + let container = this.getColumnContainer(win, day); + return container.querySelector(".day-column-heading"); + } + + /** + * Provides the calendar-event-column for the day specified. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} day - Must be between 1-7 + * + * @throws - If the day parameter is out of range. + * @returns {MozCalendarEventColumn} - The column. + */ + getEventColumn(win, day) { + let container = this.getColumnContainer(win, day); + return container.querySelector("calendar-event-column"); + } + + /** + * Provides the calendar-event-box elements for the day specified. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} day - Must be between 1-7. + * + * @returns {MozCalendarEventBox[]} - The event boxes. + */ + getEventBoxes(win, day) { + let container = this.getColumnContainer(win, day); + return container.querySelectorAll(".multiday-events-list calendar-event-box"); + } + + /** + * Provides the calendar-event-box at "index" located in the event column for + * the specified day. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} day - Must be between 1-7. + * @param {number} index - Indicates which event box to select. + * + * @returns {MozCalendarEventBox|undefined} - The event box, if it exists. + */ + getEventBoxAt(win, day, index) { + return this.getEventBoxes(win, day)[index - 1]; + } + + /** + * Provides the .multiday-hour-box element for the specified hour. This + * element can be double clicked to create a new event at that hour. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} day - Day of the week, between 1-7. + * @param {number} hour - Must be between 0-23. + * + * @throws If the day or hour are out of range. + * @returns {XULElement} - The hour box. + */ + getHourBoxAt(win, day, hour) { + let container = this.getColumnContainer(win, day); + return container.querySelectorAll(".multiday-hour-box")[hour]; + } + + /** + * Provides the all-day header, which can be double clicked to create a new + * all-day event for the specified day. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} day - Day of the week, between 1-7. + * + * @throws If the day is out of range. + * @returns {CalendarHeaderContainer} - The all-day header. + */ + getAllDayHeader(win, day) { + let container = this.getColumnContainer(win, day); + return container.querySelector("calendar-header-container"); + } + + /** + * Provides the all-day calendar-editable-item located at "index" for the + * specified day. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} day - Day of the week, between 1-7. + * @param {number} index - Indicates which item to select (starting from 1). + * + * @throws If the day or index are out of range. + * @returns {MozCalendarEditableItem|undefined} - The all-day item, if it + * exists. + */ + getAllDayItemAt(win, day, index) { + let allDayHeader = this.getAllDayHeader(win, day); + return allDayHeader.querySelectorAll("calendar-editable-item")[index - 1]; + } + + /** + * Waits for the calendar-event-box at "index", located in the event column + * for the day specified to appear. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} day - Day of the week, between 1-7. + * @param {number} index - Indicates which event box to select. + * + * @returns {MozCalendarEventBox} - The event box. + */ + async waitForEventBoxAt(win, day, index) { + return TestUtils.waitForCondition( + () => this.getEventBoxAt(win, day, index), + `calendar-event-box at day=${day}, index=${index} did not appear in time` + ); + } + + /** + * Waits until the calendar-event-box at "index", located in the event column + * for the day specified disappears. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} day - Day of the week, between 1-7. + * @param {number} index - Indicates which event box to select. + */ + async waitForNoEventBoxAt(win, day, index) { + await TestUtils.waitForCondition( + () => !this.getEventBoxAt(win, day, index), + `calendar-event-box at day=${day}, index=${index} still present` + ); + } + + /** + * Waits for the all-day calendar-editable-item at "index", located in the + * event column for the day specified to appear. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} day - Day of the week, between 1-7. + * @param {number} index - Indicates which item to select (starting from 1). + * + * @returns {Promise<MozCalendarEditableItem>} - The all-day item. + */ + async waitForAllDayItemAt(win, day, index) { + return TestUtils.waitForCondition( + () => this.getAllDayItemAt(win, day, index), + `All-day calendar-editable-item at day=${day}, index=${index} did not appear in time` + ); + } + + /** + * Opens the event dialog for viewing for the event box located at the + * specified parameters. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} day - Must be between 1-7. + * @param {number} index - Indicates which event to select. + * + * @returns {Promise<Window>} - The summary event dialog window. + */ + async viewEventAt(win, day, index) { + let item = await this.waitForEventBoxAt(win, day, index); + return CalendarTestUtils.viewItem(win, item); + } + + /** + * Opens the event dialog for editing for the event box located at the + * specified parameters. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} day - Must be between 1-7. + * @param {number} index - Indicates which event to select. + * + * @returns {Promise<EditItemAtResult>} + */ + async editEventAt(win, day, index) { + let item = await this.waitForEventBoxAt(win, day, index); + return CalendarTestUtils.editItem(win, item); + } + + /** + * Opens the event dialog for editing for a single occurrence of the event + * box located at the specified parameters. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} day - Must be between 1-7. + * @param {number} index - Indicates which event box to select. + * + * @returns {Promise<EditItemAtResult>} + */ + async editEventOccurrenceAt(win, day, index) { + let item = await this.waitForEventBoxAt(win, day, index); + return CalendarTestUtils.editItemOccurrence(win, item); + } + + /** + * Opens the event dialog for editing all occurrences of the event box + * located at the specified parameters. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} day - Must be between 1-7. + * @param {number} index - Indicates which event box to select. + * + * @returns {Promise<EditItemAtResult>} + */ + async editEventOccurrencesAt(win, day, index) { + let item = await this.waitForEventBoxAt(win, day, index); + return CalendarTestUtils.editItemOccurrences(win, item); + } +} + +/** + * Helper class for testing the multiweek and month views of the calendar. + */ +class CalendarMonthViewTestUtils { + /** + * @param {string} rootSelector + */ + constructor(rootSelector) { + this.rootSelector = rootSelector; + } + + /** + * Provides the calendar-month-day-box element located at the specified day, + * week combination. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} week - Must be between 1-6. The cap may be as low as 1 + * depending on the user preference calendar.weeks.inview. + * @param {number} day - Must be between 1-7. + * + * @throws If the day or week parameters are out of range. + * @returns {MozCalendarMonthDayBox} + */ + getDayBox(win, week, day) { + if (!(week >= 1 && week <= 6 && day >= 1 && day <= 7)) { + throw new Error( + `Invalid parameters to getDayBox(): ` + + `expected week=1-6, day=1-7, got week=${week}, day=${day},` + ); + } + + return win.document.documentElement.querySelector( + `${this.rootSelector} .monthbody > tr:nth-of-type(${week}) > + td:nth-of-type(${day}) > calendar-month-day-box` + ); + } + + /** + * Get the calendar-month-day-box-item located in the specified day box, at + * the target index. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} week - Must be between 1-6. + * @param {number} day - Must be between 1-7. + * @param {number} index - Indicates which item to select. + * + * @throws If the index, day or week parameters are out of range. + * @returns {MozCalendarMonthDayBoxItem} + */ + getItemAt(win, week, day, index) { + if (!(index >= 1)) { + throw new Error(`Invalid parameters to getItemAt(): expected index>=1, got index=${index}.`); + } + + let dayBox = this.getDayBox(win, week, day); + return dayBox.querySelector(`li:nth-of-type(${index}) calendar-month-day-box-item`); + } + + /** + * Waits for the calendar-month-day-box-item at "index", located in the + * specified week,day combination to appear. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} week - Must be between 1-6. + * @param {number} day - Must be between 1-7. + * @param {number} index - Indicates which item to select. + * + * @returns {MozCalendarMonthDayBoxItem} + */ + async waitForItemAt(win, week, day, index) { + return TestUtils.waitForCondition( + () => this.getItemAt(win, week, day, index), + `calendar-month-day-box-item at week=${week}, day=${day}, index=${index} did not appear in time` + ); + } + + /** + * Waits for the calendar-month-day-box-item at "index", located in the + * specified week,day combination to disappear. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} week - Must be between 1-6. + * @param {number} day - Must be between 1-7. + * @param {number} index - Indicates the item that should no longer be present. + */ + async waitForNoItemAt(win, week, day, index) { + await TestUtils.waitForCondition( + () => !this.getItemAt(win, week, day, index), + `calendar-month-day-box-item at week=${week}, day=${day}, index=${index} still present` + ); + } + + /** + * Opens the event dialog for viewing for the item located at the specified + * parameters. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} week - Must be between 1-6. + * @param {number} day - Must be between 1-7. + * @param {number} index - Indicates which item to select. + * + * @returns {Window} - The summary event dialog window. + */ + async viewItemAt(win, week, day, index) { + let item = await this.waitForItemAt(win, week, day, index); + return CalendarTestUtils.viewItem(win, item); + } + + /** + * Opens the event dialog for editing for the item located at the specified + * parameters. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} week - Must be between 1-6. + * @param {number} day - Must be between 1-7. + * @param {number} index - Indicates which item to select. + * + * @returns {EditItemAtResult} + */ + async editItemAt(win, week, day, index) { + let item = await this.waitForItemAt(win, week, day, index); + return CalendarTestUtils.editItem(win, item); + } + + /** + * Opens the event dialog for editing for a single occurrence of the item + * located at the specified parameters. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} week - Must be between 1-6. + * @param {number} day - Must be between 1-7. + * @param {number} index - Indicates which item to select. + * + * @returns {EditItemAtResult} + */ + async editItemOccurrenceAt(win, week, day, index) { + let item = await this.waitForItemAt(win, week, day, index); + return CalendarTestUtils.editItemOccurrence(win, item); + } + + /** + * Opens the event dialog for editing all occurrences of the item + * located at the specified parameters. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} week - Must be between 1-6. + * @param {number} day - Must be between 1-7. + * @param {number} index - Indicates which item to select. + * + * @returns {EditItemAtResult} + */ + async editItemOccurrencesAt(win, week, day, index) { + let item = await this.waitForItemAt(win, week, day, index); + return CalendarTestUtils.editItemOccurrences(win, item); + } +} + +/** + * Non-mozmill calendar helper utility. + */ +const CalendarTestUtils = { + /** + * Helper methods for item editing. + */ + items: { + cancelItemDialog, + saveAndCloseItemDialog, + setData, + }, + + /** + * Helpers specific to the day view. + */ + dayView: new CalendarDayViewTestUtils(), + + /** + * Helpers specific to the week view. + */ + weekView: new CalendarWeekViewTestUtils(), + + /** + * Helpers specific to the multiweek view. + */ + multiweekView: new CalendarMonthViewTestUtils("#multiweek-view"), + + /** + * Helpers specific to the month view. + */ + monthView: new CalendarMonthViewTestUtils("#month-view"), + + /** + * Dedent the template string tagged with this function to make indented data + * easier to read. Usage: + * + * let data = dedent` + * This is indented data it will be unindented so that the first line has + * no leading spaces and the second is indented by two spaces. + * `; + * + * @param strings The string fragments from the template string + * @param ...values The interpolated values + * @returns The interpolated, dedented string + */ + dedent(strings, ...values) { + let parts = []; + // Perform variable interpolation + let minIndent = Infinity; + for (let [i, string] of strings.entries()) { + let innerparts = string.split("\n"); + if (i == 0) { + innerparts.shift(); + } + if (i == strings.length - 1) { + innerparts.pop(); + } + for (let [j, ip] of innerparts.entries()) { + let match = ip.match(/^(\s*)\S*/); + if (j != 0) { + minIndent = Math.min(minIndent, match[1].length); + } + } + parts.push(innerparts); + } + + return parts + .map((part, i) => { + return ( + part + .map((line, j) => { + return j == 0 && i > 0 ? line : line.substr(minIndent); + }) + .join("\n") + (i < values.length ? values[i] : "") + ); + }) + .join(""); + }, + + /** + * Creates and registers a new calendar with the calendar manager. The + * created calendar will be set as the default calendar. + * + * @param {string} - name + * @param {string} - type + * + * @returns {calICalendar} + */ + createCalendar(name = "Test", type = "storage") { + let calendar = cal.manager.createCalendar(type, Services.io.newURI(`moz-${type}-calendar://`)); + calendar.name = name; + calendar.setProperty("calendar-main-default", true); + cal.manager.registerCalendar(calendar); + return calendar; + }, + + /** + * Convenience method for removing a calendar using its proxy. + * + * @param {calICalendar} calendar - A calendar to remove. + */ + removeCalendar(calendar) { + cal.manager.unregisterCalendar(calendar); + }, + + /** + * Ensures the calendar tab is open + * + * @param {Window} win + */ + async openCalendarTab(win) { + let tabmail = win.document.getElementById("tabmail"); + let calendarMode = tabmail.tabModes.calendar; + + if (calendarMode.tabs.length == 1) { + tabmail.selectedTab = calendarMode.tabs[0]; + } else { + let calendarTabButton = win.document.getElementById("calendarButton"); + EventUtils.synthesizeMouseAtCenter(calendarTabButton, { clickCount: 1 }, win); + } + + Assert.equal(calendarMode.tabs.length, 1, "calendar tab is open"); + Assert.equal(tabmail.selectedTab, calendarMode.tabs[0], "calendar tab is selected"); + + await new Promise(resolve => win.setTimeout(resolve)); + }, + + /** + * Make sure the current view has finished loading. + * + * @param {Window} win + */ + async ensureViewLoaded(win) { + await win.currentView().ready; + }, + + /** + * Ensures the calendar view is in the specified mode. + * + * @param {Window} win + * @param {string} viewName + */ + async setCalendarView(win, viewName) { + await CalendarTestUtils.openCalendarTab(win); + await CalendarTestUtils.ensureViewLoaded(win); + + let viewTabButton = win.document.querySelector( + `.calview-toggle-item[aria-controls="${viewName}-view"]` + ); + EventUtils.synthesizeMouseAtCenter(viewTabButton, { clickCount: 1 }, win); + Assert.equal(win.currentView().id, `${viewName}-view`); + + await CalendarTestUtils.ensureViewLoaded(win); + }, + + /** + * Step forward in the calendar view. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} n - Number of times to move the view forward. + */ + async calendarViewForward(win, n) { + let viewForwardButton = win.document.getElementById("nextViewButton"); + for (let i = 0; i < n; i++) { + await clickAndWait(win, viewForwardButton); + await CalendarTestUtils.ensureViewLoaded(win); + } + }, + + /** + * Step backward in the calendar view. + * + * @param {Window} win - The window the calendar is displayed in. + * @param {number} n - Number of times to move the view backward. + */ + async calendarViewBackward(win, n) { + let viewBackwardButton = win.document.getElementById("previousViewButton"); + for (let i = 0; i < n; i++) { + await clickAndWait(win, viewBackwardButton); + await CalendarTestUtils.ensureViewLoaded(win); + } + }, + + /** + * Ensures the calendar tab is not open. + * + * @param {Window} win + */ + async closeCalendarTab(win) { + let tabmail = win.document.getElementById("tabmail"); + let calendarMode = tabmail.tabModes.calendar; + + if (calendarMode.tabs.length == 1) { + tabmail.closeTab(calendarMode.tabs[0]); + } + + Assert.equal(calendarMode.tabs.length, 0, "calendar tab is not open"); + + await new Promise(resolve => win.setTimeout(resolve)); + }, + + /** + * Opens the event dialog for viewing by clicking on the provided event item. + * + * @param {Window} win - The window containing the calendar. + * @param {MozCalendarEditableItem} item - An event box item that can be + * clicked on to open the dialog. + * + * @returns {Promise<Window>} + */ + async viewItem(win, item) { + if (Services.focus.activeWindow != win) { + await BrowserTestUtils.waitForEvent(win, "focus"); + } + + let promise = this.waitForEventDialog("view"); + EventUtils.synthesizeMouseAtCenter(item, { clickCount: 2 }, win); + return promise; + }, + + async _editNewItem(win, target, type) { + let dialogPromise = CalendarTestUtils.waitForEventDialog("edit"); + + if (target) { + this.scrollViewToTarget(target, true); + EventUtils.synthesizeMouse(target, 1, 1, { clickCount: 2 }, win); + } else { + let buttonId = `sidePanelNew${type[0].toUpperCase()}${type.slice(1).toLowerCase()}`; + EventUtils.synthesizeMouseAtCenter(win.document.getElementById(buttonId), {}, win); + } + + let dialogWindow = await dialogPromise; + let iframe = dialogWindow.document.querySelector("#calendar-item-panel-iframe"); + await new Promise(resolve => iframe.contentWindow.setTimeout(resolve)); + Assert.report(false, undefined, undefined, `New ${type} dialog opened`); + return { + dialogWindow, + dialogDocument: dialogWindow.document, + iframeWindow: iframe.contentWindow, + iframeDocument: iframe.contentDocument, + }; + }, + + /** + * Opens the dialog for editing a new event. An optional day/week view + * hour box or multiweek/month view calendar-month-day-box can be specified + * to simulate creation of the event at that target. + * + * @param {Window} win - The window containing the calendar. + * @param {XULElement?} target - The <spacer> or <calendar-month-day-box> + * to click on, if not specified, the new event + * button is used. + */ + async editNewEvent(win, target) { + return this._editNewItem(win, target, "event"); + }, + + /** + * Opens the dialog for editing a new task. + * + * @param {Promise<Window>} win - The window containing the task tree. + */ + async editNewTask(win) { + return this._editNewItem(win, null, "task"); + }, + + async _editItem(win, item, selector) { + let summaryWin = await this.viewItem(win, item); + let promise = this.waitForEventDialog("edit"); + let button = summaryWin.document.querySelector(selector); + button.click(); + + let dialogWindow = await promise; + let iframe = dialogWindow.document.querySelector("#calendar-item-panel-iframe"); + return { + dialogWindow, + dialogDocument: dialogWindow.document, + iframeWindow: iframe.contentWindow, + iframeDocument: iframe.contentDocument, + }; + }, + + /** + * Opens the event dialog for editing by clicking on the provided event item. + * + * @param {Window} win - The window containing the calendar. + * @param {MozCalendarEditableItem} item - An event box item that can be + * clicked on to open the dialog. + * + * @returns {Promise<EditItemAtResult>} + */ + async editItem(win, item) { + return this._editItem(win, item, "#calendar-summary-dialog-edit-button"); + }, + + /** + * Opens the event dialog for editing a single occurrence of a repeating event + * by clicking on the provided event item. + * + * @param {Window} win - The window containing the calendar. + * @param {MozCalendarEditableItem} item - An event box item that can be + * clicked on to open the dialog. + * + * @returns {Window} + */ + async editItemOccurrence(win, item) { + return this._editItem(win, item, "#edit-button-context-menu-this-occurrence"); + }, + + /** + * Opens the event dialog for editing all occurrences of a repeating event + * by clicking on the provided event box. + * + * @param {Window} win - The window containing the calendar. + * @param {MozCalendarEditableItem} item - An event box item that can be + * clicked on to open the dialog. + * + * @returns {Window} + */ + async editItemOccurrences(win, item) { + return this._editItem(win, item, "#edit-button-context-menu-all-occurrences"); + }, + + /** + * This produces a Promise for waiting on an event dialog to open. + * The mode parameter can be specified to indicate which of the dialogs to + * wait for. + * + * @param {string} [mode="view"] Determines which dialog we are waiting on, + * can be "view" for the summary or "edit" for the editing one. + * + * @returns {Promise<Window>} + */ + waitForEventDialog(mode = "view") { + let uri = + mode === "edit" + ? "chrome://calendar/content/calendar-event-dialog.xhtml" + : "chrome://calendar/content/calendar-summary-dialog.xhtml"; + + return BrowserTestUtils.domWindowOpened(null, async win => { + await BrowserTestUtils.waitForEvent(win, "load"); + + if (win.document.documentURI != uri) { + return false; + } + + Assert.report(false, undefined, undefined, "Event dialog opened"); + await TestUtils.waitForCondition( + () => Services.focus.activeWindow == win, + "event dialog active" + ); + + if (mode === "edit") { + let iframe = win.document.getElementById("calendar-item-panel-iframe"); + await TestUtils.waitForCondition( + () => iframe.contentWindow?.onLoad?.hasLoaded, + "waiting for iframe to be loaded" + ); + await TestUtils.waitForCondition( + () => Services.focus.focusedWindow == iframe.contentWindow, + "waiting for iframe to be focused" + ); + } + return true; + }); + }, + + /** + * Go to a specific date using the minimonth. + * + * @param {Window} win - Main window + * @param {number} year - Four-digit year + * @param {number} month - 1-based index of a month + * @param {number} day - 1-based index of a day + */ + async goToDate(win, year, month, day) { + let miniMonth = win.document.getElementById("calMinimonth"); + + let activeYear = miniMonth.querySelector(".minimonth-year-name").getAttribute("value"); + + let activeMonth = miniMonth.querySelector(".minimonth-month-name").getAttribute("monthIndex"); + + async function doScroll(name, difference, sleepTime) { + if (difference === 0) { + return; + } + let query = `.${name}s-${difference > 0 ? "back" : "forward"}-button`; + let scrollArrow = await TestUtils.waitForCondition( + () => miniMonth.querySelector(query), + `Query for scroll: ${query}` + ); + + for (let i = 0; i < Math.abs(difference); i++) { + EventUtils.synthesizeMouseAtCenter(scrollArrow, {}, win); + await new Promise(resolve => win.setTimeout(resolve, sleepTime)); + } + } + + await doScroll("year", activeYear - year, 10); + await doScroll("month", activeMonth - (month - 1), 25); + + function getMiniMonthDay(week, day) { + return miniMonth.querySelector( + `.minimonth-cal-box > tr.minimonth-row-body:nth-of-type(${week + 1}) > ` + + `td.minimonth-day:nth-of-type(${day})` + ); + } + + let positionOfFirst = 7 - getMiniMonthDay(1, 7).textContent; + let weekDay = ((positionOfFirst + day - 1) % 7) + 1; + let week = Math.floor((positionOfFirst + day - 1) / 7) + 1; + + // Pick day. + EventUtils.synthesizeMouseAtCenter(getMiniMonthDay(week, weekDay), {}, win); + await CalendarTestUtils.ensureViewLoaded(win); + }, + + /** + * Go to today. + * + * @param {Window} win - Main window + */ + async goToToday(win) { + EventUtils.synthesizeMouseAtCenter(this.getNavBarTodayButton(win), {}, win); + await CalendarTestUtils.ensureViewLoaded(win); + }, + + /** + * Assert whether the given event box's edges are visually draggable (and + * hence, editable) at its edges or not. + * + * @param {MozCalendarEventBox} eventBox - The event box to test. + * @param {boolean} startDraggable - Whether we expect the start edge to be + * draggable. + * @param {boolean} endDraggable - Whether we expect the end edge to be + * draggable. + * @param {string} message - A message for assertions. + */ + async assertEventBoxDraggable(eventBox, startDraggable, endDraggable, message) { + this.scrollViewToTarget(eventBox, true); + // Hover to see if the drag gripbars appear. + let enterPromise = BrowserTestUtils.waitForEvent(eventBox, "mouseenter"); + // Hover over start. + EventUtils.synthesizeMouse(eventBox, 8, 8, { type: "mouseover" }, eventBox.ownerGlobal); + await enterPromise; + Assert.equal( + BrowserTestUtils.is_visible(eventBox.startGripbar), + startDraggable, + `Start gripbar should be ${startDraggable ? "visible" : "hidden"} on hover: ${message}` + ); + Assert.equal( + BrowserTestUtils.is_visible(eventBox.endGripbar), + endDraggable, + `End gripbar should be ${endDraggable ? "visible" : "hidden"} on hover: ${message}` + ); + }, + + /** + * Scroll the calendar view to show the given target. + * + * @param {Element} target - The target to scroll to. A descendent of a + * calendar view. + * @param {boolean} alignStart - Whether to scroll the inline and block start + * edges of the target into view, else scrolls the end edges into view. + */ + scrollViewToTarget(target, alignStart) { + let multidayView = target.closest("calendar-day-view, calendar-week-view"); + if (multidayView) { + // Multiday view has sticky headers, so scrollIntoView doesn't actually + // scroll far enough. + let scrollRect = multidayView.getScrollAreaRect(); + let targetRect = target.getBoundingClientRect(); + // We want to move the view by the difference between the starting/ending + // edge of the view and the starting/ending edge of the target. + let yDiff = alignStart + ? targetRect.top - scrollRect.top + : targetRect.bottom - scrollRect.bottom; + // In left-to-right, starting edge is the left edge. Otherwise, it is the + // right edge. + let xDiff = + alignStart == (target.ownerDocument.dir == "ltr") + ? targetRect.left - scrollRect.left + : targetRect.right - scrollRect.right; + multidayView.grid.scrollBy(xDiff, yDiff); + } else { + target.scrollIntoView(alignStart); + } + }, + + /** + * Save the current calendar views' UI states to be restored later. + * + * This is used with restoreCalendarViewsState to reset the view back to its + * initial loaded state after a test, so that later tests in the same group + * will receive the calendar view as if it was first opened after launching. + * + * @param {Window} win - The window that contains the calendar views. + * + * @returns {object} - An opaque object with data to pass to + * restoreCalendarViewsState. + */ + saveCalendarViewsState(win) { + return { + multidayViewsData: ["day", "week"].map(viewName => { + // Save the scroll state since test utilities may change the scroll + // position, and this is currently not reset on re-opening the tab. + let view = win.document.getElementById(`${viewName}-view`); + return { view, viewName, scrollMinute: view.scrollMinute }; + }), + }; + }, + + /** + * Clean up the calendar views after a test by restoring their UI to the saved + * state, and close the calendar tab. + * + * @param {Window} win - The window that contains the calendar views. + * @param {object} data - The data returned by saveCalendarViewsState. + */ + async restoreCalendarViewsState(win, data) { + for (let { view, viewName, scrollMinute } of data.multidayViewsData) { + await this.setCalendarView(win, viewName); + // The scrollMinute is rounded to the nearest integer. + // As is the scroll pixels. + // When we scrollToMinute, the scroll position is rounded to the nearest + // integer, as is the subsequent scroll minute. So calling + // scrollToMinute(min) + // will set + // scrollMinute = round(round(min * P) / P) + // where P is the pixelsPerMinute of the view. Thus + // scrollMinute = min +- round(0.5 / P) + let roundingError = Math.round(0.5 / view.pixelsPerMinute); + view.scrollToMinute(scrollMinute); + await TestUtils.waitForCondition( + () => Math.abs(view.scrollMinute - scrollMinute) <= roundingError, + "Waiting for scroll minute to restore" + ); + } + await CalendarTestUtils.closeCalendarTab(win); + }, + + /** + * Get the Today button from the navigation bar. + * + * @param {Window} win - The window which contains the calendar. + * + * @returns {HTMLElement} - The today button. + */ + getNavBarTodayButton(win) { + return win.document.getElementById("todayViewButton"); + }, + + /** + * Get the label element containing a human-readable description of the + * displayed interval. + * + * @param {Window} win - The window which contains the calendar. + * + * @returns {HTMLLabelElement} - The interval description label. + */ + getNavBarIntervalDescription(win) { + return win.document.getElementById("intervalDescription"); + }, + + /** + * Get the label element containing an indication of which week or weeks are + * displayed. + * + * @param {Window} win - The window which contains the calendar. + * + * @returns {HTMLLabelElement} - The calendar week label. + */ + getNavBarCalendarWeekBox(win) { + return win.document.getElementById("calendarWeek"); + }, +}; |