[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/controllers/drag/TimelineDragHandleFactory'],
function (TimelineDragHandleFactory) {
'use strict';
describe("A Timeline drag handle factory", function () {
var mockDragHandler,
mockSnapHandler,
mockDomainObject,
mockType,
testType,
factory;
beforeEach(function () {
mockDragHandler = jasmine.createSpyObj(
'dragHandler',
[ 'start' ]
);
mockSnapHandler = jasmine.createSpyObj(
'snapHandler',
[ 'snap' ]
);
mockDomainObject = jasmine.createSpyObj(
'domainObject',
[ 'getCapability', 'getId' ]
);
mockType = jasmine.createSpyObj(
'type',
[ 'instanceOf' ]
);
mockDomainObject.getId.andReturn('test-id');
mockDomainObject.getCapability.andReturn(mockType);
mockType.instanceOf.andCallFake(function (t) {
return t === testType;
});
factory = new TimelineDragHandleFactory(
mockDragHandler,
mockSnapHandler
);
});
it("inspects an object's type capability", function () {
factory.handles(mockDomainObject);
expect(mockDomainObject.getCapability)
.toHaveBeenCalledWith('type');
});
it("provides three handles for activities", function () {
testType = "warp.activity";
expect(factory.handles(mockDomainObject).length)
.toEqual(3);
});
it("provides two handles for timelines", function () {
testType = "warp.timeline";
expect(factory.handles(mockDomainObject).length)
.toEqual(2);
});
});
}
);

View File

