From 2b1fdc2204627a4927ce4c3dfc24cb6e476066d3 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Mon, 24 Nov 2014 15:10:29 -0800 Subject: [PATCH] [Common UI] Add specs for controllers Add specs for controllers in the commonUI/general bundle. WTD-574. --- .../general/res/templates/tree-item.html | 5 +- .../general/src/ActionGroupController.js | 4 +- .../general/src/TreeNodeController.js | 24 +-- .../general/test/ActionGroupControllerSpec.js | 52 +++++- .../general/test/ClickAwayControllerSpec.js | 64 +++++++- .../general/test/ContextMenuControllerSpec.js | 32 +++- .../general/test/ToggleControllerSpec.js | 32 ++++ .../general/test/TreeNodeControllerSpec.js | 151 ++++++++++++++++++ 8 files changed, 344 insertions(+), 20 deletions(-) diff --git a/platform/commonUI/general/res/templates/tree-item.html b/platform/commonUI/general/res/templates/tree-item.html index 7ea6ff9dba..bde748408f 100644 --- a/platform/commonUI/general/res/templates/tree-item.html +++ b/platform/commonUI/general/res/templates/tree-item.html @@ -2,7 +2,7 @@ {{toggle.isActive() ? "v" : ">"}} @@ -17,10 +17,9 @@ ng-show="toggle.isActive()" ng-if="model.composition !== undefined"> - ID: {{treeNode.getNodeObject().getId()}}? + mct-object="treeNode.hasBeenExpanded() && domainObject"> diff --git a/platform/commonUI/general/src/ActionGroupController.js b/platform/commonUI/general/src/ActionGroupController.js index 5f1c2d3ea1..339e10dd97 100644 --- a/platform/commonUI/general/src/ActionGroupController.js +++ b/platform/commonUI/general/src/ActionGroupController.js @@ -28,10 +28,10 @@ define( } } - actions.forEach(assignToGroup); + (actions || []).forEach(assignToGroup); $scope.ungrouped = ungrouped; - $scope.groups = Object.keys(groups).map(function (k) { + $scope.groups = Object.keys(groups).sort().map(function (k) { return groups[k]; }); } diff --git a/platform/commonUI/general/src/TreeNodeController.js b/platform/commonUI/general/src/TreeNodeController.js index 152c0f154b..0f4d3a370e 100644 --- a/platform/commonUI/general/src/TreeNodeController.js +++ b/platform/commonUI/general/src/TreeNodeController.js @@ -15,10 +15,11 @@ define( function TreeNodeController($scope, navigationService) { var navigatedObject = navigationService.getNavigation(), isNavigated = false, - expandedObject; + hasBeenExpanded = false; function idsEqual(objA, objB) { - return objA && objB && (objA.getId() === objB.getId()); + return (objA === objB) || + (objA && objB && (objA.getId() === objB.getId())); } function parentOf(domainObject) { @@ -35,8 +36,8 @@ define( // index, ending at the end of the node path. function checkPath(nodePath, navPath, index) { index = index || 0; - return index > nodePath.length || - (navPath[index] === nodePath[index] && + return (index >= nodePath.length) || + (idsEqual(navPath[index], nodePath[index]) && checkPath(nodePath, navPath, index + 1)); } @@ -66,10 +67,9 @@ define( // Expand if necessary if (isOnNavigationPath(nodeObject, navigatedObject) && - $scope.toggle !== undefined && - $scope.toggle.isActive()) { - $scope.toggle.toggle(); - expandedObject = nodeObject; + $scope.toggle !== undefined) { + $scope.toggle.setState(true); + hasBeenExpanded = true; } } @@ -85,11 +85,11 @@ define( $scope.$watch("domainObject", checkNavigation); return { - setNodeObject: function (domainObject) { - expandedObject = domainObject; + trackExpansion: function () { + hasBeenExpanded = true; }, - getNodeObject: function () { - return expandedObject; + hasBeenExpanded: function () { + return hasBeenExpanded; }, isNavigated: function () { return isNavigated; diff --git a/platform/commonUI/general/test/ActionGroupControllerSpec.js b/platform/commonUI/general/test/ActionGroupControllerSpec.js index 5cb6ac6cdf..f206f200ba 100644 --- a/platform/commonUI/general/test/ActionGroupControllerSpec.js +++ b/platform/commonUI/general/test/ActionGroupControllerSpec.js @@ -5,14 +5,64 @@ define( function (ActionGroupController) { "use strict"; - describe("The domain object provider", function () { + describe("The action group controller", function () { var mockScope, + mockActions, controller; + function mockAction(metadata, index) { + var action = jasmine.createSpyObj( + "action" + index, + ["perform", "getMetadata"] + ); + action.getMetadata.andReturn(metadata); + return action; + } + beforeEach(function () { + mockActions = jasmine.createSpyObj("action", ["getActions"]); mockScope = jasmine.createSpyObj("$scope", ["$watch"]); controller = new ActionGroupController(mockScope); }); + + it("watches scope that may change applicable actions", function () { + // The action capability + expect(mockScope.$watch).toHaveBeenCalledWith( + "action", + jasmine.any(Function) + ); + // The category of action to load + expect(mockScope.$watch).toHaveBeenCalledWith( + "parameters.category", + jasmine.any(Function) + ); + }); + + it("populates the scope with grouped and ungrouped actions", function () { + mockScope.action = mockActions; + mockScope.parameters = { category: "test" }; + + mockActions.getActions.andReturn([ + { group: "a", someKey: 0 }, + { group: "a", someKey: 1 }, + { group: "b", someKey: 2 }, + { group: "a", someKey: 3 }, + { group: "b", someKey: 4 }, + { someKey: 5 }, + { someKey: 6 }, + { group: "a", someKey: 7 }, + { someKey: 8 } + ].map(mockAction)); + + // Call the watch + mockScope.$watch.mostRecentCall.args[1](); + + // Should have grouped and ungrouped actions in scope now + expect(mockScope.groups.length).toEqual(2); + expect(mockScope.groups[0].length).toEqual(4); // a + expect(mockScope.groups[1].length).toEqual(2); // b + expect(mockScope.ungrouped.length).toEqual(3); // ungrouped + }); }); } ); \ No newline at end of file diff --git a/platform/commonUI/general/test/ClickAwayControllerSpec.js b/platform/commonUI/general/test/ClickAwayControllerSpec.js index 1dec8c17d6..57d81ce59b 100644 --- a/platform/commonUI/general/test/ClickAwayControllerSpec.js +++ b/platform/commonUI/general/test/ClickAwayControllerSpec.js @@ -5,7 +5,69 @@ define( function (ClickAwayController) { "use strict"; - describe("The domain object provider", function () { + describe("The click-away controller", function () { + var mockScope, + mockDocument, + controller; + + beforeEach(function () { + mockScope = jasmine.createSpyObj( + "$scope", + [ "$apply" ] + ); + mockDocument = jasmine.createSpyObj( + "$document", + [ "on", "off" ] + ); + controller = new ClickAwayController(mockScope, mockDocument); + }); + + it("is initially inactive", function () { + expect(controller.isActive()).toBe(false); + }); + + it("does not listen to the document before being toggled", function () { + expect(mockDocument.on).not.toHaveBeenCalled(); + }); + + it("tracks enabled/disabled state when toggled", function () { + controller.toggle(); + expect(controller.isActive()).toBe(true); + controller.toggle(); + expect(controller.isActive()).toBe(false); + controller.toggle(); + expect(controller.isActive()).toBe(true); + controller.toggle(); + expect(controller.isActive()).toBe(false); + }); + + it("allows active state to be explictly specified", function () { + controller.setState(true); + expect(controller.isActive()).toBe(true); + controller.setState(true); + expect(controller.isActive()).toBe(true); + controller.setState(false); + expect(controller.isActive()).toBe(false); + controller.setState(false); + expect(controller.isActive()).toBe(false); + }); + + it("registers a mouse listener when activated", function () { + controller.setState(true); + expect(mockDocument.on).toHaveBeenCalled(); + }); + + it("deactivates and detaches listener on document click", function () { + var callback; + controller.setState(true); + callback = mockDocument.on.mostRecentCall.args[1]; + callback(); + expect(controller.isActive()).toEqual(false); + expect(mockDocument.off).toHaveBeenCalledWith("mouseup", callback); + }); + + + }); } ); \ No newline at end of file diff --git a/platform/commonUI/general/test/ContextMenuControllerSpec.js b/platform/commonUI/general/test/ContextMenuControllerSpec.js index ce4c6567d4..bb6aa63f86 100644 --- a/platform/commonUI/general/test/ContextMenuControllerSpec.js +++ b/platform/commonUI/general/test/ContextMenuControllerSpec.js @@ -5,7 +5,37 @@ define( function (ContextMenuController) { "use strict"; - describe("The domain object provider", function () { + describe("The context menu controller", function () { + var mockScope, + mockActions, + controller; + + beforeEach(function () { + mockActions = jasmine.createSpyObj("action", ["getActions"]); + mockScope = jasmine.createSpyObj("$scope", ["$watch"]); + controller = new ContextMenuController(mockScope); + }); + + it("watches scope that may change applicable actions", function () { + // The action capability + expect(mockScope.$watch).toHaveBeenCalledWith( + "action", + jasmine.any(Function) + ); + }); + + it("populates the scope with grouped and ungrouped actions", function () { + mockScope.action = mockActions; + mockScope.parameters = { category: "test" }; + + mockActions.getActions.andReturn(["a", "b", "c"]); + + // Call the watch + mockScope.$watch.mostRecentCall.args[1](); + + // Should have grouped and ungrouped actions in scope now + expect(mockScope.menuActions.length).toEqual(3); + }); }); } ); \ No newline at end of file diff --git a/platform/commonUI/general/test/ToggleControllerSpec.js b/platform/commonUI/general/test/ToggleControllerSpec.js index 6049a38b18..f2059d4c25 100644 --- a/platform/commonUI/general/test/ToggleControllerSpec.js +++ b/platform/commonUI/general/test/ToggleControllerSpec.js @@ -6,6 +6,38 @@ define( "use strict"; describe("The toggle controller", function () { + var controller; + + beforeEach(function () { + controller = new ToggleController(); + }); + + it("is initially inactive", function () { + expect(controller.isActive()).toBe(false); + }); + + it("tracks enabled/disabled state when toggled", function () { + controller.toggle(); + expect(controller.isActive()).toBe(true); + controller.toggle(); + expect(controller.isActive()).toBe(false); + controller.toggle(); + expect(controller.isActive()).toBe(true); + controller.toggle(); + expect(controller.isActive()).toBe(false); + }); + + it("allows active state to be explictly specified", function () { + controller.setState(true); + expect(controller.isActive()).toBe(true); + controller.setState(true); + expect(controller.isActive()).toBe(true); + controller.setState(false); + expect(controller.isActive()).toBe(false); + controller.setState(false); + expect(controller.isActive()).toBe(false); + }); + }); } ); \ No newline at end of file diff --git a/platform/commonUI/general/test/TreeNodeControllerSpec.js b/platform/commonUI/general/test/TreeNodeControllerSpec.js index fc92708529..1db66dd82a 100644 --- a/platform/commonUI/general/test/TreeNodeControllerSpec.js +++ b/platform/commonUI/general/test/TreeNodeControllerSpec.js @@ -6,6 +6,157 @@ define( "use strict"; describe("The tree node controller", function () { + var mockScope, + mockNavigationService, + controller; + + function TestObject(id, context) { + return { + getId: function () { return id; }, + getCapability: function (key) { + return key === 'context' ? context : undefined; + } + }; + } + + beforeEach(function () { + mockScope = jasmine.createSpyObj( + "$scope", + [ "$watch", "$on" ] + ); + mockNavigationService = jasmine.createSpyObj( + "navigationService", + [ + "getNavigation", + "setNavigation", + "addListener", + "removeListener" + ] + ); + controller = new TreeNodeController( + mockScope, + mockNavigationService + ); + }); + + it("listens for navigation changes", function () { + expect(mockNavigationService.addListener) + .toHaveBeenCalledWith(jasmine.any(Function)); + }); + + it("allows tracking of expansion state", function () { + // The tree node tracks whether or not it has ever + // been expanded in order to lazily load the expanded + // portion of the tree. + expect(controller.hasBeenExpanded()).toBeFalsy(); + controller.trackExpansion(); + expect(controller.hasBeenExpanded()).toBeTruthy(); + controller.trackExpansion(); + expect(controller.hasBeenExpanded()).toBeTruthy(); + }); + + it("tracks whether or not the represented object is currently navigated-to", function () { + // This is needed to highlight the current selection + var mockContext = jasmine.createSpyObj( + "context", + [ "getParent", "getPath", "getRoot" ] + ), + obj = new TestObject("test-object", mockContext); + + mockContext.getPath.andReturn([obj]); + + // Verify precondition + expect(controller.isNavigated()).toBeFalsy(); + + mockNavigationService.getNavigation.andReturn(obj); + mockScope.domainObject = obj; + mockNavigationService.addListener.mostRecentCall.args[0](obj); + + expect(controller.isNavigated()).toBeTruthy(); + }); + + it("expands a node if it is on the navigation path", function () { + var mockParentContext = jasmine.createSpyObj( + "parentContext", + [ "getParent", "getPath", "getRoot" ] + ), + mockChildContext = jasmine.createSpyObj( + "childContext", + [ "getParent", "getPath", "getRoot" ] + ), + parent = new TestObject("parent", mockParentContext), + child = new TestObject("child", mockChildContext); + + mockChildContext.getParent.andReturn(parent); + mockChildContext.getPath.andReturn([parent, child]); + mockParentContext.getPath.andReturn([parent]); + + // Set up such that we are on, but not at the end of, a path + mockNavigationService.getNavigation.andReturn(child); + mockScope.domainObject = parent; + mockScope.toggle = jasmine.createSpyObj("toggle", ["setState"]); + + // Trigger update + mockNavigationService.addListener.mostRecentCall.args[0](child); + + expect(mockScope.toggle.setState).toHaveBeenCalledWith(true); + expect(controller.hasBeenExpanded()).toBeTruthy(); + expect(controller.isNavigated()).toBeFalsy(); + + }); + + it("does not expand a node if no context is available", function () { + var mockParentContext = jasmine.createSpyObj( + "parentContext", + [ "getParent", "getPath", "getRoot" ] + ), + mockChildContext = jasmine.createSpyObj( + "childContext", + [ "getParent", "getPath", "getRoot" ] + ), + parent = new TestObject("parent", mockParentContext), + child = new TestObject("child", undefined); + + mockChildContext.getParent.andReturn(parent); + mockChildContext.getPath.andReturn([parent, child]); + mockParentContext.getPath.andReturn([parent]); + + // Set up such that we are on, but not at the end of, a path + mockNavigationService.getNavigation.andReturn(child); + mockScope.domainObject = parent; + mockScope.toggle = jasmine.createSpyObj("toggle", ["setState"]); + + // Trigger update + mockNavigationService.addListener.mostRecentCall.args[0](child); + + expect(mockScope.toggle.setState).not.toHaveBeenCalled(); + expect(controller.hasBeenExpanded()).toBeFalsy(); + expect(controller.isNavigated()).toBeFalsy(); + + }); + + it("removes its navigation listener when the scope is destroyed", function () { + var navCallback = + mockNavigationService.addListener.mostRecentCall.args[0]; + + // Make sure the controller is listening in the first place + expect(mockScope.$on).toHaveBeenCalledWith( + "$destroy", + jasmine.any(Function) + ); + + // Verify precondition - no removeListener called + expect(mockNavigationService.removeListener) + .not.toHaveBeenCalled(); + + // Call that listener (act as if scope is being destroyed) + mockScope.$on.mostRecentCall.args[1](); + + // Verify precondition - no removeListener called + expect(mockNavigationService.removeListener) + .toHaveBeenCalledWith(navCallback); + }); + }); }