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