[Plugins] Bring over timeline, clock plugins

WTD-1239
This commit is contained in:
Victor Woeltjen
2015-09-14 16:45:38 -07:00
parent 8c1b70f085
commit c932e953bc
119 changed files with 10485 additions and 0 deletions

View File

@@ -0,0 +1,66 @@
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
define(
["../../src/actions/AbstractStartTimerAction"],
function (AbstractStartTimerAction) {
"use strict";
describe("A timer's start/restart action", function () {
var mockNow,
mockDomainObject,
mockPersistence,
testModel,
action;
function asPromise(value) {
return (value || {}).then ? value : {
then: function (callback) {
return asPromise(callback(value));
}
};
}
beforeEach(function () {
mockNow = jasmine.createSpy('now');
mockDomainObject = jasmine.createSpyObj(
'domainObject',
[ 'getCapability', 'useCapability' ]
);
mockPersistence = jasmine.createSpyObj(
'persistence',
['persist']
);
mockDomainObject.getCapability.andCallFake(function (c) {
return (c === 'persistence') && mockPersistence;
});
mockDomainObject.useCapability.andCallFake(function (c, v) {
if (c === 'mutation') {
testModel = v(testModel) || testModel;
return asPromise(true);
}
});
testModel = {};
action = new AbstractStartTimerAction(mockNow, {
domainObject: mockDomainObject
});
});
it("updates the model with a timestamp and persists", function () {
mockNow.andReturn(12000);
action.perform();
expect(testModel.timestamp).toEqual(12000);
expect(mockPersistence.persist).toHaveBeenCalled();
});
it("does not truncate milliseconds", function () {
mockNow.andReturn(42321);
action.perform();
expect(testModel.timestamp).toEqual(42321);
expect(mockPersistence.persist).toHaveBeenCalled();
});
});
}
);

View File

@@ -0,0 +1,76 @@
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
define(
["../../src/actions/RestartTimerAction"],
function (RestartTimerAction) {
"use strict";
describe("A timer's restart action", function () {
var mockNow,
mockDomainObject,
mockPersistence,
testModel,
testContext,
action;
function asPromise(value) {
return (value || {}).then ? value : {
then: function (callback) {
return asPromise(callback(value));
}
};
}
beforeEach(function () {
mockNow = jasmine.createSpy('now');
mockDomainObject = jasmine.createSpyObj(
'domainObject',
[ 'getCapability', 'useCapability', 'getModel' ]
);
mockPersistence = jasmine.createSpyObj(
'persistence',
['persist']
);
mockDomainObject.getCapability.andCallFake(function (c) {
return (c === 'persistence') && mockPersistence;
});
mockDomainObject.useCapability.andCallFake(function (c, v) {
if (c === 'mutation') {
testModel = v(testModel) || testModel;
return asPromise(true);
}
});
mockDomainObject.getModel.andCallFake(function () {
return testModel;
});
testModel = {};
testContext = { domainObject: mockDomainObject };
action = new RestartTimerAction(mockNow, testContext);
});
it("updates the model with a timestamp and persists", function () {
mockNow.andReturn(12000);
action.perform();
expect(testModel.timestamp).toEqual(12000);
expect(mockPersistence.persist).toHaveBeenCalled();
});
it("applies only to timers with a target time", function () {
testModel.type = 'warp.timer';
testModel.timestamp = 12000;
expect(RestartTimerAction.appliesTo(testContext)).toBeTruthy();
testModel.type = 'warp.timer';
testModel.timestamp = undefined;
expect(RestartTimerAction.appliesTo(testContext)).toBeFalsy();
testModel.type = 'warp.clock';
testModel.timestamp = 12000;
expect(RestartTimerAction.appliesTo(testContext)).toBeFalsy();
});
});
}
);

View File