@@ -0,0 +1,209 @@
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
define(
['../../../src/controllers/drag/TimelineDragHandler'],
function (TimelineDragHandler) {
'use strict';
describe("A Timeline drag handler", function () {
var mockLoader,
mockSelection,
testConfiguration,
mockDomainObject,
mockDomainObjects,
mockTimespans,
mockMutations,
mockPersists,
mockCallback,
handler;
function asPromise(value) {
return (value || {}).then ? value : {
then: function (callback) {
return asPromise(callback(value));
}
};
}
function subgraph(domainObject, objects) {
function lookupSubgraph(id) {
return subgraph(objects[id], objects);
}
return {
domainObject: domainObject,
composition: (domainObject.getModel().composition || [])
.map(lookupSubgraph)
};
}
function makeMockDomainObject(id, composition) {
var mockDomainObject = jasmine.createSpyObj(
'domainObject-' + id,
['getId', 'getModel', 'getCapability', 'useCapability']
);
mockDomainObject.getId.andReturn(id);
mockDomainObject.getModel.andReturn({ composition: composition });
mockDomainObject.useCapability.andReturn(asPromise(mockTimespans[id]));
mockDomainObject.getCapability.andCallFake(function (c) {
return {
persistence: mockPersists[id],
mutation: mockMutations[id]
}[c];
});
return mockDomainObject;
}
beforeEach(function () {
mockTimespans = {};
mockPersists = {};
mockMutations = {};
['a', 'b', 'c', 'd', 'e', 'f'].forEach(function (id, index) {
mockTimespans[id] = jasmine.createSpyObj(
'timespan-' + id,
[ 'getStart', 'getEnd', 'getDuration', 'setStart', 'setEnd', 'setDuration' ]
);
mockPersists[id] = jasmine.createSpyObj(
'persistence-' + id,
[ 'persist' ]
);
mockMutations[id] = jasmine.createSpyObj(
'mutation-' + id,
[ 'mutate' ]
);
mockTimespans[id].getStart.andReturn(index * 1000);
mockTimespans[id].getDuration.andReturn(4000 + index);
mockTimespans[id].getEnd.andReturn(4000 + index + index * 1000);
});
mockLoader = jasmine.createSpyObj('objectLoader', ['load']);
mockDomainObject = makeMockDomainObject('a', ['b', 'c']);
mockDomainObjects = {
a: mockDomainObject,
b: makeMockDomainObject('b', ['d']),
c: makeMockDomainObject('c', ['e', 'f']),
d: makeMockDomainObject('d', []),
e: makeMockDomainObject('e', []),
f: makeMockDomainObject('f', [])
};
mockSelection = jasmine.createSpyObj('selection', ['get', 'select']);
mockCallback = jasmine.createSpy('callback');
testConfiguration = {};
mockLoader.load.andReturn(asPromise(
subgraph(mockDomainObject, mockDomainObjects)
));
handler = new TimelineDragHandler(
mockDomainObject,
mockLoader
);
});
it("uses the loader to find subgraph", function () {
expect(mockLoader.load).toHaveBeenCalledWith(
mockDomainObject,
'timespan'
);
});
it("reports available object identifiers", function () {
expect(handler.ids())
.toEqual(Object.keys(mockDomainObjects).sort());
});
it("exposes start/end/duration from timespan capabilities", function () {
expect(handler.start('a')).toEqual(0);
expect(handler.start('b')).toEqual(1000);
expect(handler.start('c')).toEqual(2000);
expect(handler.duration('a')).toEqual(4000);
expect(handler.duration('b')).toEqual(4001);
expect(handler.duration('c')).toEqual(4002);
expect(handler.end('a')).toEqual(4000);
expect(handler.end('b')).toEqual(5001);
expect(handler.end('c')).toEqual(6002);
});
it("accepts objects instead of identifiers for start/end/duration calls", function () {
Object.keys(mockDomainObjects).forEach(function (id) {
expect(handler.start(mockDomainObjects[id])).toEqual(handler.start(id));
expect(handler.duration(mockDomainObjects[id])).toEqual(handler.duration(id));
expect(handler.end(mockDomainObjects[id])).toEqual(handler.end(id));
});
});
it("mutates objects", function () {
handler.start('a', 123);
expect(mockTimespans.a.setStart).toHaveBeenCalledWith(123);
handler.duration('b', 42);
expect(mockTimespans.b.setDuration).toHaveBeenCalledWith(42);
handler.end('c', 12321);
expect(mockTimespans.c.setEnd).toHaveBeenCalledWith(12321);
});
it("disallows negative starts, durations", function () {
handler.start('a', -100);
handler.duration('b', -1000);
expect(mockTimespans.a.setStart).toHaveBeenCalledWith(0);
expect(mockTimespans.b.setDuration).toHaveBeenCalledWith(0);
});
it("disallows starts greater than ends violations", function () {
handler.start('a', 5000);
handler.end('b', 500);
expect(mockTimespans.a.setStart).toHaveBeenCalledWith(4000); // end time
expect(mockTimespans.b.setEnd).toHaveBeenCalledWith(1000); // start time
});
it("moves objects in groups", function () {
handler.move('b', 42);
expect(mockTimespans.b.setStart).toHaveBeenCalledWith(1042);
expect(mockTimespans.b.setEnd).toHaveBeenCalledWith(5043);
expect(mockTimespans.d.setStart).toHaveBeenCalledWith(3042);
expect(mockTimespans.d.setEnd).toHaveBeenCalledWith(7045);
// Verify no other interactions
['a', 'c', 'e', 'f'].forEach(function (id) {
expect(mockTimespans[id].setStart).not.toHaveBeenCalled();
expect(mockTimespans[id].setEnd).not.toHaveBeenCalled();
});
});
it("moves whole subtrees", function () {
handler.move('a', 12321);
// We verify the math in the previous test, so just verify
// that the whole tree is effected here.
Object.keys(mockTimespans).forEach(function (id) {
expect(mockTimespans[id].setStart).toHaveBeenCalled();
});
});
it("prevents bulk moves past 0", function () {
// Have a start later; new lowest start is b, at 1000
mockTimespans.a.getStart.andReturn(10000);
handler.move('a', -10000);
// Verify that move was stopped at 0, for b, even though
// move was initiated at a
expect(mockTimespans.a.setStart).toHaveBeenCalledWith(9000);
expect(mockTimespans.b.setStart).toHaveBeenCalledWith(0);
expect(mockTimespans.c.setStart).toHaveBeenCalledWith(1000);
});
it("persists mutated objects", function () {
handler.start('a', 20);
handler.end('b', 50);
handler.duration('c', 30);
handler.persist();
expect(mockPersists.a.persist).toHaveBeenCalled();
expect(mockPersists.b.persist).toHaveBeenCalled();
expect(mockPersists.c.persist).toHaveBeenCalled();
expect(mockPersists.d.persist).not.toHaveBeenCalled();
expect(mockPersists.e.persist).not.toHaveBeenCalled();
expect(mockPersists.f.persist).not.toHaveBeenCalled();
});
});
}
);

