From 9123078293ed6b90af0429ce41b963dd326ecaa4 Mon Sep 17 00:00:00 2001 From: larkin Date: Thu, 11 Jun 2015 11:58:53 -0700 Subject: [PATCH] [Entanglement] Add entanglement bundle The entanglement bundle defines move, copy, and link actions, and exposes them as context menu actions. * The Move action moves an object from it's current parent to a new parent object. * The Copy action deep-copies an object to a new parent object. * The Link action links an object to a new parent object. These actions are implemented by three new services: moveService, copyService, and linkService. Mocks are provided for each service for easy testing of components that depend on them. Additionally, this bundle provides a DomainObjectFactory that simplifies the construction of mockDomainObjects for tests. These actions are exposed to the user as context menu options. --- platform/entanglement/README.md | 24 ++ platform/entanglement/bundle.json | 75 ++++++ .../entanglement/src/actions/CopyAction.js | 63 +++++ .../entanglement/src/actions/LinkAction.js | 61 +++++ .../entanglement/src/actions/MoveAction.js | 62 +++++ .../entanglement/src/services/CopyService.js | 79 ++++++ .../entanglement/src/services/LinkService.js | 49 ++++ .../src/services/LocationService.js | 61 +++++ .../entanglement/src/services/MoveService.js | 56 +++++ .../entanglement/test/DomainObjectFactory.js | 136 ++++++++++ .../test/actions/CopyActionSpec.js | 152 +++++++++++ .../test/actions/LinkActionSpec.js | 152 +++++++++++ .../test/actions/MoveActionSpec.js | 152 +++++++++++ .../test/services/CopyServiceSpec.js | 235 ++++++++++++++++++ .../test/services/LinkServiceSpec.js | 148 +++++++++++ .../test/services/LocationServiceSpec.js | 133 ++++++++++ .../test/services/MockCopyService.js | 78 ++++++ .../test/services/MockLinkService.js | 78 ++++++ .../test/services/MockMoveService.js | 78 ++++++ .../test/services/MoveServiceSpec.js | 154 ++++++++++++ platform/entanglement/test/suite.json | 9 + 21 files changed, 2035 insertions(+) create mode 100644 platform/entanglement/README.md create mode 100644 platform/entanglement/bundle.json create mode 100644 platform/entanglement/src/actions/CopyAction.js create mode 100644 platform/entanglement/src/actions/LinkAction.js create mode 100644 platform/entanglement/src/actions/MoveAction.js create mode 100644 platform/entanglement/src/services/CopyService.js create mode 100644 platform/entanglement/src/services/LinkService.js create mode 100644 platform/entanglement/src/services/LocationService.js create mode 100644 platform/entanglement/src/services/MoveService.js create mode 100644 platform/entanglement/test/DomainObjectFactory.js create mode 100644 platform/entanglement/test/actions/CopyActionSpec.js create mode 100644 platform/entanglement/test/actions/LinkActionSpec.js create mode 100644 platform/entanglement/test/actions/MoveActionSpec.js create mode 100644 platform/entanglement/test/services/CopyServiceSpec.js create mode 100644 platform/entanglement/test/services/LinkServiceSpec.js create mode 100644 platform/entanglement/test/services/LocationServiceSpec.js create mode 100644 platform/entanglement/test/services/MockCopyService.js create mode 100644 platform/entanglement/test/services/MockLinkService.js create mode 100644 platform/entanglement/test/services/MockMoveService.js create mode 100644 platform/entanglement/test/services/MoveServiceSpec.js create mode 100644 platform/entanglement/test/suite.json 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..83057c62f1 --- /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": "m", + "category": "contextual", + "implementation": "actions/MoveAction.js", + "depends": ["locationService", "moveService"] + }, + { + "key": "copy", + "name": "Duplicate", + "description": "Duplicate object to another location.", + "glyph": "c", + "category": "contextual", + "implementation": "actions/CopyAction.js", + "depends": ["locationService", "copyService"] + }, + { + "key": "link", + "name": "Create Link", + "description": "Create Link to object in another location.", + "glyph": "l", + "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..1fbeda1a73 --- /dev/null +++ b/platform/entanglement/src/actions/CopyAction.js @@ -0,0 +1,63 @@ +/*global define */ +define( + function () { + "use strict"; + + 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..ed830c21a7 --- /dev/null +++ b/platform/entanglement/src/actions/LinkAction.js @@ -0,0 +1,61 @@ +/*global define */ +define( + function () { + "use strict"; + + 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..a7b8c9771c --- /dev/null +++ b/platform/entanglement/src/actions/MoveAction.js @@ -0,0 +1,62 @@ +/*global define */ +define( + function () { + "use strict"; + + 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..d7a9baa868 --- /dev/null +++ b/platform/entanglement/src/services/CopyService.js @@ -0,0 +1,79 @@ +/*global define */ + +define( + function () { + "use strict"; + + 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", + object.getCapability('type'), + parentCandidate.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..8c87f106b4 --- /dev/null +++ b/platform/entanglement/src/services/LinkService.js @@ -0,0 +1,49 @@ +/*global define */ + +define( + function () { + "use strict"; + + 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", + object.getCapability('type'), + parentCandidate.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..59f557acfc --- /dev/null +++ b/platform/entanglement/src/services/LocationService.js @@ -0,0 +1,61 @@ +/*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..4ca1dc8742 --- /dev/null +++ b/platform/entanglement/src/services/MoveService.js @@ -0,0 +1,56 @@ +/*global define */ + +define( + function () { + "use strict"; + + 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", + object.getCapability('type'), + parentCandidate.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..e1f681b26c --- /dev/null +++ b/platform/entanglement/test/DomainObjectFactory.js @@ -0,0 +1,136 @@ +/*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..c382e58507 --- /dev/null +++ b/platform/entanglement/test/actions/CopyActionSpec.js @@ -0,0 +1,152 @@ +/*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..e86964186d --- /dev/null +++ b/platform/entanglement/test/actions/LinkActionSpec.js @@ -0,0 +1,152 @@ +/*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..deb5514fa6 --- /dev/null +++ b/platform/entanglement/test/actions/MoveActionSpec.js @@ -0,0 +1,152 @@ +/*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..e8f2528b12 --- /dev/null +++ b/platform/entanglement/test/services/CopyServiceSpec.js @@ -0,0 +1,235 @@ +/*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' + }); + parentCandidate = domainObjectFactory({ + name: '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("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..64c284cc49 --- /dev/null +++ b/platform/entanglement/test/services/LinkServiceSpec.js @@ -0,0 +1,148 @@ +/*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'; + parentCandidate.id = 'xyz'; + parentCandidate.model.composition = []; + }); + + 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..0fe194edff --- /dev/null +++ b/platform/entanglement/test/services/LocationServiceSpec.js @@ -0,0 +1,133 @@ +/*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'] + ); + // TODO: replace the dialogServicePromise with a deferred and + // get rid of chainedPromise. + 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 () { + // This test is testing that chainedPromise is returned. + // TODO: have better assumptions with deferred objects. + 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..cdd52920dd --- /dev/null +++ b/platform/entanglement/test/services/MockCopyService.js @@ -0,0 +1,78 @@ +/*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; + + // TODO: return a proper deferred to support composing. + 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..cb406464a3 --- /dev/null +++ b/platform/entanglement/test/services/MockLinkService.js @@ -0,0 +1,78 @@ +/*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; + + // TODO: return a proper deferred to support composing. + 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..375da8874f --- /dev/null +++ b/platform/entanglement/test/services/MockMoveService.js @@ -0,0 +1,78 @@ +/*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; + + // TODO: return a proper deferred to support composing. + 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..7a3e330a4c --- /dev/null +++ b/platform/entanglement/test/services/MoveServiceSpec.js @@ -0,0 +1,154 @@ +/*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 + } + }); + + currentParent = domainObjectFactory({ + name: 'currentParent', + id: 'b' + }); + + objectContextCapability.getParent.andReturn(currentParent); + + parentCandidate = domainObjectFactory({ + name: 'parentCandidate', + model: {composition: []}, + id: 'c' + }); + + 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("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..03d59f8d6c --- /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" +] \ No newline at end of file