@@ -0,0 +1,76 @@
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
define(
["../../src/actions/StartTimerAction"],
function (StartTimerAction) {
"use strict";
describe("A timer's start action", function () {
var mockNow,
mockDomainObject,
mockPersistence,
testModel,
testContext,
action;
function asPromise(value) {
return (value || {}).then ? value : {
then: function (callback) {
return asPromise(callback(value));
}
};
}
beforeEach(function () {
mockNow = jasmine.createSpy('now');
mockDomainObject = jasmine.createSpyObj(
'domainObject',
[ 'getCapability', 'useCapability', 'getModel' ]
);
mockPersistence = jasmine.createSpyObj(
'persistence',
['persist']
);
mockDomainObject.getCapability.andCallFake(function (c) {
return (c === 'persistence') && mockPersistence;
});
mockDomainObject.useCapability.andCallFake(function (c, v) {
if (c === 'mutation') {
testModel = v(testModel) || testModel;
return asPromise(true);
}
});
mockDomainObject.getModel.andCallFake(function () {
return testModel;
});
testModel = {};
testContext = { domainObject: mockDomainObject };
action = new StartTimerAction(mockNow, testContext);
});
it("updates the model with a timestamp and persists", function () {
mockNow.andReturn(12000);
action.perform();
expect(testModel.timestamp).toEqual(12000);
expect(mockPersistence.persist).toHaveBeenCalled();
});
it("applies only to timers without a target time", function () {
testModel.type = 'warp.timer';
testModel.timestamp = 12000;
expect(StartTimerAction.appliesTo(testContext)).toBeFalsy();
testModel.type = 'warp.timer';
testModel.timestamp = undefined;
expect(StartTimerAction.appliesTo(testContext)).toBeTruthy();
testModel.type = 'warp.clock';
testModel.timestamp = 12000;
expect(StartTimerAction.appliesTo(testContext)).toBeFalsy();
});
});
}
);

View File

@@ -0,0 +1,83 @@
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
define(
["../../src/controllers/ClockController"],
function (ClockController) {
"use strict";
// Wed, 03 Jun 2015 17:56:14 GMT
var TEST_TIMESTAMP = 1433354174000;
describe("A clock view's controller", function () {
var mockScope,
mockTicker,
mockUnticker,
mockDomainObject,
controller;
beforeEach(function () {
mockScope = jasmine.createSpyObj('$scope', ['$watch', '$on']);
mockTicker = jasmine.createSpyObj('ticker', ['listen']);
mockUnticker = jasmine.createSpy('unticker');
mockTicker.listen.andReturn(mockUnticker);
controller = new ClockController(mockScope, mockTicker);
});
it("watches for clock format from the domain object model", function () {
expect(mockScope.$watch).toHaveBeenCalledWith(
"model.clockFormat",
jasmine.any(Function)
);
});
it("subscribes to clock ticks", function () {
expect(mockTicker.listen)
.toHaveBeenCalledWith(jasmine.any(Function));
});
it("unsubscribes to ticks when destroyed", function () {
// Make sure $destroy is being listened for...
expect(mockScope.$on.mostRecentCall.args[0]).toEqual('$destroy');
expect(mockUnticker).not.toHaveBeenCalled();
// ...and makes sure that its listener unsubscribes from ticker
mockScope.$on.mostRecentCall.args[1]();
expect(mockUnticker).toHaveBeenCalled();
});
it("formats using the format string from the model", function () {
mockTicker.listen.mostRecentCall.args[0](TEST_TIMESTAMP);
mockScope.$watch.mostRecentCall.args[1]([
"YYYY-DDD hh:mm:ss",
"clock24"
]);
expect(controller.zone()).toEqual("UTC");
expect(controller.text()).toEqual("2015-154 17:56:14");
expect(controller.ampm()).toEqual("");
});
it("formats 12-hour time", function () {
mockTicker.listen.mostRecentCall.args[0](TEST_TIMESTAMP);
mockScope.$watch.mostRecentCall.args[1]([
"YYYY-DDD hh:mm:ss",
"clock12"
]);
expect(controller.zone()).toEqual("UTC");
expect(controller.text()).toEqual("2015-154 05:56:14");
expect(controller.ampm()).toEqual("PM");
});
it("does not throw exceptions when clockFormat is undefined", function () {
mockTicker.listen.mostRecentCall.args[0](TEST_TIMESTAMP);
expect(function () {
mockScope.$watch.mostRecentCall.args[1](undefined);
}).not.toThrow();
});
});
}
);

View File

