Merge remote-tracking branch 'github-open/open97' into open-master

This commit is contained in:
Pete Richards 2015-09-23 13:44:48 -07:00
commit 70bc17c79f
18 changed files with 467 additions and 199 deletions

View File

@ -86,37 +86,18 @@ define(
// composition, so that it will subsequently appear // composition, so that it will subsequently appear
// as a child contained by that parent. // as a child contained by that parent.
function addToComposition(id, parent, parentPersistence) { function addToComposition(id, parent, parentPersistence) {
var mutatationResult = parent.useCapability("mutation", function (model) { var compositionCapability = parent.getCapability('composition'),
if (Array.isArray(model.composition)) { addResult = compositionCapability &&
// Don't add if the id is already there compositionCapability.add(id);
if (model.composition.indexOf(id) === -1) {
model.composition.push(id);
}
} else {
// This is abnormal; composition should be an array
self.$log.warn(NO_COMPOSITION_WARNING + parent.getId());
return false; // Cancel mutation
}
});
return self.$q.when(mutatationResult).then(function (result) { return self.$q.when(addResult).then(function (result) {
if (!result) { if (!result) {
self.$log.error("Could not mutate " + parent.getId()); self.$log.error("Could not modify " + parent.getId());
return undefined; return undefined;
} }
return parentPersistence.persist().then(function () { return parentPersistence.persist().then(function () {
// Locate and return new Object in context of parent. return result;
return parent
.useCapability('composition')
.then(function (children) {
var i;
for (i = 0; i < children.length; i += 1) {
if (children[i].getId() === id) {
return children[i];
}
}
});
}); });
}); });
} }

View File

@ -86,7 +86,7 @@ define(
); );
mockCompositionCapability = jasmine.createSpyObj( mockCompositionCapability = jasmine.createSpyObj(
"composition", "composition",
["invoke"] ["invoke", "add"]
); );
mockContextCapability = jasmine.createSpyObj( mockContextCapability = jasmine.createSpyObj(
"context", "context",
@ -120,6 +120,7 @@ define(
mockCompositionCapability.invoke.andReturn( mockCompositionCapability.invoke.andReturn(
mockPromise([mockNewObject]) mockPromise([mockNewObject])
); );
mockCompositionCapability.add.andReturn(mockPromise(true));
creationService = new CreationService( creationService = new CreationService(
mockPersistenceService, mockPersistenceService,
@ -143,33 +144,34 @@ define(
parentModel = { composition: ["notAnyUUID"] }; parentModel = { composition: ["notAnyUUID"] };
creationService.createObject(model, mockParentObject); creationService.createObject(model, mockParentObject);
// Invoke the mutation callback // Verify that a new ID was added
expect(mockMutationCapability.invoke).toHaveBeenCalled(); expect(mockCompositionCapability.add)
mockMutationCapability.invoke.mostRecentCall.args[0](parentModel); .toHaveBeenCalledWith(jasmine.any(String));
// Should have a longer composition now, with the new UUID
expect(parentModel.composition.length).toEqual(2);
}); });
it("warns if parent has no composition", function () { it("provides the newly-created object", function () {
var model = { someKey: "some value" }, var mockDomainObject = jasmine.createSpyObj(
parentModel = { }; 'newDomainObject',
creationService.createObject(model, mockParentObject); ['getId', 'getModel', 'getCapability']
),
mockCallback = jasmine.createSpy('callback');
// Verify precondition; no prior warnings // Act as if the object had been created
expect(mockLog.warn).not.toHaveBeenCalled(); mockCompositionCapability.add.andCallFake(function (id) {
mockDomainObject.getId.andReturn(id);
mockCompositionCapability.invoke
.andReturn(mockPromise([mockDomainObject]));
return mockPromise(mockDomainObject);
});
// Invoke the mutation callback // Should find it in the composition
expect(mockMutationCapability.invoke).toHaveBeenCalled(); creationService.createObject({}, mockParentObject)
mockMutationCapability.invoke.mostRecentCall.args[0](parentModel); .then(mockCallback);
expect(mockCallback).toHaveBeenCalledWith(mockDomainObject);
// Should have a longer composition now, with the new UUID
expect(mockLog.warn).toHaveBeenCalled();
// Composition should still be undefined
expect(parentModel.composition).toBeUndefined();
}); });
it("warns if parent has no persistence capability", function () { it("warns if parent has no persistence capability", function () {
// Callbacks // Callbacks
var success = jasmine.createSpy("success"), var success = jasmine.createSpy("success"),
@ -185,7 +187,6 @@ define(
expect(mockLog.warn).toHaveBeenCalled(); expect(mockLog.warn).toHaveBeenCalled();
expect(success).not.toHaveBeenCalled(); expect(success).not.toHaveBeenCalled();
expect(failure).toHaveBeenCalled(); expect(failure).toHaveBeenCalled();
}); });
it("logs an error when mutaton fails", function () { it("logs an error when mutaton fails", function () {
@ -194,7 +195,7 @@ define(
var model = { someKey: "some value" }, var model = { someKey: "some value" },
parentModel = { composition: ["notAnyUUID"] }; parentModel = { composition: ["notAnyUUID"] };
mockMutationCapability.invoke.andReturn(mockPromise(false)); mockCompositionCapability.add.andReturn(mockPromise(false));
creationService.createObject(model, mockParentObject); creationService.createObject(model, mockParentObject);

View File

@ -36,20 +36,11 @@ define(
function LinkAction(context) { function LinkAction(context) {
this.domainObject = (context || {}).domainObject; this.domainObject = (context || {}).domainObject;
this.selectedObject = (context || {}).selectedObject; this.selectedObject = (context || {}).selectedObject;
this.selectedId = this.selectedObject && this.selectedObject.getId();
} }
LinkAction.prototype.perform = function () { LinkAction.prototype.perform = function () {
var self = this; var self = this;
// Add this domain object's identifier
function addId(model) {
if (Array.isArray(model.composition) &&
model.composition.indexOf(self.selectedId) < 0) {
model.composition.push(self.selectedId);
}
}
// Persist changes to the domain object // Persist changes to the domain object
function doPersist() { function doPersist() {
var persistence = var persistence =
@ -59,11 +50,13 @@ define(
// Link these objects // Link these objects
function doLink() { function doLink() {
return self.domainObject.useCapability("mutation", addId) var composition = self.domainObject &&
.then(doPersist); self.domainObject.getCapability('composition');
return composition && composition.add(self.selectedObject)
.then(doPersist);
} }
return this.selectedId && doLink(); return this.selectedObject && doLink();
}; };
return LinkAction; return LinkAction;

View File

@ -54,6 +54,9 @@ define(
var row = Object.create(property.getDefinition()); var row = Object.create(property.getDefinition());
row.key = index; row.key = index;
return row; return row;
}).filter(function (row) {
// Only show properties which are editable
return row.control;
}) })
}] }]
}; };

View File

@ -31,7 +31,7 @@ define(
mockDomainObject, mockDomainObject,
mockParent, mockParent,
mockContext, mockContext,
mockMutation, mockComposition,
mockPersistence, mockPersistence,
mockType, mockType,
actionContext, actionContext,
@ -67,7 +67,7 @@ define(
} }
}; };
mockContext = jasmine.createSpyObj("context", [ "getParent" ]); mockContext = jasmine.createSpyObj("context", [ "getParent" ]);
mockMutation = jasmine.createSpyObj("mutation", [ "invoke" ]); mockComposition = jasmine.createSpyObj("composition", [ "invoke", "add" ]);
mockPersistence = jasmine.createSpyObj("persistence", [ "persist" ]); mockPersistence = jasmine.createSpyObj("persistence", [ "persist" ]);
mockType = jasmine.createSpyObj("type", [ "hasFeature" ]); mockType = jasmine.createSpyObj("type", [ "hasFeature" ]);
@ -75,11 +75,11 @@ define(
mockDomainObject.getCapability.andReturn(mockContext); mockDomainObject.getCapability.andReturn(mockContext);
mockContext.getParent.andReturn(mockParent); mockContext.getParent.andReturn(mockParent);
mockType.hasFeature.andReturn(true); mockType.hasFeature.andReturn(true);
mockMutation.invoke.andReturn(mockPromise(true)); mockComposition.invoke.andReturn(mockPromise(true));
mockComposition.add.andReturn(mockPromise(true));
capabilities = { capabilities = {
mutation: mockMutation, composition: mockComposition,
persistence: mockPersistence, persistence: mockPersistence,
type: mockType type: mockType
}; };
@ -96,33 +96,17 @@ define(
}); });
it("mutates the parent when performed", function () { it("adds to the parent's composition when performed", function () {
action.perform(); action.perform();
expect(mockMutation.invoke) expect(mockComposition.add)
.toHaveBeenCalledWith(jasmine.any(Function)); .toHaveBeenCalledWith(mockDomainObject);
}); });
it("changes composition from its mutation function", function () { it("persists changes afterward", function () {
var mutator, result;
action.perform(); action.perform();
mutator = mockMutation.invoke.mostRecentCall.args[0];
result = mutator(model);
// Should not have cancelled the mutation
expect(result).not.toBe(false);
// Simulate mutate's behavior (remove can either return a
// new model or modify this one in-place)
result = result || model;
// Should have removed "test" - that was our
// mock domain object's id.
expect(result.composition).toEqual(["a", "b", "c", "test"]);
// Finally, should have persisted
expect(mockPersistence.persist).toHaveBeenCalled(); expect(mockPersistence.persist).toHaveBeenCalled();
}); });
}); });
} }
); );