View File

@@ -0,0 +1,53 @@
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
define(
['../../../src/controllers/drag/TimelineDragPopulator'],
function (TimelineDragPopulator) {
"use strict";
describe("The timeline drag populator", function () {
var mockObjectLoader,
mockPromise,
mockSwimlane,
mockDomainObject,
populator;
beforeEach(function () {
mockObjectLoader = jasmine.createSpyObj("objectLoader", ["load"]);
mockPromise = jasmine.createSpyObj("promise", ["then"]);
mockSwimlane = jasmine.createSpyObj("swimlane", ["color"]);
mockDomainObject = jasmine.createSpyObj(
"domainObject",
["getCapability", "getId"]
);
mockSwimlane.domainObject = mockDomainObject;
mockObjectLoader.load.andReturn(mockPromise);
populator = new TimelineDragPopulator(mockObjectLoader);
});
it("loads timespans for the represented object's subgraph", function () {
populator.populate(mockDomainObject);
expect(mockObjectLoader.load).toHaveBeenCalledWith(
mockDomainObject,
'timespan'
);
});
it("updates handles for selections", function () {
// Ensure we have a represented object context
populator.populate(mockDomainObject);
// Initially, no selection and no handles
expect(populator.get()).toEqual([]);
// Select the swimlane
populator.select(mockSwimlane);
// We should have handles now
expect(populator.get().length).toEqual(3);
});
});
}
);

View File

@@ -0,0 +1,96 @@
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
define(
['../../../src/controllers/drag/TimelineEndHandle', '../../../src/TimelineConstants'],
function (TimelineEndHandle, TimelineConstants) {
'use strict';
describe("A Timeline end drag handle", function () {
var mockDragHandler,
mockSnapHandler,
mockZoomController,
handle;
beforeEach(function () {
mockDragHandler = jasmine.createSpyObj(
'dragHandler',
[ 'end', 'persist' ]
);
mockSnapHandler = jasmine.createSpyObj(
'snapHandler',
[ 'snap' ]
);
mockZoomController = jasmine.createSpyObj(
'zoom',
[ 'toMillis', 'toPixels' ]
);
mockDragHandler.end.andReturn(12321);
// Echo back the value from snapper for most tests
mockSnapHandler.snap.andCallFake(function (ts) {
return ts;
});
// Double pixels to get millis, for test purposes
mockZoomController.toMillis.andCallFake(function (px) {
return px * 2;
});
mockZoomController.toPixels.andCallFake(function (ms) {
return ms / 2;
});
handle = new TimelineEndHandle(
'test-id',
mockDragHandler,
mockSnapHandler
);
});
it("provides a style for templates", function () {
var w = TimelineConstants.HANDLE_WIDTH;
expect(handle.style(mockZoomController)).toEqual({
// Left should be adjusted by zoom controller
left: (12321 / 2) - w + 'px',
// Width should match the defined constant
width: w + 'px'
});
});
it("forwards drags to the drag handler", function () {
handle.begin();
handle.drag(100, mockZoomController);
// Should have been interpreted as a +200 ms change
expect(mockDragHandler.end).toHaveBeenCalledWith(
"test-id",
12521
);
});
it("snaps drags to other end points", function () {
mockSnapHandler.snap.andReturn(42);
handle.begin();
handle.drag(-10, mockZoomController);
// Should have used snap-to timestamp
expect(mockDragHandler.end).toHaveBeenCalledWith(
"test-id",
42
);
});
it("persists when a move is complete", function () {
// Simulate normal drag cycle
handle.begin();
handle.drag(100, mockZoomController);
// Should not have persisted yet
expect(mockDragHandler.persist).not.toHaveBeenCalled();
// Finish the drag
handle.finish();
// Now it should have persisted
expect(mockDragHandler.persist).toHaveBeenCalled();
});
});
}
);

View File