@@ -0,0 +1,63 @@
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
define(
["../../src/controllers/RefreshingController"],
function (RefreshingController) {
"use strict";
describe("The refreshing controller", function () {
var mockScope,
mockTicker,
mockUnticker,
controller;
beforeEach(function () {
mockScope = jasmine.createSpyObj('$scope', ['$on']);
mockTicker = jasmine.createSpyObj('ticker', ['listen']);
mockUnticker = jasmine.createSpy('unticker');
mockTicker.listen.andReturn(mockUnticker);
controller = new RefreshingController(mockScope, mockTicker);
});
it("refreshes the represented object on every tick", function () {
var mockDomainObject = jasmine.createSpyObj(
'domainObject',
[ 'getCapability' ]
),
mockPersistence = jasmine.createSpyObj(
'persistence',
[ 'persist', 'refresh' ]
);
mockDomainObject.getCapability.andCallFake(function (c) {
return (c === 'persistence') && mockPersistence;
});
mockScope.domainObject = mockDomainObject;
mockTicker.listen.mostRecentCall.args[0](12321);
expect(mockPersistence.refresh).toHaveBeenCalled();
expect(mockPersistence.persist).not.toHaveBeenCalled();
});
it("subscribes to clock ticks", function () {
expect(mockTicker.listen)
.toHaveBeenCalledWith(jasmine.any(Function));
});
it("unsubscribes to ticks when destroyed", function () {
// Make sure $destroy is being listened for...
expect(mockScope.$on.mostRecentCall.args[0]).toEqual('$destroy');
expect(mockUnticker).not.toHaveBeenCalled();
// ...and makes sure that its listener unsubscribes from ticker
mockScope.$on.mostRecentCall.args[1]();
expect(mockUnticker).toHaveBeenCalled();
});
});
}
);

View File

@@ -0,0 +1,178 @@
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
define(
["../../src/controllers/TimerController"],
function (TimerController) {
"use strict";
// Wed, 03 Jun 2015 17:56:14 GMT
var TEST_TIMESTAMP = 1433354174000;
describe("A timer view's controller", function () {
var mockScope,
mockWindow,
mockNow,
mockDomainObject,
mockActionCapability,
mockStart,
mockRestart,
testModel,
controller;
function invokeWatch(expr, value) {
mockScope.$watch.calls.forEach(function (call) {
if (call.args[0] === expr) {
call.args[1](value);
}
});
}
beforeEach(function () {
mockScope = jasmine.createSpyObj(
'$scope',
['$watch', '$on', '$apply']
);
mockWindow = jasmine.createSpyObj(
'$window',
['requestAnimationFrame']
);
mockDomainObject = jasmine.createSpyObj(
'domainObject',
[ 'getCapability', 'useCapability', 'getModel' ]
);
mockActionCapability = jasmine.createSpyObj(
'action',
['getActions']
);
mockStart = jasmine.createSpyObj(
'start',
['getMetadata', 'perform']
);
mockRestart = jasmine.createSpyObj(
'restart',
['getMetadata', 'perform']
);
mockNow = jasmine.createSpy('now');
mockDomainObject.getCapability.andCallFake(function (c) {
return (c === 'action') && mockActionCapability;
});
mockDomainObject.getModel.andCallFake(function () {
return testModel;
});
mockActionCapability.getActions.andCallFake(function (k) {
return [{
'warp.timer.start': mockStart,
'warp.timer.restart': mockRestart
}[k]];
});
mockStart.getMetadata.andReturn({ glyph: "S", name: "Start" });
mockRestart.getMetadata.andReturn({ glyph: "R", name: "Restart" });
mockScope.domainObject = mockDomainObject;
testModel = {};
controller = new TimerController(mockScope, mockWindow, mockNow);
});
it("watches for the domain object in view", function () {
expect(mockScope.$watch).toHaveBeenCalledWith(
"domainObject",
jasmine.any(Function)
);
});
it("watches for domain object modifications", function () {
expect(mockScope.$watch).toHaveBeenCalledWith(
"model.modified",
jasmine.any(Function)
);
});
it("updates on a timer", function () {
expect(mockWindow.requestAnimationFrame)
.toHaveBeenCalledWith(jasmine.any(Function));
});
it("displays nothing when there is no target", function () {
// Notify that domain object is available via scope
invokeWatch('domainObject', mockDomainObject);
mockNow.andReturn(TEST_TIMESTAMP);
mockWindow.requestAnimationFrame.mostRecentCall.args[0]();
expect(controller.sign()).toEqual("");
expect(controller.text()).toEqual("");
});
it("formats time to display relative to target", function () {
testModel.timestamp = TEST_TIMESTAMP;
testModel.timerFormat = 'long';
// Notify that domain object is available via scope
invokeWatch('domainObject', mockDomainObject);
mockNow.andReturn(TEST_TIMESTAMP + 121000);
mockWindow.requestAnimationFrame.mostRecentCall.args[0]();
expect(controller.sign()).toEqual("+");
expect(controller.text()).toEqual("0D 00:02:01");
mockNow.andReturn(TEST_TIMESTAMP - 121000);
mockWindow.requestAnimationFrame.mostRecentCall.args[0]();
expect(controller.sign()).toEqual("-");
expect(controller.text()).toEqual("0D 00:02:01");
mockNow.andReturn(TEST_TIMESTAMP);
mockWindow.requestAnimationFrame.mostRecentCall.args[0]();
expect(controller.sign()).toEqual("");
expect(controller.text()).toEqual("0D 00:00:00");
});
it("shows glyph & name for the applicable start/restart action", function () {
invokeWatch('domainObject', mockDomainObject);
expect(controller.buttonGlyph()).toEqual("S");
expect(controller.buttonText()).toEqual("Start");
testModel.timestamp = 12321;
invokeWatch('model.modified', 1);
expect(controller.buttonGlyph()).toEqual("R");
expect(controller.buttonText()).toEqual("Restart");
});
it("performs correct start/restart action on click", function () {
invokeWatch('domainObject', mockDomainObject);
expect(mockStart.perform).not.toHaveBeenCalled();
controller.clickButton();
expect(mockStart.perform).toHaveBeenCalled();
testModel.timestamp = 12321;
invokeWatch('model.modified', 1);
expect(mockRestart.perform).not.toHaveBeenCalled();
controller.clickButton();
expect(mockRestart.perform).toHaveBeenCalled();
});
it("stops requesting animation frames when destroyed", function () {
var initialCount = mockWindow.requestAnimationFrame.calls.length;
// First, check that normally new frames keep getting requested
mockWindow.requestAnimationFrame.mostRecentCall.args[0]();
expect(mockWindow.requestAnimationFrame.calls.length)
.toEqual(initialCount + 1);
mockWindow.requestAnimationFrame.mostRecentCall.args[0]();
expect(mockWindow.requestAnimationFrame.calls.length)
.toEqual(initialCount + 2);
// Now, verify that it stops after $destroy
expect(mockScope.$on.mostRecentCall.args[0])
.toEqual('$destroy');
mockScope.$on.mostRecentCall.args[1]();
// Frames should no longer get requested
mockWindow.requestAnimationFrame.mostRecentCall.args[0]();
expect(mockWindow.requestAnimationFrame.calls.length)
.toEqual(initialCount + 2);
mockWindow.requestAnimationFrame.mostRecentCall.args[0]();
expect(mockWindow.requestAnimationFrame.calls.length)
.toEqual(initialCount + 2);
});
});
}
);

