diff --git a/bundles.json b/bundles.json index 666e520854..de005c5961 100644 --- a/bundles.json +++ b/bundles.json @@ -16,6 +16,7 @@ "platform/forms", "platform/persistence/queue", "platform/policy", + "platform/entanglement", "example/persistence", "example/generator" diff --git a/platform/commonUI/browse/src/creation/CreationService.js b/platform/commonUI/browse/src/creation/CreationService.js index 4d60e6dc9f..015573c8bd 100644 --- a/platform/commonUI/browse/src/creation/CreationService.js +++ b/platform/commonUI/browse/src/creation/CreationService.js @@ -77,7 +77,19 @@ define( return undefined; } - return parentPersistence.persist(); + return parentPersistence.persist().then(function () { + // Locate and return new Object in context of parent. + return parent + .useCapability('composition') + .then(function (children) { + var i; + for (i = 0; i < children.length; i += 1) { + if (children[i].getId() === id) { + return children[i]; + } + } + }); + }); }); } @@ -126,4 +138,4 @@ define( return CreationService; } -); \ No newline at end of file +); diff --git a/platform/commonUI/browse/test/creation/CreationServiceSpec.js b/platform/commonUI/browse/test/creation/CreationServiceSpec.js index 3280350d22..277b195eaf 100644 --- a/platform/commonUI/browse/test/creation/CreationServiceSpec.js +++ b/platform/commonUI/browse/test/creation/CreationServiceSpec.js @@ -34,8 +34,10 @@ define( mockQ, mockLog, mockParentObject, + mockNewObject, mockMutationCapability, mockPersistenceCapability, + mockCompositionCapability, mockCapabilities, creationService; @@ -69,6 +71,10 @@ define( "parentObject", [ "getId", "getCapability", "useCapability" ] ); + mockNewObject = jasmine.createSpyObj( + "newObject", + [ "getId" ] + ); mockMutationCapability = jasmine.createSpyObj( "mutation", [ "invoke" ] @@ -77,9 +83,14 @@ define( "persistence", [ "persist", "getSpace" ] ); + mockCompositionCapability = jasmine.createSpyObj( + "composition", + ["invoke"] + ); mockCapabilities = { mutation: mockMutationCapability, - persistence: mockPersistenceCapability + persistence: mockPersistenceCapability, + composition: mockCompositionCapability }; mockPersistenceService.createObject.andReturn( @@ -93,8 +104,15 @@ define( return mockCapabilities[key].invoke(value); }); + mockPersistenceCapability.persist.andReturn( + mockPromise(true) + ); + mockMutationCapability.invoke.andReturn(mockPromise(true)); mockPersistenceCapability.getSpace.andReturn("testSpace"); + mockCompositionCapability.invoke.andReturn( + mockPromise([mockNewObject]) + ); creationService = new CreationService( mockPersistenceService, diff --git a/platform/entanglement/README.md b/platform/entanglement/README.md new file mode 100644 index 0000000000..f3685bad05 --- /dev/null +++ b/platform/entanglement/README.md @@ -0,0 +1,24 @@ +# Entanglement + +Entanglement is the process of moving, copying, and linking domain objects +in such a way that their relationships are impossible to discern. + +This bundle provides move, copy, and link functionality. Acheiving a state of +entanglement is left up to the end user. + + +## Services implement logic + +Each method (move, copy, link) is implemented as a service, and each service +provides two functions: `validate` and `perform`. + +`validate(object, parentCandidate)` returns true if the `object` can be +move/copy/linked into the `parentCandidate`'s composition. + +`perform(object, parentObject)` move/copy/links the `object` into the +`parentObject`'s composition. + +## Actions implement user interactions + +Actions are used to expose move/copy/link to the user. They prompt for input +where necessary, and complete the actions. diff --git a/platform/entanglement/bundle.json b/platform/entanglement/bundle.json new file mode 100644 index 0000000000..7220669988 --- /dev/null +++ b/platform/entanglement/bundle.json @@ -0,0 +1,75 @@ +{ + "name": "Entanglement", + "description": "Tools to assist you in entangling the world of WARP.", + "configuration": {}, + "extensions": { + "actions": [ + { + "key": "move", + "name": "Move", + "description": "Move object to another location.", + "glyph": "f", + "category": "contextual", + "implementation": "actions/MoveAction.js", + "depends": ["locationService", "moveService"] + }, + { + "key": "copy", + "name": "Duplicate", + "description": "Duplicate object to another location.", + "glyph": "+", + "category": "contextual", + "implementation": "actions/CopyAction.js", + "depends": ["locationService", "copyService"] + }, + { + "key": "link", + "name": "Create Link", + "description": "Create Link to object in another location.", + "glyph": "\u00E8", + "category": "contextual", + "implementation": "actions/LinkAction.js", + "depends": ["locationService", "linkService"] + } + ], + "components": [ + ], + "controllers": [ + ], + "capabilities": [ + ], + "services": [ + { + "key": "moveService", + "name": "Move Service", + "description": "Provides a service for moving objects", + "implementation": "services/MoveService.js", + "depends": ["policyService", "linkService"] + }, + { + "key": "linkService", + "name": "Link Service", + "description": "Provides a service for linking objects", + "implementation": "services/LinkService.js", + "depends": ["policyService"] + }, + { + "key": "copyService", + "name": "Copy Service", + "description": "Provides a service for copying objects", + "implementation": "services/CopyService.js", + "depends": ["$q", "creationService", "policyService"] + }, + { + "key": "locationService", + "name": "Location Service", + "description": "Provides a service for prompting a user for locations.", + "implementation": "services/LocationService.js", + "depends": ["dialogService"] + } + + ], + "licenses": [ + ] + } +} diff --git a/platform/entanglement/src/actions/CopyAction.js b/platform/entanglement/src/actions/CopyAction.js new file mode 100644 index 0000000000..aff8b94fb5 --- /dev/null +++ b/platform/entanglement/src/actions/CopyAction.js @@ -0,0 +1,92 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/*global define */ +define( + function () { + "use strict"; + + + /** + * The CopyAction is available from context menus and allows a user to + * deep copy an object to another location of their choosing. + * + * @implements Action + */ + function CopyAction(locationService, copyService, context) { + + var object, + newParent, + currentParent; + + if (context.selectedObject) { + newParent = context.domainObject; + object = context.selectedObject; + } else { + object = context.domainObject; + } + + currentParent = object + .getCapability('context') + .getParent(); + + return { + perform: function () { + + if (newParent) { + return copyService + .perform(object, newParent); + } + + var dialogTitle, + label, + validateLocation; + + dialogTitle = [ + "Duplicate ", + object.getModel().name, + " to a location" + ].join(""); + + label = "Duplicate To"; + + validateLocation = function (newParent) { + return copyService + .validate(object, newParent); + }; + + return locationService.getLocationFromUser( + dialogTitle, + label, + validateLocation, + currentParent + ).then(function (newParent) { + return copyService + .perform(object, newParent); + }); + } + }; + } + + return CopyAction; + } +); diff --git a/platform/entanglement/src/actions/LinkAction.js b/platform/entanglement/src/actions/LinkAction.js new file mode 100644 index 0000000000..c6b4be100f --- /dev/null +++ b/platform/entanglement/src/actions/LinkAction.js @@ -0,0 +1,89 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/*global define */ +define( + function () { + "use strict"; + + /** + * The LinkAction is available from context menus and allows a user to + * link an object to another location of their choosing. + * + * @implements Action + */ + function LinkAction(locationService, linkService, context) { + + var object, + newParent, + currentParent; + + if (context.selectedObject) { + newParent = context.domainObject; + object = context.selectedObject; + } else { + object = context.domainObject; + } + + currentParent = object + .getCapability('context') + .getParent(); + + return { + perform: function () { + if (newParent) { + return linkService + .perform(object, newParent); + } + var dialogTitle, + label, + validateLocation; + + dialogTitle = [ + "Link ", + object.getModel().name, + " to a new location" + ].join(""); + + label = "Link To"; + + validateLocation = function (newParent) { + return linkService + .validate(object, newParent); + }; + + return locationService.getLocationFromUser( + dialogTitle, + label, + validateLocation, + currentParent + ).then(function (newParent) { + return linkService + .perform(object, newParent); + }); + } + }; + } + + return LinkAction; + } +); diff --git a/platform/entanglement/src/actions/MoveAction.js b/platform/entanglement/src/actions/MoveAction.js new file mode 100644 index 0000000000..63f1517c56 --- /dev/null +++ b/platform/entanglement/src/actions/MoveAction.js @@ -0,0 +1,90 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/*global define */ +define( + function () { + "use strict"; + + /** + * The MoveAction is available from context menus and allows a user to + * move an object to another location of their choosing. + * + * @implements Action + */ + function MoveAction(locationService, moveService, context) { + + var object, + newParent, + currentParent; + + if (context.selectedObject) { + newParent = context.domainObject; + object = context.selectedObject; + } else { + object = context.domainObject; + } + + currentParent = object + .getCapability('context') + .getParent(); + + return { + perform: function () { + if (newParent) { + return moveService + .perform(object, newParent); + } + + var dialogTitle, + label, + validateLocation; + + dialogTitle = [ + "Move ", + object.getModel().name, + " to a new location" + ].join(""); + + label = "Move To"; + + validateLocation = function (newParent) { + return moveService + .validate(object, newParent); + }; + + return locationService.getLocationFromUser( + dialogTitle, + label, + validateLocation, + currentParent + ).then(function (newParent) { + return moveService + .perform(object, newParent); + }); + } + }; + } + + return MoveAction; + } +); diff --git a/platform/entanglement/src/services/CopyService.js b/platform/entanglement/src/services/CopyService.js new file mode 100644 index 0000000000..487568c475 --- /dev/null +++ b/platform/entanglement/src/services/CopyService.js @@ -0,0 +1,106 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/*global define */ + +define( + function () { + "use strict"; + + /** + * CopyService provides an interface for deep copying objects from one + * location to another. It also provides a method for determining if + * an object can be copied to a specific location. + */ + function CopyService($q, creationService, policyService) { + + /** + * duplicateObject duplicates a `domainObject` into the composition + * of `parent`, and then duplicates the composition of + * `domainObject` into the new object. + * + * This function is a recursive deep copy. + * + * @param {DomainObject} domainObject - the domain object to + * duplicate. + * @param {DomainObject} parent - the parent domain object to + * create the duplicate in. + * @returns {Promise} A promise that is fulfilled when the + * duplicate operation has completed. + */ + function duplicateObject(domainObject, parent) { + var model = JSON.parse(JSON.stringify(domainObject.getModel())); + if (domainObject.hasCapability('composition')) { + model.composition = []; + } + + return creationService + .createObject(model, parent) + .then(function (newObject) { + if (!domainObject.hasCapability('composition')) { + return; + } + + return domainObject + .useCapability('composition') + .then(function (composees) { + // Duplicate composition serially to prevent + // write conflicts. + return composees.reduce(function (promise, composee) { + return promise.then(function () { + return duplicateObject(composee, newObject); + }); + }, $q.when(undefined)); + }); + }); + } + + return { + /** + * Returns true if `object` can be copied into + * `parentCandidate`'s composition. + */ + validate: function (object, parentCandidate) { + if (!parentCandidate || !parentCandidate.getId) { + return false; + } + if (parentCandidate.getId() === object.getId()) { + return false; + } + return policyService.allow( + "composition", + parentCandidate.getCapability('type'), + object.getCapability('type') + ); + }, + /** + * Wrapper, @see {@link duplicateObject} for implementation. + */ + perform: function (object, parentObject) { + return duplicateObject(object, parentObject); + } + }; + } + + return CopyService; + } +); diff --git a/platform/entanglement/src/services/LinkService.js b/platform/entanglement/src/services/LinkService.js new file mode 100644 index 0000000000..acecab3b8d --- /dev/null +++ b/platform/entanglement/src/services/LinkService.js @@ -0,0 +1,76 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/*global define */ + +define( + function () { + "use strict"; + + /** + * LinkService provides an interface for linking objects to additional + * locations. It also provides a method for determining if an object + * can be copied to a specific location. + */ + function LinkService(policyService) { + return { + /** + * Returns `true` if `object` can be linked into + * `parentCandidate`'s composition. + */ + validate: function (object, parentCandidate) { + if (!parentCandidate || !parentCandidate.getId) { + return false; + } + if (parentCandidate.getId() === object.getId()) { + return false; + } + if (parentCandidate.getModel().composition.indexOf(object.getId()) !== -1) { + return false; + } + return policyService.allow( + "composition", + parentCandidate.getCapability('type'), + object.getCapability('type') + ); + }, + /** + * Link `object` into `parentObject`'s composition. + * + * @returns {Promise} A promise that is fulfilled when the + * linking operation has completed. + */ + perform: function (object, parentObject) { + return parentObject.useCapability('mutation', function (model) { + if (model.composition.indexOf(object.getId()) === -1) { + model.composition.push(object.getId()); + } + }).then(function () { + return parentObject.getCapability('persistence').persist(); + }); + } + }; + } + + return LinkService; + } +); diff --git a/platform/entanglement/src/services/LocationService.js b/platform/entanglement/src/services/LocationService.js new file mode 100644 index 0000000000..3e71011503 --- /dev/null +++ b/platform/entanglement/src/services/LocationService.js @@ -0,0 +1,83 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/*global define */ + +define( + function () { + "use strict"; + + /** + * The LocationService allows for easily prompting the user for a + * location in the root tree. + */ + function LocationService(dialogService) { + return { + /** Prompt the user to select a location. Returns a promise + * that is resolved with a domainObject representing the + * location selected by the user. + * + * @param {string} title - title of location dialog + * @param {string} label - label for location input field + * @param {function} validate - function that validates + * selections. + * @param {domainObject} initialLocation - tree location to + * display at start + * @returns {Promise} promise for a domain object. + */ + getLocationFromUser: function (title, label, validate, initialLocation) { + var formStructure, + formState; + + formStructure = { + sections: [ + { + name: 'Location', + rows: [ + { + name: label, + control: "locator", + validate: validate, + key: 'location' + } + ] + } + ], + name: title + }; + + formState = { + location: initialLocation + }; + + return dialogService + .getUserInput(formStructure, formState) + .then(function (formState) { + return formState.location; + }); + } + }; + } + + return LocationService; + } +); diff --git a/platform/entanglement/src/services/MoveService.js b/platform/entanglement/src/services/MoveService.js new file mode 100644 index 0000000000..e91d381453 --- /dev/null +++ b/platform/entanglement/src/services/MoveService.js @@ -0,0 +1,83 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/*global define */ + +define( + function () { + "use strict"; + + /** + * MoveService provides an interface for moving objects from one + * location to another. It also provides a method for determining if + * an object can be copied to a specific location. + */ + function MoveService(policyService, linkService) { + return { + /** + * Returns `true` if `object` can be moved into + * `parentCandidate`'s composition. + */ + validate: function (object, parentCandidate) { + var currentParent = object + .getCapability('context') + .getParent(); + + if (!parentCandidate || !parentCandidate.getId) { + return false; + } + if (parentCandidate.getId() === currentParent.getId()) { + return false; + } + if (parentCandidate.getId() === object.getId()) { + return false; + } + if (parentCandidate.getModel().composition.indexOf(object.getId()) !== -1) { + return false; + } + return policyService.allow( + "composition", + parentCandidate.getCapability('type'), + object.getCapability('type') + ); + }, + /** + * Move `object` into `parentObject`'s composition. + * + * @returns {Promise} A promise that is fulfilled when the + * move operation has completed. + */ + perform: function (object, parentObject) { + return linkService + .perform(object, parentObject) + .then(function () { + return object + .getCapability('action') + .perform('remove'); + }); + } + }; + } + + return MoveService; + } +); diff --git a/platform/entanglement/test/DomainObjectFactory.js b/platform/entanglement/test/DomainObjectFactory.js new file mode 100644 index 0000000000..3f62841e42 --- /dev/null +++ b/platform/entanglement/test/DomainObjectFactory.js @@ -0,0 +1,158 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/*global define, jasmine, */ + +define( + function () { + "use strict"; + + /** + * @typedef DomainObjectConfig + * @type {object} + * @property {string} [name] a name for the underlying jasmine spy + * object mockDomainObject. Used as + * @property {string} [id] initial id value for the domainOBject. + * @property {object} [model] initial values for the object's model. + * @property {object} [capabilities] an object containing + * capability definitions. + */ + + var configObjectProps = ['model', 'capabilities']; + + /** + * Internal function for ensuring an object is an instance of a + * DomainObjectConfig. + */ + function ensureValidConfigObject(config) { + if (!config || !config.hasOwnProperty) { + config = {}; + } + if (!config.name) { + config.name = 'domainObject'; + } + configObjectProps.forEach(function (prop) { + if (!config[prop] || !config[prop].hasOwnProperty) { + config[prop] = {}; + } + }); + return config; + } + + /** + * Defines a factory function which takes a `config` object and returns + * a mock domainObject. The config object is an easy way to provide + * initial properties for the domainObject-- they can be changed at any + * time by directly modifying the domainObject's properties. + * + * @param {Object} [config] initial configuration for a domain object. + * @returns {Object} mockDomainObject + */ + function domainObjectFactory(config) { + config = ensureValidConfigObject(config); + + var domainObject = jasmine.createSpyObj(config.name, [ + 'getId', + 'getModel', + 'getCapability', + 'hasCapability', + 'useCapability' + ]); + + domainObject.model = JSON.parse(JSON.stringify(config.model)); + domainObject.capabilities = config.capabilities; + domainObject.id = config.id; + + /** + * getId: Returns `domainObject.id`. + * + * @returns {string} id + */ + domainObject.getId.andCallFake(function () { + return domainObject.id; + }); + + /** + * getModel: Returns `domainObject.model`. + * + * @returns {object} model + */ + domainObject.getModel.andCallFake(function () { + return domainObject.model; + }); + + /** + * getCapability: returns a `capability` object defined in + * domainObject.capabilities. Returns undefined if capability + * does not exist. + * + * @param {string} capability name of the capability to return. + * @returns {*} capability object + */ + domainObject.getCapability.andCallFake(function (capability) { + if (config.capabilities.hasOwnProperty(capability)) { + return config.capabilities[capability]; + } + }); + + /** + * hasCapability: return true if domainObject.capabilities has a + * property named `capability`, otherwise returns false. + * + * @param {string} capability name of the capability to test for + * existence of. + * @returns {boolean} + */ + domainObject.hasCapability.andCallFake(function (capability) { + return config.capabilities.hasOwnProperty(capability); + }); + + /** + * useCapability: find a capability in domainObject.capabilities + * and call that capabilities' invoke method. If the capability + * does not have an invoke method, will throw an error. + * + * @param {string} capability name of a capability to invoke. + * @param {...*} params to pass to the capability's `invoke` method. + * @returns {*} result whatever was returned by `invoke`. + */ + domainObject.useCapability.andCallFake(function (capability) { + if (config.capabilities.hasOwnProperty(capability)) { + if (!config.capabilities[capability].invoke) { + throw new Error( + capability + ' missing invoke function.' + ); + } + var passThroughArgs = [].slice.call(arguments, 1); + return config + .capabilities[capability] + .invoke + .apply(null, passThroughArgs); + } + }); + + return domainObject; + } + + return domainObjectFactory; + } +); \ No newline at end of file diff --git a/platform/entanglement/test/actions/CopyActionSpec.js b/platform/entanglement/test/actions/CopyActionSpec.js new file mode 100644 index 0000000000..4284a22e59 --- /dev/null +++ b/platform/entanglement/test/actions/CopyActionSpec.js @@ -0,0 +1,174 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/*global define,describe,beforeEach,it,jasmine,expect */ + +define( + [ + '../../src/actions/CopyAction', + '../services/MockCopyService', + '../DomainObjectFactory' + ], + function (CopyAction, MockCopyService, domainObjectFactory) { + "use strict"; + + describe("Copy Action", function () { + + var copyAction, + locationService, + locationServicePromise, + copyService, + context, + selectedObject, + selectedObjectContextCapability, + currentParent, + newParent; + + beforeEach(function () { + selectedObjectContextCapability = jasmine.createSpyObj( + 'selectedObjectContextCapability', + [ + 'getParent' + ] + ); + + selectedObject = domainObjectFactory({ + name: 'selectedObject', + model: { + name: 'selectedObject' + }, + capabilities: { + context: selectedObjectContextCapability + } + }); + + currentParent = domainObjectFactory({ + name: 'currentParent' + }); + + selectedObjectContextCapability + .getParent + .andReturn(currentParent); + + newParent = domainObjectFactory({ + name: 'newParent' + }); + + locationService = jasmine.createSpyObj( + 'locationService', + [ + 'getLocationFromUser' + ] + ); + + locationServicePromise = jasmine.createSpyObj( + 'locationServicePromise', + [ + 'then' + ] + ); + + locationService + .getLocationFromUser + .andReturn(locationServicePromise); + + copyService = new MockCopyService(); + }); + + + describe("with context from context-action", function () { + beforeEach(function () { + context = { + domainObject: selectedObject + }; + + copyAction = new CopyAction( + locationService, + copyService, + context + ); + }); + + it("initializes happily", function () { + expect(copyAction).toBeDefined(); + }); + + describe("when performed it", function () { + beforeEach(function () { + copyAction.perform(); + }); + + it("prompts for location", function () { + expect(locationService.getLocationFromUser) + .toHaveBeenCalledWith( + "Duplicate selectedObject to a location", + "Duplicate To", + jasmine.any(Function), + currentParent + ); + }); + + it("waits for location from user", function () { + expect(locationServicePromise.then) + .toHaveBeenCalledWith(jasmine.any(Function)); + }); + + it("copys object to selected location", function () { + locationServicePromise + .then + .mostRecentCall + .args[0](newParent); + + expect(copyService.perform) + .toHaveBeenCalledWith(selectedObject, newParent); + }); + }); + }); + + describe("with context from drag-drop", function () { + beforeEach(function () { + context = { + selectedObject: selectedObject, + domainObject: newParent + }; + + copyAction = new CopyAction( + locationService, + copyService, + context + ); + }); + + it("initializes happily", function () { + expect(copyAction).toBeDefined(); + }); + + + it("performs copy immediately", function () { + copyAction.perform(); + expect(copyService.perform) + .toHaveBeenCalledWith(selectedObject, newParent); + }); + }); + }); + } +); diff --git a/platform/entanglement/test/actions/LinkActionSpec.js b/platform/entanglement/test/actions/LinkActionSpec.js new file mode 100644 index 0000000000..03967a6672 --- /dev/null +++ b/platform/entanglement/test/actions/LinkActionSpec.js @@ -0,0 +1,174 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/*global define,describe,beforeEach,it,jasmine,expect */ + +define( + [ + '../../src/actions/LinkAction', + '../services/MockLinkService', + '../DomainObjectFactory' + ], + function (LinkAction, MockLinkService, domainObjectFactory) { + "use strict"; + + describe("Link Action", function () { + + var linkAction, + locationService, + locationServicePromise, + linkService, + context, + selectedObject, + selectedObjectContextCapability, + currentParent, + newParent; + + beforeEach(function () { + selectedObjectContextCapability = jasmine.createSpyObj( + 'selectedObjectContextCapability', + [ + 'getParent' + ] + ); + + selectedObject = domainObjectFactory({ + name: 'selectedObject', + model: { + name: 'selectedObject' + }, + capabilities: { + context: selectedObjectContextCapability + } + }); + + currentParent = domainObjectFactory({ + name: 'currentParent' + }); + + selectedObjectContextCapability + .getParent + .andReturn(currentParent); + + newParent = domainObjectFactory({ + name: 'newParent' + }); + + locationService = jasmine.createSpyObj( + 'locationService', + [ + 'getLocationFromUser' + ] + ); + + locationServicePromise = jasmine.createSpyObj( + 'locationServicePromise', + [ + 'then' + ] + ); + + locationService + .getLocationFromUser + .andReturn(locationServicePromise); + + linkService = new MockLinkService(); + }); + + + describe("with context from context-action", function () { + beforeEach(function () { + context = { + domainObject: selectedObject + }; + + linkAction = new LinkAction( + locationService, + linkService, + context + ); + }); + + it("initializes happily", function () { + expect(linkAction).toBeDefined(); + }); + + describe("when performed it", function () { + beforeEach(function () { + linkAction.perform(); + }); + + it("prompts for location", function () { + expect(locationService.getLocationFromUser) + .toHaveBeenCalledWith( + "Link selectedObject to a new location", + "Link To", + jasmine.any(Function), + currentParent + ); + }); + + it("waits for location from user", function () { + expect(locationServicePromise.then) + .toHaveBeenCalledWith(jasmine.any(Function)); + }); + + it("links object to selected location", function () { + locationServicePromise + .then + .mostRecentCall + .args[0](newParent); + + expect(linkService.perform) + .toHaveBeenCalledWith(selectedObject, newParent); + }); + }); + }); + + describe("with context from drag-drop", function () { + beforeEach(function () { + context = { + selectedObject: selectedObject, + domainObject: newParent + }; + + linkAction = new LinkAction( + locationService, + linkService, + context + ); + }); + + it("initializes happily", function () { + expect(linkAction).toBeDefined(); + }); + + + it("performs link immediately", function () { + linkAction.perform(); + expect(linkService.perform) + .toHaveBeenCalledWith(selectedObject, newParent); + }); + }); + }); + } +); diff --git a/platform/entanglement/test/actions/MoveActionSpec.js b/platform/entanglement/test/actions/MoveActionSpec.js new file mode 100644 index 0000000000..52a7c6e301 --- /dev/null +++ b/platform/entanglement/test/actions/MoveActionSpec.js @@ -0,0 +1,174 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/*global define,describe,beforeEach,it,jasmine,expect */ + +define( + [ + '../../src/actions/MoveAction', + '../services/MockMoveService', + '../DomainObjectFactory' + ], + function (MoveAction, MockMoveService, domainObjectFactory) { + "use strict"; + + describe("Move Action", function () { + + var moveAction, + locationService, + locationServicePromise, + moveService, + context, + selectedObject, + selectedObjectContextCapability, + currentParent, + newParent; + + beforeEach(function () { + selectedObjectContextCapability = jasmine.createSpyObj( + 'selectedObjectContextCapability', + [ + 'getParent' + ] + ); + + selectedObject = domainObjectFactory({ + name: 'selectedObject', + model: { + name: 'selectedObject' + }, + capabilities: { + context: selectedObjectContextCapability + } + }); + + currentParent = domainObjectFactory({ + name: 'currentParent' + }); + + selectedObjectContextCapability + .getParent + .andReturn(currentParent); + + newParent = domainObjectFactory({ + name: 'newParent' + }); + + locationService = jasmine.createSpyObj( + 'locationService', + [ + 'getLocationFromUser' + ] + ); + + locationServicePromise = jasmine.createSpyObj( + 'locationServicePromise', + [ + 'then' + ] + ); + + locationService + .getLocationFromUser + .andReturn(locationServicePromise); + + moveService = new MockMoveService(); + }); + + + describe("with context from context-action", function () { + beforeEach(function () { + context = { + domainObject: selectedObject + }; + + moveAction = new MoveAction( + locationService, + moveService, + context + ); + }); + + it("initializes happily", function () { + expect(moveAction).toBeDefined(); + }); + + describe("when performed it", function () { + beforeEach(function () { + moveAction.perform(); + }); + + it("prompts for location", function () { + expect(locationService.getLocationFromUser) + .toHaveBeenCalledWith( + "Move selectedObject to a new location", + "Move To", + jasmine.any(Function), + currentParent + ); + }); + + it("waits for location from user", function () { + expect(locationServicePromise.then) + .toHaveBeenCalledWith(jasmine.any(Function)); + }); + + it("moves object to selected location", function () { + locationServicePromise + .then + .mostRecentCall + .args[0](newParent); + + expect(moveService.perform) + .toHaveBeenCalledWith(selectedObject, newParent); + }); + }); + }); + + describe("with context from drag-drop", function () { + beforeEach(function () { + context = { + selectedObject: selectedObject, + domainObject: newParent + }; + + moveAction = new MoveAction( + locationService, + moveService, + context + ); + }); + + it("initializes happily", function () { + expect(moveAction).toBeDefined(); + }); + + + it("performs move immediately", function () { + moveAction.perform(); + expect(moveService.perform) + .toHaveBeenCalledWith(selectedObject, newParent); + }); + }); + }); + } +); diff --git a/platform/entanglement/test/services/CopyServiceSpec.js b/platform/entanglement/test/services/CopyServiceSpec.js new file mode 100644 index 0000000000..6d7b1d5069 --- /dev/null +++ b/platform/entanglement/test/services/CopyServiceSpec.js @@ -0,0 +1,272 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/*global define,describe,beforeEach,it,jasmine,expect,spyOn */ + +define( + [ + '../../src/services/CopyService', + '../DomainObjectFactory' + ], + function (CopyService, domainObjectFactory) { + "use strict"; + + function synchronousPromise(value) { + var promise = { + then: function (callback) { + return synchronousPromise(callback(value)); + } + }; + spyOn(promise, 'then').andCallThrough(); + return promise; + } + + describe("CopyService", function () { + describe("validate", function () { + + var policyService, + copyService, + object, + parentCandidate, + validate; + + beforeEach(function () { + policyService = jasmine.createSpyObj( + 'policyService', + ['allow'] + ); + copyService = new CopyService( + null, + null, + policyService + ); + object = domainObjectFactory({ + name: 'object', + capabilities: { + type: { type: 'object' } + } + }); + parentCandidate = domainObjectFactory({ + name: 'parentCandidate', + capabilities: { + type: { type: 'parentCandidate' } + } + }); + validate = function () { + return copyService.validate(object, parentCandidate); + }; + }); + + it("does not allow invalid parentCandidate", function () { + parentCandidate = undefined; + expect(validate()).toBe(false); + parentCandidate = {}; + expect(validate()).toBe(false); + }); + + it("does not allow copying into source object", function () { + object.id = parentCandidate.id = 'abc'; + expect(validate()).toBe(false); + }); + + describe("defers to policyService", function () { + beforeEach(function () { + object.id = 'a'; + parentCandidate.id = 'b'; + }); + + it("calls policy service with correct args", function () { + validate(); + expect(policyService.allow).toHaveBeenCalledWith( + "composition", + parentCandidate.capabilities.type, + object.capabilities.type + ); + }); + + it("and returns false", function () { + policyService.allow.andReturn(false); + expect(validate()).toBe(false); + }); + + it("and returns true", function () { + policyService.allow.andReturn(true); + expect(validate()).toBe(true); + }); + }); + }); + + describe("perform", function () { + + var mockQ, + creationService, + createObjectPromise, + copyService, + object, + newParent, + copyResult, + copyFinished; + + describe("on domain object without composition", function () { + beforeEach(function () { + object = domainObjectFactory({ + name: 'object', + id: 'abc', + model: { + name: 'some object' + } + }); + newParent = domainObjectFactory({ + name: 'newParent', + id: '456', + model: { + composition: [] + } + }); + creationService = jasmine.createSpyObj( + 'creationService', + ['createObject'] + ); + createObjectPromise = synchronousPromise(undefined); + creationService.createObject.andReturn(createObjectPromise); + copyService = new CopyService(null, creationService); + copyResult = copyService.perform(object, newParent); + copyFinished = jasmine.createSpy('copyFinished'); + copyResult.then(copyFinished); + }); + + it("uses creation service", function () { + expect(creationService.createObject) + .toHaveBeenCalledWith(jasmine.any(Object), newParent); + + expect(createObjectPromise.then) + .toHaveBeenCalledWith(jasmine.any(Function)); + }); + + it("deep clones object model", function () { + var newModel = creationService + .createObject + .mostRecentCall + .args[0]; + + expect(newModel).toEqual(object.model); + expect(newModel).not.toBe(object.model); + }); + + it("returns a promise", function () { + expect(copyResult).toBeDefined(); + expect(copyFinished).toHaveBeenCalled(); + }); + + }); + + describe("on domainObject with composition", function () { + var childObject, + compositionCapability, + compositionPromise; + + beforeEach(function () { + mockQ = jasmine.createSpyObj('mockQ', ['when']); + mockQ.when.andCallFake(synchronousPromise); + childObject = domainObjectFactory({ + name: 'childObject', + id: 'def', + model: { + name: 'a child object' + } + }); + compositionCapability = jasmine.createSpyObj( + 'compositionCapability', + ['invoke'] + ); + compositionPromise = jasmine.createSpyObj( + 'compositionPromise', + ['then'] + ); + compositionCapability + .invoke + .andReturn(compositionPromise); + object = domainObjectFactory({ + name: 'object', + id: 'abc', + model: { + name: 'some object', + composition: ['def'] + }, + capabilities: { + composition: compositionCapability + } + }); + newParent = domainObjectFactory({ + name: 'newParent', + id: '456', + model: { + composition: [] + } + }); + creationService = jasmine.createSpyObj( + 'creationService', + ['createObject'] + ); + createObjectPromise = synchronousPromise(undefined); + creationService.createObject.andReturn(createObjectPromise); + copyService = new CopyService(mockQ, creationService); + copyResult = copyService.perform(object, newParent); + copyFinished = jasmine.createSpy('copyFinished'); + copyResult.then(copyFinished); + }); + + it("uses creation service", function () { + expect(creationService.createObject) + .toHaveBeenCalledWith(jasmine.any(Object), newParent); + + expect(createObjectPromise.then) + .toHaveBeenCalledWith(jasmine.any(Function)); + }); + + it("clears model composition", function () { + var newModel = creationService + .createObject + .mostRecentCall + .args[0]; + + expect(newModel.composition.length).toBe(0); + expect(newModel.name).toBe('some object'); + }); + + it("recursively clones it's children", function () { + expect(creationService.createObject.calls.length).toBe(1); + expect(compositionCapability.invoke).toHaveBeenCalled(); + compositionPromise.then.mostRecentCall.args[0]([childObject]); + expect(creationService.createObject.calls.length).toBe(2); + }); + + it("returns a promise", function () { + expect(copyResult.then).toBeDefined(); + expect(copyFinished).toHaveBeenCalled(); + }); + }); + + }); + }); + } +); diff --git a/platform/entanglement/test/services/LinkServiceSpec.js b/platform/entanglement/test/services/LinkServiceSpec.js new file mode 100644 index 0000000000..0551256277 --- /dev/null +++ b/platform/entanglement/test/services/LinkServiceSpec.js @@ -0,0 +1,183 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/*global define,describe,beforeEach,it,jasmine,expect */ + +define( + [ + '../../src/services/LinkService', + '../DomainObjectFactory' + ], + function (LinkService, domainObjectFactory) { + "use strict"; + + describe("LinkService", function () { + + var linkService, + mockPolicyService; + + beforeEach(function () { + mockPolicyService = jasmine.createSpyObj( + 'policyService', + ['allow'] + ); + linkService = new LinkService(mockPolicyService); + }); + + describe("validate", function () { + + var object, + parentCandidate, + validate; + + beforeEach(function () { + + object = domainObjectFactory({ + name: 'object' + }); + parentCandidate = domainObjectFactory({ + name: 'parentCandidate' + }); + validate = function () { + return linkService.validate(object, parentCandidate); + }; + }); + + it("does not allow invalid parentCandidate", function () { + parentCandidate = undefined; + expect(validate()).toBe(false); + parentCandidate = {}; + expect(validate()).toBe(false); + }); + + it("does not allow parent to be object", function () { + parentCandidate.id = object.id = 'abc'; + expect(validate()).toBe(false); + }); + + it("does not allow parent that contains object", function () { + object.id = 'abc'; + parentCandidate.id = 'xyz'; + parentCandidate.model.composition = ['abc']; + expect(validate()).toBe(false); + }); + + describe("defers to policyService", function () { + beforeEach(function () { + object.id = 'abc'; + object.capabilities.type = { type: 'object' }; + parentCandidate.id = 'xyz'; + parentCandidate.capabilities.type = { + type: 'parentCandidate' + }; + parentCandidate.model.composition = []; + }); + + it("calls policy service with correct args", function () { + validate(); + expect(mockPolicyService.allow).toHaveBeenCalledWith( + "composition", + parentCandidate.capabilities.type, + object.capabilities.type + ); + }); + + it("and returns false", function () { + mockPolicyService.allow.andReturn(true); + expect(validate()).toBe(true); + expect(mockPolicyService.allow).toHaveBeenCalled(); + }); + + it("and returns true", function () { + mockPolicyService.allow.andReturn(false); + expect(validate()).toBe(false); + expect(mockPolicyService.allow).toHaveBeenCalled(); + }); + }); + }); + + describe("perform", function () { + + var object, + parentModel, + parentObject, + mutationPromise, + persistenceCapability; + + beforeEach(function () { + mutationPromise = jasmine.createSpyObj( + 'promise', + ['then'] + ); + persistenceCapability = jasmine.createSpyObj( + 'persistenceCapability', + ['persist'] + ); + parentModel = { + composition: [] + }; + parentObject = domainObjectFactory({ + name: 'parentObject', + model: parentModel, + capabilities: { + mutation: { + invoke: function (mutator) { + mutator(parentModel); + return mutationPromise; + } + }, + persistence: persistenceCapability + } + }); + + object = domainObjectFactory({ + name: 'object', + id: 'xyz' + }); + + parentObject.getCapability.andReturn(persistenceCapability); + }); + + + it("modifies parent model composition", function () { + expect(parentModel.composition.length).toBe(0); + linkService.perform(object, parentObject); + expect(parentObject.useCapability).toHaveBeenCalledWith( + 'mutation', + jasmine.any(Function) + ); + expect(parentModel.composition).toContain('xyz'); + }); + + it("persists parent", function () { + linkService.perform(object, parentObject); + expect(mutationPromise.then).toHaveBeenCalled(); + mutationPromise.then.calls[0].args[0](); + expect(parentObject.getCapability) + .toHaveBeenCalledWith('persistence'); + + expect(persistenceCapability.persist).toHaveBeenCalled(); + }); + }); + }); + } +); diff --git a/platform/entanglement/test/services/LocationServiceSpec.js b/platform/entanglement/test/services/LocationServiceSpec.js new file mode 100644 index 0000000000..c469392f23 --- /dev/null +++ b/platform/entanglement/test/services/LocationServiceSpec.js @@ -0,0 +1,151 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/*global define,describe,beforeEach,it,jasmine,expect */ + +define( + [ + '../../src/services/LocationService' + ], + function (LocationService) { + "use strict"; + + describe("LocationService", function () { + var dialogService, + locationService, + dialogServicePromise, + chainedPromise; + + beforeEach(function () { + dialogService = jasmine.createSpyObj( + 'dialogService', + ['getUserInput'] + ); + dialogServicePromise = jasmine.createSpyObj( + 'dialogServicePromise', + ['then'] + ); + chainedPromise = jasmine.createSpyObj( + 'chainedPromise', + ['then'] + ); + dialogServicePromise.then.andReturn(chainedPromise); + dialogService.getUserInput.andReturn(dialogServicePromise); + locationService = new LocationService(dialogService); + }); + + describe("getLocationFromUser", function () { + var title, + label, + validate, + initialLocation, + locationResult, + formStructure, + formState; + + beforeEach(function () { + title = "Get a location to do something"; + label = "a location"; + validate = function () { return true; }; + initialLocation = { key: "a key" }; + locationResult = locationService.getLocationFromUser( + title, + label, + validate, + initialLocation + ); + formStructure = dialogService + .getUserInput + .mostRecentCall + .args[0]; + formState = dialogService + .getUserInput + .mostRecentCall + .args[1]; + }); + + it("calls through to dialogService", function () { + expect(dialogService.getUserInput).toHaveBeenCalledWith( + jasmine.any(Object), + jasmine.any(Object) + ); + expect(formStructure.name).toBe(title); + }); + + it("returns a promise", function () { + expect(locationResult.then).toBeDefined(); + }); + + describe("formStructure", function () { + var locationSection, + inputRow; + + beforeEach(function () { + locationSection = formStructure.sections[0]; + inputRow = locationSection.rows[0]; + }); + + it("has a location section", function () { + expect(locationSection).toBeDefined(); + expect(locationSection.name).toBe('Location'); + }); + + it("has a input row", function () { + expect(inputRow.control).toBe('locator'); + expect(inputRow.key).toBe('location'); + expect(inputRow.name).toBe(label); + expect(inputRow.validate).toBe(validate); + }); + }); + + describe("formState", function () { + it("has an initial location", function () { + expect(formState.location).toBe(initialLocation); + }); + }); + + describe("resolution of dialog service promise", function () { + var resolution, + resolver, + dialogResult, + selectedLocation; + + beforeEach(function () { + resolver = + dialogServicePromise.then.mostRecentCall.args[0]; + + selectedLocation = { key: "i'm a location key" }; + dialogResult = { + location: selectedLocation + }; + + resolution = resolver(dialogResult); + }); + + it("returns selectedLocation", function () { + expect(resolution).toBe(selectedLocation); + }); + }); + }); + }); + } +); diff --git a/platform/entanglement/test/services/MockCopyService.js b/platform/entanglement/test/services/MockCopyService.js new file mode 100644 index 0000000000..0e7e3517f3 --- /dev/null +++ b/platform/entanglement/test/services/MockCopyService.js @@ -0,0 +1,99 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/*global define,jasmine */ + +define( + function () { + "use strict"; + + /** + * MockCopyService provides the same interface as the copyService, + * returning promises where it would normally do so. At it's core, + * it is a jasmine spy object, but it also tracks the promises it + * returns and provides shortcut methods for resolving those promises + * synchronously. + * + * Usage: + * + * ```javascript + * var copyService = new MockCopyService(); + * + * // validate is a standard jasmine spy. + * copyService.validate.andReturn(true); + * var isValid = copyService.validate(object, parentCandidate); + * expect(isValid).toBe(true); + * + * // perform returns promises and tracks them. + * var whenCopied = jasmine.createSpy('whenCopied'); + * copyService.perform(object, parentObject).then(whenCopied); + * expect(whenCopied).not.toHaveBeenCalled(); + * copyService.perform.mostRecentCall.resolve('someArg'); + * expect(whenCopied).toHaveBeenCalledWith('someArg'); + * ``` + */ + function MockCopyService() { + // track most recent call of a function, + // perform automatically returns + var mockCopyService = jasmine.createSpyObj( + 'MockCopyService', + [ + 'validate', + 'perform' + ] + ); + + mockCopyService.perform.andCallFake(function () { + var performPromise, + callExtensions, + spy; + + performPromise = jasmine.createSpyObj( + 'performPromise', + ['then'] + ); + + callExtensions = { + promise: performPromise, + resolve: function (resolveWith) { + performPromise.then.calls.forEach(function (call) { + call.args[0](resolveWith); + }); + } + }; + + spy = this.perform; + + Object.keys(callExtensions).forEach(function (key) { + spy.mostRecentCall[key] = callExtensions[key]; + spy.calls[spy.calls.length - 1][key] = callExtensions[key]; + }); + + return performPromise; + }); + + return mockCopyService; + } + + return MockCopyService; + } +); diff --git a/platform/entanglement/test/services/MockLinkService.js b/platform/entanglement/test/services/MockLinkService.js new file mode 100644 index 0000000000..58a82333d3 --- /dev/null +++ b/platform/entanglement/test/services/MockLinkService.js @@ -0,0 +1,99 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/*global define,jasmine */ + +define( + function () { + "use strict"; + + /** + * MockLinkService provides the same interface as the linkService, + * returning promises where it would normally do so. At it's core, + * it is a jasmine spy object, but it also tracks the promises it + * returns and provides shortcut methods for resolving those promises + * synchronously. + * + * Usage: + * + * ```javascript + * var linkService = new MockLinkService(); + * + * // validate is a standard jasmine spy. + * linkService.validate.andReturn(true); + * var isValid = linkService.validate(object, parentObject); + * expect(isValid).toBe(true); + * + * // perform returns promises and tracks them. + * var whenLinked = jasmine.createSpy('whenLinked'); + * linkService.perform(object, parentObject).then(whenLinked); + * expect(whenLinked).not.toHaveBeenCalled(); + * linkService.perform.mostRecentCall.resolve('someArg'); + * expect(whenLinked).toHaveBeenCalledWith('someArg'); + * ``` + */ + function MockLinkService() { + // track most recent call of a function, + // perform automatically returns + var mockLinkService = jasmine.createSpyObj( + 'MockLinkService', + [ + 'validate', + 'perform' + ] + ); + + mockLinkService.perform.andCallFake(function () { + var performPromise, + callExtensions, + spy; + + performPromise = jasmine.createSpyObj( + 'performPromise', + ['then'] + ); + + callExtensions = { + promise: performPromise, + resolve: function (resolveWith) { + performPromise.then.calls.forEach(function (call) { + call.args[0](resolveWith); + }); + } + }; + + spy = this.perform; + + Object.keys(callExtensions).forEach(function (key) { + spy.mostRecentCall[key] = callExtensions[key]; + spy.calls[spy.calls.length - 1][key] = callExtensions[key]; + }); + + return performPromise; + }); + + return mockLinkService; + } + + return MockLinkService; + } +); diff --git a/platform/entanglement/test/services/MockMoveService.js b/platform/entanglement/test/services/MockMoveService.js new file mode 100644 index 0000000000..52278a49d5 --- /dev/null +++ b/platform/entanglement/test/services/MockMoveService.js @@ -0,0 +1,99 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/*global define,jasmine */ + +define( + function () { + "use strict"; + + /** + * MockMoveService provides the same interface as the moveService, + * returning promises where it would normally do so. At it's core, + * it is a jasmine spy object, but it also tracks the promises it + * returns and provides shortcut methods for resolving those promises + * synchronously. + * + * Usage: + * + * ```javascript + * var moveService = new MockMoveService(); + * + * // validate is a standard jasmine spy. + * moveService.validate.andReturn(true); + * var isValid = moveService.validate(object, parentCandidate); + * expect(isValid).toBe(true); + * + * // perform returns promises and tracks them. + * var whenCopied = jasmine.createSpy('whenCopied'); + * moveService.perform(object, parentObject).then(whenCopied); + * expect(whenCopied).not.toHaveBeenCalled(); + * moveService.perform.mostRecentCall.resolve('someArg'); + * expect(whenCopied).toHaveBeenCalledWith('someArg'); + * ``` + */ + function MockMoveService() { + // track most recent call of a function, + // perform automatically returns + var mockMoveService = jasmine.createSpyObj( + 'MockMoveService', + [ + 'validate', + 'perform' + ] + ); + + mockMoveService.perform.andCallFake(function () { + var performPromise, + callExtensions, + spy; + + performPromise = jasmine.createSpyObj( + 'performPromise', + ['then'] + ); + + callExtensions = { + promise: performPromise, + resolve: function (resolveWith) { + performPromise.then.calls.forEach(function (call) { + call.args[0](resolveWith); + }); + } + }; + + spy = this.perform; + + Object.keys(callExtensions).forEach(function (key) { + spy.mostRecentCall[key] = callExtensions[key]; + spy.calls[spy.calls.length - 1][key] = callExtensions[key]; + }); + + return performPromise; + }); + + return mockMoveService; + } + + return MockMoveService; + } +); diff --git a/platform/entanglement/test/services/MoveServiceSpec.js b/platform/entanglement/test/services/MoveServiceSpec.js new file mode 100644 index 0000000000..d1ffb52bc1 --- /dev/null +++ b/platform/entanglement/test/services/MoveServiceSpec.js @@ -0,0 +1,189 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/*global define,describe,beforeEach,it,jasmine,expect */ +define( + [ + '../../src/services/MoveService', + '../services/MockLinkService', + '../DomainObjectFactory' + ], + function (MoveService, MockLinkService, domainObjectFactory) { + "use strict"; + + describe("MoveService", function () { + + var moveService, + policyService, + linkService; + + beforeEach(function () { + policyService = jasmine.createSpyObj( + 'policyService', + ['allow'] + ); + linkService = new MockLinkService(); + moveService = new MoveService(policyService, linkService); + }); + + describe("validate", function () { + var object, + objectContextCapability, + currentParent, + parentCandidate, + validate; + + beforeEach(function () { + + objectContextCapability = jasmine.createSpyObj( + 'objectContextCapability', + [ + 'getParent' + ] + ); + + object = domainObjectFactory({ + name: 'object', + id: 'a', + capabilities: { + context: objectContextCapability, + type: { type: 'object' } + } + }); + + currentParent = domainObjectFactory({ + name: 'currentParent', + id: 'b' + }); + + objectContextCapability.getParent.andReturn(currentParent); + + parentCandidate = domainObjectFactory({ + name: 'parentCandidate', + model: { composition: [] }, + id: 'c', + capabilities: { + type: { type: 'parentCandidate' } + } + }); + + validate = function () { + return moveService.validate(object, parentCandidate); + }; + }); + + it("does not allow an invalid parent", function () { + parentCandidate = undefined; + expect(validate()).toBe(false); + parentCandidate = {}; + expect(validate()).toBe(false); + }); + + it("does not allow moving to current parent", function () { + parentCandidate.id = currentParent.id = 'xyz'; + expect(validate()).toBe(false); + }); + + it("does not allow moving to self", function () { + object.id = parentCandidate.id = 'xyz'; + expect(validate()).toBe(false); + }); + + it("does not allow moving to the same location", function () { + object.id = 'abc'; + parentCandidate.model.composition = ['abc']; + expect(validate()).toBe(false); + }); + + describe("defers to policyService", function () { + + it("calls policy service with correct args", function () { + validate(); + expect(policyService.allow).toHaveBeenCalledWith( + "composition", + parentCandidate.capabilities.type, + object.capabilities.type + ); + }); + + it("and returns false", function () { + policyService.allow.andReturn(false); + expect(validate()).toBe(false); + }); + + it("and returns true", function () { + policyService.allow.andReturn(true); + expect(validate()).toBe(true); + }); + }); + }); + + describe("perform", function () { + + var object, + parentObject, + actionCapability; + + beforeEach(function () { + actionCapability = jasmine.createSpyObj( + 'actionCapability', + ['perform'] + ); + + object = domainObjectFactory({ + name: 'object', + capabilities: { + action: actionCapability + } + }); + + parentObject = domainObjectFactory({ + name: 'parentObject' + }); + + moveService.perform(object, parentObject); + }); + + it("links object to parentObject", function () { + expect(linkService.perform).toHaveBeenCalledWith( + object, + parentObject + ); + }); + + it("waits for result of link", function () { + expect(linkService.perform.mostRecentCall.promise.then) + .toHaveBeenCalledWith(jasmine.any(Function)); + }); + + it("removes object when link is completed", function () { + linkService.perform.mostRecentCall.resolve(); + expect(object.getCapability) + .toHaveBeenCalledWith('action'); + expect(actionCapability.perform) + .toHaveBeenCalledWith('remove'); + }); + + }); + }); + } +); diff --git a/platform/entanglement/test/suite.json b/platform/entanglement/test/suite.json new file mode 100644 index 0000000000..40c300e213 --- /dev/null +++ b/platform/entanglement/test/suite.json @@ -0,0 +1,9 @@ +[ + "actions/CopyAction", + "actions/LinkAction", + "actions/MoveAction", + "services/CopyService", + "services/LinkService", + "services/MoveService", + "services/LocationService" +] diff --git a/pom.xml b/pom.xml index b8cc32ca06..194594d6e6 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ gov.nasa.arc.wtd open-mct-web Open MCT Web - 0.7.0-SNAPSHOT + 0.7.1-SNAPSHOT war