@@ -0,0 +1,163 @@
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
define(
['../../../src/controllers/drag/TimelineMoveHandle', '../../../src/TimelineConstants'],
function (TimelineMoveHandle, TimelineConstants) {
'use strict';
describe("A Timeline move drag handle", function () {
var mockDragHandler,
mockSnapHandler,
mockZoomController,
handle;
beforeEach(function () {
mockDragHandler = jasmine.createSpyObj(
'dragHandler',
[ 'start', 'duration', 'end', 'move', 'persist' ]
);
mockSnapHandler = jasmine.createSpyObj(
'snapHandler',
[ 'snap' ]
);
mockZoomController = jasmine.createSpyObj(
'zoom',
[ 'toMillis', 'toPixels' ]
);
mockDragHandler.start.andReturn(12321);
mockDragHandler.duration.andReturn(4200);
mockDragHandler.end.andReturn(12321 + 4200);
// Echo back the value from snapper for most tests
mockSnapHandler.snap.andCallFake(function (ts) {
return ts;
});
// Double pixels to get millis, for test purposes
mockZoomController.toMillis.andCallFake(function (px) {
return px * 2;
});
mockZoomController.toPixels.andCallFake(function (ms) {
return ms / 2;
});
handle = new TimelineMoveHandle(
'test-id',
mockDragHandler,
mockSnapHandler
);
});
it("provides a style for templates", function () {
var w = TimelineConstants.HANDLE_WIDTH;
expect(handle.style(mockZoomController)).toEqual({
// Left should be adjusted by zoom controller
left: (12321 / 2) + w + 'px',
// Width should be duration minus end points
width: 2100 - (w * 2) + 'px'
});
});
it("forwards drags to the drag handler", function () {
handle.begin();
handle.drag(100, mockZoomController);
// Should have been interpreted as a +200 ms change
expect(mockDragHandler.move).toHaveBeenCalledWith(
"test-id",
200
);
});
it("tracks drags incrementally", function () {
handle.begin();
handle.drag(100, mockZoomController);
// Should have been interpreted as a +200 ms change...
expect(mockDragHandler.move).toHaveBeenCalledWith(
"test-id",
200
);
// Reflect the change from the drag handler
mockDragHandler.start.andReturn(12521);
mockDragHandler.end.andReturn(12521 + 4200);
// ....followed by a +100 ms change.
handle.drag(150, mockZoomController);
expect(mockDragHandler.move).toHaveBeenCalledWith(
"test-id",
100
);
});
it("snaps drags to other end points", function () {
mockSnapHandler.snap.andCallFake(function (ts) {
return ts + 10;
});
handle.begin();
handle.drag(100, mockZoomController);
// Should have used snap-to timestamp, which was 10
// ms greater than the provided one
expect(mockDragHandler.move).toHaveBeenCalledWith(
"test-id",
210
);
});
it("considers snaps for both endpoints", function () {
handle.begin();
expect(mockSnapHandler.snap).not.toHaveBeenCalled();
handle.drag(100, mockZoomController);
expect(mockSnapHandler.snap.calls.length).toEqual(2);
});
it("chooses the closest snap-to location", function () {
// Use a toggle to give snapped timestamps that are
// different distances away from the original.
// The move handle needs to choose the closest snap-to,
// regardless of whether it is the start/end (which
// will vary based on the initial state of this toggle.)
var toggle = false;
mockSnapHandler.snap.andCallFake(function (ts) {
toggle = !toggle;
return ts + (toggle ? -5 : 10);
});
handle.begin();
handle.drag(100, mockZoomController);
expect(mockDragHandler.move).toHaveBeenCalledWith(
"test-id",
195 // Chose the -5
);
// Reflect the change from the drag handler
mockDragHandler.start.andReturn(12521 - 5);
mockDragHandler.end.andReturn(12521 + 4200 - 5);
toggle = true; // Change going-in state
handle.drag(300, mockZoomController);
// Note that the -5 offset is shown in the current state,
// so snapping to the -5 implies that the full 400ms will
// be moved (again, relative to dragHandler's reported state)
expect(mockDragHandler.move).toHaveBeenCalledWith(
"test-id",
400 // Still chose the -5
);
});
it("persists when a move is complete", function () {
// Simulate normal drag cycle
handle.begin();
handle.drag(100, mockZoomController);
// Should not have persisted yet
expect(mockDragHandler.persist).not.toHaveBeenCalled();
// Finish the drag
handle.finish();
// Now it should have persisted
expect(mockDragHandler.persist).toHaveBeenCalled();
});
});
}
);

View File

