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/. */
var { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs");
var { TestUtils } = ChromeUtils.importESModule("resource://testing-common/TestUtils.sys.mjs");
ChromeUtils.defineESModuleGetters(this, {
});
var EXPECT_NONE = 0;
var EXPECT_FIRED = 1;
var EXPECT_TIMER = 2;
function do_check_xor(a, b, aMessage) {
return ok((a && !b) || (!a && b), aMessage);
}
var alarmObserver = {
QueryInterface: ChromeUtils.generateQI(["calIAlarmServiceObserver"]),
service: null,
firedMap: {},
expectedMap: {},
pendingOps: {},
onAlarm(aItem, aAlarm) {
this.firedMap[aItem.hashId] = this.firedMap[aItem.hashId] || {};
this.firedMap[aItem.hashId][aAlarm.icalString] = true;
},
onNotification() {},
onRemoveAlarmsByItem(aItem) {
if (aItem.hashId in this.firedMap) {
delete this.firedMap[aItem.hashId];
}
},
onRemoveAlarmsByCalendar() {},
onAlarmsLoaded(aCalendar) {
this.checkLoadStatus();
if (aCalendar.id in this.pendingOps) {
this.pendingOps[aCalendar.id].call();
}
},
async doOnAlarmsLoaded(aCalendar) {
this.checkLoadStatus();
if (
aCalendar.id in this.service.mLoadedCalendars &&
this.service.mLoadedCalendars[aCalendar.id]
) {
// the calendar's alarms have already been loaded
} else {
await new Promise(resolve => {
// the calendar hasn't been fully loaded yet, set as a pending operation
this.pendingOps[aCalendar.id] = resolve;
});
}
},
getTimer(aCalendarId, aItemId, aAlarmStr) {
return aCalendarId in this.service.mTimerMap &&
aItemId in this.service.mTimerMap[aCalendarId] &&
aAlarmStr in this.service.mTimerMap[aCalendarId][aItemId]
? this.service.mTimerMap[aCalendarId][aItemId][aAlarmStr]
: null;
},
expectResult(aCalendar, aItem, aAlarm, aExpected) {
const expectedAndTitle = {
expected: aExpected,
title: aItem.title,
};
this.expectedMap[aCalendar.id] = this.expectedMap[aCalendar.id] || {};
this.expectedMap[aCalendar.id][aItem.hashId] =
this.expectedMap[aCalendar.id][aItem.hashId] || {};
this.expectedMap[aCalendar.id][aItem.hashId][aAlarm.icalString] = expectedAndTitle;
},
expectOccurrences(aCalendar, aItem, aAlarm, aExpectedArray) {
// we need to be earlier than the first occurrence
let date = aItem.startDate.clone();
date.second -= 1;
for (const expected of aExpectedArray) {
const occ = aItem.recurrenceInfo.getNextOccurrence(date);
occ.QueryInterface(Ci.calIEvent);
date = occ.startDate;
this.expectResult(aCalendar, occ, aAlarm, expected);
}
},
checkExpected(aMessage) {
for (const calId in this.expectedMap) {
for (const id in this.expectedMap[calId]) {
for (const icalString in this.expectedMap[calId][id]) {
const expectedAndTitle = this.expectedMap[calId][id][icalString];
// if no explicit message has been passed, take the item title
const message = typeof aMessage == "string" ? aMessage : expectedAndTitle.title;
// only alarms expected as fired should exist in our fired alarm map
do_check_xor(
expectedAndTitle.expected != EXPECT_FIRED,
id in this.firedMap && icalString in this.firedMap[id],
message + "; check fired"
);
// only alarms expected as timers should exist in the service's timer map
do_check_xor(
expectedAndTitle.expected != EXPECT_TIMER,
!!this.getTimer(calId, id, icalString),
message + "; check timer"
);
}
}
}
},
checkLoadStatus() {
for (const calId in this.service.mLoadedCalendars) {
if (!this.service.mLoadedCalendars[calId]) {
// at least one calendar hasn't finished loading alarms
ok(this.service.isLoading);
return;
}
}
ok(!this.service.isLoading);
},
clear() {
this.firedMap = {};
this.pendingOps = {};
this.expectedMap = {};
},
};
add_setup(async function () {
do_get_profile();
await new Promise(resolve =>
do_calendar_startup(() => {
alarmObserver.service = Cc["@mozilla.org/calendar/alarm-service;1"].getService(
Ci.calIAlarmService
).wrappedJSObject;
ok(!alarmObserver.service.mStarted);
alarmObserver.service.startup(null);
ok(alarmObserver.service.mStarted);
// we need to replace the existing observers with our observer
for (const obs of alarmObserver.service.mObservers.values()) {
alarmObserver.service.removeObserver(obs);
}
alarmObserver.service.addObserver(alarmObserver);
resolve();
})
);
});
function createAlarmFromDuration(aOffset) {
const alarm = new CalAlarm();
alarm.related = Ci.calIAlarm.ALARM_RELATED_START;
alarm.offset = cal.createDuration(aOffset);
return alarm;
}
function createEventWithAlarm(aCalendar, aStart, aEnd, aOffset, aRRule) {
let alarm = null;
const item = new CalEvent();
item.id = cal.getUUID();
item.calendar = aCalendar;
item.startDate = aStart || cal.dtz.now();
item.endDate = aEnd || cal.dtz.now();
if (aOffset) {
alarm = createAlarmFromDuration(aOffset);
item.addAlarm(alarm);
}
if (aRRule) {
item.recurrenceInfo = new CalRecurrenceInfo(item);
item.recurrenceInfo.appendRecurrenceItem(cal.createRecurrenceRule(aRRule));
}
return [item, alarm];
}
async function addTestItems(aCalendar) {
let item, alarm;
// alarm on an item starting more than a month in the past should not fire
let date = cal.dtz.now();
date.day -= 32;
[item, alarm] = createEventWithAlarm(aCalendar, date, date, "P7D");
item.title = "addTestItems Test 1";
alarmObserver.expectResult(aCalendar, item, alarm, EXPECT_NONE);
await aCalendar.addItem(item);
// alarm 15 minutes ago should fire
date = cal.dtz.now();
[item, alarm] = createEventWithAlarm(aCalendar, date, date, "-PT15M");
item.title = "addTestItems Test 2";
alarmObserver.expectResult(aCalendar, item, alarm, EXPECT_FIRED);
await aCalendar.addItem(item);
// alarm within 6 hours should have a timer set
[item, alarm] = createEventWithAlarm(aCalendar, date, date, "PT1H");
item.title = "addTestItems Test 3";
alarmObserver.expectResult(aCalendar, item, alarm, EXPECT_TIMER);
await aCalendar.addItem(item);
// alarm more than 6 hours in the future should not have a timer set
[item, alarm] = createEventWithAlarm(aCalendar, date, date, "PT7H");
item.title = "addTestItems Test 4";
alarmObserver.expectResult(aCalendar, item, alarm, EXPECT_NONE);
await aCalendar.addItem(item);
// test multiple alarms on an item
[item, alarm] = createEventWithAlarm(aCalendar, date, date);
item.title = "addTestItems Test 5";
const firedOffsets = [
["-PT1H", EXPECT_FIRED],
["-PT15M", EXPECT_FIRED],
["PT1H", EXPECT_TIMER],
["PT7H", EXPECT_NONE],
["P7D", EXPECT_NONE],
];
firedOffsets.forEach(([offset, expected]) => {
alarm = createAlarmFromDuration(offset);
item.addAlarm(alarm);
alarmObserver.expectResult(aCalendar, item, alarm, expected);
});
await aCalendar.addItem(item);
// Bug 1344068 - Alarm with lastAck on exception, should take parent lastAck.
// Alarm 15 minutes ago should fire.
date = cal.dtz.now();
[item, alarm] = createEventWithAlarm(aCalendar, date, date, "-PT15M", "RRULE:FREQ=DAILY;COUNT=1");
item.title = "addTestItems Test 6";
// Parent item is acknowledged before alarm, so it should fire.
const lastAck = item.startDate.clone();
lastAck.hour -= 1;
item.alarmLastAck = lastAck;
// Occurrence is acknowledged after alarm (start date), so if the alarm
// service wrongly uses the exception occurrence then we catch it.
const occ = item.recurrenceInfo.getOccurrenceFor(item.startDate);
occ.alarmLastAck = item.startDate.clone();
item.recurrenceInfo.modifyException(occ, true);
alarmObserver.expectOccurrences(aCalendar, item, alarm, [EXPECT_FIRED]);
await aCalendar.addItem(item);
// daily repeating event starting almost 2 full days ago. The alarms on the first 2 occurrences
// should fire, and a timer should be set for the next occurrence only
date = cal.dtz.now();
date.hour -= 47;
[item, alarm] = createEventWithAlarm(aCalendar, date, date, "-PT15M", "RRULE:FREQ=DAILY");
item.title = "addTestItems Test 7";
alarmObserver.expectOccurrences(aCalendar, item, alarm, [
EXPECT_FIRED,
EXPECT_FIRED,
EXPECT_TIMER,
EXPECT_NONE,
EXPECT_NONE,
]);
await aCalendar.addItem(item);
// monthly repeating event starting 2 months and a day ago. The alarms on the first 2 occurrences
// should be ignored, the alarm on the next occurrence only should fire.
// Missing recurrences of the event in particular days of the year generate exceptions to the
// regular sequence of alarms.
date = cal.dtz.now();
const statusAlarmSequences = {
reg: [EXPECT_NONE, EXPECT_NONE, EXPECT_FIRED, EXPECT_NONE, EXPECT_NONE],
excep1: [EXPECT_NONE, EXPECT_FIRED, EXPECT_NONE, EXPECT_NONE, EXPECT_NONE],
excep2: [EXPECT_NONE, EXPECT_NONE, EXPECT_NONE, EXPECT_NONE, EXPECT_NONE],
};
let expected = [];
if (date.day == 1) {
// Exceptions for missing occurrences on months with 30 days when the event starts on 31st.
const sequence = [
"excep1",
"reg",
"excep2",
"excep1",
"reg",
"excep1",
"reg",
"excep1",
"reg",
"excep2",
"excep1",
"reg",
][date.month];
expected = statusAlarmSequences[sequence];
} else if (date.day == 30 && (date.month == 2 || date.month == 3)) {
// Exceptions for missing occurrences or different start date caused by February.
const leapYear = date.endOfYear.yearday == 366;
expected = leapYear ? statusAlarmSequences.reg : statusAlarmSequences.excep1;
} else if (date.day == 31 && date.month == 2) {
// Exceptions for missing occurrences caused by February.
expected = statusAlarmSequences.excep1;
} else {
// Regular sequence of alarms expected for all the others days.
expected = statusAlarmSequences.reg;
}
date.month -= 2;
date.day -= 1;
[item, alarm] = createEventWithAlarm(aCalendar, date, date, "-PT15M", "RRULE:FREQ=MONTHLY");
item.title = "addTestItems Test 8";
alarmObserver.expectOccurrences(aCalendar, item, alarm, expected);
await aCalendar.addItem(item);
}
async function doModifyItemTest(aCalendar) {
// begin with item starting before the alarm date range
const date = cal.dtz.now();
date.day -= 32;
const [item, alarm] = createEventWithAlarm(aCalendar, date, date, "PT0S");
await aCalendar.addItem(item);
alarmObserver.expectResult(aCalendar, item, alarm, EXPECT_NONE);
alarmObserver.checkExpected("doModifyItemTest Test 1");
// move event into the fired range
let oldItem = item.clone();
date.day += 31;
item.startDate = date.clone();
item.generation++;
await aCalendar.modifyItem(item, oldItem);
alarmObserver.expectResult(aCalendar, item, alarm, EXPECT_FIRED);
alarmObserver.checkExpected("doModifyItemTest Test 2");
// move event into the timer range
oldItem = item.clone();
date.hour += 25;
item.startDate = date.clone();
item.generation++;
await aCalendar.modifyItem(item, oldItem);
alarmObserver.expectResult(aCalendar, item, alarm, EXPECT_TIMER);
alarmObserver.checkExpected("doModifyItemTest Test 3");
// move event past the timer range
oldItem = item.clone();
date.hour += 6;
item.startDate = date.clone();
item.generation++;
await aCalendar.modifyItem(item, oldItem);
alarmObserver.expectResult(aCalendar, item, alarm, EXPECT_NONE);
alarmObserver.checkExpected("doModifyItemTest Test 4");
// re-move the event in the timer range and verify that the timer
// doesn't change when the timezone changes to floating (bug 1300493).
oldItem = item.clone();
date.hour -= 6;
item.startDate = date.clone();
item.generation++;
await aCalendar.modifyItem(item, oldItem);
alarmObserver.expectResult(aCalendar, item, alarm, EXPECT_TIMER);
alarmObserver.checkExpected("doModifyItemTest Test 5");
const oldTimer = alarmObserver.getTimer(aCalendar.id, item.hashId, alarm.icalString);
oldItem = item.clone();
// change the timezone to floating
item.startDate.timezone = cal.dtz.floating;
item.generation++;
await aCalendar.modifyItem(item, oldItem);
// the alarm must still be timer and with the same value (apart from milliseconds)
alarmObserver.expectResult(aCalendar, item, alarm, EXPECT_TIMER);
alarmObserver.checkExpected("doModifyItemTest Test 5, floating timezone");
const newTimer = alarmObserver.getTimer(aCalendar.id, item.hashId, alarm.icalString);
Assert.lessOrEqual(
newTimer.delay - oldTimer.delay,
1000,
"doModifyItemTest Test 5, floating timezone; check timer value"
);
}
async function doDeleteItemTest(aCalendar) {
alarmObserver.clear();
// create a fired alarm and a timer
const date = cal.dtz.now();
const [item, alarm] = createEventWithAlarm(aCalendar, date, date, "-PT5M");
const [item2, alarm2] = createEventWithAlarm(aCalendar, date, date, "PT1H");
item.title = "doDeleteItemTest item Test 1";
item2.title = "doDeleteItemTest item2 Test 1";
await aCalendar.addItem(item);
await aCalendar.addItem(item2);
alarmObserver.expectResult(aCalendar, item, alarm, EXPECT_FIRED);
alarmObserver.expectResult(aCalendar, item2, alarm2, EXPECT_TIMER);
alarmObserver.checkExpected();
// item deletion should clear the fired alarm and timer
await aCalendar.deleteItem(item);
await aCalendar.deleteItem(item2);
alarmObserver.expectResult(aCalendar, item, alarm, EXPECT_NONE);
alarmObserver.expectResult(aCalendar, item2, alarm2, EXPECT_NONE);
alarmObserver.checkExpected("doDeleteItemTest, cleared fired alarm and timer");
}
async function doAcknowledgeTest(aCalendar) {
alarmObserver.clear();
// create the fired alarms
const date = cal.dtz.now();
const [item, alarm] = createEventWithAlarm(aCalendar, date, date, "-PT5M");
const [item2, alarm2] = createEventWithAlarm(aCalendar, date, date, "-PT5M");
item.title = "doAcknowledgeTest item Test 1";
item2.title = "doAcknowledgeTest item2 Test 1";
await aCalendar.addItem(item);
await aCalendar.addItem(item2);
alarmObserver.expectResult(aCalendar, item, alarm, EXPECT_FIRED);
alarmObserver.expectResult(aCalendar, item2, alarm2, EXPECT_FIRED);
alarmObserver.checkExpected();
// test snooze alarm
alarmObserver.service.snoozeAlarm(item, alarm, cal.createDuration("PT1H"));
alarmObserver.expectResult(aCalendar, item, alarm, EXPECT_TIMER);
alarmObserver.checkExpected("doAcknowledgeTest, test snooze alarm");
// the snoozed alarm timer delay should be close to an hour
const tmr = alarmObserver.getTimer(aCalendar.id, item.hashId, alarm.icalString);
Assert.lessOrEqual(
Math.abs(tmr.delay - 3600000),
1000,
"doAcknowledgeTest, snoozed alarm timer delay close to an hour"
);
// test dismiss alarm
alarmObserver.service.dismissAlarm(item2, alarm2);
alarmObserver.expectResult(aCalendar, item2, alarm2, EXPECT_NONE);
alarmObserver.checkExpected("doAcknowledgeTest, test dismiss alarm");
}
async function doRunTest(aOnCalendarCreated) {
alarmObserver.clear();
const memory = cal.manager.createCalendar("memory", Services.io.newURI("moz-memory-calendar://"));
memory.id = cal.getUUID();
if (aOnCalendarCreated) {
await aOnCalendarCreated(memory);
}
cal.manager.registerCalendar(memory);
await alarmObserver.doOnAlarmsLoaded(memory);
return memory;
}
/**
* Test the initial alarm loading of a calendar with existing data.
*/
add_task(async function test_loadCalendar() {
await doRunTest(async memory => addTestItems(memory));
alarmObserver.checkExpected();
});
/**
* Test adding alarm data to a calendar already registered.
*/
add_task(async function test_addItems() {
const memory = await doRunTest();
await addTestItems(memory);
alarmObserver.checkExpected();
});
/**
* Test response to modification of alarm data.
*/
add_task(async function test_modifyItems() {
const memory = await doRunTest();
await doModifyItemTest(memory);
await doDeleteItemTest(memory);
await doAcknowledgeTest(memory);
});
/**
* Test an array of timers has expected delay values.
*
* @param {nsITimer[]} timers - An array of nsITimer.
* @param {number[]} expected - Expected delays in seconds.
*/
function matchTimers(timers, expected) {
const delays = timers.map(timer => timer.delay / 1000);
let matched = true;
for (let i = 0; i < delays.length; i++) {
if (Math.abs(delays[i] - expected[i]) > 2) {
matched = false;
break;
}
}
ok(matched, `Delays=${delays} should match Expected=${expected}`);
}
/**
* Test notification timers are set up correctly when add/modify/remove a
* calendar item.
*/
add_task(async function test_notificationTimers() {
const memory = await doRunTest();
// Add an item.
const date = cal.dtz.now();
date.hour += 1;
const [item] = createEventWithAlarm(memory, date, date, null);
await memory.addItem(item);
equal(
alarmObserver.service.mNotificationTimerMap[item.calendar.id],
undefined,
"should have no notification timer"
);
// Set the pref to have one notifiaction.
Services.prefs.setCharPref("calendar.notifications.times", "-PT1H");
let oldItem = item.clone();
date.hour += 1;
item.startDate = date.clone();
item.generation++;
await memory.modifyItem(item, oldItem);
// Should have one notification timer
matchTimers(alarmObserver.service.mNotificationTimerMap[item.calendar.id][item.hashId], [3600]);
// Set the pref to have three notifiactions.
Services.prefs.setCharPref("calendar.notifications.times", "END:PT2M,PT0M,END:-PT30M,-PT5M");
oldItem = item.clone();
date.hour -= 1;
item.startDate = date.clone();
date.hour += 1;
item.endDate = date.clone();
item.generation++;
await memory.modifyItem(item, oldItem);
// Should have four notification timers.
matchTimers(alarmObserver.service.mNotificationTimerMap[item.calendar.id][item.hashId], [
3300, // 55 minutes
3600, // 60 minutes
5400, // 90 minutes, which is 30 minutes before the end (END:-PT30M)
7320, // 122 minutes, which is 2 minutes after the end (END:PT2M)
]);
alarmObserver.service.removeFiredNotificationTimer(item);
// Should have three notification timers.
matchTimers(
alarmObserver.service.mNotificationTimerMap[item.calendar.id][item.hashId],
[3600, 5400, 7320]
);
await memory.deleteItem(item);
equal(
alarmObserver.service.mNotificationTimerMap[item.calendar.id],
undefined,
"notification timers should be removed"
);
Services.prefs.clearUserPref("calendar.notifications.times");
});
/**
* Test notification timers are set up correctly according to the calendar level
* notifications.times config.
*/
add_task(async function test_calendarLevelNotificationTimers() {
let loaded = false;
let item;
const memory = await doRunTest();
if (!loaded) {
loaded = true;
// Set the global pref to have one notifiaction.
Services.prefs.setCharPref("calendar.notifications.times", "-PT1H");
// Add an item.
const date = cal.dtz.now();
date.hour += 2;
[item] = createEventWithAlarm(memory, date, date, null);
await memory.addItem(item);
// Should have one notification timer.
matchTimers(alarmObserver.service.mNotificationTimerMap[item.calendar.id][item.hashId], [3600]);
// Set the calendar level pref to have two notification timers.
memory.setProperty("notifications.times", "-PT5M,PT0M");
}
await TestUtils.waitForCondition(
() => alarmObserver.service.mNotificationTimerMap[item.calendar.id]?.[item.hashId].length == 2
);
// Should have two notification timers
matchTimers(alarmObserver.service.mNotificationTimerMap[item.calendar.id][item.hashId], [
6900, // 105 minutes
7200, // 120 minutes
]);
Services.prefs.clearUserPref("calendar.notifications.times");
});
registerCleanupFunction(() => {
Services.prefs.clearUserPref("calendar.notifications.times");
});