View File

@ -39,7 +39,7 @@ define(
return { return {
getValue: function (model) { return model[k]; }, getValue: function (model) { return model[k]; },
setValue: function (model, v) { model[k] = v; }, setValue: function (model, v) { model[k] = v; },
getDefinition: function () { return {}; } getDefinition: function () { return { control: 'textfield '}; }
}; };
}); });

View File

@ -63,7 +63,12 @@
"provides": "modelService", "provides": "modelService",
"type": "provider", "type": "provider",
"implementation": "models/PersistedModelProvider.js", "implementation": "models/PersistedModelProvider.js",
"depends": [ "persistenceService", "$q", "PERSISTENCE_SPACE" ] "depends": [
"persistenceService",
"$q",
"PERSISTENCE_SPACE",
"ADDITIONAL_PERSISTENCE_SPACES"
]
}, },
{ {
"provides": "modelService", "provides": "modelService",
@ -218,6 +223,17 @@
"composition": [] "composition": []
} }
} }
],
"constants": [
{
"key": "PERSISTENCE_SPACE",
"value": "mct"
},
{
"key": "ADDITIONAL_PERSISTENCE_SPACES",
"value": [],
"description": "An array of additional persistence spaces to load models from."
}
] ]
} }
} }

View File

@ -50,6 +50,66 @@ define(
this.domainObject = domainObject; this.domainObject = domainObject;
} }
/**
* Add a domain object to the composition of the field.
* This mutates but does not persist the modified object.
*
* If no index is given, this is added to the end of the composition.
*
* @param {DomainObject|string} domainObject the domain object to add,
* or simply its identifier
* @param {number} [index] the index at which to add the object
* @returns {Promise.<DomainObject>} a promise for the added object
* in its new context
*/
CompositionCapability.prototype.add = function (domainObject, index) {
var self = this,
id = typeof domainObject === 'string' ?
domainObject : domainObject.getId(),
model = self.domainObject.getModel(),
composition = model.composition,
oldIndex = composition.indexOf(id);
// Find the object with the above id, used to contextualize
function findObject(objects) {
var i;
for (i = 0; i < objects.length; i += 1) {
if (objects[i].getId() === id) {
return objects[i];
}
}
}
function contextualize(mutationResult) {
return mutationResult && self.invoke().then(findObject);
}
function addIdToModel(model) {
// Pick a specific index if needed.
index = isNaN(index) ? composition.length : index;
// Also, don't put past the end of the array
index = Math.min(composition.length, index);
// Remove the existing instance of the id
if (oldIndex !== -1) {
model.composition.splice(oldIndex, 1);
}
// ...and add it back at the appropriate index.
model.composition.splice(index, 0, id);
}
// If no index has been specified already and the id is already
// present, nothing to do. If the id is already at that index,
// also nothing to do, so cancel mutation.
if ((isNaN(index) && oldIndex !== -1) || (index === oldIndex)) {
return contextualize(true);
}
return this.domainObject.useCapability('mutation', addIdToModel)
.then(contextualize);
};
/** /**
* Request the composition of this object. * Request the composition of this object.
* @returns {Promise.<DomainObject[]>} a list of all domain * @returns {Promise.<DomainObject[]>} a list of all domain

View File

@ -39,23 +39,37 @@ define(
* @param {PersistenceService} persistenceService the service in which * @param {PersistenceService} persistenceService the service in which
* domain object models are persisted. * domain object models are persisted.
* @param $q Angular's $q service, for working with promises * @param $q Angular's $q service, for working with promises
* @param {string} SPACE the name of the persistence space from which * @param {string} space the name of the persistence space(s)
* models should be retrieved. * from which models should be retrieved.
* @param {string} spaces additional persistence spaces to use
*/ */
function PersistedModelProvider(persistenceService, $q, space) { function PersistedModelProvider(persistenceService, $q, space, spaces) {
this.persistenceService = persistenceService; this.persistenceService = persistenceService;
this.$q = $q; this.$q = $q;
this.space = space; this.spaces = [space].concat(spaces || []);
}
// Take the most recently modified model, for cases where
// multiple persistence spaces return models.
function takeMostRecent(modelA, modelB) {
return (!modelB || modelB.modified === undefined) ? modelA :
(!modelA || modelA.modified === undefined) ? modelB :
modelB.modified > modelA.modified ? modelB :
modelA;
} }
PersistedModelProvider.prototype.getModels = function (ids) { PersistedModelProvider.prototype.getModels = function (ids) {
var persistenceService = this.persistenceService, var persistenceService = this.persistenceService,
$q = this.$q, $q = this.$q,
space = this.space; spaces = this.spaces;
// Load a single object model from persistence // Load a single object model from any persistence spaces
function loadModel(id) { function loadModel(id) {
return persistenceService.readObject(space, id); return $q.all(spaces.map(function (space) {
return persistenceService.readObject(space, id);
})).then(function (models) {
return models.reduce(takeMostRecent);
});
} }
// Package the result as id->model // Package the result as id->model

View File

@ -51,7 +51,7 @@ define(
// so support that, but don't introduce complication of // so support that, but don't introduce complication of
// native promises. // native promises.
function mockPromise(value) { function mockPromise(value) {
return { return (value || {}).then ? value : {
then: function (callback) { then: function (callback) {
return mockPromise(callback(value)); return mockPromise(callback(value));
} }
@ -123,6 +123,98 @@ define(
}); });
it("allows domain objects to be added", function () {
var result,
testModel = { composition: [] },
mockChild = jasmine.createSpyObj("child", DOMAIN_OBJECT_METHODS);
mockDomainObject.getModel.andReturn(testModel);
mockObjectService.getObjects.andReturn(mockPromise({a: mockChild}));
mockChild.getCapability.andReturn(undefined);
mockChild.getId.andReturn('a');
mockDomainObject.useCapability.andCallFake(function (key, mutator) {
if (key === 'mutation') {
mutator(testModel);
return mockPromise(true);
}
});
composition.add(mockChild).then(function (domainObject) {
result = domainObject;
});
expect(testModel.composition).toEqual(['a']);
// Should have returned the added object in its new context
expect(result.getId()).toEqual('a');
expect(result.getCapability('context')).toBeDefined();
expect(result.getCapability('context').getParent())
.toEqual(mockDomainObject);
});
it("does not re-add IDs which are already present", function () {
var result,
testModel = { composition: [ 'a' ] },
mockChild = jasmine.createSpyObj("child", DOMAIN_OBJECT_METHODS);
mockDomainObject.getModel.andReturn(testModel);
mockObjectService.getObjects.andReturn(mockPromise({a: mockChild}));
mockChild.getCapability.andReturn(undefined);
mockChild.getId.andReturn('a');
mockDomainObject.useCapability.andCallFake(function (key, mutator) {
if (key === 'mutation') {
mutator(testModel);
return mockPromise(true);
}
});
composition.add(mockChild).then(function (domainObject) {
result = domainObject;
});
// Still just 'a'
expect(testModel.composition).toEqual(['a']);
// Should have returned the added object in its new context
expect(result.getId()).toEqual('a');
expect(result.getCapability('context')).toBeDefined();
expect(result.getCapability('context').getParent())
.toEqual(mockDomainObject);
});
it("can add objects at a specified index", function () {
var result,
testModel = { composition: [ 'a', 'b', 'c' ] },
mockChild = jasmine.createSpyObj("child", DOMAIN_OBJECT_METHODS);
mockDomainObject.getModel.andReturn(testModel);
mockObjectService.getObjects.andReturn(mockPromise({a: mockChild}));
mockChild.getCapability.andReturn(undefined);
mockChild.getId.andReturn('a');
mockDomainObject.useCapability.andCallFake(function (key, mutator) {
if (key === 'mutation') {
mutator(testModel);
return mockPromise(true);
}
});
composition.add(mockChild, 1).then(function (domainObject) {
result = domainObject;
});
// Still just 'a'
expect(testModel.composition).toEqual(['b', 'a', 'c']);
// Should have returned the added object in its new context
expect(result.getId()).toEqual('a');
expect(result.getCapability('context')).toBeDefined();
expect(result.getCapability('context').getParent())
.toEqual(mockDomainObject);
});
}); });
} }
); );