View File

@@ -0,0 +1,96 @@
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
define(
["../../src/controllers/TimerFormatter"],
function (TimerFormatter) {
"use strict";
var MS_IN_SEC = 1000,
MS_IN_MIN = MS_IN_SEC * 60,
MS_IN_HR = MS_IN_MIN * 60,
MS_IN_DAY = MS_IN_HR * 24;
describe("The timer value formatter", function () {
var formatter = new TimerFormatter();
function sum(a, b) {
return a + b;
}
function toDuration(days, hours, mins, secs) {
return [
days * MS_IN_DAY,
hours * MS_IN_HR,
mins * MS_IN_MIN,
secs * MS_IN_SEC
].reduce(sum, 0);
}
function twoDigits(n) {
return n < 10 ? ('0' + n) : n;
}
it("formats short-form values (no days)", function () {
expect(formatter.short(toDuration(0, 123, 2, 3) + 123))
.toEqual("123:02:03");
});
it("formats negative short-form values (no days)", function () {
expect(formatter.short(-toDuration(0, 123, 2, 3) + 123))
.toEqual("123:02:03");
});
it("formats long-form values (with days)", function () {
expect(formatter.long(toDuration(0, 123, 2, 3) + 123))
.toEqual("5D 03:02:03");
});
it("formats negative long-form values (no days)", function () {
expect(formatter.long(-toDuration(0, 123, 2, 3) + 123))
.toEqual("5D 03:02:03");
});
it("rounds seconds down for positive durations", function () {
expect(formatter.short(MS_IN_SEC + 600))
.toEqual("00:00:01");
});
it("rounds seconds up for negative durations", function () {
expect(formatter.short(-MS_IN_SEC - 600))
.toEqual("00:00:02");
});
it("short-formats correctly around negative time borders", function () {
expect(formatter.short(-1)).toEqual("00:00:01");
expect(formatter.short(-1000)).toEqual("00:00:01");
expect(formatter.short(-1001)).toEqual("00:00:02");
expect(formatter.short(-2000)).toEqual("00:00:02");
expect(formatter.short(-59001)).toEqual("00:01:00");
expect(formatter.short(-60000)).toEqual("00:01:00");
expect(formatter.short(-MS_IN_HR + 999)).toEqual("01:00:00");
expect(formatter.short(-MS_IN_HR)).toEqual("01:00:00");
});
it("differentiates between values around zero", function () {
// These are more than 1000 ms apart so should not appear
// as the same second
expect(formatter.short(-999))
.not.toEqual(formatter.short(999));
});
it("handles negative days", function () {
expect(formatter.long(-10 * MS_IN_DAY))
.toEqual("10D 00:00:00");
expect(formatter.long(-10 * MS_IN_DAY + 100))
.toEqual("10D 00:00:00");
expect(formatter.long(-10 * MS_IN_DAY + 999))
.toEqual("10D 00:00:00");
expect(formatter.short(-10 * MS_IN_DAY + 100))
.toEqual("240:00:00");
});
});
}
);

