mirror of
https://github.com/nasa/openmct.git
synced 2025-01-31 16:36:13 +00:00
[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.
This commit is contained in:
parent
8811f2a53e
commit
9123078293
24
platform/entanglement/README.md
Normal file
24
platform/entanglement/README.md
Normal file
@ -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.
|
75
platform/entanglement/bundle.json
Normal file
75
platform/entanglement/bundle.json
Normal file
@ -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": [
|
||||
]
|
||||
}
|
||||
}
|
63
platform/entanglement/src/actions/CopyAction.js
Normal file
63
platform/entanglement/src/actions/CopyAction.js
Normal file
@ -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;
|
||||
}
|
||||
);
|
61
platform/entanglement/src/actions/LinkAction.js
Normal file
61
platform/entanglement/src/actions/LinkAction.js
Normal file
@ -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;
|
||||
}
|
||||
);
|
62
platform/entanglement/src/actions/MoveAction.js
Normal file
62
platform/entanglement/src/actions/MoveAction.js
Normal file
@ -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;
|
||||
}
|
||||
);
|
79
platform/entanglement/src/services/CopyService.js
Normal file
79
platform/entanglement/src/services/CopyService.js
Normal file
@ -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;
|
||||
}
|
||||
);
|
49
platform/entanglement/src/services/LinkService.js
Normal file
49
platform/entanglement/src/services/LinkService.js
Normal file
@ -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;
|
||||
}
|
||||
);
|
61
platform/entanglement/src/services/LocationService.js
Normal file
61
platform/entanglement/src/services/LocationService.js
Normal file
@ -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;
|
||||
}
|
||||
);
|
56
platform/entanglement/src/services/MoveService.js
Normal file
56
platform/entanglement/src/services/MoveService.js
Normal file
@ -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;
|
||||
}
|
||||
);
|
136
platform/entanglement/test/DomainObjectFactory.js
Normal file
136
platform/entanglement/test/DomainObjectFactory.js
Normal file
@ -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;
|
||||
}
|
||||
);
|
152
platform/entanglement/test/actions/CopyActionSpec.js
Normal file
152
platform/entanglement/test/actions/CopyActionSpec.js
Normal file
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
152
platform/entanglement/test/actions/LinkActionSpec.js
Normal file
152
platform/entanglement/test/actions/LinkActionSpec.js
Normal file
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
152
platform/entanglement/test/actions/MoveActionSpec.js
Normal file
152
platform/entanglement/test/actions/MoveActionSpec.js
Normal file
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
235
platform/entanglement/test/services/CopyServiceSpec.js
Normal file
235
platform/entanglement/test/services/CopyServiceSpec.js
Normal file
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
148
platform/entanglement/test/services/LinkServiceSpec.js
Normal file
148
platform/entanglement/test/services/LinkServiceSpec.js
Normal file
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
133
platform/entanglement/test/services/LocationServiceSpec.js
Normal file
133
platform/entanglement/test/services/LocationServiceSpec.js
Normal file
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
78
platform/entanglement/test/services/MockCopyService.js
Normal file
78
platform/entanglement/test/services/MockCopyService.js
Normal file
@ -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;
|
||||
}
|
||||
);
|
78
platform/entanglement/test/services/MockLinkService.js
Normal file
78
platform/entanglement/test/services/MockLinkService.js
Normal file
@ -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;
|
||||
}
|
||||
);
|
78
platform/entanglement/test/services/MockMoveService.js
Normal file
78
platform/entanglement/test/services/MockMoveService.js
Normal file
@ -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;
|
||||
}
|
||||
);
|
154
platform/entanglement/test/services/MoveServiceSpec.js
Normal file
154
platform/entanglement/test/services/MoveServiceSpec.js
Normal file
@ -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');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
9
platform/entanglement/test/suite.json
Normal file
9
platform/entanglement/test/suite.json
Normal file
@ -0,0 +1,9 @@
|
||||
[
|
||||
"actions/CopyAction",
|
||||
"actions/LinkAction",
|
||||
"actions/MoveAction",
|
||||
"services/CopyService",
|
||||
"services/LinkService",
|
||||
"services/MoveService",
|
||||
"services/LocationService"
|
||||
]
|
Loading…
x
Reference in New Issue
Block a user