View File

@ -32,7 +32,9 @@ define(
describe("The persisted model provider", function () { describe("The persisted model provider", function () {
var mockQ, var mockQ,
mockPersistenceService, mockPersistenceService,
SPACE = "some space", SPACE = "space0",
spaces = [ "space1" ],
modTimes,
provider; provider;
function mockPromise(value) { function mockPromise(value) {
@ -51,12 +53,14 @@ define(
} }
beforeEach(function () { beforeEach(function () {
modTimes = {};
mockQ = { when: mockPromise, all: mockAll }; mockQ = { when: mockPromise, all: mockAll };
mockPersistenceService = { mockPersistenceService = {
readObject: function (space, id) { readObject: function (space, id) {
return mockPromise({ return mockPromise({
space: space, space: space,
id: id id: id,
modified: (modTimes[space] || {})[id]
}); });
} }
}; };
@ -64,7 +68,8 @@ define(
provider = new PersistedModelProvider( provider = new PersistedModelProvider(
mockPersistenceService, mockPersistenceService,
mockQ, mockQ,
SPACE SPACE,
spaces
); );
}); });
@ -82,6 +87,24 @@ define(
}); });
}); });
it("reads object models from multiple spaces", function () {
var models;
modTimes.space1 = {
'x': 12321
};
provider.getModels(["a", "x", "zz"]).then(function (m) {
models = m;
});
expect(models).toEqual({
a: { space: SPACE, id: "a" },
x: { space: 'space1', id: "x", modified: 12321 },
zz: { space: SPACE, id: "zz" }
});
});
}); });
} }
); );