@@ -0,0 +1,60 @@
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
define(
['../../../src/controllers/drag/TimelineSnapHandler'],
function (TimelineSnapHandler) {
'use strict';
describe("A Timeline snap handler", function () {
var mockDragHandler,
handler;
beforeEach(function () {
var starts = { a: 1000, b: 2000, c: 2500, d: 2600 },
ends = { a: 2050, b: 3000, c: 2700, d: 10000 };
mockDragHandler = jasmine.createSpyObj(
'dragHandler',
[ 'start', 'end', 'ids' ]
);
mockDragHandler.ids.andReturn(['a', 'b', 'c', 'd']);
mockDragHandler.start.andCallFake(function (id) {
return starts[id];
});
mockDragHandler.end.andCallFake(function (id) {
return ends[id];
});
handler = new TimelineSnapHandler(mockDragHandler);
});
it("provides a preferred snap location within tolerance", function () {
expect(handler.snap(2511, 15, 'a')).toEqual(2500); // c's start
expect(handler.snap(2488, 15, 'a')).toEqual(2500); // c's start
expect(handler.snap(10, 1000, 'b')).toEqual(1000); // a's start
expect(handler.snap(2711, 20, 'd')).toEqual(2700); // c's end
});
it("excludes provided id from snapping", function () {
// Don't want objects to snap to themselves, so we need
// this exclusion.
expect(handler.snap(2010, 50, 'b')).toEqual(2050); // a's end
// Verify that b's start would have been used had the
// id not been provided
expect(handler.snap(2010, 50, 'd')).toEqual(2000);
});
it("snaps to the closest point, when multiple match", function () {
// 2600 and 2700 (plus others) are both in range here
expect(handler.snap(2651, 1000, 'a')).toEqual(2700);
});
it("does not snap if no points are within tolerance", function () {
// Closest are 1000 and 2000, which are well outside of tolerance
expect(handler.snap(1503, 100, 'd')).toEqual(1503);
});
});
}
);

View File

@@ -0,0 +1,95 @@
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
define(
['../../../src/controllers/drag/TimelineStartHandle', '../../../src/TimelineConstants'],
function (TimelineStartHandle, TimelineConstants) {
'use strict';
describe("A Timeline start drag handle", function () {
var mockDragHandler,
mockSnapHandler,
mockZoomController,
handle;
beforeEach(function () {
mockDragHandler = jasmine.createSpyObj(
'dragHandler',
[ 'start', 'persist' ]
);
mockSnapHandler = jasmine.createSpyObj(
'snapHandler',
[ 'snap' ]
);
mockZoomController = jasmine.createSpyObj(
'zoom',
[ 'toMillis', 'toPixels' ]
);
mockDragHandler.start.andReturn(12321);
// Echo back the value from snapper for most tests
mockSnapHandler.snap.andCallFake(function (ts) {
return ts;
});
// Double pixels to get millis, for test purposes
mockZoomController.toMillis.andCallFake(function (px) {
return px * 2;
});
mockZoomController.toPixels.andCallFake(function (ms) {
return ms / 2;
});
handle = new TimelineStartHandle(
'test-id',
mockDragHandler,
mockSnapHandler
);
});
it("provides a style for templates", function () {
expect(handle.style(mockZoomController)).toEqual({
// Left should be adjusted by zoom controller
left: (12321 / 2) + 'px',
// Width should match the defined constant
width: TimelineConstants.HANDLE_WIDTH + 'px'
});
});
it("forwards drags to the drag handler", function () {
handle.begin();
handle.drag(100, mockZoomController);
// Should have been interpreted as a +200 ms change
expect(mockDragHandler.start).toHaveBeenCalledWith(
"test-id",
12521
);
});
it("snaps drags to other end points", function () {
mockSnapHandler.snap.andReturn(42);
handle.begin();
handle.drag(-10, mockZoomController);
// Should have used snap-to timestamp
expect(mockDragHandler.start).toHaveBeenCalledWith(
"test-id",
42
);
});
it("persists when a move is complete", function () {
// Simulate normal drag cycle
handle.begin();
handle.drag(100, mockZoomController);
// Should not have persisted yet
expect(mockDragHandler.persist).not.toHaveBeenCalled();
// Finish the drag
handle.finish();
// Now it should have persisted
expect(mockDragHandler.persist).toHaveBeenCalled();
});
});
}
);