View File

@@ -0,0 +1,40 @@
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
define(
["../../src/indicators/ClockIndicator"],
function (ClockIndicator) {
"use strict";
// Wed, 03 Jun 2015 17:56:14 GMT
var TEST_TIMESTAMP = 1433354174000,
TEST_FORMAT = "YYYY-DDD HH:mm:ss";
describe("The clock indicator", function () {
var mockTicker,
mockUnticker,
indicator;
beforeEach(function () {
mockTicker = jasmine.createSpyObj('ticker', ['listen']);
mockUnticker = jasmine.createSpy('unticker');
mockTicker.listen.andReturn(mockUnticker);
indicator = new ClockIndicator(mockTicker, TEST_FORMAT);
});
it("displays the current time", function () {
mockTicker.listen.mostRecentCall.args[0](TEST_TIMESTAMP);
expect(indicator.getText()).toEqual("2015-154 17:56:14 UTC");
});
it("implements the Indicator interface", function () {
expect(indicator.getGlyph()).toEqual(jasmine.any(String));
expect(indicator.getGlyphClass()).toEqual(jasmine.any(String));
expect(indicator.getText()).toEqual(jasmine.any(String));
expect(indicator.getDescription()).toEqual(jasmine.any(String));
});
});
}
);

View File

@@ -0,0 +1,43 @@
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
define(
["../../src/services/TickerService"],
function (TickerService) {
"use strict";
var TEST_TIMESTAMP = 1433354174000;
describe("The ticker service", function () {
var mockTimeout,
mockNow,
mockCallback,
tickerService;
beforeEach(function () {
mockTimeout = jasmine.createSpy('$timeout');
mockNow = jasmine.createSpy('now');
mockCallback = jasmine.createSpy('callback');
mockNow.andReturn(TEST_TIMESTAMP);
tickerService = new TickerService(mockTimeout, mockNow);
});
it("notifies listeners of clock ticks", function () {
tickerService.listen(mockCallback);
mockNow.andReturn(TEST_TIMESTAMP + 12321);
mockTimeout.mostRecentCall.args[0]();
expect(mockCallback)
.toHaveBeenCalledWith(TEST_TIMESTAMP + 12321);
});
it("allows listeners to unregister", function () {
tickerService.listen(mockCallback)(); // Unregister immediately
mockNow.andReturn(TEST_TIMESTAMP + 12321);
mockTimeout.mostRecentCall.args[0]();
expect(mockCallback).not
.toHaveBeenCalledWith(TEST_TIMESTAMP + 12321);
});
});
}
);

View File

@@ -0,0 +1,11 @@
[
"actions/AbstractStartTimerAction",
"actions/RestartTimerAction",
"actions/StartTimerAction",
"controllers/ClockController",
"controllers/RefreshingController",
"controllers/TimerController",
"controllers/TimerFormatter",
"indicators/ClockIndicator",
"services/TickerService"
]