View File

@ -32,7 +32,8 @@ define(
* @private * @private
*/ */
/** /**
* Change the composition of the specified objects. * Change the composition of the specified objects. Note that this
* should only be invoked after successfully validating.
* *
* @param {DomainObject} domainObject the domain object to * @param {DomainObject} domainObject the domain object to
* move, copy, or link. * move, copy, or link.
@ -43,7 +44,8 @@ define(
* @method platform/entanglement.AbstractComposeService#perform * @method platform/entanglement.AbstractComposeService#perform
*/ */
/** /**
* Check if one object can be composed into another. * Check if this composition change is valid for these objects.
*
* @param {DomainObject} domainObject the domain object to * @param {DomainObject} domainObject the domain object to
* move, copy, or link. * move, copy, or link.
* @param {DomainObject} parent the domain object whose composition * @param {DomainObject} parent the domain object whose composition

View File

@ -64,6 +64,12 @@ define(
return self.perform(domainObject, parent); return self.perform(domainObject, parent);
} }
if (!this.validate(domainObject, parent)) {
throw new Error(
"Tried to copy objects without validating first."
);
}
if (domainObject.hasCapability('composition')) { if (domainObject.hasCapability('composition')) {
model.composition = []; model.composition = [];
} }

View File

@ -45,6 +45,9 @@ define(
if (parentCandidate.getId() === object.getId()) { if (parentCandidate.getId() === object.getId()) {
return false; return false;
} }
if (!parentCandidate.hasCapability('composition')) {
return false;
}
if (parentCandidate.getModel().composition.indexOf(object.getId()) !== -1) { if (parentCandidate.getModel().composition.indexOf(object.getId()) !== -1) {
return false; return false;
} }
@ -56,26 +59,18 @@ define(
}; };
LinkService.prototype.perform = function (object, parentObject) { LinkService.prototype.perform = function (object, parentObject) {
function findChild(children) { if (!this.validate(object, parentObject)) {
var i; throw new Error(
for (i = 0; i < children.length; i += 1) { "Tried to link objects without validating first."
if (children[i].getId() === object.getId()) { );
return children[i];
}
}
} }
return parentObject.useCapability('mutation', function (model) { return parentObject.getCapability('composition').add(object)
if (model.composition.indexOf(object.getId()) === -1) { .then(function (objectInNewContext) {
model.composition.push(object.getId()); return parentObject.getCapability('persistence')
} .persist()
}).then(function () { .then(function () { return objectInNewContext; });
return parentObject.getCapability('persistence').persist(); });
}).then(function getObjectWithNewContext() {
return parentObject
.useCapability('composition')
.then(findChild);
});
}; };
return LinkService; return LinkService;

View File

@ -82,6 +82,12 @@ define(
} }
} }
if (!this.validate(object, parentObject)) {
throw new Error(
"Tried to move objects without validating first."
);
}
return this.linkService return this.linkService
.perform(object, parentObject) .perform(object, parentObject)
.then(relocate) .then(relocate)

View File

@ -41,19 +41,23 @@ define(
} }
describe("CopyService", function () { describe("CopyService", function () {
var policyService;
beforeEach(function () {
policyService = jasmine.createSpyObj(
'policyService',
['allow']
);
});
describe("validate", function () { describe("validate", function () {
var policyService, var copyService,
copyService,
object, object,
parentCandidate, parentCandidate,
validate; validate;
beforeEach(function () { beforeEach(function () {
policyService = jasmine.createSpyObj(
'policyService',
['allow']
);
copyService = new CopyService( copyService = new CopyService(
null, null,
null, null,
@ -126,6 +130,16 @@ define(
copyResult, copyResult,
copyFinished; copyFinished;
beforeEach(function () {
creationService = jasmine.createSpyObj(
'creationService',
['createObject']
);
createObjectPromise = synchronousPromise(undefined);
creationService.createObject.andReturn(createObjectPromise);
policyService.allow.andReturn(true);
});
describe("on domain object without composition", function () { describe("on domain object without composition", function () {
beforeEach(function () { beforeEach(function () {
object = domainObjectFactory({ object = domainObjectFactory({
@ -142,13 +156,7 @@ define(
composition: [] composition: []
} }
}); });
creationService = jasmine.createSpyObj( copyService = new CopyService(null, creationService, policyService);
'creationService',
['createObject']
);
createObjectPromise = synchronousPromise(undefined);
creationService.createObject.andReturn(createObjectPromise);
copyService = new CopyService(null, creationService);
copyResult = copyService.perform(object, newParent); copyResult = copyService.perform(object, newParent);
copyFinished = jasmine.createSpy('copyFinished'); copyFinished = jasmine.createSpy('copyFinished');
copyResult.then(copyFinished); copyResult.then(copyFinished);
@ -180,7 +188,8 @@ define(
}); });
describe("on domainObject with composition", function () { describe("on domainObject with composition", function () {
var childObject, var newObject,
childObject,
compositionCapability, compositionCapability,
compositionPromise; compositionPromise;
@ -216,6 +225,17 @@ define(
composition: compositionCapability composition: compositionCapability
} }
}); });
newObject = domainObjectFactory({
name: 'object',
id: 'abc2',
model: {
name: 'some object',
composition: []
},
capabilities: {
composition: compositionCapability
}
});
newParent = domainObjectFactory({ newParent = domainObjectFactory({
name: 'newParent', name: 'newParent',
id: '456', id: '456',
@ -223,13 +243,10 @@ define(
composition: [] composition: []
} }
}); });
creationService = jasmine.createSpyObj(
'creationService', createObjectPromise = synchronousPromise(newObject);
['createObject']
);
createObjectPromise = synchronousPromise(undefined);
creationService.createObject.andReturn(createObjectPromise); creationService.createObject.andReturn(createObjectPromise);
copyService = new CopyService(mockQ, creationService); copyService = new CopyService(mockQ, creationService, policyService);
copyResult = copyService.perform(object, newParent); copyResult = copyService.perform(object, newParent);
copyFinished = jasmine.createSpy('copyFinished'); copyFinished = jasmine.createSpy('copyFinished');
copyResult.then(copyFinished); copyResult.then(copyFinished);
@ -266,6 +283,38 @@ define(
}); });
}); });
describe("on invalid inputs", function () {
beforeEach(function () {
object = domainObjectFactory({
name: 'object',
capabilities: {
type: { type: 'object' }
}
});
newParent = domainObjectFactory({
name: 'parentCandidate',
capabilities: {
type: { type: 'parentCandidate' }
}
});
});
it("throws an error", function () {
var copyService =
new CopyService(mockQ, creationService, policyService);
function perform() {
copyService.perform(object, newParent);
}
spyOn(copyService, "validate");
copyService.validate.andReturn(true);
expect(perform).not.toThrow();
copyService.validate.andReturn(false);
expect(perform).toThrow();
});
});
}); });
}); });
} }

View File

@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
/*global define,describe,beforeEach,it,jasmine,expect */ /*global define,describe,beforeEach,it,jasmine,expect,spyOn */
define( define(
[ [
@ -41,6 +41,7 @@ define(
'policyService', 'policyService',
['allow'] ['allow']
); );
mockPolicyService.allow.andReturn(true);
linkService = new LinkService(mockPolicyService); linkService = new LinkService(mockPolicyService);
}); });
@ -55,7 +56,13 @@ define(
name: 'object' name: 'object'
}); });
parentCandidate = domainObjectFactory({ parentCandidate = domainObjectFactory({
name: 'parentCandidate' name: 'parentCandidate',
capabilities: {
composition: jasmine.createSpyObj(
'composition',
['invoke', 'add']
)
}
}); });
validate = function () { validate = function () {
return linkService.validate(object, parentCandidate); return linkService.validate(object, parentCandidate);
@ -81,6 +88,18 @@ define(
expect(validate()).toBe(false); expect(validate()).toBe(false);
}); });
it("does not allow parents without composition", function () {
parentCandidate = domainObjectFactory({
name: 'parentCandidate'
});
object.id = 'abc';
parentCandidate.id = 'xyz';
parentCandidate.hasCapability.andCallFake(function (c) {
return c !== 'composition';
});
expect(validate()).toBe(false);
});
describe("defers to policyService", function () { describe("defers to policyService", function () {
beforeEach(function () { beforeEach(function () {
object.id = 'abc'; object.id = 'abc';
@ -121,16 +140,16 @@ define(
linkedObject, linkedObject,
parentModel, parentModel,
parentObject, parentObject,
mutationPromise,
compositionPromise, compositionPromise,
persistencePromise, persistencePromise,
addPromise,
compositionCapability, compositionCapability,
persistenceCapability; persistenceCapability;
beforeEach(function () { beforeEach(function () {
mutationPromise = new ControlledPromise();
compositionPromise = new ControlledPromise(); compositionPromise = new ControlledPromise();
persistencePromise = new ControlledPromise(); persistencePromise = new ControlledPromise();
addPromise = new ControlledPromise();
persistenceCapability = jasmine.createSpyObj( persistenceCapability = jasmine.createSpyObj(
'persistenceCapability', 'persistenceCapability',
['persist'] ['persist']
@ -138,9 +157,10 @@ define(
persistenceCapability.persist.andReturn(persistencePromise); persistenceCapability.persist.andReturn(persistencePromise);
compositionCapability = jasmine.createSpyObj( compositionCapability = jasmine.createSpyObj(
'compositionCapability', 'compositionCapability',
['invoke'] ['invoke', 'add']
); );
compositionCapability.invoke.andReturn(compositionPromise); compositionCapability.invoke.andReturn(compositionPromise);
compositionCapability.add.andReturn(addPromise);
parentModel = { parentModel = {
composition: [] composition: []
}; };
@ -151,7 +171,7 @@ define(
mutation: { mutation: {
invoke: function (mutator) { invoke: function (mutator) {
mutator(parentModel); mutator(parentModel);
return mutationPromise; return new ControlledPromise();
} }
}, },
persistence: persistenceCapability, persistence: persistenceCapability,
@ -172,20 +192,17 @@ define(
}); });
it("modifies parent model composition", function () { it("adds to the parent's composition", function () {
expect(parentModel.composition.length).toBe(0); expect(compositionCapability.add).not.toHaveBeenCalled();
linkService.perform(object, parentObject); linkService.perform(object, parentObject);
expect(parentObject.useCapability).toHaveBeenCalledWith( expect(compositionCapability.add)
'mutation', .toHaveBeenCalledWith(object);
jasmine.any(Function)
);
expect(parentModel.composition).toContain('xyz');
}); });
it("persists parent", function () { it("persists parent", function () {
linkService.perform(object, parentObject); linkService.perform(object, parentObject);
expect(mutationPromise.then).toHaveBeenCalled(); expect(addPromise.then).toHaveBeenCalled();
mutationPromise.resolve(); addPromise.resolve(linkedObject);
expect(parentObject.getCapability) expect(parentObject.getCapability)
.toHaveBeenCalledWith('persistence'); .toHaveBeenCalledWith('persistence');
expect(persistenceCapability.persist).toHaveBeenCalled(); expect(persistenceCapability.persist).toHaveBeenCalled();
@ -197,11 +214,23 @@ define(
whenComplete = jasmine.createSpy('whenComplete'); whenComplete = jasmine.createSpy('whenComplete');
returnPromise.then(whenComplete); returnPromise.then(whenComplete);
mutationPromise.resolve(); addPromise.resolve(linkedObject);
persistencePromise.resolve(); persistencePromise.resolve();
compositionPromise.resolve([linkedObject]); compositionPromise.resolve([linkedObject]);
expect(whenComplete).toHaveBeenCalledWith(linkedObject); expect(whenComplete).toHaveBeenCalledWith(linkedObject);
}); });
it("throws an error when performed on invalid inputs", function () {
function perform() {
linkService.perform(object, parentObject);
}
spyOn(linkService, 'validate');
linkService.validate.andReturn(true);
expect(perform).not.toThrow();
linkService.validate.andReturn(false);
expect(perform).toThrow();
});
}); });
}); });
} }

View File

@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
/*global define,describe,beforeEach,it,jasmine,expect */ /*global define,describe,beforeEach,it,jasmine,expect,spyOn */
define( define(
[ [
'../../src/services/MoveService', '../../src/services/MoveService',
@ -40,58 +40,57 @@ define(
var moveService, var moveService,
policyService, policyService,
object,
objectContextCapability,
currentParent,
parentCandidate,
linkService; linkService;
beforeEach(function () { beforeEach(function () {
objectContextCapability = jasmine.createSpyObj(
'objectContextCapability',
[
'getParent'
]
);
object = domainObjectFactory({
name: 'object',
id: 'a',
capabilities: {
context: objectContextCapability,
type: { type: 'object' }
}
});
currentParent = domainObjectFactory({
name: 'currentParent',
id: 'b'
});
objectContextCapability.getParent.andReturn(currentParent);
parentCandidate = domainObjectFactory({
name: 'parentCandidate',
model: { composition: [] },
id: 'c',
capabilities: {
type: { type: 'parentCandidate' }
}
});
policyService = jasmine.createSpyObj( policyService = jasmine.createSpyObj(
'policyService', 'policyService',
['allow'] ['allow']
); );
linkService = new MockLinkService(); linkService = new MockLinkService();
policyService.allow.andReturn(true);
moveService = new MoveService(policyService, linkService); moveService = new MoveService(policyService, linkService);
}); });
describe("validate", function () { describe("validate", function () {
var object, var validate;
objectContextCapability,
currentParent,
parentCandidate,
validate;
beforeEach(function () { beforeEach(function () {
objectContextCapability = jasmine.createSpyObj(
'objectContextCapability',
[
'getParent'
]
);
object = domainObjectFactory({
name: 'object',
id: 'a',
capabilities: {
context: objectContextCapability,
type: { type: 'object' }
}
});
currentParent = domainObjectFactory({
name: 'currentParent',
id: 'b'
});
objectContextCapability.getParent.andReturn(currentParent);
parentCandidate = domainObjectFactory({
name: 'parentCandidate',
model: { composition: [] },
id: 'c',
capabilities: {
type: { type: 'parentCandidate' }
}
});
validate = function () { validate = function () {
return moveService.validate(object, parentCandidate); return moveService.validate(object, parentCandidate);
}; };
@ -145,14 +144,15 @@ define(
describe("perform", function () { describe("perform", function () {
var object, var actionCapability,
newParent,
actionCapability,
locationCapability, locationCapability,
locationPromise, locationPromise,
newParent,
moveResult; moveResult;
beforeEach(function () { beforeEach(function () {
newParent = parentCandidate;
actionCapability = jasmine.createSpyObj( actionCapability = jasmine.createSpyObj(
'actionCapability', 'actionCapability',
['perform'] ['perform']
@ -175,7 +175,9 @@ define(
name: 'object', name: 'object',
capabilities: { capabilities: {
action: actionCapability, action: actionCapability,
location: locationCapability location: locationCapability,
context: objectContextCapability,
type: { type: 'object' }
} }
}); });
@ -194,6 +196,18 @@ define(
.toHaveBeenCalledWith(jasmine.any(Function)); .toHaveBeenCalledWith(jasmine.any(Function));
}); });
it("throws an error when performed on invalid inputs", function () {
function perform() {
moveService.perform(object, newParent);
}
spyOn(moveService, "validate");
moveService.validate.andReturn(true);
expect(perform).not.toThrow();
moveService.validate.andReturn(false);
expect(perform).toThrow();
});
describe("when moving an original", function () { describe("when moving an original", function () {
beforeEach(function () { beforeEach(function () {
locationCapability.getContextualLocation locationCapability.getContextualLocation