Publish Vulpes Preview 15 sources for desktop, Android and Tundra

Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,23 @@
'use strict';
/*global assert */
assert.hasProperties = function chai_hasProperties(given, props, msg) {
msg = (typeof(msg) === 'undefined') ? '' : msg + ': ';
if (props instanceof Array) {
props.forEach(function(prop) {
assert.ok(
(prop in given),
msg + 'given should have "' + prop + '" property'
);
});
} else {
for (var key in props) {
assert.deepEqual(
given[key],
props[key],
msg + ' property equality for (' + key + ') '
);
}
}
};

View file

@ -0,0 +1,27 @@
'use strict';
/* jshint node: true, mocha: true */
/* global suiteSetup */
var helper = require('helper');
suite('Calendar tests', function() {
suiteSetup(helper.cleanupWorkspace);
teardown(helper.cleanupWorkspace);
test('APP=calendar make', function(done) {
helper.exec('APP=calendar make', function(error, stdout, stderr) {
helper.checkError(error, stdout, stderr);
var zipPath = process.cwd() +
'/profile/webapps/calendar.gaiamobile.org/application.zip';
var checkList = ['manifest.webapp'];
checkList.forEach(function(filePath) {
helper.checkFilePathInZip(zipPath, filePath);
});
done();
});
});
});

View file

@ -0,0 +1,231 @@
/* global __dirname, require, marionette, setup, suite, teardown, test */
'use strict';
var Calendar = require('../marionette/lib/calendar'),
assert = require('chai').assert,
debug = require('common/debug')('interop:basic'),
fs = require('fs'),
path = require('path');
// See https://bugzil.la/1011192 about Yahoo!
var providers = [
'oracle'
];
marionette('interop basic', function() {
var app;
var client = marionette.client();
providers.forEach(function(provider) {
var config = JSON.parse(
fs.readFileSync(
path.join(__dirname, provider, 'config.json')
)
);
var vanillaEvent = Object.freeze({
title: 'Vanilla event',
location: 'Baskin Robbins',
description: 'What better a place for a vanilla event!',
calendar: config.calendars[0],
startHour: 17,
reminders: []
});
var allDayEvent = Object.freeze({
title: 'All day event',
location: 'Planet Earth',
description: 'Except for the mars rover!',
calendar: config.calendars[0],
allDay: true,
reminders: []
});
var eventWithAlarms = Object.freeze({
title: 'Event with alarms',
location: 'Fire Station',
description: 'Wee-Woo-Wee-Woo-Wee-Woo',
calendar: config.calendars[0],
startHour: 10,
reminders: ['5 minutes before', '1 hour before']
});
var vanillaElement, allDayElement, elementWithAlarms;
suite(provider, function() {
setup(function() {
debug('Launch calendar.');
app = new Calendar(client);
app.launch();
app.setupAccount(setupOptions(provider, config));
app.monthDay.events.forEach(function(event) {
switch (event.title.trim()) {
case vanillaEvent.title:
vanillaElement = event;
break;
case allDayEvent.title:
allDayElement = event;
break;
case eventWithAlarms.title:
elementWithAlarms = event;
break;
default:
break;
}
});
});
teardown(function() {
debug('Delete account from calendar app.');
app.teardownAccount(config.user);
});
/**
* Major hack around suiteSetup not being fully supported
* in marionette-js-runner.
*/
test('suiteSetup', function() {
debug('Create test data.');
[
vanillaEvent,
allDayEvent,
eventWithAlarms
].forEach(function(event) {
app.createEvent(event);
});
debug('Sync created data to ' + provider + '.');
app.sync();
});
test('should get all calendars', function() {
debug('Reading list of calendars from settings view.');
app.openSettingsView();
var synced = app.settings.calendars();
app.closeSettingsView();
config.calendars.forEach(function(calendar) {
debug('Looking for ' + calendar + '.');
assert.include(synced, calendar, calendar + ' synced');
});
});
test('should get existing events', function() {
var events = app.monthDay.events;
[
vanillaEvent,
allDayEvent,
eventWithAlarms
].forEach(function(created) {
debug('Looking for event titled ' + created.title + '.');
assert.ok(
events.some(function(event) {
app.monthDay.scrollToEvent(event);
return event.title.trim() === created.title;
})
);
});
});
test('event start times', function() {
assert.strictEqual(
vanillaElement.closestHour,
vanillaEvent.startHour
);
assert.strictEqual(
elementWithAlarms.closestHour,
eventWithAlarms.startHour
);
// Make sure that we are inside an all day element.
assert.ok(
!!allDayElement.closestAllDay,
'should recognize all day events'
);
});
test('event descriptions', function() {
[
{ element: vanillaElement, event: vanillaEvent },
{ element: allDayElement, event: allDayEvent },
{ element: elementWithAlarms, event: eventWithAlarms }
].forEach(function(obj) {
var element = obj.element,
event = obj.event;
app.monthDay.scrollToEvent(element);
element.click();
app.readEvent.waitForDisplay();
assert.strictEqual(
app.readEvent.description.trim(),
event.description
);
app.readEvent.cancel();
app.month.waitForDisplay();
});
});
test('event locations', function() {
assert.strictEqual(
vanillaElement.address.trim(),
vanillaEvent.location
);
assert.strictEqual(
allDayElement.address.trim(),
allDayEvent.location
);
assert.strictEqual(
elementWithAlarms.address.trim(),
eventWithAlarms.location
);
});
test('event alarms', function() {
assert.isFalse(vanillaElement.hasAlarms());
assert.isFalse(allDayElement.hasAlarms());
assert.isTrue(elementWithAlarms.hasAlarms());
});
test('suiteTeardown', function() {
debug('Delete test data.');
var events = app.monthDay.events;
events.forEach(function(event) {
app.monthDay.scrollToEvent(event);
event.click();
app.readEvent.waitForDisplay();
if (app.readEvent.editable) {
app.readEvent.edit();
app.editEvent.waitForDisplay();
app.editEvent.delete();
} else {
app.readEvent.cancel();
}
app.monthDay.waitForDisplay();
});
debug('Sync deletions to ' + provider + '.');
app.sync();
});
});
});
});
function setupOptions(provider, config) {
debug('Authorize ' + provider + ' account.');
var options = {
user: config.user,
password: config.password,
accountType: provider
};
if (provider !== 'google' && provider !== 'yahoo') {
options.accountType = 'caldav';
}
if ('fullUrl' in config) {
options.fullUrl = config.fullUrl;
}
return options;
}

View file

@ -0,0 +1,6 @@
{
"user": "caltest1",
"password": "CHANGE ME",
"fullUrl": "https://ocucsdev2.developer.ucspreview.com",
"calendars": [ "caltest1" ]
}

View file

@ -0,0 +1,18 @@
{
"extends": "../../../../.jshintrc",
"node": true,
"predef": [
"suite",
"test",
"setup",
"teardown",
"suiteSetup",
"suiteTeardown",
"marionette",
"marionetteScriptFinished",
"require",
"define",
"requireApp",
"sinon"
]
}

View file

@ -0,0 +1,134 @@
'use strict';
var Calendar = require('./lib/calendar'),
assert = require('chai').assert;
var SHARED_PATH = __dirname + '/../../../../shared/test/integration';
marionette('alarm', function() {
var app;
var client = marionette.client({
profile: {
prefs: {
// we need to disable the keyboard to avoid intermittent failures on
// Travis (transitions might take longer to run and block UI)
'dom.mozInputMethod.enabled': false,
// Do not require the B2G-desktop app window to have focus (as per the
// system window manager) in order for it to do focus-related things.
'focusmanager.testmode': true
}
},
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
suite(' > with alarm mocks', function() {
setup(function() {
app = new Calendar(client);
client.contentScript.inject(SHARED_PATH +
'/mock_navigator_mozalarms.js');
client.contentScript.inject(SHARED_PATH +
'/mock_navigator_moz_set_message_handler.js');
app.launch();
});
suite('create event with a single reminder', function() {
setup(function() {
createEventWithReminders(app, ['5 minutes before']);
app.monthDay.scrollToEvent();
});
test('should display reminder in read-only event view', function() {
var reminders = getEventReminders(app);
assert.equal(reminders.length, 1);
assert.include(reminders, '5 minutes before');
});
test('should add alarm to alarms api', function() {
var alarms = getMozAlarms(client);
assert.equal(alarms.length, 1);
});
});
suite('create an event with two valid reminders', function() {
setup(function() {
createEventWithReminders(app, ['5 minutes before',
'15 minutes before']);
app.monthDay.scrollToEvent();
});
test('should display both reminders in read-only view', function() {
var reminders = getEventReminders(app);
assert.equal(reminders.length, 2);
assert.include(reminders, '5 minutes before');
assert.include(reminders, '15 minutes before');
});
test('should only fire one alarm', function() {
var alarms = getMozAlarms(client);
assert.equal(alarms.length, 1);
});
});
});
suite('> without alarm mocks', function() {
setup(function() {
app = new Calendar(client);
app.launch();
});
suite('create two events with simultaneous reminders', function() {
setup(function() {
var startTime = Date.now() + 5000; // 5 seconds in future
app.createEvent({
title: 'First Event',
startDate: new Date(startTime),
reminders: ['At time of event']
});
app.createEvent({
title: 'Second Event',
startDate: new Date(startTime),
reminders: ['At time of event']
});
});
test('both events trigger notifications', function() {
client.switchToFrame();
client.waitFor(function() {
var notificationSelector = '#desktop-notifications-container > div';
return client.findElements(notificationSelector).length > 1;
});
});
});
});
});
function createEventWithReminders(app, reminders) {
app.createEvent({
title: 'Panic!',
location: 'The Disco',
startDate: new Date(),
reminders: reminders
});
}
function getEventReminders(app) {
var event = app.monthDay.events[0];
event.click();
app.readEvent.waitForDisplay();
return app.readEvent.alarms;
}
function getMozAlarms(client) {
var alarms;
client.waitFor(function() {
alarms = client.executeScript(function() {
return window.wrappedJSObject.navigator.__mozFakeAlarms;
});
return alarms && alarms.length;
});
return alarms;
}

View file

@ -0,0 +1,130 @@
'use strict';
var Calendar = require('./lib/calendar'),
Radicale = require('./lib/radicale'),
assert = require('chai').assert,
dateFormat = require('dateformat');
var ACCOUNT_USERNAME = 'firefox-os',
TITLE = 'Go for a dream',
DATE_PATTERN = 'yyyymmdd"T"HHMMssZ';
marionette('configure CalDAV accounts', function() {
var client = marionette.client({
profile: {
prefs: {
// we need to disable the keyboard to avoid intermittent failures on
// Travis (transitions might take longer to run and block UI)
'dom.mozInputMethod.enabled': false,
// Do not require the B2G-desktop app window to have focus (as per the
// system window manager) in order for it to do focus-related things.
'focusmanager.testmode': true,
}
},
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var serverHelper = new Radicale(),
app = null;
setup(function(done) {
app = new Calendar(client);
app.launch();
serverHelper.start(function(error, port) {
if (error) {
return done(error);
}
var accountUrl = 'http://localhost:' + port + '/' + ACCOUNT_USERNAME,
startDate = new Date(),
endDate = new Date(),
event = {};
app.setupAccount({
accountType: 'caldav',
user: ACCOUNT_USERNAME,
fullUrl: accountUrl
});
// Make sure we have a event item
// in the top of event-list view in month view.
startDate.setMinutes(0);
endDate.setMinutes(59);
event = {
startDate: dateFormat(startDate, DATE_PATTERN),
endDate: dateFormat(endDate, DATE_PATTERN),
title: TITLE
};
serverHelper.addEvent(ACCOUNT_USERNAME, event);
app.sync();
// Make sure we start the server before the setup is ended.
done();
});
});
teardown(function(done) {
// No matter how, we close the server in the end.
try {
serverHelper.removeAllEvents();
} catch (error) {
console.error(error.message);
} finally {
serverHelper.close(done);
}
});
test('should show a event', function() {
var event = app.monthDay.events[0];
// Scroll so that the first one is in view and click it.
app.monthDay.scrollToEvent(event);
event.click();
assertEvent(ACCOUNT_USERNAME, TITLE);
});
test('events from disabled calendars should not be displayed', function() {
var events = app.monthDay.events;
assert.equal(events.length, 1, 'at least one event');
app.openSettingsView();
app.settings.toggleCalendar(ACCOUNT_USERNAME);
app.closeSettingsView();
assert.deepEqual(app.monthDay.events, [], 'events should be removed');
var startDate = new Date();
var endDate = new Date();
endDate.setHours(endDate.getHours() + 1);
var event = {
startDate: dateFormat(startDate, DATE_PATTERN),
endDate: dateFormat(endDate, DATE_PATTERN),
title: TITLE
};
serverHelper.addEvent(ACCOUNT_USERNAME, event);
app.openSettingsView();
app.settings.sync();
app.closeSettingsView();
assert.deepEqual(app.monthDay.events, [], 'events should not be added');
});
function assertEvent(username, title) {
app.readEvent.waitForDisplay();
assert.deepEqual(
app.readEvent.title,
title,
'event should have correct title'
);
// Make sure the event is created in the CalDAV username.
assert.deepEqual(
app.readEvent.calendar,
username,
'event should be created by the correct user'
);
}
});

View file

@ -0,0 +1,257 @@
'use strict';
var Calendar = require('./lib/calendar');
var assert = require('chai').assert;
marionette('creating events', function() {
var client = marionette.client({
profile: {
prefs: {
// we need to disable the keyboard to avoid intermittent failures on
// Travis (transitions might take longer to run and block UI)
'dom.mozInputMethod.enabled': false,
// Do not require the B2G-desktop app window to have focus (as per the
// system window manager) in order for it to do focus-related things.
'focusmanager.testmode': true,
}
},
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var app;
var startDate = new Date('Sep 08 1991 12:34:56');
var startDateNextHour = new Date(startDate);
startDateNextHour.setHours(startDate.getHours() + 1);
var startDateNextDay = new Date(startDate);
startDateNextDay.setDate(startDate.getDate() + 1);
var scenarios = [
{
name: 'one day',
allDay: false,
startDate: startDate,
endDate: startDateNextHour,
reminders: ['5 minutes before'],
verifyMonthView: function() {
var event = app.monthDay.events[0];
assert.equal(event.startTime, '12:34 PM', 'start time');
assert.equal(event.endTime, '1:34 PM', 'end time');
},
verifyReadEventView: function() {
var readEvent = app.readEvent;
assert.equal(
readEvent.durationTime,
'Sunday, September 08, 1991\n' +
'from 12:34 PM to 1:34 PM',
'duration time'
);
assert.equal(
readEvent.reminders[0].text(),
'5 minutes before',
'reminder'
);
}
},
{
name: 'multiple day',
allDay: false,
startDate: startDate,
endDate: startDateNextDay,
reminders: ['5 minutes before'],
verifyMonthView: function() {
var month = app.month;
assert.lengthOf(month.squareDots(month.daySquares[7]), 1);
assert.lengthOf(month.squareDots(month.daySquares[8]), 1);
assert.equal(month.busyDots.length, 2, '2 busy dots');
var event = app.monthDay.events[0];
assert.equal(event.startTime, '12:34 PM', 'start time');
assert.equal(event.endTime, '12:34 PM', 'end time');
},
verifyReadEventView: function() {
var readEvent = app.readEvent;
assert.equal(
readEvent.durationTime,
'From 12:34 PM Sunday, September 08, 1991\n' +
'to 12:34 PM Monday, September 09, 1991',
'duration time'
);
assert.equal(
readEvent.reminders[0].text(),
'5 minutes before',
'reminder'
);
}
},
{
name: 'one all day',
allDay: true,
startDate: startDate,
endDate: startDateNextHour,
reminders: ['On day of event'],
verifyMonthView: function() {
var event = app.monthDay.events[0];
assert.equal(event.allDay, 'All Day', 'event hour');
},
verifyReadEventView: function() {
var readEvent = app.readEvent;
assert.equal(
readEvent.durationTime,
'All day\n' +
'Sunday, September 08, 1991',
'duration time'
);
assert.equal(
readEvent.reminders[0].text(),
'On day of event',
'reminder'
);
}
},
{
name: 'multiple all day',
allDay: true,
startDate: startDate,
endDate: startDateNextDay,
reminders: ['On day of event'],
verifyMonthView: function() {
var month = app.month;
assert.lengthOf(month.squareDots(month.daySquares[7]), 1);
assert.lengthOf(month.squareDots(month.daySquares[8]), 1);
assert.equal(month.busyDots.length, 2, '2 busy dots');
assert.equal(
app.monthDay.events[0].allDay,
'All Day',
'event hour'
);
},
verifyReadEventView: function() {
var readEvent = app.readEvent;
assert.equal(
readEvent.durationTime,
'All day from Sunday, September 08, 1991\n' +
'to Monday, September 09, 1991',
'duration time'
);
assert.equal(
readEvent.reminders[0].text(),
'On day of event',
'reminder'
);
}
},
{
name: '24 hour format',
allDay: false,
startDate: startDate,
endDate: startDateNextHour,
reminders: ['5 minutes before'],
verifyMonthView: function() {
app.switch24HourTimeFormat();
var event = app.monthDay.events[0];
assert.equal(event.startTime, '12:34', 'start time');
assert.equal(event.endTime, '13:34', 'end time');
},
verifyReadEventView: function() {
app.switch24HourTimeFormat();
var readEvent = app.readEvent;
assert.equal(
readEvent.durationTime,
'Sunday, September 08, 1991\n' +
'from 12:34 to 13:34',
'duration time'
);
assert.equal(
readEvent.reminders[0].text(),
'5 minutes before',
'reminder'
);
}
}
];
setup(function() {
app = new Calendar(client);
app.launch();
});
scenarios.forEach(function(scenario) {
suite('creating ' + scenario.name + ' event', function() {
setup(function() {
// we use default title/description/location to avoid duplication but
// allow overriding if needed
scenario.title = scenario.title ||
'Puppy Bowl dogefortlongtextfotestloremipsumdolorsitamet';
scenario.description = scenario.description ||
'lorem ipsum dolor sit amet maecennas ullamcor';
scenario.location = scenario.location ||
'Animal Planet reallylongwordthatshouldnotoverflowbecausewewrap';
app.createEvent(scenario);
app.month.waitForDisplay();
});
test('should display the created event in months day', function() {
var event = app.monthDay.events[0];
assert.equal(event.title, scenario.title, 'event title');
assert.equal(event.address, scenario.location, 'event location');
scenario.verifyMonthView();
});
suite('opening event in read view', function() {
setup(function() {
var event = app.monthDay.events[0];
// Scroll so that the first one is in view and click it.
app.monthDay.scrollToEvent(event);
event.click();
// Wait until the read view is displayed.
app.readEvent.waitForDisplay();
});
test('should display the created event in read view', function() {
var readEvent = app.readEvent;
assert.equal(readEvent.title, scenario.title, 'event title');
assert.equal(
readEvent.description,
scenario.description,
'event description'
);
assert.equal(readEvent.location, scenario.location, 'location');
assert.equal(readEvent.calendar, 'Offline calendar');
assert.equal(readEvent.calendarColor, 'rgb(249, 124, 23)');
scenario.verifyReadEventView();
});
test('should not overflow title, location, or description',
function() {
var readEvent = app.readEvent;
app.checkOverflow(readEvent.titleContainer, 'title');
app.checkOverflow(readEvent.descriptionContainer, 'description');
app.checkOverflow(readEvent.locationContainer, 'location');
});
});
});
});
test('only enable save button if title or location are set', function() {
app.openModifyEventView();
var editEvent = app.editEvent;
client.waitFor(function() {
return editEvent.saveButton.getAttribute('disabled') === 'true';
});
editEvent.title = 'foo';
client.waitFor(function() {
return editEvent.saveButton.getAttribute('disabled') === 'false';
});
editEvent.title = '';
client.waitFor(function() {
return editEvent.saveButton.getAttribute('disabled') === 'true';
});
editEvent.location = 'bar';
client.waitFor(function() {
return editEvent.saveButton.getAttribute('disabled') === 'false';
});
});
});

View file

@ -0,0 +1,364 @@
'use strict';
var Calendar = require('./lib/calendar'),
assert = require('chai').assert;
marionette('day view', function() {
var app;
var day;
var month;
var client = marionette.client({
profile: {
prefs: {
// we need to disable the keyboard to avoid intermittent failures on
// Travis (transitions might take longer to run and block UI)
'dom.mozInputMethod.enabled': false,
// Do not require the B2G-desktop app window to have focus (as per the
// system window manager) in order for it to do focus-related things.
'focusmanager.testmode': true,
}
},
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
setup(function() {
app = new Calendar(client);
app.launch();
app.openDayView();
day = app.day;
month = app.month;
});
test('header copy should not overflow', function() {
// XXX: we don't use app.checkOverflow() because of Bug 971691
// 20 chars is a "safe" limit if font-family is Fira Sans
assert.operator(app.headerContent.text().length, '<', 21);
});
suite('events longer than 2h', function() {
setup(function() {
app.createEvent({
title: 'Lorem Ipsum',
location: 'Dolor Amet',
startHour: 0,
duration: 3
});
day.waitForDisplay();
day.waitForHourScrollEnd(0);
});
test('click after first hour', function() {
day.events[0].click();
app.readEvent.waitForDisplay();
assert.equal(
app.readEvent.title,
'Lorem Ipsum',
'title should match'
);
});
test('double tap after event end', function() {
// we need to actually grab the event position + height to avoid issues
// with DST (see Bug 981441)
var event = day.events[0];
var body = client.findElement('body');
var position = event.location();
var size = event.size();
app.actions
.doubleTap(body, position.x + 20, position.y + size.height + 20)
.perform();
// there is a delay between tap and view display
app.editEvent.waitForDisplay();
});
});
test('all day', function() {
assert(day.allDayIcon.displayed(), 'should display all day icon');
});
test('event', function() {
var eventData = {
title: 'Test Day View',
location: 'Somewhere',
startHour: 1,
reminders: ['5 minutes before']
};
app.createEvent(eventData);
var event = day.events[0];
assert.equal(
event.title.text(), eventData.title, 'display event title'
);
assert.equal(
event.address.text(), eventData.location, 'display location'
);
// calendar colors
assert.match(
event.cssProperty('background-color'),
/rgba\(.+0.2\)/,
'should set bg color'
);
var borderColor = event.cssProperty('border-left-color');
assert.ok(
borderColor,
'should set the border color'
);
assert.ok(
parseFloat(event.cssProperty('border-left-width')) > 0,
'should have border'
);
assert.equal(
event.cssProperty('border-left-style'),
'solid',
'should have solid border'
);
assert.equal(
event.iconAlarm.cssProperty('color'),
borderColor,
'alarm icon color should match border color'
);
});
suite('overlaps', function() {
function createEvent(title, location) {
app.createEvent({
title: title,
location: location,
startHour: 1
});
}
function eventsTitleAndLocationDisplayed() {
return day.events.every(titleAndLocationDisplayed);
}
function titleAndLocationDisplayed(event) {
return checkDisplay(event.title) &&
checkDisplay(event.address);
}
function checkDisplay(el, name, i) {
return el.text().length > 0 && el.displayed();
}
test('event details display', function() {
createEvent('1 Overlap', 'Somewhere');
createEvent('2 Overlap', 'Somewhere Else');
createEvent('3 Overlap', 'Here');
createEvent('4 Overlap', 'There');
assert.ok(
eventsTitleAndLocationDisplayed(),
'details should be displayed if we have less than 5 events overlaps'
);
createEvent('5 Overlap', 'Earth');
assert.isFalse(
eventsTitleAndLocationDisplayed(),
'details should be hidden if we have more than 4 events overlaps'
);
createEvent('6 Overlap', 'Mars');
createEvent('7 Overlap', 'Mind');
assert.isFalse(
eventsTitleAndLocationDisplayed(),
'details should be hidden if we have more than 4 events overlaps'
);
});
});
test('current-time', function() {
day.waitForHourScrollEnd();
var currentTime = day.currentTime;
assert.ok(
currentTime.displayed(),
'current-time should be active'
);
assert.ok(
intersect(currentTime, day.currentHour),
'current time should be inside current hour range'
);
assert.ok(
!day.currentDisplayHour.displayed(),
'hour should be hidden if overlapping'
);
function intersect(el1, el2) {
var b1 = getBounds(el1);
var b2 = getBounds(el2);
return (
b1.left <= b2.right &&
b2.left <= b1.right &&
b1.top <= b2.bottom &&
b2.top <= b1.bottom
);
}
function getBounds(element) {
return element.scriptWith(function(el) {
return el.getBoundingClientRect();
});
}
// it should hide the currentTime if current day is not visible
app.swipeLeft();
client.waitFor(function() {
return !currentTime.displayed() && day.currentDisplayHour.displayed();
});
});
suite('animated scrolling', function() {
setup(function() {
day.waitForHourScrollEnd();
});
test('today', function() {
assert.equal(
day.scrollTop,
day.getDestinationScrollTop(new Date().getHours() - 1),
'scroll to the previous hour of current time'
);
});
test('select next or previous day in the month', function() {
var today = new Date();
var selectedDay;
app.openMonthView();
if (today.getDate() === 1 && today.getDay() === 0) {
selectedDay = month.days[1];
} else {
selectedDay = month.days[0];
}
selectedDay.click();
app.openDayView();
day.waitForHourScrollEnd();
assert.equal(
day.scrollTop,
day.getDestinationScrollTop(8),
'scroll to the 8AM element'
);
});
test('swipe to the next day', function() {
var previousScrollTop = day.scrollTop;
app.swipeLeft();
assert.equal(
day.scrollTop,
previousScrollTop,
'same scrollTop'
);
});
test('swipe to the previous day', function() {
var previousScrollTop = day.scrollTop;
app.swipeRight();
assert.equal(
day.scrollTop,
previousScrollTop,
'same scrollTop'
);
});
test('swipe to the today', function() {
app.swipeRight();
app.swipeLeft();
assert.equal(
day.scrollTop,
day.getDestinationScrollTop(new Date().getHours() - 1),
'scroll to the 8AM element'
);
});
});
suite('12/24 hour format', function() {
test('default format: 12 hour', function() {
assert.equal(day.sideBarHours[0].text(), '12 AM');
assert.equal(day.sideBarHours[13].text(), '1 PM');
assert.equal(day.sideBarHours[23].text(), '11 PM');
var now = new Date();
var minutes = now.getMinutes();
minutes = minutes < 10 ? (0 + String(minutes)) : minutes;
var hours = now.getHours();
var currentTime = (hours > 12 ? hours - 12 : hours ) + ':' + minutes;
assert.equal(day.currentTime.text(), currentTime);
});
test('switch to 24 hour format', function() {
app.switch24HourTimeFormat();
// Settings changes are async, so we might need waitFor() the
// UI components to update.
client.waitFor(function() {
return day.sideBarHours[0].text() === '0' &&
day.sideBarHours[13].text() === '13' &&
day.sideBarHours[23].text() === '23';
});
var now = new Date();
var currentTime = pad(now.getHours()) + ':' + pad(now.getMinutes());
assert.equal(day.currentTime.text(), currentTime);
});
});
test('double tap all day + toggle all day', function() {
day.waitForHourScrollEnd();
day.actions
.doubleTap(day.activeAllDays[0], 150, 30)
.perform();
var event = app.editEvent;
event.waitForDisplay();
assert.ok(event.allDay, 'is all day event');
var oldStart = event.startTime;
var oldEnd = event.endTime;
event.allDay = false;
event.title = 'Foo';
client.waitFor(function() {
return event.startTime !== oldStart && event.endTime !== oldEnd;
});
var now = new Date();
var start = new Date(now.getTime());
start.setHours(now.getHours() + 1, 0, 0, 0);
var end = new Date(start.getTime());
end.setHours(start.getHours() + 1, 0, 0, 0);
var isToday = start.toISOString().slice(0, 10) === event.startDate;
var expectedStart = isToday ? pad(start.getHours()) + ':00:00' : '08:00:00';
var expectedEnd = isToday ? pad(end.getHours()) + ':00:00' : '09:00:00';
assert.equal(event.startTime, expectedStart, 'startTime');
assert.equal(event.endTime, expectedEnd, 'endTime');
event.save();
day.waitForDisplay();
});
function pad(n) {
return n < 10 ? '0' + n : String(n);
}
});

View file

@ -0,0 +1,351 @@
'use strict';
var AdvancedSettings = require('./views/advanced_settings'),
CreateAccount = require('./views/create_account'),
Day = require('./views/day'),
EditEvent = require('./views/edit_event'),
Marionette = require('marionette-client'),
ModifyAccount = require('./views/modify_account'),
Month = require('./views/month'),
MonthDay = require('./views/month_day'),
ReadEvent = require('./views/read_event'),
Settings = require('./views/settings'),
Week = require('./views/week');
function Calendar(client) {
this.client = client.scope({ searchTimeout: 5000 });
this.actions = new Marionette.Actions(this.client);
// Initialize our view remotes.
this.advancedSettings = new AdvancedSettings(client);
this.createAccount = new CreateAccount(client);
this.day = new Day(client);
this.editEvent = new EditEvent(client);
this.modifyAccount = new ModifyAccount(client);
this.month = new Month(client);
this.monthDay = new MonthDay(client);
this.readEvent = new ReadEvent(client);
this.settings = new Settings(client);
this.week = new Week(client);
}
module.exports = Calendar;
Calendar.ORIGIN = 'app://calendar.gaiamobile.org';
Calendar.prototype = {
launch: function() {
var client = this.client;
client.apps.launch(Calendar.ORIGIN);
client.apps.switchToApp(Calendar.ORIGIN);
// Wait for the document body to know we're really 'launched'.
this.client.helper.waitForElement('body');
},
get addEventButton() {
return this.client.findElement('#time-header a[href="/event/add/"]');
},
get headerContent() {
return this.client.findElement('#current-month-year');
},
get timeHeader() {
return this.client.findElement('#time-header');
},
get themeColor() {
return this.client.findElement('meta[name="theme-color"]')
.getAttribute('content');
},
openModifyEventView: function() {
this.addEventButton.click();
this.editEvent.waitForDisplay();
return this;
},
openSettingsView: function() {
this._toggleSettingsView(true);
return this;
},
closeSettingsView: function() {
this._toggleSettingsView(false);
return this;
},
_toggleSettingsView: function(isOpen) {
var client = this.client;
client.helper
.waitForElement(this.timeHeader)
.tap(25, 25);
// Wait for the animation to be complete before trying to click on
// items in the drawer.
var drawer = this.client.findElement('#settings .settings-drawer');
this.client.waitFor(function() {
return drawer.getAttribute('data-animstate') === 'done';
});
if (!isOpen) {
var body = this.client.findElement('body');
// Also wait for the UI to be in a non-settings state after
// waiting for the animation to finish and the app UI to go
// back to non-settings state.
this.client.waitFor(function() {
return body.getAttribute('data-path') !== '/settings/';
});
}
return this;
},
openAdvancedSettingsView: function() {
this.openSettingsView();
this.settings.setupAdvancedSettings();
this.advancedSettings.waitForDisplay();
return this;
},
closeAdvancedSettingsView: function() {
var advancedSettings = this.advancedSettings;
advancedSettings.close();
advancedSettings.waitForHide();
},
openDayView: function() {
this.client
.findElement('#view-selector a[href="/day/"]')
.click();
this.day.waitForDisplay();
return this;
},
openMonthView: function() {
this.client
.findElement('#view-selector a[href="/month/"]')
.click();
this.month.waitForDisplay();
return this;
},
openWeekView: function() {
this.client
.findElement('#view-selector a[href="/week/"]')
.click();
this.week.waitForDisplay();
return this;
},
clickToday: function() {
this.client
.findElement('#view-selector a[href="#today"]')
.click();
return this;
},
setupAccount: function(options) {
this.openAdvancedSettingsView();
this.advancedSettings.createAccount();
this.createAccount.waitForDisplay();
this.createAccount.chooseAccountType(options.accountType);
var modifyAccount = this.modifyAccount;
modifyAccount.waitForDisplay();
[
'user',
'password',
'fullUrl'
].forEach(function(key) {
if (key in options) {
modifyAccount[key] = options[key];
}
});
modifyAccount.save();
modifyAccount.waitForHide();
this.closeSettingsView();
return this;
},
teardownAccount: function(calendarName, user) {
this.openAdvancedSettingsView();
this.advancedSettings.clickAccount(calendarName, user);
var modifyAccount = this.modifyAccount;
modifyAccount.waitForDisplay();
modifyAccount.delete();
modifyAccount.waitForHide();
this.closeAdvancedSettingsView();
this.closeSettingsView();
return this;
},
sync: function() {
this.openSettingsView();
this.settings.sync();
this.closeSettingsView();
return this;
},
/**
* Create an event.
*
* Options:
* (String) calendar - calendar name [defaults to "Offline calendar"].
* (String) title - event title.
* (String) location - event location.
* (Date) startDate - event start date.
* (Date) endDate - event end date.
* (Number) startHour - shortcut for creating an event that starts today.
* (Number) duration - length of event in hours.
* (Boolean) allDay - whether this is an all day event.
* (Array) reminders - array of strings like '5 minutes before'.
*/
createEvent: function(opts) {
var startDate;
if (opts.startDate) {
startDate = opts.startDate;
} else {
startDate = new Date();
// startHour can be zero!
if (opts.startHour != null) {
startDate.setHours(opts.startHour, 0, 0, 0);
}
}
var endDate;
if (opts.endDate) {
endDate = opts.endDate;
} else {
// 1h by default
var duration = opts.duration || 1;
endDate = new Date(startDate.getTime() + duration * 60 * 60 * 1000);
}
this.openModifyEventView();
var editEvent = this.editEvent;
editEvent.title = opts.title;
editEvent.location = opts.location || '';
editEvent.description = opts.description || '';
editEvent.calendar = opts.calendar || 'Offline calendar';
editEvent.startDate = startDate;
editEvent.endDate = endDate;
if (opts.allDay) {
editEvent.allDay = opts.allDay;
} else {
editEvent.startTime = startDate;
editEvent.endTime = endDate;
}
// no reminders by default to avoid triggering notifications by mistake.
// see: https://bugzil.la/1012507
editEvent.reminders = opts.reminders == null ? [] : opts.reminders;
editEvent.save();
editEvent.waitForHide();
return this;
},
/**
* Tests if content is bigger than container. If something is wrong it
* throws errors.
*/
checkOverflow: function(element, msg) {
msg = msg ? msg + ': ' : '';
var wid = element.scriptWith(function(el) {
return {
content: el.scrollWidth,
container: el.clientWidth
};
});
if (!wid.content) {
throw new Error(msg + 'invalid content width');
}
if (!wid.container) {
throw new Error(msg + 'invalid container width');
}
// we use a buffer of 1px to account for potential rounding issues
// see: Bug 959901
if (Math.abs(wid.content - wid.container) > 1) {
msg += 'content (' + wid.content + 'px) is wider than container (' +
wid.container + 'px)';
throw new Error(msg);
}
return this;
},
formatDate: function(date) {
var month = date.getMonth() + 1,
day = date.getDate(),
year = date.getFullYear();
if (month.toString().length === 1) {
month = '0' + month;
}
if (day.toString().length === 1) {
day = '0' + day;
}
return [month, day, year].join('/');
},
swipeLeft: function() {
return this._swipe({ direction: 'left' });
},
swipeRight: function() {
return this._swipe({ direction: 'right' });
},
/**
* Options:
* (String) direction is one of 'left', 'right'.
*/
_swipe: function(options) {
var bodySize = this.client.executeScript(function() {
return {
height: document.body.clientHeight,
width: document.body.clientWidth
};
});
// (x1, y1) is swipe start.
// (x2, y2) is swipe end.
var x1, x2, y1, y2;
y1 = y2 = bodySize.height * 0.5;
if (options.direction === 'left') {
x1 = bodySize.width * 0.8;
x2 = 0;
} else if (options.direction === 'right') {
x1 = bodySize.width * 0.2;
x2 = bodySize.width;
} else {
throw new Error('swipe needs a direction');
}
var body = this.client.findElement('body');
this.actions
.flick(body, x1, y1, x2, y2)
.perform();
return this;
},
switch12HourTimeFormat: function() {
this._switchTimeFormat(true);
},
switch24HourTimeFormat: function() {
this._switchTimeFormat(false);
},
_switchTimeFormat: function(is12Hour) {
var client = this.client;
// Switch to System frame to get the permission
// of writing settings values.
client.switchToFrame();
client.settings.set('locale.hour12', is12Hour);
client.apps.switchToApp(Calendar.ORIGIN);
}
};

View file

@ -0,0 +1,159 @@
'use strict';
var childProcess = require('child_process'),
fs = require('fs'),
path = require('path'),
os = require('os'),
ejs = require('ejs'),
emptyPort = require('empty-port');
var CAL_ICS_TMP_PATH = __dirname + '/template/calendar.ics.ejs',
CONFIG_TMP_PATH = __dirname + '/template/config.ejs',
START_PORT = 50000;
/**
* Radicale inherit CaldavServer.
* It is a kind of CaldavServer implemented by pure Python.
*
* We install it from the gaia-marionette shell script file
* during running the tests.
*/
function Radicale() {
// The instance of the child process.
this.childProcess = null;
// The events stored in the server.
this.events = [];
// Set up dav directories.
this.configFilePath = path.join(os.tmpdir(), 'config');
this.fileSystemPath = path.join(os.tmpdir(), 'collections');
}
Radicale.prototype = {
/**
* Server port.
* @type {Number}
*/
port: null,
/**
* The path to the config file.
* @type {String}
*/
configFilePath: null,
/**
* The path to the directory of the file system in server.
* @type {String}
*/
fileSystemPath: null,
/**
* Setup the config file for the Radicale server.
*/
_setup: function() {
// Setup the Radicale config file,
// and we create it at path /radicale/install/path/config in the disk.
var configTemplate,
config;
if (!fs.existsSync(this.fileSystemPath)) {
fs.mkdirSync(this.fileSystemPath);
}
configTemplate = fs.readFileSync(CONFIG_TMP_PATH, 'utf8'),
config = ejs.render(configTemplate, {
port: this.port,
filesystemPath: this.fileSystemPath
});
fs.writeFileSync(this.configFilePath, config);
},
/**
* Start the Radicale server with config file.
*
* @param {Object} options for the server.
* @param {Function} callback with port param execute
* after server is started.
*/
start: function(options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
if ('port' in options) {
this.port = options.port;
this._executeServer();
return callback && callback(null, options.port);
}
emptyPort({ startPort: START_PORT }, function(error, port) {
if (error) {
return callback && callback(error);
}
this.port = port;
this._executeServer();
return callback && callback(null, port);
}.bind(this));
},
/**
* Execute the server with terminal command.
*/
_executeServer: function() {
this._setup();
this.childProcess =
childProcess.spawn('radicale', ['--config', this.configFilePath]);
},
/**
* Shutdown the Radicale server.
*
* @param {Function} callback execute after server is closed.
*/
close: function(callback) {
this.childProcess.on('exit', function() {
if (callback && typeof(callback) === 'function') {
callback();
}
});
this.childProcess.kill();
},
/**
* Add events in the CalDAV server.
*
* @param {String} username The user you would like to add for.
* @param {Array|Object} event JSON objects, the structure is like
* { startDate, endDate, title, location, description }.
*/
addEvent: function(username, event) {
if (Array.isArray(event)) {
this.events = this.events.concat(event);
} else {
this.events.push(event);
}
var calendarIcsTemplate = fs.readFileSync(CAL_ICS_TMP_PATH, 'utf8'),
calendarIcs = ejs.render(calendarIcsTemplate, { events: this.events });
fs.writeFileSync(
path.join(this.fileSystemPath, username),
calendarIcs
);
},
/**
* Remove all events for all users.
*/
removeAllEvents: function() {
this.events = [];
fs.readdirSync(this.fileSystemPath).forEach(function(file) {
fs.unlinkSync(path.join(this.fileSystemPath, file));
}.bind(this));
}
};
module.exports = Radicale;

View file

@ -0,0 +1,13 @@
BEGIN:VCALENDAR
VERSION:2.0
<% for (var i = 0; i < events.length; i++) { %>
BEGIN:VEVENT
UID:<%= i %>
DTSTART:<%= events[i].startDate || '' %>
DTEND:<%= events[i].endDate || '' %>
SUMMARY:<%= events[i].title || '' %>
LOCATION:<%= events[i].location || '' %>
DESCRIPTION:<%= events[i].description || '' %>
END:VEVENT
<% } %>
END:VCALENDAR

View file

@ -0,0 +1,10 @@
[server]
# CalDAV server hostnames separated by a comma
hosts = 127.0.0.1:<%= port || 5232 %>
[storage]
# Storage backend
type = filesystem
# Folder for storing local collections, created if not present
filesystem_folder = <%= filesystemPath || '' %>

View file

@ -0,0 +1,58 @@
'use strict';
var View = require('./view');
function AdvancedSettings() {
View.apply(this, arguments);
}
module.exports = AdvancedSettings;
AdvancedSettings.prototype = {
__proto__: View.prototype,
selector: '#advanced-settings-view',
close: function() {
this
.findElement('a[href="/settings/"]')
.click();
},
createAccount: function() {
this
.findElement('[href="/select-preset/"]')
.click();
},
clickAccount: function(calendarName, user) {
var account;
this
.findElements('.account-list > li')
.some(function(element) {
var text = element.text();
if (text.indexOf(calendarName) !== -1 ||
(user && text.indexOf(user) !== -1)) {
account = element;
return true;
}
});
if (!account) {
throw new Error(
'Could not find account for user ' + user +
' or calendar ' + calendarName
);
}
account.click();
},
waitForHide: function() {
return this.client.waitFor(function() {
var zIndex = this
.getElement()
.cssProperty('z-index');
return zIndex === '-1';
}.bind(this));
}
};

View file

@ -0,0 +1,25 @@
'use strict';
var View = require('./view');
function CreateAccount() {
View.apply(this, arguments);
}
module.exports = CreateAccount;
CreateAccount.prototype = {
__proto__: View.prototype,
selector: '#create-account-view',
/**
* @param {String} accountType is one of
* 'google', 'yahoo', or 'caldav'.
*/
chooseAccountType: function(accountType) {
var selector = '.' + accountType;
this
.findElement(selector)
.click();
}
};

View file

@ -0,0 +1,25 @@
'use strict';
var WeekView = require('./week'),
DayEvent = require('./day_event');
function Day() {
WeekView.apply(this, arguments);
}
module.exports = Day;
Day.prototype = {
__proto__: WeekView.prototype,
selector: '#day-view',
get activeDay() {
return this.findElement('.md__day[aria-hidden="false"]');
},
get events() {
return this.activeDay.findElements('.md__event').map(function(el) {
return new DayEvent(this.client, el);
}, this);
}
};

View file

@ -0,0 +1,28 @@
'use strict';
var MarionetteElement = require('./marionette_element');
/**
* Represents a single Day Event
*/
function DayEvent() {
MarionetteElement.apply(this, arguments);
}
module.exports = DayEvent;
DayEvent.prototype = {
__proto__: MarionetteElement.prototype,
get title() {
return this.findElement('.md__event-title');
},
// Marionette.Element already have a method location()
get address() {
return this.findElement('.md__event-location');
},
get iconAlarm() {
return this.findElement('.icon-calendar-alarm');
}
};

View file

@ -0,0 +1,159 @@
'use strict';
var View = require('./view');
function EditEvent() {
View.apply(this, arguments);
}
module.exports = EditEvent;
EditEvent.prototype = {
__proto__: View.prototype,
selector: '#modify-event-view',
waitForDisplay: function() {
// event details are loaded asynchronously, so we need to wait until the
// "loading" class is removed from the view; this will avoid intermittent
// failures on Travis (caused by race condition)
var element = this.getElement();
var selectIsReady = function() {
// the calendar list is also loaded asynchronously from the database, so
// we need to wait until it's ready as well
var select = element.findElement('select[name="calendarId"]');
return select.getAttribute('className').indexOf('loading') === -1;
};
this.client.waitFor(function() {
return element.displayed() &&
element.getAttribute('className').indexOf('loading') === -1 &&
selectIsReady();
});
},
get form() {
return this.findElement('form');
},
get errors() {
return this.form.findElement('.errors').text();
},
set title(value) {
this.setFormValue('title', value);
},
set description(value) {
this.setFormValue('description', value);
},
set location(value) {
this.setFormValue('location', value);
},
set calendar(value) {
var select = this.findElement('select[name="calendarId"]');
this.client.helper.tapSelectOption(select, value);
},
set startDate(value) {
this.setFormValue('startDate', value);
},
get startDate() {
return this.getFormValue('startDate');
},
set startTime(value) {
this.setFormValue('startTime', value);
},
get startTime() {
return this.getFormValue('startTime');
},
get startTimeLocale() {
return this.findElement('#start-time-locale');
},
set endDate(value) {
this.setFormValue('endDate', value);
},
get endDate() {
return this.getFormValue('endDate');
},
get endTimeLocale() {
return this.findElement('#end-time-locale');
},
set endTime(value) {
this.setFormValue('endTime', value);
},
get endTime() {
return this.getFormValue('endTime');
},
get allDayCheckbox() {
return this.findElement('gaia-switch[name="allday"]');
},
get allDay() {
return this.allDayCheckbox.getAttribute('checked');
},
set allDay(value) {
if (value !== this.allDay) {
this.allDayCheckbox.click();
}
},
set reminders(values) {
if (!values || !values.length) {
values = ['None'];
}
values.forEach(this.setReminderValue, this);
},
setReminderValue: function(value, index) {
// maximum amount of reminders is 5
var nth = Math.min(index, 4) + 1;
// we are using this complex selector, instead of querying for all the
// available <select> elements, because client.helper.tapSelectOption
// will wait for element to be displayed before trying to click on it; that
// way we avoid potential race conditions
var el = this.findElement('.alarms > *:nth-child(' + nth + ') select');
this.client.helper.tapSelectOption(el, value);
},
get reminders() {
return this.findElements('[name="alarm[]"]').map(function(select) {
return select.scriptWith(function(el) {
return el.options[el.options.selectedIndex].textContent.trim();
});
});
},
cancel: function() {
this
.findElement('.cancel')
.click();
},
delete: function() {
this
.findElement('.delete-record')
.click();
},
get saveButton() {
return this.findElement('.save');
},
save: function() {
this.saveButton.click();
}
};

View file

@ -0,0 +1,22 @@
'use strict';
var Marionette = require('marionette-client');
/**
* This base class is used to inherit all methods from Marionette.Element, this
* will allow us to call methods like `text()`, `click()`, etc in the View
* context.
*/
function MarionetteElement(client, el) {
this.client = client;
this.element = el;
}
module.exports = MarionetteElement;
// Mix-in methods from Marionette.Element.prototype
Object.keys(Marionette.Element.prototype).forEach(function(funcName) {
var elementFn = Marionette.Element.prototype[funcName];
MarionetteElement.prototype[funcName] = function() {
return elementFn.apply(this.element, arguments);
};
});

View file

@ -0,0 +1,65 @@
'use strict';
var View = require('./view');
function ModifyAccount() {
View.apply(this, arguments);
}
module.exports = ModifyAccount;
ModifyAccount.prototype = {
__proto__: View.prototype,
selector: '#modify-account-view',
get form() {
return this.findElement('form');
},
set user(value) {
this.setFormValue('user', value);
},
set password(value) {
this.setFormValue('password', value);
},
set fullUrl(value) {
this.setFormValue('fullUrl', value);
},
save: function() {
this
.findElement('.save')
.click();
},
delete: function() {
this
.findElement('.delete-record')
.click();
this.client.waitFor(function() {
var confirm = this.findElement('.delete-confirm');
if (!confirm.displayed()) {
return false;
}
confirm.click();
return true;
}.bind(this));
},
/**
* Workaround to wait for the modify account view to disappear.
* Remove once https://bugzil.la/995563 is fixed.
*/
waitForHide: function() {
return this.client.waitFor(function() {
var zIndex = this
.getElement()
.cssProperty('z-index');
return zIndex === '-1';
}.bind(this));
}
};

View file

@ -0,0 +1,50 @@
'use strict';
var View = require('./view');
function Month() {
View.apply(this, arguments);
}
module.exports = Month;
Month.prototype = {
__proto__: View.prototype,
selector: '#month-view',
get activeMonth() {
return this.findElement('.month.active');
},
get currentDay() {
return this.activeMonth.findElement('.present > .day');
},
get busyDots() {
return this.activeMonth.findElements('.icon-calendar-dot');
},
get days() {
return this.activeMonth.findElements('.day');
},
get weekdayHeaders() {
return this
.findElements('.month.active #month-days li')
.map(function(li) {
return li.text();
});
},
get daySquares() {
return this.findElements('.month.active > ol li');
},
get todaySquare() {
return this.activeMonth.findElement('.present');
},
squareDots: function(square) {
return square.findElements('.icon-calendar-dot');
}
};

View file

@ -0,0 +1,47 @@
'use strict';
var MonthDayEvent = require('./month_day_event'),
View = require('./view');
function MonthDay() {
View.apply(this, arguments);
}
module.exports = MonthDay;
MonthDay.prototype = {
__proto__: View.prototype,
selector: '#month-day-agenda',
get container() {
return this.findElement('#event-list');
},
get events() {
return this.findElements('.event').map(function(el) {
return new MonthDayEvent(this.client, el);
}, this);
},
get date() {
return this
.findElement('#event-list-date')
.text();
},
/**
* If event is not specified, we'll use today's first event.
*/
scrollToEvent: function(event) {
if (!event) {
event = this.events[0];
} else if (typeof event === 'number') {
event = this.events[event];
}
event.scriptWith(function(element) {
var container = document.getElementById('month-day-agenda');
container.scrollTop = element.offsetTop;
});
}
};

View file

@ -0,0 +1,60 @@
'use strict';
var MarionetteElement = require('./marionette_element');
/**
* Represents a single Day Event
*/
function MonthDayEvent() {
MarionetteElement.apply(this, arguments);
}
module.exports = MonthDayEvent;
MonthDayEvent.prototype = {
__proto__: MarionetteElement.prototype,
get container() {
return this.findElement('.container');
},
get title() {
return this
.findElement('h5')
.text();
},
// Marionette.Element already have a method location()
get address() {
return this
.findElement('.location')
.text();
},
get allDay() {
return this
.findElement('.all-day')
.text();
},
get startTime() {
return this
.findElement('.start-time')
.text();
},
get endTime() {
return this
.findElement('.end-time')
.text();
},
get iconAlarm() {
return this.findElement('.icon-calendar-alarm');
},
hasAlarms: function() {
return this.element.scriptWith(function(element) {
return element.classList.contains('has-alarms');
});
}
};

View file

@ -0,0 +1,103 @@
'use strict';
var View = require('./view');
function ReadEvent() {
View.apply(this, arguments);
}
module.exports = ReadEvent;
ReadEvent.prototype = {
__proto__: View.prototype,
selector: '#event-view',
waitForDisplay: function() {
// event details are loaded asynchronously, so we need to wait until the
// "loading" class is removed from the view; this will avoid intermittent
// failures on Travis (caused by race condition)
var element = this.getElement();
this.client.waitFor(function(){
return element.displayed() &&
element.getAttribute('className').indexOf('loading') === -1;
});
},
get alarms() {
return this
.findElements('.alarms > .content > div')
.map(function(element) {
return element.text();
});
},
get calendarColor() {
return this
.findElement('.current-calendar > .icon-calendar-dot')
.cssProperty('color');
},
get calendar() {
return this
.findElement('.current-calendar > .content')
.text();
},
get description() {
return this
.findElement('.description > .content')
.text();
},
get descriptionContainer() {
return this.findElement('.description');
},
get durationTime() {
return this
.findElement('.duration-time > .content')
.text();
},
get location() {
return this
.findElement('.location > .content')
.text();
},
get locationContainer() {
return this.findElement('.location');
},
get reminders() {
return this.findElements('.alarms > .content > div');
},
get title() {
return this
.findElement('.title .content')
.text();
},
get titleContainer() {
return this.findElement('.title');
},
get editable() {
return this
.findElement('.edit')
.enabled();
},
cancel: function() {
return this
.findElement('.cancel')
.click();
},
edit: function() {
return this
.findElement('.edit')
.click();
}
};

View file

@ -0,0 +1,44 @@
'use strict';
var View = require('./view');
function Settings() {
View.apply(this, arguments);
}
module.exports = Settings;
Settings.prototype = {
__proto__: View.prototype,
selector: '#settings',
setupAdvancedSettings: function() {
this.client
.findElement('li.settings')
.click();
},
sync: function() {
this
.findElement('[role="tablist"] .sync')
.click();
},
calendars: function() {
return this
.findElements('.calendars .name')
.map(function(element) {
return element.text();
});
},
toggleCalendar: function(name) {
name = name || 'Offline calendar';
this
.findElements('gaia-checkbox label')
.filter(function(element) {
return element.text() === name;
})[0]
.click();
}
};

View file

@ -0,0 +1,83 @@
'use strict';
var Marionette = require('marionette-client');
function View(client) {
this.client = client.scope({ searchTimeout: 5000 });
this.actions = new Marionette.Actions(client);
}
module.exports = View;
View.prototype = {
/**
* Namespaces a Marionette.Client#findElement call into this view.
*/
findElement: function(selector) {
return this
.getElement()
.findElement(selector);
},
findElements: function(selector) {
return this
.getElement()
.findElements(selector);
},
/**
* Grab the root element associated with this view.
*/
getElement: function() {
return this.client.findElement(this.selector);
},
get element() {
return this.getElement();
},
/**
* Whether or not the root element associated with
* this view is displayed.
*/
displayed: function() {
return this.getElement().displayed();
},
waitForDisplay: function() {
return this.client.waitFor(this.displayed.bind(this));
},
waitForHide: function() {
return this.client.waitFor(function() {
return !this.displayed();
}.bind(this));
},
/* Form helpers */
setFormValue: function(name, value) {
var formData = {};
formData[name] = value;
this.setFormData(formData);
// Send input event to trigger the listener method.
// e.g. Set the start date to trigger doing auto change end date.
this.form.findElement('[name="' + name + '"]')
.scriptWith(function(el) {
el.dispatchEvent(new Event('input'));
el.dispatchEvent(new Event('change'));
});
this.form.scriptWith(function(el) {
el.dispatchEvent(new Event('input'));
el.dispatchEvent(new Event('change'));
});
},
setFormData: function(formData) {
this.client.forms.fill(this.form, formData);
},
getFormValue: function(name) {
return this.findElement('[name="' + name + '"]').getAttribute('value');
}
};

View file

@ -0,0 +1,268 @@
'use strict';
var View = require('./view');
function Week() {
View.apply(this, arguments);
}
module.exports = Week;
Week.prototype = {
__proto__: View.prototype,
selector: '#week-view',
get sideBarHours() {
return this.findElements('.md__sidebar .md__hour .md__display-hour');
},
get events() {
return this.findElements('.md__event');
},
get addEventButton() {
return this.findElement('.md__add-event');
},
get todayDates() {
return this.findElements('.md__sticky .md__allday h1.is-today');
},
get dayNames() {
return this.findElements('.md__day-name').map(function(el) {
return el.text();
});
},
get daysHolder() {
return this.findElement('.md__days');
},
get days() {
return this.findElements('.md__day');
},
get hours() {
return this.findElements('.md__hour');
},
get currentTime() {
return this.findElement('.md__current-time');
},
get currentHour() {
var now = new Date();
return this.findElement('.md__hour-'+ now.getHours());
},
get currentDisplayHour() {
return this.currentHour.findElement('.md__display-hour');
},
get main() {
return this.findElement('.md__main');
},
get allDayIcon() {
return this.findElement('.md__all-day');
},
get activeAllDays() {
return this.findElements('.md__allday[aria-hidden="false"]');
},
get allDaysHolder() {
return this.findElement('.md__alldays');
},
get scrollTop() {
return this.main.scriptWith(function(el) {
return el.scrollTop;
});
},
/**
* This method tries to find out where the hour square is located in the
* window.
*
* @param {Number} hour The hour to locate.
* @returns {Number} 0 if it's displayed, or the delta otherwise: negative
* if it's above or positive if it's below.
*/
_getDeltaToFullyDisplayHour: function(hour) {
var hourElt = this.hours[hour];
var mainScroll = this.scrollTop;
var hourOffset = hourElt.scriptWith(function(el) { return el.offsetTop; });
var mainClientHeight = this.main.rect().height;
var hourHeight = hourElt.rect().height;
if (hourOffset < mainScroll) {
return hourOffset - mainScroll;
}
var bottomHourOffset = hourOffset + hourHeight;
var bottomScroll = mainScroll + mainClientHeight;
if (bottomHourOffset > bottomScroll) {
return bottomHourOffset - bottomScroll;
}
return 0;
},
/**
* This method waits until we don't scroll.
*
* Note this doesn't seem to work properly when the view is just displayed.
* That's why waitForHourScrollEnd is still useful.
*/
waitForNoScroll: function() {
var prevScrollTop = null;
this.client.waitFor(function() {
var currentScrollTop = this.scrollTop;
if (prevScrollTop !== null && prevScrollTop === currentScrollTop) {
return true;
}
prevScrollTop = currentScrollTop;
return false;
}.bind(this));
},
/**
* This method uses just the right amount of flicking to scroll page by page
* until finding the right hour line.
*
* @param {Number} hour The hour to scroll to.
*/
scrollToHour: function(hour) {
var mainRect = this.main.rect();
var middleOfMain = mainRect.width / 2;
this.client.waitFor(function() {
this.waitForNoScroll();
var delta = this._getDeltaToFullyDisplayHour(hour);
if (delta === 0) {
return true;
}
var moveDelta = mainRect.height / 4;
var beginPosition = 10;
// element is hidden above the visible part
// we need to move it up
if (delta < 0) {
this.actions.flick(
this.main,
middleOfMain, beginPosition, middleOfMain, beginPosition + moveDelta
).perform();
}
// element is hidden below the visible part.
// we need to move it down
if (delta > 0) {
this.actions.flick(
this.main,
middleOfMain, beginPosition + moveDelta, middleOfMain, beginPosition
).perform();
}
return false;
}.bind(this));
},
/**
* Tap the right square. This might throw if the square is not visible, so you
* should use scrollToHour before calling this method.
*
* @param {Object} options
* @param {Number} options.day The day to tap. This needs to be a visible day.
* @param {Number} options.hour The hour to tap. It needs to be visible as
* well.
* @param {Number} [options.left] If present, it allows to tap at a specific
* location in the square. It can be negative to tap at the left of the
* button. Only one of `left`, `right` can be specified.
* @param {Number} [options.right] If present, it allows to tap at a specific
* location in the square. It can be negative to tap at the right of the
* button.
* @param {Number} [options.top] If present, it allows to tap at a specific
* location in the square. It can be negative to tap at the top of the
* button. Only one of `top`, `bottom` can be spcified.
* @param {Number} [options.bottom] If present, it allows to tap at a specific
* location in the square. It can be negative to tap at the bottom of the
* button.
*/
tapDayHour: function(options) {
var day = options.day;
var hour = options.hour;
var left = options.left;
var right = options.right;
var bottom = options.bottom;
var top = options.top;
var dayElt = this.days[day];
var hourElt = this.hours[hour];
var dayEltRect = dayElt.rect();
var hourEltRect = hourElt.rect();
if (left === undefined && right === undefined) {
left = dayEltRect.width / 2;
}
if (left === undefined) {
left = dayEltRect.width - right;
}
if (top === undefined && bottom === undefined) {
top = hourEltRect.height / 2;
}
if (top === undefined) {
top = hourEltRect.height - bottom;
}
top += hourEltRect.height * hour;
dayElt.tap(left, top);
},
isShowingAddEventLink: function() {
return this.element.scriptWith(function(el) {
return !!el.querySelector('.md__add-event');
});
},
waitForHourScrollEnd: function(hour) {
// if displaying current day it scrolls to current time, if not it scrolls
// to 8AM; it should also scroll to the created event, that's why we allow
// overriding the `hour`.
if (hour == null) {
hour = this.todayDates.length ?
Math.max(new Date().getHours() - 1, 0) :
8;
}
var expected = this.getDestinationScrollTop(hour);
this._waitForScrollEnd(expected);
},
_waitForScrollEnd: function(expected) {
this.client.waitFor(function() {
return this.scrollTop === expected;
}.bind(this));
this.client.waitFor(function() {
return this.main.cssProperty('overflowY') !== 'hidden';
}.bind(this));
},
getDestinationScrollTop: function(hour) {
hour = Math.max(hour, 0);
var bottomScrollTop = this.main.scriptWith(function(el) {
return el.scrollHeight - el.clientHeight;
});
var hourOffsetTop = this.hours[hour].scriptWith(function(el) {
return el.offsetTop;
});
return Math.min(hourOffsetTop, bottomScrollTop);
},
scrollToTop: function() {
this.scrollToHour(0);
}
};

View file

@ -0,0 +1,145 @@
'use strict';
var Calendar = require('./lib/calendar'),
assert = require('chai').assert;
marionette('modify event view', function() {
var app;
var client = marionette.client({
profile: {
prefs: {
// we need to disable the keyboard to avoid intermittent failures on
// Travis (transitions might take longer to run and block UI)
'dom.mozInputMethod.enabled': false,
// Do not require the B2G-desktop app window to have focus (as per the
// system window manager) in order for it to do focus-related things.
'focusmanager.testmode': true
}
},
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var editEvent;
setup(function() {
app = new Calendar(client);
editEvent = app.editEvent;
app.launch();
app.openModifyEventView();
// we need a title or location otherwise event can't be saved
editEvent.title = 'Reminder Test';
});
suite('reminders', function() {
test('default', function() {
assert.deepEqual(editEvent.reminders, ['5 minutes before', 'None']);
});
test('default allday', function() {
editEvent.allDay = true;
client.waitFor(function() {
return editEvent.reminders[0] !== '5 minutes before';
});
assert.deepEqual(editEvent.reminders, ['On day of event', 'None']);
});
test('set to none', function() {
editEvent.setReminderValue('None', 0);
client.waitFor(function() {
return editEvent.reminders.length === 1;
});
assert.deepEqual(editEvent.reminders, ['None']);
});
test('multiple reminders', function() {
var reminders = [
'At time of event',
'5 minutes before',
'15 minutes before',
'30 minutes before',
'1 hour before',
'2 hours before',
'1 day before'
];
editEvent.reminders = reminders;
assert.deepEqual(
editEvent.reminders,
// maximum amount of reminders is 5, so it will override the last one
reminders.slice(0, 4).concat(['1 day before'])
);
});
test('duplicates', function() {
// it should only save a single reminder if user adds duplicates
editEvent.reminders = [
'15 minutes before',
'15 minutes before',
'15 minutes before',
'15 minutes before'
];
editEvent.save();
app.monthDay.waitForDisplay();
app.monthDay.events[0].click();
app.readEvent.waitForDisplay();
app.readEvent.edit();
editEvent.waitForDisplay();
assert.deepEqual(editEvent.reminders, ['15 minutes before', 'None']);
});
});
suite('auto change end date and time', function() {
var startDate = new Date('Sep 08 2014 12:34:00');
var startDatePreviousHour = new Date(startDate);
var startDatePreviousDay = new Date(startDate);
var startDateNextHour = new Date(startDate);
var startDateNextDay = new Date(startDate);
startDatePreviousHour.setHours(startDate.getHours() - 1);
startDatePreviousDay.setDate(startDate.getDate() - 1);
startDateNextHour.setHours(startDate.getHours() + 1);
startDateNextDay.setDate(startDate.getDate() + 1);
setup(function() {
editEvent.startDate = startDate;
editEvent.startTime = startDate;
editEvent.endDate = startDateNextDay;
editEvent.endTime = startDateNextHour;
});
test('end date should auto change to previous day', function() {
editEvent.startDate = startDatePreviousDay;
assert.equal(editEvent.endDate, '2014-09-08', 'change to previous day');
});
test('end time should auto change to previous hour', function() {
editEvent.startTime = startDatePreviousHour;
assert.equal(editEvent.endTime, '12:34:00', 'change to previous hour');
});
test('start datetime could not be after end datetime', function() {
editEvent.endDate = startDatePreviousDay;
assert.equal(
editEvent.errors,
'The event cannot end before its start date',
'show the correct error message'
);
});
suite('12/24 hour format', function() {
test('default format: 12 hour', function() {
app.switch12HourTimeFormat();
assert.equal(editEvent.startTimeLocale.text(), '12:34 PM',
'check start time locale');
assert.equal(editEvent.endTimeLocale.text(), '1:34 PM',
'check end time locale');
});
test('switch to 24 hour format', function() {
app.switch24HourTimeFormat();
assert.equal(editEvent.startTimeLocale.text(), '12:34',
'check start time locale');
assert.equal(editEvent.endTimeLocale.text(), '13:34',
'check end time locale');
});
});
});
});

View file

@ -0,0 +1,240 @@
'use strict';
var Calendar = require('./lib/calendar'),
assert = require('chai').assert;
marionette('month view', function() {
var app;
var client = marionette.client({
profile: {
prefs: {
// we need to disable the keyboard to avoid intermittent failures on
// Travis (transitions might take longer to run and block UI)
'dom.mozInputMethod.enabled': false,
// Do not require the B2G-desktop app window to have focus (as per the
// system window manager) in order for it to do focus-related things.
'focusmanager.testmode': true,
}
}
});
setup(function() {
app = new Calendar(client);
app.launch();
});
test('#month-day-agenda scroll', function() {
// Create a lot of events so we can scroll so fun!
for (var i = 0; i < 5; i++) {
app.createEvent({
title: 'Launch the Hypermatter Reactor',
location: 'Alderaan',
startHour: i
});
}
app.openMonthView();
var monthDay = app.monthDay.getElement();
var container = app.monthDay.container;
assert.equal(
monthDay.getAttribute('scrollTop'),
0,
'scroll should start at zero'
);
var pos = container.location();
var x = pos.x + 30;
var body = client.findElement('body');
// fast vertical swipe, needs to happen on the body since we want
// coordinates to be absolute
app.actions
.flick(body, x, pos.y + 10, x, 50)
.perform();
// this will timeout if scroll did not change
client.waitFor(function() {
return monthDay.getAttribute('scrollTop') > 0;
});
});
test('week day headers should only have one letter', function() {
var month = app.month;
var weekdayHeaders = month.weekdayHeaders;
assert.deepEqual(weekdayHeaders, ['S', 'M', 'T', 'W', 'T', 'F', 'S']);
});
test('day number text should be centered', function() {
var month = app.month;
var daySquares = month.daySquares;
var textAlign = daySquares[0].scriptWith(function(element) {
return window.getComputedStyle(element).textAlign;
});
assert.equal(textAlign, 'center');
});
test('should gray out numbers outside current month', function() {
function isOutsideMonthWithGrayText(square) {
var gray = square.scriptWith(function(element) {
return window.getComputedStyle(element).color;
}) === 'rgb(189, 189, 189)';
var otherMonth = square
.getAttribute('className')
.indexOf('other-month') !== -1;
return gray && otherMonth;
}
// Either the first or last day chronologically must be
// outside of the current month (except for in February!).
// Swipe to next month if it's February.
var monthDay = app.monthDay;
var date = monthDay.date;
date = date.toLowerCase();
if (date.indexOf('feb') !== -1) {
// Swipe to another month.
app.swipeLeft();
}
var month = app.month;
var daySquares = month.daySquares;
assert.isTrue(daySquares.some(isOutsideMonthWithGrayText));
});
test('should not change background outside current month', function() {
function isOutsideMonthWithNonWhiteBackground(square) {
var nonwhite = square.scriptWith(function(element) {
return window.getComputedStyle(element).backgroundColor;
}) !== 'rgb(255, 255, 255)';
var otherMonth = square
.getAttribute('className')
.indexOf('other-month') !== -1;
return nonwhite && otherMonth;
}
var month = app.month;
var daySquares = month.daySquares;
assert.isFalse(daySquares.some(isOutsideMonthWithNonWhiteBackground));
});
test('should show event dots', function() {
function createEvent(startHour) {
app.createEvent({
title: 'Launch the Hypermatter Reactor',
location: 'Alderaan',
startHour: startHour
});
}
var month = app.month;
var todaySquare = month.todaySquare;
// 1 dot.
createEvent(1 /* startHour */);
assert.lengthOf(month.squareDots(todaySquare), 1);
// 2 dots.
createEvent(2 /* startHour */);
assert.lengthOf(month.squareDots(todaySquare), 2);
// 3 dots.
createEvent(3 /* startHour */);
assert.lengthOf(month.squareDots(todaySquare), 3);
// More dots?
createEvent(4 /* startHour */);
assert.lengthOf(month.squareDots(todaySquare), 3);
});
test('should have today date in day events section', function() {
var today = new Date();
var expected =
dayName(today.getDay()).toUpperCase() + ', ' +
monthName(today.getMonth()).toUpperCase().substring(0, 3) + ' ' +
today.getDate();
var monthDay = app.monthDay;
var actual = monthDay.date;
assert.equal(actual, expected);
});
test('today event', function() {
app.createEvent({
title: 'Launch the Hypermatter Reactor',
location: 'Alderaan',
startHour: 1
});
app.openMonthView();
var monthDay = app.monthDay;
var events = monthDay.events;
var event = events[0];
var text = event.text();
assert.include(text, '1:00 AM', 'should show start time');
assert.include(text, 'Launch', 'should show title');
assert.include(event.text(), 'Alderaan', 'should show location');
});
test('double tap', function() {
var month = app.month;
month.actions.doubleTap(month.currentDay).perform();
var editEvent = app.editEvent;
editEvent.waitForDisplay();
var now = new Date();
var start = new Date(now.getTime());
start.setHours(now.getHours() + 1, 0, 0, 0);
var end = new Date(start.getTime());
end.setHours(start.getHours() + 1, 0, 0, 0);
assert.equal(editEvent.startDate, toIso(start), 'startDate');
assert.equal(editEvent.endDate, toIso(end), 'endDate');
assert.equal(editEvent.startTime, toTime(start), 'startTime');
assert.equal(editEvent.endTime, toTime(end), 'endTime');
});
});
function toIso(date) {
return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' +
pad(date.getDate());
}
function pad(num) {
return num > 9 ? num : '0' + num;
}
function toTime(date) {
return pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' +
pad(date.getSeconds());
}
function dayName(num) {
return [
'Sunday',
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday'
][num];
}
function monthName(num) {
return [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
][num];
}

View file

@ -0,0 +1,241 @@
/* global require, marionette, setup, suite, teardown, test */
'use strict';
var Calendar = require('./lib/calendar'),
Radicale = require('./lib/radicale'),
assert = require('chai').assert,
debug = require('common/debug')('marionette:server_test');
var calendarName = 'firefox-os';
marionette('interop basic', function() {
var app, server;
var client = marionette.client({
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var vanillaEvent = Object.freeze({
title: 'Vanilla event',
location: 'Baskin Robbins',
description: 'What better a place for a vanilla event!',
calendar: calendarName,
startHour: 17,
reminders: []
});
var allDayEvent = Object.freeze({
title: 'All day event',
location: 'Planet Earth',
description: 'Except for the mars rover!',
calendar: calendarName,
allDay: true,
reminders: []
});
var eventWithAlarms = Object.freeze({
title: 'Event with alarms',
location: 'Fire Station',
description: 'Wee-Woo-Wee-Woo-Wee-Woo',
calendar: calendarName,
startHour: 10,
reminders: ['5 minutes before', '1 hour before']
});
var vanillaElement, allDayElement, elementWithAlarms;
function onServerUp(callback) {
debug('Launch calendar.');
app = new Calendar(client);
app.launch();
app.setupAccount({
accountType: 'caldav',
user: calendarName,
fullUrl: 'http://127.0.0.1:' + server.port + '/' + calendarName
});
app.monthDay.events.forEach(function(event) {
switch (event.title.trim()) {
case vanillaEvent.title:
vanillaElement = event;
break;
case allDayEvent.title:
allDayElement = event;
break;
case eventWithAlarms.title:
elementWithAlarms = event;
break;
default:
break;
}
});
callback();
}
suite('server', function() {
setup(function(done) {
// Start server if it's not already up.
if (server) {
return onServerUp(done);
}
server = new Radicale();
server.start(function() {
onServerUp(done);
});
});
teardown(function() {
debug('Delete account from calendar app.');
app.teardownAccount(calendarName);
// Now make sure that the account was actually deleted (see bug 1036753)
var synced = getCalendars(app);
assert.notInclude(synced, calendarName, 'calendar should be deleted');
});
/**
* Major hack around suiteSetup not being fully supported
* in marionette-js-runner.
*/
test('suiteSetup', function() {
debug('Create test data.');
[
vanillaEvent,
allDayEvent,
eventWithAlarms
].forEach(function(event) {
app.createEvent(event);
});
debug('Sync created data.');
app.sync();
});
test('should get calendar', function() {
var synced = getCalendars(app);
assert.include(synced, calendarName, calendarName + ' should be synced');
});
test('should get existing events', function() {
var events = app.monthDay.events;
[
vanillaEvent,
allDayEvent,
eventWithAlarms
].forEach(function(created) {
debug('Looking for event titled ' + created.title + '.');
assert.ok(
events.some(function(event) {
app.monthDay.scrollToEvent(event);
debug('Found ' + event.title.trim());
return event.title.trim() === created.title;
})
);
});
});
// TODO(gareth): Fix me!
test.skip('event start times', function() {
assert.strictEqual(
vanillaElement.closestHour,
vanillaEvent.startHour
);
assert.strictEqual(
elementWithAlarms.closestHour,
eventWithAlarms.startHour
);
// Make sure that we are inside an all day element.
assert.ok(
!!allDayElement.closestAllDay,
'should recognize all day events'
);
});
// TODO(gareth): Fix me!
test.skip('event descriptions', function() {
[
{ element: vanillaElement, event: vanillaEvent },
{ element: allDayElement, event: allDayEvent },
{ element: elementWithAlarms, event: eventWithAlarms }
].forEach(function(obj) {
var element = obj.element,
event = obj.event;
app.monthDay.scrollToEvent(element);
element.click();
app.readEvent.waitForDisplay();
assert.strictEqual(
app.readEvent.description.trim(),
event.description
);
app.readEvent.cancel();
app.month.waitForDisplay();
});
});
test('event locations', function() {
assert.strictEqual(
vanillaElement.address.trim(),
vanillaEvent.location
);
assert.strictEqual(
allDayElement.address.trim(),
allDayEvent.location
);
assert.strictEqual(
elementWithAlarms.address.trim(),
eventWithAlarms.location
);
});
test('event alarms', function() {
assert.isFalse(vanillaElement.hasAlarms());
assert.isFalse(allDayElement.hasAlarms());
assert.isTrue(elementWithAlarms.hasAlarms());
});
test('suiteTeardown', function(done) {
// TODO(gareth): Should be able to delete test data
// from within calendar app.
debug('Delete test data.');
/*
var events = app.monthDay.events;
events.forEach(function(event) {
app.monthDay.scrollToEvent(event);
event.click();
app.readEvent.waitForDisplay();
if (app.readEvent.editable) {
app.readEvent.edit();
app.editEvent.waitForDisplay();
app.editEvent.delete();
} else {
app.readEvent.cancel();
}
app.monthDay.waitForDisplay();
});
debug('Sync deletions.');
app.sync();
*/
try {
server.removeAllEvents();
} catch (error) {
console.error(error.toString());
} finally {
server.close(done);
}
});
});
});
function getCalendars(app) {
debug('Reading list of calendars from settings view.');
app.openSettingsView();
var synced = app.settings.calendars();
app.closeSettingsView();
return synced;
}

View file

@ -0,0 +1,32 @@
'use strict';
var Calendar = require('./lib/calendar'),
assert = require('chai').assert;
marionette('settings', function() {
var app;
var client = marionette.client({
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
setup(function() {
app = new Calendar(client);
app.launch();
});
test('theme color', function() {
assert.equal(app.themeColor, 'var(--header-background)', 'index');
app.openSettingsView();
assert.equal(app.themeColor, 'var(--header-background)', 'settings');
app.closeSettingsView();
app.openDayView();
assert.equal(app.themeColor, 'var(--header-background)', 'day view');
app.openAdvancedSettingsView();
assert.equal(app.themeColor, '#eeeeee', 'advanced settings');
app.closeAdvancedSettingsView();
assert.equal(app.themeColor, 'var(--header-background)', 'settings #2');
app.closeSettingsView();
assert.equal(app.themeColor, 'var(--header-background)', 'day view #2');
});
});

View file

@ -0,0 +1,79 @@
'use strict';
var Calendar = require('./lib/calendar'),
assert = require('chai').assert,
moment = require('moment');
var DATE_FORMAT = 'YYYY-MM-DD';
marionette('today', function() {
var app;
var client = marionette.client({
profile: {
prefs: {
// we need to disable the keyboard to avoid intermittent failures on
// Travis (transitions might take longer to run and block UI)
'dom.mozInputMethod.enabled': false,
// Do not require the B2G-desktop app window to have focus (as per the
// system window manager) in order for it to do focus-related things.
'focusmanager.testmode': true,
}
}
});
setup(function() {
app = new Calendar(client);
app.launch();
});
var scenarios = [
{
name: 'should show in header, highlighted day',
setup: function() {
// In the first scenario we do nothing
// and expect for the current date to be
// reflected in the month view.
}
},
{
name: 'should be show in header, highlighted day after tap today',
setup: function() {
// In the second scenario, we swipe to the next month
// and then click on the today button. We also
// expect for the current date to be reflected in the month view.
app
.swipeLeft()
.clickToday();
}
}
];
scenarios.forEach(function(scenario) {
test(scenario.name, function() {
var expectedDate = new Date();
scenario.setup();
// Check to make sure the "present" day matches today's day.
var day = app.month.currentDay;
var currentDate = day.text();
// Check to make sure the month feeding the date header is correct.
var date = app
.headerContent
.getAttribute('data-date');
assert.equal(currentDate, expectedDate.getDate());
assert.equal(
moment(date).format(DATE_FORMAT),
moment(expectedDate).format(DATE_FORMAT)
);
});
});
test('should show correct date in the today icon', function() {
var todayDate = client.findElement(
'#view-selector a[href="#today"] .icon-calendar-today'
);
assert.equal(todayDate.text(), new Date().getDate());
});
});

View file

@ -0,0 +1,111 @@
'use strict';
var Calendar = require('./lib/calendar'),
assert = require('chai').assert;
marionette('toggle calendar', function() {
var app;
var client = marionette.client({
profile: {
prefs: {
// we need to disable the keyboard to avoid intermittent failures on
// Travis (transitions might take longer to run and block UI)
'dom.mozInputMethod.enabled': false,
// Do not require the B2G-desktop app window to have focus (as per the
// system window manager) in order for it to do focus-related things.
'focusmanager.testmode': true,
}
},
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
setup(function() {
app = new Calendar(client);
app.launch();
});
function toggleLocalCalendar() {
app.openSettingsView();
app.settings.toggleCalendar();
app.closeSettingsView();
}
suite('> regular event', function() {
setup(function() {
app.createEvent({
title: 'Toggle Calendar Test',
location: 'Some Place'
});
toggleLocalCalendar();
});
suite('disable calendar', function() {
test('month view', function() {
client.waitFor(function() {
return app.monthDay.events.length === 0;
});
assert.equal(app.month.busyDots.length, 0, 'no busy dots');
});
test('week view', function() {
app.openWeekView();
client.waitFor(function() {
return app.week.events.length === 0;
});
});
test('day view', function() {
app.openDayView();
client.waitFor(function() {
return app.day.events.length === 0;
});
});
});
suite('enable calendar', function() {
setup(toggleLocalCalendar);
test('month view', function() {
client.waitFor(function() {
return app.monthDay.events.length;
});
});
test('week view', function() {
app.openWeekView();
client.waitFor(function() {
return app.week.events.length;
});
});
test('day view', function() {
app.openDayView();
client.waitFor(function() {
return app.day.events.length;
});
});
});
});
suite('> all day event', function() {
setup(function() {
app.createEvent({
title: 'Toggle Calendar Test',
location: 'Some Place',
allDay: true
});
toggleLocalCalendar();
});
test('should not hide all day on day view', function() {
app.openDayView();
client.waitFor(function() {
return app.day.events.length === 0;
});
assert.ok(
app.day.allDaysHolder.displayed(),
'all day should be displayed'
);
});
});
});

View file

@ -0,0 +1,431 @@
'use strict';
var Calendar = require('./lib/calendar'),
assert = require('chai').assert;
marionette('week view', function() {
var app, week;
var client = marionette.client({
profile: {
prefs: {
// we need to disable the keyboard to avoid intermittent failures on
// Travis (transitions might take longer to run and block UI)
'dom.mozInputMethod.enabled': false,
// Do not require the B2G-desktop app window to have focus (as per the
// system window manager) in order for it to do focus-related things.
'focusmanager.testmode': true,
}
},
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
setup(function() {
app = new Calendar(client);
app.launch();
week = app.week;
app.openWeekView();
});
test('hours', function() {
var hours = week.hours;
var i = -1, h, hour;
var currentHour = (new Date()).getHours();
while (++i < 24) {
h = i % 12 || 12;
hour = hours[i];
var text = h + '\n' + (i < 12 ? 'AM' : 'PM');
// current hour is hidden because "current time" line overlaps it!
if (i === currentHour) {
text = '';
}
assert.strictEqual(hour.text(), text, 'hour text');
}
});
test('swipe should change date', function() {
var prevText = app.headerContent.text();
var swipeCount = 0;
var headerCount = 0;
var multiMonthCount = 0;
// match the header on a multi week view, we just check for 2 dates since
// month names will have different patterns on each locale
var multiMonthPattern = /\d{4}.+\d{4}$/;
while (++swipeCount < 30) {
var text = app.headerContent.text();
// we are not checking for real overflow since font is different on each
// environment (Travis uses a wider font) which would make test to fail
// https://groups.google.com/forum/#!topic/mozilla.dev.gaia/DrQzv7qexw4
assert.operator(text.length, '<', 21, 'header should not overflow');
if (multiMonthPattern.test(text)) {
multiMonthCount += 1;
}
// jshint -W083
// there was a bug during implementation that caused week view to add
// duplicate dates to the DOM, checking if the days have a different
// "name" should be enough to catch regressions
var duplicateNames = week.dayNames.filter(function(name, i, arr) {
return arr.indexOf(name, i + 1) !== -1;
});
assert.equal(duplicateNames.join(', '), '', 'duplicate dates');
// jshint +W083
if (prevText !== text) {
prevText = text;
headerCount += 1;
}
app.swipeLeft();
}
assert.operator(headerCount, '>', 1, 'should update header at least 2x');
assert.operator(multiMonthCount, '>', 0, 'header with multiple months');
});
test('event + style + scroll + click', function() {
// "better" to do all these checks at once because of performance
// we don't set the startHour because the view will scroll to current time
// by default (since it's displaying today)
var eventData = {
title: 'Test Week View',
location: 'Somewhere'
};
app.createEvent(eventData);
// this is also enough to test if the view is scrolling to the proper
// destination (will timeout if it doesn't end or click won't trigger on
// proper element)
week.waitForHourScrollEnd();
var event = week.events[0];
assert.strictEqual(
event.text(),
eventData.title,
'display event title'
);
assert.match(
event.cssProperty('background-color'),
/rgba\(.+0.2\)/,
'should set bg color'
);
assert.ok(
event.cssProperty('border-left-color'),
'should set the border color'
);
assert.operator(
parseFloat(event.cssProperty('border-left-width')), '>', 0,
'should have border left width'
);
assert.strictEqual(
event.cssProperty('border-left-style'),
'solid',
'should have solid border'
);
event.click();
app.readEvent.waitForDisplay();
assert.strictEqual(
app.readEvent.title,
eventData.title,
'title'
);
assert.strictEqual(
app.readEvent.location,
eventData.location,
'location'
);
});
test('overlaps', function() {
function eventTitlesDisplayed() {
return week.events.every(checkDisplay);
}
function checkDisplay(el) {
return el.text().length > 0 && el.displayed();
}
app.createEvent({
title: '1 Overlap',
location: 'Somewhere'
});
assert.ok(
eventTitlesDisplayed(),
'title should be displayed if no overlaps'
);
app.createEvent({
title: '2 Overlap',
location: 'Somewhere Else'
});
assert.isFalse(
eventTitlesDisplayed(),
'title should be hidden if we have overlaps'
);
app.createEvent({
title: '3 Overlap',
location: 'Here'
});
assert.isFalse(
eventTitlesDisplayed(),
'title should be hidden if we have overlaps'
);
});
test('today', function() {
var todayDates = week.todayDates;
assert.strictEqual(todayDates.length, 1, 'single date marked as today');
// checking for style is usually a bad idea but since the class name
// doesn't guarantee that it's being applied properly we make sure to
// test for it as well
var today = todayDates[0];
assert.strictEqual(
today.cssProperty('font-weight'),
'500',
'font-weight'
);
assert.strictEqual(
today.cssProperty('color'),
'rgb(0, 142, 171)',
'text color'
);
});
test('current-time', function() {
week.waitForHourScrollEnd();
var currentTime = week.currentTime;
assert.ok(
currentTime.displayed(),
'current-time should be active'
);
assert.ok(
intersect(currentTime, week.currentHour),
'current time should be inside current hour range'
);
assert.ok(
!week.currentDisplayHour.displayed(),
'hour should be hidden if overlapping'
);
function intersect(el1, el2) {
var b1 = getBounds(el1);
var b2 = getBounds(el2);
return (
b1.left <= b2.right &&
b2.left <= b1.right &&
b1.top <= b2.bottom &&
b2.top <= b1.bottom
);
}
function getBounds(element) {
return element.scriptWith(function(el) {
return el.getBoundingClientRect();
});
}
// it should hide the currentTime if current day is not visible
app.swipeLeft();
client.waitFor(function() {
return !currentTime.displayed() && week.currentDisplayHour.displayed();
});
});
suite('double tap', function() {
var dateIso, time, editEvent;
setup(function() {
editEvent = app.editEvent;
week.waitForHourScrollEnd();
// we always click on the day that is in the middle of the screen
var day = week.days[7].scriptWith(function(el) {
return el.dataset.date;
});
dateIso = (new Date(day)).toISOString().slice(0, 10);
// we always click the 3rd hour from the top
time = Math.floor((week.scrollTop + 100) / 50);
});
test('fast', function() {
week.actions
.doubleTap(week.element, 200, 200)
.perform();
assertEditEvent();
});
test('slow + hide', function() {
week.actions
.tap(week.element, 200, 200)
.perform();
client.waitFor(function() {
return week.isShowingAddEventLink();
});
// clicking on another hour should hide the link
week.actions
.tap(week.element, 200, 260)
.perform();
client.waitFor(function() {
return !week.isShowingAddEventLink();
});
week.actions
.tap(week.element, 200, 200)
.wait(0.5)
.tap(week.element, 200, 200)
.perform();
assertEditEvent();
});
function assertEditEvent() {
editEvent.waitForDisplay();
assert.equal(editEvent.startDate, dateIso, 'startDate');
assert.equal(editEvent.endDate, dateIso, 'endDate');
assert.equal(editEvent.startTime, pad(time) + ':00:00', 'startTime');
assert.equal(editEvent.endTime, pad(time + 1) + ':00:00', 'endTime');
}
});
suite('event retargeting tests', function() {
test('When tapping around an event we can create another event',
function() {
// When displaying the view, for some reason (likely APZC) scrollTop is
// incorrect for some time. So we wait for the expected value using
// waitForHourScrollEnd().
// Also rarely we're not scrolled at first, and so this fails with a
// timeout.
week.waitForHourScrollEnd();
var testedHour = 6;
// it's really the 3rd visible day as 5 invisible days are in the markup
var testedDay = 7;
week.scrollToHour(testedHour);
week.tapDayHour({ day: testedDay, hour: testedHour });
week.actions.wait(1).perform();
assert.ok(
week.isShowingAddEventLink(), 'The `add-event` link is displayed'
);
// The "+" button should work with event retargeting.
week.tapDayHour({ day: testedDay, hour: testedHour, bottom: -1 });
// tapping using "-1" could theorically fail because scrollToHour does not
// ensure we have some pixels after the element. Moreover this fails for
// sure if testedHour is 23.
app.editEvent.waitForDisplay();
app.editEvent.title = 'Test event';
app.editEvent.save();
week.waitForDisplay();
week.tapDayHour({ day: testedDay, hour: testedHour, bottom: -1 });
assert.ok(
week.isShowingAddEventLink(), 'The `add-event` link is displayed'
);
});
});
suite('12/24 hour format', function() {
// Refer to http://bugzil.la/1061135.
test.skip('default format: 12 hour', function() {
assert.equal(week.sideBarHours[0].text(), '12\nAM');
assert.equal(week.sideBarHours[13].text(), '1\nPM');
assert.equal(week.sideBarHours[23].text(), '11\nPM');
var now = new Date();
var minutes = now.getMinutes();
minutes = minutes < 10 ? (0 + String(minutes)) : minutes;
var currentTime = (now.getHours() % 12) + ':' + minutes;
assert.equal(week.currentTime.text(), currentTime);
});
test('switch to 24 hour format', function() {
app.switch24HourTimeFormat();
assert.equal(week.sideBarHours[0].text(), '0');
assert.equal(week.sideBarHours[13].text(), '13');
assert.equal(week.sideBarHours[23].text(), '23');
var now = new Date();
var currentTime = pad(now.getHours()) + ':' + pad(now.getMinutes());
assert.equal(week.currentTime.text(), currentTime);
});
});
test('delete', function() {
app.createEvent({
title: 'Foo',
location: 'Bar'
});
client.waitFor(function() {
return app.week.events.length === 1;
}, { timeout: 2000 });
app.openMonthView();
app.monthDay.events[0].click();
app.readEvent.waitForDisplay();
app.readEvent.edit();
app.editEvent.waitForDisplay();
app.editEvent.delete();
app.month.waitForDisplay();
app.openWeekView();
client.waitFor(function() {
return app.week.events.length === 0;
}, { timeout: 2000 });
});
test('scroll to event', function() {
week.waitForHourScrollEnd();
week.scrollToTop();
var startDate = new Date();
startDate.setHours(13, 0, 0, 0);
app.createEvent({
title: 'Test Week View',
location: 'Somewhere',
startDate: startDate
});
week.waitForDisplay();
// scroll to 1h before event
week.waitForHourScrollEnd(12);
});
function pad(n) {
return n < 10 ? '0' + n : String(n);
}
});

View file

@ -0,0 +1,22 @@
{
"extends": "../../../../.jshintrc",
"predef": [
"CSSStyleSheet",
"IDBTransaction",
"assert",
"define",
"expect",
"require",
"requirejs",
"requireCommon",
"setup",
"suite",
"suiteGroup",
"suiteSetup",
"suiteTeardown",
"teardown",
"test",
"testSupport",
"sinon"
]
}

View file

@ -0,0 +1,33 @@
define(function(require) {
'use strict';
var mochaPromise = require('test/support/mocha_promise');
var threads = require('ext/threads');
suite('calendar service', function() {
var thread, client;
setup(function() {
thread = threads.create({
src: '/js/backend/calendar_worker.js',
type: 'worker'
});
client = threads.client('calendar', { thread: thread });
});
teardown(function() {
thread.destroy();
});
mochaPromise(test, 'calling service method', function() {
var call = client.method('echo', 'foo', 'bar', 'baz');
return expect(call).to.eventually.deep.equal([
'foo',
'bar',
'baz'
]);
});
});
});

View file

@ -0,0 +1,842 @@
define(function(require) {
'use strict';
/* global MockMozIntl */
require('/shared/test/unit/mocks/mock_moz_intl.js');
var Timespan = require('common/timespan');
var Calc = require('common/calc');
//Worth noting that these tests will fail
//in horrible ways outside of US timezone.
suite('calendar/calc', function() {
var subject, mocked = {};
setup(function() {
subject = Calc;
window.mozIntl = MockMozIntl;
});
teardown(function() {
var key;
for (key in mocked) {
if (mocked.hasOwnProperty(key)) {
subject[key] = mocked[key];
}
}
});
function mock(fn, value) {
if (!(fn in mocked)) {
mocked[fn] = subject[fn];
}
subject[fn] = function() {
return value;
};
}
suite('#isOnlyDate', function() {
function verify(date, message, isTrue=true) {
test(message + ' ' + date.toString() + ' === ' + isTrue, function() {
assert.equal(
subject.isOnlyDate(date),
isTrue,
message
);
});
}
verify(new Date(2012, 1), 'month');
verify(
new Date(2012, 1, 1),
'YYYY:DD:MM'
);
verify(
new Date(2012, 1, 1, 1),
'hour',
false
);
verify(
new Date(2012, 1, 1, 0, 1),
'minute',
false
);
verify(
new Date(2012, 1, 1, 0, 0, 1),
'second',
false
);
});
suite('#dayOfWeekFromStartDay', function() {
test('sunday', function() {
subject.startDay = 0;
var expected = [
['sun', 0, 0],
['mon', 1, 1],
['tue', 2, 2],
['wed', 3, 3],
['thu', 4, 4],
['fri', 5, 5],
['sat', 6, 6]
];
expected.forEach(function(line) {
var [name, date, numeric] = line;
assert.equal(
subject.dayOfWeekFromStartDay(date),
numeric,
name
);
});
});
test('Monday', function() {
subject.startDay = 1;
var expected = [
['mon', 1, 0],
['tue', 2, 1],
['wed', 3, 2],
['thu', 4, 3],
['fri', 5, 4],
['sat', 6, 5],
['sun', 0, 6]
];
expected.forEach(function(line) {
var [name, date, numeric] = line;
assert.equal(
subject.dayOfWeekFromStartDay(date),
numeric,
name
);
});
});
test('Saturday', function() {
subject.startDay = 6;
var expected = [
['sat', 6, 0],
['sun', 0, 1],
['mon', 1, 2],
['tue', 2, 3],
['wed', 3, 4],
['thu', 4, 5],
['fri', 5, 6]
];
expected.forEach(function(line) {
var [name, date, numeric] = line;
assert.equal(
subject.dayOfWeekFromStartDay(date),
numeric,
name
);
});
});
});
test('#dayOfWeekFromMonday', function() {
var expected = [
['mon', 1, 0],
['tue', 2, 1],
['wed', 3, 2],
['thu', 4, 3],
['fri', 5, 4],
['sat', 6, 5],
['sun', 0, 6]
];
expected.forEach(function(line) {
var [name, date, numeric] = line;
assert.equal(
subject.dayOfWeekFromMonday(date),
numeric,
name
);
});
});
suite('#dayOfWeek', function() {
var realStartDay;
var date = new Date(2012, 0, 1); // 2012-01-01 is a Sunday
suiteSetup(function() {
realStartDay = subject.startDay;
});
suiteTeardown(function() {
subject.startDay = realStartDay;
});
test('weekStartDay = 0', function() {
subject.startDay = 0; // week starts on Sunday => Sunday is day 0
assert.equal(
subject.dayOfWeek(date),
0
);
});
test('weekStartDay = 1', function() {
subject.startDay = 1; // week starts on Monday => Sunday is day 6
assert.equal(
subject.dayOfWeek(date),
6
);
});
test('weekStartDay = 6', function() {
subject.startDay = 6; // week starts on Saturday => Sunday is day 1
assert.equal(
subject.dayOfWeek(date),
1
);
});
});
suite('handle localization events', function() {
var firstDayOfTheWeek = 0;
suiteSetup(function() {
sinon.stub(window.mozIntl, 'calendarInfo', function(token) {
return Promise.resolve(firstDayOfTheWeek);
});
});
suiteTeardown(function() {
window.mozIntl.calendarInfo.restore();
});
test('firstDayOfTheWeek = 1', function(done) {
firstDayOfTheWeek = 1;
window.dispatchEvent(new window.Event('localized'));
Promise.resolve().then(() => {
assert.equal(subject.startDay, 1, 'week starts on Monday');
done();
});
});
test('firstDayOfTheWeek = 6', function(done) {
firstDayOfTheWeek = 6;
window.dispatchEvent(new window.Event('localized'));
Promise.resolve().then(() => {
assert.equal(subject.startDay, 6, 'week starts on Saturday');
done();
});
});
test('firstDayOfTheWeek = 0', function(done) {
firstDayOfTheWeek = 0;
window.dispatchEvent(new window.Event('localized'));
Promise.resolve().then(() => {
assert.equal(subject.startDay, 0, 'week starts on Sunday');
done();
});
});
});
suite('#spanOfMonth', function() {
function time(year, month, day, hour, min) {
var date = new Date(
year,
month || 0,
day || 1,
hour || 0,
min || 0
);
return v(date);
}
function v(date) {
return date.valueOf();
}
test('5 week month', function() {
var date = new Date(2012, 3, 1, 5, 1);
var range = subject.spanOfMonth(date);
assert.equal(
time(2012, 3, 1),
range.start
);
var end = new Date(2012, 4, 6);
end.setMilliseconds(-1);
assert.equal(
range.end,
v(end)
);
});
test('6 week month', function() {
var date = new Date(2012, 11, 1);
var range = subject.spanOfMonth(date);
assert.equal(
time(2012, 10, 25),
range.start
);
var end = new Date(2013, 0, 6);
end.setMilliseconds(-1);
assert.equal(
range.end,
v(end)
);
});
test('4 week month', function() {
var date = new Date(2009, 1, 1);
var range = subject.spanOfMonth(date);
var end = new Date(2009, 2, 1);
end.setMilliseconds(-1);
assert.equal(
range.end,
v(end)
);
});
});
suite('#dateToTransport', function() {
test('ICAL date', function() {
var date = new Date(2012, 0, 1, 11);
var utc = Date.UTC(
date.getFullYear(),
date.getMonth(),
date.getDate(),
date.getHours(),
date.getMinutes(),
date.getSeconds(),
date.getMilliseconds()
);
var expected = {
tzid: subject.FLOATING,
utc: utc,
offset: 0,
isDate: true
};
assert.deepEqual(
subject.dateToTransport(date, null, true),
expected
);
});
test('floating tz', function() {
var date = new Date(2012, 0, 1, 11, 1, 7);
var utc = Date.UTC(
date.getFullYear(),
date.getMonth(),
date.getDate(),
date.getHours(),
date.getMinutes(),
date.getSeconds(),
date.getMilliseconds()
);
var expected = {
tzid: subject.FLOATING,
utc: utc,
offset: 0
};
assert.deepEqual(
subject.dateToTransport(date, subject.FLOATING),
expected
);
});
test('utc', function() {
var date = new Date(2012, 0, 1, 11, 1);
var utc = Date.UTC(2012, 0, 1, 11, 1);
var offset = utc - date;
var expected = {
offset: offset,
utc: utc
};
assert.deepEqual(
subject.dateToTransport(date),
expected
);
});
});
suite('#getUTC', function() {
test('utc - conversion', function() {
var date = new Date(2012, 9, 1, 7, 11);
assert.notEqual(
date,
subject.getUTC(date)
);
});
});
suite('#dateFromTransport', function() {
test('utc - DST', function() {
// NOTE: I don't expect this test to fail
// in other timezones but it relies on
// hard coded DST info to verify that DST
// actually works in PST.
var date = new Date(2012, 9, 1, 7, 11);
var data = subject.dateToTransport(date);
assert.deepEqual(
subject.dateFromTransport(data),
date
);
});
test('utc - standard', function() {
var expected = new Date(2012, 0, 1, 0, 5);
var data = subject.dateToTransport(expected);
assert.deepEqual(
subject.dateFromTransport(data),
expected
);
});
test('floating', function() {
var expected = new Date(2012, 0, 8, 9, 10);
var data = subject.dateToTransport(
expected, subject.FLOATING
);
assert.deepEqual(
subject.dateFromTransport(data),
expected
);
});
});
suite('#getWeekStartDate', function() {
// we are testing for first week of Aug 2012
var expected = new Date(2012, 6, 29);
function matches(given) {
assert.equal(
subject.getWeekStartDate(given).valueOf(),
expected.valueOf()
);
}
test('when given middle', function() {
matches(new Date(2012, 7, 2));
});
test('when given start', function() {
matches(new Date(2012, 6, 29));
});
});
suite('#spanOfDay', function() {
var date = new Date(2012, 1, 1, 10, 33);
test('include time', function() {
var end = new Date(
date.getFullYear(),
date.getMonth(),
date.getDate() + 1
);
var out = subject.spanOfDay(date, true);
assert.deepEqual(out, new Timespan(date, end));
});
test('ignore time', function() {
var start = new Date(
date.getFullYear(),
date.getMonth(),
date.getDate()
);
var end = new Date(start.valueOf());
end.setDate(end.getDate() + 1);
var out = subject.spanOfDay(date);
assert.deepEqual(out, new Timespan(start, end));
});
});
test('#hourDiff', function() {
var start = new Date(2012, 0, 5);
var end = new Date(2012, 0, 7);
var expected = 48;
var out = subject.hourDiff(start, end);
assert.equal(out, expected);
});
suite('#daysBetween', function() {
//Nov 29th 2012
var start = new Date(2012, 10, 29, 2);
// Dec 2nd 2012
var end = new Date(2012, 11, 2, 5);
test('same day', function() {
var end = new Date(
start.getFullYear(),
start.getMonth(),
start.getDate(),
start.getHours() + 1
);
var expected = new Date(
start.getFullYear(),
start.getMonth(),
start.getDate()
);
assert.deepEqual(
subject.daysBetween(start, end),
[expected]
);
});
test('include time', function() {
var expected = [
start,
new Date(2012, 10, 30),
new Date(2012, 11, 1),
end
];
assert.deepEqual(
subject.daysBetween(start, end, true),
expected
);
});
test('exclude time', function() {
var expected = [
new Date(2012, 10, 29),
new Date(2012, 10, 30),
new Date(2012, 11, 1),
new Date(2012, 11, 2)
];
assert.deepEqual(
subject.daysBetween(start, end),
expected
);
});
});
suite('#getWeekEndDate', function() {
// we are testing for first week of Aug 2012
var expected = new Date(2012, 7, 4, 23, 59, 59, 999);
function matches(given) {
assert.equal(
subject.getWeekEndDate(given).valueOf(),
expected.valueOf()
);
}
test('when given middle', function() {
matches(new Date(2012, 7, 2));
});
test('when given start', function() {
matches(new Date(2012, 6, 29));
});
});
suite('#isSameDate', function() {
test('same day', function() {
assert.isTrue(subject.isSameDate(
new Date(2012, 1, 1, 8),
new Date(2012, 1, 1, 23)
));
});
test('same day different month', function() {
assert.isFalse(subject.isSameDate(
new Date(2012, 2, 1, 8),
new Date(2012, 1, 1, 8)
));
});
});
suite('#isToday', function() {
test('when given is today', function() {
var result = subject.isToday(new Date());
assert.isTrue(result, 'should be true when given today');
});
test('when given is not today', function() {
var now = new Date();
now.setDate(now.getDate() - 1);
var result = subject.isToday(now);
assert.isFalse(result, 'should be false when given is not today');
});
});
suite('#isPast', function() {
test('when date is passed', function() {
var date = new Date();
date.setTime(Date.now() - 1000);
var result = subject.isPast(date);
assert.isTrue(result, 'should be true when given is in the past');
});
test('when given is in the future', function() {
var date = new Date();
date.setTime(Date.now() + 100);
var result = subject.isPast(date);
assert.isFalse(result, 'should return false when date is in the future');
});
});
suite('#isFuture', function() {
test('when date is passed', function() {
var date = new Date();
date.setTime(Date.now() - 100);
var result = subject.isFuture(date);
assert.isFalse(result);
});
test('when given is in the future', function() {
var date = new Date(Date.now() + 100);
var result = subject.isFuture(date);
assert.isTrue(result);
});
});
suite('#dateFromId', function() {
var id,
date = new Date(2012, 7, 3);
suite('from day', function() {
setup(function() {
id = subject.getDayId(date);
});
test('id to date', function() {
assert.deepEqual(
subject.dateFromId(id),
date
);
});
});
suite('from month', function() {
setup(function() {
id = subject.getMonthId(date);
});
test('id to date', function() {
assert.deepEqual(
subject.dateFromId(id),
new Date(2012, 7, 1)
);
});
});
});
suite('#getWeeksDays', function() {
test('starting from monday', function() {
//starts on thursday
var start = new Date(2012, 10, 1);
//expected days by their getDayId
var expected = [
'd-2012-9-28',
'd-2012-9-29',
'd-2012-9-30',
'd-2012-9-31',
'd-2012-10-1',
'd-2012-10-2',
'd-2012-10-3'
];
var result = subject.getWeeksDays(start).map(
subject.getDayId
);
assert.deepEqual(result, expected);
});
});
test('#getDayId', function() {
var result = subject.getDayId(
new Date(2012, 3, 7)
);
assert.equal(result, 'd-2012-3-7');
});
test('#getMonthId', function() {
var result = subject.getMonthId(
new Date(2012, 3, 7)
);
assert.equal(result, 'm-2012-3');
});
suite('#relativeState', function() {
setup(function() {
mock('isToday', false);
});
test('when in the past', function() {
mock('isPast', true);
var state = subject.relativeState(
new Date(1991, 1, 1),
new Date(1991, 1, 1)
);
assert.equal(state, subject.PAST);
});
test('when in the future', function() {
mock('isPast', false);
var state = subject.relativeState(
new Date(1991, subject.today.getMonth(), 1),
new Date(1991, subject.today.getMonth(), 1)
);
assert.equal(state, subject.FUTURE);
});
test('when is in a different month in the past', function() {
mock('isPast', true);
var state = subject.relativeState(
new Date(1991, subject.today.getMonth() - 1, 1),
new Date(1991, subject.today.getMonth(), 1)
);
assert.include(state, subject.PAST);
assert.include(state, subject.OTHER_MONTH);
});
test('when is in a different month in the future', function() {
mock('isPast', false);
var state = subject.relativeState(
new Date(1991, subject.today.getMonth() + 1, 1),
new Date(1991, subject.today.getMonth(), 1)
);
assert.include(state, subject.FUTURE);
assert.include(state, subject.OTHER_MONTH);
});
test('when is today', function() {
mock('isToday', true);
var state = subject.relativeState(new Date(1991, 1, 1));
assert.equal(state, subject.PRESENT);
});
});
suite('#isAllDay', function() {
test('full day', function() {
assert.isTrue(subject.isAllDay(
new Date(2014, 9, 5),
new Date(2014, 9, 5),
new Date(2014, 9, 6)
));
});
test('not start of the day', function() {
assert.isFalse(subject.isAllDay(
new Date(2014, 9, 5),
new Date(2014, 9, 5, 5),
new Date(2014, 9, 6)
));
});
test('longer than a full day', function() {
assert.isTrue(subject.isAllDay(
new Date(2014, 9, 5),
new Date(2014, 9, 5),
new Date(2014, 9, 6, 5)
));
});
test('multiple days', function() {
assert.isTrue(subject.isAllDay(
new Date(2014, 9, 5),
new Date(2014, 9, 5),
new Date(2014, 9, 16)
));
assert.isTrue(subject.isAllDay(
new Date(2014, 9, 8),
new Date(2014, 9, 5),
new Date(2014, 9, 16)
));
});
test('same date', function() {
// yahoo uses same start/end dates for recurring all day events
assert.isTrue(subject.isAllDay(
new Date(2014, 9, 5),
new Date(2014, 9, 5),
new Date(2014, 9, 5)
));
});
});
test('#monthStart', function() {
assert.deepEqual(
Calc.monthStart(new Date(2014, 10, 1)),
new Date(2014, 10, 1),
'first day'
);
assert.deepEqual(
Calc.monthStart(new Date(2014, 10, 12)),
new Date(2014, 10, 1),
'middle'
);
});
});
});

View file

@ -0,0 +1,12 @@
define(function(require) {
'use strict';
var compare = require('common/compare');
test('compare', function() {
assert.equal(compare(0, 1), -1);
assert.equal(compare(1, 0), 1);
assert.equal(compare(10, 10), 0);
});
});

View file

@ -0,0 +1,25 @@
define(function(require) {
'use strict';
var CalendarError = require('common/error');
suite('errors', function() {
function verify(symbol) {
test(symbol, function() {
var detail = {};
assert.ok(CalendarError[symbol], 'CalendarError.' + symbol);
var err = new CalendarError[symbol](detail);
assert.equal(detail, err.detail, 'uses detail');
assert.ok(err.l10nID, 'has l10nID');
assert.ok(err.name, 'has name');
});
}
([
'Authentication',
'InvalidServer',
'ServerFailure'
]).forEach(verify);
});
});

View file

@ -0,0 +1,22 @@
define(function(require) {
'use strict';
var nextTick = require('common/next_tick');
test('nextTick', function(done) {
var inc = 0;
nextTick(() => {
assert.equal(inc, 1);
nextTick(() => {
assert.equal(inc, 2);
nextTick(done);
});
inc++;
});
inc = 1;
});
});

View file

@ -0,0 +1,63 @@
define(function(require) {
'use strict';
var object = require('common/object');
suite('Object', function() {
var subject;
setup(function() {
subject = object;
});
test('#filter', function() {
assert.deepEqual(
subject.filter(
{
'0': 'red',
'1': 'orange',
'2': 'yellow',
'3': 'green',
'4': 'blue',
'5': 'violet'
},
(key, value) => {
return value === 'red' || parseInt(key, 10) % 2 === 1;
}
),
[
'red',
'orange',
'green',
'violet'
]
);
});
test('#forEach', function() {
var input = { a: 'b', p: 'q', x: 'y' };
var result = {};
subject.forEach(input, (key, value) => {
result[key] = value;
});
assert.deepEqual(input, result);
});
test('#map', function() {
var input = { a: 'b', p: 'q', x: 'y' };
var result = subject.map(input, (key, value) => {
return value;
});
assert.deepEqual(result, ['b', 'q', 'y']);
});
test('#values', function() {
var input = { a: 'b', p: 'q', x: 'y' };
var result = subject.values(input);
assert.deepEqual(result, ['b', 'q', 'y']);
});
});
});

View file

@ -0,0 +1,10 @@
define(function(require) {
'use strict';
var presets = require('common/presets');
test('list', function() {
assert.instanceOf(presets, Object);
});
});

View file

@ -0,0 +1,146 @@
define(function(require) {
'use strict';
var promise = require('common/promise');
function errorOrFive(shouldError, callback) {
var error;
if (shouldError) {
error = new Error('You told me to error!');
} else {
error = null;
}
return callback(error, !shouldError && 5);
}
suite('denodeify', function() {
var subject;
setup(function() {
subject = promise.denodeify(errorOrFive);
});
test('node-style error', function(done) {
subject(true /* shouldError */, function(error) {
assert.instanceOf(error, Error);
assert.strictEqual(error.message, 'You told me to error!');
done();
});
});
test('node-style without error', function(done) {
subject(false /* shouldError */, function(error, result) {
assert.isNull(error);
assert.strictEqual(result, 5);
done();
});
});
test('promise with error', function(done) {
subject(true /* shouldError */)
.catch(function(error) {
assert.instanceOf(error, Error);
assert.strictEqual(error.message, 'You told me to error!');
done();
});
});
test('promise without error', function(done) {
subject(false /* shouldError */)
.then(function(value) {
assert.strictEqual(value, 5);
done();
});
});
test('no callback and no promise', function() {
// Should not hiccup.
subject(false /* shouldError */);
});
});
function SlowCalculator() {
this.currentValue = 0;
promise.denodeifyAll(this, [
'add',
'multiply'
]);
}
SlowCalculator.prototype = {
add: function(x, callback) {
var error = null;
if (typeof x !== 'number') {
error = new Error('Can only add numbers!');
}
this.currentValue += x;
setTimeout(function() {
callback(error, error || this.currentValue);
}.bind(this), 10);
},
multiply: function(x, callback) {
var error = null;
if (typeof x !== 'number') {
error = new Error('Can only multiply numbers!');
}
this.currentValue *= x;
setTimeout(function() {
callback(error, error || this.currentValue);
}.bind(this), 10);
}
};
suite('denodeifyAll', function() {
var subject;
setup(function() {
subject = new SlowCalculator();
});
test('node-style error', function(done) {
subject.multiply('lolcats', function(error) {
assert.instanceOf(error, Error);
assert.strictEqual(error.message, 'Can only multiply numbers!');
done();
});
});
test('node-style without error', function(done) {
subject.add(5, function(error, result) {
assert.isNull(error);
assert.strictEqual(result, 5);
done();
});
});
test('promise with error', function(done) {
subject.add(/\s+/).catch(function(error) {
assert.instanceOf(error, Error);
assert.strictEqual(error.message, 'Can only add numbers!');
done();
});
});
test('promise without error', function(done) {
subject.add(9)
.then(function(value) {
assert.strictEqual(value, 9);
return subject.multiply(8);
})
.then(function(value) {
assert.strictEqual(value, 72);
done();
});
});
test('no callback and no promise', function() {
// Should not hiccup.
subject.multiply(3.14);
});
});
});

View file

@ -0,0 +1,87 @@
/**
* Responder is actually an downstream of test-agent
* porting the handleEvent thing over here for now
* to eventually be moved back with the other tweaks.
*/
define(function(require) {
'use strict';
var Responder = require('common/responder');
suite('responder', function() {
var subject;
setup(function() {
subject = new Responder();
});
test('multi arg responder', function() {
var calledWith;
subject.on('test', function() {
calledWith = Array.slice(arguments);
});
subject.respond(['test', 'one', 'two', 'three']);
assert.deepEqual(calledWith, ['one', 'two', 'three']);
});
suite('handleEvent', function() {
test('object', function() {
var events = {};
var target = {
// mimic the actual eventTarget api's
handleEvent: function(e) {
var type = e.type;
if (!(type in events)) {
events[type] = [];
}
events[type].push(e.data);
}
};
subject.on('foo', target);
subject.on('foo', target);
subject.on('bar', target);
subject.emit('foo', 1);
subject.emit('bar', 1, 2);
assert.deepEqual(
events.foo,
[[1], [1]]
);
assert.deepEqual(
events.bar,
[[1, 2]]
);
});
});
test('#emitWhenListener', function(done) {
subject.emitWhenListener('foo', 'bar');
subject.emitWhenListener('foo', 'baz');
// Let the event loop tick before listening for the 'foo' topic.
setTimeout(() => {
var count = 0;
subject.on('foo', function onFoo(word) {
if (count === 0) {
assert.equal(word, 'bar');
count++;
return;
}
assert.equal(word, 'baz');
subject.off('foo', onFoo);
done();
});
}, 0);
});
});
});

View file

@ -0,0 +1,358 @@
define(function(require) {
'use strict';
var Calc = require('common/calc');
var Timespan = require('common/timespan');
suite('timespan', function() {
var subject;
var startDate;
var endDate;
setup(function() {
startDate = new Date(2012, 1, 1);
endDate = new Date(2012, 12, 1);
subject = new Timespan(
startDate,
endDate
);
});
test('initializer', function() {
assert.equal(
subject.start,
startDate.valueOf()
);
assert.equal(
subject.end,
endDate.valueOf()
);
});
test('#isEqual', function() {
var span = new Timespan(
new Date(2012, 1, 1),
new Date(2012, 1, 5)
);
var eqlSpan = new Timespan(
new Date(2012, 1, 1),
new Date(2012, 1, 5)
);
var notEqualSpan = new Timespan(
new Date(2012, 1, 1),
new Date(2012, 1, 6)
);
assert.isTrue(span.isEqual(eqlSpan));
assert.isFalse(span.isEqual(notEqualSpan));
});
test('#daysBetween', function() {
var range = new Timespan(
new Date(2012, 1, 1),
new Date(2012, 1, 3)
);
var dates = Calc.daysBetween(range);
assert.deepEqual(
dates[0],
new Date(2012, 1, 1)
);
assert.deepEqual(
dates[1],
new Date(2012, 1, 2)
);
assert.deepEqual(
dates[2],
new Date(2012, 1, 3)
);
});
suite('#trimOverlap', function() {
var before;
var subject;
var none;
setup(function() {
none = new Timespan(
new Date(2012, 5, 1),
new Date(2012, 5, 15)
);
before = new Timespan(
// July 1
new Date(2012, 6, 1),
// Aug 4th
new Date(2012, 7, 4)
);
subject = new Timespan(
// July 29th
new Date(2012, 6, 29),
// Aug 31
new Date(2012, 7, 31)
);
});
test('middle', function() {
var long = new Timespan(
new Date(2012, 0, 1),
new Date(2012, 0, 31)
);
var short = new Timespan(
new Date(2012, 0, 5),
new Date(2012, 0, 10)
);
assert.isNull(
long.trimOverlap(short),
'should return null subject contains given'
);
assert.isNull(
short.trimOverlap(long),
'should return null when given contains subject'
);
});
test('no overlap', function() {
var out = before.trimOverlap(none);
assert.deepEqual(out, none);
});
test('overlaps && input.end > subject.start', function() {
var output = subject.trimOverlap(
before
);
var start = new Date(2012, 6, 1);
var end = new Date(
2012, 6, 29
);
end.setMilliseconds(-1);
var expected = new Timespan(
start, end
);
assert.deepEqual(output, expected);
});
test('overlaps && subject.end > input.start', function() {
var output = before.trimOverlap(
subject
);
var start = new Date(2012, 7, 4);
start.setMilliseconds(
start.getMilliseconds() + 1
);
var end = new Date(
2012, 7, 31
);
var expected = new Timespan(
start, end
);
assert.deepEqual(output, expected);
});
});
suite('#overlaps', function() {
var dates;
suiteSetup(function() {
dates = {
'range: inside': {
assert: true,
value: new Timespan(
startDate,
endDate
)
},
'range: starts on end date': {
assert: false,
value: new Timespan(
endDate,
new Date(2015, 1, 1)
)
},
'range: outside before': {
assert: false,
value: new Timespan(
new Date(1991, 1, 1),
new Date(2000, 1, 1)
)
},
'range: outside after': {
assert: false,
value: new Timespan(
new Date(2015, 1, 1),
new Date(2016, 1, 1)
)
},
'range: greater': {
assert: true,
value: new Timespan(
new Date(2011, 12, 31),
new Date(2012, 12, 2)
)
},
'range overlap: start before': {
assert: true,
value: new Timespan(
new Date(2011, 1, 1),
new Date(2012, 1, 15)
)
},
'range overlap: end before': {
assert: true,
value: new Timespan(
new Date(2012, 1, 2),
new Date(2012, 11, 12)
)
}
};
});
test('overlaping times', function() {
var key;
for (key in dates) {
if (dates.hasOwnProperty(key)) {
var value = dates[key].value;
assert.equal(
subject.overlaps(value),
dates[key].assert,
key
);
}
}
});
test('overlaping times as dates', function() {
var key;
for (key in dates) {
if (dates.hasOwnProperty(key)) {
var value = dates[key].value;
var start = new Date(value.start);
var end = new Date(value.end);
assert.equal(
subject.overlaps(start, end),
dates[key].assert,
key
);
}
}
});
});
suite('#contains', function() {
var dates;
suiteSetup(function() {
dates = {
'day of': {
assert: true,
value: new Date(2012, 1, 1)
},
'one day after': {
assert: true,
value: new Date(2012, 1, 2)
},
'day of end range': {
assert: true,
value: new Date(2012, 12, 1)
},
'day before range': {
assert: false,
value: new Date(2011, 12, 31)
},
'day after range': {
assert: false,
value: new Date(2012, 12, 2)
}
};
});
test('timespan in range', function() {
var span = new Timespan(
new Date(2012, 1, 1),
new Date(2012, 1, 5)
);
assert.isTrue(
subject.contains(span)
);
assert.isFalse(
span.contains(subject)
);
});
function testWith(method, useNumeric) {
var key;
var prefix = '';
if (useNumeric) {
prefix += '[numeric] ';
}
prefix += method;
for (key in dates) {
if (dates.hasOwnProperty(key)) {
var value = dates[key].value.valueOf();
if (useNumeric) {
value = value.valueOf();
}
assert.equal(
subject.contains(value),
dates[key].assert,
prefix + key
);
}
}
}
test('numeric (contains)', function() {
testWith('contains', true);
});
test('numeric (containsNumeric)', function() {
testWith('containsNumeric', true);
});
test('date', function() {
testWith('contains', false);
});
});
});
});

View file

@ -0,0 +1,160 @@
define(function(require) {
'use strict';
var CalendarError = require('common/error');
var ErrorController = require('controllers/error');
var Factory = require('test/support/factory');
var Responder = require('common/responder');
var core = require('core');
var nextTick = require('common/next_tick');
var notification = require('notification');
suite('controllers/error', function() {
/**
* Because of uplifting difficulties I chose to copy/paste
* the following code rather then attempt to uplift related testing fixes...
* I may later live to regret this but given the short time we have until
* shipping v1.0.1 I have chosen to copy/paste.
*/
function mockRequestWakeLock(handler) {
var realApi;
function lockMock() {
return {
mAquired: false,
mIsUnlocked: false,
unlock: function() {
this.mIsUnlocked = true;
}
};
}
suiteSetup(function() {
realApi = navigator.requestWakeLock;
navigator.requestWakeLock = function(type) {
var lock = lockMock();
lock.type = type;
lock.mAquired = true;
handler && handler(lock);
return lock;
};
});
suiteTeardown(function() {
navigator.requestWakeLock = realApi;
});
}
var subject;
var detail;
setup(function(done) {
testSupport.calendar.core();
subject = new ErrorController();
core.db.open(done);
detail = {
account: Factory('account')
};
});
teardown(function(done) {
testSupport.calendar.clearStore(
core.db,
['accounts'],
function() {
core.db.close();
done();
}
);
});
test('initialization', function() {
assert.instanceOf(subject, Responder);
});
suite('default handling', function() {
test('authenticate', function(done) {
var callsAuth = false;
var error = new CalendarError.Authentication(detail);
subject.handleAuthenticate = function(account) {
assert.equal(account, detail.account, 'sends account');
callsAuth = true;
};
subject.once('error', function(givenErr) {
done(function() {
assert.ok(callsAuth);
assert.equal(error, givenErr, 'sends error');
});
});
subject.dispatch(error);
});
});
test('.accountErrorUrl', function() {
assert.ok(subject.accountErrorUrl);
});
suite('#handleAuthenticate', function() {
var sent;
var account;
var lock;
mockRequestWakeLock(function(_lock) {
lock = _lock;
});
var realSendApi;
suiteSetup(function() {
realSendApi = notification.sendNotification;
notification.sendNotification = function() {
sent = Array.slice(arguments);
return Promise.resolve();
};
});
suiteTeardown(function() {
notification.sendNotification = realSendApi;
});
setup(function() {
sent = null;
lock = null;
account = Factory('account', { _id: 'woot' });
});
test('with count 1', function(done) {
var expectedURL = subject.accountErrorUrl + account._id;
account.error = { count: 1 };
subject.handleAuthenticate(account, function() {
nextTick(function() {
done(function() {
assert.ok(lock.mIsUnlocked, 'unlocks');
assert.ok(sent, 'sends notification');
assert.equal(sent[2], expectedURL, 'sends to modify account');
});
});
});
assert.ok(lock, 'aquires lock');
});
test('with count 2', function(done) {
account.error = { count: 2 };
subject.handleAuthenticate(account, function() {
done(function() {
assert.ok(!lock, 'does not aquire lock');
assert.ok(!sent, 'does not resend error');
});
});
});
});
});
});

View file

@ -0,0 +1,122 @@
define(function(require) {
'use strict';
/* global MockMozIntl */
require('/shared/test/unit/mocks/mock_moz_intl.js');
var Factory = require('test/support/factory');
var mockRequestWakeLock = require('test/support/mock_request_wake_lock');
var controller = require('controllers/notifications');
var core = require('core');
var mochaPromise = require('test/support/mocha_promise');
var notification = require('notification');
var waitFor = require('test/support/wait_for');
suite('controllers/notifications', function() {
var alarm;
var db;
var sendNotification;
var storeFactory;
setup(function() {
window.mozIntl = MockMozIntl;
mockRequestWakeLock.setup();
sendNotification = sinon.spy(notification, 'sendNotification');
});
setup(function(done) {
storeFactory = core.storeFactory;
db = core.db;
db.open(done);
});
setup(function(done) {
var start = new Date();
var end = new Date();
end.setHours(end.getHours() + 1);
// Inject event, busytime, and alarm into db.
var event = Factory('event', {
_id: 'event-one',
remote: {
title: 'Birthday',
startDate: start,
endDate: end
}
});
event.description = null;
event.remote.description = null;
var busytime = Factory('busytime', {
eventId: 'event-one',
startDate: start,
endDate: end,
_id: 'busytime-one'
});
alarm = Factory('alarm', {
eventId: 'event-one',
busytimeId: 'busytime-one',
_id: 'alarm-one'
});
var eventStore = storeFactory.get('Event');
var busytimeStore = storeFactory.get('Busytime');
var alarmStore = storeFactory.get('Alarm');
Promise.all([
eventStore.persist(event),
busytimeStore.persist(busytime),
alarmStore.persist(alarm)
])
.then(() => done());
});
teardown(function() {
sendNotification.restore();
mockRequestWakeLock.teardown();
});
teardown(function(done) {
testSupport.calendar.clearStore(db, [
'events',
'busytimes',
'alarms'
], done);
});
teardown(function() {
db.close();
});
suite('#onAlarm', function() {
mochaPromise(test, 'cpu lock', function() {
controller.onAlarm(alarm);
return waitFor(function() {
var locks = mockRequestWakeLock.locks;
var lock = locks[0];
return lock && lock.type === 'cpu' && lock.unlocked;
});
});
mochaPromise(test, 'should send alarm notification', function() {
controller.onAlarm(alarm);
return waitFor(function() {
// Notably, even though our event description was null, we don't pass on
// 'null' to the notification body.
return sendNotification.calledWith(
{
id: 'alarm-started-notice',
args: { title: 'Birthday', distance: 'pretty date' }
},
{ raw: '' }
);
});
});
});
});
});

View file

@ -0,0 +1,153 @@
/**
* TODO(gareth): It would be really wonderful for the interactions
* with the account store to be real (not stubbed) but there
* is currently some bizarreness around testSupport.calendar.dbFixtures
* which needs to be addressed first.
*/
define(function(require) {
'use strict';
var createDOMPromise = require('common/create_dom_promise');
var mockAlarms = require('test/support/mock_alarms');
var periodicSync = require('controllers/periodic_sync');
suite('controllers/periodic_sync', function() {
var db, storeFactory;
setup(function(done) {
mockAlarms.setup();
var core = testSupport.calendar.core();
db = core.db;
storeFactory = core.storeFactory;
db.open(done);
});
teardown(function() {
mockAlarms.teardown();
db.close();
});
test('mozAlarms mock', function(done) {
mockAlarms.add(new Date(), 'ignoreTimezone', { type: 'sync' });
createDOMPromise(mockAlarms.getAll())
.should
.eventually
.have
.length(1)
.notify(done);
});
suite('periodic sync', function() {
var settings;
setup(function(done) {
settings = storeFactory.get('Setting');
settings.set('syncFrequency', 1).then(() => done());
});
setup(function(done) {
periodicSync.observe().then(() => done());
});
teardown(function() {
settings._clearCache();
});
teardown(function(done) {
testSupport.calendar.clearStore(db, [ 'settings' ], done);
});
teardown(function() {
periodicSync.unobserve();
});
suite('without initial caldav calendar', function() {
setup(function() {
var accounts = storeFactory.get('Account');
sinon.stub(accounts, 'syncableAccounts').returns(Promise.resolve([]));
});
teardown(function() {
var accounts = storeFactory.get('Account');
accounts.syncableAccounts.restore();
});
test('should not schedule sync initially', function(done) {
createDOMPromise(mockAlarms.getAll())
.should
.eventually
.have
.length(0)
.notify(done);
});
});
suite('with initial caldav calendar', function() {
setup(function() {
var accounts = storeFactory.get('Account');
sinon.stub(accounts, 'syncableAccounts').returns(Promise.resolve([
{ _id: 'one', providerType: 'Caldav' }
]));
});
setup(function(done) {
periodicSync.events.once('schedule', () => done());
var accounts = storeFactory.get('Account');
accounts.emit('persist');
});
teardown(function() {
var accounts = storeFactory.get('Account');
accounts.syncableAccounts.restore();
});
test('should schedule sync initially', function(done) {
createDOMPromise(mockAlarms.getAll())
.should
.eventually
.have
.length(1)
.notify(done);
});
test('sync interval change revokes previous alarm', function(done) {
var first;
periodicSync.events.once('schedule', () => {
var getAll = createDOMPromise(mockAlarms.getAll());
getAll.then(result => {
assert.lengthOf(result, 1);
var alarm = result[0];
assert.notStrictEqual(alarm.alarmId, first);
done();
});
});
var getAll = createDOMPromise(mockAlarms.getAll());
getAll.then(result => {
var alarm = result[0];
first = alarm.alarmId;
var settings = storeFactory.get('Setting');
settings.set('syncFrequency', 2);
});
});
test('removing last caldav account should stop sync', function(done) {
periodicSync.events.on('pause', () => {
var getAll = createDOMPromise(mockAlarms.getAll());
getAll.then(result => {
assert.lengthOf(result, 0);
done();
});
});
var accounts = storeFactory.get('Account');
accounts.syncableAccounts.restore();
sinon.stub(accounts, 'syncableAccounts').returns(Promise.resolve([]));
accounts.emit('remove');
});
});
});
});
});

View file

@ -0,0 +1,305 @@
define(function(require) {
'use strict';
var Factory = require('test/support/factory');
var RecurringEvents = require('controllers/recurring_events');
var Responder = require('common/responder');
var core = require('core');
var nextTick = require('common/next_tick');
suite('Controllers.RecurringEvents', function() {
var db;
var storeFactory;
var subject;
var syncController;
var timeController;
setup(function(done) {
db = core.db;
storeFactory = core.storeFactory;
syncController = core.syncController;
timeController = core.timeController;
subject = new RecurringEvents();
db.open(done);
});
teardown(function(done) {
subject.unobserve();
testSupport.calendar.clearStore(
db,
['accounts'],
function() {
done(function() {
db.close();
});
}
);
});
test('initialization', function() {
assert.instanceOf(subject, Responder);
});
test('#observe', function() {
var date = new Date(2012, 1, 1);
timeController.move(date);
var expandCall;
subject.queueExpand = function(date) {
expandCall = date;
};
subject.observe();
assert.deepEqual(expandCall, date, 'expands initial time');
});
suite('controller events', function() {
var date = new Date(2012, 1, 1);
setup(function(done) {
subject.observe();
subject.waitBeforeMove = 10;
timeController.move(date);
subject.once('expandComplete', done);
});
test('syncComplete', function(done) {
subject.queueExpand = function(date) {
done(function() {
assert.deepEqual(timeController.position, date);
});
};
syncController.emit('syncComplete');
});
/*
// These tests are currently failing and have been temporarily disabled as per
// Bug 838993. They should be fixed and re-enabled as soon as possible as per
// Bug 840489.
test('monthChange', function(done) {
var expectedDate = new Date(2012, 10, 1);
// modify wait before move for faster tests.
subject.queueExpand = function(date) {
done(function() {
assert.deepEqual(expectedDate, date);
});
};
timeController.move(new Date(2012, 8, 1));
timeController.move(new Date(2012, 9, 1));
Calendar.nextTick(function() {
timeController.move(expectedDate);
});
});
*/
});
suite('#queueExpand', function() {
var expandCalls;
var expandStartEvents;
setup(function() {
expandCalls = [];
subject.expand = function(date, cb) {
expandCalls.push([date, cb]);
};
expandStartEvents = 0;
subject.on('expandStart', function() {
expandStartEvents++;
});
});
test('multiple expansions', function(done) {
var dates = [];
subject.once('expandComplete', function() {
done(function() {
assert.equal(expandStartEvents, 1, 'calls expand once');
assert.deepEqual(
dates,
[
new Date(2012, 1, 1),
new Date(2012, 8, 1),
new Date(2012, 10, 7)
]
);
});
});
subject.expand = function(date, cb) {
dates.push(date);
nextTick(cb);
};
// should actually trigger because its the first
// item in the queue...
subject.queueExpand(new Date(2012, 1, 1));
nextTick(function() {
subject.queueExpand(new Date(2012, 7, 7));
});
// should be skipped because the next is greater and
// we are still processing the first item in th queue.
subject.queueExpand(new Date(2012, 2, 7));
// queued
subject.queueExpand(new Date(2012, 8, 1));
// should be skipped its less then others
subject.queueExpand(new Date(2012, 1, 2));
nextTick(function() {
// after the second expansion this fires
// so should the final expansion this tests
// some complicated async ordering.
subject.queueExpand(new Date(2012, 10, 7));
});
});
test('single expand', function(done) {
var date = new Date(2012, 1, 7);
subject.once('expandComplete', function() {
done(function() {
assert.ok(!subject.pending);
assert.equal(expandStartEvents, 1);
});
});
subject.queueExpand(date);
assert.ok(subject.pending);
assert.lengthOf(expandCalls, 1);
// verify right date is being expanded.
assert.deepEqual(expandCalls[0][0], date);
// trigger callback
expandCalls[0][1]();
});
});
suite('#expand', function() {
var account;
var provider;
var expandDate = new Date(2012, 1, 1);
var expectedDate;
setup(function() {
expectedDate = new Date(expandDate.valueOf());
expectedDate.setDate(expectedDate.getDate() + subject.paddingInDays);
});
function setupProvider(type, id) {
setup(function(done) {
account = Factory('account', {
providerType: type,
_id: id || type
});
provider = core.providerFactory.get(type);
storeFactory.get('Account').persist(account, done);
});
}
test('with no accounts', function(done) {
subject.expand(expandDate, done);
});
suite('provider that cannot expand', function() {
setupProvider('Local');
test('will not expand', function(done) {
provider.ensureRecurrencesExpanded = function() {
throw new Error('local should not expand');
};
subject.expand(expandDate, done);
});
});
suite('provider that can expand', function() {
// two caldav accounts to catch duplicate busytimes error (Bug 1018833)
setupProvider('Caldav', 1);
setupProvider('Caldav', 2);
// custom helper to allow each test
// to inject specific logic while sharing the
// spy/mock easily... override spyHandler to use.
var spyHandler;
// number of times ensureRecurrencesExpanded was called.
var calledEnsure;
setup(function() {
spyHandler = null;
calledEnsure = 0;
provider.ensureRecurrencesExpanded = function(date, cb) {
calledEnsure++;
// we use the same date for all tests
assert.deepEqual(date, expectedDate);
if (!spyHandler) {
setTimeout(cb, 0, null, false);
} else {
spyHandler(cb);
}
};
});
test('with no expansions required', function(done) {
subject.expand(expandDate, function() {
done(function() {
assert.equal(calledEnsure, 1, 'calls ensure');
});
});
});
test('multiple expansions required', function(done) {
// expected number of times
var requiredTries = 3;
spyHandler = function(cb) {
if (calledEnsure === requiredTries) {
// did not expand
setTimeout(cb, 0, null, false);
} else {
// did expand
setTimeout(cb, 0, null, true);
}
};
subject.expand(expandDate, function() {
done(function() {
assert.equal(calledEnsure, requiredTries, 'number of tries');
});
});
});
test('expand beyond maximum', function(done) {
spyHandler = function(cb) {
nextTick(cb.bind(this, null, true));
};
subject.expand(expandDate, function() {
done(function() {
assert.equal(calledEnsure, subject.maximumExpansions);
});
});
});
});
});
});
});

View file

@ -0,0 +1,47 @@
define(function(require) {
'use strict';
var Manager = require('worker/manager');
var ServiceController = require('controllers/service');
suite('Controllers.Service', function() {
var subject;
setup(function() {
subject = new ServiceController();
});
test('initialize', function() {
assert.instanceOf(subject, Manager);
});
test('#start', function() {
subject.start();
assert.ok(subject._ensureActiveWorker('caldav'));
});
/*
// These tests are currently failing and have been temporarily disabled as per
// Bug 838993. They should be fixed and re-enabled as soon as possible as per
// Bug 840489.
test('caldav worker', function(done) {
subject.start();
subject.request('caldav', 'noop', function(data) {
done(function() {
assert.deepEqual(data, { ready: true });
});
});
});
*/
teardown(function() {
subject.workers.forEach(function(worker) {
worker.instance.terminate();
worker.instance = null;
});
});
});
});

View file

@ -0,0 +1,307 @@
define(function(require) {
'use strict';
var CalendarError = require('common/error');
var Factory = require('test/support/factory');
var SyncController = require('controllers/sync');
var core = require('core');
var nextTick = require('common/next_tick');
suite('Controllers.Sync', function() {
var account;
var calendar;
var event;
var subject;
var db;
var accModel;
setup(function(done) {
db = core.db;
subject = new SyncController();
var storeFactory = core.storeFactory;
calendar = storeFactory.get('Calendar');
account = storeFactory.get('Account');
event = storeFactory.get('Event');
accModel = Factory('account', {
_id: 'one'
});
db.open(function(err) {
if (err) {
done(err);
return;
}
done();
});
});
teardown(function(done) {
testSupport.calendar.clearStore(
db,
['accounts', 'calendars', 'events', 'busytimes'],
done
);
});
teardown(function() {
db.close();
});
test('initialization', function() {
assert.equal(subject.pending, 0);
});
test('sync all, no accounts', function(done) {
subject.once('syncComplete', done);
subject.all();
});
test('sync all, no accounts with callback', function(done) {
subject.all(done);
});
suite('#all', function() {
var list = [];
setup(function(done) {
var trans = db.transaction('accounts', 'readwrite');
list.push(Factory('account'));
list.push(Factory('account'));
account.persist(list[0], trans);
account.persist(list[1], trans);
trans.oncomplete = function() {
done();
};
});
test('sync account', function(done) {
var calledModels = [];
var pending = 2;
function complete() {
list.forEach(function(item, idx) {
assert.hasProperties(item, calledModels[idx]);
});
}
subject.account = function(model) {
calledModels.push(model);
if (!--pending) {
done(complete);
}
};
subject.all();
});
});
suite('individual sync operations', function() {
var calendars;
var accountSyncCall;
var calendarSyncCalls;
var events;
function assertEmit(event) {
assert.isTrue(
(event in events),
'has emitted ' + event
);
}
function assertDoesNotEmit(event) {
assert.isFalse(
(event in events),
'emitted ' + event
);
}
var handler = {
handleEvent: function(event) {
events[event.type] = event.data;
}
};
setup(function(done) {
var trans = db.transaction(
['accounts', 'calendars'],
'readwrite'
);
trans.oncomplete = function() {
done();
};
// setup events
events = {};
subject.on('syncStart', handler);
subject.on('syncComplete', handler);
// setup mocks
calendarSyncCalls = [];
account.sync = function() {
accountSyncCall = Array.slice(arguments);
};
calendar.sync = function() {
calendarSyncCalls.push(Array.slice(arguments));
};
// setup db
calendars = [];
account.persist(accModel, trans);
var numberOfCalendars = 2;
while (numberOfCalendars--) {
var item = Factory('calendar', {
accountId: accModel._id
});
calendars.push(item);
calendar.persist(item, trans);
}
});
suite('#account', function() {
var err;
setup(function() {
err = new CalendarError();
account.sync = function() {
var args = Array.slice(arguments);
var cb = args.pop();
nextTick(cb.bind(this, err));
};
});
teardown(function() {
delete account.sync;
delete core.errorController.dispatch;
});
test('error without a callback', function(done) {
core.errorController.dispatch = function(given) {
done(function() {
assert.equal(subject.pending, 0);
assert.equal(err, given);
});
};
assert.equal(subject.pending, 0);
subject.account(accModel);
assert.equal(subject.pending, 1);
});
test('error with a callback', function(done) {
subject.account(accModel, function(givenErr) {
done(function() {
assert.equal(givenErr, err, 'sends error');
assert.equal(subject.pending, 0, 'pending');
});
});
});
test('success', function(done) {
var pendingCalendarSync = 2;
// verify we sync accounts
account.sync = function(account, callback) {
assert.equal(account._id, accModel._id);
assertDoesNotEmit('syncComplete');
callback();
};
var lastCalendar;
// verify we sync calendars
calendar.sync = function(acc, calendar, callback) {
assert.equal(accModel._id, acc._id);
assert.equal(calendar.accountId, acc._id);
nextTick(function() {
callback();
if (!--pendingCalendarSync) {
assert.notEqual(lastCalendar._id, calendar._id);
} else {
lastCalendar = calendar;
assertDoesNotEmit('syncComplete');
}
});
};
subject.account(accModel, function() {
done(function() {
assertEmit('syncComplete');
});
});
});
});
suite('#calendar', function() {
test('multiple in progress', function() {
subject.calendar(accModel, calendars[0]);
assertEmit('syncStart');
delete events.syncStart;
subject.calendar(accModel, calendars[1]);
assertDoesNotEmit('syncStart');
var firstSync = calendarSyncCalls.shift();
firstSync[firstSync.length - 1]();
// because there are two pending...
assertDoesNotEmit('syncComplete');
var secondSync = calendarSyncCalls.shift();
secondSync[secondSync.length - 1]();
// now both are fully completed.
assertEmit('syncComplete');
});
test('success', function(done) {
subject.calendar(accModel, calendars[0], function() {
assertEmit('syncComplete');
done();
});
assertEmit('syncStart');
assert.lengthOf(calendarSyncCalls, 1, 'emits syncComplete');
var sync = calendarSyncCalls[0];
assert.deepEqual(
sync.slice(0, 2),
[accModel, calendars[0]]
);
assertDoesNotEmit('syncComplete');
sync[sync.length - 1]();
});
test('failure', function(done) {
var sentErr = new Error();
subject.calendar(accModel, calendars[0], function(err) {
assert.equal(err, sentErr);
assertEmit('syncComplete');
done();
});
var sync = calendarSyncCalls.shift();
assert.ok(sync);
sync[sync.length - 1](sentErr);
});
});
});
});
});

View file

@ -0,0 +1,256 @@
define(function(require) {
'use strict';
var Responder = require('common/responder');
var TimeController = require('controllers/time');
var core = require('core');
window.page = window.page || {};
suite('Controllers.Time', function() {
var subject;
var busytimeStore;
var db;
setup(function(done) {
subject = new TimeController();
subject.calendarStore = {
shouldDisplayCalendar: function(id) {
return true;
},
on: function() {}
};
busytimeStore = core.storeFactory.get('Busytime');
db = core.db;
db.open(function() {
done();
});
});
teardown(function(done) {
testSupport.calendar.clearStore(
core.db,
['events', 'busytimes', 'alarms'],
done
);
});
teardown(function() {
core.db.close();
});
test('initialize', function() {
assert.instanceOf(subject, Responder);
});
suite('#handleEvent', function() {
test('switching between days', function() {
function type() {
return subject.mostRecentDayType;
}
subject.selectedDay = new Date(2012, 1, 5);
assert.deepEqual(
subject.mostRecentDay,
subject.selectedDay,
'mostRecentDay - selected day'
);
assert.equal(
type(),
'selectedDay',
'"selectedDay" change should update type'
);
subject.move(new Date(2012, 1, 10));
assert.equal(
type(), 'day',
'move - sets most recent type'
);
assert.deepEqual(
subject.mostRecentDay,
subject.position,
'mostRecentDay - day'
);
// back & forth
subject.move(new Date(2012, 1, 15));
assert.equal(type(), 'day');
subject.selectedDay = new Date(2012, 1, 20);
assert.equal(type(), 'selectedDay');
});
});
test('#scale', function() {
var calledWith;
subject.on('scaleChange', function() {
calledWith = Array.slice(arguments);
});
subject.scale = 'year';
assert.deepEqual(calledWith, ['year', null]);
calledWith = null;
subject.scale = 'year';
assert.isNull(calledWith, 'should not trigger change when value is same');
subject.scale = 'day';
assert.deepEqual(
calledWith,
['day', 'year']
);
});
test('#moveToMostRecentDay', function() {
var date = new Date();
var calledMove;
subject.move(date);
subject.move = function() {
TimeController.prototype.move.apply(this, arguments);
calledMove = arguments;
};
subject.selectedDay = new Date(2012, 1, 1);
subject.moveToMostRecentDay();
assert.equal(
calledMove[0],
subject.selectedDay,
'should move to selected day'
);
calledMove = null;
subject.moveToMostRecentDay();
assert.ok(!calledMove, 'should not move when "day" was last changed');
});
test('#selectedDay', function() {
var calledWith;
subject.on('selectedDayChange', function() {
calledWith = arguments;
});
var date = new Date(2012, 1, 1);
subject.selectedDay = date;
assert.equal(calledWith[0], date);
assert.equal(subject.selectedDay, date);
calledWith = null;
// try and set it again with same object...
subject.selectedDay = new Date(2012, 1, 1);
assert.isNull(calledWith, 'should not fire event when day is same');
var newDate = new Date(2012, 1, 2);
subject.selectedDay = newDate;
assert.equal(subject.selectedDay, newDate);
assert.equal(calledWith[0], newDate);
assert.equal(calledWith[1], date);
});
suite('#move', function() {
var events;
var date;
function clearEvents() {
events = {
year: [],
month: [],
day: []
};
}
setup(function() {
clearEvents();
date = new Date(2012, 5, 5);
var handle = {
handleEvent: function(e) {
var name = e.type.replace('Change', '');
events[name].push(e.data[0]);
}
};
subject.on('yearChange', handle);
subject.on('monthChange', handle);
subject.on('dayChange', handle);
subject.move(date);
});
function fires(type, value) {
var eventValue = events[type][0];
assert.deepEqual(
eventValue, value, 'fires: ' + type + ' change'
);
assert.deepEqual(
subject[type],
value,
'should update .' + type
);
}
function doesNotFire(type) {
assert.deepEqual(
events[type],
[]
);
}
test('initial move', function() {
fires('year', new Date(2012, 0, 1));
fires('month', new Date(2012, 5, 1));
fires('day', new Date(2012, 5, 5));
});
test('move day', function() {
clearEvents();
subject.move(new Date(2012, 5, 6));
doesNotFire('year');
doesNotFire('month');
fires('day', new Date(2012, 5, 6));
});
test('move month', function() {
clearEvents();
subject.move(new Date(2012, 8, 6));
doesNotFire('year');
fires('month', new Date(2012, 8, 1));
fires('day', new Date(2012, 8, 6));
});
test('move into the past', function() {
clearEvents();
subject.move(new Date(2011, 5, 4));
fires('year', new Date(2011, 0, 1));
fires('month', new Date(2011, 5, 1));
fires('day', new Date(2011, 5, 4));
});
});
});
});

View file

@ -0,0 +1,355 @@
define(function(require) {
'use strict';
var Factory = require('test/support/factory');
var core = require('core');
var dayObserver = require('day_observer');
suite('day_observer', function() {
var busyToday1, busyToday2, busyTomorrow, busyTodayAllday;
var event1, event2, event3, event4;
var calendar, hiddenCalendar;
var calendarColor = '#00aacc';
var hiddenCalendarColor = '#BADA55';
var busytimeStore, calendarStore, eventStore;
var subject;
var today;
var tomorrow;
var yesterday;
var oneHour = 60 * 60 * 1000;
suiteSetup(function(done) {
subject = dayObserver;
var storeFactory = core.storeFactory;
eventStore = storeFactory.get('Event');
busytimeStore = storeFactory.get('Busytime');
calendarStore = storeFactory.get('Calendar');
today = new Date();
today.setHours(8, 0, 0, 0);
// Set the date to the 15th, so it's in the middle of the month.
// This helps avoid bug 1128172.
var DAY = 15;
today.setDate(DAY);
yesterday = new Date();
yesterday.setHours(8, 0, 0, 0);
yesterday.setDate(DAY - 1);
tomorrow = new Date();
tomorrow.setHours(8, 0, 0, 0);
tomorrow.setDate(DAY + 1);
core.db.open(done);
});
suiteSetup(function(done) {
calendarStore.all((err) => {
done(err);
});
});
// we add the events to the DB before calling subject.init() on purpose, just
// to simulate the app start
suiteSetup(function(done) {
calendar = Factory('calendar', {
localDisplayed: true,
color: calendarColor
});
hiddenCalendar = Factory('calendar', {
localDisplayed: false,
color: hiddenCalendarColor
});
var calendarId = calendar._id;
event1 = Factory('event', {
_id: 'foo',
calendarId: calendarId
});
event2 = Factory('event', {
_id: 'bar',
calendarId: calendarId
});
event3 = Factory('event', {
_id: 'baz',
calendarId: calendarId
});
event4 = Factory('event', {
_id: 'bag',
calendarId: calendarId
});
busyToday1 = Factory('busytime', {
calendarId: calendarId,
eventId: event1._id,
startDate: today
});
// tests events that spans thru multiple days
busyToday2 = Factory('busytime', {
calendarId: calendarId,
eventId: event2._id,
startDate: new Date(today.getTime() + oneHour),
endDate: tomorrow
});
busyTomorrow = Factory('busytime', {
calendarId: calendarId,
eventId: event3._id,
startDate: tomorrow
});
var todayStart = new Date(today.getTime());
todayStart.setHours(0, 0, 0, 0);
var tomorrowStart = new Date(todayStart.getTime());
tomorrowStart.setHours(24);
busyTodayAllday = Factory('busytime', {
calendarId: calendarId,
eventId: event4._id,
startDate: todayStart,
endDate: tomorrowStart
});
// events should be persisted before the busytimes
Promise.all([
calendarStore.persist(calendar),
calendarStore.persist(hiddenCalendar),
eventStore.persist(event1),
eventStore.persist(event2),
eventStore.persist(event3),
eventStore.persist(event4)
]).then(function() {
Promise.all([
busytimeStore.persist(busyToday1),
busytimeStore.persist(busyToday2),
busytimeStore.persist(busyTomorrow),
busytimeStore.persist(busyTodayAllday)
]).then(function() {
subject.init();
done();
});
});
});
suiteTeardown(function(done) {
testSupport.calendar.clearStore(
core.db,
['events', 'busytimes', 'calendars'],
done
);
});
suiteTeardown(function() {
core.db.close();
});
teardown(function() {
subject.removeAllListeners();
});
suite('empty cache', function() {
test('#on: today', function(done) {
subject.on(today, function(records) {
assert.equal(records.amount, 0);
assert.deepEqual(records.basic, []);
assert.deepEqual(records.allday, []);
done();
});
});
test('#findAssociated', function(done) {
subject.findAssociated(busyTomorrow._id).then(function(record) {
assert.deepEqual(record.busytime, busyTomorrow);
assert.deepEqual(record.event, event3);
done();
});
});
});
suite('cached', function() {
suiteSetup(function(done) {
// make sure we wait enough time until all records are loaded from DB
function onRecords(records) {
if (records.amount > 0) {
subject.off(today, onRecords);
done();
}
}
// ensures 'monthChange' triggers the load of busytimes, we are not
// calling `move()` because this is more flexible and less error prone
core.timeController.emit('monthChange', today);
subject.on(today, onRecords);
});
test('#on: today', function(done) {
subject.on(today, function(records) {
assert.equal(records.amount, 3);
assert.deepEqual(records.basic, [
{
event: event1,
busytime: busyToday1,
color: calendarColor
},
{
event: event2,
busytime: busyToday2,
color: calendarColor
}
]);
assert.deepEqual(records.allday, [
{
event: event4,
busytime: busyTodayAllday,
color: calendarColor
}
]);
done();
});
});
test('#on: tomorrow', function(done) {
subject.on(tomorrow, function(records) {
assert.equal(records.amount, 2);
assert.deepEqual(records.basic, [
{
event: event2,
busytime: busyToday2,
color: calendarColor
},
{
event: event3,
busytime: busyTomorrow,
color: calendarColor
}
]);
assert.deepEqual(records.allday, []);
done();
});
});
test('#on: yesterday', function(done) {
subject.on(yesterday, function(records) {
assert.equal(records.amount, 0);
assert.deepEqual(records.basic, []);
assert.deepEqual(records.allday, []);
done();
});
});
test('#on: yesterday + persist + remove + visibility', function(done) {
/* disabled due to intermittent failures see bug 1128275 */
/* jshint -W027 */
return done();
var count = 0;
var busyYesterday = Factory('busytime', {
calendarId: calendar._id,
eventId: event3._id,
startDate: yesterday
});
var busyYesterday2 = Factory('busytime', {
calendarId: calendar._id,
eventId: event4._id,
startDate: new Date(yesterday.getTime() + oneHour)
});
// events from hidden calendars should not be displayed
var busyYesterdayHidden = Factory('busytime', {
calendarId: hiddenCalendar._id,
eventId: event4._id,
startDate: new Date(yesterday.getTime() + oneHour)
});
subject.on(yesterday, function(records) {
count += 1;
// it should call it once without the new events to make sure our views
// are "eventually" in sync
if (count === 1) {
assert.equal(records.amount, 0, 'first');
assert.deepEqual(records.basic, []);
assert.deepEqual(records.allday, []);
return;
}
if (count === 2) {
if (records.amount === 1) {
// busytimeStore.persist is async and might take longer than
// a single dispatch to display all the busytimes, so we make sure
// we only bump the count if it really updated the value to match
// the expected result. that is enough to prove that UI will
// "eventually" reflect the correct amount of items. (Bug 1115083)
count -= 1;
return;
}
assert.equal(records.amount, 2, 'after persist');
assert.deepEqual(records.basic, [
{
event: event3,
busytime: busyYesterday,
color: calendarColor
},
{
event: event4,
busytime: busyYesterday2,
color: calendarColor
}
]);
assert.deepEqual(records.allday, []);
// remove busytime
busytimeStore.remove(busyYesterday2._id);
return;
}
if (count === 3) {
assert.equal(records.amount, 1, 'after remove');
assert.deepEqual(records.basic, [
{
event: event3,
busytime: busyYesterday,
color: calendarColor
}
]);
assert.deepEqual(records.allday, []);
// toggle calendar visibility
calendarStore.persist(Factory('calendar', {
_id: hiddenCalendar._id,
localDisplayed: true
}));
return;
}
assert.equal(records.amount, 2, 'after calendar visible');
assert.deepEqual(records.basic, [
{
event: event3,
busytime: busyYesterday,
color: calendarColor
},
{
event: event4,
busytime: busyYesterdayHidden,
color: calendarColor
}
]);
assert.deepEqual(records.allday, []);
done();
});
busytimeStore.persist(busyYesterday);
busytimeStore.persist(busyYesterday2);
busytimeStore.persist(busyYesterdayHidden);
});
test('#findAssociated', function(done) {
subject.findAssociated(busyToday1._id).then(function(record) {
assert.deepEqual(record.busytime, busyToday1);
assert.deepEqual(record.event, event1);
done();
});
});
});
});
});

View file

@ -0,0 +1,529 @@
define(function(require) {
'use strict';
var Db = require('db');
var Factory = require('test/support/factory');
var Responder = require('common/responder');
var core = require('core');
suite('db', function() {
var subject;
var storeFactory;
setup(function(done) {
var db = core.db;
db.deleteDatabase(function(err, success) {
assert.ok(!err, 'should not have an error when deleting db');
assert.ok(success, 'should be able to delete the db');
done();
});
});
teardown(function() {
subject.close();
});
setup(function() {
subject = core.db;
storeFactory = core.storeFactory;
});
test('initialization', function() {
// create test db
assert.ok(subject.name);
assert.include(subject.name, 'test');
assert.ok(subject.store);
assert.instanceOf(subject, Responder);
assert.isTrue(Object.isFrozen(subject.store));
});
suite('#transaction', function() {
setup(done => subject.open(done));
test('result', function(done) {
var trans = subject.transaction(['events'], 'readonly');
assert.equal(trans.mode, 'readonly');
trans.onabort = function() {
done();
};
trans.abort();
});
});
suite('#open', function() {
suite('on version change', function() {
suite('#setupDefaults', function() {
var accountStore;
var calendarStore;
var storeLoads;
setup(function() {
storeLoads = {};
});
setup(function(done) {
accountStore = storeFactory.get('Account');
calendarStore = storeFactory.get('Calendar');
subject.load(done);
});
['Calendar', 'Account'].forEach(function(storeName) {
setup(function(done) {
var store = storeFactory.get(storeName);
var humanName = storeName.toLowerCase() + 's';
store.all(function(err, all) {
storeLoads[humanName] = all;
done();
});
});
});
test('default account', function() {
var list = Object.keys(storeLoads.accounts);
assert.lengthOf(list, 1);
var item = storeLoads.accounts[list[0]];
assert.ok(item);
assert.equal(item.providerType, 'Local', 'provider');
assert.equal(item.preset, 'local', 'preset');
});
test('default calendar', function() {
var list = Object.keys(storeLoads.calendars);
assert.lengthOf(list, 1);
var item = storeLoads.calendars[list[0]];
assert.ok(item);
assert.equal(item.remote.name, 'Offline calendar');
var account = storeLoads.accounts[item.accountId];
assert.ok(account, 'has account');
assert.equal(account.providerType, 'Local');
});
});
test('creation of stores', function(done) {
var finishedOpen = false;
assert.ok(!subject.connection, 'connection should be closed');
subject.on('open', function onopen() {
subject.off('open', onopen);
if (!finishedOpen) {
done(new Error(
'fired callback/event out of order ' +
'callback should fire then events'
));
} else {
done(function() {
// check that each store now exists
var stores = subject.connection.objectStoreNames;
var actualStore;
for (actualStore in subject.store) {
assert.ok(
(stores.contains(actualStore)),
actualStore + ' was not created'
);
}
});
subject.close();
}
});
subject.open(function() {
assert.ok(subject.connection, 'has connection');
assert.ok(subject.isOpen, 'is open');
assert.ok(subject.version, 'has version');
assert.equal(subject.oldVersion, 0, 'upgraded from 0');
assert.isTrue(subject.hasUpgraded, 'has upgraded');
assert.equal(subject.connection.name, subject.name);
finishedOpen = true;
});
});
suite('Bug 887698', function() {
/**
* @type {Calendar.Store}
*/
var busytimeStore, calendarStore, eventStore;
/**
* Keys for test events.
* @const {number}
*/
var EVENT_ONE_ID, EVENT_TWO_ID, EVENT_THREE_ID;
/**
* Keys for test busytimes.
* @const {number}
*/
var BUSYTIME_ONE_ID, BUSYTIME_TWO_ID, BUSYTIME_THREE_ID;
/**
* The version immediately before we fix the corrupt calendarIds.
* @const {number}
*/
var OLD_VERSION;
var LOCAL_CALENDAR;
var NEW_VERSION;
setup(function(done) {
// 13 is the version for v1.0.1
OLD_VERSION = 13;
// 15 is the version for v1.1
NEW_VERSION = 15;
LOCAL_CALENDAR = 'local-first';
// These need their calendarId fixed.
EVENT_ONE_ID = 'evt1';
BUSYTIME_ONE_ID = 'bt1';
// These are fine as is.
EVENT_TWO_ID = 'evt2';
BUSYTIME_TWO_ID = 'bt2';
// These need to be deleted.
EVENT_THREE_ID = 'evt3';
BUSYTIME_THREE_ID = 'bt3';
busytimeStore = storeFactory.get('Busytime');
calendarStore = storeFactory.get('Calendar');
eventStore = storeFactory.get('Event');
subject.open(OLD_VERSION, function() {
var trans = null;
calendarStore.persist(Factory('calendar',
{ _id: LOCAL_CALENDAR }), trans);
[
Factory('event', {
calendarId: LOCAL_CALENDAR,
_id: EVENT_ONE_ID
}),
Factory('event', {
calendarId: 2,
_id: EVENT_TWO_ID
}),
Factory('event', {
calendarId: '3',
_id: EVENT_THREE_ID
})
].forEach(function(obj) {
eventStore.persist(obj, trans);
});
[
Factory('busytime', {
eventId: EVENT_ONE_ID,
calendarId: LOCAL_CALENDAR,
_id: BUSYTIME_ONE_ID
}),
Factory('busytime', {
eventId: EVENT_TWO_ID,
calendarId: 2,
_id: BUSYTIME_TWO_ID
}),
Factory('busytime', {
eventId: EVENT_THREE_ID,
calendarId: '3',
_id: BUSYTIME_THREE_ID
})
].forEach(function(obj) {
busytimeStore.persist(obj, trans);
});
trans = subject.transaction(
['busytimes', 'calendars', 'events'],
'readwrite'
);
trans.oncomplete = function() {
subject.close();
done();
};
});
});
setup(function(done) {
subject.open(NEW_VERSION, done);
});
teardown(function() {
if (subject.isOpen) {
subject.close();
}
});
test('should turn event str calendarId into int', function(done) {
var trans = subject.transaction(['events'], 'readwrite');
var store = trans.objectStore('events');
store.get(EVENT_ONE_ID).onsuccess = function(evt) {
assert.strictEqual(evt.target.result.calendarId, LOCAL_CALENDAR);
done();
};
});
test('should not modify event int calendarId', function(done) {
var trans = subject.transaction(['events'], 'readwrite');
var store = trans.objectStore('events');
store.get(EVENT_TWO_ID).onsuccess = function(evt) {
assert.strictEqual(evt.target.result.calendarId, 2);
done();
};
});
test('should delete event if its calendar died', function(done) {
var trans = subject.transaction(['events'], 'readwrite');
var store = trans.objectStore('events');
store.get(EVENT_THREE_ID).onsuccess = function(evt) {
assert.strictEqual(evt.target.result, undefined);
done();
};
});
test('should turn busytime str calendarId into int', function(done) {
var trans = subject.transaction(['busytimes'], 'readwrite');
var store = trans.objectStore('busytimes');
store.get(BUSYTIME_ONE_ID).onsuccess = function(evt) {
assert.strictEqual(evt.target.result.calendarId, LOCAL_CALENDAR);
done();
};
});
test('should not modify busytime int calendarId', function(done) {
var trans = subject.transaction(['busytimes'], 'readwrite');
var store = trans.objectStore('busytimes');
store.get(BUSYTIME_TWO_ID).onsuccess = function(evt) {
assert.strictEqual(evt.target.result.calendarId, 2);
done();
};
});
test('should delete busytime if its calendar died', function(done) {
var trans = subject.transaction(['busytimes'], 'readwrite');
var store = trans.objectStore('busytimes');
store.get(BUSYTIME_THREE_ID).onsuccess = function(evt) {
assert.strictEqual(evt.target.result, undefined);
done();
};
});
});
suite('Bug 851003', function() {
/*jshint -W027 */
/** bug 912087: renable once tests pass consistently */
return;
/**
* @type {Calendar.Store}
*/
var busytimeStore, calendarStore, eventStore;
/**
* Keys for test events.
* @const {number}
*/
var EVENT_ONE_ID, EVENT_TWO_ID, EVENT_THREE_ID;
/**
* Keys for test busytimes.
* @const {number}
*/
var BUSYTIME_ONE_ID, BUSYTIME_TWO_ID, BUSYTIME_THREE_ID;
/**
* The version immediately before we fix the corrupt calendarIds.
* @const {number}
*/
var OLD_VERSION;
setup(function(done) {
OLD_VERSION = 14;
// These need their calendarId fixed.
EVENT_ONE_ID = 'evt1';
BUSYTIME_ONE_ID = 'bt1';
// These are fine as is.
EVENT_TWO_ID = 'evt2';
BUSYTIME_TWO_ID = 'bt2';
// These need to be deleted.
EVENT_THREE_ID = 'evt3';
BUSYTIME_THREE_ID = 'bt3';
busytimeStore = storeFactory.get('Busytime');
calendarStore = storeFactory.get('Calendar');
eventStore = storeFactory.get('Event');
subject.open(OLD_VERSION, function() {
var trans = null;
calendarStore.persist(Factory('calendar', { _id: 1 }), trans);
[
Factory('event', {
calendarId: '1',
_id: EVENT_ONE_ID
}),
Factory('event', {
calendarId: 2,
_id: EVENT_TWO_ID
}),
Factory('event', {
calendarId: '3',
_id: EVENT_THREE_ID
})
].forEach(function(obj) {
eventStore.persist(obj, trans);
});
[
Factory('busytime', {
eventId: EVENT_ONE_ID,
calendarId: '1',
_id: BUSYTIME_ONE_ID
}),
Factory('busytime', {
eventId: EVENT_TWO_ID,
calendarId: 2,
_id: BUSYTIME_TWO_ID
}),
Factory('busytime', {
eventId: EVENT_THREE_ID,
calendarId: '3',
_id: BUSYTIME_THREE_ID
})
].forEach(function(obj) {
busytimeStore.persist(obj, trans);
});
trans = subject.transaction(
['busytimes', 'calendars', 'events'],
'readwrite'
);
trans.oncomplete = function() {
subject.close();
done();
};
});
});
teardown(function() {
if (subject.isOpen) {
subject.close();
}
});
test('should turn event str calendarId into int', function(done) {
subject.open(OLD_VERSION + 1, function() {
var trans = subject.transaction(['events'], 'readwrite');
var store = trans.objectStore('events');
store.get(EVENT_ONE_ID).onsuccess = function(evt) {
assert.strictEqual(evt.target.result.calendarId, 1);
done();
};
});
});
test('should not modify event int calendarId', function(done) {
subject.open(OLD_VERSION + 1, function() {
var trans = subject.transaction(['events'], 'readwrite');
var store = trans.objectStore('events');
store.get(EVENT_TWO_ID).onsuccess = function(evt) {
assert.strictEqual(evt.target.result.calendarId, 2);
done();
};
});
});
test('should delete event if its calendar died', function(done) {
subject.open(OLD_VERSION + 1, function() {
var trans = subject.transaction(['events'], 'readwrite');
var store = trans.objectStore('events');
store.get(EVENT_THREE_ID).onsuccess = function(evt) {
assert.strictEqual(evt.target.result, undefined);
done();
};
});
});
test('should turn busytime str calendarId into int', function(done) {
subject.open(OLD_VERSION + 1, function() {
var trans = subject.transaction(['busytimes'], 'readwrite');
var store = trans.objectStore('busytimes');
store.get(BUSYTIME_ONE_ID).onsuccess = function(evt) {
assert.strictEqual(evt.target.result.calendarId, 1);
done();
};
});
});
test('should not modify busytime int calendarId', function(done) {
subject.open(OLD_VERSION + 1, function() {
var trans = subject.transaction(['busytimes'], 'readwrite');
var store = trans.objectStore('busytimes');
store.get(BUSYTIME_TWO_ID).onsuccess = function(evt) {
assert.strictEqual(evt.target.result.calendarId, 2);
done();
};
});
});
test('should delete busytime if its calendar died', function(done) {
subject.open(OLD_VERSION + 1, function() {
var trans = subject.transaction(['busytimes'], 'readwrite');
var store = trans.objectStore('busytimes');
store.get(BUSYTIME_THREE_ID).onsuccess = function(evt) {
assert.strictEqual(evt.target.result, undefined);
done();
};
});
});
});
});
suite('after version change', function() {
teardown(function() {
subject.close();
});
setup(function(done) {
// make sure db is open
subject.open(function() {
done();
});
});
test('open', function(done) {
// close it
subject.close();
subject = new Db(subject.name);
subject.open(function() {
done();
});
});
});
});
});
});

View file

@ -0,0 +1,237 @@
define(function(require) {
'use strict';
var Calc = require('common/calc');
var EventMutations = require('event_mutations');
var Factory = require('test/support/factory');
var core = require('core');
suite('EventMutations', function() {
var subject;
var db;
var controller;
var eventStore;
var busytimeStore;
var alarmStore;
var componentStore;
var shouldDisplay;
var storeFactory;
setup(function(done) {
subject = EventMutations;
db = core.db;
controller = core.timeController;
shouldDisplay = controller._shouldDisplayBusytime;
controller._shouldDisplayBusytime = function() {
return true;
};
storeFactory = core.storeFactory;
eventStore = storeFactory.get('Event');
busytimeStore = storeFactory.get('Busytime');
alarmStore = storeFactory.get('Alarm');
componentStore = storeFactory.get('IcalComponent');
db.open(done);
});
teardown(function(done) {
controller._shouldDisplayBusytime = shouldDisplay;
testSupport.calendar.clearStore(
db,
[
'events',
'busytimes',
'alarms',
'icalComponents'
],
function() {
db.close();
done();
}
);
});
suite('#create', function() {
var event;
var component;
var mutation;
setup(function(done) {
event = Factory('event');
component = Factory('icalComponent', {
eventId: event._id
});
// Set the event to start and end in the past
event.remote.start = Calc.dateToTransport(
new Date(Date.now() - 2 * 60 * 60 * 1000)
);
// Ending one hour in the future
event.remote.end = Calc.dateToTransport(
new Date(Date.now() - 1 * 60 * 60 * 1000)
);
mutation = subject.create({
event: event,
icalComponent: component
});
mutation.commit(done);
});
test('event', function(done) {
eventStore.get(event._id, function(err, value) {
done(function() {
assert.ok(value);
});
});
});
test('busytime', function(done) {
var busytime = mutation.busytime;
assert.ok(busytime._id, 'has _id');
busytimeStore.get(busytime._id, function(err, value) {
done(function() {
assert.hasProperties(value, {
eventId: event._id,
calendarId: event.calendarId,
start: event.remote.start,
end: event.remote.end
});
});
});
});
test('alarms', function(done) {
var expectedAlarms = [];
var busyId = mutation.busytime._id;
alarmStore.findAllByBusytimeId(busyId, function(err, values) {
done(function() {
assert.equal(values.length, expectedAlarms.length);
});
});
});
test('icalComponent', function(done) {
componentStore.get(event._id, function(err, value) {
done(function() {
assert.deepEqual(value, component);
});
});
});
});
suite('#update', function() {
var event;
var component;
setup(function(done) {
event = Factory('event');
component = Factory('icalComponent', {
eventId: event._id
});
var create = subject.create({
event: event,
icalComponent: component
});
create.commit(done);
});
var mutation;
setup(function(done) {
event.remote.foo = true;
// Starting one hour in the past
event.remote.start = Calc.dateToTransport(
new Date(Date.now() - 1 * 60 * 60 * 1000)
);
// Ending one hour in the future
event.remote.end = Calc.dateToTransport(
new Date(Date.now() + 1 * 60 * 60 * 1000)
);
var futureTrigger = Date.now() - event.remote.start.utc + 5000;
event.remote.alarms = [
{action: 'DISPLAY', trigger: 60},
{action: 'DISPLAY', trigger: 300},
// Create an alarm in the future
{action: 'DISPLAY', trigger: futureTrigger}
];
component.data = { changed: true };
mutation = subject.update({
event: event,
icalComponent: component
});
mutation.commit(done);
});
test('event', function(done) {
eventStore.get(event._id, function(err, value) {
done(function() {
assert.hasProperties(
value.remote,
{
start: event.remote.start,
end: event.remote.end
}
);
});
});
});
test('busytime', function(done) {
busytimeStore.get(mutation.busytime._id, function(err, value) {
done(function() {
assert.hasProperties(value, {
eventId: event._id,
start: event.remote.start,
end: event.remote.end
});
});
});
});
test('alarms', function(done) {
var expectedAlarms = event.remote.alarms;
var busyId = mutation.busytime._id;
alarmStore.findAllByBusytimeId(busyId, function(err, values) {
done(function() {
assert.equal(values.length, expectedAlarms.length);
for (var i = 0; i < expectedAlarms.length; i++) {
assert.equal(
event.remote.start.utc + expectedAlarms[i].trigger * 1000,
values[i].trigger.utc
);
}
});
});
});
test('icalComponent', function(done) {
componentStore.get(event._id, function(err, value) {
done(function() {
assert.deepEqual(value, component);
});
});
});
});
});
});

View file

@ -0,0 +1,42 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:Zimbra-Calendar-Provider
BEGIN:VTIMEZONE
TZID:America/Los_Angeles
BEGIN:STANDARD
DTSTART:19710101T020000
TZOFFSETTO:-0800
TZOFFSETFROM:-0700
RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=11;BYDAY=1SU
TZNAME:PST
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:19710101T020000
TZOFFSETTO:-0700
TZOFFSETFROM:-0800
RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=3;BYDAY=2SU
TZNAME:PDT
END:DAYLIGHT
END:VTIMEZONE
BEGIN:VEVENT
UID:623c13c0-6c2b-45d6-a12b-c33ad61c4868
DESCRIPTION:IAM FOO
RRULE:FREQ=DAILY;BYDAY=TU
SUMMARY:Crazy Event Thingy!
LOCATION:PLACE
ORGANIZER;CN=James Lal:mailto:jlal@mozilla.com
DTSTART;TZID=America/Los_Angeles:20181002T100000
DTEND;TZID=America/Los_Angeles:20181002T103000
STATUS:CONFIRMED
CLASS:PUBLIC
TRANSP:OPAQUE
LAST-MODIFIED:20180912T171506Z
DTSTAMP:20180912T171506Z
SEQUENCE:0
BEGIN:VALARM
ACTION:DISPLAY
TRIGGER;RELATED=START:-PT5M
DESCRIPTION:Reminder
END:VALARM
END:VEVENT
END:VCALENDAR

View file

@ -0,0 +1,41 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:Zimbra-Calendar-Provider
BEGIN:VTIMEZONE
TZID:America/Los_Angeles
BEGIN:STANDARD
DTSTART:19710101T020000
TZOFFSETTO:-0800
TZOFFSETFROM:-0700
RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=11;BYDAY=1SU
TZNAME:PST
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:19710101T020000
TZOFFSETTO:-0700
TZOFFSETFROM:-0800
TZNAME:PDT
END:DAYLIGHT
END:VTIMEZONE
BEGIN:VEVENT
UID:623c13c0-6c2b-45d6-a12b-c33ad61c4868
DESCRIPTION:IAM FOO
RRULE:FREQ=MINUTELY;INTERVAL=45;UNTIL=20121005T100000
SUMMARY:Crazy Event Thingy!
LOCATION:PLACE
ORGANIZER;CN=James Lal:mailto:jlal@mozilla.com
DTSTART;TZID=America/Los_Angeles:20121002T100000
DTEND;TZID=America/Los_Angeles:20121002T103000
STATUS:CONFIRMED
CLASS:PUBLIC
TRANSP:OPAQUE
LAST-MODIFIED:20120912T171506Z
DTSTAMP:20120912T171506Z
SEQUENCE:0
BEGIN:VALARM
ACTION:DISPLAY
TRIGGER;RELATED=START:-PT5M
DESCRIPTION:Reminder
END:VALARM
END:VEVENT
END:VCALENDAR

View file

@ -0,0 +1,94 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:Zimbra-Calendar-Provider
BEGIN:VTIMEZONE
TZID:America/Los_Angeles
BEGIN:STANDARD
DTSTART:19710101T020000
TZOFFSETTO:-0800
TZOFFSETFROM:-0700
RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=11;BYDAY=1SU
TZNAME:PST
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:19710101T020000
TZOFFSETTO:-0700
TZOFFSETFROM:-0800
RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=3;BYDAY=2SU
TZNAME:PDT
END:DAYLIGHT
END:VTIMEZONE
BEGIN:VEVENT
UID:623c13c0-6c2b-45d6-a12b-c33ad61c4868
DESCRIPTION:IAM FOO
RRULE:FREQ=MONTHLY;INTERVAL=1;BYDAY=1TU
SUMMARY:Crazy Event Thingy!
ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=Sahaja
Lal;X-NUM-GUESTS=0:mailto:calmozilla1@gmail.com
ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN=ja
mes@lightsofapollo.com;X-NUM-GUESTS=0:mailto:james@lightsofapollo.com
ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN=ia
m.revelation@gmail.com;X-NUM-GUESTS=0:mailto:iam.revelation@gmail.com
LOCATION:PLACE
ORGANIZER;CN=James Lal:mailto:jlal@mozilla.com
DTSTART;TZID=America/Los_Angeles:20121002T100000
DTEND;TZID=America/Los_Angeles:20121002T103000
STATUS:CONFIRMED
CLASS:PUBLIC
TRANSP:OPAQUE
LAST-MODIFIED:20120912T171506Z
DTSTAMP:20120912T171506Z
SEQUENCE:0
RDATE;TZID=America/Los_Angeles:20121110T100000
RDATE;TZID=America/Los_Angeles:20121105T100000
EXDATE;TZID=America/Los_Angeles:20130402T100000
EXDATE;TZID=America/Los_Angeles:20121204T100000
EXDATE;TZID=America/Los_Angeles:20130205T100000
BEGIN:VALARM
ACTION:DISPLAY
TRIGGER;RELATED=START:-PT5M
DESCRIPTION:Reminder
END:VALARM
END:VEVENT
BEGIN:VEVENT
UID:623c13c0-6c2b-45d6-a12b-c33ad61c4868
SUMMARY:Crazy Event Thingy!
DESCRIPTION:I HAZ CHANGED!
ORGANIZER;CN=James Lal:mailto:jlal@mozilla.com
DTSTART;TZID=America/Los_Angeles:20121002T150000
DTEND;TZID=America/Los_Angeles:20121002T153000
STATUS:CONFIRMED
CLASS:PUBLIC
TRANSP:OPAQUE
RECURRENCE-ID;TZID=America/Los_Angeles:20121002T100000
LAST-MODIFIED:20120912T171540Z
DTSTAMP:20120912T171540Z
SEQUENCE:1
BEGIN:VALARM
ACTION:DISPLAY
TRIGGER;RELATED=START:-PT5M
DESCRIPTION:Reminder
END:VALARM
END:VEVENT
BEGIN:VEVENT
UID:623c13c0-6c2b-45d6-a12b-c33ad61c4868
SUMMARY:Crazy Event Thingy!
ORGANIZER;CN=James Lal:mailto:jlal@mozilla.com
DTSTART;TZID=America/Los_Angeles:20121106T200000
DTEND;TZID=America/Los_Angeles:20121106T203000
STATUS:CONFIRMED
CLASS:PUBLIC
TRANSP:OPAQUE
RECURRENCE-ID;TZID=America/Los_Angeles:20121106T100000
LAST-MODIFIED:20120912T171820Z
DTSTAMP:20120912T171820Z
SEQUENCE:1
BEGIN:VALARM
ACTION:DISPLAY
TRIGGER;RELATED=START:-PT5M
DESCRIPTION:Reminder
END:VALARM
END:VEVENT
END:VCALENDAR

View file

@ -0,0 +1,116 @@
BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
X-WR-CALNAME:kevingrandon@yahoo.com
X-WR-TIMEZONE:America/Los_Angeles
BEGIN:VTIMEZONE
TZID:America/Los_Angeles
X-LIC-LOCATION:America/Los_Angeles
BEGIN:DAYLIGHT
TZOFFSETFROM:-0800
TZOFFSETTO:-0700
TZNAME:PDT
DTSTART:19700308T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:-0700
TZOFFSETTO:-0800
TZNAME:PST
DTSTART:19701101T020000
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;TZID=America/Los_Angeles:20130311T070000
DTEND;TZID=America/Los_Angeles:20130311T093000
RRULE:FREQ=WEEKLY;COUNT=10;BYDAY=MO,WE,FR
DTSTAMP:20130315T195941Z
UID:1ptfstkbsqn5lfrrpc3igiun98@google.com
CREATED:20130315T195941Z
DESCRIPTION:
LAST-MODIFIED:20130315T195941Z
LOCATION:
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:Hello Event!
TRANSP:OPAQUE
BEGIN:VALARM
ACTION:DISPLAY
DESCRIPTION:This is an event reminder
TRIGGER;VALUE=DATE-TIME:20120620T233000Z
END:VALARM
BEGIN:VALARM
ACTION:EMAIL
DESCRIPTION:This is an event reminder
SUMMARY:Alarm notification
ATTENDEE:mailto:kgrandon@mozilla.com
TRIGGER:-P0DT0H10M0S
END:VALARM
BEGIN:VALARM
ACTION:DISPLAY
DESCRIPTION:This is an event reminder
TRIGGER:-P0DT0H30M0S
END:VALARM
END:VEVENT
BEGIN:VEVENT
DTSTART;TZID=America/Los_Angeles:20130313T100000
DTEND;TZID=America/Los_Angeles:20130313T123000
DTSTAMP:20130315T195946Z
UID:1ptfstkbsqn5lfrrpc3igiun98@google.com
RECURRENCE-ID;TZID=America/Los_Angeles:20130313T070000
CREATED:20130315T195941Z
DESCRIPTION:
LAST-MODIFIED:20130315T195946Z
LOCATION:
SEQUENCE:1
STATUS:CONFIRMED
SUMMARY:Hello Event!
TRANSP:OPAQUE
BEGIN:VALARM
ACTION:EMAIL
DESCRIPTION:This is an event reminder
SUMMARY:Alarm notification
ATTENDEE:mailto:kgrandon@mozilla.com
TRIGGER:-P0DT0H10M0S
END:VALARM
BEGIN:VALARM
ACTION:DISPLAY
DESCRIPTION:This is an event reminder
TRIGGER:-P0DT0H30M0S
END:VALARM
BEGIN:VALARM
ACTION:DISPLAY
DESCRIPTION:This is an event reminder
TRIGGER;VALUE=DATE-TIME:20120620T233000Z
END:VALARM
END:VEVENT
BEGIN:VEVENT
DTSTART;TZID=America/Los_Angeles:20130313T100000
DTEND;TZID=America/Los_Angeles:20130313T123000
DTSTAMP:20130315T195946Z
UID:1ptfstkbsqn5lfrrpc3igiun98@google.com
RECURRENCE-ID;TZID=America/Los_Angeles:20130313T070000
CREATED:20130315T195941Z
DESCRIPTION:
LAST-MODIFIED:20130315T195946Z
LOCATION:
SEQUENCE:1
STATUS:CONFIRMED
SUMMARY:Hello Event!
TRANSP:OPAQUE
BEGIN:VALARM
ACTION:EMAIL
DESCRIPTION:This is an event reminder
SUMMARY:Alarm notification
ATTENDEE:mailto:kgrandon@mozilla.com
TRIGGER:-P0DT0H10M0S
END:VALARM
BEGIN:VALARM
ACTION:DISPLAY
DESCRIPTION:This is an event reminder
TRIGGER:-P0DT0H30M0S
END:VALARM
END:VEVENT
END:VCALENDAR

View file

@ -0,0 +1,58 @@
BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
X-WR-CALNAME:calmozilla1@gmail.com
X-WR-TIMEZONE:America/Los_Angeles
BEGIN:VTIMEZONE
TZID:America/Los_Angeles
X-LIC-LOCATION:America/Los_Angeles
BEGIN:DAYLIGHT
TZOFFSETFROM:-0800
TZOFFSETTO:-0700
TZNAME:PDT
DTSTART:19700308T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:-0700
TZOFFSETTO:-0800
TZNAME:PST
DTSTART:19701101T020000
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;TZID=America/Los_Angeles:20120630T060000
DTEND;TZID=America/Los_Angeles:20120630T070000
DTSTAMP:20120724T212411Z
UID:dn4vrfmfn5p05roahsopg57h48@google.com
CREATED:20120724T212411Z
DESCRIPTION:ICAL Description
LAST-MODIFIED:20120724T212411Z
LOCATION:My Loc
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:Summary Name
TRANSP:OPAQUE
BEGIN:VALARM
ACTION:EMAIL
DESCRIPTION:ICAL description
SUMMARY:Alarm notification
ATTENDEE:mailto:calmozilla1@gmail.com
TRIGGER:-P0DT0H30M0S
END:VALARM
BEGIN:VALARM
ACTION:DISPLAY
DESCRIPTION:This is an event reminder
TRIGGER:-P0DT0H30M0S
END:VALARM
BEGIN:VALARM
ACTION:DISPLAY
DESCRIPTION:This is an event reminder
TRIGGER;VALUE=DATE-TIME:20130312T233000Z
END:VALARM
END:VEVENT
END:VCALENDAR

View file

@ -0,0 +1,45 @@
addEventListener('message', function prepare(e) {
'use strict';
requirejs(['worker/thread'], (Thread) => {
var thread = new Thread(this);
this.console = new thread.console('W1');
thread.addRole('test');
thread.roles.test.on('relay', function() {
var args = Array.prototype.slice.call(arguments);
var callback = args.pop();
callback.apply(this, args);
});
thread.roles.test.on('error', function(callback) {
var err;
try {
throw new Error('message');
} catch (e) {
err = e;
}
callback(err);
});
thread.roles.test.on('stream', function(data, stream, callback) {
var args = Array.prototype.slice.call(arguments);
args.pop();
args.pop();
setTimeout(function() {
callback(args);
}, 0);
stream.emit('data', 1);
stream.emit('data', 2);
stream.emit('error', 'err');
});
thread.send('ready');
});
});

View file

@ -0,0 +1,52 @@
BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
X-WR-CALNAME:calmozilla1@gmail.com
X-WR-TIMEZONE:America/Los_Angeles
BEGIN:VTIMEZONE
TZID:America/Los_Angeles
X-LIC-LOCATION:America/Los_Angeles
BEGIN:DAYLIGHT
TZOFFSETFROM:-0800
TZOFFSETTO:-0700
TZNAME:PDT
DTSTART:19700308T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:-0700
TZOFFSETTO:-0800
TZNAME:PST
DTSTART:19701101T020000
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;TZID=America/Los_Angeles:20120630T060000
DTEND;TZID=America/Los_Angeles:20120630T070000
DTSTAMP:20120724T212411Z
UID:dn4vrfmfn5p05roahsopg57h48@google.com
CREATED:20120724T212411Z
DESCRIPTION:ICAL Description
LAST-MODIFIED:20120724T212411Z
LOCATION:My Loc
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:Summary Name
TRANSP:OPAQUE
BEGIN:VALARM
ACTION:EMAIL
DESCRIPTION:ICAL description
SUMMARY:Alarm notification
ATTENDEE:mailto:calmozilla1@gmail.com
TRIGGER:-P0DT0H30M0S
END:VALARM
BEGIN:VALARM
ACTION:DISPLAY
DESCRIPTION:This is an event reminder
TRIGGER:-P0DT0H30M0S
END:VALARM
END:VEVENT
END:VCALENDAR

View file

@ -0,0 +1,34 @@
<div class="template" id="month-templates" style="display: none;">
<section class="busy">
<span class="busy-">&nbsp;</span>
</section>
<section class="currentMonth">
<span class="month"></span>
<span class="year"></span>
</section>
<section class="monthDaysHeader">
<header id="month-days" role="row">
<ol role="row">
</ol>
</header>
</section>
<section class="monthSection">
<section class="month">
</section>
</section>
<section class="sectionRow">
<ol class="row"></ol>
</section>
<section class="day">
<li class="day">
<span class="date"></span>
<div class="busy-indicator"></div>
</li>
</section>
</div>

View file

@ -0,0 +1,603 @@
define(function(require) {
'use strict';
var Factory = require('test/support/factory');
var IntervalTree = require('interval_tree');
var Timespan = require('common/timespan');
var binsearch = require('common/binsearch');
var compare = require('common/compare');
suite('interval_tree', function() {
var subject;
var items;
var list;
var expectedRange;
var span;
var Node;
function factory(id, start, end) {
return {
_id: id,
_startDateMS: start,
_endDateMS: end
};
}
suiteSetup(function() {
Node = IntervalTree.Node;
});
setup(function() {
IntervalTree.overlapTime = 0;
// we use this range as a baseline
// through the less complicated tests
expectedRange = {
start: 1200,
end: 1300
};
span = new Timespan(expectedRange.start, expectedRange.end);
// setup the basic list of items
items = {};
items.before = factory(1, 100, 800);
items.overlapBefore = factory(4, 1050, 1400);
items.middle = factory(3, 1250, 1280);
items.after = factory(2, 1500, 2000);
// should be in order of start times
list = [
items.before,
items.overlapBefore,
items.middle,
items.after
];
subject = new IntervalTree(list);
});
test('integration', function() {
var span = new Timespan(
100,
800
);
var result = subject.query(span);
assert.deepEqual(result, [items.before]);
subject.remove(items.before);
result = subject.query(span);
assert.deepEqual(result, []);
var added = factory(30, 400, 1200);
subject.add(added);
subject.add(factory(31, 1100, 1200));
assert.isFalse(subject.synced);
span.end = 1100;
assert.deepEqual(
subject.query(span),
[
added,
items.overlapBefore
]
);
});
test('initialization', function() {
assert.deepEqual(subject.items, list);
assert.ok(!subject.synced);
assert.ok(!subject.rootNode);
assert.deepEqual(subject.byId, {});
});
test('init without list', function() {
var subject = new IntervalTree();
assert.deepEqual(subject.items, []);
});
test('#build', function() {
subject.build();
var node = subject.rootNode;
assert.ok(subject.synced);
assert.ok(node);
subject.build();
assert.equal(
node, subject.rootNode,
'should not rebuild tree when in sync'
);
});
suite('#add', function() {
setup(function() {
// start from clean tree
subject.items = [];
subject.synced = true;
subject.add(items.after);
});
test('re-add item with same _id', function() {
var id = items.after._id;
var obj = { _id: id };
subject.add(obj);
assert.deepEqual(
subject.items,
[items.after]
);
});
test('first add', function() {
assert.deepEqual(
subject.items,
[items.after]
);
assert.isFalse(subject.synced);
assert.equal(
subject.byId[items.after._id],
items.after,
'should add to byId cache'
);
});
test('multiple adds', function() {
subject.add(items.overlapBefore);
subject.add(items.before);
assert.isFalse(subject.synced);
assert.deepEqual(
subject.items,
[
items.before,
items.overlapBefore,
items.after
]
);
});
test('yahoo recurring allday', function() {
var allday = factory(555, 1000, 1000);
subject.add(allday);
assert.deepEqual(subject.items, [allday, items.after]);
});
});
suite('#removeFutureIntervals', function() {
var list;
setup(function() {
subject.items = [];
list = {};
});
function add(name, start, end) {
setup(function() {
list[name] = factory(name, start, end);
subject.add(list[name]);
});
}
add('before', 10, 100);
add('on', 50, 100);
add('just after', 51, 100);
add('after', 60, 100);
test('when items need to be removed', function() {
assert.isFalse(subject.synced, 'not synced');
subject.synced = true;
subject.removeFutureIntervals(50);
var expectedIds = [
'before',
'on'
];
assert.ok(!subject.byId['just after']);
assert.ok(!subject.byId.after);
var ids = subject.items.map(function(item) {
return item._id;
});
assert.deepEqual(ids, expectedIds);
assert.isFalse(subject.synced, 'should be synced');
});
test('when last item needs to be removed', function() {
subject.removeFutureIntervals(59);
var expectedIds = [
'before',
'on',
'just after'
];
var ids = subject.items.map(function(item) {
return item._id;
});
assert.deepEqual(ids, expectedIds);
});
});
suite('#removePastIntervals', function() {
var list;
setup(function() {
subject.items = [];
list = {};
});
function add(name, start, end) {
setup(function() {
list[name] = factory(name, start, end);
subject.add(list[name]);
});
}
add('overlap all', 1, 1000);
add('ends before', 2, 50);
add('ends on 1', 3, 150);
add('ends on 2', 4, 150);
add('ends on 3', 5, 150);
add('ends just after', 6, 151);
add('ends after', 7, 200);
test('when items need to be removed', function() {
assert.isFalse(subject.synced, 'not synced');
subject.synced = true;
subject.removePastIntervals(150);
var expectedIds = [
'overlap all',
'ends just after',
'ends after'
];
assert.ok(!subject.byId['ends on 1']);
assert.ok(!subject.byId['ends on 2']);
assert.ok(!subject.byId['ends on 3']);
var ids = subject.items.map(function(item) {
assert.ok(subject.byId[item._id], 'should have: ' + item._id);
return item._id;
});
assert.deepEqual(ids, expectedIds);
assert.isFalse(subject.synced, 'should be synced');
});
});
suite('#remove', function() {
test('when removing nonexisting item', function() {
subject.synced = true;
var result = subject.remove({ start: 100 });
assert.isFalse(result, 'should return false when item is not removed');
assert.isTrue(subject.synced, 'should not remark sync to false');
});
test('item that does not share start', function() {
subject.synced = true;
var result = subject.remove(items.middle);
assert.isTrue(result, 'remove should return true');
assert.isFalse(subject.synced);
assert.deepEqual(
subject.items,
[
items.before,
items.overlapBefore,
items.after
]
);
});
suite('items that share start time', function() {
var middle;
var addedBefore;
var addedAfter;
setup(function() {
middle = items.before;
subject.items = [];
subject.synced = false;
addedBefore = factory(10, middle[subject.START], middle[subject.END]);
addedAfter = factory(12, middle[subject.START], middle[subject.END]);
// its going to shuffle
// each item is going to displace
// the next pushing it further in
// the array.
subject.add(addedBefore);
subject.add(middle);
subject.add(addedAfter);
assert.deepEqual(
subject.items,
[
addedAfter,
middle,
addedBefore
]
);
});
test('remove last added item', function() {
subject.remove(addedAfter);
assert.deepEqual(
subject.items,
[
middle,
addedBefore
]
);
assert.ok(!subject.byId[addedAfter._id]);
});
test('remove first added item', function() {
subject.remove(addedBefore);
assert.deepEqual(
subject.items,
[
addedAfter,
middle
]
);
});
test('remove middle item', function() {
subject.remove(middle);
assert.deepEqual(
subject.items,
[
addedAfter,
addedBefore
]
);
});
});
});
suite('Node', function() {
setup(function() {
subject.build();
subject = subject.rootNode;
});
suite('node alignment', function() {
test('base alignment', function() {
var left = subject.left;
var right = subject.right;
assert.isTrue(
(left.median < subject.median),
'lower values should be on the left'
);
assert.isTrue(
(right.median > subject.median),
'higher values should be on the right'
);
});
});
test('#traverse', function() {
var results = [];
subject.traverse(span, function(item, node) {
results.push(item);
assert.instanceOf(node, IntervalTree.Node);
});
assert.deepEqual(
results,
[items.overlapBefore, items.middle]
);
});
suite('#query', function() {
var sublist;
var id;
setup(function() {
id = 0;
sublist = [];
});
function compareDateMS(aObj, bObj) {
var a = aObj._startDateMS;
var b = bObj._startDateMS;
return compare(a, b);
}
function orderedAdd(item, arr) {
var idx = binsearch.insert(
arr,
item,
compareDateMS
);
arr.splice(idx, 0, item);
}
function add(start, end) {
var item = factory(id++, start, end);
orderedAdd(item, sublist);
return item;
}
test('large dataset with gaps', function() {
var expected = [];
var i = 0;
// create some out of range in lower bounds
for (i = 0; i < 1021; i++) {
add(i, i + 200);
}
// create two really huge spans
for (i = 0; i < 75; i++) {
orderedAdd(add(i, 5500), expected);
}
for (i = 0; i < 1099; i++) {
orderedAdd(add(5201 + i, 6000 + i), expected);
}
orderedAdd(add(7000, 7070), expected);
orderedAdd(add(8000, 8100), expected);
orderedAdd(add(9000, 9001), expected);
// middle values
orderedAdd(add(10200, 10500), expected);
orderedAdd(add(10300, 10500), expected);
orderedAdd(add(10400, 10500), expected);
orderedAdd(add(10000, 10800), expected);
// yahoo recurring all day events have same start/end dates
orderedAdd(add(10000, 10000), expected);
// create some out of range in upper bounds
for (i = 0; i < 1021; i++) {
add(i + 2500, i + 2505);
}
subject = new IntervalTree(sublist);
var result = subject.query(new Timespan(
5201,
11750
));
// this deep equality assertion is more expensive
// then the entire process of building
// the tree and finding the results...
assert.deepEqual(
result,
expected
);
});
test('basic start range query', function() {
var range = new Timespan(
80,
900
);
assert.deepEqual(
subject.query(range),
[items.before]
);
});
test('basic end range query', function() {
var range = new Timespan(
1600,
1800
);
assert.deepEqual(
subject.query(range),
[items.after]
);
});
test('basic middle query', function() {
var range = new Timespan(
expectedRange.start,
expectedRange.end
);
var results = subject.query(range);
assert.deepEqual(
results,
[
items.overlapBefore,
items.middle
]
);
});
});
});
suite('#createIndex / #index', function() {
var one;
var two;
var three;
setup(function() {
one = Factory('busytime', { eventId: 'xxx' });
two = Factory('busytime', { eventId: 'xxx' });
three = Factory('busytime', { eventId: 'xxx' });
subject.createIndex('eventId');
subject.add(one);
subject.add(two);
subject.add(three);
});
test('add', function() {
assert.deepEqual(
subject.index('eventId', one.eventId),
[one, two, three]
);
});
test('remove middle', function() {
subject.remove(two);
assert.deepEqual(
subject.index('eventId', one.eventId),
[one, three]
);
});
test('remove start', function() {
subject.remove(one);
assert.deepEqual(
subject.index('eventId', one.eventId),
[two, three]
);
});
test('remove end', function() {
subject.remove(three);
assert.deepEqual(
subject.index('eventId', one.eventId),
[one, two]
);
});
test('remove all', function() {
subject.remove(one);
subject.remove(two);
subject.remove(three);
assert.ok(!subject.index('eventId', one.eventId));
});
});
});
});

View file

@ -0,0 +1,65 @@
define(function(require) {
'use strict';
var Account = require('models/account');
suite('Models.Account', function() {
var subject;
setup(function() {
subject = new Account({ user: 'foo', providerType: 'Local' });
});
test('initialization', function() {
assert.equal(subject.user, 'foo');
});
suite('fullUrl', function() {
test('get', function() {
subject.domain = 'http://google.com';
subject.entrypoint = '/foo';
assert.equal(subject.fullUrl, 'http://google.com/foo');
});
test('set', function() {
subject.fullUrl = 'http://google.com/foo/bar';
assert.equal(subject.domain, 'http://google.com');
assert.equal(subject.entrypoint, '/foo/bar');
});
});
suite('#toJSON', function() {
setup(function() {
subject._id = '1';
subject.entrypoint = 'url';
subject.domain = 'domain';
subject.password = 'pass';
subject.user = 'user';
subject.providerType = 'local';
subject.preset = 'google';
subject.calendarHome = '/foo/home';
subject.oauth = { code: 'xxx' };
});
test('output', function() {
var expected = {
_id: subject._id,
entrypoint: subject.entrypoint,
domain: subject.domain,
password: subject.password,
user: subject.user,
providerType: subject.providerType,
calendarHome: '/foo/home',
preset: 'google',
oauth: { code: 'xxx' },
error: undefined
};
assert.deepEqual(subject.toJSON(), expected);
});
});
});
});

View file

@ -0,0 +1,120 @@
define(function(require) {
'use strict';
var Calendar = require('models/calendar');
suite('Models.Calendar', function() {
var subject;
var remoteCalendar;
var CalendarModel;
suiteSetup(function() {
CalendarModel = Calendar;
});
setup(function() {
remoteCalendar = {
id: 'one',
syncToken: 'xxx'
};
subject = new Calendar({
name: 'foo',
remote: remoteCalendar
});
});
suite('initialization', function() {
test('when given a remote', function() {
var date = new Date();
subject = new CalendarModel({
remote: remoteCalendar,
lastEventSyncToken: '0',
lastEventSyncDate: date
});
assert.deepEqual(
subject.remote,
remoteCalendar
);
assert.equal(subject.lastEventSyncDate, date);
});
test('when given remote', function() {
subject = new CalendarModel({
remote: remoteCalendar
});
assert.deepEqual(subject.remote, remoteCalendar);
});
});
test('#updateRemote', function() {
remoteCalendar.id = 'foo';
subject.updateRemote(remoteCalendar);
assert.equal(subject.remote.id, 'foo');
});
suite('#eventSyncNeeded', function() {
test('when remote and local match', function() {
subject.remote.syncToken = 'foo';
subject.lastEventSyncToken = 'foo';
assert.isFalse(subject.eventSyncNeeded());
});
test('when remote and local differ', function() {
subject.remote.syncToken = 'foo';
subject.lastEventSyncToken = 'bar';
assert.isTrue(subject.eventSyncNeeded());
});
});
test('#toJSON', function() {
subject._id = '1';
var expected = {
error: undefined,
lastEventSyncToken: subject.lastEventSyncToken,
lastEventSyncDate: subject.lastEventSyncDate,
localDisplayed: subject.localDisplayed,
firstEventSyncDate: subject.firstEventSyncDate,
accountId: subject.accountId,
_id: subject._id,
remote: subject.remote
};
assert.deepEqual(subject.toJSON(), expected);
});
test('#name', function() {
subject.remote.name = 'foo';
assert.equal(subject.name, 'foo');
});
suite('#color', function() {
test('basic getter', function() {
subject.remote.color = '#ccc';
assert.equal(subject.color, '#ccc');
});
test('filter hex value', function() {
subject.remote.color = '#7BD148FF';
assert.equal(subject.color, '#7BD148');
});
});
test('#description', function() {
subject.remote.description = 'foo';
assert.equal(subject.description, 'foo');
});
});
});

View file

@ -0,0 +1,297 @@
define(function(require) {
'use strict';
var Calc = require('common/calc');
var Event = require('models/event');
var Factory = require('test/support/factory');
suite('Models.Event', function() {
var subject;
var rawEvent;
var remote;
var start;
var end;
var originalDates;
suiteSetup(function() {
start = new Date(2012, 0, 1);
end = new Date(2012, 0, 1, 12);
originalDates = {
start: start,
end: end
};
});
setup(function() {
rawEvent = Factory.create('event', {
remote: {
syncToken: '7ee',
startDate: start,
endDate: end,
start: Calc.dateToTransport(start),
end: Calc.dateToTransport(end)
}
});
remote = rawEvent.remote;
subject = new Event(rawEvent);
});
suite('initialization', function() {
test('from existing model', function() {
assert.equal(subject.data, rawEvent);
});
test('from existing "allday" model', function() {
var data = Factory.build('event', {
remote: {
startDate: new Date(2012, 1, 1),
endDate: new Date(2012, 1, 2)
}
});
var subject = new Event(data);
assert.isTrue(subject.isAllDay, 'is all day');
});
test('from existing model without .startDate/.endDate', function() {
var event = new Event();
var start = new Date(2012, 0, 1);
var end = new Date(2012, 0, 5);
event.startDate = start;
event.endDate = end;
var data = event.data;
delete data.remote.startDate;
delete data.remote.endDate;
var newEvent = new Event(data);
assert.deepEqual(newEvent.startDate, start);
assert.deepEqual(newEvent.endDate, end);
});
test('from new model', function() {
subject = new Event();
assert.ok(subject.data, 'has data');
assert.ok(subject.data.remote, 'has remote');
assert.instanceOf(
subject.startDate,
Date,
'pre-populates start date'
);
assert.instanceOf(
subject.endDate,
Date,
'pre-populates end date'
);
});
});
function remoteSetter(type) {
test('#' + type + ' setter/getter', function() {
var value = 'foo';
subject[type] = value;
assert.equal(rawEvent.remote[type], value, 'raw data ' + type);
assert.equal(subject[type], value, 'get ' + type);
});
}
// need a var for gjslint parse error?
var dateFields = ['start', 'end'];
dateFields.forEach(function(field) {
var remoteDateField = field + 'Date';
suite('#' + remoteDateField, function() {
test('initial value', function() {
assert.deepEqual(
originalDates[field],
subject[remoteDateField]
);
});
test('set value', function() {
var date = new Date(2012, 0, 1, 2);
var transport = Calc.dateToTransport(date);
subject[remoteDateField] = date;
assert.deepEqual(
remote[remoteDateField],
date,
remoteDateField
);
assert.deepEqual(
remote[field],
transport,
field
);
});
test('prevents non-date values when isAllDay', function() {
subject.isAllDay = true;
var date = new Date(2012, 1, 1, 1, 5);
var expected = new Date(2012, 1, 1);
var transport = Calc.dateToTransport(
expected, null, true
);
subject[remoteDateField] = date;
assert.deepEqual(
subject[remoteDateField],
expected,
'date'
);
assert.deepEqual(
subject.remote[field],
transport,
'trasport'
);
});
test('clears when is .allDay', function() {
subject.isAllDay = true;
assert.isTrue(
Calc.isOnlyDate(subject[remoteDateField]),
'is only date'
);
assert.isTrue(
subject.remote[field].isDate,
'is date'
);
});
});
});
test('#isAllDay', function() {
subject.isAllDay = true;
assert.isTrue(
Calc.isOnlyDate(subject.startDate),
'removes time from start'
);
assert.isTrue(
Calc.isOnlyDate(subject.endDate),
'removes time from end'
);
assert.isTrue(
subject.remote.start.isDate,
'sets start to date'
);
assert.isTrue(
subject.remote.end.isDate,
'sets end to date'
);
});
test('#calendarId', function() {
subject.calendarId = 7;
assert.equal(rawEvent.calendarId, 7);
assert.equal(subject.calendarId, 7);
});
test('#calendarId with string input', function() {
subject.calendarId = '7';
assert.strictEqual(rawEvent.calendarId, 7);
assert.strictEqual(subject.calendarId, 7);
});
test('#_id', function() {
rawEvent._id = 'foo';
assert.equal(subject._id, 'foo');
});
suite('#validationErrors', function() {
test('no errors', function() {
var event = new Event();
event.startDate = new Date(2012, 0, 1);
event.endDate = new Date(2012, 0, 2);
assert.ok(!event.validationErrors(), 'has no errors');
});
function hasError(event, type) {
var errors = event.validationErrors();
assert.lengthOf(errors, 1);
assert.deepEqual(
errors[0], {
name: type
},
'has error: "' + type + '"'
);
}
test('start date >(=) end date', function() {
var event = new Event();
event.startDate = new Date(2020, 0, 2);
event.endDate = new Date(2012, 0, 1);
// start date > end date
hasError(event, 'start-after-end');
// start date == end date
event.startDate = new Date(event.endDate.valueOf());
hasError(event, 'start-after-end');
});
});
suite('update attributes', function() {
var eventWithoutErrors;
var eventWithErrors;
var event;
var model;
setup(function() {
event = Factory.create('event', {
remote: {
syncToken: '7ee',
startDate: new Date(2019, 1, 2),
endDate: new Date(2020, 0, 2),
start: Calc.dateToTransport(start),
end: Calc.dateToTransport(end)
}
});
model = new Event(event);
eventWithoutErrors = {
startDate: new Date(2019, 1, 2),
endDate: new Date(2020, 1, 2)
};
eventWithErrors = {
startDate: new Date(2019, 1, 2),
endDate: new Date(2019, 0, 2)
};
});
test('does not update attributes', function() {
var errors = model.updateAttributes(eventWithErrors);
assert.ok(errors);
assert.deepEqual(errors[0].name, 'start-after-end');
});
test('will update attributes', function() {
var errors = model.updateAttributes(eventWithoutErrors);
assert.strictEqual(errors, true);
assert.deepEqual(model.endDate, eventWithoutErrors.endDate);
});
});
remoteSetter('syncToken');
remoteSetter('location');
remoteSetter('description');
remoteSetter('title');
remoteSetter('alarms');
});
});

View file

@ -0,0 +1,209 @@
define(function(require) {
'use strict';
var OAuthWindow = require('oauth_window');
var QueryString = require('querystring');
var View = require('view');
suite('OAuthWindow', function() {
var subject;
var url = 'https://foobar.com';
var element;
var params = {
client_id: 'foo',
redirect_uri: 'http://oauth.gaiamobile.org/foobar'
};
function emitLocationChange(url) {
var event = new CustomEvent('mozbrowserlocationchange', {
detail: url
});
subject.browserFrame.dispatchEvent(event);
}
setup(function() {
element = document.createElement('section');
element.innerHTML = [
'<section role="region">',
'<gaia-header id="oauth-header" action="cancel">',
'<h1 class="oauth-browser-title"></h1>',
'</gaia-header>',
'<div class="browser-container"></div>',
'</section>'
].join('');
subject = new OAuthWindow(
element,
url,
params
);
});
test('initialization', function() {
assert.deepEqual(subject.params, params);
assert.equal(subject.element, element);
assert.deepEqual(
subject.target,
url + '?' + QueryString.stringify(params),
'.target'
);
});
test('.browserContainer', function() {
assert.ok(subject.browserContainer, 'has container');
assert.equal(subject.browserContainer, element.querySelector(
subject.selectors.browserContainer
));
});
test('.browserTitle', function() {
assert.equal(
subject.browserTitle,
element.querySelector(subject.selectors.browserTitle)
);
});
test('.browserHeader', function() {
assert.equal(
subject.browserHeader,
element.querySelector(subject.selectors.browserHeader)
);
});
test('without redirect_uri', function() {
assert.throws(function() {
return new OAuthWindow(url, {});
});
});
suite('#open', function() {
setup(function() {
subject.open();
});
test('.isOpen', function() {
assert.isTrue(subject.isOpen);
});
test('is active', function() {
assert.ok(
subject.element.classList.contains(View.ACTIVE),
'is active'
);
});
test('has iframe', function() {
var iframe = element.querySelector(
'iframe'
);
assert.ok(
iframe.getAttribute('mozbrowser'),
'is mozbrowser'
);
assert.equal(
iframe.getAttribute('src'),
subject.target,
'is at url'
);
assert.equal(subject.browserFrame, iframe);
assert.equal(
subject.browserContainer.firstElementChild,
subject.browserFrame,
'is inside container'
);
});
test('url changed to something other then redirect_uri', function() {
var newUrl = 'http://foobar.com/bar/bar';
emitLocationChange(newUrl);
assert.equal(
subject.browserTitle.textContent,
newUrl,
'changes url'
);
});
test('click cancel button', function(done) {
var hasClosed;
subject.onabort = function() {
assert.ok(hasClosed, 'called close');
done();
};
subject.close = function() {
hasClosed = true;
};
testSupport.calendar.triggerEvent(
subject.browserHeader,
'action'
);
});
test('redirects to redirect_uri', function(done) {
var successParams = {
code: 'wow',
state: 'youwin'
};
var hasClosed;
subject.close = function() {
hasClosed = true;
};
subject.oncomplete = function(params) {
assert.ok(hasClosed, 'closed window');
assert.deepEqual(successParams, params, 'parsed params');
done();
};
emitLocationChange(
params.redirect_uri + '?' +
QueryString.stringify(successParams)
);
});
});
suite('#close', function() {
setup(function() {
subject.open();
});
test('removes iframe', function() {
assert.ok(subject.browserFrame, 'has frame');
subject.close();
assert.ok(!subject.browserFrame, 'removes frame');
assert.ok(!element.querySelector('iframe'), 'removes from dom');
assert.isFalse(subject.isOpen, 'isOpen');
assert.ok(
!element.classList.contains(View.ACTIVE),
'is inactive'
);
});
test('cleans up events', function() {
var triggedClose;
subject.close();
subject.close();
subject.close = function() {
triggedClose = true;
};
testSupport.calendar.triggerEvent(
subject.browserHeader,
'action'
);
assert.ok(!triggedClose, 'does not trigger close');
});
});
});
});

View file

@ -0,0 +1 @@
// TODO(gareth)

View file

@ -0,0 +1,31 @@
define(function(require) {
'use strict';
var Abstract = require('provider/abstract');
suite('Provider.Abstract', function() {
var subject;
setup(function() {
subject = new Abstract();
});
test('initializer', function() {
assert.ok(subject);
});
test('#eventCapabilities', function(done) {
// just trying to assert the api contract is correct.
subject.eventCapabilities({}, function(err, list) {
if (err) {
return done(err);
}
done(function() {
assert.instanceOf(list, Object);
});
});
});
});
});

View file

@ -0,0 +1,700 @@
define(function(require) {
'use strict';
var AccountModel = require('models/account');
var CalendarModel = require('models/calendar');
var CaldavPullEvents = require('provider/caldav_pull_events');
var CaldavService = require('service/caldav');
var Responder = require('common/responder');
var Factory = require('test/support/factory');
var ServiceSupport = require('test/service/helper');
var core = require('core');
suite('Provider.CaldavPullEvents', function() {
var fixtures;
var ical;
var subject;
var stream;
var db;
var service;
var storeFactory;
function createSubject(options) {
if (typeof(options) === 'undefined') {
options = Object.create(null);
}
if (!options.calendar) {
options.calendar = calendar;
}
if (!options.account) {
options.account = account;
}
stream = new Responder();
return new CaldavPullEvents(stream, options);
}
setup(function(done) {
ical = new ServiceSupport.Fixtures('ical');
ical.load('single_event');
ical.load('daily_event');
ical.load('recurring_event');
ical.onready = done;
fixtures = {};
ServiceSupport.setExpansionLimit(10);
service = new CaldavService(new Responder());
});
teardown(function() {
ServiceSupport.resetExpansionLimit();
});
['singleEvent', 'dailyEvent', 'recurringEvent'].forEach(function(item) {
setup(function(done) {
service.parseEvent(ical[item], function(err, event) {
fixtures[item] = service._formatEvent(
'abc', '/foobar.ics',
ical[item], event
);
done();
});
});
});
function serviceEvent(type) {
// poor mans clone
var json = JSON.stringify(
fixtures[type]
);
return JSON.parse(json);
}
setup(function(done) {
db = core.db;
storeFactory = core.storeFactory;
db.open(function(err) {
assert.ok(!err);
done();
});
});
var account;
setup(function(done) {
account = Factory.create('account');
storeFactory.get('Account').persist(account, done);
});
var calendar;
setup(function(done) {
calendar = Factory.create('calendar');
calendar.accountId = account._id;
calendar.remote.syncToken = 'not-same-as-other';
calendar.syncToken = 'neq';
storeFactory.get('Calendar').persist(calendar, done);
});
setup(function() {
subject = createSubject();
});
teardown(function(done) {
testSupport.calendar.clearStore(
db,
['accounts', 'calendars', 'icalComponents',
'events', 'busytimes', 'alarms'],
done
);
});
teardown(function() {
db.close();
});
test('initializer', function() {
var subject = createSubject();
assert.instanceOf(subject.account, AccountModel, '.account');
assert.instanceOf(subject.calendar, CalendarModel, '.calendar');
});
suite('#eventIdFromRemote', function() {
test('exception', function() {
var event = serviceEvent('singleEvent');
event.recurrenceId = { utc: 100 };
var id = calendar._id + '-' + event.id + '-100';
assert.deepEqual(
subject.eventIdFromRemote(event),
id
);
});
test('non exception', function() {
var event = serviceEvent('singleEvent');
var id = calendar._id + '-' + event.id;
assert.deepEqual(
subject.eventIdFromRemote(event),
id
);
});
});
suite('#formatEvent', function() {
test('recurring event', function() {
var event = serviceEvent('recurringEvent');
var exceptions = event.exceptions;
delete event.exceptions;
var primary = subject.formatEvent(
event
);
var exception = subject.formatEvent(
exceptions[0]
);
assert.ok(exception.remote, 'has remote');
assert.deepEqual(
exception.parentId,
primary._id,
'has parent id'
);
assert.equal(
exception._id,
primary._id + '-' + exception.remote.recurrenceId.utc,
'has exception id'
);
});
test('single event', function() {
var event = serviceEvent('singleEvent');
var remote = serviceEvent('singleEvent');
delete event.exceptions;
delete remote.exceptions;
var expected = {
_id: subject.eventIdFromRemote(remote),
calendarId: calendar._id,
remote: remote
};
assert.deepEqual(
subject.formatEvent(event),
expected
);
});
});
suite('#formatBusytime', function() {
var times = [];
var event;
function expand(name, limit) {
setup(function(done) {
times.length = 0;
event = serviceEvent(name);
var stream = new Responder();
stream.on('occurrence', function(item) {
times.push(item);
});
service.expandRecurringEvent(
ical[name],
{},
stream,
done
);
});
}
suite('without exceptions', function() {
expand('dailyEvent');
test('non-exception result', function() {
var time = times[1];
assert.isFalse(time.isException, 'is exception');
var result = subject.formatBusytime(time);
var modelCopy = Object.create(result);
modelCopy = storeFactory.get('Busytime').initRecord(modelCopy);
assert.hasProperties(
result,
modelCopy,
'is a model'
);
var eventId = result.eventId;
var calendarId = result.calendarId;
assert.ok(eventId, 'has event');
assert.ok(calendarId, 'has calendar');
assert.instanceOf(result.alarms, Array);
result.alarms.forEach(function(alarm) {
assert.equal(alarm.busytimeId, result._id);
assert.equal(alarm.eventId, eventId);
});
});
});
suite('with exceptions', function() {
expand('recurringEvent');
test('result', function() {
var time = times[2];
assert.isTrue(time.isException, 'is exception');
var eventId = subject.eventIdFromRemote(
time
);
var result = subject.formatBusytime(time);
assert.ok(result._id, 'has id');
assert.equal(result.calendarId, calendar._id);
assert.include(
result.eventId,
times[2].recurrenceId.utc,
'has utc time'
);
assert.equal(result.eventId, eventId);
});
});
});
suite('bug 809607', function() {
var accountStore;
var account;
setup(function(done) {
accountStore = storeFactory.get('Account');
account = Factory.create('account');
accountStore.persist(account, done);
});
var calendar;
setup(function(done) {
calendar = Factory.create('calendar');
calendar.accountId = account._id;
calendar.remote.syncToken = 'bug-809607';
calendar.syncToken = 'bug-809607';
storeFactory.get('Calendar').persist(calendar, done);
});
var subject;
var events;
var eventPersistCt;
var eventStore;
setup(function() {
subject = createSubject({account: account, calendar: calendar});
events = [];
for (var i = 0; i < 4; i++) {
events.push(serviceEvent('singleEvent'));
}
// Set up to count events persisted.
eventPersistCt = 0;
eventStore = storeFactory.get('Event');
eventStore.on('persist', function(id) {
eventPersistCt++;
});
});
test('account deletion during sync aborts processing', function(done) {
// Stream a couple of events before account removal
stream.emit('event', events[0]);
stream.emit('event', events[1]);
function afterRemove() {
// Stream a couple of events after account removal
stream.emit('event', events[2]);
stream.emit('event', events[3]);
// Attempt to commit the events
subject.commit(function() {
setTimeout(afterCommit, 0);
});
}
function afterCommit() {
// After account removal and commit, no events should have been saved.
assert.equal(eventPersistCt, 0);
done();
}
// Kick off the removal and subsequent steps...
accountStore.remove(account._id, function() {
setTimeout(afterRemove, 0);
});
});
test('abort during #commit also aborts transaction', function(done) {
// Stream some events...
for (var i = 0; i < events.length; i++) {
stream.emit('event', events[i]);
}
subject.commit(function() {
// No-op
});
// Detection of transaction abort is a test success.
subject._trans.onabort = function() {
// Restore the exception handler.
done();
};
// Queue up account removal...
accountStore.emit('remove', account._id);
});
});
suite('#commit', function() {
var removed = [];
var eventStore;
var componentStore;
var newEvent;
var newBusytime;
var alarm;
var addedComponent;
setup(function() {
removed.length = 0;
eventStore = storeFactory.get('Event');
componentStore = storeFactory.get('IcalComponent');
eventStore.remove = function(id) {
removed.push(id);
};
newEvent = serviceEvent('singleEvent');
newEvent = subject.formatEvent(newEvent);
addedComponent = {
eventId: newEvent._id,
ical: 'foo'
};
subject.icalQueue.push(addedComponent);
subject.eventQueue.push(newEvent);
subject.removeList = ['1'];
newBusytime = Factory('busytime', {
eventId: newEvent._id,
calendarId: newEvent.calendarId
});
subject.busytimeQueue.push(newBusytime);
alarm = Factory('alarm', {
startDate: new Date(),
eventId: newEvent._id,
busytimeId: newBusytime._id
});
subject.alarmQueue.push(alarm);
});
function commit(fn) {
setup(function(done) {
subject.commit(function() {
setTimeout(function() {
done();
}, 0);
});
});
}
suite('busytime/alarm', function() {
commit();
test('alarm', function(done) {
var trans = db.transaction('alarms');
var store = trans.objectStore('alarms');
var index = store.index('busytimeId');
index.get(alarm.busytimeId).onsuccess = function(e) {
done(function() {
var data = e.target.result;
assert.ok(data, 'has alarm');
assert.hasProperties(data, alarm, 'alarm matches');
});
};
});
test('busytimes', function(done) {
var id = newBusytime._id;
var trans = db.transaction('busytimes');
var store = trans.objectStore('busytimes');
store.get(id).onsuccess = function(e) {
done(function() {
var result = e.target.result;
assert.ok(result);
assert.hasProperties(result, {
start: newBusytime.start,
end: newBusytime.end,
eventId: newBusytime.eventId,
calendarId: newBusytime.calendarId
});
});
};
});
});
suite('without remove list', function() {
setup(function() {
subject.removeList = null;
});
test('result', function(done) {
subject.commit(done);
});
});
suite('event/component', function() {
commit();
test('component', function(done) {
componentStore.get(newEvent._id, function(err, record) {
if (err) {
done(err);
return;
}
done(function() {
assert.ok(record, 'has records');
assert.deepEqual(record, addedComponent);
});
});
});
test('event', function(done) {
eventStore.findByIds([newEvent._id], function(err, list) {
done(function() {
assert.lengthOf(Object.keys(list), 1, 'saved events');
assert.ok(list[newEvent._id], 'saved event id');
});
});
});
});
});
suite('#handleOccurrenceSync', function() {
var times = [];
var event;
setup(function(done) {
event = serviceEvent('dailyEvent');
var stream = new Responder();
stream.on('occurrence', function(item) {
times.push(item);
});
service.expandRecurringEvent(
ical.dailyEvent,
{},
stream,
done
);
});
function copy(idx) {
var json = JSON.stringify(times[idx]);
return JSON.parse(json);
}
test('single', function() {
var expected = subject.formatBusytime(
copy(0)
);
var alarms = expected.alarms;
delete expected.alarms;
assert.ok(alarms, 'has alarms');
stream.emit('occurrence', times[0]);
assert.lengthOf(subject.busytimeQueue, 1);
// ids are unique each time
expected._id = subject.busytimeQueue[0]._id;
assert.ok(expected._id, 'has id');
assert.hasProperties(
subject.busytimeQueue[0],
expected,
'queued'
);
assert.ok(!subject.busytimeQueue[0].alarms, 'removes alarms');
assert.deepEqual(
subject.alarmQueue.length, alarms.length,
'moves moves to alarm queue'
);
});
});
test('#handleMissingEvents', function() {
stream.emit('missingEvents', ['1', '2']);
assert.deepEqual(subject.removeList, ['1', '2']);
});
suite('#handleComponentSync', function() {
test('incomplete recurrence', function() {
var data = {
eventId: 'foo',
lastRecurrenceId: { year: 2012 },
ical: 'ical'
};
var expected = {};
for (var key in data) {
expected[key] = data[key];
}
expected.eventId = subject.eventIdFromRemote(
data
);
expected.calendarId = calendar._id;
stream.emit('component', data);
assert.lengthOf(subject.icalQueue, 1);
assert.deepEqual(
subject.icalQueue[0],
expected
);
});
test('complete recurrence', function() {
var data = {
lastRecurrenceId: false,
ical: 'ical'
};
stream.emit('component', data);
assert.lengthOf(subject.icalQueue, 1);
assert.ok(
!('lastRecurrenceId' in subject.icalQueue[0]),
'has not lastRecurrenceId'
);
});
});
suite('#handleEventSync', function() {
test('recurring', function() {
// control is needed to verify we have not mutated
// the results by using the same object during the test.
var control = serviceEvent('recurringEvent');
control = subject.formatEvent(control);
assert.lengthOf(control.remote.exceptions, 2);
var exceptions = control.remote.exceptions;
delete control.remote.exceptions;
exceptions = exceptions.map(subject.formatEvent, subject);
// we need to do this twice so this is a clean
// copy and the above can be mutated
var event = serviceEvent('recurringEvent');
stream.emit('event', event);
assert.lengthOf(subject.eventQueue, 3);
assert.lengthOf(subject.icalQueue, 1);
assert.hasProperties(
subject.icalQueue[0],
{
eventId: control._id,
data: control.remote.icalComponent
}
);
var order = [control].concat(exceptions);
// ensure event and all its exceptions are queued
// and in the correct order (event first exceptions after)
order.forEach(function(item, idx) {
assert.hasProperties(subject.eventQueue[idx].remote, {
id: item.remote.id,
eventId: item.remote.eventId,
recurrenceId: item.remote.recurrenceId
});
});
});
test('new event', function() {
var existing = serviceEvent('singleEvent');
existing = subject.formatEvent(existing);
existing.remote.syncToken = 'abx1';
subject = createSubject();
var newEvent = serviceEvent('singleEvent');
newEvent.syncToken = 'bbx1';
assert.notEqual(
newEvent.syncToken,
existing.remote.syncToken,
'sync tokens match'
);
stream.emit('event', newEvent);
assert.lengthOf(
subject.eventQueue,
1
);
});
test('event new', function() {
var event = serviceEvent('singleEvent');
stream.emit('event', event);
assert.deepEqual(
subject.eventQueue,
[subject.formatEvent(event)]
);
});
});
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,212 @@
define(function(require) {
'use strict';
var Abstract = require('provider/abstract');
var Factory = require('test/support/factory');
var Local = require('provider/local');
var core = require('core');
suite('provider/local', function() {
var subject;
var db;
var storeFactory;
setup(function(done) {
subject = new Local();
storeFactory = core.storeFactory;
db = core.db;
db.open(function(err) {
assert.ok(!err);
done();
});
});
teardown(function(done) {
testSupport.calendar.clearStore(
db,
['accounts', 'calendars', 'events', 'busytimes'],
done
);
});
teardown(function() {
db.close();
});
test('initialization', function() {
assert.instanceOf(subject, Abstract);
});
test('#getAccount', function(done) {
subject.getAccount({}, function(err, success) {
assert.ok(!err);
assert.deepEqual(success, {});
done();
});
});
/* disabled in Bug 838993, to be reenabled asap in Bug 840489.
* this happens when using a firefox launched with a french locale, we
* probably need to mock the l10n library instead of using it.
*
* 1) [calendar] provider/local #findCalendars:
* Error: expected 'Agenda hors-ligne' to equal 'Offline calendar'
*
test('#findCalendars', function(done) {
// local will always return the same
// calendar id
subject.findCalendars({}, function(err, list) {
done(function() {
var first = list['local-first'];
assert.equal(first.id, 'local-first');
assert.equal(first.name, 'Offline calendar');
});
});
});
*/
suite('mutations', function() {
var events;
var busytimes;
setup(function() {
events = storeFactory.get('Event');
busytimes = storeFactory.get('Busytime');
});
function find(eventId, done) {
var trans = db.transaction(
events._dependentStores,
'readwrite'
);
trans.oncomplete = function() {
done(busytime, event);
};
var event;
var busytime;
events.get(eventId, trans, function(err, record) {
event = record;
});
var index = trans.objectStore('busytimes').index('eventId');
index.get(eventId).onsuccess = function(e) {
busytime = e.target.result;
};
}
suite('#createEvent', function() {
var event;
function verify(done) {
subject.createEvent(event, function() {
find(event._id, function(busytime, event) {
done(function() {
assert.deepEqual(event, event);
});
});
});
}
test('without remote.id', function(done) {
event = Factory('event');
delete event.remote.id;
delete event._id;
verify(done);
assert.ok(event.remote.id, 'adds id');
});
test('with remote.id', function(done) {
event = Factory('event');
delete event._id;
var id = event.remote.id;
verify(done);
assert.equal(event.remote.id, id, 'id change');
});
});
suite('#updateEvent', function() {
//XXX: in the future we should skip the saving.
suite('update with same values', function() {
var event;
var busytime;
setup(function(done) {
event = Factory('event');
subject.createEvent(event, done);
});
setup(function(done) {
subject.updateEvent(event, busytime, function(err, busy, ev) {
event = ev;
busytime = busy;
done();
});
});
test('event', function(done) {
assert.ok(event);
events.count(function(err, count) {
done(function() {
assert.equal(count, 1);
});
});
});
test('busytime', function(done) {
assert.hasProperties(busytime, {
eventId: event._id, calendarId: event.calendarId
});
busytimes.count(function(err, count) {
done(function() {
assert.equal(count, 1);
});
});
});
});
});
suite('#deleteEvent', function() {
var event;
setup(function(done) {
event = Factory('event');
subject.createEvent(event, done);
});
setup(function(done) {
subject.deleteEvent(event, done);
});
test('busytime count', function(done) {
busytimes.count(function(err, count) {
done(function() {
assert.equal(count, 0);
});
});
});
test('event count', function(done) {
events.count(function(err, count) {
done(function() {
assert.equal(count, 0);
});
});
});
});
});
});
});

View file

@ -0,0 +1,68 @@
define(function(require, exports, module) {
'use strict';
var Responder = require('common/responder');
function MockStream() {
Responder.call(this);
this._queue = [];
this._workingQueue = false;
}
module.exports = MockStream;
var emit = Responder.prototype.emit;
MockStream.prototype = {
__proto__: Responder.prototype,
emit: function() {
var args = Array.prototype.slice.call(arguments);
var self = this;
this._queue.push(function() {
emit.apply(self, args);
});
this._workQueue();
},
request: function(callback) {
if (this._requestCallback) {
throw new Error('request may only be called once.');
}
this._requestCallback = callback;
if (this.mockonrequest) {
this.mockonrequest();
}
},
_workQueue: function() {
if (!this._workingQueue && this._queue.length) {
var self = this;
var next = this._queue.shift();
this._workingQueue = true;
setTimeout(function() {
next();
self._workingQueue = false;
self._workQueue();
}, 0);
}
},
close: function() {
var self = this;
this._queue.push(function() {
self._requestCallback.apply(self, arguments);
});
this._workQueue();
}
};
});

View file

@ -0,0 +1,199 @@
define(function(require) {
'use strict';
var Factory = require('test/support/factory');
var nextTick = require('common/next_tick');
suite('provider/mock', function() {
var subject;
setup(function() {
var core = testSupport.calendar.core();
subject = core.providerFactory.get('Mock');
});
test('staged data events', function(done) {
var account = { user: 'user' };
var handle = subject.stageGetAccount(
'user',
null
);
var firedBefore = false;
handle.onbefore = function() {
firedBefore = true;
};
var firedAfter = false;
handle.onafter = function() {
firedAfter = true;
};
subject.getAccount(account, function() {
assert.ok(firedBefore, 'fires before event');
assert.ok(!firedAfter, 'after fires after callback');
nextTick(function() {
done(function() {
assert.ok(firedAfter, 'fires after');
});
});
});
assert.ok(!firedBefore, 'before is async');
});
suite('#getAccount', function() {
test('with staged account', function(done) {
var obj = {};
subject.stageGetAccount('user', null, obj);
subject.getAccount({ user: 'user' }, function(err, result) {
assert.ok(!err);
assert.equal(result, obj, 'uses staged object');
subject.getAccount({ user: 'user' }, function(err, result) {
assert.ok(!err);
assert.notEqual(obj, result, 'staged object is not resued');
done();
});
});
});
test('without mock signal', function(done) {
subject.getAccount({}, function(err, value) {
done(function() {
assert.ok(!err);
assert.deepEqual(value, {});
});
});
});
});
suite('#findCalendars', function() {
test('with staging', function(done) {
var list = {
one: Factory('remote.calendar', { id: 'one' })
};
subject.stageFindCalendars('user', null, list);
subject.findCalendars({ user: 'user' }, function(err, result) {
done(function() {
assert.ok(!err);
assert.deepEqual(list, result);
});
});
});
test('without staging', function(done) {
subject.findCalendars({}, function(err, list) {
done(function() {
assert.ok(!err);
assert.deepEqual(list, {});
});
});
});
});
suite('#syncEvents', function() {
var calendar;
var account;
setup(function() {
account = Factory('account');
calendar = Factory('calendar');
});
test('without staged data', function(done) {
subject.syncEvents({}, {}, done);
});
test('with staged data', function(done) {
var err = new Error('I win!');
subject.stageSyncEvents(
account.user,
calendar.remote.id,
err
);
subject.syncEvents(account, calendar, function(gotErr) {
done(function() {
assert.equal(err, gotErr);
});
});
});
});
suite('#calendarCapabilities', function() {
test('with staging data', function() {
subject.stageCalendarCapabilities(1, {
canDeleteEvent: false
});
assert.deepEqual(
subject.calendarCapabilities({ _id: 1 }),
{
canDeleteEvent: false,
canUpdateEvent: true,
canCreateEvent: true
}
);
});
test('without staging data', function() {
assert.deepEqual(
subject.calendarCapabilities({ _id: 1 }),
{
canDeleteEvent: true,
canUpdateEvent: true,
canCreateEvent: true
}
);
});
});
suite('#eventCapabilities', function() {
test('with staging data', function(done) {
subject.stageEventCapabilities(1, null, {
canDelete: false
});
subject.eventCapabilities({ _id: 1 }, function(err, result) {
if (err) {
return done(err);
}
done(function() {
assert.deepEqual(result, {
canCreate: true,
canDelete: false,
canUpdate: true
});
});
});
});
test('without staging data', function(done) {
subject.eventCapabilities({}, function(err, result) {
if (err) {
return done(err);
}
done(function() {
assert.deepEqual(result, {
canCreate: true,
canUpdate: true,
canDelete: true
});
});
});
});
});
});
});

View file

@ -0,0 +1,86 @@
define(function(require) {
'use strict';
var FakePage = require('test/support/fake_page');
var MockView = require('test/support/mock_view');
var router = require('router');
suite('router', function() {
var subject;
var page;
var View;
suiteSetup(function() {
View = MockView;
});
setup(function() {
page = FakePage;
subject = router;
subject.page = FakePage;
subject._activeObjects = [];
page.routes.length = 0;
});
test('initializer', function() {
assert.equal(subject.page, page, 'should have page');
assert.deepEqual(subject._activeObjects, [], 'should have active objects');
});
suite('#mangeObject', function() {
var object;
setup(function() {
object = {};
});
test('with onactive', function() {
var calledWith;
object.onactive = function() {
calledWith = arguments;
object.onactiveCalled = true;
};
subject.mangeObject(object, 'foo');
assert.ok(object.onactiveCalled);
assert.equal(subject._activeObjects[0], object);
assert.equal(calledWith[0], 'foo');
});
test('without onactive', function() {
subject.mangeObject(object);
assert.ok(!subject.__routerActive);
assert.equal(subject._activeObjects[0], object);
});
});
test('#clearObjects', function() {
var one = new View();
var two = new View();
two.active = one.active = true;
subject._activeObjects.push(one);
subject._activeObjects.push(two);
subject.clearObjects({});
assert.isFalse(one.active);
assert.isFalse(two.active);
});
var wrappedMethods = ['start', 'stop', 'show'];
wrappedMethods.forEach(function(method) {
test('#' + method, function() {
//hard to test because we are going
//to bind them to page to ensure the
//correct scope.
assert.ok(subject[method]);
});
});
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,101 @@
define(function(require, exports, module) {
'use strict';
var IcalRecurExpansion = require('service/ical_recur_expansion');
function request(file, cb) {
var xhr = new XMLHttpRequest();
xhr.open('GET', '/test/unit/' + file + '?' + Date.now(), true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status !== 200) {
cb(new Error('file not found or other error (' + file + ')', xhr));
} else {
cb(null, xhr.responseText);
}
}
};
xhr.send(null);
}
function camelize(string) {
var out = string.replace(/(?:^|[-_])(\w)/g, function(_, c) {
return c ? c.toUpperCase() : '';
});
return out[0].toLowerCase() + out.slice(1);
}
const ICAL_ROOT = 'fixtures/caldav/ical/';
var Handlers = {
/**
* Load and parse an ical sample
*
* @param {String} file location of ical file.
* @param {Function} node style callback: err, contents.
*/
ical: function icalLoader(file, callback) {
request(ICAL_ROOT + file + '.ics', function(err, text) {
if (err) {
callback(err);
return;
}
callback(null, text);
});
}
};
function Fixtures(type) {
var pending = 0;
var self = this;
var handler = Handlers[type];
function next() {
if (!(--pending)) {
if (self._error) {
throw self._error;
}
self.onready();
}
}
this.load = function loadFixture(file) {
pending++;
handler(file, function(err, contents) {
if (err) {
self._error = err;
} else {
self[camelize(file)] = contents;
}
next();
});
};
return this;
}
var Helper = {
Fixtures: Fixtures,
_originalExpansionLimit: null,
resetExpansionLimit: function() {
if (!Helper._originalExpansionLimit) {
IcalRecurExpansion.forEachLimit = Helper._originalExpansionLimit;
}
},
setExpansionLimit: function(newLimit) {
if (!Helper._originalExpansionLimit) {
Helper._originalExpansionLimit = IcalRecurExpansion.forEachLimit;
}
IcalRecurExpansion.forEachLimit = newLimit;
}
};
module.exports = Helper;
});

View file

@ -0,0 +1,275 @@
define(function(require) {
'use strict';
var CaldavService = require('service/caldav');
var ICAL = require('ext/ical');
var IcalRecurExpansion = require('service/ical_recur_expansion');
var Responder = require('common/responder');
var ServiceSupport = require('test/service/helper');
suite('service/ical_recur_expansion', function() {
var fixtures;
var subject;
var parseEvent;
var forEachLimit = 30;
suiteSetup(function() {
subject = IcalRecurExpansion;
// replace the maximum iterators with a smaller
// number suitable for tests.
ServiceSupport.setExpansionLimit(forEachLimit);
});
suiteTeardown(function() {
ServiceSupport.resetExpansionLimit();
});
// setup fixtures...
suiteSetup(function(done) {
fixtures = new ServiceSupport.Fixtures('ical');
fixtures.load('recurring_event');
fixtures.onready = done;
var service = new CaldavService(new Responder());
parseEvent = service.parseEvent.bind(service);
});
test('initialization', function() {
assert.ok(subject, 'exists');
});
suite('#forEach', function() {
var minDate;
var maxDate;
var inclusiveDates = [];
var iterator;
suiteSetup(function() {
// min will be the 5th date - 1 second
// max will be the 14th + 1 secound
// so 5-15 will be the range we expect.
parseEvent(fixtures.recurringEvent, function(err, event) {
var iter = event.iterator();
var num = 15;
while (--num) {
var last = iter.next();
if (num === 1) {
maxDate = last;
}
if (num === 10) {
minDate = last;
}
if (num <= 10) {
inclusiveDates.push(last.toJSDate());
}
}
minDate.second -= 1;
maxDate.second -= 1;
});
});
// create closed scope for sent/each for re-usability.
var verifyBounds = (function() {
var sent = [];
function each(item) {
sent.push(item.toJSDate());
}
function verifyMinMax() {
test('min & max', function() {
sent.length = 0;
var iter = subject.forEach(
event,
iterator,
each,
minDate,
maxDate
);
assert.deepEqual(
sent,
inclusiveDates
);
assert.deepEqual(
iter.last.toJSDate(),
inclusiveDates[inclusiveDates.length - 1]
);
});
}
function verifyMax() {
test('max', function() {
sent.length = 0;
var iter = subject.forEach(
event,
iterator,
each,
null,
maxDate
);
assert.deepEqual(
iter.last.toJSDate(),
inclusiveDates[inclusiveDates.length - 1],
'has last date'
);
});
}
function verifyMin() {
test('min', function() {
sent.length = 0;
subject.forEach(
event,
iterator,
each,
minDate
);
assert.deepEqual(
sent[0],
inclusiveDates[0]
);
assert.lengthOf(sent, forEachLimit, 'limit bounds');
});
test('min verify exclusive', function() {
sent.length = 0;
var exclusiveMin = minDate.clone();
exclusiveMin.second += 1;
subject.forEach(
event,
iterator,
each,
exclusiveMin
);
assert.deepEqual(
sent[0],
inclusiveDates[1]
);
assert.lengthOf(sent, forEachLimit, 'limit bounds');
});
}
return function verifyBounds() {
suite('bounds', function() {
verifyMax();
verifyMin();
verifyMinMax();
});
};
}());
var event;
setup(function(done) {
parseEvent(fixtures.recurringEvent, function(err, result) {
event = result;
done();
});
});
suite('with a pre-existing iterator', function() {
var last;
setup(function() {
iterator = event.iterator();
iterator.next();
last = iterator.next();
});
test('with a broken iterator', function() {
// round trip the data so we can clone it.
var iterStr = JSON.stringify(iterator);
var iterClone = new ICAL.RecurExpansion(
JSON.parse(iterStr)
);
var expected = [];
subject.forEach(event, iterClone, function(item) {
expected.push(item.toJSDate());
}, minDate, maxDate);
var sent = [];
subject.forEach(event, {}, function(item) {
sent.push(item.toJSDate());
}, minDate, maxDate);
assert.deepEqual(sent, expected);
});
test('resuming iteration', function() {
var expectedIter = event.iterator();
expectedIter.next();
expectedIter.next();
var expected = expectedIter.next();
var sent = [];
iterator = JSON.parse(
JSON.stringify(iterator)
);
subject.forEach(event, iterator, function(item) {
sent.push(item);
});
assert.deepEqual(
sent[0].toJSDate(),
expected.toJSDate()
);
assert.ok(
sent[0].compare(sent[1]) < 0,
'future instances are in the future'
);
});
verifyBounds();
});
suite('without an iterator', function() {
// ensure helpers have no iterators
setup(function() {
iterator = null;
});
test('without timeframe', function() {
var sent = [];
var iter = subject.forEach(event, null, function(last) {
sent.push(last);
});
assert.instanceOf(iter, ICAL.RecurExpansion);
assert.lengthOf(sent, forEachLimit);
// test sanity we need an infinite recur iterator
// or our tests simply suck...
assert.isFalse(iter.complete, 'should be incomplete');
});
verifyBounds();
});
});
});
});

View file

@ -0,0 +1,36 @@
(function(window) {
'use strict';
var getTestLoader = require('/js/ext/alameda.js').then(() => {
requirejs.config({
baseUrl: '/js',
paths: {
css: '/test/unit/support/css',
dom: '/test/unit/support/dom',
shared: '/shared/js',
sharedtest: '/shared/test/unit',
test: '/test/unit'
},
map: {
'*': {
'ext/page': 'test/support/fake_page'
}
},
shim: {
'ext/caldav': { exports: 'Caldav' },
'ext/ical': { exports: 'ICAL' },
'shared/gesture_detector': { exports: 'GestureDetector' },
'shared/notification_helper': { exports: 'NotificationHelper' },
'shared/intl_helper': { exports: 'IntlHelper' },
'sharedtest/mocks/mock_l10n': { exports: 'MockL10n' }
}
});
return new Promise(res => requirejs(['test/support/test_loader'], res));
});
window.testAgentRuntime.testLoader = function(path) {
return getTestLoader.then(testLoader => testLoader(path));
};
})(this);

View file

@ -0,0 +1,11 @@
define(function(require) {
'use strict';
var snakeCase = require('snake_case');
test('snake_case', function() {
assert.strictEqual(snakeCase('FooBarBaz'), 'foo_bar_baz');
assert.strictEqual(snakeCase('fooBarBaz'), 'foo_bar_baz');
});
});

View file

@ -0,0 +1,453 @@
define(function(require, exports, module) {
'use strict';
var Abstract = require('store/abstract');
var Factory = require('test/support/factory');
var Responder = require('common/responder');
var core = require('core');
suite('store/abstract', function() {
var subject;
var db;
setup(function(done) {
db = core.db;
subject = new Abstract(db);
// set _store to accounts so we can actually
// persist stuff.
subject._store = 'accounts';
db.open(function(err) {
assert.ok(!err);
done();
});
});
teardown(function(done) {
var trans = db.transaction(subject._store, 'readwrite');
var accounts = trans.objectStore(subject._store);
var res = accounts.clear();
res.onerror = function() {
done(new Error('could not wipe accounts db'));
};
res.onsuccess = function() {
done();
};
});
teardown(function() {
db.close();
});
test('initialization', function() {
assert.instanceOf(subject, Responder);
assert.deepEqual(subject._cached, {});
});
suite('#get', function() {
test('missing id', function(done) {
subject.get('FOO_NOT_HERE', function(err, record) {
done(function() {
assert.ok(!err);
assert.ok(!record);
});
});
});
suite('id present', function() {
var record;
setup(function(done) {
record = Factory('account', { _id: 'foo' });
subject.persist(record, done);
});
test('result', function(done) {
subject.get(record._id, function(err, result) {
done(function() {
assert.hasProperties(
record,
result,
'record matches persisted value'
);
});
});
});
});
});
suite('#persist', function() {
var events;
var id;
var object;
function watchEvent(event, done) {
subject.once(event, function() {
events[event] = arguments;
if (typeof(done) === 'function') {
done();
}
});
}
function checkEvent(event, id, object) {
var list = events[event];
assert.equal(list[0], id);
assert.equal(list[1], object);
}
suiteSetup(function() {
this.testData = {};
});
setup(function(done) {
object = this.testData.object;
events = {};
subject._addDependents = function() {};
if (this.testData.persist !== false) {
subject.persist(object, function(err, key) {
id = key;
});
watchEvent('add');
watchEvent('update');
watchEvent('persist', done);
} else {
done();
}
});
suite('with transaction', function() {
suiteSetup(function() {
this.testData.persist = false;
});
suiteTeardown(function() {
delete this.testData.persist;
});
test('result', function(done) {
var trans;
var obj = { name: 'foo' };
var callbackFired = false;
var transFired = false;
var pending = 2;
function next() {
pending--;
if (!pending) {
complete();
}
}
function complete() {
done(function() {
assert.isTrue(callbackFired);
assert.isTrue(transFired);
});
}
trans = core.db.transaction(
subject._store,
'readwrite'
);
trans.oncomplete = function() {
transFired = true;
next();
};
subject.persist(obj, trans, function() {
callbackFired = true;
next();
});
});
});
suite('update', function() {
var id = 'uniq';
suiteSetup(function() {
this.testData.persist = true;
this.testData.object = { providerType: 'local', _id: 'uniq' };
});
suiteTeardown(function() {
delete this.testData.persist;
delete this.testData.object;
});
test('update event', function() {
checkEvent('update', id, object);
});
test('persist event', function() {
checkEvent('persist', id, object);
});
test('db persistance', function(done) {
subject.get(id, function(err, result) {
if (err) {
done(err);
return;
}
done(function() {
assert.equal(object._id, id);
assert.equal(subject._cached[id], object);
assert.deepEqual(result.providerType, object.providerType);
});
});
});
});
suite('add', function() {
suiteSetup(function() {
this.testData.persist = true;
this.testData.object = { providerType: 'local' };
});
suiteTeardown(function() {
delete this.testData.persist;
delete this.testData.object;
});
test('add event', function() {
checkEvent('add', id, object);
});
test('persist event', function() {
checkEvent('persist', id, object);
});
test('db persistence', function(done) {
subject.get(id, function(err, result) {
if (err) {
done(err);
return;
}
done(function() {
assert.equal(object._id, id);
assert.equal(subject._cached[id], object);
assert.deepEqual(result.providerType, object.providerType);
});
});
});
});
});
suite('#remove', function() {
var id;
var removeEvent;
var callbackCalled = false;
var removeDepsCalled = false;
setup(function(done) {
subject.persist({ providerType: 'Local' }, function(err, key) {
if (err) {
done(new Error('could not add'));
} else {
id = key;
done();
}
});
});
setup(function(done) {
var preRemoveCalled = false;
callbackCalled = false;
removeDepsCalled = false;
subject._removeDependents = function() {
removeDepsCalled = arguments;
};
subject.once('preRemove', function(_id) {
assert.equal(id, _id, 'same id');
preRemoveCalled = true;
});
subject.remove(id, function() {
callbackCalled = true;
});
assert.ok(preRemoveCalled, 'removes event');
subject.once('remove', function() {
removeEvent = arguments;
// wait until next tick so other events
// have finished firing...
setTimeout(function() {
done();
}, 0);
});
});
test('event', function() {
assert.equal(removeEvent[0], id);
});
test('remove', function() {
assert.ok(callbackCalled);
assert.equal(removeDepsCalled[0], id);
assert.instanceOf(removeDepsCalled[1], IDBTransaction);
assert.ok(!subject._cached[id], 'should remove cached account');
});
});
suite('#count', function() {
setup(function(done) {
var trans = core.db.transaction(
subject._store,
'readwrite'
);
trans.oncomplete = function() {
done();
};
subject.persist(Factory('account'), trans);
subject.persist(Factory('account'), trans);
});
test('result', function(done) {
subject.count(function(err, number) {
done(function() {
assert.equal(number, 2);
});
});
});
});
suite('#all', function() {
var ids = [];
setup(function() {
ids.length = 0;
});
function add() {
setup(function(done) {
subject.persist({ providerType: 'Local' }, function(err, id) {
ids.push(id.toString());
done();
});
});
}
add();
add();
test('multiple all calls in parallel', function(done) {
var expected = 3;
var pending = expected;
var results = [];
function complete() {
assert.lengthOf(results, expected);
var idx = 1;
for (; idx < expected; idx++) {
for (var key in results[idx]) {
assert.equal(
results[0][key],
results[idx][key],
'objects are equal: ' + key
);
}
}
}
function next(err, list) {
if (err) {
return done(err);
}
var obj = Object.create(null);
for (var key in list) {
obj[key] = list[key];
}
results.push(obj);
if (!--pending) {
done(complete);
}
}
// load in parallel
subject.all(next);
subject.all(next);
subject.all(next);
});
test('results', function(done) {
var result;
function verify() {
var keys = Object.keys(result);
var key;
assert.deepEqual(
keys.sort(),
ids.sort()
);
for (key in result) {
var obj = result[key];
assert.ok(subject._cached[key]);
assert.ok(obj._id);
assert.equal(obj.providerType, 'Local');
}
}
subject.all(function(err, data) {
if (err) {
done(err);
}
result = data;
done(verify);
});
});
});
suite('#_objectData', function() {
test('with toJSON', function() {
var obj = {};
obj.toJSON = function() {
return 'foo';
};
assert.equal(subject._objectData(obj), 'foo');
});
test('without toJSON', function() {
var obj = Object.create(null);
obj.foo = '1';
assert.equal(subject._objectData(obj), obj);
});
});
});
});

View file

@ -0,0 +1,695 @@
define(function(require) {
'use strict';
var Abstract = require('store/abstract');
var AccountModel = require('models/account');
var CalendarError = require('common/error');
var CalendarModel = require('models/calendar');
var CalendarStore = require('store/calendar');
var Factory = require('test/support/factory');
suite('store/account', function() {
var subject;
var db;
var providerFactory;
var calendarStore;
setup(function(done) {
var core = testSupport.calendar.core();
db = core.db;
providerFactory = core.providerFactory;
subject = core.storeFactory.get('Account');
calendarStore = core.storeFactory.get('Calendar');
db.open(function(err) {
assert.ok(!err);
done();
});
});
teardown(function(done) {
subject._clearCache();
calendarStore._clearCache();
testSupport.calendar.clearStore(
db,
['accounts', 'calendars'],
done
);
});
teardown(function() {
db.close();
});
test('initialization', function() {
assert.instanceOf(subject, Abstract);
assert.deepEqual(subject._cached, {});
});
suite('#markWithError', function() {
var accounts = testSupport.calendar.dbFixtures(
'account',
'Account', {
one: { _id: 55, providerType: 'Mock' }
}
);
testSupport.calendar.dbFixtures(
'calendar',
'Calendar', {
one: { _id: 'one', accountId: 55 },
two: { _id: 'two', accountId: 55 }
}
);
var model;
setup(function() {
model = accounts.one;
});
suite('marking error', function() {
var error;
setup(function(done) {
error = new CalendarError.Authentication();
subject.markWithError(model, error, done);
});
function markedWithError(expectedCount) {
expectedCount = expectedCount || 1;
test('after erorr mark #' + expectedCount, function(done) {
subject.get(model._id, function(getErr, result) {
done(function() {
assert.ok(!getErr, 'is successful');
assert.equal(result.error.count, expectedCount, 'has count');
assert.equal(
result.error.name,
error.name,
'model is marked with error'
);
assert.instanceOf(
result.error.date,
Date,
'has date of occurrence'
);
});
});
});
}
markedWithError(1);
suite('second mark', function() {
setup(function(done) {
subject.markWithError(model, error, done);
});
markedWithError(2);
});
});
suite('dependant calendars', function() {
var err;
setup(function(done) {
err = new CalendarError.Authentication();
subject.markWithError(model, err, done);
});
function verifyCalendar(key) {
test('ensure calendar is marked: ' + key, function(done) {
calendarStore.get(key, function(getErr, result) {
done(function() {
assert.ok(result, 'has calendar');
assert.ok(result.error, 'sets error');
assert.equal(result.error.name, err.name, 'sets error');
});
});
});
}
verifyCalendar('one');
verifyCalendar('two');
});
});
suite('#availablePresets', function() {
var presetAccount;
var presets = {
'multiUse': {},
'singleUse': {
singleUse: true
}
};
test('without single use', function(done) {
subject.availablePresets({ 'foo': {} }, function(err, available) {
if (err) {
done();
}
done(function() {
assert.deepEqual(available, ['foo']);
});
});
});
test('when single use is available', function(done) {
subject.availablePresets(presets, function(err, available) {
if (err) {
done(err);
}
done(function() {
assert.deepEqual(available, ['multiUse', 'singleUse']);
});
});
});
suite('when single use is used', function() {
setup(function(done) {
// stage account
presetAccount = Factory('account', {
preset: 'singleUse'
});
subject.persist(presetAccount, done);
});
test('exclusion of single use preset', function(done) {
subject.availablePresets(presets, function(err, available) {
if (err) {
return done(err);
}
done(function() {
assert.deepEqual(available, ['multiUse']);
});
});
});
});
});
suite('#verifyAndPersist', function() {
var error;
var result;
var model;
var calledWith;
var modelParams;
var originalCaldav;
setup(function() {
error = null;
result = {};
modelParams = Factory.build('account', {
providerType: 'Caldav'
});
model = new AccountModel(modelParams);
originalCaldav = providerFactory.providers.Caldav;
providerFactory.providers.Caldav = {
getAccount: function(details, callback) {
calledWith = details;
setTimeout(function() {
callback(error, result);
}, 0);
}
};
});
teardown(function() {
providerFactory.providers.Caldav = originalCaldav;
});
suite('duplicate account failure', function() {
var existingAccount;
var existingParams = {
providerType: 'Caldav',
user: 'foobar',
fullUrl: 'http://google.com/foo'
};
function sendsDuplicateError(done) {
return function(err, id, model) {
done(function() {
assert.ok(!id, 'is not persisted');
assert.ok(err, 'sends error on duplicate account');
assert.equal(err.name, 'account-exist');
});
};
}
setup(function(done) {
existingAccount = Factory('account', existingParams);
subject.persist(existingAccount, done);
});
test('initial input is duplicate', function(done) {
var account = Factory('account', existingParams);
subject.verifyAndPersist(account, sendsDuplicateError(done));
});
test('input is updated to be duplicate', function(done) {
var account = Factory('account', existingParams);
account.user = '';
result = { user: existingParams.user };
subject.verifyAndPersist(account, sendsDuplicateError(done));
});
});
suite('existing account', function() {
// setup an account to modify
setup(function(done) {
model.error = {};
subject.persist(model, done);
});
// bug 870512
test('should be able to update', function(done) {
result = {};
model.password = 'new';
subject.verifyAndPersist(model, function(err, data) {
if (err) {
return done(err);
}
subject.get(model._id, function(getErr, result) {
if (getErr) {
return done(err);
}
done(function() {
assert.equal(result.password, 'new', 'updates pass');
assert.ok(!result.error, 'clears errors');
});
});
});
});
});
// mock out the provider
test('when verify fails', function(done) {
error = new Error('bad stuff');
subject.verifyAndPersist(model, function(err, data) {
done(function() {
assert.ok(!data);
assert.equal(err, error);
assert.deepEqual(calledWith, model.toJSON());
});
});
});
test('persist + url change', function(done) {
result = Factory('caldav.account');
subject.verifyAndPersist(model, function(err, id, data) {
done(function() {
assert.instanceOf(data, AccountModel);
assert.equal(data.domain, result.domain);
assert.equal(data.entrypoint, result.entrypoint);
assert.equal(data.calendarHome, result.calendarHome);
});
});
});
test('persist + oauth change', function(done) {
model.oauth = { code: 'xxx' };
result = Factory('caldav.account', {
oauth: { refresh_token: 'xxx' }
});
subject.verifyAndPersist(model, function(err, id, data) {
done(function() {
assert.equal(data.oauth, result.oauth);
});
});
});
test('persist no change', function(done) {
result = {};
subject.verifyAndPersist(model, function(err, id, data) {
done(function() {
assert.instanceOf(data, AccountModel);
assert.equal(data.domain, modelParams.domain);
assert.equal(data.calendarHome, modelParams.calendarHome);
});
});
});
});
suite('#remove', function() {
var model;
var calendars;
setup(function(done) {
calendars = {};
model = subject._createModel({ providerType: 'Local' });
subject.persist(model, done);
});
setup(function(done) {
assert.ok(model._id);
// we will eventually remove this
calendars[1] = new CalendarModel({
accountId: model._id,
remote: { id: 777 }
});
calendarStore.persist(calendars[1], done);
});
setup(function(done) {
calendars[2] = new CalendarModel({
accountId: 'some-other',
remote: { id: 666 }
});
// this is our control to ensure
// we are not removing extra stuff
calendarStore.persist(calendars[2], done);
});
suite('removal', function() {
var id;
setup(function(done) {
id = model._id;
subject.remove(id, done);
});
test('removes account', function(done) {
subject.get(id, function(err, result) {
done(function() {
assert.ok(!result, 'removes account');
});
});
});
test('removes associated calendars', function(done) {
calendarStore.all(function(err, calendars) {
done(function() {
assert.ok(calendars.accountId != id, 'removes calendars');
});
});
});
});
});
suite('#_createModel', function() {
test('with id', function() {
var result = subject._createModel({ providerType: 'Local' }, 'id');
assert.equal(result.providerType, 'Local');
assert.equal(result._id, 'id');
assert.instanceOf(result, AccountModel);
});
test('without id', function() {
var result = subject._createModel({ providerType: 'Local' });
assert.equal(result.providerType, 'Local');
assert.isFalse(('_id' in result));
});
});
suite('#syncableAccounts', function() {
var accounts = testSupport.calendar.dbFixtures(
'account',
'Account', {
nosync: { _id: 55, providerType: 'Local' },
sync: { _id: 56, providerType: 'Caldav' }
}
);
var results;
setup(function(done) {
subject.syncableAccounts(function(err, list) {
if (err) {
return done(err);
}
results = list;
done();
});
});
test('found accounts', function() {
assert.lengthOf(results, 1);
assert.equal(results[0]._id, accounts.sync._id);
});
suite('no syncable accounts', function() {
setup(function(done) {
subject.remove(accounts.sync._id, done);
});
test('result', function(done) {
subject.syncableAccounts(function(err, list) {
if (err) {
return done(err);
}
assert.equal(list.length, 0);
done();
});
});
});
});
suite('#sync: add, remove, update', function() {
var remote;
var events;
var account;
var cals;
var remoteCalledWith;
function watchEvent(eventName) {
calendarStore.on(eventName, function() {
if (!(eventName in events)) {
events[eventName] = [];
}
events[eventName].push(arguments);
});
}
setup(function() {
account = Factory.create('account', {
_id: 1,
providerType: 'Mock'
});
cals = {};
cals.add = Factory('calendar', {
accountId: account._id,
remote: { name: 'add' }
});
cals.add2 = Factory('calendar', {
accountId: account._id,
remote: { name: 'add2' }
});
cals.remove = Factory('calendar', {
accountId: account._id,
remote: { name: 'remove' }
});
cals.update = Factory('calendar', {
accountId: account._id,
// this color won't be used since it is not part of the palette (test
// case where user is updating the app and already have calendars
// stored in the DB)
remote: { name: 'update', color: '#00FFCC' },
error: {}
});
});
setup(function(done) {
calendarStore.persist(cals.update, done);
});
setup(function(done) {
calendarStore.persist(cals.remove, done);
});
setup(function(done) {
// clear cache
calendarStore._remoteByAccount = Object.create(null);
calendarStore._cached = Object.create(null);
calendarStore._usedColors = [];
// reload from db
calendarStore.all(done);
});
setup(function(done) {
events = {};
remoteCalledWith = null;
watchEvent('add');
watchEvent('update');
watchEvent('remove');
remote = {};
remote[cals.update.remote.id] = {
id: cals.update.remote.id,
name: 'update!',
description: 'new desc',
// this color will be ignored
color: '#F00'
};
remote[cals.add.remote.id] = {
id: cals.add.remote.id,
name: 'new item'
};
remote[cals.add2.remote.id] = {
id: cals.add2.remote.id,
name: 'add 2 calendar',
// this color will be ignored
color: '#0FC'
};
providerFactory.get('Mock').stageFindCalendars(
account.user,
null,
remote
);
subject.sync(account, function(err) {
if (err) {
return done(err);
}
done();
});
});
var syncResults;
setup(function(done) {
calendarStore.remotesByAccount(account._id, function(err, list) {
done(function() {
syncResults = list;
});
});
});
test('after sync', function() {
assert.equal(
Object.keys(syncResults).length, 3,
'should only have three records'
);
// EVENTS
assert.ok(events.remove[0][0]);
var updateObj = events.update[0][1].remote;
assert.equal(
updateObj.id,
cals.update.remote.id
);
var addObj = events.add[0][1].remote;
assert.equal(
addObj.id,
cals.add.remote.id
);
var add2Obj = events.add[1][1].remote;
assert.equal(
add2Obj.id,
cals.add2.remote.id
);
var remoteUpdate = syncResults[cals.update.remote.id];
var remoteAdd = syncResults[cals.add.remote.id];
var remoteAdd2 = syncResults[cals.add2.remote.id];
var palette = CalendarStore.REMOTE_COLORS;
// update
assert.instanceOf(
remoteUpdate,
CalendarModel,
'should update cache'
);
assert.ok(!remoteUpdate.error, 'removes error');
assert.equal(
remoteUpdate.remote.description,
'new desc',
'should update changed descripton'
);
assert.equal(
remoteUpdate.name,
'update!',
'should update changed name'
);
assert.equal(
remoteUpdate.color,
palette[0],
'should ignore color from remote and only use colors from palette'
);
// add
assert.instanceOf(
remoteAdd,
CalendarModel,
'should add new calendar'
);
assert.equal(
remoteAdd.name,
'new item',
'should use remote data when creating new calendar'
);
assert.equal(
remoteAdd.color,
palette[1],
'should add new color from palette'
);
// add 2
assert.instanceOf(
remoteAdd,
CalendarModel,
'should add new calendar'
);
assert.equal(
remoteAdd2.name,
'add 2 calendar',
'should use remote data when creating new calendar'
);
assert.equal(
remoteAdd2.color,
palette[2],
'should add new color from palette'
);
assert.notEqual(
remoteAdd.color,
remoteAdd2.color,
'each calendar should use a different color'
);
});
});
});
});

View file

@ -0,0 +1,438 @@
define(function(require) {
'use strict';
var Abstract = require('store/abstract');
var Calc = require('common/calc');
var Factory = require('test/support/factory');
var Responder = require('common/responder');
var notificationsController = require('controllers/notifications');
suite('store/alarm', function() {
var core;
var subject;
var db;
setup(function(done) {
core = testSupport.calendar.core();
db = core.db;
subject = core.storeFactory.get('Alarm');
db.open(done);
});
teardown(function(done) {
testSupport.calendar.clearStore(
core.db,
['alarms'],
done
);
});
teardown(function() {
db.close();
});
suite('#findAllByBusytimeId', function() {
suite('existing', function() {
var alarms;
var busytimeId = 'xfoo';
var busytime = { _id: busytimeId };
setup(function(done) {
alarms = [];
var trans = db.transaction('alarms', 'readwrite');
for (var i = 0; i < 3; i++) {
var alarm = Factory('alarm', { busytimeId: busytimeId });
alarms.push(alarm);
subject.persist(alarm, trans);
}
trans.oncomplete = function() {
done();
};
trans.onerror = function(e) {
done(e.target.error);
};
});
test('result', function(done) {
subject.findAllByBusytimeId(busytime._id, function(err, result) {
done(function() {
assert.ok(!err);
assert.equal(result.length, alarms.length);
});
});
});
});
/*
Error: expected false to be truthy (for the second assert)
this works when executing only this file but not when executing all
calendar tests.
Disabled in Bug 838993, to be enabled asap in Bug 840489
test('missing', function(done) {
subject.findAllByBusytimeId('foo', function(err, result) {
try {
assert.ok(!err);
assert.ok(!result);
done();
} catch (e) {
done(e);
}
});
});
*/
});
suite('#_objectData', function() {
var alarm;
var time = new Date(2018, 0, 1);
setup(function(done) {
alarm = Factory('alarm', { _id: 'a1', startDate: time });
subject.persist(alarm, done);
});
test('get', function(done) {
subject.get('a1', function(err, result) {
done(function() {
assert.ok(result.startDate);
assert.deepEqual(
result.trigger,
result.startDate,
'pending trigger should be same date as trigger'
);
});
});
});
});
suite('#_addDependents', function() {
var worksQueue = 0;
var auto;
var originalWork;
setup(function() {
worksQueue = 0;
auto = subject.autoQueue;
originalWork = subject.workQueue;
subject.autoQueue = true;
subject.workQueue = function() {
worksQueue++;
};
});
teardown(function() {
subject.autoQueue = auto;
subject.workQueue = originalWork;
});
test('after persist transaction', function(done) {
var trans = core.db.transaction('alarms', 'readwrite');
subject.persist({}, trans);
subject.persist({}, trans);
subject.persist({}, trans);
subject.persist({}, trans);
trans.addEventListener('complete', function() {
done(function() {
assert.equal(worksQueue, 1);
});
});
});
});
test('initialization', function() {
assert.instanceOf(subject, Abstract);
assert.deepEqual(subject._cached, {});
});
function add(date, busytime, event, floating) {
setup(function(done) {
var record = Factory('alarm', {
startDate: date,
busytimeId: busytime || 'busyId',
eventId: event || 'eventId'
});
if (floating) {
record.startDate.offset = 0;
record.startDate.tzid = Calc.FLOATING;
}
subject.persist(record, done);
});
}
suite('#workQueue', function() {
var getAllResults = [];
var added = [];
var lastId = 0;
var now = new Date(2018, 0, 1);
var realApi;
var mockApi = {
getAll: function() {
var req = new Responder();
setTimeout(function() {
req.result = getAllResults.concat([]);
var event = {
target: req
};
if (req.onsuccess) {
req.onsuccess(event);
}
req.emit('success', event);
}, 1);
return req;
},
add: function(date, tz, data) {
added.push(Array.prototype.slice.call(arguments));
var req = new Responder();
setTimeout(function() {
lastId++;
if (req.onsuccess) {
req.onsuccess(lastId);
}
req.emit('success', lastId);
}, 1);
return req;
}
};
suiteSetup(function() {
realApi = navigator.mozAlarms;
});
suiteTeardown(function() {
navigator.mozAlarms = realApi;
});
var allAlarms, handleAlarm, onAlarm;
setup(function() {
allAlarms = [];
handleAlarm = null;
added.length = 0;
getAllResults.length = 0;
navigator.mozAlarms = mockApi;
onAlarm = notificationsController.onAlarm;
notificationsController.onAlarm = function() {
handleAlarm = arguments;
allAlarms.push(handleAlarm);
};
});
teardown(function() {
// Restore notificationsController.onAlarm
notificationsController.onAlarm = onAlarm;
});
suite('without alarm api', function() {
test('should silently stop', function(done) {
navigator.mozAlarms = null;
subject.workQueue(function() {
done();
});
});
});
suite('alarm in the past', function() {
var now = new Date();
now.setMilliseconds(-1);
add(now, 'busyMe');
// Bug 857284 - We should only fire one alarm when there are multiple
// valid alarms for a single event.
var duplicate = new Date();
duplicate.setMilliseconds(-61);
add(duplicate, 'busyMe');
setup(function(done) {
subject.workQueue(() => done());
});
test('passing alarm directly to controller', function() {
var alarm = handleAlarm[0];
// verify right alarm is sent
assert.equal(alarm.busytimeId, 'busyMe');
// verify only one alarm sent
assert.lengthOf(allAlarms, 1);
});
});
suite('alarm in db and no alarm with 48 hours', function() {
add(new Date(2018, 0, 3), 3);
setup(function(done) {
getAllResults.push({
data: Factory('alarm', {
trigger: new Date(),
eventId: 'xx'
})
});
subject.workQueue(now, () => done());
});
test('after', function() {
assert.lengthOf(added, 0);
});
});
suite('unrelated alarm in db', function() {
add(new Date(2018, 0, 3), 3);
setup(function(done) {
getAllResults.push({
data: { _randomField: true }
});
subject.workQueue(now, () => done());
});
test('after complete', function() {
assert.lengthOf(added, 1);
assert.deepEqual(
added[0][0],
new Date(2018, 0, 3)
);
});
});
suite('no alarm in db and no alarm within 48 hours', function() {
add(new Date(2018, 0, 3), 3);
setup(function(done) {
subject.workQueue(now, () => done());
});
test('after complete', function() {
assert.lengthOf(added, 1);
assert.deepEqual(
added[0][0],
new Date(2018, 0, 3)
);
});
});
suite('initial add', function() {
// 5 hours from now floating time
add(new Date(2018, 0, 1, 5), 1, 1, true);
// 23 hours from now
add(new Date(2018, 0, 1, 23), 2);
// over 48 hours from now (yes this works)
// this record should be skipped and left
// in the database.
add(new Date(2018, 0, 3), 3);
setup(function(done) {
subject.workQueue(now, () => done());
});
/*
was disabled by Bug 838993
to be reenabled by Bug 840489
Error: has record: expected [ { _id: 1, busytimeId: 'foo' },
{ startDate: { utc: 1514782800000, offset: 0, tzid: 'floating' },
busytimeId: 1,
eventId: 1,
_id: 40 },
{ startDate: { utc: 1514847600000, offset: 3600000 },
busytimeId: 2,
eventId: 'eventId',
_id: 41 },
{ startDate: { utc: 1514937600000, offset: 3600000 },
busytimeId: 3,
eventId: 'eventId',
trigger: { utc: 1514937600000, offset: 3600000 },
_id: 42 } ] to have a length of 3 but got 4
note: this works when running only this file, but not when we run the
whole calendar tests
test('alarms', function(done) {
getAll(function(records) {
done(function() {
assert.length(records, 3, 'has record');
var hasTrigger = records.filter(function(item) {
return 'trigger' in item;
});
assert.length(hasTrigger, 1);
assert.equal(hasTrigger[0].busytimeId, 3);
});
});
});
*/
test('after complete', function() {
assert.lengthOf(added, 2);
assert.deepEqual(added[0][0], new Date(2018, 0, 1, 5));
assert.equal(
added[0][1], 'ignoreTimezone', 'floating time ignoreTimezone'
);
assert.hasProperties(added[0][2], {
busytimeId: 1
});
assert.deepEqual(added[1][0], new Date(2018, 0, 1, 23), 'abs trigger');
assert.equal(added[1][1], 'honorTimezone', 'abs time honorTimezone');
assert.hasProperties(added[1][2], {
busytimeId: 2
});
});
/*
Error: expected 4 to equal 3
this works when executing only this file but not when executing all
calendar tests.
Disabled in Bug 838993, to be enabled asap in Bug 840489
test('second work queue', function(done) {
added.length = 0;
subject.workQueue(function() {
// verify add
assert.equal(added[0][2].busytimeId, 3);
// verify we didn't delete the records
subject.count(function(err, len) {
done(function() {
assert.equal(len, 3);
});
});
});
});
*/
});
});
});
});

View file

@ -0,0 +1,286 @@
define(function(require) {
'use strict';
var Factory = require('test/support/factory');
var Timespan = require('common/timespan');
var core = require('core');
suite('store/busytime', function() {
var subject;
var db;
var id = 0;
setup(function(done) {
id = 0;
db = core.db;
subject = core.storeFactory.get('Busytime');
db.open(function(err) {
assert.ok(!err);
done();
});
});
teardown(function(done) {
testSupport.calendar.clearStore(
core.db,
done
);
});
teardown(function() {
core.db.close();
});
suite('#_removeDependents', function() {
suite('alarm store deps', function() {
var alarmStore;
var busytime;
function createTrans(done) {
var trans = core.db.transaction(
['busytimes', 'alarms'], 'readwrite'
);
if (done) {
trans.addEventListener('complete', function() {
done();
});
}
}
// create records
setup(function(done) {
var trans = createTrans(done);
alarmStore = core.storeFactory.get('Alarm');
busytime = Factory('busytime', { _id: 'foo' });
subject.persist(busytime, trans);
alarmStore.persist({ _id: 1, busytimeId: busytime._id }, trans);
});
test('count check', function(done) {
var pending = 2;
var alarmCount = 0;
var busytimeCount = 0;
function next() {
if (!(--pending)) {
done(function() {
assert.equal(busytimeCount, 1, 'busytime');
assert.equal(alarmCount, 1, 'alarm');
});
}
}
subject.count(function(err, value) {
busytimeCount = value;
next();
});
alarmStore.count(function(err, value) {
alarmCount = value;
next();
});
});
test('after delete', function(done) {
subject.remove(busytime._id, function() {
alarmStore.count(function(err, value) {
done(function() {
assert.equal(value, 0, 'removes alarm');
});
});
});
});
});
});
suite('#loadSpan', function() {
var list;
var span;
setup(function() {
list = Object.create(null);
span = new Timespan(
new Date(2012, 1, 5),
new Date(2012, 1, 10)
);
});
function add(name, start, end) {
setup(function(done) {
var item = list[name] = Factory('busytime', {
startDate: start,
endDate: end
});
delete item.startDate;
delete item.endDate;
subject.persist(item, done);
});
}
add('before long', new Date(2011, 1, 1), new Date(2011, 3, 1));
add('overlap', new Date(2012, 1, 1), new Date(2013, 1, 1));
add('starts before', new Date(2012, 1, 3), new Date(2012, 1, 6));
add('during', new Date(2012, 1, 5), new Date(2012, 1, 9));
add('ends after', new Date(2012, 1, 9), new Date(2012, 1, 11));
add('after', new Date(2012, 1, 12), new Date(2012, 1, 15));
var results;
var expectedIds;
function expected(name) {
expectedIds.push(
list[name]._id
);
}
setup(function(done) {
// because we just added events
// we need to remove them from the cache
subject._setupCache();
// build the list of expected
// busytimes to be returned by their
// event id
expectedIds = [];
// order is important we expect them
// to be sorted by start date
expected('overlap');
expected('starts before');
expected('during');
expected('ends after');
// load
subject.loadSpan(span, function(err, data) {
if (err) {
return done(err);
}
results = data;
// wait until next tick for event
// to fire...
setTimeout(done, 0, null);
});
});
test('load results', function() {
// verify correct data is returned;
var idMap = Object.create(null);
results.forEach(function(item) {
// verify startDate/endDate is present
var model = JSON.parse(JSON.stringify(item));
model = subject.initRecord(model);
assert.hasProperties(
item,
model,
'is model'
);
var id = item._id;
if (!(id in idMap)) {
idMap[id] = true;
}
});
var actualIds = Object.keys(idMap);
assert.deepEqual(
actualIds,
expectedIds,
'load event ids'
);
});
});
suite('#_createModel', function() {
var start = new Date(2012, 7, 1);
var end = new Date(2012, 7, 8);
var busy;
setup(function(done) {
busy = Factory('busytime', {
startDate: start,
endDate: end
});
subject.persist(busy, done);
});
test('db-round trip', function(done) {
subject.get(busy._id, function(err, record) {
done(function() {
assert.deepEqual(record.startDate, start, 'startDate');
assert.deepEqual(record.endDate, end, 'endDate');
});
});
});
});
suite('#removeEvent', function() {
var remove;
var keep;
var removeEvents = [];
setup(function(done) {
removeEvents.length = 0;
var trans = core.db.transaction(
'busytimes', 'readwrite'
);
remove = Factory('busytime', {
eventId: 'remove'
});
keep = Factory('busytime', {
eventId: 'keep'
});
subject.persist(remove, trans);
subject.persist(keep, trans);
trans.oncomplete = function() {
done();
};
});
setup(function(done) {
subject.on('remove', function(id) {
removeEvents.push(id);
});
subject.removeEvent('remove', done);
});
test('removed busytime', function(done) {
assert.deepEqual(
removeEvents,
[remove._id]
);
subject.get(remove._id, function(err, record) {
done(function() {
assert.ok(!record);
});
});
});
test('control - kept busytime', function(done) {
subject.get(keep._id, function(err, record) {
done(function() {
assert.ok(record);
});
});
});
});
});
});

View file

@ -0,0 +1,459 @@
define(function(require) {
'use strict';
var Abstract = require('store/abstract');
var AccountModel = require('models/account');
var CalendarError = require('common/error');
var CalendarModel = require('models/calendar');
var CalendarStore = require('store/calendar');
var Factory = require('test/support/factory');
var core = require('core');
var localCalendarId = require('common/constants').localCalendarId;
suite('store/calendar', function() {
var subject;
var db;
var model;
setup(function(done) {
db = core.db;
subject = core.storeFactory.get('Calendar');
model = Factory('calendar', {
_id: 1,
remote: { id: 'uuid' },
accountId: 'acc1'
});
db.open(function(err) {
assert.ok(!err);
done();
});
});
testSupport.calendar.accountEnvironment();
teardown(function(done) {
testSupport.calendar.clearStore(
db,
['accounts', 'calendars', 'events', 'busytimes'],
done
);
});
teardown(function() {
db.close();
});
test('initialization', function() {
assert.instanceOf(subject, Abstract);
assert.equal(subject._store, 'calendars');
});
suite('cache handling', function() {
setup(function() {
subject._addToCache(model);
});
test('#_addToCache', function() {
assert.equal(subject._cached[1], model);
});
test('#_removeFromCache', function() {
subject._removeFromCache(1);
assert.ok(!subject._cached[1]);
});
});
suite('#markWithError', function() {
var calendar;
setup(function(done) {
calendar = Factory('calendar');
subject.persist(calendar, done);
});
test('success', function(done) {
var err = new CalendarError.Authentication();
subject.markWithError(calendar, err, function(markErr) {
assert.ok(!markErr);
subject.get(calendar._id, function(getErr, result) {
done(function() {
assert.ok(result.error, 'has error');
assert.equal(result.error.name, err.name, 'set error');
});
});
});
});
});
suite('#persist', function() {
var stubUpdateColor;
setup(function() {
stubUpdateColor = sinon.stub(subject, '_updateCalendarColor');
});
teardown(function() {
stubUpdateColor.restore();
});
test('error case', function(done) {
var sample = Factory.create('calendar');
subject.persist(sample, function(err, data) {
sinon.assert.calledOnce(stubUpdateColor);
sinon.assert.calledWith(stubUpdateColor, sample);
done();
});
});
});
suite('#_createModel', function() {
var remote = {};
test('with id', function() {
var result = subject._createModel({
remote: remote
}, 'id');
assert.equal(result.remote, remote);
assert.equal(result._id, 'id');
assert.instanceOf(result, CalendarModel);
});
test('without id', function() {
var result = subject._createModel({
remote: remote
});
assert.equal(result.remote, remote);
assert.isFalse(('_id' in result));
});
});
suite('#remotesByAccount', function() {
var expected;
var models = testSupport.calendar.dbFixtures('calendar', 'Calendar', {
one: { accountId: 1, remote: { id: 'one' } },
two: { accountId: 1, remote: { id: 'two' } },
three: { accountId: 2, remote: { id: 'three' } }
});
setup(function(done) {
subject.persist(model, done);
});
function verify(accountId, done) {
subject.remotesByAccount(accountId, function(err, list) {
if (err) {
done(err);
}
done(function() {
var expectedIds = Object.keys(expected).sort();
assert.deepEqual(
Object.keys(list).sort(),
expectedIds,
'has same keys'
);
expectedIds.forEach(function(id) {
assert.hasProperties(
expected[id],
list[id],
id
);
});
});
});
}
test('one calendar', function(done) {
expected = {
three: models.three
};
verify(2, done);
});
test('no calendars', function(done) {
expected = {};
verify(3, done);
});
test('multiple calendars', function(done) {
expected = {
one: models.one,
two: models.two
};
verify(1, done);
});
});
suite('#sync - provider skip', function() {
var account, calendar;
setup(function() {
account = Factory('account', { providerType: 'Local' });
calendar = Factory('calendar', {
_id: 1,
lastEventSyncToken: null,
remote: { syncToken: 'synced' }
});
});
setup(function(done) {
core.storeFactory.get('Account').persist(account, done);
});
setup(function(done) {
subject.persist(calendar, done);
});
test('result', function(done) {
// should not sync because local cannot sync
subject.sync(account, calendar, function() {
done();
});
});
});
suite('#remove', function() {
var eventStore;
var stubRemoveColor;
var model;
var events;
setup(function(done) {
// setup fixtures
eventStore = core.storeFactory.get('Event');
events = {};
// transaction for initial creation of records.
var trans = core.db.transaction(
subject._dependentStores,
'readwrite'
);
// setup calendars
model = Factory('calendar', { accountId: 1 });
subject.persist(model, trans);
// setup events
events[1] = Factory('event', { calendarId: model._id });
events[2] = Factory('event', { calendarId: 'some-other' });
// we will eventually remove this record.
eventStore.persist(events[1], trans);
// this is our control to ensure
// we are not removing extra stuff
eventStore.persist(events[2], done);
trans.addEventListener('complete', function() {
done();
});
});
setup(function() {
stubRemoveColor = sinon.stub(subject, '_removeCalendarColorFromCache');
});
teardown(function() {
stubRemoveColor.restore();
});
setup(function(done) {
subject.remove(model._id, function() {
eventStore.count(function(err, count) {
assert.equal(count, 1);
sinon.assert.calledOnce(stubRemoveColor);
sinon.assert.calledWith(stubRemoveColor, model._id);
done();
});
});
});
test('after remove', function(done) {
eventStore.get(events[1]._id, function(err, result) {
done(function() {
assert.ok(!result);
});
});
});
});
suite('#ownersOf', function() {
test('given an id', function(done) {
var id = this.calendar._id;
subject.ownersOf(id, (err, owners) => {
done(() => {
assert.instanceOf(owners.account, AccountModel);
assert.instanceOf(owners.calendar, CalendarModel);
assert.equal(owners.account._id, this.account._id, 'account id');
assert.equal(owners.calendar._id, this.calendar._id, 'calendar id');
});
});
});
});
test('#providerFor', function(done) {
subject.providerFor(this.calendar, function(err, provider) {
done(function() {
assert.equal(provider, core.providerFactory.get('Mock'));
});
});
});
suite('#_updateCalendarColor', function(done) {
var palette = CalendarStore.REMOTE_COLORS;
function resetUsedColors() {
subject._usedColors.length = 0;
}
setup(resetUsedColors);
teardown(resetUsedColors);
test('> local calendar', function() {
var calendar = Factory('calendar', {
color: '#BADA55',
_id: localCalendarId
});
subject._updateCalendarColor(calendar);
assert.equal(
calendar.color,
CalendarStore.LOCAL_COLOR,
'should use local calendar color'
);
});
suite('> remote calendars', function() {
suite('> add', function() {
test('return first unused color from palette', function() {
assert.ok(palette.length, 'palette');
palette.forEach(function(color) {
assert.ok(
subject._usedColors.indexOf(color) === -1,
'should not repeat color ' + color
);
// it will ignore color from the remote and use color from palette
// instead
var calendar = { color: '#00FFCC' };
subject._updateCalendarColor(calendar);
assert.equal(
calendar.color,
color,
'color should match'
);
assert.ok(
subject._usedColors.indexOf(color) !== -1,
'should update the used colors ' + color
);
});
});
});
suite('> many calendars', function() {
setup(function() {
subject._usedColors = palette.slice(0, palette.length - 1);
});
test('loop colors if too many calendars', function() {
var calendar;
calendar = {};
subject._updateCalendarColor(calendar);
assert.equal(
calendar.color,
palette[palette.length - 1],
'last color'
);
calendar = {};
subject._updateCalendarColor(calendar);
assert.equal(
calendar.color,
palette[0],
'first color'
);
calendar = {};
subject._updateCalendarColor(calendar);
assert.equal(
calendar.color,
palette[1],
'second color'
);
});
});
suite('> update', function() {
var foo, bar;
setup(function() {
foo = { color: palette[3], _id: 'foo' };
bar = { color: '#F00', _id: 'bar' };
subject._cached.foo = foo;
subject._cached.bar = bar;
subject._usedColors = [foo.color, bar.color];
});
teardown(function() {
delete subject._cached.foo;
delete subject._cached.bar;
});
teardown(resetUsedColors);
test('keep previous color if from palette', function() {
subject._updateCalendarColor(foo);
assert.equal(
foo.color,
palette[3],
'should keep same color'
);
assert.deepEqual(
subject._usedColors,
[bar.color, foo.color],
'should keep _usedColors in sync'
);
});
test('override color if not from palette', function() {
// this test simulates an update on the color scheme and/or old
// calendars (stored in the DB before the 2.0 visual refresh)
subject._updateCalendarColor(bar);
assert.equal(
bar.color,
palette[0],
'should use first color from palette'
);
assert.deepEqual(
subject._usedColors,
[foo.color, bar.color],
'should keep _usedColors in sync'
);
});
});
});
});
});
});

View file

@ -0,0 +1,330 @@
define(function(require) {
'use strict';
var Abstract = require('store/abstract');
var AccountModel = require('models/account');
var Calc = require('common/calc');
var CalendarModel = require('models/calendar');
var Factory = require('test/support/factory');
var core = require('core');
suite('store/event', function() {
var subject;
var db;
var id = 0;
function event(date) {
if (typeof(date) === 'undefined') {
date = new Date();
}
return Factory('event', { remote: { startDate: date, _id: ++id } });
}
setup(function(done) {
id = 0;
db = core.db;
subject = core.storeFactory.get('Event');
db.open(function(err) {
assert.ok(!err);
done();
});
});
testSupport.calendar.accountEnvironment();
teardown(function(done) {
testSupport.calendar.clearStore(
core.db,
[
'accounts', 'calendars', 'events',
'busytimes', 'icalComponents'
],
function() {
db.close();
done();
}
);
});
test('initialization', function() {
assert.instanceOf(subject, Abstract);
assert.equal(subject._store, 'events');
});
test('#_createModel', function() {
var input = Factory.build('event');
var output = subject._createModel(input, 1);
assert.equal(output._id, 1);
assert.equal(output.name, output.name);
assert.deepEqual(
output.remote.startDate,
Calc.dateFromTransport(output.remote.start),
'startDate'
);
assert.deepEqual(
output.remote.endDate,
Calc.dateFromTransport(output.remote.end),
'endDate'
);
});
suite('#(x)For', function() {
var event;
setup(function(done) {
event = Factory('event', {
calendarId: this.calendar._id
});
subject.persist(event, done);
});
test('#ownersOf', function(done) {
subject.ownersOf(event, (err, owners) => {
done(() => {
assert.instanceOf(owners.account, AccountModel);
assert.instanceOf(owners.calendar, CalendarModel);
assert.equal(owners.account._id, this.account._id, 'account id');
assert.equal(owners.calendar._id, this.calendar._id, 'calendar id');
});
});
});
test('#providerFor', function(done) {
subject.providerFor(event, function(err, provider) {
assert.equal(
provider,
core.providerFactory.get('Mock')
);
done();
});
});
});
suite('#eventsForCalendar', function() {
var inCal;
var outCal;
setup(function(done) {
inCal = Factory('event', {
calendarId: 1
});
subject.persist(inCal, done);
});
setup(function(done) {
outCal = Factory('event', {
calendarId: 2
});
subject.persist(outCal, done);
});
test('result', function(done) {
subject.eventsForCalendar(1, function(err, result) {
done(function() {
assert.ok(!err);
assert.deepEqual(
result,
[inCal]
);
});
});
});
});
suite('#findByIds', function() {
var events = {};
function persist() {
setup(function(done) {
var item = event();
events[item._id] = item;
subject.persist(item, done);
});
}
persist();
persist();
persist();
persist();
test('result', function(done) {
var ids = Object.keys(events);
ids.push('random-not-here');
subject.findByIds(ids, function(err, items) {
done(function() {
assert.equal(
Object.keys(items).length,
4,
'should find all items'
);
for (var id in events) {
assert.deepEqual(
items[id],
events[id],
'should find event with id ' + id
);
}
});
});
});
});
suite('#remove', function() {
//TODO: busytime removal tests?
//
suite('remove ical component', function() {
var component;
var event;
var componentStore;
setup(function(done) {
componentStore = core.storeFactory.get('IcalComponent');
event = Factory('event');
component = Factory('icalComponent', {
eventId: event._id
});
var trans = core.db.transaction(
['events', 'icalComponents'],
'readwrite'
);
trans.oncomplete = function() {
done();
};
subject.persist(event, trans);
componentStore.persist(component, trans);
});
setup(function(done) {
subject.remove(event._id, done);
});
test('after removing the event', function(done) {
componentStore.get(event._id, function(err, result) {
done(function() {
assert.ok(!result, 'removes component');
});
});
});
});
suite('parent items /w children', function() {
var parentId = 'parentStuff';
var childId = 'child';
var id = 'foobar1';
setup(function(done) {
var item = Factory('event', {
_id: parentId
});
subject.persist(item, done);
});
setup(function(done) {
subject.persist(
Factory('event', { _id: childId, parentId: id }),
done
);
});
setup(function(done) {
subject.remove(id, done);
});
test('removes parent event', function(done) {
subject.get(parentId, function() {
done(function(err, result) {
assert.ok(!err);
assert.ok(!result);
});
});
});
test('removes child event', function(done) {
subject.get(childId, function() {
done(function(err, result) {
assert.ok(!err);
assert.ok(!result);
});
});
});
});
});
suite('#removeByIndex', function() {
var busytime;
var byCalendar = {};
setup(function() {
byCalendar = {};
});
function persistEvent(calendarId) {
setup(function(done) {
var event = Factory('event', {
calendarId: calendarId
});
if (!(calendarId in byCalendar)) {
byCalendar[calendarId] = [];
}
byCalendar[calendarId].push(event._id);
subject.persist(event, done);
});
}
persistEvent(1);
persistEvent(1);
persistEvent(2);
setup(function(done) {
busytime = core.storeFactory.get('Busytime');
subject.get(byCalendar[2][0], function(err, result) {
done(function() {
assert.ok(result, 'has control event');
});
});
});
test('removed all events for 1', function(done) {
subject.removeByIndex('calendarId', 1, function() {
assert.ok(
!busytime._byEventId[byCalendar[1][0]],
'should remove events from busytime'
);
assert.ok(
!busytime._byEventId[byCalendar[1][1]],
'should remove events from busytime'
);
subject.get(byCalendar[2][0], function(err, result) {
done(function() {
assert.ok(!err);
assert.ok(result, 'should not remove control');
});
});
});
});
});
});
});

View file

@ -0,0 +1,94 @@
define(function(require) {
'use strict';
var Abstract = require('store/abstract');
var Factory = require('test/support/factory');
var core = require('core');
suite('store/ical_component', function() {
var subject;
var db;
setup(function(done) {
db = core.db;
subject = core.storeFactory.get('IcalComponent');
db.open(function(err) {
assert.ok(!err);
done();
});
});
teardown(function(done) {
testSupport.calendar.clearStore(
core.db,
['icalComponents'],
done
);
});
teardown(function() {
db.close();
});
test('initialization', function() {
assert.instanceOf(subject, Abstract);
assert.deepEqual(subject._cached, {});
});
suite('#findRecurrencesBefore', function() {
var max = new Date(2012, 3, 1);
var after;
var expected;
setup(function(done) {
var trans = db.transaction('icalComponents', 'readwrite');
trans.oncomplete = function() {
done();
};
expected = [
new Date(2012, 1, 1),
new Date(2012, 2, 1),
new Date(2012, 3, 0)
];
after = [
new Date(2012, 3, 1, 1),
new Date(2012, 4, 1)
];
var id = 0;
function persistList(list, update) {
list.forEach(function(item, idx) {
item = Factory('icalComponent', {
eventId: ++id,
lastRecurrenceId: item
});
if (update) {
list[idx] = item;
}
subject.persist(item, trans);
});
}
persistList(expected, true);
persistList(after);
});
test('found', function(done) {
subject.findRecurrencesBefore(max, function(err, list) {
done(function() {
assert.lengthOf(list, expected.length);
assert.deepEqual(list, expected);
});
});
});
});
});
});

View file

@ -0,0 +1,138 @@
define(function(require) {
'use strict';
var Abstract = require('store/abstract');
var core = require('core');
suite('store/setting', function() {
var subject;
var db;
setup(function(done) {
db = core.db;
subject = core.storeFactory.get('Setting');
db.open(function(err) {
assert.ok(!err);
done();
});
});
teardown(function(done) {
testSupport.calendar.clearStore(
core.db,
['settings'],
done
);
});
teardown(function() {
db.close();
});
test('initialization', function() {
assert.instanceOf(subject, Abstract);
assert.deepEqual(subject._cached, {});
});
function get(id, callback) {
var trans = db.transaction('settings');
var store = trans.objectStore('settings');
store.get(id).onsuccess = function(e) {
callback(e.target.result);
};
}
suite('#set', function() {
var name = 'someFoo';
var calledEvent;
setup(function(done) {
subject.on(name + 'Change', function() {
calledEvent = Array.slice(arguments);
});
subject.set(name, 'first', done);
});
test('persistence', function(done) {
get(name, function(record) {
done(function() {
assert.equal(record.value, 'first', 'has correct value');
assert.instanceOf(record.createdAt, Date, 'updatedAt');
assert.instanceOf(record.updatedAt, Date, 'createdAt');
assert.deepEqual(
calledEvent,
['first', record],
'calls ' + name + 'Change'
);
});
});
});
});
suite('#getValue', function() {
test('with a default', function(done) {
subject.getValue('syncFrequency', function(err, value) {
done(function() {
assert.equal(value, subject.defaults.syncFrequency);
assert.ok(value);
});
});
});
test('with a zero default', function(done) {
subject.defaults.someZeroDefault = 0;
subject.getValue('someZeroDefault', function(err, value) {
done(function() {
assert.equal(value, subject.defaults.someZeroDefault);
assert.equal(value, 0);
});
});
});
suite('with value', function() {
setup(function(done) {
subject.set('syncFrequency', 200, done);
});
test('after set', function(done) {
subject.getValue('syncFrequency', function(err, value) {
// test cached version
subject.getValue('syncFrequency', function(err, cachedValue) {
done(function() {
assert.equal(value, 200, 'returns correct value');
assert.equal(value, cachedValue, 'cached value is equal');
});
});
});
});
});
});
suite('Bug #855782 - Settings were not being cached', function() {
test('getting, then seting a value will overwrite cache', function(done) {
var key = 'someFooBar';
// The cache needs to be empty to test this case
assert.ok(!subject._cached[key]);
subject.defaults[key] = 'bbq';
subject.getValue(key, function(err, value) {
assert.equal(value, subject.defaults[key]);
subject.set(key, 'ketchup', null, function(err, value) {
assert.notEqual(value, subject.defaults[key]);
subject.getValue(key, function(err, value) {
assert.equal(value, 'ketchup');
assert.notEqual(value, subject.defaults[key]);
done();
});
});
});
});
});
});
});

View file

@ -0,0 +1,139 @@
define(function(require, exports) {
'use strict';
var Db = require('db');
var Factory = require('test/support/factory');
var MockProvider = require('test/support/mock_provider');
var app = require('app');
var core = require('core');
require('test/support/factories/all');
exports._lastEnvId = 0;
exports.core = function() {
var pendingManager = app.pendingManager;
if (pendingManager) {
// hack to ensure clean tests
pendingManager.objects.forEach(item => pendingManager.unregister(item));
}
if (core.db && core.db.isOpen) {
core.db.close();
}
app.setupCore('b2g-test-calendar');
core.providerFactory.providers.Mock = new MockProvider();
return core;
};
exports.accountEnvironment = function(accOverrides, calOverrides) {
accOverrides = accOverrides || {};
accOverrides.providerType = accOverrides.providerType || 'Mock';
calOverrides = calOverrides || {};
setup(function(done) {
var id = ++exports._lastEnvId;
accOverrides._id = id;
calOverrides.accountId = id;
var trans = core.db.transaction(['accounts', 'calendars'], 'readwrite');
trans.oncomplete = () => done();
trans.onerror = event => done(event.target.error);
var account = this.account = Factory('account', accOverrides);
var calendar = this.calendar = Factory('calendar', calOverrides);
var storeFactory = core.storeFactory;
var accountStore = storeFactory.get('Account');
var calendarStore = storeFactory.get('Calendar');
accountStore.persist(account, trans);
calendarStore.persist(calendar, trans);
});
};
exports.eventEnvironment = function(busytimeOverrides, eventOverrides) {
eventOverrides = eventOverrides || {};
eventOverrides._id = eventOverrides._id || 'one';
busytimeOverrides = busytimeOverrides || {};
setup(function(done) {
var storeFactory = core.storeFactory;
var busytimeStore = storeFactory.get('Busytime');
var eventStore = storeFactory.get('Event');
var trans = core.db.transaction(eventStore._dependentStores, 'readwrite');
trans.oncomplete = () => done();
trans.onerror = event => done(event.target.error);
eventOverrides.calendarId = this.calendar._id;
var event = this.event = Factory('event', eventOverrides);
busytimeOverrides.calendarId = this.calendar._id;
busytimeOverrides.eventId = this.event._id || eventOverrides._id;
var busytime = this.busytime = Factory('busytime', busytimeOverrides);
eventStore.persist(event, trans);
busytimeStore.persist(busytime, trans);
});
};
exports.dbFixtures = function(factory, storeName, list) {
var object = Object.create(null);
setup(function(done) {
var db = core.db;
var store = core.storeFactory.get(storeName);
var trans = db.transaction(store._dependentStores, 'readwrite');
trans.oncomplete = () => done();
trans.onerror = event => done(event.target.error);
for (var key in list) {
store.persist(
(object[key] = Factory(factory, list[key])),
trans
);
}
});
return object;
};
exports.clearStore = function(db, name, callback) {
if (typeof(name) === 'function') {
callback = name;
name = Object.keys(Db.prototype.store);
}
var trans = db.transaction(name, 'readwrite');
trans.oncomplete = () => callback();
trans.onerror = () => callback(new Error('Could not wipe accounts db'));
name = [].concat(name);
name.forEach(storeName => {
var store = trans.objectStore(storeName);
store.clear();
});
};
exports.bench = function(iter, cb) {
var start = window.performance.now();
for (var i = 0; i < iter; i++) {
cb();
}
return window.performance.now() - start;
};
exports.vs = function(iter, cmds) {
var results = {};
for (var key in cmds) {
results[key] = exports.bench(iter, cmds[key]);
}
return results;
};
exports.triggerEvent = function(element, eventType) {
var event = document.createEvent('HTMLEvents');
event.initEvent(eventType, true, true);
element.dispatchEvent(event);
};
});

View file

@ -0,0 +1,10 @@
define(function(require, exports) {
'use strict';
var nextTick = require('common/next_tick');
exports.load = function(id, require, onLoad) {
nextTick(() => onLoad());
};
});

View file

@ -0,0 +1,12 @@
/* global requireElements */
define(function(require, exports) {
'use strict';
var nextTick = require('common/next_tick');
exports.load = function(id, require, onLoad) {
requireElements('calendar/elements/' + id + '.html');
nextTick(() => onLoad());
};
});

View file

@ -0,0 +1,232 @@
define(function(require) {
'use strict';
/**
* Module dependencies
*/
var AccountModel = require('models/account');
var CalendarModel = require('models/calendar');
var Caldav = require('ext/caldav');
var Calc = require('common/calc');
var Factory = require('test/support/factory');
/**
* Module state
*/
var calendarId = 0;
function handleTransportDate(obj) {
// handle case of incoming start date
if (obj.startDate) {
obj.start = Calc.dateToTransport(obj.startDate);
}
if (obj.endDate) {
obj.end = Calc.dateToTransport(obj.endDate);
}
// handle case of defaults
if (!obj.start) {
obj.start = Calc.dateToTransport(
new Date()
);
}
if (obj.start) {
obj.startDate = Calc.dateFromTransport(obj.start);
}
if (!obj.end) {
obj.end = Calc.dateToTransport(
// 1 hour after start
new Date(obj.startDate.getTime() + 3600 * 1000)
);
}
if (obj.end) {
obj.endDate = Calc.dateFromTransport(obj.end);
}
}
Factory.define('remote.calendar', {
properties: {
id: '',
url: '/',
name: 'my calendar',
color: '#333',
description: 'description',
syncToken: '1',
// read, write, delete
privilegeSet: ['read', 'write-content', 'unbind']
},
onbuild: function(obj) {
var id = obj.id;
if (!id) {
id = obj.id = 'cuuid/' + calendarId++;
}
}
});
var eventId = 0;
Factory.define('remote.event', {
properties: {
location: 'location',
isRecurring: false,
alarms: [
{action: 'DISPLAY', trigger: 60000}
]
//XXX: raw data
},
onbuild: function(obj) {
var id = obj.id;
if (!id) {
id = obj.id = 'euuid/' + eventId++;
}
handleTransportDate(obj);
if (!obj.title) {
obj.title = 'title ' + id;
}
if (!obj.description) {
obj.description = 'description ' + id;
}
}
});
Factory.define('caldav.calendar', {
get object() {
return Caldav.Resources.Calendar;
},
properties: {
url: '/cal',
name: 'name',
color: '#333',
ctag: 'token',
description: '1',
privilegeSet: [
'read',
'write',
'write-content'
]
}
});
Factory.define('caldav.account', {
properties: {
user: 'user',
password: 'password',
domain: 'http://google.com',
entrypoint: '/dav/',
calendarHome: '/dav/my/foobar'
}
});
Factory.define('caldav.connection', {
extend: 'caldav.account',
get object() {
return Caldav.Connection;
}
});
Factory.define('account', {
get object() {
return AccountModel;
},
properties: {
providerType: 'Local',
user: 'user',
password: 'password',
domain: 'http://google.com',
entrypoint: '/',
preset: 'local'
}
});
Factory.define('calendar', {
get object() {
return CalendarModel;
},
properties: {
accountId: 1,
remote: Factory.get('remote.calendar')
},
oncreate: function(obj) {
if (!obj._id) {
obj._id = obj.accountId + '-' + obj.remote.id;
}
}
});
Factory.define('event', {
properties: {
calendarId: 1,
remote: Factory.get('remote.event')
},
oncreate: function(obj) {
if (!obj._id) {
obj._id = obj.calendarId + '-' + obj.remote.id;
}
}
});
var busytimeId = 0;
Factory.define('busytime', {
properties: {
calendarId: 'calendar-1',
eventId: 'event-1'
},
oncreate: function(obj) {
if (!obj._id) {
obj._id = obj.eventId + '-' + (++busytimeId);
}
handleTransportDate(obj);
if (obj.endDate === obj.startDate) {
obj.endDate.setHours(obj.endDate.getHours() + 2);
handleTransportDate(obj);
}
}
});
Factory.define('alarm', {
oncreate: function(obj) {
if (obj.startDate && obj.startDate instanceof Date) {
obj.startDate = Calc.dateToTransport(obj.startDate);
}
}
});
Factory.define('icalComponent', {
oncreate: function(obj) {
if (obj.lastRecurrenceId && obj.lastRecurrenceId instanceof Date) {
obj.lastRecurrenceId = Calc.dateToTransport(obj.lastRecurrenceId);
}
},
properties: {
ical: 'fooo!'
}
});
});

View file

@ -0,0 +1,209 @@
/**
* Test factory builder. Inspired by FactoryGirl (of ruby/rails fame).
* Tries very hard to respect getters in js objects
* as a result there is a performance hit so factories
* should not be used when building thousands of objects
* but should be fine for the 10-100 objects in small-large
* tests.
*/
define(function(require, exports, module) {
'use strict';
function propIsFactory(object, key) {
var descriptor = Object.getOwnPropertyDescriptor(
object, key
);
if (!descriptor) {
return false;
}
return (
descriptor.value &&
(descriptor.value instanceof Factory)
);
}
/**
* Copy a set of property descriptors from
* one object to another
*/
function copyProp(from, keys, to) {
var list = [].concat(keys);
list.forEach(function(key) {
if (!from.hasOwnProperty(key)) {
return;
}
Object.defineProperty(
to,
key,
Object.getOwnPropertyDescriptor(
from,
key
)
);
});
return to;
}
/**
* Copies all properties
* (in order from left to right to the final argument)
*/
function copy() {
var args = Array.prototype.slice.call(arguments);
var target = args.pop();
args.forEach(function(object) {
if (!object) {
return;
}
for (var key in object) {
if (object.hasOwnProperty(key)) {
Object.defineProperty(
target,
key,
Object.getOwnPropertyDescriptor(
object,
key
)
);
}
}
});
return target;
}
/* static api */
Factory._defined = Object.create(null);
Factory.get = function(name) {
return Factory._defined[name];
};
Factory.define = function(name, options) {
if (options.extend) {
var factory = Factory.get(options.extend);
Factory._defined[name] = factory.extend(
options
);
} else {
Factory._defined[name] = new Factory(
options
);
}
return Factory._defined[name];
};
Factory.create = function(name, opts) {
return Factory.get(name).create(opts);
};
Factory.build = function(name, opts) {
return Factory.get(name).build(opts);
};
function Factory(options) {
if (!(this instanceof Factory)) {
return Factory.create.apply(Factory, arguments);
}
copy(options, this);
return this;
}
module.exports = Factory;
Factory.prototype = {
parent: null,
object: null,
properties: {},
extend: function(options) {
var newFactory = {};
// we need to copy the prop
// rather then do an assignment for lazy-est
// possible evaluation of properties.
copyProp(
this,
['object', 'onbuild', 'oncreate'],
newFactory
);
copy(options, newFactory);
newFactory.properties = copy(
this.properties,
newFactory.properties,
{}
);
return new Factory(newFactory);
},
build: function(overrides, childFactoryMethod) {
if (typeof(overrides) === 'undefined') {
overrides = {};
}
if (typeof(childFactoryMethod) === 'undefined') {
childFactoryMethod = 'build';
}
var defaults = {};
var key;
var props = this.properties;
copy(props, overrides, defaults);
// expand factories
var factoryOverrides;
for (key in defaults) {
// when default property is a factory
if (propIsFactory(props, key)) {
factoryOverrides = undefined;
if (!propIsFactory(defaults, key)) {
// user overrides defaults
factoryOverrides = defaults[key];
}
defaults[key] = props[key][childFactoryMethod](
factoryOverrides
);
}
}
if (typeof(this.onbuild) === 'function') {
this.onbuild(defaults);
}
return defaults;
},
create: function(overrides) {
var result;
var constructor = this.object;
var attrs = this.build(overrides, 'create');
if (constructor) {
result = new constructor(attrs);
} else {
result = attrs;
}
if (typeof(this.oncreate) === 'function') {
this.oncreate(result);
}
return result;
}
};
});

View file

@ -0,0 +1,429 @@
define(function(require) {
'use strict';
var Factory = require('test/support/factory');
suite('factory', function() {
var subject;
var realDefined;
setup(function() {
realDefined = Factory._defined;
Factory._defined = Object.create(null);
});
teardown(function() {
Factory._defined = realDefined;
});
function Model(options) {
var key;
for (key in options) {
if (options.hasOwnProperty(key)) {
this[key] = options[key];
}
}
}
suite('hooks', function() {
setup(function() {
subject = new Factory({
onbuild: function(obj) {
obj.hitBuild = true;
},
oncreate: function(obj) {
obj.hitCreate = true;
},
properties: {
one: true
}
});
});
test('create', function() {
var out = subject.create();
assert.isTrue(out.hitBuild);
assert.isTrue(out.hitCreate);
});
test('build', function() {
var out = subject.build();
assert.isTrue(out.hitBuild);
assert.ok(!out.hitCreate);
});
});
suite('simple property factory', function() {
setup(function() {
subject = new Factory({
properties: {
get one() {
return 'cool';
},
two: 'foo'
}
});
});
test('initialization', function() {
assert.deepEqual(subject.properties, {
one: 'cool',
two: 'foo'
});
});
test('create - no overrides', function() {
var result = subject.create();
assert.deepEqual(
result,
subject.properties
);
});
test('create - overrides', function() {
var result = subject.create(
{ one: '1', three: '3' }
);
assert.deepEqual(
result, {
one: '1',
two: 'foo',
three: '3'
}
);
});
});
suite('with getters', function() {
var increment;
setup(function() {
increment = 0;
subject = new Factory({
properties: {
get increment() {
return increment++;
}
}
});
});
test('increment getter', function() {
subject.create();
subject.create();
subject.create();
var result = subject.create();
// This is probably something
// you would not want to do in real
// life but it provides that
// increments are clean.
assert.equal(result.increment, 0);
assert.equal(result.increment, 1);
assert.equal(result.increment, 2);
assert.equal(result.increment, 3);
});
});
suite('with constructors', function() {
setup(function() {
subject = new Factory({
object: Model,
properties: {
value: 'foo'
}
});
});
test('create - no overrides', function() {
var result = subject.create();
assert.instanceOf(result, Model);
assert.equal(result.value, 'foo');
});
test('create - with overrides', function() {
var result = subject.create(
{ value: 'val', other: true }
);
assert.instanceOf(result, Model);
assert.equal(result.value, 'val');
assert.equal(result.other, true);
});
test('build', function() {
var result = subject.build({ val: 1 });
assert.isFalse((
result instanceof Model
));
assert.equal(result.val, 1);
assert.equal(result.value, 'foo');
});
});
suite('with factory props', function() {
var childFactory;
setup(function() {
childFactory = new Factory({
object: Model,
properties: {
get child() {
return true;
}
}
});
subject = new Factory({
properties: {
parent: 'yes',
child: childFactory,
inertChild: childFactory
}
});
});
test('create', function() {
var result = subject.create({
parentVal: 1,
child: {
otherVal: 1
}
});
assert.instanceOf(result.child, Model);
assert.instanceOf(result.inertChild, Model);
assert.equal(result.parent, 'yes');
assert.equal(result.parentVal, 1);
assert.equal(result.inertChild.child, true);
assert.equal(result.child.child, true);
assert.equal(result.child.otherVal, 1);
});
test('build', function() {
var result = subject.build({
child: {
otherVal: 1
}
});
assert.isFalse((
result.child instanceof Model
));
assert.isFalse((
result.inertChild instanceof Model
));
});
test('#extend', function() {
var sub = subject.extend({
object: Model,
properties: {
foo: 'bar'
}
});
assert.ok(!subject.properties.foo);
assert.equal(sub.object, Model);
var result = sub.create({
other: 'fooz'
});
assert.instanceOf(result, Model);
assert.notEqual(
sub.properties,
subject.properties
);
assert.equal(result.other, 'fooz');
assert.instanceOf(result.child, Model);
assert.instanceOf(result.inertChild, Model);
assert.equal(result.child.child, true);
});
});
suite('static api', function() {
var defined;
setup(function() {
defined = Factory.define('one', {
object: Model,
properties: {
one: true
}
});
});
test('#define', function() {
assert.instanceOf(defined, Factory);
assert.equal(defined.object, Model);
assert.deepEqual(
defined.properties,
{ one: true }
);
assert.equal(Factory.get('one'), defined);
});
test('#define - /w extend', function() {
Factory.define('one.ext', {
extend: 'one',
properties: {
two: false
}
});
assert.instanceOf(
Factory.get('one.ext'),
Factory
);
var created = Factory.create('one.ext');
assert.equal(created.one, true);
assert.equal(created.two, false);
});
test('#create', function() {
var result = Factory.create('one', {
extra: true
});
assert.instanceOf(result, Model);
assert.equal(result.extra, true);
assert.equal(result.one, true);
});
test('#build', function() {
var result = Factory.build('one', {
extra: true
});
assert.isFalse((
result instanceof Model
));
assert.equal(result.extra, true);
assert.equal(result.one, true);
});
});
suite('acceptance', function() {
var result;
function Event() { Model.apply(this, arguments); }
function Cal() { Model.apply(this, arguments); }
function Account() { Model.apply(this, arguments); }
setup(function() {
Factory.define('event', {
object: Event,
properties: {
isEvent: true
}
});
Factory.define('calendar', {
object: Cal,
properties: {
isCalendar: true,
event: Factory.get('event')
}
});
Factory.define('account', {
object: Account,
properties: {
isAccount: true,
normalCal: Factory.get('calendar'),
extendCal: Factory.get('calendar').extend({
properties: {
isCrazy: true,
event: Factory.get('event').extend({
properties: {
isCrazy: true
}
})
}
})
}
});
});
function hasProps() {
// account
assert.isTrue(result.isAccount);
// normal cal
assert.isTrue(result.normalCal.isCalendar);
assert.ok(!result.normalCal.isCrazy);
// crazy cal
assert.isTrue(result.extendCal.isCrazy);
assert.isTrue(result.extendCal.isCalendar);
// normal event
assert.isTrue(result.normalCal.event.isEvent);
assert.ok(!result.normalCal.event.isCrazy);
// crazy calendar
assert.isTrue(result.extendCal.event.isEvent);
assert.isTrue(result.extendCal.event.isCrazy);
}
test('#create - no overrides', function() {
result = Factory.create('account');
assert.instanceOf(result, Account, 'result Account');
assert.instanceOf(result.normalCal, Cal, 'normalCal');
assert.instanceOf(result.extendCal, Cal, 'extendCal');
assert.instanceOf(result.normalCal.event, Event, 'normalCal.event');
assert.instanceOf(result.extendCal.event, Event, 'extendCal.event');
hasProps();
});
test('#build - no overrides', function() {
result = Factory.build('account');
assert.isFalse(
result instanceof Account
);
hasProps();
});
test('deep overrides', function() {
result = Factory('account', {
normalCal: {
foo: true,
event: {
nested: true
}
}
});
// we did not kill the defaults
hasProps();
assert.isTrue(result.normalCal.foo);
assert.isTrue(result.normalCal.event.nested);
});
});
});
});

View file

@ -0,0 +1,23 @@
define(function(require, exports, module) {
'use strict';
function page() {
page.routes.push(Array.prototype.slice.call(arguments));
}
module.exports = page;
page.routes = [];
page.show = function(item) {
this.shown = item;
};
page.start = function() {
this.started = true;
};
page.stop = function() {
this.started = false;
};
});

View file

@ -0,0 +1,28 @@
define(function(require, exports, module) {
'use strict';
module.exports = function mochaPromise(mochaFn, description, callback) {
if (typeof description === 'function') {
callback = description;
description = null;
}
function execute(done) {
var promise;
try {
promise = callback.call();
} catch (error) {
return done(error);
}
promise.then(() => done()).catch(done);
}
if (description) {
mochaFn(description, execute);
} else {
mochaFn(execute);
}
};
});

View file

@ -0,0 +1,54 @@
define(function(require, exports) {
'use strict';
var _id = 0;
var alarms = [];
var mozAlarms;
exports.setup = function() {
mozAlarms = navigator.mozAlarms;
navigator.mozAlarms = exports;
};
exports.teardown = function() {
removeAll();
navigator.mozAlarms = mozAlarms;
};
exports.add = function(date, honorTimezone, data) {
var alarmId = _id++;
alarms.push({
alarmId: alarmId,
date: date,
honorTimezone: honorTimezone,
data: data
});
return resolve(alarmId);
};
exports.getAll = function() {
return resolve(alarms);
};
exports.remove = function(alarmId) {
alarms = alarms.filter(alarm => alarm.alarmId !== alarmId);
};
function removeAll() {
alarms = [];
}
function resolve(result) {
var request = { onsuccess: null, onerror: null };
var requestContext = {};
requestContext.result = result;
setTimeout(() => {
return request.onsuccess &&
request.onsuccess.call(requestContext, result);
}, 10);
return request;
}
});

View file

@ -0,0 +1,213 @@
define(function(require, exports, module) {
'use strict';
var Parent = require('provider/abstract');
var nextTick = require('common/next_tick');
function accountGeneric(mockName) {
return function(account, callback) {
// next tick is scheduled before _resolveStage intentionally
nextTick(function() {
callback.apply(
null,
this._resolveStaged(mockName, account) || [null, {}]
);
}.bind(this));
};
}
function accountStaging(mockName) {
return function() {
var args = Array.slice(arguments);
var user = args.shift();
function matcher(object) {
return object.user === user;
}
return this._stage(mockName, matcher, args);
};
}
var stageEvents = {
onbefore: function() {},
onafter: function() {}
};
function Mock() {
Parent.apply(this, arguments);
// mock call storage
this._stageObjects = Object.create(null);
}
module.exports = Mock;
Mock.prototype = {
__proto__: Parent.prototype,
// opt-into sync by default
canSync: true,
_stage: function(name, matcher, args) {
if (!(this._stageObjects[name])) {
this._stageObjects[name] = [];
}
var handler = Object.create(stageEvents);
this._stageObjects[name].push({
matches: matcher,
args: args,
handler: handler
});
return handler;
},
/**
* Used to find staged objects.
* The found object must be saved as
* this method will also remove any found item.
*
* Stops at first found item that matches condition.
*
* var stagedValue = this._resolveStaged('type', input) || someDefault;
*
*/
_resolveStaged: function(name, input) {
var list = this._stageObjects[name];
if (!list) {
return null;
}
var i = 0;
var len = list.length;
for (; i < len; i++) {
if (list[i].matches(input)) {
// remove and return args
var item = list.splice(i, 1)[0];
var args = item.args;
var handler = item.handler;
handler.onbefore(args);
nextTick(handler.onafter.bind(null, args));
return args;
}
}
return null;
},
/**
* Stage a getAccount call.
*
* @param {String} user name to stage information for.
* @param {Object|Null} err given to callback.
* @param {Object|Null} object given to callback.
*/
stageGetAccount: accountStaging('getAccount'),
stageFindCalendars: accountStaging('findCalendars'),
stageSyncEvents: function(user, calendarRemoteId, err) {
err = err || null;
function matcher(args) {
return (
args[0].user === user &&
args[1].remote.id === calendarRemoteId
);
}
return this._stage('syncEvents', matcher, err);
},
stageEventCapabilities: function() {
var args = Array.slice(arguments);
var id = args.shift();
function matcher(event) {
return event._id === id;
}
return this._stage('eventCapabilities', matcher, args);
},
stageCalendarCapabilities: function() {
var args = Array.slice(arguments);
var id = args.shift();
function matcher(calendar) {
return calendar._id === id;
}
return this._stage('calendarCapabilities', matcher, args);
},
/**
* Stage findCalendars call.
*
*
* provider.stageFindCalendars(
* accountModel.user,
* err || null,
* {
* remoteId: { id: 'remoteId', ... },
* ...
* }
* );
*/
findCalendars: accountGeneric('findCalendars'),
getAccount: accountGeneric('getAccount'),
syncEvents: function(account, calendar, callback) {
// first so this tick is scheduled first
nextTick(function() {
callback(
this._resolveStaged('syncEvents', [account, calendar])
);
}.bind(this));
},
calendarCapabilities: function(calendar) {
var defaults = {
canUpdateEvent: true,
canCreateEvent: true,
canDeleteEvent: true
};
var args =
this._resolveStaged('calendarCapabilities', calendar) || [];
var overrides = args[0] || {};
for (var key in overrides) {
defaults[key] = overrides[key];
}
return defaults;
},
eventCapabilities: function(event, callback) {
var defaults = {
canUpdate: true,
canCreate: true,
canDelete: true
};
nextTick(function() {
var args = this._resolveStaged('eventCapabilities', event) || [];
var err = args[0] || null;
var overrides = args[1] || {};
for (var key in overrides) {
defaults[key] = overrides[key];
}
callback(err, defaults);
}.bind(this));
}
};
});

View file

@ -0,0 +1,33 @@
define(function(require, exports) {
'use strict';
var requestWakeLock;
exports.setup = function() {
requestWakeLock = navigator.requestWakeLock;
navigator.requestWakeLock = fakeRequestWakeLock;
};
exports.teardown = function() {
navigator.requestWakeLock = requestWakeLock;
locks = exports.locks = [];
};
var locks = exports.locks = [];
function fakeRequestWakeLock(type) {
var lock = createFakeLock(type);
locks.push(lock);
return lock;
}
function createFakeLock(type) {
return {
type: type,
unlocked: false,
unlock: function() {
this.unlocked = true;
}
};
}
});

Some files were not shown because too many files have changed in this diff Show more