Revision control

Copy as Markdown

Other Tools

/* 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 * as EventUtils from "resource://testing-common/mail/EventUtils.sys.mjs";
import { BrowserTestUtils } from "resource://testing-common/BrowserTestUtils.sys.mjs";
import { TestUtils } from "resource://testing-common/TestUtils.sys.mjs";
import { Assert } from "resource://testing-common/Assert.sys.mjs";
import {
cancelItemDialog,
saveAndCloseItemDialog,
setData,
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}.`
);
}
const 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) {
const 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) {
const 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) {
const 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) {
const 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) {
const 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) {
const 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) {
const 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) {
const 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) {
const 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) {
const 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}.`);
}
const 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) {
const 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) {
const 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) {
const 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) {
const item = await this.waitForItemAt(win, week, day, index);
return CalendarTestUtils.editItemOccurrences(win, item);
}
}
/**
* Non-mozmill calendar helper utility.
*/
export 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) {
const parts = [];
// Perform variable interpolation
let minIndent = Infinity;
for (const [i, string] of strings.entries()) {
const innerparts = string.split("\n");
if (i == 0) {
innerparts.shift();
}
if (i == strings.length - 1) {
innerparts.pop();
}
for (const [j, ip] of innerparts.entries()) {
const 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") {
const 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) {
const tabmail = win.document.getElementById("tabmail");
const calendarMode = tabmail.tabModes.calendar;
if (calendarMode.tabs.length == 1) {
tabmail.selectedTab = calendarMode.tabs[0];
} else {
const 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);
const 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) {
const 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) {
const 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) {
const tabmail = win.document.getElementById("tabmail");
const 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");
}
const promise = this.waitForEventDialog("view");
EventUtils.synthesizeMouseAtCenter(item, { clickCount: 2 }, win);
return promise;
},
async _editNewItem(win, target, type) {
const dialogPromise = CalendarTestUtils.waitForEventDialog("edit");
if (target) {
this.scrollViewToTarget(target, true);
await new Promise(resolve => win.setTimeout(resolve));
EventUtils.synthesizeMouse(target, 1, 1, { clickCount: 2 }, win);
} else {
const buttonId = `sidePanelNew${type[0].toUpperCase()}${type.slice(1).toLowerCase()}`;
EventUtils.synthesizeMouseAtCenter(win.document.getElementById(buttonId), {}, win);
}
const dialogWindow = await dialogPromise;
const 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) {
const summaryWin = await this.viewItem(win, item);
const promise = this.waitForEventDialog("edit");
const button = summaryWin.document.querySelector(selector);
button.click();
const dialogWindow = await promise;
const 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") {
const uri =
mode === "edit"
return BrowserTestUtils.domWindowOpenedAndLoaded(null, async win => {
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") {
const 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) {
const miniMonth = win.document.getElementById("calMinimonth");
const activeYear = miniMonth.querySelector(".minimonth-year-name").getAttribute("value");
const activeMonth = miniMonth.querySelector(".minimonth-month-name").getAttribute("monthIndex");
async function doScroll(name, difference, sleepTime) {
if (difference === 0) {
return;
}
const query = `.${name}s-${difference > 0 ? "back" : "forward"}-button`;
const 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})`
);
}
const positionOfFirst = 7 - getMiniMonthDay(1, 7).textContent;
const weekDay = ((positionOfFirst + day - 1) % 7) + 1;
const 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.
const enterPromise = BrowserTestUtils.waitForEvent(eventBox, "mouseenter");
// Hover over start.
EventUtils.synthesizeMouse(eventBox, 8, 8, { type: "mouseover" }, eventBox.ownerGlobal);
await enterPromise;
Assert.equal(
BrowserTestUtils.isVisible(eventBox.startGripbar),
startDraggable,
`Start gripbar should be ${startDraggable ? "visible" : "hidden"} on hover: ${message}`
);
Assert.equal(
BrowserTestUtils.isVisible(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) {
const multidayView = target.closest("calendar-day-view, calendar-week-view");
if (multidayView) {
// Multiday view has sticky headers, so scrollIntoView doesn't actually
// scroll far enough.
const scrollRect = multidayView.getScrollAreaRect();
const 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.
const 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.
const 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.
const 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 (const { 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)
const 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");
},
};