[Time Conductor] Prevent route change when time conductor values change (#1342)

* [Time Conductor] Prevent route change on setting search parameters. fixes #1341

* [Inspector] Fixed incorrect listener deregistration which was causing errors on scope destruction

* Bare route is redirect to browse

* [Browse] handle routing without breaking $route

Manage route transitions such that route changes are properly
prevented and navigation events occur while still updating the url.

Resolves a number of issues where path and search updates had
to be supported in a very hacky manner.

https://github.com/nasa/openmct/pull/1342

* [URL] Set search without hacks

Changes in previous commit allow the search parameters to be modified
without accidentally triggering a page reload.

https://github.com/nasa/openmct/pull/1342

* [Views] Update on location changes

If the user has a bookmark or tries to change the current view of an
object by specifying view=someView as a search parameter, the change would
not previously take effect.  This resolves that bug.

https://github.com/nasa/openmct/pull/1342

* [TC] Set query params to undefined

Instead of setting params to null, which would eventually result in those
parameters equaling undefined, set them to undefined to skip the extra
step.

https://github.com/nasa/openmct/pull/1342

* [Instantiate] Instantiate objects with context

Add context to instantiate objects so that they can be navigated
to for editing.

https://github.com/nasa/openmct/pull/1342

* [Tests] Update specs

Update specs to match new expectations.

* [Style] Fix style

* [TC] Remove unused dependency

Remove $route dependency from time conductor controller as it was
not being used.  Resolves review comments.

https://github.com/nasa/openmct/pull/1342#pullrequestreview-11449260
This commit is contained in:
Andrew Henry 2016-12-07 13:33:53 -08:00 committed by GitHub
parent b2da0cb12f
commit 45de84c183
10 changed files with 245 additions and 320 deletions

View File

@ -72,14 +72,13 @@ define([
"extensions": { "extensions": {
"routes": [ "routes": [
{ {
"when": "/browse/:ids*", "when": "/browse/:ids*?",
"template": browseTemplate, "template": browseTemplate,
"reloadOnSearch": false "reloadOnSearch": false
}, },
{ {
"when": "", "when": "",
"template": browseTemplate, "redirectTo": "/browse/"
"reloadOnSearch": false
} }
], ],
"constants": [ "constants": [

View File

@ -28,8 +28,6 @@ define(
[], [],
function () { function () {
var ROOT_ID = "ROOT";
/** /**
* The BrowseController is used to populate the initial scope in Browse * The BrowseController is used to populate the initial scope in Browse
* mode. It loads the root object from the objectService and makes it * mode. It loads the root object from the objectService and makes it
@ -49,74 +47,21 @@ define(
urlService, urlService,
defaultPath defaultPath
) { ) {
var path = [ROOT_ID].concat( var initialPath = ($route.current.params.ids || defaultPath).split("/");
($route.current.params.ids || defaultPath).split("/")
);
function updateRoute(domainObject) { var currentIds = $route.current.params.ids;
var priorRoute = $route.current,
// Act as if params HADN'T changed to avoid page reload
unlisten;
unlisten = $scope.$on('$locationChangeSuccess', function () { $scope.treeModel = {
// Checks path to make sure /browse/ is at front selectedObject: {}
// if so, change $route.current };
if ($location.path().indexOf("/browse/") === 0) {
$route.current = priorRoute;
}
unlisten();
});
// urlService.urlForLocation used to adjust current
// path to new, addressed, path based on
// domainObject
$location.path(urlService.urlForLocation("browse", domainObject));
function idsForObject(domainObject) {
return urlService
.urlForLocation("", domainObject)
.replace('/', '');
} }
function setScopeObjects(domainObject, navigationAllowed) { // Find an object in an array of objects.
if (navigationAllowed) {
$scope.navigatedObject = domainObject;
$scope.treeModel.selectedObject = domainObject;
updateRoute(domainObject);
} else {
//If navigation was unsuccessful (ie. blocked), reset
// the selected object in the tree to the currently
// navigated object
$scope.treeModel.selectedObject = $scope.navigatedObject ;
}
}
// Callback for updating the in-scope reference to the object
// that is currently navigated-to.
function setNavigation(domainObject) {
if (domainObject === $scope.navigatedObject) {
//do nothing;
return;
}
if (domainObject) {
domainObject.getCapability("action").perform("navigate").then(setScopeObjects.bind(undefined, domainObject));
} else {
setScopeObjects(domainObject, true);
}
}
function navigateTo(domainObject) {
// Check if an object has been navigated-to already...
// If not, or if an ID path has been explicitly set in the URL,
// navigate to the URL-specified object.
if (!navigationService.getNavigation() || $route.current.params.ids) {
// If not, pick a default as the last
// root-level component (usually "mine")
navigationService.setNavigation(domainObject);
$scope.navigatedObject = domainObject;
} else {
// Otherwise, just expose the currently navigated object.
$scope.navigatedObject = navigationService.getNavigation();
updateRoute($scope.navigatedObject);
}
}
function findObject(domainObjects, id) { function findObject(domainObjects, id) {
var i; var i;
for (i = 0; i < domainObjects.length; i += 1) { for (i = 0; i < domainObjects.length; i += 1) {
@ -126,63 +71,92 @@ define(
} }
} }
// Navigate to the domain object identified by path[index], // helper, fetch a single object from the object service.
// which we expect to find in the composition of the passed function getObject(id) {
// domain object. return objectService.getObjects([id])
function doNavigate(domainObject, index) { .then(function (results) {
var composition = domainObject.useCapability("composition"); return results[id];
if (composition) {
composition.then(function (c) {
var nextObject = findObject(c, path[index]);
if (nextObject) {
if (index + 1 >= path.length) {
navigateTo(nextObject);
} else {
doNavigate(nextObject, index + 1);
}
} else if (index === 1 && c.length > 0) {
// Roots are in a top-level container that we don't
// want to be selected, so if we couldn't find an
// object at the path we wanted, at least select
// one of its children.
navigateTo(c[c.length - 1]);
} else {
// Couldn't find the next element of the path
// so navigate to the last path object we did find
navigateTo(domainObject);
}
}); });
} else {
// Similar to above case; this object has no composition,
// so navigate to it instead of subsequent path elements.
navigateTo(domainObject);
}
} }
// Load the root object, put it in the scope. // recursively locate and return an object inside of a container
// Also, load its immediate children, and (possibly) // via a path. If at any point in the recursion it fails to find
// navigate to one of them, so that navigation state has // the next object, it will return the parent.
// a useful initial value. function findViaComposition(containerObject, path) {
objectService.getObjects([path[0]]).then(function (objects) { var nextId = path.shift();
$scope.domainObject = objects[path[0]]; if (!nextId) {
doNavigate($scope.domainObject, 1); return containerObject;
}
return containerObject.useCapability('composition')
.then(function (composees) {
var nextObject = findObject(composees, nextId);
if (!nextObject) {
return containerObject;
}
if (!nextObject.hasCapability('composition')) {
return nextObject;
}
return findViaComposition(nextObject, path);
});
}
function navigateToObject(desiredObject) {
$scope.navigatedObject = desiredObject;
$scope.treeModel.selectedObject = desiredObject;
navigationService.setNavigation(desiredObject);
currentIds = idsForObject(desiredObject);
$route.current.pathParams.ids = currentIds;
$location.path('/browse/' + currentIds);
}
function navigateToPath(path) {
return getObject('ROOT')
.then(function (root) {
return findViaComposition(root, path);
})
.then(navigateToObject);
}
getObject('ROOT')
.then(function (root) {
$scope.domainObject = root;
navigateToPath(initialPath);
}); });
// Provide a model for the tree to modify // Handle navigation events from view service. Only navigates
$scope.treeModel = { // if path has changed.
selectedObject: navigationService.getNavigation() function navigateDirectlyToModel(domainObject) {
}; var newIds = idsForObject(domainObject);
if (currentIds !== newIds) {
currentIds = newIds;
navigateToObject(domainObject);
}
}
// Listen for changes in navigation state. // Listen for changes in navigation state.
navigationService.addListener(setNavigation); navigationService.addListener(navigateDirectlyToModel);
// Also listen for changes which come from the tree. Changes in // Also listen for changes which come from the tree. Changes in
// the tree will trigger a change in browse navigation state. // the tree will trigger a change in browse navigation state.
$scope.$watch("treeModel.selectedObject", setNavigation); $scope.$watch("treeModel.selectedObject", navigateDirectlyToModel);
// Listen for route changes which are caused by browser events
// (e.g. bookmarks to pages in OpenMCT) and prevent them. Instead,
// navigate to the path ourselves, which results in it being
// properly set.
$scope.$on('$routeChangeStart', function (event, route) {
if (route.$$route === $route.current.$$route &&
route.pathParams.ids !== $route.current.pathParams.ids) {
event.preventDefault();
navigateToPath(route.pathParams.ids.split('/'));
}
});
// Clean up when the scope is destroyed // Clean up when the scope is destroyed
$scope.$on("$destroy", function () { $scope.$on("$destroy", function () {
navigationService.removeListener(setNavigation); navigationService.removeListener(navigateDirectlyToModel);
}); });
} }

View File

@ -51,24 +51,16 @@ define(
} }
function updateQueryParam(viewKey) { function updateQueryParam(viewKey) {
var unlisten, if (viewKey && $location.search().view !== viewKey) {
priorRoute = $route.current;
if (viewKey) {
$location.search('view', viewKey); $location.search('view', viewKey);
unlisten = $scope.$on('$locationChangeSuccess', function () {
// Checks path to make sure /browse/ is at front
// if so, change $route.current
if ($location.path().indexOf("/browse/") === 0) {
$route.current = priorRoute;
}
unlisten();
});
} }
} }
$scope.$watch('domainObject', setViewForDomainObject); $scope.$watch('domainObject', setViewForDomainObject);
$scope.$watch('representation.selected.key', updateQueryParam); $scope.$watch('representation.selected.key', updateQueryParam);
$scope.$on('$locationChangeSuccess', function () {
setViewForDomainObject($scope.domainObject);
});
$scope.doAction = function (action) { $scope.doAction = function (action) {
return $scope[action] && $scope[action](); return $scope[action] && $scope[action]();

View File

@ -64,11 +64,11 @@ define(
attachStatusListener(domainObject); attachStatusListener(domainObject);
} }
var navigationListener = navigationService.addListener(attachStatusListener); navigationService.addListener(attachStatusListener);
$scope.$on("$destroy", function () { $scope.$on("$destroy", function () {
statusListener(); statusListener();
navigationListener(); navigationService.removeListener(attachStatusListener);
}); });
} }

View File

@ -35,18 +35,17 @@ define(
mockNavigationService, mockNavigationService,
mockRootObject, mockRootObject,
mockUrlService, mockUrlService,
mockDomainObject, mockDefaultRootObject,
mockOtherDomainObject,
mockNextObject, mockNextObject,
testDefaultRoot, testDefaultRoot,
mockActionCapability,
controller; controller;
function mockPromise(value) { function waitsForNavigation() {
return { var calls = mockNavigationService.setNavigation.calls.length;
then: function (callback) { waitsFor(function () {
return mockPromise(callback(value)); return mockNavigationService.setNavigation.calls.length > calls ;
} });
};
} }
function instantiateController() { function instantiateController() {
@ -68,15 +67,27 @@ define(
"$scope", "$scope",
["$on", "$watch"] ["$on", "$watch"]
); );
mockRoute = { current: { params: {} } }; mockRoute = { current: { params: {}, pathParams: {} } };
mockLocation = jasmine.createSpyObj(
"$location",
["path"]
);
mockUrlService = jasmine.createSpyObj( mockUrlService = jasmine.createSpyObj(
"urlService", "urlService",
["urlForLocation"] ["urlForLocation"]
); );
mockUrlService.urlForLocation.andCallFake(function (mode, object) {
if (object === mockDefaultRootObject) {
return [mode, testDefaultRoot].join('/');
}
if (object === mockOtherDomainObject) {
return [mode, 'other'].join('/');
}
if (object === mockNextObject) {
return [mode, testDefaultRoot, 'next'].join('/');
}
throw new Error('Tried to get url for unexpected object');
});
mockLocation = jasmine.createSpyObj(
"$location",
["path"]
);
mockObjectService = jasmine.createSpyObj( mockObjectService = jasmine.createSpyObj(
"objectService", "objectService",
["getObjects"] ["getObjects"]
@ -91,62 +102,78 @@ define(
] ]
); );
mockRootObject = jasmine.createSpyObj( mockRootObject = jasmine.createSpyObj(
"domainObject", "rootObjectContainer",
["getId", "getCapability", "getModel", "useCapability"] ["getId", "getCapability", "getModel", "useCapability", "hasCapability"]
); );
mockDomainObject = jasmine.createSpyObj( mockDefaultRootObject = jasmine.createSpyObj(
"domainObject", "defaultRootObject",
["getId", "getCapability", "getModel", "useCapability"] ["getId", "getCapability", "getModel", "useCapability", "hasCapability"]
);
mockOtherDomainObject = jasmine.createSpyObj(
"otherDomainObject",
["getId", "getCapability", "getModel", "useCapability", "hasCapability"]
); );
mockNextObject = jasmine.createSpyObj( mockNextObject = jasmine.createSpyObj(
"nextObject", "nestedDomainObject",
["getId", "getCapability", "getModel", "useCapability"] ["getId", "getCapability", "getModel", "useCapability", "hasCapability"]
); );
mockObjectService.getObjects.andReturn(Promise.resolve({
mockObjectService.getObjects.andReturn(mockPromise({
ROOT: mockRootObject ROOT: mockRootObject
})); }));
mockRootObject.useCapability.andReturn(mockPromise([ mockRootObject.useCapability.andReturn(Promise.resolve([
mockDomainObject mockOtherDomainObject,
mockDefaultRootObject
])); ]));
mockDomainObject.useCapability.andReturn(mockPromise([ mockRootObject.hasCapability.andReturn(true);
mockDefaultRootObject.useCapability.andReturn(Promise.resolve([
mockNextObject mockNextObject
])); ]));
mockDefaultRootObject.hasCapability.andReturn(true);
mockOtherDomainObject.hasCapability.andReturn(false);
mockNextObject.useCapability.andReturn(undefined); mockNextObject.useCapability.andReturn(undefined);
mockNextObject.hasCapability.andReturn(false);
mockNextObject.getId.andReturn("next"); mockNextObject.getId.andReturn("next");
mockDomainObject.getId.andReturn(testDefaultRoot); mockDefaultRootObject.getId.andReturn(testDefaultRoot);
mockActionCapability = jasmine.createSpyObj('actionCapability', ['perform']);
instantiateController(); instantiateController();
waitsForNavigation();
}); });
it("uses composition to set the navigated object, if there is none", function () { it("uses composition to set the navigated object, if there is none", function () {
instantiateController(); instantiateController();
waitsForNavigation();
runs(function () {
expect(mockNavigationService.setNavigation) expect(mockNavigationService.setNavigation)
.toHaveBeenCalledWith(mockDomainObject); .toHaveBeenCalledWith(mockDefaultRootObject);
});
}); });
it("navigates to a root-level object, even when default path is not found", function () { it("navigates to a root-level object, even when default path is not found", function () {
mockDomainObject.getId mockDefaultRootObject.getId
.andReturn("something-other-than-the-" + testDefaultRoot); .andReturn("something-other-than-the-" + testDefaultRoot);
instantiateController(); instantiateController();
waitsForNavigation();
runs(function () {
expect(mockNavigationService.setNavigation) expect(mockNavigationService.setNavigation)
.toHaveBeenCalledWith(mockDomainObject); .toHaveBeenCalledWith(mockDefaultRootObject);
}); });
});
//
it("does not try to override navigation", function () { it("does not try to override navigation", function () {
mockNavigationService.getNavigation.andReturn(mockDomainObject); mockNavigationService.getNavigation.andReturn(mockDefaultRootObject);
instantiateController(); instantiateController();
expect(mockScope.navigatedObject).toBe(mockDomainObject); waitsForNavigation();
expect(mockScope.navigatedObject).toBe(mockDefaultRootObject);
}); });
//
it("updates scope when navigated object changes", function () { it("updates scope when navigated object changes", function () {
// Should have registered a listener - call it // Should have registered a listener - call it
mockNavigationService.addListener.mostRecentCall.args[0]( mockNavigationService.addListener.mostRecentCall.args[0](
mockDomainObject mockOtherDomainObject
); );
expect(mockScope.navigatedObject).toEqual(mockDomainObject); expect(mockScope.navigatedObject).toEqual(mockOtherDomainObject);
}); });
@ -166,10 +193,13 @@ define(
it("uses route parameters to choose initially-navigated object", function () { it("uses route parameters to choose initially-navigated object", function () {
mockRoute.current.params.ids = testDefaultRoot + "/next"; mockRoute.current.params.ids = testDefaultRoot + "/next";
instantiateController(); instantiateController();
waitsForNavigation();
runs(function () {
expect(mockScope.navigatedObject).toBe(mockNextObject); expect(mockScope.navigatedObject).toBe(mockNextObject);
expect(mockNavigationService.setNavigation) expect(mockNavigationService.setNavigation)
.toHaveBeenCalledWith(mockNextObject); .toHaveBeenCalledWith(mockNextObject);
}); });
});
it("handles invalid IDs by going as far as possible", function () { it("handles invalid IDs by going as far as possible", function () {
// Idea here is that if we get a bad path of IDs, // Idea here is that if we get a bad path of IDs,
@ -177,9 +207,13 @@ define(
// it hits an invalid ID. // it hits an invalid ID.
mockRoute.current.params.ids = testDefaultRoot + "/junk"; mockRoute.current.params.ids = testDefaultRoot + "/junk";
instantiateController(); instantiateController();
expect(mockScope.navigatedObject).toBe(mockDomainObject); waitsForNavigation();
runs(function () {
expect(mockScope.navigatedObject).toBe(mockDefaultRootObject);
expect(mockNavigationService.setNavigation) expect(mockNavigationService.setNavigation)
.toHaveBeenCalledWith(mockDomainObject); .toHaveBeenCalledWith(mockDefaultRootObject);
});
}); });
it("handles compositionless objects by going as far as possible", function () { it("handles compositionless objects by going as far as possible", function () {
@ -188,84 +222,33 @@ define(
// should stop at it since remaining IDs cannot be loaded. // should stop at it since remaining IDs cannot be loaded.
mockRoute.current.params.ids = testDefaultRoot + "/next/junk"; mockRoute.current.params.ids = testDefaultRoot + "/next/junk";
instantiateController(); instantiateController();
waitsForNavigation();
runs(function () {
expect(mockScope.navigatedObject).toBe(mockNextObject); expect(mockScope.navigatedObject).toBe(mockNextObject);
expect(mockNavigationService.setNavigation) expect(mockNavigationService.setNavigation)
.toHaveBeenCalledWith(mockNextObject); .toHaveBeenCalledWith(mockNextObject);
}); });
});
it("updates the displayed route to reflect current navigation", function () { it("updates the displayed route to reflect current navigation", function () {
var mockContext = jasmine.createSpyObj('context', ['getPath']), // In order to trigger a route update and not a route change,
mockUnlisten = jasmine.createSpy('unlisten'), // the current route must be updated before location.path is
mockMode = "browse"; // called.
expect(mockRoute.current.pathParams.ids)
mockContext.getPath.andReturn( .not
[mockRootObject, mockDomainObject, mockNextObject] .toBe(testDefaultRoot + '/next');
); mockLocation.path.andCallFake(function () {
expect(mockRoute.current.pathParams.ids)
//Return true from navigate action .toBe(testDefaultRoot + '/next');
mockActionCapability.perform.andReturn(mockPromise(true));
mockNextObject.getCapability.andCallFake(function (c) {
return (c === 'context' && mockContext) ||
(c === 'action' && mockActionCapability);
}); });
mockScope.$on.andReturn(mockUnlisten);
// Provide a navigation change
mockNavigationService.addListener.mostRecentCall.args[0]( mockNavigationService.addListener.mostRecentCall.args[0](
mockNextObject mockNextObject
); );
// Allows the path index to be checked
// prior to setting $route.current
mockLocation.path.andReturn("/browse/");
mockNavigationService.setNavigation.andReturn(true);
mockActionCapability.perform.andReturn(mockPromise(true));
// Exercise the Angular workaround
mockNavigationService.addListener.mostRecentCall.args[0]();
mockScope.$on.mostRecentCall.args[1]();
expect(mockUnlisten).toHaveBeenCalled();
// location.path to be called with the urlService's
// urlFor function with the next domainObject and mode
expect(mockLocation.path).toHaveBeenCalledWith( expect(mockLocation.path).toHaveBeenCalledWith(
mockUrlService.urlForLocation(mockMode, mockNextObject) '/browse/' + testDefaultRoot + '/next'
); );
}); });
it("after successful navigation event sets the selected tree " +
"object", function () {
mockScope.navigatedObject = mockDomainObject;
mockNavigationService.setNavigation.andReturn(true);
mockActionCapability.perform.andReturn(mockPromise(true));
mockNextObject.getCapability.andReturn(mockActionCapability);
//Simulate a change in selected tree object
mockScope.treeModel = {selectedObject: mockDomainObject};
mockScope.$watch.mostRecentCall.args[1](mockNextObject);
expect(mockScope.treeModel.selectedObject).toBe(mockNextObject);
expect(mockScope.treeModel.selectedObject).not.toBe(mockDomainObject);
});
it("after failed navigation event resets the selected tree" +
" object", function () {
mockScope.navigatedObject = mockDomainObject;
//Return false from navigation action
mockActionCapability.perform.andReturn(mockPromise(false));
mockNextObject.getCapability.andReturn(mockActionCapability);
//Simulate a change in selected tree object
mockScope.treeModel = {selectedObject: mockDomainObject};
mockScope.$watch.mostRecentCall.args[1](mockNextObject);
expect(mockScope.treeModel.selectedObject).not.toBe(mockNextObject);
expect(mockScope.treeModel.selectedObject).toBe(mockDomainObject);
});
}); });
} }
); );

View File

@ -29,7 +29,6 @@ define(
var mockScope, var mockScope,
mockLocation, mockLocation,
mockRoute, mockRoute,
mockUnlisten,
controller; controller;
// Utility function; look for a $watch on scope and fire it // Utility function; look for a $watch on scope and fire it
@ -51,9 +50,7 @@ define(
"$location", "$location",
["path", "search"] ["path", "search"]
); );
mockUnlisten = jasmine.createSpy("unlisten"); mockLocation.search.andReturn({});
mockScope.$on.andReturn(mockUnlisten);
controller = new BrowseObjectController( controller = new BrowseObjectController(
mockScope, mockScope,
@ -69,10 +66,6 @@ define(
// Allows the path index to be checked // Allows the path index to be checked
// prior to setting $route.current // prior to setting $route.current
mockLocation.path.andReturn("/browse/"); mockLocation.path.andReturn("/browse/");
// Exercise the Angular workaround
mockScope.$on.mostRecentCall.args[1]();
expect(mockUnlisten).toHaveBeenCalled();
}); });
it("sets the active view from query parameters", function () { it("sets the active view from query parameters", function () {

View File

@ -68,7 +68,13 @@ define(
this.instantiateFn = this.instantiateFn || this.instantiateFn = this.instantiateFn ||
this.$injector.get("instantiate"); this.$injector.get("instantiate");
return this.instantiateFn(model, id); var newObject = this.instantiateFn(model, id);
this.contextualizeFn = this.contextualizeFn ||
this.$injector.get("contextualize");
return this.contextualizeFn(newObject, this.domainObject);
}; };
/** /**

View File

@ -28,6 +28,7 @@ define(
var mockInjector, var mockInjector,
mockIdentifierService, mockIdentifierService,
mockInstantiate, mockInstantiate,
mockContextualize,
mockIdentifier, mockIdentifier,
mockNow, mockNow,
mockDomainObject, mockDomainObject,
@ -36,6 +37,7 @@ define(
beforeEach(function () { beforeEach(function () {
mockInjector = jasmine.createSpyObj("$injector", ["get"]); mockInjector = jasmine.createSpyObj("$injector", ["get"]);
mockInstantiate = jasmine.createSpy("instantiate"); mockInstantiate = jasmine.createSpy("instantiate");
mockContextualize = jasmine.createSpy("contextualize");
mockIdentifierService = jasmine.createSpyObj( mockIdentifierService = jasmine.createSpyObj(
'identifierService', 'identifierService',
['parse', 'generate'] ['parse', 'generate']
@ -50,8 +52,10 @@ define(
); );
mockInjector.get.andCallFake(function (key) { mockInjector.get.andCallFake(function (key) {
return key === 'instantiate' ? return {
mockInstantiate : undefined; 'instantiate': mockInstantiate,
'contextualize': mockContextualize
}[key];
}); });
mockIdentifierService.parse.andReturn(mockIdentifier); mockIdentifierService.parse.andReturn(mockIdentifier);
mockIdentifierService.generate.andReturn("some-id"); mockIdentifierService.generate.andReturn("some-id");
@ -72,7 +76,7 @@ define(
expect(instantiation.invoke).toBe(instantiation.instantiate); expect(instantiation.invoke).toBe(instantiation.instantiate);
}); });
it("uses the instantiate service to create domain objects", function () { it("uses instantiate and contextualize to create domain objects", function () {
var mockDomainObj = jasmine.createSpyObj('domainObject', [ var mockDomainObj = jasmine.createSpyObj('domainObject', [
'getId', 'getId',
'getModel', 'getModel',
@ -81,6 +85,9 @@ define(
'hasCapability' 'hasCapability'
]), testModel = { someKey: "some value" }; ]), testModel = { someKey: "some value" };
mockInstantiate.andReturn(mockDomainObj); mockInstantiate.andReturn(mockDomainObj);
mockContextualize.andCallFake(function (x) {
return x;
});
expect(instantiation.instantiate(testModel)) expect(instantiation.instantiate(testModel))
.toBe(mockDomainObj); .toBe(mockDomainObj);
expect(mockInstantiate) expect(mockInstantiate)
@ -88,6 +95,8 @@ define(
someKey: "some value", someKey: "some value",
modified: mockNow() modified: mockNow()
}, jasmine.any(String)); }, jasmine.any(String));
expect(mockContextualize)
.toHaveBeenCalledWith(mockDomainObj, mockDomainObject);
}); });
}); });

View File

@ -96,6 +96,19 @@ define(
this.conductor.on('timeSystem', this.changeTimeSystem); this.conductor.on('timeSystem', this.changeTimeSystem);
} }
/**
* Used as a url search param setter in place of $location.search(...)
*
* Invokes $location.search(...) but prevents an Angular route
* change from occurring as a consequence which will cause
* controllers to reload and strangeness to ensue.
*
* @private
*/
TimeConductorController.prototype.setParam = function (name, value) {
this.$location.search(name, value);
};
/** /**
* @private * @private
*/ */
@ -185,8 +198,8 @@ define(
this.setFormFromBounds(bounds); this.setFormFromBounds(bounds);
if (this.conductorViewService.mode() === 'fixed') { if (this.conductorViewService.mode() === 'fixed') {
//Set bounds in URL on change //Set bounds in URL on change
this.$location.search(SEARCH.START_BOUND, bounds.start); this.setParam(SEARCH.START_BOUND, bounds.start);
this.$location.search(SEARCH.END_BOUND, bounds.end); this.setParam(SEARCH.END_BOUND, bounds.end);
} }
} }
}; };
@ -299,8 +312,8 @@ define(
this.conductorViewService.deltas(deltas); this.conductorViewService.deltas(deltas);
//Set Deltas in URL on change //Set Deltas in URL on change
this.$location.search(SEARCH.START_DELTA, deltas.start); this.setParam(SEARCH.START_DELTA, deltas.start);
this.$location.search(SEARCH.END_DELTA, deltas.end); this.setParam(SEARCH.END_DELTA, deltas.end);
} }
}; };
@ -315,23 +328,23 @@ define(
*/ */
TimeConductorController.prototype.setMode = function (newModeKey, oldModeKey) { TimeConductorController.prototype.setMode = function (newModeKey, oldModeKey) {
//Set mode in URL on change //Set mode in URL on change
this.$location.search(SEARCH.MODE, newModeKey); this.setParam(SEARCH.MODE, newModeKey);
if (newModeKey !== oldModeKey) { if (newModeKey !== oldModeKey) {
this.conductorViewService.mode(newModeKey); this.conductorViewService.mode(newModeKey);
this.setFormFromMode(newModeKey); this.setFormFromMode(newModeKey);
if (newModeKey === "fixed") { if (newModeKey === "fixed") {
this.$location.search(SEARCH.START_DELTA, null); this.setParam(SEARCH.START_DELTA, undefined);
this.$location.search(SEARCH.END_DELTA, null); this.setParam(SEARCH.END_DELTA, undefined);
} else { } else {
this.$location.search(SEARCH.START_BOUND, null); this.setParam(SEARCH.START_BOUND, undefined);
this.$location.search(SEARCH.END_BOUND, null); this.setParam(SEARCH.END_BOUND, undefined);
var deltas = this.conductorViewService.deltas(); var deltas = this.conductorViewService.deltas();
if (deltas) { if (deltas) {
this.$location.search(SEARCH.START_DELTA, deltas.start); this.setParam(SEARCH.START_DELTA, deltas.start);
this.$location.search(SEARCH.END_DELTA, deltas.end); this.setParam(SEARCH.END_DELTA, deltas.end);
} }
} }
} }
@ -363,7 +376,7 @@ define(
*/ */
TimeConductorController.prototype.changeTimeSystem = function (newTimeSystem) { TimeConductorController.prototype.changeTimeSystem = function (newTimeSystem) {
//Set time system in URL on change //Set time system in URL on change
this.$location.search(SEARCH.TIME_SYSTEM, newTimeSystem.metadata.key); this.setParam(SEARCH.TIME_SYSTEM, newTimeSystem.metadata.key);
if (newTimeSystem && (newTimeSystem !== this.$scope.timeSystemModel.selected)) { if (newTimeSystem && (newTimeSystem !== this.$scope.timeSystemModel.selected)) {
this.setFormFromTimeSystem(newTimeSystem); this.setFormFromTimeSystem(newTimeSystem);

View File

@ -37,6 +37,7 @@ define(['./TimeConductorController'], function (TimeConductorController) {
"$watch", "$watch",
"$on" "$on"
]); ]);
mockWindow = jasmine.createSpyObj("$window", ["requestAnimationFrame"]); mockWindow = jasmine.createSpyObj("$window", ["requestAnimationFrame"]);
mockTimeConductor = jasmine.createSpyObj( mockTimeConductor = jasmine.createSpyObj(
"TimeConductor", "TimeConductor",
@ -258,9 +259,7 @@ define(['./TimeConductorController'], function (TimeConductorController) {
return mockTimeSystem; return mockTimeSystem;
}; };
}); });
});
it("sets the mode on scope", function () {
controller = new TimeConductorController( controller = new TimeConductorController(
mockScope, mockScope,
mockWindow, mockWindow,
@ -270,7 +269,9 @@ define(['./TimeConductorController'], function (TimeConductorController) {
mockTimeSystemConstructors, mockTimeSystemConstructors,
mockFormatService mockFormatService
); );
});
it("sets the mode on scope", function () {
mockConductorViewService.availableTimeSystems.andReturn(mockTimeSystems); mockConductorViewService.availableTimeSystems.andReturn(mockTimeSystems);
controller.setMode(mode); controller.setMode(mode);
@ -278,16 +279,6 @@ define(['./TimeConductorController'], function (TimeConductorController) {
}); });
it("sets available time systems on scope when mode changes", function () { it("sets available time systems on scope when mode changes", function () {
controller = new TimeConductorController(
mockScope,
mockWindow,
mockLocation,
{conductor: mockTimeConductor},
mockConductorViewService,
mockTimeSystemConstructors,
mockFormatService
);
mockConductorViewService.availableTimeSystems.andReturn(mockTimeSystems); mockConductorViewService.availableTimeSystems.andReturn(mockTimeSystems);
controller.setMode(mode); controller.setMode(mode);
@ -303,16 +294,6 @@ define(['./TimeConductorController'], function (TimeConductorController) {
end: 10 end: 10
}; };
controller = new TimeConductorController(
mockScope,
mockWindow,
mockLocation,
{conductor: mockTimeConductor},
mockConductorViewService,
mockTimeSystemConstructors,
mockFormatService
);
controller.setBounds(formModel); controller.setBounds(formModel);
expect(mockTimeConductor.bounds).toHaveBeenCalledWith(formModel); expect(mockTimeConductor.bounds).toHaveBeenCalledWith(formModel);
}); });
@ -327,16 +308,6 @@ define(['./TimeConductorController'], function (TimeConductorController) {
endDelta: deltas.end endDelta: deltas.end
}; };
controller = new TimeConductorController(
mockScope,
mockWindow,
mockLocation,
{conductor: mockTimeConductor},
mockConductorViewService,
mockTimeSystemConstructors,
mockFormatService
);
controller.setDeltas(formModel); controller.setDeltas(formModel);
expect(mockConductorViewService.deltas).toHaveBeenCalledWith(deltas); expect(mockConductorViewService.deltas).toHaveBeenCalledWith(deltas);
}); });
@ -357,22 +328,7 @@ define(['./TimeConductorController'], function (TimeConductorController) {
} }
}; };
mockTimeSystems = [ controller.timeSystems = [timeSystem];
// Wrap as constructor function
function () {
return timeSystem;
}
];
controller = new TimeConductorController(
mockScope,
mockWindow,
mockLocation,
{conductor: mockTimeConductor},
mockConductorViewService,
mockTimeSystems,
mockFormatService
);
controller.selectTimeSystemByKey('testTimeSystem'); controller.selectTimeSystemByKey('testTimeSystem');
expect(mockTimeConductor.timeSystem).toHaveBeenCalledWith(timeSystem, defaultBounds); expect(mockTimeConductor.timeSystem).toHaveBeenCalledWith(timeSystem, defaultBounds);