'use strict'; var chai = require('chai'); var _ = require('lodash'); var q = require('q'); var zeit = require('../'); var assert = chai.assert; chai.use(require('chai-timers')); assert.momentEql = function (expected, actual, msg) { assert.equal(expected.toString(), actual.toString(), msg); }; var ZERO_MILLIS = 0; var REPEAT_OF_ONE_SECOND = 1000; var AFTER_INTERVAL = 1234; function EventCapture(emitter) { var events = {}; return { listenTo: function () { _.each(arguments, function (name) { events[name] = []; emitter.on(name, function (event) { events[name].push(event); }); }); return this; }, captured: events }; } function PromiseToExecute(actualFn) { var invocationsCounter = []; var f = function () { invocationsCounter.push(0); return actualFn(); }; f.invocations = function () { return invocationsCounter.length; }; return f; } function setUpTest(ClockCtr, returnValueFn) { return function () { var clock = new ClockCtr(); var scheduler = new zeit.Scheduler(clock); var promise = new PromiseToExecute(returnValueFn); var events = new EventCapture(scheduler).listenTo('start', 'finish', 'error'); return { startTime: clock.now(), scheduler: scheduler, clock: clock, executionCountIsLessThan: function (expected) { return function () { return promise.invocations() < expected; } }, executionCountIs: function (expected) { return function () { return promise.invocations() === expected; } }, startSchedule: function (modify) { return modify(scheduler.execute(promise).named('my function'), clock).start(); }, scheduleFor: function (scheduleId) { return scheduler.activeSchedule(scheduleId); }, clockTimeoutFor: function (scheduleId) { return clock.timeouts()[this.scheduleFor(scheduleId).clockId]; }, assertInvocationCount: function (expected) { assert.equal(promise.invocations(), expected); }, assertEventCounts: function (started, finished, errors) { assert.deepEqual(events.captured['start'].length, started); assert.deepEqual(events.captured['finish'].length, finished); assert.deepEqual(events.captured['error'].length, errors); }, triggerAllClockSchedulesAndAssertExecuted: function (expectedScheduleIds) { var expectedClockIds = _.map(expectedScheduleIds, function (scheduleId) { return scheduler.activeSchedule(scheduleId).clockId; }); assert.deepEqual(clock.triggerAll(), expectedClockIds); }, assertThereIsNoActiveScheduleFor: function (scheduleId) { assert.equal(this.scheduleFor(scheduleId), undefined); } }; }; } function describeRepetitionScenariosFor(name, expectedRepeatInterval, testFn, scheduleBuilderFn) { describe(name, function () { describe('', function () { var t = testFn(); var scheduleId = t.startSchedule(scheduleBuilderFn); it('has a 0ms initial delay', function () { assert.deepEqual(t.clockTimeoutFor(scheduleId), t.clock.numberOfMillisecondsAsDuration(0)); assert.momentEql(t.scheduleFor(scheduleId).nextTriggerTime, t.clock.lastKnownTime()); }); }); describe('combined with after()', function () { var t = testFn(); var startDelay = t.clock.numberOfMillisecondsAsDuration(AFTER_INTERVAL); var scheduleId = t.startSchedule(function (s, clock) { return scheduleBuilderFn(s, clock).after(startDelay); }); it('initially is scheduled with the defined delay', function () { assert.deepEqual(t.clockTimeoutFor(scheduleId), startDelay); assert.momentEql(t.scheduleFor(scheduleId).nextTriggerTime, t.clock.timeIn(startDelay)); }); it('triggers when executed', function () { t.triggerAllClockSchedulesAndAssertExecuted([scheduleId]); t.assertInvocationCount(1); }); it('is rescheduled at the defined interval', function () { assert.deepEqual(t.clockTimeoutFor(scheduleId), t.clock.numberOfMillisecondsAsDuration(expectedRepeatInterval)); assert.momentEql(t.scheduleFor(scheduleId).nextTriggerTime, t.clock.timeIn(t.clock.numberOfMillisecondsAsDuration(expectedRepeatInterval))); }); }); describe('combined with at()', function () { var t = testFn(); var startDelay = t.clock.numberOfMillisecondsAsDuration(AFTER_INTERVAL); var scheduleId = t.startSchedule(function (s, clock) { return scheduleBuilderFn(s, clock).at(clock.timeIn(startDelay)); }); it('initially is scheduled with the defined delay', function () { assert.deepEqual(t.clockTimeoutFor(scheduleId), startDelay); assert.momentEql(t.scheduleFor(scheduleId).nextTriggerTime, t.clock.timeIn(startDelay)); }); it('triggers when executed', function () { t.triggerAllClockSchedulesAndAssertExecuted([scheduleId]); t.assertInvocationCount(1); }); it('is rescheduled at the defined interval', function () { assert.deepEqual(t.clockTimeoutFor(scheduleId), t.clock.numberOfMillisecondsAsDuration(expectedRepeatInterval)); assert.momentEql(t.scheduleFor(scheduleId).nextTriggerTime, t.clock.timeIn(t.clock.numberOfMillisecondsAsDuration(expectedRepeatInterval))); }); }); describe('combined with whilst()', function () { var t = testFn(); var scheduleId = t.startSchedule(function (s, clock) { return scheduleBuilderFn(s, clock).whilst(t.executionCountIsLessThan(2)); }); it('triggers when executed', function () { t.triggerAllClockSchedulesAndAssertExecuted([scheduleId]); t.assertInvocationCount(1); }); it('is rescheduled at the defined interval', function () { assert.deepEqual(t.clockTimeoutFor(scheduleId), t.clock.numberOfMillisecondsAsDuration(expectedRepeatInterval)); assert.momentEql(t.scheduleFor(scheduleId).nextTriggerTime, t.clock.timeIn(t.clock.numberOfMillisecondsAsDuration(expectedRepeatInterval))); }); it('triggers when executed again', function () { t.triggerAllClockSchedulesAndAssertExecuted([scheduleId]); t.assertInvocationCount(2); }); it('is now not rescheduled', function () { t.assertThereIsNoActiveScheduleFor(scheduleId); }); }); describe('combined with until()', function () { var t = testFn(); var scheduleId = t.startSchedule(function (s, clock) { return scheduleBuilderFn(s, clock).until(t.executionCountIs(2)); }); it('triggers when executed', function () { t.triggerAllClockSchedulesAndAssertExecuted([scheduleId]); t.assertInvocationCount(1); }); it('is rescheduled at the defined interval', function () { assert.deepEqual(t.clockTimeoutFor(scheduleId), t.clock.numberOfMillisecondsAsDuration(expectedRepeatInterval)); assert.momentEql(t.scheduleFor(scheduleId).nextTriggerTime, t.clock.timeIn(t.clock.numberOfMillisecondsAsDuration(expectedRepeatInterval))); }); it('triggers when executed again', function () { t.triggerAllClockSchedulesAndAssertExecuted([scheduleId]); t.assertInvocationCount(2); }); it('is now not rescheduled', function () { t.assertThereIsNoActiveScheduleFor(scheduleId); }); }); }); } function describeNoRepetitionScenariosFor(name, testFn, scheduleBuilderFn) { describe(name, function () { describe('', function () { var t = testFn(); var scheduleId = t.startSchedule(scheduleBuilderFn); it('has a 0ms initial delay', function () { assert.deepEqual(t.clockTimeoutFor(scheduleId), t.clock.numberOfMillisecondsAsDuration(0)); assert.momentEql(t.scheduleFor(scheduleId).nextTriggerTime, t.clock.lastKnownTime()); }); }); describe('after() sets a custom initial delay', function () { var t = testFn(); var startDelay = t.clock.numberOfMillisecondsAsDuration(AFTER_INTERVAL); var scheduleId = t.startSchedule(function (s, clock) { return scheduleBuilderFn(s, clock).after(startDelay); }); it('initially is scheduled with the defined delay', function () { assert.deepEqual(t.clockTimeoutFor(scheduleId), startDelay); assert.momentEql(t.scheduleFor(scheduleId).nextTriggerTime, t.clock.timeIn(startDelay)); }); it('triggers when executed', function () { t.triggerAllClockSchedulesAndAssertExecuted([scheduleId]); t.assertInvocationCount(1); }); it('is now not rescheduled', function () { t.assertThereIsNoActiveScheduleFor(scheduleId); }); }); describe('at() sets a initial delay at to a specific time', function () { var t = testFn(); var startDelay = t.clock.numberOfMillisecondsAsDuration(AFTER_INTERVAL); var scheduleId = t.startSchedule(function (s, clock) { return scheduleBuilderFn(s, clock).at(clock.timeIn(startDelay)); }); it('initially is scheduled with the defined delay', function () { assert.deepEqual(t.clockTimeoutFor(scheduleId), startDelay); assert.momentEql(t.scheduleFor(scheduleId).nextTriggerTime, t.clock.timeIn(startDelay)); }); it('triggers when executed', function () { t.triggerAllClockSchedulesAndAssertExecuted([scheduleId]); t.assertInvocationCount(1); }); it('is now not rescheduled', function () { t.assertThereIsNoActiveScheduleFor(scheduleId); }); }); describe('whilst() sets a pre-condition for execution', function () { var t = testFn(); var scheduleId = t.startSchedule(function (s, clock) { return scheduleBuilderFn(s, clock).whilst(t.executionCountIsLessThan(0)); }); it('triggers when executed, but does not invoke the promise', function () { t.triggerAllClockSchedulesAndAssertExecuted([scheduleId]); t.assertInvocationCount(0); }); it('is now not rescheduled', function () { t.assertThereIsNoActiveScheduleFor(scheduleId); }); }); describe('until cannot be scheduled without a repeat', function () { var t = testFn(); it('blows up as cannot have a post-condition for a one-off execution', function () { try { t.startSchedule(function (s, clock) { return scheduleBuilderFn(s, clock).until(t.executionCountIs(2)); }); assert.fail(name); } catch (err) { assert.equal(err.message, 'Cannot specify an until() without repeating!', 'error message'); } }); }); }); } function describeSchedulerWhenCallback(name, testFn, expectedStartEvents, expectedFinishEvents, expectedErrorEvents) { describe(name + ',', function () { describe('correct events are emitted', function () { var t = testFn(); var scheduleId = t.startSchedule(_.identity); it('triggers when executed', function () { t.triggerAllClockSchedulesAndAssertExecuted([scheduleId]); t.assertInvocationCount(1); }); it('emits then correct events', function () { t.assertEventCounts(expectedStartEvents, expectedFinishEvents, expectedErrorEvents); }); }); describe('cancelling a schedule', function () { var t = testFn(); var scheduleId = t.startSchedule(_.identity); it('returns the cancelled details' ,function() { assert.equal(t.scheduler.cancel(scheduleId).id, scheduleId); }); it('is now not scheduled', function () { t.assertThereIsNoActiveScheduleFor(scheduleId); }); }); describe('cancelling all schedules', function () { var t = testFn(); var scheduleId = t.startSchedule(_.identity); it('returns all cancelled details' ,function() { var cancelled = t.scheduler.cancelAll(); assert.equal(_.size(cancelled), 1); assert.equal(cancelled[scheduleId].id, scheduleId); }); it('is now not scheduled', function () { t.assertThereIsNoActiveScheduleFor(scheduleId); }); }); describeNoRepetitionScenariosFor('one-off', testFn, _.identity); describeNoRepetitionScenariosFor('once()', testFn, function (scheduler) { return scheduler.once(); }); describeRepetitionScenariosFor('exactly()', ZERO_MILLIS, testFn, function (scheduler) { return scheduler.exactly(3); }); describeRepetitionScenariosFor('andRepeatAfter(1000ms)', REPEAT_OF_ONE_SECOND, testFn, function (scheduler, clock) { return scheduler.andRepeatAfter(clock.numberOfMillisecondsAsDuration(REPEAT_OF_ONE_SECOND)); }); describeRepetitionScenariosFor('andRepeatAfter(0ms)', ZERO_MILLIS, testFn, function (scheduler, clock) { return scheduler.andRepeatAfter(clock.numberOfMillisecondsAsDuration(ZERO_MILLIS)); }); describeRepetitionScenariosFor('atFixedIntervalOf(1000ms)', REPEAT_OF_ONE_SECOND, testFn, function (scheduler, clock) { return scheduler.atFixedIntervalOf(clock.numberOfMillisecondsAsDuration(REPEAT_OF_ONE_SECOND)); }); describeRepetitionScenariosFor('atFixedIntervalOf(0ms)', ZERO_MILLIS, testFn, function (scheduler, clock) { return scheduler.atFixedIntervalOf(clock.numberOfMillisecondsAsDuration(ZERO_MILLIS)); }); }); } function describeSchedulerUsing(name, ClockCtr) { describe('(' + name + ' clock): when', function () { describeSchedulerWhenCallback('returns a promise which is resolved', setUpTest(ClockCtr, function() { return q.resolve('ok value'); }), 1, 1, 0); describeSchedulerWhenCallback('returns a promise which is then rejected', setUpTest(ClockCtr, function() { return q.reject('err value'); }), 1, 0, 1); describeSchedulerWhenCallback('callback succeeds', setUpTest(ClockCtr, function() { return 'ok value'; }), 1, 1, 0); describeSchedulerWhenCallback('exception is thrown', setUpTest(ClockCtr, function() { throw 'err value'; }), 1, 0, 1); }); } describe('Scheduler', function () { describeSchedulerUsing('date-based', zeit.StubDateClock); describeSchedulerUsing('moment-based', zeit.StubMomentClock); });