1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5const EXPORTED_SYMBOLS = [
6  "cancelItemDialog",
7  "menulistSelect",
8  "saveAndCloseItemDialog",
9  "setData",
10];
11
12var { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm");
13var { BrowserTestUtils } = ChromeUtils.import("resource://testing-common/BrowserTestUtils.jsm");
14var { sendString, synthesizeKey, synthesizeMouseAtCenter } = ChromeUtils.import(
15  "resource://testing-common/mozmill/EventUtils.jsm"
16);
17
18var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
19var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
20var { setTimeout } = ChromeUtils.import("resource://gre/modules/Timer.jsm");
21
22function sleep(window, time = 0) {
23  return new Promise(resolve => window.setTimeout(resolve, time));
24}
25
26/**
27 * @callback dialogCallback
28 * @param {Window} - The calendar-event-dialog-recurrence.xhtml dialog.
29 */
30
31/**
32 * Helper function to enter event/task dialog data.
33 *
34 * @param {Window} dialogWindow - Item dialog outer window.
35 * @param {Window} iframeWindow - Item dialog inner iframe.
36 * @param {object} data
37 * @param {string} data.title - Item title.
38 * @param {string} data.location - Item location.
39 * @param {string} data.description - Item description.
40 * @param {string[]} data.categories - Category names to set - leave empty to clear.
41 * @param {string} data.calendar - ID of the calendar the item should be in.
42 * @param {boolean} data.allday
43 * @param {calIDateTime} data.startdate
44 * @param {calIDateTime} data.starttime
45 * @param {calIDateTime} data.enddate
46 * @param {calIDateTime} data.endtime
47 * @param {boolean} data.timezonedisplay - false for hidden, true for shown.
48 * @param {string} data.timezone - String identifying the timezone.
49 * @param {string|dialogCallback} data.repeat - Recurrence value, one of
50 *     none/daily/weekly/every.weekday/bi.weekly/monthly/yearly or a callback function to set a
51 *     custom value.
52 * @param {calIDateTime} data.repeatuntil
53 * @param {string} data.reminder -
54 *     none/0minutes/5minutes/15minutes/30minutes/1hour/2hours/12hours/1day/2days/1week
55 *     (Custom is not supported.)
56 * @param {string} data.priority - none/low/normal/high
57 * @param {string} data.privacy - public/confidential/private
58 * @param {string} data.status - none/tentative/confirmed/canceled for events
59 *     none/needs-action/in-process/completed/cancelled for tasks
60 * @param {calIDateTime} data.completed - Completion date (tasks only)
61 * @param {string} data.percent - Percentage complete (tasks only)
62 * @param {string} data.freebusy - free/busy
63 * @param {string} data.attachment.add - URL to add
64 * @param {string} data.attachment.remove - Label of url to remove. (without http://)
65 * @param {string} data.attendees.add - Email of attendees to add, comma separated.
66 * @param {string} data.attendees.remove - Email of attendees to remove, comma separated.
67 */
68async function setData(dialogWindow, iframeWindow, data) {
69  function replaceText(input, text) {
70    synthesizeMouseAtCenter(input, {}, iframeWindow);
71    synthesizeKey("a", { accelKey: true }, iframeWindow);
72    sendString(text, iframeWindow);
73  }
74
75  let dialogDocument = dialogWindow.document;
76  let iframeDocument = iframeWindow.document;
77
78  let isEvent = iframeWindow.calendarItem.isEvent();
79  let startPicker = iframeDocument.getElementById(isEvent ? "event-starttime" : "todo-entrydate");
80  let endPicker = iframeDocument.getElementById(isEvent ? "event-endtime" : "todo-duedate");
81
82  let startdateInput = startPicker._datepicker._inputField;
83  let enddateInput = endPicker._datepicker._inputField;
84  let starttimeInput = startPicker._timepicker._inputField;
85  let endtimeInput = endPicker._timepicker._inputField;
86  let completeddateInput = iframeDocument.getElementById("completed-date-picker")._inputField;
87  let untilDateInput = iframeDocument.getElementById("repeat-until-datepicker")._inputField;
88
89  let dateFormatter = cal.dtz.formatter;
90  // Wait for input elements' values to be populated.
91  await sleep(iframeWindow, 500);
92
93  // title
94  if (data.title !== undefined) {
95    let titleInput = iframeDocument.getElementById("item-title");
96    replaceText(titleInput, data.title);
97  }
98
99  // location
100  if (data.location !== undefined) {
101    let locationInput = iframeDocument.getElementById("item-location");
102    replaceText(locationInput, data.location);
103  }
104
105  // categories
106  if (data.categories !== undefined) {
107    await setCategories(iframeWindow, data.categories);
108    await sleep(iframeWindow);
109  }
110
111  // calendar
112  if (data.calendar !== undefined) {
113    await menulistSelect(iframeDocument.getElementById("item-calendar"), data.calendar);
114    await sleep(iframeWindow);
115  }
116
117  // all-day
118  if (data.allday !== undefined && isEvent) {
119    let checkbox = iframeDocument.getElementById("event-all-day");
120    if (checkbox.checked != data.allday) {
121      synthesizeMouseAtCenter(checkbox, {}, iframeWindow);
122    }
123  }
124
125  // timezonedisplay
126  if (data.timezonedisplay !== undefined) {
127    let menuitem = dialogDocument.getElementById("options-timezones-menuitem");
128    if (menuitem.getAttribute("checked") != data.timezonedisplay) {
129      synthesizeMouseAtCenter(menuitem, {}, iframeWindow);
130    }
131  }
132
133  // timezone
134  if (data.timezone !== undefined) {
135    await setTimezone(dialogWindow, iframeWindow, data.timezone);
136  }
137
138  // startdate
139  if (data.startdate !== undefined && data.startdate instanceof Ci.calIDateTime) {
140    let startdate = dateFormatter.formatDateShort(data.startdate);
141
142    if (!isEvent) {
143      let checkbox = iframeDocument.getElementById("todo-has-entrydate");
144      if (!checkbox.checked) {
145        synthesizeMouseAtCenter(checkbox, {}, iframeWindow);
146      }
147    }
148    replaceText(startdateInput, startdate);
149  }
150
151  // starttime
152  if (data.starttime !== undefined && data.starttime instanceof Ci.calIDateTime) {
153    let starttime = dateFormatter.formatTime(data.starttime);
154    replaceText(starttimeInput, starttime);
155    await sleep(iframeWindow);
156  }
157
158  // enddate
159  if (data.enddate !== undefined && data.enddate instanceof Ci.calIDateTime) {
160    let enddate = dateFormatter.formatDateShort(data.enddate);
161    if (!isEvent) {
162      let checkbox = iframeDocument.getElementById("todo-has-duedate");
163      if (!checkbox.checked) {
164        synthesizeMouseAtCenter(checkbox, {}, iframeWindow);
165      }
166    }
167    replaceText(enddateInput, enddate);
168  }
169
170  // endtime
171  if (data.endtime !== undefined && data.endtime instanceof Ci.calIDateTime) {
172    let endtime = dateFormatter.formatTime(data.endtime);
173    replaceText(endtimeInput, endtime);
174  }
175
176  // recurrence
177  if (data.repeat !== undefined) {
178    if (typeof data.repeat == "function") {
179      let repeatWindowPromise = BrowserTestUtils.promiseAlertDialog(
180        undefined,
181        "chrome://calendar/content/calendar-event-dialog-recurrence.xhtml",
182        {
183          async callback(recurrenceWindow) {
184            Assert.report(false, undefined, undefined, "Reccurrence dialog opened");
185            if (Services.focus.activeWindow != recurrenceWindow) {
186              await BrowserTestUtils.waitForEvent(recurrenceWindow, "focus");
187            }
188
189            recurrenceWindow.setTimeout(() => data.repeat(recurrenceWindow), 500);
190          },
191        }
192      );
193      await Promise.all([
194        menulistSelect(iframeDocument.getElementById("item-repeat"), "custom"),
195        repeatWindowPromise,
196      ]);
197      Assert.report(false, undefined, undefined, "Reccurrence dialog closed");
198    } else {
199      await menulistSelect(iframeDocument.getElementById("item-repeat"), data.repeat);
200    }
201  }
202  if (data.repeatuntil !== undefined && data.repeatuntil instanceof Ci.calIDateTime) {
203    // Only fill in date, when the Datepicker is visible.
204    if (!iframeDocument.getElementById("repeat-untilDate").hidden) {
205      let untildate = dateFormatter.formatDateShort(data.repeatuntil);
206      replaceText(untilDateInput, untildate);
207    }
208  }
209
210  // reminder
211  if (data.reminder !== undefined) {
212    await setReminderMenulist(iframeWindow, data.reminder);
213  }
214
215  // priority
216  if (data.priority !== undefined) {
217    dialogDocument.getElementById(`options-priority-${data.priority}-label`).click();
218  }
219
220  // privacy
221  if (data.privacy !== undefined) {
222    let button = dialogDocument.getElementById("button-privacy");
223    let shownPromise = BrowserTestUtils.waitForEvent(button, "popupshown");
224    synthesizeMouseAtCenter(button, {}, dialogWindow);
225    await shownPromise;
226    let hiddenPromise = BrowserTestUtils.waitForEvent(button, "popuphidden");
227    synthesizeMouseAtCenter(
228      dialogDocument.getElementById(`event-privacy-${data.privacy}-menuitem`),
229      {},
230      dialogWindow
231    );
232    await hiddenPromise;
233    await sleep(iframeWindow);
234  }
235
236  // status
237  if (data.status !== undefined) {
238    if (isEvent) {
239      dialogDocument.getElementById(`options-status-${data.status}-menuitem`).click();
240    } else {
241      await menulistSelect(iframeDocument.getElementById("todo-status"), data.status.toUpperCase());
242    }
243  }
244
245  let currentStatus = iframeDocument.getElementById("todo-status").value;
246
247  // completed on
248  if (data.completed !== undefined && data.completed instanceof Ci.calIDateTime && !isEvent) {
249    let completeddate = dateFormatter.formatDateShort(data.completed);
250    if (currentStatus == "COMPLETED") {
251      replaceText(completeddateInput, completeddate);
252    }
253  }
254
255  // percent complete
256  if (
257    data.percent !== undefined &&
258    (currentStatus == "NEEDS-ACTION" ||
259      currentStatus == "IN-PROCESS" ||
260      currentStatus == "COMPLETED")
261  ) {
262    let percentCompleteInput = iframeDocument.getElementById("percent-complete-textbox");
263    replaceText(percentCompleteInput, data.percent);
264  }
265
266  // free/busy
267  if (data.freebusy !== undefined) {
268    dialogDocument.getElementById(`options-freebusy-${data.freebusy}-menuitem`).click();
269  }
270
271  // description
272  if (data.description !== undefined) {
273    synthesizeMouseAtCenter(
274      iframeDocument.getElementById("event-grid-tab-description"),
275      {},
276      iframeWindow
277    );
278    let descField = iframeDocument.getElementById("item-description");
279    replaceText(descField, data.description);
280  }
281
282  // attachment
283  if (data.attachment !== undefined) {
284    if (data.attachment.add !== undefined) {
285      await handleAddingAttachment(dialogWindow, data.attachment.add);
286    }
287    if (data.attachment.remove !== undefined) {
288      synthesizeMouseAtCenter(
289        iframeDocument.getElementById("event-grid-tab-attachments"),
290        {},
291        iframeWindow
292      );
293      let attachmentBox = iframeDocument.getElementById("attachment-link");
294      let attachments = attachmentBox.children;
295      for (let attachment of attachments) {
296        if (attachment.tooltipText.includes(data.attachment.remove)) {
297          synthesizeMouseAtCenter(attachment, {}, iframeWindow);
298          synthesizeKey("VK_DELETE", {}, dialogWindow);
299        }
300      }
301    }
302  }
303
304  // attendees
305  if (data.attendees !== undefined) {
306    // Display attendees Tab.
307    synthesizeMouseAtCenter(
308      iframeDocument.getElementById("event-grid-tab-attendees"),
309      {},
310      iframeWindow
311    );
312    // Make sure no notifications are sent, since handling this dialog is
313    // not working when deleting a parent of a recurring event.
314    let attendeeCheckbox = iframeDocument.getElementById("notify-attendees-checkbox");
315    if (!attendeeCheckbox.disabled && attendeeCheckbox.checked) {
316      synthesizeMouseAtCenter(attendeeCheckbox, {}, iframeWindow);
317    }
318
319    // add
320    if (data.attendees.add !== undefined) {
321      await addAttendees(dialogWindow, iframeWindow, data.attendees.add);
322    }
323    // delete
324    if (data.attendees.remove !== undefined) {
325      await deleteAttendees(iframeWindow, data.attendees.remove);
326    }
327  }
328
329  await sleep(iframeWindow);
330}
331
332/**
333 * Closes an event dialog window, saving the event.
334 *
335 * @param {Window} dialogWindow
336 */
337async function saveAndCloseItemDialog(dialogWindow) {
338  let dialogClosing = BrowserTestUtils.domWindowClosed(dialogWindow);
339  synthesizeMouseAtCenter(
340    dialogWindow.document.getElementById("button-saveandclose"),
341    {},
342    dialogWindow
343  );
344  await dialogClosing;
345  await new Promise(resolve => setTimeout(resolve));
346}
347
348/**
349 * Closes an event dialog window, discarding any changes.
350 *
351 * @param {Window} dialogWindow
352 */
353function cancelItemDialog(dialogWindow) {
354  synthesizeKey("VK_ESCAPE", {}, dialogWindow);
355}
356
357/**
358 * Select an item in the reminder menulist.
359 * Custom reminders are not supported.
360 *
361 * @param {Window} iframeWindow - The event dialog iframe.
362 * @param {string} id - Identifying string of menuitem id.
363 */
364async function setReminderMenulist(iframeWindow, id) {
365  let iframeDocument = iframeWindow.document;
366  let menulist = iframeDocument.querySelector(".item-alarm");
367  let menuitem = iframeDocument.getElementById(`reminder-${id}-menuitem`);
368
369  Assert.ok(menulist, `<menulist id=${menulist.id}> exists`);
370  Assert.ok(menuitem, `<menuitem id=${id}> exists`);
371
372  menulist.focus();
373
374  synthesizeMouseAtCenter(menulist, {}, iframeWindow);
375  await BrowserTestUtils.waitForEvent(menulist, "popupshown");
376  synthesizeMouseAtCenter(menuitem, {}, iframeWindow);
377  await BrowserTestUtils.waitForEvent(menulist, "popuphidden");
378  await sleep(iframeWindow);
379}
380
381/**
382 * Set the categories in the event-dialog menulist-panel.
383 *
384 * @param {Window} iframeWindow - The event dialog iframe.
385 * @param {string[]} categories - Category names to set - leave empty to clear.
386 */
387async function setCategories(iframeWindow, categories) {
388  let iframeDocument = iframeWindow.document;
389  let menulist = iframeDocument.getElementById("item-categories");
390  let menupopup = iframeDocument.getElementById("item-categories-popup");
391
392  synthesizeMouseAtCenter(menulist, {}, iframeWindow);
393  await BrowserTestUtils.waitForEvent(menupopup, "popupshown");
394
395  // Iterate over categories and check if needed.
396  for (let item of menupopup.children) {
397    if (categories.includes(item.label)) {
398      item.setAttribute("checked", "true");
399    } else {
400      item.removeAttribute("checked");
401    }
402  }
403
404  let hiddenPromise = BrowserTestUtils.waitForEvent(menupopup, "popuphidden");
405  menupopup.hidePopup();
406  await hiddenPromise;
407}
408
409/**
410 * Add an URL attachment.
411 *
412 * @param {Window} dialogWindow - The event dialog.
413 * @param {string} url - URL to be added.
414 */
415async function handleAddingAttachment(dialogWindow, url) {
416  let dialogDocument = dialogWindow.document;
417  let attachButton = dialogDocument.querySelector("#button-url");
418  let menu = dialogDocument.querySelector("#button-attach-menupopup");
419  let menuShowing = BrowserTestUtils.waitForEvent(menu, "popupshown");
420  synthesizeMouseAtCenter(attachButton, {}, dialogWindow);
421  await menuShowing;
422
423  let dialogPromise = BrowserTestUtils.promiseAlertDialog(undefined, undefined, {
424    callback(attachmentWindow) {
425      Assert.report(false, undefined, undefined, "Attachment dialog opened");
426      let attachmentDocument = attachmentWindow.document;
427
428      attachmentDocument.getElementById("loginTextbox").value = url;
429      attachmentDocument
430        .querySelector("dialog")
431        .getButton("accept")
432        .click();
433    },
434  });
435  synthesizeMouseAtCenter(dialogDocument.querySelector("#button-attach-url"), {}, dialogWindow);
436  await dialogPromise;
437  Assert.report(false, undefined, undefined, "Attachment dialog closed");
438  await sleep(dialogWindow);
439}
440
441/**
442 * Add attendees to the event.
443 *
444 * @param {Window} dialogWindow - The event dialog.
445 * @param {Window} iframeWindow - The event dialog iframe.
446 * @param {string} attendeesString - Comma separated list of email-addresses to add.
447 */
448async function addAttendees(dialogWindow, iframeWindow, attendeesString) {
449  let dialogDocument = dialogWindow.document;
450
451  let attendees = attendeesString.split(",");
452  for (let attendee of attendees) {
453    let calAttendee = iframeWindow.attendees.find(aAtt => aAtt.id == `mailto:${attendee}`);
454    // Only add if not already present.
455    if (!calAttendee) {
456      let dialogPromise = BrowserTestUtils.promiseAlertDialog(
457        undefined,
458        "chrome://calendar/content/calendar-event-dialog-attendees.xhtml",
459        {
460          async callback(attendeesWindow) {
461            Assert.report(false, undefined, undefined, "Attendees dialog opened");
462            await sleep(attendeesWindow);
463            let attendeesDocument = attendeesWindow.document;
464
465            await sleep(attendeesWindow);
466            Assert.equal(attendeesDocument.activeElement.localName, "input");
467            Assert.equal(attendeesDocument.activeElement.value, "");
468            sendString(attendee, attendeesWindow);
469            synthesizeMouseAtCenter(
470              attendeesDocument.querySelector("dialog").getButton("accept"),
471              {},
472              attendeesWindow
473            );
474          },
475        }
476      );
477      synthesizeMouseAtCenter(dialogDocument.getElementById("button-attendees"), {}, dialogWindow);
478      await dialogPromise;
479      Assert.report(false, undefined, undefined, "Attendees dialog closed");
480      await sleep(iframeWindow);
481    }
482  }
483}
484
485/**
486 * Delete attendees from the event.
487 *
488 * @param {Window} iframeWindow - The event dialog iframe.
489 * @param {string} attendeesString - Comma separated list of email-addresses to delete.
490 */
491async function deleteAttendees(iframeWindow, attendeesString) {
492  let iframeDocument = iframeWindow.document;
493  let menupopup = iframeDocument.getElementById("attendee-popup");
494
495  // Now delete the attendees.
496  let attendees = attendeesString.split(",");
497  for (let attendee of attendees) {
498    let attendeeToDelete = iframeDocument.querySelector(
499      `.attendee-list [attendeeid="mailto:${attendee}"]`
500    );
501    if (attendeeToDelete) {
502      attendeeToDelete.focus();
503      synthesizeMouseAtCenter(attendeeToDelete, { type: "contextmenu" }, iframeWindow);
504      await BrowserTestUtils.waitForEvent(menupopup, "popupshown");
505      menupopup.activateItem(
506        iframeDocument.getElementById("attendee-popup-removeattendee-menuitem")
507      );
508      await BrowserTestUtils.waitForEvent(menupopup, "popuphidden");
509    }
510  }
511  await sleep(iframeWindow);
512}
513
514/**
515 * Set the timezone for the item
516 *
517 * @param {Window} dialogWindow - The event dialog.
518 * @param {Window} iframeWindow - The event dialog iframe.
519 * @param {string} timezone - String identifying the timezone.
520 */
521async function setTimezone(dialogWindow, iframeWindow, timezone) {
522  let dialogDocument = dialogWindow.document;
523  let iframeDocument = iframeWindow.document;
524
525  let menuitem = dialogDocument.getElementById("options-timezones-menuitem");
526  let label = iframeDocument.getElementById("timezone-starttime");
527  let menupopup = iframeDocument.getElementById("timezone-popup");
528  let customMenuitem = iframeDocument.getElementById("timezone-custom-menuitem");
529
530  if (!BrowserTestUtils.is_visible(label)) {
531    menuitem.click();
532    await sleep(iframeWindow);
533  }
534
535  Assert.ok(BrowserTestUtils.is_visible(label));
536
537  let shownPromise = BrowserTestUtils.waitForEvent(menupopup, "popupshown");
538  let dialogPromise = BrowserTestUtils.promiseAlertDialog(
539    undefined,
540    "chrome://calendar/content/calendar-event-dialog-timezone.xhtml",
541    {
542      async callback(timezoneWindow) {
543        Assert.report(false, undefined, undefined, "Timezone dialog opened");
544        if (Services.focus.activeWindow != timezoneWindow) {
545          let focus = BrowserTestUtils.waitForEvent(timezoneWindow, "focus", true);
546          timezoneWindow.focus();
547          await focus;
548        }
549
550        let timezoneDocument = timezoneWindow.document;
551        let timezoneMenulist = timezoneDocument.getElementById("timezone-menulist");
552        let timezoneMenuitem = timezoneMenulist.querySelector(`[value="${timezone}"]`);
553
554        let popupshown = BrowserTestUtils.waitForEvent(timezoneMenulist, "popupshown");
555        synthesizeMouseAtCenter(timezoneMenulist, {}, timezoneWindow);
556        await popupshown;
557
558        timezoneMenuitem.scrollIntoView();
559
560        let popuphidden = BrowserTestUtils.waitForEvent(timezoneMenulist, "popuphidden");
561        synthesizeMouseAtCenter(timezoneMenuitem, {}, timezoneWindow);
562        await popuphidden;
563
564        synthesizeMouseAtCenter(
565          timezoneDocument.querySelector("dialog").getButton("accept"),
566          {},
567          timezoneWindow
568        );
569      },
570    }
571  );
572
573  synthesizeMouseAtCenter(label, {}, iframeWindow);
574  await shownPromise;
575
576  synthesizeMouseAtCenter(customMenuitem, {}, iframeWindow);
577  await dialogPromise;
578  Assert.report(false, undefined, undefined, "Timezone dialog closed");
579
580  await new Promise(resolve => iframeWindow.setTimeout(resolve, 500));
581}
582
583/**
584 * Selects an item from a menulist.
585 *
586 * @param {Element} menulist
587 * @param {string} value
588 */
589async function menulistSelect(menulist, value) {
590  let win = menulist.ownerGlobal;
591  Assert.ok(menulist, `<menulist id=${menulist.id}> exists`);
592  let menuitem = menulist.querySelector(`menupopup > menuitem[value='${value}']`);
593  Assert.ok(menuitem, `<menuitem value=${value}> exists`);
594
595  menulist.focus();
596
597  let shownPromise = BrowserTestUtils.waitForEvent(menulist, "popupshown");
598  synthesizeMouseAtCenter(menulist, {}, win);
599  await shownPromise;
600
601  let hiddenPromise = BrowserTestUtils.waitForEvent(menulist, "popuphidden");
602  synthesizeMouseAtCenter(menuitem, {}, win);
603  await hiddenPromise;
604
605  await new Promise(resolve => win.setTimeout(resolve));
606  Assert.equal(menulist.value, value);
607}
608