From 3746b1cdc0e344460c58cd52f85564b0c4f5940a Mon Sep 17 00:00:00 2001 From: Eric O'Connor Date: Fri, 26 Jul 2013 00:57:22 -0400 Subject: [PATCH] Bug 898666 - [clock] Clock refactor alarm object and add test 1. An Alarm constructor is used to create new Alarms, instead of creating new object literals. 2. The AlarmsDB API is changed to pass error messages in the function(err, value) {...} style. 3. Asyncronous API calls are checked using callbacks. The async functions in Utils make this easy (although including async.js would be easier. 4. Testing is added for the Alarm object. 5. Added mock_alarmsDB and mock_mozAlarms for testing Alarms 6. Fixed alarm_edit tests. Moved to a new alarm_edit_test.js file. 7. Add tests for new utils methods (async and safeCpuLock) 8. Rename some variables. Remove "_name" naming scheme. 9. Removed in app alarm set indicator 10. Protect id, repeat, and enabled properties on the Alarm object. r=jugglinmike --- apps/clock/index.html | 1 + apps/clock/js/active_alarm.js | 149 ++--- apps/clock/js/alarm.js | 366 ++++++++++++ apps/clock/js/alarm_edit.js | 77 ++- apps/clock/js/alarm_list.js | 110 ++-- apps/clock/js/alarm_manager.js | 186 ++---- apps/clock/js/alarmsdb.js | 78 +-- apps/clock/js/onring.js | 78 +-- apps/clock/js/utils.js | 207 ++++--- apps/clock/test/unit/alarm_edit_test.js | 285 +++++++++ apps/clock/test/unit/alarm_test.js | 552 +++++++++++++----- .../test/unit/mocks/mock_alarm_manager.js | 7 +- apps/clock/test/unit/mocks/mock_alarmsDB.js | 71 +++ apps/clock/test/unit/mocks/mock_mozAlarm.js | 104 ++++ .../test/unit/mocks/mock_requestWakeLock.js | 45 ++ apps/clock/test/unit/utils_test.js | 446 +++++++------- 16 files changed, 1896 insertions(+), 866 deletions(-) create mode 100644 apps/clock/js/alarm.js create mode 100644 apps/clock/test/unit/alarm_edit_test.js create mode 100644 apps/clock/test/unit/mocks/mock_alarmsDB.js create mode 100644 apps/clock/test/unit/mocks/mock_mozAlarm.js create mode 100644 apps/clock/test/unit/mocks/mock_requestWakeLock.js diff --git a/apps/clock/index.html b/apps/clock/index.html index 25ad0b845a6a..2a3694c48474 100644 --- a/apps/clock/index.html +++ b/apps/clock/index.html @@ -30,6 +30,7 @@ + diff --git a/apps/clock/js/active_alarm.js b/apps/clock/js/active_alarm.js index 18d24fbd7727..34728204e0d5 100644 --- a/apps/clock/js/active_alarm.js +++ b/apps/clock/js/active_alarm.js @@ -12,113 +12,90 @@ var ActiveAlarm = { * A snooze alarm should be turned off. */ - _onFireAlarm: {}, - _onFireChildWindow: null, + firedAlarm: null, + message: null, + childwindow: null, init: function am_init() { - var self = this; - navigator.mozSetMessageHandler('alarm', function gotMessage(message) { - self.onAlarmFiredHandler(message); - }); + navigator.mozSetMessageHandler('alarm', this.handler.bind(this)); AlarmManager.updateAlarmStatusBar(); }, - onAlarmFiredHandler: function aac_onAlarmFiredHandler(message) { - // We have to ensure the CPU doesn't sleep during the process of - // handling alarm message, so that it can be handled on time. - var cpuWakeLock = navigator.requestWakeLock('cpu'); - + handler: function aac_handler(message) { // Set a watchdog to avoid locking the CPU wake lock too long, // because it'd exhaust the battery quickly which is very bad. // This could probably happen if the app failed to launch or // handle the alarm message due to any unexpected reasons. - var unlockCpuWakeLock = function unlockCpuWakeLock() { - if (cpuWakeLock) { - cpuWakeLock.unlock(); - cpuWakeLock = null; - } - }; - setTimeout(unlockCpuWakeLock, 30000); + Utils.safeCpuLock(30000, function(done) { + // receive and parse the alarm id from the message + var id = message.data.id; + var type = message.data.type; - // receive and parse the alarm id from the message - var id = message.data.id; - var type = message.data.type; - // clear the requested id of went off alarm to DB - var clearAlarmRequestId = function clearAlarmRequestId(alarm, callback) { - if (type === 'normal') { - alarm.normalAlarmId = ''; - } else { - alarm.snoozeAlarmId = ''; - } - - AlarmManager.putAlarm(alarm, function aac_putAlarm(alarmFromDB) { - // Set the next repeat alarm when nornal alarm goes off. - if (type === 'normal' && - !Utils.isEmptyRepeat(alarmFromDB.repeat) && - callback) { - alarmFromDB.enabled = false; - callback(alarmFromDB); - } else { - // Except repeat alarm, the active alarm should be turned off. - if (!alarmFromDB.normalAlarmId) - AlarmList.toggleAlarmEnableState(false, alarmFromDB); - } + // Unlock the CPU when these functions have been called + var finalizer = Utils.async.namedParallel([ + 'onReschedule', + 'onReceivedAlarm' + ], function(err) { + AlarmList.refresh(); + AlarmManager.updateAlarmStatusBar(); + done(); }); - }; - - // set the next repeat alarm - var setRepeatAlarm = function setRepeatAlarm(alarm) { - AlarmList.toggleAlarmEnableState(true, alarm); - }; - - // use the alarm id to query db - // find out which alarm is being fired. - var self = this; - AlarmManager.getAlarmById(id, function aac_gotAlarm(alarm) { - if (!alarm) { - unlockCpuWakeLock(); - return; - } - // clear the requested id of went off alarm to DB - clearAlarmRequestId(alarm, setRepeatAlarm); // If previous active alarm is showing, // turn it off and stop its notification - if (self._onFireChildWindow !== null && - typeof self._onFireChildWindow !== 'undefined' && - !self._onFireChildWindow.closed) { - if (self._onFireChildWindow.RingView) { - self._onFireChildWindow.RingView.stopAlarmNotification(); - } + if (this.childwindow !== null && + typeof this.childwindow !== 'undefined' && + !this.childwindow.closed) { + if (this.childwindow.RingView) { + this.childwindow.RingView.stopAlarmNotification(); } + } - // prepare to pop out attention screen, ring the ringtone, vibrate - self._onFireAlarm = alarm; - var protocol = window.location.protocol; - var host = window.location.host; - self._onFireChildWindow = - window.open(protocol + '//' + host + '/onring.html', - 'ring_screen', 'attention'); - self._onFireChildWindow.onload = function childWindowLoaded() { - unlockCpuWakeLock(); - }; + AlarmsDB.getAlarm(id, function aac_gotAlarm(err, alarm) { + if (err) { + done(); + return; + } + this.firedAlarm = alarm; + if (type === 'normal') { + alarm.schedule({ + type: 'normal', + first: false + }, alarm.saveCallback(finalizer.onReschedule)); + } else { + alarm.cancel('snooze'); + alarm.save(finalizer.onReschedule); + } + // prepare to pop out attention screen, ring the ringtone, vibrate + this.firedAlarm = alarm; + this.message = message; + var protocol = window.location.protocol; + var host = window.location.host; + this.childwindow = + window.open(protocol + '//' + host + '/onring.html', + 'ring_screen', 'attention'); + finalizer.onReceivedAlarm(); + }.bind(this)); - }); - AlarmManager.updateAlarmStatusBar(); + }.bind(this)); }, snoozeHandler: function aac_snoozeHandler() { - var id = this._onFireAlarm.id; - AlarmManager.getAlarmById(id, function aac_gotAlarm(alarm) { - alarm.enabled = true; - AlarmManager.putAlarm(alarm, function aac_putAlarm(alarm) { - AlarmManager.set(alarm, true); // set a snooze alarm + Utils.safeCpuLock(30000, function(done) { + var id = this.firedAlarm.id; + AlarmsDB.getAlarm(id, function aac_gotAlarm(err, alarm) { + if (err) { + return; + } + alarm.schedule({ + type: 'snooze' + }, alarm.saveCallback(function(err, alarm) { + AlarmList.refreshItem(alarm); + AlarmManager.updateAlarmStatusBar(); + done(); + }.bind(this))); }); - }); - }, - - getOnFireAlarm: function aac_getOnFireAlarm() { - return this._onFireAlarm; + }.bind(this)); } }; diff --git a/apps/clock/js/alarm.js b/apps/clock/js/alarm.js new file mode 100644 index 000000000000..409e09f2e4e8 --- /dev/null +++ b/apps/clock/js/alarm.js @@ -0,0 +1,366 @@ +(function(exports) { + + 'use strict'; + + // define WeakMaps for protected properties + var protectedProperties = (function() { + var protectedWeakMaps = new Map(); + ['id', 'repeat', 'registeredAlarms'].forEach(function(x) { + protectedWeakMaps.set(x, new WeakMap()); + }); + return protectedWeakMaps; + })(); + + // define variables + var idMap = protectedProperties.get('id'); + var repeatMap = protectedProperties.get('repeat'); + var registeredAlarmsMap = protectedProperties.get('registeredAlarms'); + + // --------------------------------------------------------- + // Alarm Object + + function Alarm(config) { + if (config instanceof Alarm) { + config = config.toSerializable(); + } + var econfig = Utils.extend(this.defaultProperties(), config || {}); + this.extractProtected(econfig); + Utils.extend(this, econfig); + } + + Alarm.prototype = { + + constructor: Alarm, + + // --------------------------------------------------------- + // Initialization methods + + extractProtected: function(config) { + for (var i in config) { + if (protectedProperties.has(i)) { + var map = protectedProperties.get(i); + map.set(this, config[i]); + delete config[i]; + } + } + }, + + defaultProperties: function() { + var now = new Date(); + return { + registeredAlarms: {}, // set -> this.schedule & this.cancel + repeat: {}, + hour: now.getHours(), + minute: now.getMinutes(), + + // Raw Fields + label: '', + sound: 'ac_classic_clock_alarm.opus', + vibrate: 1, + snooze: 5, + color: 'Darkorange' + }; + }, + + // --------------------------------------------------------- + // Persisted form + + toSerializable: function alarm_toSerializable() { + var retval = {}; + for (var i in this) { + if (this.hasOwnProperty(i)) { + retval[i] = this[i]; + } + } + for (var kv of protectedProperties) { + var prop = kv[0], map = kv[1]; + if (map.has(this) && map.get(this) !== undefined) { + retval[prop] = map.get(this); + } + } + return retval; + }, + + // --------------------------------------------------------- + // Getters and Setters + + set time(x) { + // destructure passed array + this.minute = +x[1]; + this.hour = +x[0]; + }, + + get time() { + return [this.hour, this.minute]; + }, + + get id() { + return idMap.get(this) || undefined; + }, + + get registeredAlarms() { + return registeredAlarmsMap.get(this) || {}; + }, + + set repeat(x) { + var rep = {}; + for (var y of DAYS) { + if (x[y] === true) { + rep[y] = true; + } + } + repeatMap.set(this, rep); + }, + + get repeat() { + return repeatMap.get(this); + }, + + set enabled(x) { + throw 'use setEnabled to set (async requires callback)'; + }, + + get enabled() { + for (var i in this.registeredAlarms) { + if (i === 'normal') { + return true; + } + } + return false; + }, + + // --------------------------------------------------------- + // Time Handling + + summarizeDaysOfWeek: function alarm_summarizeRepeat() { + var _ = navigator.mozL10n.get; + // Build a bitset + var value = 0; + for (var i = 0; i < DAYS.length; i++) { + var dayName = DAYS[i]; + if (this.repeat[dayName] === true) { + value |= (1 << i); + } + } + var summary; + if (value === 127) { // 127 = 0b1111111 + summary = _('everyday'); + } else if (value === 31) { // 31 = 0b0011111 + summary = _('weekdays'); + } else if (value === 96) { // 96 = 0b1100000 + summary = _('weekends'); + } else if (value !== 0) { // any day was true + var weekdays = []; + for (var i = 0; i < DAYS.length; i++) { + var dayName = DAYS[i]; + if (this.repeat[dayName]) { + // Note: here, Monday is the first day of the week + // whereas in JS Date(), it's Sunday -- hence the (+1) here. + weekdays.push(_('weekday-' + ((i + 1) % 7) + '-short')); + } + summary = weekdays.join(', '); + } + } else { // no day was true + summary = _('never'); + } + return summary; + }, + + isAlarmPassedToday: function alarm_isAlarmPassedToday() { + var now = new Date(); + if (this.hour > now.getHours() || + (this.hour === now.getHours() && + this.minute > now.getMinutes())) { + return false; + } + return true; + }, + + isDateInRepeat: function alarm_isDateInRepeat(date) { + // return true if repeat contains date + var day = DAYS[(date.getDay() + 6) % 7]; + return !!this.repeat[day]; + }, + + repeatDays: function alarm_repeatDays() { + var count = 0; + for (var i in this.repeat) { + if (this.repeat[i]) { + count++; + } + } + return count; + }, + + isRepeating: function alarm_isRepeating() { + return this.repeatDays() !== 0; + }, + + getNextAlarmFireTime: function alarm_getNextAlarmFireTime() { + var now = new Date(), nextFire = new Date(); + nextFire.setHours(this.hour, this.minute, 0, 0); + while (nextFire <= now || + !(this.repeatDays() === 0 || + this.isDateInRepeat(nextFire))) { + nextFire.setDate(nextFire.getDate() + 1); + } + return nextFire; + }, + + getNextSnoozeFireTime: function alarm_getNextSnoozeFireTime() { + if (this.snooze && (typeof this.snooze) === 'number') { + var now = new Date(); + now.setMinutes(now.getMinutes() + this.snooze); + return now; + } + return null; + }, + + // --------------------------------------------------------- + // Wholistic methods (Alarm API and Database) + + setEnabled: function alarm_setEnabled(value, callback) { + if (value) { + var scheduleWithID = function(err, alarm) { + this.schedule({ + type: 'normal', + first: true + }, this.saveCallback(callback)); + }; + if (!this.id) { + // if we don't have an ID yet, save to IndexedDB to + // get one, and then call scheduleWithID + this.save(scheduleWithID.bind(this)); + } else { + // otherwise, just call scheduleWithID + setTimeout(scheduleWithID.bind(this, null, this), 0); + } + } else if (this.enabled) { + this.cancel(); + this.save(callback); + } else if (callback) { + setTimeout(callback.bind(undefined, null, this), 0); + } + }, + + delete: function alarm_delete(callback) { + this.cancel(); + AlarmsDB.deleteAlarm(this.id, + function alarm_innerDelete(err, alarm) { + callback(err, this); + }.bind(this)); + }, + + // --------------------------------------------------------- + // Database Integration + + saveCallback: function alarm_saveCallback(callback) { + return function(err, value) { + if (!err) { + this.save(callback); + } else { + if (callback) { + callback(err, value); + } + } + }.bind(this); + }, + + save: function alarm_save(callback) { + AlarmsDB.putAlarm(this, function(err, alarm) { + idMap.set(this, alarm.id); + callback && callback(err, this); + }.bind(this)); + }, + + // --------------------------------------------------------- + // Alarm API + + scheduleHelper: function alarm_scheduleHelper(type, date, callback) { + var data = { + id: this.id, + type: type + }; + var request = navigator.mozAlarms.add( + date, 'ignoreTimezone', data); + request.onsuccess = (function(ev) { + var registeredAlarms = registeredAlarmsMap.get(this) || {}; + registeredAlarms[type] = ev.target.result; + registeredAlarmsMap.set(this, registeredAlarms); + if (callback) { + callback(null, this); + } + }).bind(this); + request.onerror = function(ev) { + if (callback) { + callback(ev.target.error); + } + }; + }, + + schedule: function alarm_schedule(options, callback) { + /* + * Schedule + * + * Schedule a mozAlarm to wake up the app at a certain time. + * + * @options {Object} an object containing parameters for the + * scheduled alarm. + * - type: 'normal' or 'snooze' + * - first: {boolean} + * + * First is used true when an alarm is "first" in a sequence + * of repeating normal alarms. + * For no-repeat alarms, the sequence of length 1, and so + * the alarm is always first. + * Snooze alarms are never first, since they have a normal + * alarm parent. + * + */ + options = options || {}; // defaults + if (typeof options.type === 'undefined') { + options.type = 'normal'; + } + if (typeof options.first === 'undefined') { + options.first = true; + } + if (!options.first && !this.isRepeating()) { + this.cancel('normal'); + callback(null, this); + return; + } + this.cancel(options.type); + if (options.type === 'normal') { + var firedate = this.getNextAlarmFireTime(); + } else if (options.type === 'snooze') { + var firedate = this.getNextSnoozeFireTime(); + } + this.scheduleHelper(options.type, firedate, callback); + }, + + cancel: function alarm_cancel(cancelType) { + // cancel an alarm type ('normal' or 'snooze') + // type == false to cancel all + function removeAlarm(type, id) { + navigator.mozAlarms.remove(id); + var registeredAlarms = this.registeredAlarms; + delete registeredAlarms[type]; + registeredAlarmsMap.set(this, registeredAlarms); + } + if (!cancelType) { + for (var type in this.registeredAlarms) { + removeAlarm.call(this, type, this.registeredAlarms[type]); + } + } else { + removeAlarm.call(this, cancelType, this.registeredAlarms[cancelType]); + } + } + + }; + + // --------------------------------------------------------- + // Export + + exports.Alarm = Alarm; + +})(this); diff --git a/apps/clock/js/alarm_edit.js b/apps/clock/js/alarm_edit.js index ebf5116b6a31..e689e8edd225 100644 --- a/apps/clock/js/alarm_edit.js +++ b/apps/clock/js/alarm_edit.js @@ -1,6 +1,7 @@ var AlarmEdit = { - alarm: {}, + alarm: null, + alarmRef: null, timePicker: { hour: null, minute: null, @@ -125,10 +126,12 @@ var AlarmEdit = { break; case this.doneButton: ClockView.show(); - if (!this.save()) { - evt.preventDefault(); - return; - } + this.save(function aev_saveCallback(err, alarm) { + if (err) { + return; + } + AlarmList.refreshItem(alarm); + }); break; case this.timeMenu: this.focusMenu(this.timeSelect); @@ -179,25 +182,6 @@ var AlarmEdit = { setTimeout(function() { menu.focus(); }, 10); }, - getDefaultAlarm: function aev_getDefaultAlarm() { - // Reset the required message with default value - var now = new Date(); - return { - id: '', // for Alarm APP indexedDB id - normalAlarmId: '', // for request AlarmAPI id (once, repeat) - snoozeAlarmId: '', // for request AlarmAPI id (snooze) - label: '', - hour: now.getHours(), // use current hour - minute: now.getMinutes(), // use current minute - enabled: true, - repeat: {}, - sound: 'ac_classic_clock_alarm.opus', - vibrate: 1, - snooze: 5, - color: 'Darkorange' - }; - }, - load: function aev_load(alarm) { if (this.element.classList.contains('hidden')) { this.element.classList.remove('hidden'); @@ -212,12 +196,12 @@ var AlarmEdit = { if (!alarm) { this.element.classList.add('new'); this.alarmTitle.textContent = _('newAlarm'); - alarm = this.getDefaultAlarm(); + alarm = new Alarm(); } else { this.element.classList.remove('new'); this.alarmTitle.textContent = _('editAlarm'); } - this.alarm = alarm; + this.alarm = new Alarm(alarm); this.element.dataset.id = alarm.id; this.labelInput.value = alarm.label; @@ -275,8 +259,12 @@ var AlarmEdit = { }, refreshRepeatMenu: function aev_refreshRepeatMenu(repeatOpts) { - var daysOfWeek = (repeatOpts) ? repeatOpts : this.alarm.repeat; - this.repeatMenu.textContent = Utils.summarizeDaysOfWeek(daysOfWeek); + var daysOfWeek; + if (repeatOpts) { + this.alarm.repeat = this.getRepeatSelect(); + } + daysOfWeek = this.alarm.repeat; + this.repeatMenu.textContent = this.alarm.summarizeDaysOfWeek(daysOfWeek); }, initSoundSelect: function aev_initSoundSelect() { @@ -355,35 +343,46 @@ var AlarmEdit = { var error = false; this.alarm.label = this.labelInput.value; - this.alarm.enabled = true; var time = this.getTimeSelect(); - this.alarm.hour = time.hour; - this.alarm.minute = time.minute; + this.alarm.time = [time.hour, time.minute]; this.alarm.repeat = this.getRepeatSelect(); this.alarm.sound = this.getSoundSelect(); this.alarm.vibrate = this.getVibrateSelect(); this.alarm.snooze = parseInt(this.getSnoozeSelect(), 10); if (!error) { - AlarmManager.putAlarm(this.alarm, function al_putAlarmList(alarm) { - AlarmManager.toggleAlarm(alarm, alarm.enabled); - AlarmList.refresh(); - callback && callback(alarm); + this.alarm.cancel(); + this.alarm.setEnabled(true, function(err, alarm) { + if (err) { + callback && callback(err, alarm); + return; + } + AlarmList.refreshItem(alarm); + AlarmManager.renderBannerBar(alarm.getNextAlarmFireTime()); + AlarmManager.updateAlarmStatusBar(); + callback && callback(null, alarm); }); + } else { + // error + if (callback) { + callback(error); + } } return !error; }, delete: function aev_delete(callback) { - if (!this.element.dataset.id) + if (!this.alarm.id) { + setTimeout(callback.bind(null, new Error('no alarm id')), 0); return; + } - var alarm = this.alarm; - AlarmManager.delete(alarm, function aev_delete() { + this.alarm.delete(function aev_delete(err, alarm) { AlarmList.refresh(); - callback && callback(alarm); + AlarmManager.updateAlarmStatusBar(); + callback && callback(err, alarm); }); } diff --git a/apps/clock/js/alarm_list.js b/apps/clock/js/alarm_list.js index 91a1d2052239..854ffcbb96c5 100644 --- a/apps/clock/js/alarm_list.js +++ b/apps/clock/js/alarm_list.js @@ -64,16 +64,21 @@ var AlarmList = { }, refresh: function al_refresh() { - var self = this; - AlarmManager.getAlarmList(function al_gotAlarmList(list) { - self.fillList(list); - }); + AlarmsDB.getAlarmList(function al_gotAlarmList(err, list) { + if (!err) { + this.fillList(list); + } else { + console.error(err); + } + }.bind(this)); }, buildAlarmContent: function al_buildAlarmContent(alarm) { - var summaryRepeat = (Utils.isEmptyRepeat(alarm.repeat)) ? - '' : Utils.summarizeDaysOfWeek(alarm.repeat); - var isChecked = alarm.enabled ? ' checked="true"' : ''; + var summaryRepeat = !alarm.isRepeating() ? + '' : alarm.summarizeDaysOfWeek(); + var alarmActive = alarm.registeredAlarms['normal'] || + alarm.registeredAlarms['snooze']; + var isChecked = !!alarmActive ? ' checked="true"' : ''; var d = new Date(); d.setHours(alarm.hour); d.setMinutes(alarm.minute); @@ -95,34 +100,42 @@ var AlarmList = { ''; }, + createItem: function al_createItem(alarm, appendTarget) { + var li = document.createElement('li'); + li.className = 'alarm-cell'; + li.innerHTML = this.buildAlarmContent(alarm); + if (appendTarget) { + appendTarget.appendChild(li); + if (this._previousAlarmCount !== this.getAlarmCount()) { + this._previousAlarmCount = this.getAlarmCount(); + ClockView.resizeAnalogClock(); + } + } + return li; + }, refreshItem: function al_refreshItem(alarm) { - this.setAlarmFromList(alarm.id, alarm); - var id = 'a[data-id="' + alarm.id + '"]'; - var alarmItem = this.alarms.querySelector(id); - alarmItem.parentNode.innerHTML = this.buildAlarmContent(alarm); - // clear the refreshing alarm's flag - var index = this.refreshingAlarms.indexOf(alarm.id); - this.refreshingAlarms.splice(index, 1); + if (!this.getAlarmFromList(alarm.id)) { + this.alarmList.push(alarm); + this.createItem(alarm, this.alarms); + } else { + this.setAlarmFromList(alarm.id, alarm); + var id = 'a[data-id="' + alarm.id + '"]'; + var alarmItem = this.alarms.querySelector(id); + alarmItem.parentNode.innerHTML = this.buildAlarmContent(alarm); + // clear the refreshing alarm's flag + var index = this.refreshingAlarms.indexOf(alarm.id); + this.refreshingAlarms.splice(index, 1); + } }, fillList: function al_fillList(alarmDataList) { this.alarmList = alarmDataList; var content = ''; - this.alarms.innerHTML = ''; alarmDataList.forEach(function al_fillEachList(alarm) { - var li = document.createElement('li'); - li.className = 'alarm-cell'; - li.innerHTML = this.buildAlarmContent(alarm); - this.alarms.appendChild(li); + this.createItem(alarm, this.alarms); }.bind(this)); - - if (this._previousAlarmCount !== this.getAlarmCount()) { - this._previousAlarmCount = this.getAlarmCount(); - ClockView.resizeAnalogClock(); - } - }, getAlarmFromList: function al_getAlarmFromList(id) { @@ -147,32 +160,35 @@ var AlarmList = { }, toggleAlarmEnableState: function al_toggleAlarmEnableState(enabled, alarm) { + // Todo: queue actions instead of dropping them if (this.refreshingAlarms.indexOf(alarm.id) !== -1) { return; } - - if (alarm.enabled === enabled) - return; - - alarm.enabled = enabled; - this.refreshingAlarms.push(alarm.id); - - var self = this; - AlarmManager.putAlarm(alarm, function al_putAlarm(alarm) { - if (!alarm.enabled && !alarm.normalAlarmId && !alarm.snoozeAlarmId) { - self.refreshItem(alarm); - } else { - AlarmManager.toggleAlarm(alarm, alarm.enabled); + var changed = false; + // has a snooze active + if (alarm.registeredAlarms['snooze'] !== undefined) { + if (!enabled) { + alarm.cancel('snooze'); + changed = true; } - }); - }, - - deleteCurrent: function al_deleteCurrent(id) { - var alarm = this.getAlarmFromList(id); - var self = this; - AlarmManager.delete(alarm, function al_deleted() { - self.refresh(); - }); + } + // normal state needs to change + if (alarm.enabled !== enabled) { + this.refreshingAlarms.push(alarm.id); + // setEnabled saves to database + alarm.setEnabled(!alarm.enabled, function al_putAlarm(err, alarm) { + if (alarm.enabled) { + AlarmManager.renderBannerBar(alarm.getNextAlarmFireTime()); + } + this.refreshItem(alarm); + AlarmManager.updateAlarmStatusBar(); + }.bind(this)); + } else { + if (changed) { + alarm.save(); + } + AlarmManager.updateAlarmStatusBar(); + } } }; diff --git a/apps/clock/js/alarm_manager.js b/apps/clock/js/alarm_manager.js index 91b309d38a11..9c126b69baf2 100644 --- a/apps/clock/js/alarm_manager.js +++ b/apps/clock/js/alarm_manager.js @@ -1,17 +1,13 @@ /* An Alarm's ID: * ID in Clock app ID in mozAlarms API * id (unique) id (unique) - * normalAlarmId (comes from mozAlarms API) type: 'normal' or 'snooze' - * snoozeAlarmId (comes from mozAlarms API) + * type: 'normal' or 'snooze' + * * * An alarm has its own id in the Clock app's indexDB(alarmsdb.js). * In order to maintain(add,remove) an alarm by mozAlarms API, - * we prepare two request id(normalAlarmId, snoozeAlarmId) for each alarm. - * The two id is used to store return id from mozAlarms API. - * normalAlarmId: Maintain an alarm's life(once, repeat). - * snoozeAlarmId: Maintain an snooze alarm's life only(snooze). - * (If user click snooze button, - * we always maintain it with snoozeAlarmId.) + * we prepare an registeredAlarms object that contains each alarm type: + * 'snooze' and 'normal' * * In order to identify the active alarm which comes from mozAlarms API, * we pass id and type in JSON object data during adding an alarm by API. @@ -22,7 +18,7 @@ * An Alarm's Life: * We maintain an alarm's life cycle immediately when the alarm goes off. * If user click the snooze button when the alarm goes off, - * we request a snooze alarm with snoozeAlarmId immediately. + * we request a snooze alarm immediately. * * * Example: @@ -32,8 +28,8 @@ * R: a repeatable alarm * S: a snooze alarm * - * ====>: the flow of normalAlarmId - * ---->: the flow of snoozeAlarmId + * ====>: the flow of normal alarm + * ---->: the flow of snooze alarm * |: User click the snooze button * * Flow map: @@ -60,152 +56,46 @@ var AlarmManager = { toggleAlarm: function am_toggleAlarm(alarm, enabled, callback) { - if (enabled) { - this.set(alarm, false, callback); - } else { - this.unset(alarm, callback); - } - }, - - set: function am_set(alarm, bSnooze, callback) { - // Do not need to unset repeat alarm when set a snooze alarm - if (!bSnooze) { - // Unset the requested alarm which does not goes off - this.unset(alarm); - } - - var nextAlarmFireTime = null; - if (bSnooze) { - nextAlarmFireTime = new Date(); - nextAlarmFireTime.setMinutes(nextAlarmFireTime.getMinutes() + - alarm.snooze); - } else { - nextAlarmFireTime = Utils.getNextAlarmFireTime(alarm); - } - - if (!navigator.mozAlarms) - return; - - var type = bSnooze ? 'snooze' : 'normal'; - var data = { - id: alarm.id, - type: type - }; - var request = navigator.mozAlarms.add( - nextAlarmFireTime, - 'ignoreTimezone', - data); - - // give the alarm id for the request - var self = this; - request.onsuccess = function(e) { - if (bSnooze) { - alarm.snoozeAlarmId = e.target.result; - } else { - alarm.normalAlarmId = e.target.result; - } - - // save the AlarmAPI's request id to DB - AlarmsDB.putAlarm(alarm, function am_putAlarm(alarm) { - if (self._updateAlarmEableStateHandler) - self._updateAlarmEableStateHandler(alarm); - - if (callback) - callback(alarm); - }); - self.updateAlarmStatusBar(); - LazyLoader.load( - [ - 'js/banner.js' - ], - function() { - BannerView.setStatus(nextAlarmFireTime); - }); - }; - request.onerror = function(e) { - console.log('onerror!!!!'); - var logInfo = bSnooze ? ' snooze' : ''; - console.log('set' + logInfo + ' alarm fail'); - }; - - }, - - unset: function am_unset(alarm, callback) { - var isNeedToUpdateAlarmDB = false; - if (alarm.normalAlarmId) { - navigator.mozAlarms.remove(alarm.normalAlarmId); - alarm.normalAlarmId = ''; - isNeedToUpdateAlarmDB = true; - } - if (alarm.snoozeAlarmId) { - navigator.mozAlarms.remove(alarm.snoozeAlarmId); - alarm.snoozeAlarmId = ''; - isNeedToUpdateAlarmDB = true; - } - if (isNeedToUpdateAlarmDB) { - // clear the AlarmAPI's request id to DB - var self = this; - AlarmsDB.putAlarm(alarm, function am_putAlarm(alarm) { - if (self._updateAlarmEableStateHandler) - self._updateAlarmEableStateHandler(alarm); - - if (callback) - callback(alarm); - }); - this.updateAlarmStatusBar(); - } - - }, - - delete: function am_delete(alarm, callback) { - if (alarm.normalAlarmId || alarm.snoozeAlarmId) - this.toggleAlarm(alarm, false); - - var self = this; - AlarmsDB.deleteAlarm(alarm.id, function am_deletedAlarm() { - if (callback) - callback(); - - }); - }, - - getAlarmList: function am_getAlarmList(callback) { - AlarmsDB.getAlarmList(function am_gotAlarmList(list) { - if (callback) - callback(list); - - }); - }, - - getAlarmById: function am_getAlarmById(id, callback) { - AlarmsDB.getAlarm(id, function am_gotAlarm(alarm) { - if (callback) - callback(alarm); - - }); + alarm.setEnabled(enabled, callback); }, - putAlarm: function am_putAlarm(alarm, callback) { - AlarmsDB.putAlarm(alarm, function am_putAlarm(alarm) { - if (callback) - callback(alarm); - + renderBannerBar: function am_renderBannerBar(date) { + LazyLoader.load(['js/banner.js'], function() { + BannerView.setStatus(date); }); }, updateAlarmStatusBar: function am_updateAlarmStatusBar() { - if (!('mozSettings' in navigator)) - return; - if (!navigator.mozAlarms) - return; var request = navigator.mozAlarms.getAll(); - request.onsuccess = function(event) { - navigator.mozSettings.createLock().set({ - 'alarm.enabled': !!event.target.result.length + request.onsuccess = function(e) { + var hasAlarmEnabled = false; + var generator = Utils.async.generator(function(err) { + if (!err) { + navigator.mozSettings.createLock().set({ + 'alarm.enabled': hasAlarmEnabled + }); + } }); + var endCb = generator(); + for (var i = 0; i < e.target.result.length && !hasAlarmEnabled; i++) { + AlarmsDB.getAlarm(e.target.result[i].data.id, + (function(mozAlarm, doneCb) { + return function(err, alarm) { + if (!err) { + for (var j in alarm.registeredAlarms) { + if (alarm.registeredAlarms[j] === mozAlarm.id) { + hasAlarmEnabled = true; + } + } + } + doneCb(); + }; + })(e.target.result[i], generator())); + } + endCb(); }; - request.onerror = function(event) { - console.log('get all alarm fail'); + request.onerror = function(e) { + console.error('get all alarm fail'); }; }, diff --git a/apps/clock/js/alarmsdb.js b/apps/clock/js/alarmsdb.js index bfc95d369040..39c08c5fff54 100644 --- a/apps/clock/js/alarmsdb.js +++ b/apps/clock/js/alarmsdb.js @@ -1,21 +1,11 @@ 'use strict'; -// extend function from Angus Crolls article about mixins -function extend(destination, source) { - for (var k in source) { - if (source.hasOwnProperty(k)) { - destination[k] = source[k]; - } - } - return destination; -} - var BaseIndexDB = function(objectStoreOptions) { this.query = function ad_query(dbName, storeName, func, callback, data) { var indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.msIndexedDB; - var request = indexedDB.open(dbName, 5); + var request = indexedDB.open(dbName, 6); request.onsuccess = function(event) { func(request.result, storeName, callback, data); @@ -39,77 +29,80 @@ var BaseIndexDB = function(objectStoreOptions) { this.put = function ad_put(database, storeName, callback, item) { var txn = database.transaction(storeName, 'readwrite'); var store = txn.objectStore(storeName); - var putreq = store.put(item); putreq.onsuccess = function(event) { item.id = event.target.result; - if (typeof callback === 'function') - callback(item); + callback && callback(null, item); }; putreq.onerror = function(e) { - console.error('Add operation failure: ', database.name, - storeName, e.message, putreq.errorCode); + callback && callback({ + database: database, + store: storeName, + message: e.message, + code: putreq.errorCode + }); }; }; this.load = function ad_load(database, storeName, callback) { - if (typeof callback !== 'function') - callback = function() {}; - var alarms = []; - var txn = database.transaction(storeName); var store = txn.objectStore(storeName); - var cursor = store.openCursor(null, 'prev'); + cursor.onsuccess = function(event) { var item = event.target.result; if (item) { alarms.push(item.value); item.continue(); } else { - callback(alarms); + callback && callback(null, alarms); } }; cursor.onerror = function(event) { - callback([]); + callback && callback(event); }; }; this.get = function ad_get(database, storeName, callback, key) { - if (typeof callback !== 'function') - callback = function() {}; - var txn = database.transaction(storeName); var store = txn.objectStore(storeName); var request = store.get(key); request.onsuccess = function(event) { - callback(request.result); + callback && callback(null, request.result); }; request.onerror = function(event) { - console.error('Get operation failure: ', database.name, - storeName, event.message, request.errorCode); + callback && callback({ + database: database, + store: storeName, + message: event.message, + code: request.errorCode + }); }; }; this.delete = function ad_delete(database, storeName, callback, key) { - if (typeof callback !== 'function') - callback = function() {}; var txn = database.transaction(storeName, 'readwrite'); var store = txn.objectStore(storeName); var request = store.delete(key); - request.onsuccess = callback; + request.onsuccess = function(e) { + callback && callback(null, e); + }; request.onerror = function(e) { - console.error('Delete operation failure: ', database.name, - storeName, e.message, request.errorCode); + callback && callback({ + database: database, + store: storeName, + message: event.message, + code: request.errorCode + }); }; }; }; @@ -120,15 +113,24 @@ var AlarmsDB = { // Database methods getAlarmList: function ad_getAlarmList(callback) { - this.query(this.DBNAME, this.STORENAME, this.load, callback); + function getAlarmList_mapper(err, list) { + callback(err, (list || []).map(function(x) { + return new Alarm(x); + })); + } + this.query(this.DBNAME, this.STORENAME, this.load, getAlarmList_mapper); }, putAlarm: function ad_putAlarm(alarm, callback) { - this.query(this.DBNAME, this.STORENAME, this.put, callback, alarm); + this.query(this.DBNAME, this.STORENAME, this.put, callback, + alarm.toSerializable()); }, getAlarm: function ad_getAlarm(key, callback) { - this.query(this.DBNAME, this.STORENAME, this.get, callback, key); + this.query(this.DBNAME, this.STORENAME, this.get, + function(err, result) { + callback(err, new Alarm(result)); + }, key); }, deleteAlarm: function ad_deleteAlarm(key, callback) { @@ -136,4 +138,4 @@ var AlarmsDB = { } }; -extend(AlarmsDB, new BaseIndexDB({keyPath: 'id', autoIncrement: true})); +Utils.extend(AlarmsDB, new BaseIndexDB({keyPath: 'id', autoIncrement: true})); diff --git a/apps/clock/js/onring.js b/apps/clock/js/onring.js index b55b1f61b605..cd81e53fa601 100644 --- a/apps/clock/js/onring.js +++ b/apps/clock/js/onring.js @@ -6,11 +6,12 @@ var _ = navigator.mozL10n.get; var RingView = { - _ringtonePlayer: null, - _vibrateInterval: null, - _screenLock: null, - _onFireAlarm: {}, - _started: false, + ringtonePlayer: null, + vibrateInterval: null, + screenLock: null, + firedAlarm: {}, + message: {}, + started: false, get time() { delete this.time; @@ -39,8 +40,8 @@ var RingView = { init: function rv_init() { document.addEventListener('visibilitychange', this); - this._onFireAlarm = window.opener.ActiveAlarm.getOnFireAlarm(); - var self = this; + this.firedAlarm = window.opener.ActiveAlarm.firedAlarm; + this.message = window.opener.ActiveAlarm.message; if (!document.hidden) { this.startAlarmNotification(); } else { @@ -54,16 +55,16 @@ var RingView = { // We should just put a "silent" alarm screen // underneath the oncall screen if (!document.hidden) { - self.startAlarmNotification(); + this.startAlarmNotification(); } // Our final chance is to rely on visibilitychange event handler. - }, 0); + }.bind(this), 0); } navigator.mozL10n.ready(function rv_waitLocalized() { - self.setAlarmTime(); - self.setAlarmLabel(); - }); + this.setAlarmTime(); + this.setAlarmLabel(); + }.bind(this)); this.snoozeButton.addEventListener('click', this); this.closeButton.addEventListener('click', this); @@ -78,10 +79,10 @@ var RingView = { } if (enabled) { - this._screenLock = navigator.requestWakeLock('screen'); - } else if (this._screenLock) { - this._screenLock.unlock(); - this._screenLock = null; + this.screenLock = navigator.requestWakeLock('screen'); + } else if (this.screenLock) { + this.screenLock.unlock(); + this.screenLock = null; } }, @@ -98,8 +99,7 @@ var RingView = { }, ring: function rv_ring() { - this._ringtonePlayer = new Audio(); - var ringtonePlayer = this._ringtonePlayer; + var ringtonePlayer = this.ringtonePlayer = new Audio(); ringtonePlayer.addEventListener('mozinterruptbegin', this); ringtonePlayer.mozAudioChannelType = 'alarm'; ringtonePlayer.loop = true; @@ -118,7 +118,7 @@ var RingView = { vibrate: function rv_vibrate() { if ('vibrate' in navigator) { - this._vibrateInterval = window.setInterval(function vibrate() { + this.vibrateInterval = window.setInterval(function vibrate() { navigator.vibrate([1000]); }, 2000); /* If user don't handle the onFire alarm, @@ -133,15 +133,15 @@ var RingView = { startAlarmNotification: function rv_startAlarmNotification() { // Ensure called only once. - if (this._started) + if (this.started) return; - this._started = true; + this.started = true; this.setWakeLockEnabled(true); - if (this._onFireAlarm.sound) { + if (this.firedAlarm.sound) { this.ring(); } - if (this._onFireAlarm.vibrate == 1) { + if (this.firedAlarm.vibrate == 1) { this.vibrate(); } }, @@ -149,22 +149,22 @@ var RingView = { stopAlarmNotification: function rv_stopAlarmNotification(action) { switch (action) { case 'ring': - if (this._ringtonePlayer) - this._ringtonePlayer.pause(); - + if (this.ringtonePlayer) { + this.ringtonePlayer.pause(); + } break; case 'vibrate': - if (this._vibrateInterval) - window.clearInterval(this._vibrateInterval); - + if (this.vibrateInterval) { + window.clearInterval(this.vibrateInterval); + } break; default: - if (this._ringtonePlayer) - this._ringtonePlayer.pause(); - - if (this._vibrateInterval) - window.clearInterval(this._vibrateInterval); - + if (this.ringtonePlayer) { + this.ringtonePlayer.pause(); + } + if (this.vibrateInterval) { + window.clearInterval(this.vibrateInterval); + } break; } this.setWakeLockEnabled(false); @@ -172,17 +172,17 @@ var RingView = { getAlarmTime: function am_getAlarmTime() { var d = new Date(); - d.setHours(this._onFireAlarm.hour); - d.setMinutes(this._onFireAlarm.minute); + d.setHours(this.message.date.getHours()); + d.setMinutes(this.message.date.getMinutes()); return d; }, getAlarmLabel: function am_getAlarmLabel() { - return this._onFireAlarm.label; + return this.firedAlarm.label; }, getAlarmSound: function am_getAlarmSound() { - return this._onFireAlarm.sound; + return this.firedAlarm.sound; }, handleEvent: function rv_handleEvent(evt) { diff --git a/apps/clock/js/utils.js b/apps/clock/js/utils.js index b5a0d98ae7a7..92636ce988f8 100644 --- a/apps/clock/js/utils.js +++ b/apps/clock/js/utils.js @@ -3,6 +3,21 @@ var Utils = {}; +Utils.extend = function(initialObject, extensions) { + // extend({}, a, b, c ... d) -> {...} + // rightmost properties (on 'd') take precedence + extensions = Array.prototype.slice.call(arguments, 1); + for (var i = 0; i < extensions.length; i++) { + var extender = extensions[i]; + for (var prop in extender) { + if (Object.prototype.hasOwnProperty.call(extender, prop)) { + initialObject[prop] = extender[prop]; + } + } + } + return initialObject; +}; + Utils.escapeHTML = function(str, escapeQuotes) { var span = document.createElement('span'); span.textContent = str; @@ -12,49 +27,6 @@ Utils.escapeHTML = function(str, escapeQuotes) { return span.innerHTML; }; -Utils.summarizeDaysOfWeek = function(repeat) { - var _ = navigator.mozL10n.get; - // Build a bitset - var value = 0; - for (var i = 0; i < DAYS.length; i++) { - var dayName = DAYS[i]; - if (repeat[dayName] === true) { - value |= (1 << i); - } - } - var summary; - if (value === 127) { // 127 = 0b1111111 - summary = _('everyday'); - } else if (value === 31) { // 31 = 0b0011111 - summary = _('weekdays'); - } else if (value === 96) { // 96 = 0b1100000 - summary = _('weekends'); - } else if (value !== 0) { // any day was true - var weekdays = []; - for (var i = 0; i < DAYS.length; i++) { - var dayName = DAYS[i]; - if (repeat[dayName]) { - // Note: here, Monday is the first day of the week - // whereas in JS Date(), it's Sunday -- hence the (+1) here. - weekdays.push(_('weekday-' + ((i + 1) % 7) + '-short')); - } - summary = weekdays.join(', '); - } - } else { // no day was true - summary = _('never'); - } - return summary; -}; - -Utils.isEmptyRepeat = function(repeat) { - for (var i in repeat) { - if (repeat[i] === true) { - return false; - } - } - return true; -}; - Utils.is12hFormat = function() { var localeTimeFormat = navigator.mozL10n.get('dateTimeFormat_%X'); var is12h = (localeTimeFormat.indexOf('%p') >= 0); @@ -70,44 +42,6 @@ Utils.getLocaleTime = function(d) { }; }; -// check alarm has passed or not -Utils.isAlarmPassToday = function(hour, minute) { - var now = new Date(); - if (hour > now.getHours() || - (hour == now.getHours() && minute > now.getMinutes())) { - return false; - } - return true; -}; - -// get the next alarm fire time -Utils.isDateInRepeat = function alarm_isDateInRepeat(repeat, date) { - // return true if repeat contains date - var day = DAYS[(date.getDay() + 6) % 7]; - return !!repeat[day]; -}; - -Utils.repeatDays = function alarm_repeatDays(repeat) { - var count = 0; - for (var i in repeat) { - if (repeat[i]) { - count++; - } - } - return count; -}; - -Utils.getNextAlarmFireTime = function(alarm) { - var now = new Date(), next = new Date(); - next.setHours(alarm.hour, alarm.minute, 0, 0); - while (next < now || - !(Utils.repeatDays(alarm.repeat) === 0 || - Utils.isDateInRepeat(alarm.repeat, next))) { - next.setDate(next.getDate() + 1); - } - return next; -}; - Utils.changeSelectByValue = function(selectElement, value) { var options = selectElement.options; for (var i = 0; i < options.length; i++) { @@ -162,6 +96,117 @@ Utils.parseTime = function(time) { }; }; +Utils.safeCpuLock = function(timeoutMs, fn) { + /* + * safeCpuLock + * + * Create a CPU lock that is automatically released after + * timeoutMs. + * + * + * @timeoutMs {integer} a number of milliseconds + * @callback {Function} a function to be called after + * all other generated callbacks have been + * called + * function ([err]) -> undefined + */ + var cpuWakeLock, unlockTimeout; + var unlockFn = function() { + clearTimeout(unlockTimeout); + if (cpuWakeLock) { + cpuWakeLock.unlock(); + cpuWakeLock = null; + } + }; + unlockTimeout = setTimeout(unlockFn, timeoutMs); + try { + cpuWakeLock = navigator.requestWakeLock('cpu'); + fn(unlockFn); + } catch (err) { + unlockFn(); + throw err; + } +}; + +Utils.async = { + + generator: function(latchCallback) { + /* + * Generator + * + * Create an async generator. Each time the generator is + * called, it will return a new callback. When all issued + * callbacks have been called, the latchCallback is called. + * + * If any of the callbacks are called with and error as + * the first argument, the latchCallback will be called + * immediately with that error. + * + * @latchCallback {Function} a function to be called after + * all other generated callbacks have been + * called + * function ([err]) -> undefined + */ + var tracker = new Map(); + var issuedCallbackCount = 0; + var disabled = false; + var testFn = function(err) { + var trackerSize; + if (!disabled) { + // FF18 defines size to be a method, so we need to test here: + // Remove with FF18 support + if (typeof tracker.size === 'function') { + trackerSize = tracker.size(); + } else { + trackerSize = tracker.size; + } + if (err || trackerSize === issuedCallbackCount) { + disabled = true; + latchCallback && latchCallback(err); + } + } + }; + return function() { + return (function() { + var i = issuedCallbackCount++; + return function(err) { + tracker.set(i, true); + testFn(err); + }; + })(); + }; + }, + + namedParallel: function(names, latchCallback) { + /* + * namedParallel + * + * Create an async namedParallel. + * + * The return value is an object containing the parameters + * specified in the names array. Each parameter is set to + * a callback. When all callbacks have been called, latchCallback + * is called. + * + * If any named callback is called with an error as the first + * parameter, latchCallback is immediately called with that + * error. Future calls to callbacks are then no-ops. + * + * @names {List} - A list of strings to be used as + * parameter names for callbacks on the returned object. + */ + var generator = Utils.async.generator(latchCallback); + var done = generator(); + var ret = {}; + for (var i = 0; i < names.length; i++) { + ret[names[i]] = generator(); + } + done(); + return ret; + } + +}; + exports.Utils = Utils; }(this)); diff --git a/apps/clock/test/unit/alarm_edit_test.js b/apps/clock/test/unit/alarm_edit_test.js new file mode 100644 index 000000000000..311930cb8e97 --- /dev/null +++ b/apps/clock/test/unit/alarm_edit_test.js @@ -0,0 +1,285 @@ +requireApp('clock/js/constants.js'); +requireApp('clock/js/utils.js'); +requireApp('clock/js/alarm.js'); +requireApp('clock/js/alarmsdb.js'); +requireApp('clock/js/alarm_manager.js'); +requireApp('clock/js/alarm_edit.js'); +requireApp('clock/js/alarm_list.js'); +requireApp('clock/js/active_alarm.js'); + +requireApp('clock/test/unit/mocks/mock_alarmsDB.js'); +requireApp('clock/test/unit/mocks/mock_alarm_list.js'); +requireApp('clock/test/unit/mocks/mock_alarm_manager.js'); +requireApp('clock/test/unit/mocks/mock_asyncstorage.js'); +requireApp('clock/test/unit/mocks/mock_navigator_mozl10n.js'); +requireApp('clock/test/unit/mocks/mock_mozAlarm.js'); + +suite('AlarmEditView', function() { + var _AlarmsDB; + var al, am, nml; + var id = 1; + + suiteSetup(function() { + sinon.stub(ActiveAlarm, 'handler'); + navigator.mozAlarms = new MockMozAlarms( + ActiveAlarm.handler); + _AlarmsDB = window.AlarmsDB; + al = AlarmList; + am = AlarmManager; + nml = navigator.mozL10n; + + AlarmList = MockAlarmList; + AlarmManager = MockAlarmManager; + AlarmsDB = new MockAlarmsDB(); + navigator.mozL10n = MockmozL10n; + + loadBodyHTML('/index.html'); + }); + + suiteTeardown(function() { + AlarmList = al; + AlarmManager = am; + AlarmsDB = _AlarmsDB; + ActiveAlarm.handler.restore(); + }); + + suite('Alarm persistence', function() { + + setup(function() { + // Create an Alarm + var alarm = AlarmEdit.alarm = new Alarm({ + id: 42, + hour: 6, + minute: 34 + }); + alarm.repeat = { + monday: true, wednesday: true, friday: true + }; + AlarmEdit.element.dataset.id = alarm.id; + + // Store to alarmsdb + AlarmsDB.alarms.clear(); + AlarmsDB.alarms.set(alarm.id, alarm); + AlarmsDB.idCount = 43; + + // shim the edit alarm view + delete AlarmEdit.labelInput; + AlarmEdit.labelInput = document.createElement('input'); + delete AlarmEdit.timeSelect; + AlarmEdit.timeSelect = document.createElement('input'); + AlarmEdit.initTimeSelect(); + + this.sinon.stub(AlarmEdit, 'getTimeSelect'); + this.sinon.stub(AlarmEdit, 'getSoundSelect'); + this.sinon.stub(AlarmEdit, 'getVibrateSelect'); + this.sinon.stub(AlarmEdit, 'getSnoozeSelect'); + this.sinon.stub(AlarmEdit, 'getRepeatSelect'); + + this.sinon.stub(AlarmManager, 'toggleAlarm'); + this.sinon.stub(AlarmManager, 'renderBannerBar'); + this.sinon.stub(AlarmManager, 'updateAlarmStatusBar'); + + // Define the stubs to return the same values set in the + // default alarm object. + AlarmEdit.getTimeSelect.returns({ + hour: alarm.hour, + minute: alarm.minute + }); + AlarmEdit.getSoundSelect.returns(AlarmEdit.alarm.sound); + AlarmEdit.getVibrateSelect.returns(AlarmEdit.alarm.vibrate); + AlarmEdit.getSnoozeSelect.returns(AlarmEdit.alarm.snooze); + AlarmEdit.getRepeatSelect.returns(AlarmEdit.alarm.repeat); + + this.sinon.useFakeTimers(); + }); + + test('should save an alarm, no id', function(done) { + this.sinon.stub(AlarmList, 'refreshItem'); + AlarmEdit.alarm = new Alarm({ + hour: 5, + minute: 17, + repeat: { + monday: true, wednesday: true, friday: true + } + }); + AlarmEdit.element.dataset.id = null; + + AlarmEdit.save(function(err, alarm) { + assert.ok(!err); + assert.ok(alarm.id); + assert.equal(alarm.id, 43); + // Refreshed AlarmList + assert.ok(AlarmList.refreshItem.calledOnce); + assert.ok(AlarmList.refreshItem.calledWithExactly(alarm)); + + // Rendered BannerBar + assert.ok(AlarmManager.renderBannerBar.calledOnce); + assert.ok(AlarmManager.updateAlarmStatusBar.calledOnce); + done(); + }); + this.sinon.clock.tick(100); + }); + + test('should save an alarm, existing id', function(done) { + this.sinon.stub(AlarmList, 'refreshItem'); + var curid = AlarmsDB.idCount++; + AlarmEdit.alarm = new Alarm({ + id: curid, + hour: 5, + minute: 17, + repeat: { + monday: true, wednesday: true, friday: true + } + }); + AlarmEdit.element.dataset.id = AlarmEdit.alarm.id; + + AlarmEdit.save(function(err, alarm) { + assert.ok(!err); + assert.ok(alarm.id); + assert.equal(alarm.id, curid); + // Refreshed AlarmList + assert.ok(AlarmList.refreshItem.calledOnce); + assert.ok(AlarmList.refreshItem.calledWithExactly(alarm)); + + // Rendered BannerBar + assert.ok(AlarmManager.renderBannerBar.calledOnce); + assert.ok(AlarmManager.updateAlarmStatusBar.calledOnce); + done(); + }); + this.sinon.clock.tick(100); + }); + + test('should delete an alarm', function(done) { + var called = false; + this.sinon.stub(AlarmList, 'refresh'); + AlarmEdit.delete(function(err, alarm) { + assert.ok(!err, 'delete reported error'); + assert.ok(!AlarmsDB.alarms.has(alarm.id)); + assert.ok(AlarmList.refresh.calledOnce); + assert.ok(AlarmManager.updateAlarmStatusBar.calledOnce); + called = true; + done(); + }); + this.sinon.clock.tick(10); + if (!called) { + done('was not called'); + } + }); + + test('should add an alarm with sound, no vibrate', function(done) { + this.sinon.stub(AlarmList, 'refreshItem'); + + // mock the view to turn off vibrate + AlarmEdit.getVibrateSelect.returns('0'); + + var curid = AlarmsDB.idCount; + AlarmEdit.alarm = new Alarm({ + hour: 5, + minute: 17, + repeat: { + monday: true, wednesday: true, friday: true + } + }); + AlarmEdit.element.dataset.id = null; + + AlarmEdit.save(function(err, alarm) { + assert.equal(alarm.id, curid); + assert.ok(AlarmList.refreshItem.calledOnce); + AlarmsDB.getAlarm(alarm.id, function(err, alarm) { + assert.equal(alarm.vibrate, 0); + assert.notEqual(alarm.sound, 0); + done(); + }); + }); + this.sinon.clock.tick(10); + }); + + test('should update existing alarm with no sound, vibrate', function(done) { + this.sinon.stub(AlarmList, 'refresh'); + this.sinon.stub(AlarmList, 'refreshItem'); + // mock the view to turn sound on and vibrate off + AlarmEdit.getVibrateSelect.returns('0'); + AlarmEdit.alarm.save(function(err, alarm) { + assert.ok(alarm.id); + + AlarmEdit.getVibrateSelect.returns('1'); + AlarmEdit.getSoundSelect.returns('0'); + AlarmEdit.alarm = alarm; + AlarmEdit.element.dataset.id = alarm.id; + + AlarmEdit.save(function(err, alarm) { + assert.ok(alarm.id); + assert.ok(AlarmList.refreshItem.calledOnce); + AlarmsDB.getAlarm(alarm.id, function(err, alarm) { + assert.equal(alarm.vibrate, 1); + assert.equal(alarm.sound, 0); + done(); + }); + }); + }); + this.sinon.clock.tick(10); + }); + + }); + + suite('initTimeSelect', function() { + var alarm; + + suiteSetup(function() { + alarm = AlarmEdit.alarm; + }); + + suiteTeardown(function() { + AlarmEdit.alarm = alarm; + }); + + test('0:0, should init time select with format of system time picker', + function() { + AlarmEdit.alarm.hour = '0'; + AlarmEdit.alarm.minute = '0'; + AlarmEdit.initTimeSelect(); + assert.equal(AlarmEdit.timeSelect.value, '00:00'); + }); + + test('3:5, should init time select with format of system time picker', + function() { + AlarmEdit.alarm.hour = '3'; + AlarmEdit.alarm.minute = '5'; + AlarmEdit.initTimeSelect(); + assert.equal(AlarmEdit.timeSelect.value, '03:05'); + }); + + test('9:25, should init time select with format of system time picker', + function() { + AlarmEdit.alarm.hour = '9'; + AlarmEdit.alarm.minute = '25'; + AlarmEdit.initTimeSelect(); + assert.equal(AlarmEdit.timeSelect.value, '09:25'); + }); + + test('12:55, should init time select with format of system time picker', + function() { + AlarmEdit.alarm.hour = '12'; + AlarmEdit.alarm.minute = '55'; + AlarmEdit.initTimeSelect(); + assert.equal(AlarmEdit.timeSelect.value, '12:55'); + }); + + test('15:5, should init time select with format of system time picker', + function() { + AlarmEdit.alarm.hour = '15'; + AlarmEdit.alarm.minute = '5'; + AlarmEdit.initTimeSelect(); + assert.equal(AlarmEdit.timeSelect.value, '15:05'); + }); + + test('23:0, should init time select with format of system time picker', + function() { + AlarmEdit.alarm.hour = '23'; + AlarmEdit.alarm.minute = '0'; + AlarmEdit.initTimeSelect(); + assert.equal(AlarmEdit.timeSelect.value, '23:00'); + }); + }); + +}); diff --git a/apps/clock/test/unit/alarm_test.js b/apps/clock/test/unit/alarm_test.js index d9db3726aa4a..85bcbfebf456 100644 --- a/apps/clock/test/unit/alarm_test.js +++ b/apps/clock/test/unit/alarm_test.js @@ -1,198 +1,466 @@ +requireApp('clock/js/constants.js'); +requireApp('clock/js/utils.js'); +requireApp('clock/js/alarm.js'); requireApp('clock/js/alarmsdb.js'); +requireApp('clock/js/alarm_manager.js'); requireApp('clock/js/alarm_edit.js'); requireApp('clock/js/alarm_list.js'); -requireApp('clock/js/alarm_manager.js'); -requireApp('clock/js/utils.js'); +requireApp('clock/js/active_alarm.js'); +requireApp('clock/test/unit/mocks/mock_alarmsDB.js'); requireApp('clock/test/unit/mocks/mock_alarm_list.js'); requireApp('clock/test/unit/mocks/mock_alarm_manager.js'); requireApp('clock/test/unit/mocks/mock_asyncstorage.js'); requireApp('clock/test/unit/mocks/mock_navigator_mozl10n.js'); +requireApp('clock/test/unit/mocks/mock_mozAlarm.js'); +suite('Alarm Test', function() { -suite('AlarmEditView', function() { - var al, am, nml; - var id = 1; + var nativeMozL10n = navigator.mozL10n; + var nativeAlarmsDB = window.AlarmsDB; + var nativeActiveAlarmHandler; suiteSetup(function() { - al = AlarmList; - am = AlarmManager; - nml = navigator.mozL10n; - - AlarmList = MockAlarmList; - AlarmManager = MockAlarmManager; - navigator.mozL10n = MockmozL10n; - - loadBodyHTML('/index.html'); + navigator.mozL10n = MockL10n; + window.AlarmsDB = new MockAlarmsDB(); + navigator.mozAlarms = new MockMozAlarms( + ActiveAlarm.handler); }); suiteTeardown(function() { - AlarmList = al; - AlarmManager = am; - navigator.mozL10n = nml; + navigator.mozL10n = nativeMozL10n; + window.AlarmsDB = nativeAlarmsDB; }); - setup(function() { - // shim the edit alarm view - delete AlarmEdit.labelInput; - AlarmEdit.labelInput = document.createElement('input'); - delete AlarmEdit.timeSelect; - AlarmEdit.timeSelect = document.createElement('input'); - AlarmEdit.initTimeSelect(); - - - this.sinon.stub(AlarmEdit, 'getSoundSelect'); - this.sinon.stub(AlarmEdit, 'getVibrateSelect'); - this.sinon.stub(AlarmEdit, 'getSnoozeSelect'); - this.sinon.stub(AlarmEdit, 'getRepeatSelect'); + this.sinon.stub(ActiveAlarm, 'handler'); + }); - this.sinon.stub(AlarmManager, 'toggleAlarm'); + suite('Date handling', function() { + + var clockSetter = function(thisVal) { + return function(x) { + this.sinon.clock.tick( + (-1 * this.sinon.clock.tick()) + x); + }.bind(thisVal); + }; + + var days = ['monday', 'tuesday', 'wednesday', 'thursday', + 'friday', 'saturday', 'sunday']; + + setup(function() { + // Wed Jul 17 2013 19:07:18 GMT-0400 (EDT) + this.startDate = new Date(1374102438043); + this.startDate.setHours(19, 7, 18); + while (this.startDate.getDay() !== 3) { + this.startDate.setHours(this.startDate.getHours() + 24); + } + this.start = this.startDate.getTime(); + this.alarm = new Alarm({ + hour: this.startDate.getHours(), + minute: this.startDate.getMinutes(), + repeat: { + tuesday: true, thursday: true, saturday: true, + sunday: true + } + }); + this.sinon.useFakeTimers(); + }); - this.sinon.stub(AlarmManager, 'putAlarm', function(alarm, callback) { - alarm.id = id++; - callback(alarm); + suite('initialization', function() { + test('time', function() { + assert.deepEqual(this.alarm.time, [19, 7]); + }); + test('repeat', function() { + assert.deepEqual(this.alarm.repeat, { + tuesday: true, thursday: true, + saturday: true, sunday: true + }); + }); }); - this.sinon.stub(AlarmManager, 'delete', function(alarm, callback) { - callback(alarm); + suite('configuration', function() { + test('time', function() { + this.alarm.time = [15, 43]; + assert.deepEqual(this.alarm.time, [15, 43]); + }); + test('repeat', function() { + this.alarm.repeat = { + monday: true, + tuesday: true + }; + assert.deepEqual(this.alarm.repeat, { + monday: true, tuesday: true + }); + }); }); - AlarmEdit.alarm = AlarmEdit.getDefaultAlarm(); + suite('Alarm snooze', function() { - // Define the stubs to return the same values set in the - // default alarm object. - AlarmEdit.getSoundSelect.returns(AlarmEdit.alarm.sound); - AlarmEdit.getVibrateSelect.returns(AlarmEdit.alarm.vibrate); - AlarmEdit.getSnoozeSelect.returns(AlarmEdit.alarm.snooze); - AlarmEdit.getRepeatSelect.returns(AlarmEdit.alarm.repeat); - }); + test('snooze for 5 minutes', function() { + clockSetter(this)(this.start); + this.alarm.snooze = 5; + assert.equal(this.alarm.getNextSnoozeFireTime().getTime(), + this.start + (5 * 60 * 1000)); + }); - test('should save and delete an alarm', function(done) { - this.sinon.stub(AlarmList, 'refresh'); + test('snooze for 5 minutes, test seconds and milliseconds', function() { + var msOffset = 12512; + clockSetter(this)(this.start + msOffset); + this.alarm.snooze = 5; + assert.equal(this.alarm.getNextSnoozeFireTime().getTime(), + this.start + (5 * 60 * 1000) + msOffset); + }); - AlarmEdit.save(function(alarm) { - assert.ok(alarm.id); - assert.ok(AlarmList.refresh.calledOnce); - assert.ok(AlarmManager.putAlarm.calledOnce); - assert.ok(AlarmManager.toggleAlarm.calledOnce); + test('snooze for 5 minutes', function() { + clockSetter(this)(this.start); + this.alarm.snooze = null; + assert.equal(this.alarm.getNextSnoozeFireTime(), null, + 'no snooze set, getNextSnoozeFireTime should be null'); + }); - AlarmEdit.alarm = alarm; - AlarmEdit.element.dataset.id = alarm.id; + }); - AlarmEdit.delete(function() { - assert.ok(AlarmList.refresh.calledTwice); - done(); + test('Alarm custom summary', function() { + // Account for week beginning differences between date and alarm order + var shortNames = [1, 2, 3, 4, 5, 6, 0].map(function(x) { + return 'weekday-' + x + '-short'; }); + // test all possible combinations, except special cases + for (var i = 0; i < 128; i++) { + /* + 0 => never + 31 => weekdays + 96 => weekends + 127 => every day + */ + if ([0, 31, 96, 127].indexOf(i) !== -1) { + continue; + } + var expected = []; + var repeat = {}; + for (var j = 0; j < 7; j++) { + if ((i & (1 << j)) !== 0) { + expected.push(shortNames[j]); + repeat[days[j]] = true; + } + } + this.alarm.repeat = repeat; + // Assorted days test + assert.equal(this.alarm.summarizeDaysOfWeek(), + expected.join(', '), + 'Summarizing ' + JSON.stringify(repeat)); + } }); - }); - - test('should add an alarm with sound, no vibrate', function(done) { - this.sinon.stub(AlarmList, 'refresh'); - // mock the view to turn off vibrate - AlarmEdit.getVibrateSelect.returns(0); - AlarmEdit.save(function(alarm) { - assert.ok(alarm.id); - assert.ok(AlarmList.refresh.calledOnce); - assert.ok(AlarmManager.putAlarm.calledOnce); - assert.ok(AlarmManager.toggleAlarm.calledOnce); + test('Weekend alarm summary', function() { + // Weekend test + this.alarm.repeat = {saturday: true, sunday: true}; + assert.equal(this.alarm.summarizeDaysOfWeek(), 'weekends'); + }); + test('Weekday alarm summary', function() { + // Weekdays test + this.alarm.repeat = { + monday: true, tuesday: true, wednesday: true, + thursday: true, friday: true + }; + // Everyday test + assert.equal(this.alarm.summarizeDaysOfWeek(), 'weekdays'); + }); - assert.equal(alarm.vibrate, 0); - assert.notEqual(alarm.sound, 0); + test('Every day alarm summary', function() { + this.alarm.repeat = { + monday: true, tuesday: true, wednesday: true, + thursday: true, friday: true, saturday: true, + sunday: true + }; + assert.equal(this.alarm.summarizeDaysOfWeek(), 'everyday'); + }); - done(); + test('Never alarm summary', function() { + // never test + this.alarm.repeat = {}; + assert.equal(this.alarm.summarizeDaysOfWeek(), 'never'); }); - }); - test('should update existing alarm with no sound, vibrate', function(done) { - this.sinon.stub(AlarmList, 'refresh'); + test('Alarm Passed Today', function() { + var setClock = clockSetter(this); + // Wed Jul 17 2013 06:30:00 GMT-0400 (EDT) + var alarmDate = new Date(1374057000000); + alarmDate.setHours(18, 30, 0); + var alarmTime = alarmDate.getTime(); + // set the alarm time + this.alarm.time = [alarmDate.getHours(), alarmDate.getMinutes()]; + // test all combinations of +/- minute/hour + var minuteOffsets = [-61, -60, -1, 0, 1, 60, 61]; + for (var i = 0; i < minuteOffsets.length; i++) { + setClock(alarmTime + (minuteOffsets[i] * 60000)); + assert.equal(this.alarm.isAlarmPassedToday(), + minuteOffsets[i] >= 0); + } + }); - // mock the view to turn sound on and vibrate off - AlarmEdit.getVibrateSelect.returns(0); - AlarmEdit.save(function(alarm) { - assert.ok(alarm.id); - assert.ok(AlarmList.refresh.calledOnce); - assert.ok(AlarmManager.putAlarm.calledOnce); - assert.ok(AlarmManager.toggleAlarm.calledOnce); + suite('isDateInRepeat', function() { + + var daymap = new Map(); + + var repeatodd = { + monday: true, // 1 + wednesday: true, // 3 + friday: true // 5 + }; + + var repeateven = { + sunday: true, // 0 + tuesday: true, // 2 + thursday: true, // 4 + saturday: true // 6 + }; + + var cur = new Date(); + for (var i = 0; i < 7; i++) { + daymap.set(cur.getDay(), cur); + cur = new Date(cur.getTime() + (24 * 3600 * 1000)); + } + + for (var el of daymap) { + (function(el) { + var repday = (el[0] + 6) % 7; + var oddeven = ((el[0] % 2) === 1) ? repeatodd : repeateven; + var evenodd = ((el[0] % 2) === 0) ? repeatodd : repeateven; + test(days[repday] + '[' + el[0] + '] is in ' + + JSON.stringify(oddeven), function() { + var testalarm = new Alarm({ + repeat: oddeven + }); + assert.ok(testalarm.isDateInRepeat(daymap.get(el[0]))); + }); + test(days[repday] + '[' + el[0] + '] is not in ' + + JSON.stringify(evenodd), function() { + var testalarm = new Alarm({ + repeat: evenodd + }); + assert.ok(!testalarm.isDateInRepeat(daymap.get(el[0]))); + }); + })(el); + } + }); - AlarmEdit.getVibrateSelect.returns(1); - AlarmEdit.getSoundSelect.returns(0); + suite('repeatDays', function() { + for (var i = 0; i <= 7; i++) { + var rep = {}; + for (var j = 0; j < i; j++) { + rep[days[j]] = true; + } + (function(testrepeat, value) { + test(JSON.stringify(testrepeat) + ' has ' + value + ' repeat days', + function() { + var testalarm = new Alarm({ + repeat: testrepeat + }); + assert.equal(testalarm.repeatDays(), value); + }); + })(rep, i); + } + }); - AlarmEdit.alarm = alarm; - AlarmEdit.element.dataset.id = alarm.id; + suite('Next alarm time', function() { + + var alarmTime, alarmDate, setClock; + + setup(function() { + setClock = clockSetter(this); + // Wed Jul 17 2013 06:30:00 GMT-0400 (EDT) + alarmTime = 1374057000000; + alarmDate = new Date(alarmTime); + alarmDate.setHours(6, 30, 0); + while (alarmDate.getDay() !== 3) { + alarmDate.setHours(alarmDate.getHours() + 24); + } + alarmTime = alarmDate.getTime(); + this.alarm.time = [alarmDate.getHours(), alarmDate.getMinutes()]; + this.alarm.repeat = {}; + }); - AlarmEdit.save(function(alarm) { - assert.ok(alarm.id); - assert.ok(AlarmList.refresh.calledTwice); - assert.ok(AlarmManager.putAlarm.calledTwice); - assert.ok(AlarmManager.toggleAlarm.calledTwice); + test('No repeat -> today', function() { + setClock(alarmTime - 60000); + assert.equal(this.alarm.getNextAlarmFireTime().getTime(), alarmTime); + }); - assert.equal(alarm.vibrate, 1); - assert.equal(alarm.sound, 0); - done(); + test('No repeat -> tomorrow', function() { + setClock(alarmTime + 60000); + assert.equal(this.alarm.getNextAlarmFireTime().getTime(), + alarmTime + (24 * 60 * 60 * 1000)); }); + + for (var i = 0; i <= 7; i++) { + // starting on wednesday, today, then loop around and cover + // wednesday, tomorrow + var thisDay = days[(2 + i) % 7]; + test('Check ' + + ((i > 0) ? 'alarm-passed' : 'alarm-today') + + ' with repeat ' + thisDay, (function(i, thisDay) { + return function() { + var compareDate = new Date(alarmTime); + compareDate.setDate(compareDate.getDate() + i); + if (i === 0) { + setClock(alarmTime - 60000); + } else { + setClock(alarmTime + 60000); + } + // choose a repeat day + var repeat = {}; + repeat[thisDay] = true; + this.alarm.repeat = repeat; + assert.equal(this.alarm.getNextAlarmFireTime().getTime(), + compareDate.getTime()); + }; + })(i, thisDay)); + } }); - }); - suite('initTimeSelect', function() { - var alarm; + suite('Alarm scheduling', function() { + var navMozAlarms, setClock; - suiteSetup(function() { - alarm = AlarmEdit.alarm; - }); + suiteSetup(function() { + navMozAlarms = navigator.mozAlarms; + navigator.mozAlarms = new MockMozAlarms( + ActiveAlarm.handler); + }); - suiteTeardown(function() { - AlarmEdit.alarm = alarm; - }); + suiteTeardown(function() { + navigator.mozAlarms = navMozAlarms; + }); - test('0:0, should init time select with format of system time picker', - function() { - AlarmEdit.alarm.hour = '0'; - AlarmEdit.alarm.minute = '0'; - AlarmEdit.initTimeSelect(); - assert.equal(AlarmEdit.timeSelect.value, '00:00'); - }); + setup(function() { + setClock = clockSetter(this); + this.date = new Date(1374102438043); + // 6:17 on a Wednesday + this.date.setHours(6, 17, 0, 0); + while (this.date.getDay() !== 3) { + this.date = new Date(this.date.getTime() + (24 * 3600 * 1000)); + } + setClock(this.date.getTime()); + + this.alarm = new Alarm({ + id: 1, + repeat: { wednesday: true, friday: true }, + hour: this.date.getHours(), + minute: this.date.getMinutes(), + snooze: 17 + }); + }); - test('3:5, should init time select with format of system time picker', - function() { - AlarmEdit.alarm.hour = '3'; - AlarmEdit.alarm.minute = '5'; - AlarmEdit.initTimeSelect(); - assert.equal(AlarmEdit.timeSelect.value, '03:05'); - }); + teardown(function() { + var alarms = navigator.mozAlarms.alarms; + for (var i = 0; i < alarms.length; i++) { + clearTimeout(alarms[i].timeout); + } + navigator.mozAlarms.alarms = []; + }); - test('9:25, should init time select with format of system time picker', - function() { - AlarmEdit.alarm.hour = '9'; - AlarmEdit.alarm.minute = '25'; - AlarmEdit.initTimeSelect(); - assert.equal(AlarmEdit.timeSelect.value, '09:25'); - }); + test('Scheduling a normal alarm for the first time, repeat', + function(done) { + var cur = this.date.getTime() + (60 * 1000); + setClock(cur); + this.alarm.schedule({ + type: 'normal', first: true + }, function(err, alarm) { + var alarms = navigator.mozAlarms.alarms; + assert.equal(alarms.length, 1); + assert.equal(alarms[0].id, alarm.registeredAlarms['normal']); + assert.equal(alarms[0].date.getTime(), + this.date.getTime() + (2 * 24 * 3600 * 1000)); + done(); + }.bind(this)); + this.sinon.clock.tick(1000); + }); - test('12:55, should init time select with format of system time picker', - function() { - AlarmEdit.alarm.hour = '12'; - AlarmEdit.alarm.minute = '55'; - AlarmEdit.initTimeSelect(); - assert.equal(AlarmEdit.timeSelect.value, '12:55'); - }); + test('Scheduling a normal alarm for the first time, no repeat', + function(done) { + this.alarm.repeat = {}; + var cur = this.date.getTime() + (60 * 1000); + setClock(cur); + this.alarm.schedule({ + type: 'normal', first: true + }, function(err, alarm) { + var alarms = navigator.mozAlarms.alarms; + assert.equal(alarms.length, 1); + assert.equal(alarms[0].id, alarm.registeredAlarms['normal']); + assert.equal(alarms[0].date.getTime(), + this.date.getTime() + (24 * 3600 * 1000)); + done(); + }.bind(this)); + this.sinon.clock.tick(1000); + }); - test('15:5, should init time select with format of system time picker', - function() { - AlarmEdit.alarm.hour = '15'; - AlarmEdit.alarm.minute = '5'; - AlarmEdit.initTimeSelect(); - assert.equal(AlarmEdit.timeSelect.value, '15:05'); - }); + test('Scheduling a normal alarm for the second time, repeat', + function(done) { + setClock(this.date.getTime()); + this.alarm.schedule({ + type: 'normal', first: false + }, function(err, alarm) { + var alarms = navigator.mozAlarms.alarms; + assert.equal(alarms.length, 1); + assert.equal(alarms[0].id, alarm.registeredAlarms['normal']); + assert.equal(alarms[0].date.getTime(), + this.date.getTime() + (2 * 24 * 3600 * 1000)); + done(); + }.bind(this)); + this.sinon.clock.tick(1000); + }); - test('23:0, should init time select with format of system time picker', - function() { - AlarmEdit.alarm.hour = '23'; - AlarmEdit.alarm.minute = '0'; - AlarmEdit.initTimeSelect(); - assert.equal(AlarmEdit.timeSelect.value, '23:00'); + test('Scheduling a normal alarm for the first time, no repeat', + function(done) { + this.alarm.repeat = {}; + var cur = this.date.getTime() + (60 * 1000); + setClock(cur); + this.alarm.schedule({ + type: 'normal', first: true + }, function(err, alarm) { + var alarms = navigator.mozAlarms.alarms; + assert.equal(alarms.length, 1); + assert.equal(alarms[0].id, alarm.registeredAlarms['normal']); + assert.equal(alarms[0].date.getTime(), + this.date.getTime() + (24 * 3600 * 1000)); + done(); + }.bind(this)); + this.sinon.clock.tick(1000); + }); + + test('Scheduling a normal alarm for the second time, no repeat', + function(done) { + this.alarm.repeat = {}; + var cur = this.date.getTime() + (60 * 1000); + setClock(cur); + this.alarm.schedule({ + type: 'normal', first: false + }, function(err, alarm) { + var alarms = navigator.mozAlarms.alarms; + assert.equal(alarms.length, 0); + done(); + }.bind(this)); + this.sinon.clock.tick(1000); + }); + + test('Scheduling a snooze alarm', + function(done) { + this.alarm.repeat = {}; + setClock(this.date.getTime()); + this.alarm.schedule({ + type: 'snooze' + }, function(err, alarm) { + var alarms = navigator.mozAlarms.alarms; + assert.equal(alarms.length, 1); + assert.equal(alarms[0].id, alarm.registeredAlarms['snooze']); + assert.equal(alarms[0].date.getTime(), + this.date.getTime() + (17 * 60 * 1000)); + done(); + }.bind(this)); + this.sinon.clock.tick(1000); + }); }); }); }); + diff --git a/apps/clock/test/unit/mocks/mock_alarm_manager.js b/apps/clock/test/unit/mocks/mock_alarm_manager.js index 58c3d1fff620..e5060ca01be3 100644 --- a/apps/clock/test/unit/mocks/mock_alarm_manager.js +++ b/apps/clock/test/unit/mocks/mock_alarm_manager.js @@ -1,11 +1,6 @@ MockAlarmManager = { toggleAlarm: function() {}, - set: function() {}, - unset: function() {}, - delete: function() {}, - getAlarmList: function() {}, - getAlarmById: function() {}, - putAlarm: function() {}, + renderBannerBar: function() {}, updateAlarmStatusBar: function() {}, regUpdateAlarmEnableState: function() {} }; diff --git a/apps/clock/test/unit/mocks/mock_alarmsDB.js b/apps/clock/test/unit/mocks/mock_alarmsDB.js new file mode 100644 index 000000000000..95b5700a19ca --- /dev/null +++ b/apps/clock/test/unit/mocks/mock_alarmsDB.js @@ -0,0 +1,71 @@ +(function(exports) { + 'use strict'; + + function MockAlarmsDB() { + this.init(); + } + + MockAlarmsDB.prototype = { + + init: function() { + this.alarms = new Map(); + this.idCount = 0; + }, + + getAlarmList: function(callback) { + var collect = []; + for (var k of this.alarms) { + collect.push(k[0]); + } + setTimeout(function() { + callback(null, collect); + }, 0); + }, + + putAlarm: function(alarm, callback) { + if (!alarm.id) { + alarm = new Alarm(Utils.extend( + alarm.toSerializable(), + { id: this.idCount++ } + )); + } else { + alarm = new Alarm(alarm.toSerializable()); + } + this.alarms.set(alarm.id, alarm); + setTimeout(function() { + callback(null, alarm); + }, 0); + }, + + getAlarm: function(key, callback) { + if (this.alarms.has(key)) { + setTimeout(function() { + callback(null, this.alarms.get(key)); + }.bind(this), 0); + } else { + setTimeout(function() { + callback(new Error('key not found ' + key)); + }.bind(this), 0); + } + }, + + deleteAlarm: function(key, callback) { + if (this.alarms.has(key)) { + var value = this.alarms.get(key); + this.alarms.delete(key); + setTimeout(function() { + callback(null, value); + }, 0); + } else { + setTimeout(function() { + callback(new Error('key not found ' + key)); + }, 0); + } + + } + + }; + + exports.MockAlarmsDB = MockAlarmsDB; + +})(this); diff --git a/apps/clock/test/unit/mocks/mock_mozAlarm.js b/apps/clock/test/unit/mocks/mock_mozAlarm.js new file mode 100644 index 000000000000..079771c3055d --- /dev/null +++ b/apps/clock/test/unit/mocks/mock_mozAlarm.js @@ -0,0 +1,104 @@ +(function(exports) { + 'use strict'; + + var mozAlarmsId = 0; + + function MockOSAlarm(date, respectTimezone, data, callback) { + this.date = date || new Date(); + this.data = data || {}; + this.callback = callback; + this.id = mozAlarmsId++; + this.respectTimezone = + this.timeout = setTimeout(function() { + if ((typeof this.callback) === 'function') { + this.callback({ + id: this.id, + respectTimezone: this.respectTimezone, + date: this.date, + data: this.data + }); + } + }.bind(this), this.date.getTime() - Date.now()); + } + + function MockMozAlarmRequest(config) { + this.init(config || {}); + } + + MockMozAlarmRequest.prototype = { + + init: function mar_init(config) { + setTimeout(function() { + this.call(config.error, config.result); + }.bind(this), 0); + }, + + call: function mar_call(error, result) { + if (!error) { + this.onsuccess && this.onsuccess({ + target: { + result: result + } + }); + } else { + this.onerror && this.onerror({ + target: { + error: result + } + }); + } + } + + }; + + function MockMozAlarms(callback) { + this.init(callback); + } + + MockMozAlarms.prototype = { + + init: function moza_init(callback) { + this.alarms = []; + this.callback = callback; + }, + + getAll: function moza_getAll() { + return new MockMozAlarmRequest({ + result: this.alarms + }); + }, + + add: function moza_add(date, timezone, data) { + if (!(timezone === 'ignoreTimezone' || + timezone === 'honorTimezone')) { + throw new Error('invalid timezone argument'); + } + var alarm = new MockOSAlarm(date, timezone, data, function(x) { + this.callback(x); + this.remove(x.id); + }.bind(this)); + this.alarms.push(alarm); + return new MockMozAlarmRequest({ + result: alarm.id + }); + }, + + remove: function moza_remove(id) { + for (var i = 0; i < this.alarms.length; i++) { + var alarm = this.alarms[i]; + if (alarm && (alarm.id === id)) { + clearTimeout(alarm.timeout); + this.alarms.splice(i, 1); + return true; + } + } + return false; + } + + }; + + exports.MockOSAlarm = MockOSAlarm; + exports.MockMozAlarmRequest = MockMozAlarmRequest; + exports.MockMozAlarms = MockMozAlarms; + +}(this)); diff --git a/apps/clock/test/unit/mocks/mock_requestWakeLock.js b/apps/clock/test/unit/mocks/mock_requestWakeLock.js new file mode 100644 index 000000000000..c1d3ae1a6146 --- /dev/null +++ b/apps/clock/test/unit/mocks/mock_requestWakeLock.js @@ -0,0 +1,45 @@ +(function(exports) { + 'use strict'; + + function MockLock(type) { + this.type = type; + this.locked = true; + this.unlocks = 0; + } + + MockLock.prototype = { + unlock: function() { + this.locked = false; + this.unlocks++; + } + }; + + function MockRequestWakeLock() { + this.issued = new Map(); + } + + MockRequestWakeLock.prototype = { + + requestWakeLock: function mock_requestWakeLock(type) { + var lock = new MockLock(type); + this.issued.set(lock, true); + return lock; + }, + + getissued: function mock_getissued() { + var ret = []; + for (var el of this.issued) { + ret.push(el[0]); + } + return ret; + }, + + reset: function() { + this.issued.clear(); + } + + }; + + exports.MockRequestWakeLock = MockRequestWakeLock; + +})(this); diff --git a/apps/clock/test/unit/utils_test.js b/apps/clock/test/unit/utils_test.js index 97a8985e1e84..4db3b24c32ed 100644 --- a/apps/clock/test/unit/utils_test.js +++ b/apps/clock/test/unit/utils_test.js @@ -1,247 +1,9 @@ requireApp('clock/js/constants.js'); requireApp('clock/js/utils.js'); -suite('Time functions', function() { - - var _; - - var DAYS = [ - 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', - 'saturday', 'sunday' - ]; - - suiteSetup(function() { - _ = MockL10n.get; - }); - - suite('#summarizeDaysOfWeek', function() { - var summarizeDaysOfWeek; - - before(function() { - summarizeDaysOfWeek = Utils.summarizeDaysOfWeek; - }); - - test('should summarize everyday', function() { - assert.equal(summarizeDaysOfWeek({ - monday: true, - tuesday: true, - wednesday: true, - thursday: true, - friday: true, - saturday: true, - sunday: true - }), _('everyday')); - }); - - test('should summarize weekdays', function() { - assert.equal(summarizeDaysOfWeek({ - monday: true, - tuesday: true, - wednesday: true, - thursday: true, - friday: true - }), _('weekdays')); - }); - - test('should summarize weekends', function() { - assert.equal(summarizeDaysOfWeek({ - saturday: true, - sunday: true - }), _('weekends')); - }); - - test('should summarize never', function() { - assert.equal(summarizeDaysOfWeek({}), _('never')); - }); - - test('should summarize a single day', function() { - assert.equal(summarizeDaysOfWeek({monday: true}), - _('weekday-1-short')); - }); - - test('should summarize a single day', function() { - var monTueWed = _('weekday-1-short') + ', ' + - _('weekday-2-short') + ', ' + - _('weekday-3-short'); - assert.equal(summarizeDaysOfWeek({ - monday: true, - tuesday: true, - wednesday: true - }), monTueWed); - }); - - }); - - suite('isDateInRepeat', function() { - - var daymap = new Map(); - - var repeatodd = { - monday: true, // 1 - wednesday: true, // 3 - friday: true // 5 - }; - - var repeateven = { - sunday: true, // 0 - tuesday: true, // 2 - thursday: true, // 4 - saturday: true // 6 - }; - - var cur = new Date(); - for (var i = 0; i < 7; i++) { - daymap.set(cur.getDay(), cur); - cur = new Date(cur.getTime() + (24 * 3600 * 1000)); - } - - for (var el of daymap) { - (function(el) { - var repday = (el[0] + 6) % 7; - var oddeven = ((el[0] % 2) === 1) ? repeatodd : repeateven; - var evenodd = ((el[0] % 2) === 0) ? repeatodd : repeateven; - test(DAYS[repday] + '[' + el[0] + '] is in ' + - JSON.stringify(oddeven), function() { - assert.ok(Utils.isDateInRepeat(oddeven, daymap.get(el[0]))); - }); - test(DAYS[repday] + '[' + el[0] + '] is not in ' + - JSON.stringify(evenodd), function() { - assert.ok(!Utils.isDateInRepeat(evenodd, daymap.get(el[0]))); - }); - })(el); - } - }); - - suite('repeatDays', function() { - for (var i = 0; i <= 7; i++) { - var rep = {}; - for (var j = 0; j < i; j++) { - rep[DAYS[j]] = true; - } - (function(testrepeat, value) { - test(JSON.stringify(testrepeat) + ' has ' + value + ' repeat days', - function() { - assert.equal(Utils.repeatDays(testrepeat), value); - }); - })(rep, i); - } - }); - - suite('#isAlarmPassToday', function() { - - var isAlarmPassToday; - - setup(function() { - var time = new Date(); - time.setHours(6, 30); - this.clock = sinon.useFakeTimers(time.getTime()); - isAlarmPassToday = Utils.isAlarmPassToday; - }); - - teardown(function() { - this.clock.restore(); - }); - - test('prior hour, prior minute', function() { - assert.isTrue(isAlarmPassToday(5, 00)); - }); - - test('prior hour, current minute', function() { - assert.isTrue(isAlarmPassToday(5, 30)); - }); - - test('prior hour, later minute', function() { - assert.isTrue(isAlarmPassToday(5, 45)); - }); - - test('current hour, prior minute', function() { - assert.isTrue(isAlarmPassToday(6, 29)); - }); - - test('current hour, current minute', function() { - assert.isTrue(isAlarmPassToday(6, 30)); - }); - - test('current hour, later minute', function() { - assert.isFalse(isAlarmPassToday(6, 31)); - }); - - test('later hour, prior minute', function() { - assert.isFalse(isAlarmPassToday(7, 29)); - }); - - test('later hour, current minute', function() { - assert.isFalse(isAlarmPassToday(7, 30)); - }); - - test('later hour, later minute', function() { - assert.isFalse(isAlarmPassToday(7, 31)); - }); - - }); - - suite('Next alarm time', function() { - - var clockSetter = function(thisVal) { - return function(x) { - this.sinon.clock.tick( - (-1 * this.sinon.clock.tick()) + x); - }.bind(thisVal); - }; - - var alarmTime, alarmDate, setClock; +requireApp('clock/test/unit/mocks/mock_requestWakeLock.js'); - setup(function() { - setClock = clockSetter(this); - // Wed Jul 17 2013 19:07:18 GMT-0400 (EDT) - alarmTime = 1374057000000; - alarmDate = new Date(alarmTime); - this.alarm = { - repeat: {}, - hour: alarmDate.getHours(), - minute: alarmDate.getMinutes() - }; - this.sinon.useFakeTimers(); - }); - - test('No repeat -> today', function() { - setClock(alarmTime - 60000); - assert.equal( - Utils.getNextAlarmFireTime(this.alarm).getTime(), - alarmTime); - }); - - test('No repeat -> tomorrow', function() { - setClock(alarmTime + 60000); - assert.equal(Utils.getNextAlarmFireTime(this.alarm).getTime(), - alarmTime + (24 * 60 * 60 * 1000)); - }); - - for (var i = 0; i <= 7; i++) { - // starting on wednesday, today, then loop around and cover - // wednesday, tomorrow - var thisDay = DAYS[(2 + i) % 7]; - test('Check ' + - ((i > 0) ? 'alarm-passed' : 'alarm-today') + - ' with repeat ' + thisDay, (function(i, thisDay) { - return function() { - var compareDate = new Date(alarmTime); - compareDate.setDate(compareDate.getDate() + i); - if (i === 0) { - setClock(alarmTime - 60000); - } else { - setClock(alarmTime + 60000); - } - // choose a repeat day - var repeat = {}; - repeat[thisDay] = true; - this.alarm.repeat = repeat; - assert.equal(Utils.getNextAlarmFireTime(this.alarm).getTime(), - compareDate.getTime()); - }; - })(i, thisDay)); - } - }); +suite('Time functions', function() { suite('#changeSelectByValue', function() { @@ -350,4 +112,208 @@ suite('Time functions', function() { assert.equal(time.minute, 45); }); }); + + suite('extend tests', function() { + + function hasOwn(obj, prop) { + return Object.prototype.hasOwnProperty.call(obj, prop); + } + + function hasAny(obj, prop) { + return hasOwn(obj, prop) || ( + obj !== Object.prototype && hasAny(obj.__proto__, prop)); + } + + function testObject() { + return Object.create( + Object.create({ grandparents: 4 }, + { parents: { value: 2 } }), { + me: { value: 1 } + }); + } + + test('basic extend', function() { + var x = {}; + var y = Utils.extend(x, {a: 1, b: 2, c: 3}); + assert.ok(x === y, 'x === y'); + assert.ok(hasOwn(x, 'a') && x.a === 1); + assert.ok(hasOwn(x, 'b') && x.b === 2); + assert.ok(hasOwn(x, 'c') && x.c === 3); + }); + + test('extend only affects child instance', function() { + var x = testObject(), y; + y = Utils.extend(x, {favoriteColor: 'green'}); + assert.ok(x === y, 'x === y'); + assert.equal(x.favoriteColor, 'green'); + assert.ok(hasOwn(x, 'favoriteColor')); + assert.ok(!hasAny(x.__proto__, 'favoriteColor')); + }); + + test('multiple extend', function() { + var x = Utils.extend({}, { + a: 42 + }, { + a: 1, + b: 19, + c: 45 + }, { + b: 199 + }); + assert.equal(x.a, 1); + assert.equal(x.b, 199); + assert.equal(x.c, 45); + }); + + }); + + suite('safeCpuLock tests', function() { + + setup(function() { + this.mocklock = new MockRequestWakeLock(); + this.sinon.stub(navigator, 'requestWakeLock', + this.mocklock.requestWakeLock.bind(this.mocklock)); + this.sinon.useFakeTimers(); + }); + + teardown(function() { + this.sinon.restore(navigator, 'requestWakeLock'); + }); + + test('locks CPU', function() { + var callback = this.sinon.spy(function(unlock) { + assert.ok(navigator.requestWakeLock.calledOnce); + }); + Utils.safeCpuLock(15000, callback); + this.sinon.clock.tick(16000); + assert.ok(callback.calledOnce); + }); + + test('single unlock', function() { + Utils.safeCpuLock(15000, function(unlock) { + unlock(); + }); + this.sinon.clock.tick(16000); + var locks = this.mocklock.getissued(); + assert.equal(locks.length, 1); + assert.equal(locks[0].unlocks, 1); + }); + + test('no duplicate unlock', function() { + var here = 0; + Utils.safeCpuLock(15000, function(unlock) { + setTimeout(function() { + here++; + unlock(); + }, 16000); + here++; + }); + this.sinon.clock.tick(17000); + var locks = this.mocklock.getissued(); + assert.equal(locks.length, 1); + assert.equal(locks[0].unlocks, 1); + assert.equal(here, 2); + }); + + test('timeout unlock', function() { + var here = false; + Utils.safeCpuLock(15000, function(unlock) { + here = true; + }); + this.sinon.clock.tick(16000); + var locks = this.mocklock.getissued(); + assert.equal(locks.length, 1); + assert.equal(locks[0].unlocks, 1); + assert.ok(here); + }); + + test('exception in callback still unlocks CPU', function() { + var here = false; + try { + Utils.safeCpuLock(15000, function(unlock) { + here = true; + throw new Error('gotcha'); + }); + this.sinon.clock.tick(16000); + } catch (err) { + assert.equal(err.message, 'gotcha'); + } + var locks = this.mocklock.getissued(); + assert.equal(locks.length, 1); + assert.equal(locks[0].unlocks, 1); + assert.ok(here); + }); + + }); + + suite('async', function() { + test('generators', function() { + var spy = this.sinon.spy(function() { + assert.equal(arguments[0], null); + assert.equal(arguments.length, 1); + }); + var gen = Utils.async.generator(spy); + var collection = []; + for (var i = 0; i < 100; i++) { + collection.push(gen()); + } + assert.ok(!spy.called); + for (var k of collection) { + k(); + } + assert.ok(spy.calledOnce); + }); + + test('generators error', function() { + var spy = this.sinon.spy(function() { + assert.equal(arguments[0], 1); + assert.equal(arguments.length, 1); + }); + var gen = Utils.async.generator(spy); + var collection = []; + for (var i = 0; i < 100; i++) { + collection.push(gen()); + } + assert.ok(!spy.called); + var j = 1; + for (var k of collection) { + k(j++); + } + assert.ok(spy.calledOnce); + }); + + test('namedParallel', function() { + var spy = this.sinon.spy(function() { + assert.equal(arguments[0], null); + assert.equal(arguments.length, 1); + }); + var names = Utils.async.namedParallel([ + 'testa', 'testb', 'testc' + ], spy); + assert.ok(!spy.called); + names.testa(); + assert.ok(!spy.called); + names.testb(); + assert.ok(!spy.called); + names.testc(); + assert.ok(spy.calledOnce); + }); + + test('namedParallel error', function() { + var spy = this.sinon.spy(function() { + assert.equal(arguments[0], 'testing b'); + assert.equal(arguments.length, 1); + }); + var names = Utils.async.namedParallel([ + 'testa', 'testb', 'testc' + ], spy); + assert.ok(!spy.called); + names.testa(); + assert.ok(!spy.called); + names.testb('testing b'); + assert.ok(spy.calledOnce); + names.testc(); + assert.ok(spy.calledOnce); + }); + }); });