Merge branch 'master' of https://github.com/nasa/openmctweb into open72

Conflicts:
	platform/commonUI/general/res/css/theme-espresso.css
This commit is contained in:
slhale 2015-08-20 13:25:39 -07:00
commit dcfcfa74bb
23 changed files with 571 additions and 160 deletions

View File

@ -28,6 +28,7 @@
"start": "node app.js", "start": "node app.js",
"test": "karma start --single-run", "test": "karma start --single-run",
"jshint": "jshint platform example || exit 0", "jshint": "jshint platform example || exit 0",
"watch": "karma start",
"jsdoc": "jsdoc -c jsdoc.json -r -d target/docs/api", "jsdoc": "jsdoc -c jsdoc.json -r -d target/docs/api",
"otherdoc": "node docs/gendocs.js --in docs/src --out target/docs", "otherdoc": "node docs/gendocs.js --in docs/src --out target/docs",
"docs": "npm run jsdoc ; npm run otherdoc" "docs": "npm run jsdoc ; npm run otherdoc"

View File

@ -69,8 +69,8 @@
{ {
"key": "grid-item", "key": "grid-item",
"templateUrl": "templates/items/grid-item.html", "templateUrl": "templates/items/grid-item.html",
"uses": [ "type", "action" ], "uses": [ "type", "action", "location" ],
"gestures": [ "info","menu" ] "gestures": [ "info", "menu" ]
}, },
{ {
"key": "object-header", "key": "object-header",
@ -88,12 +88,12 @@
{ {
"key": "navigationService", "key": "navigationService",
"implementation": "navigation/NavigationService.js" "implementation": "navigation/NavigationService.js"
}, },
{ {
"key": "creationService", "key": "creationService",
"implementation": "creation/CreationService.js", "implementation": "creation/CreationService.js",
"depends": [ "persistenceService", "$q", "$log" ] "depends": [ "persistenceService", "$q", "$log" ]
} }
], ],
"actions": [ "actions": [
{ {

View File

@ -27,12 +27,18 @@
<mct-include key="_checkbox"></mct-include> <mct-include key="_checkbox"></mct-include>
</div> </div>
<div class='right abs'> <div class='right abs'>
<div class='ui-symbol icon alert hidden' onclick="alert('Not yet functional. When this is visible, it means that this object needs to be updated. Clicking will allow that action via a dialog.');">!</div> <div class='ui-symbol icon l-icon-alert'></div>
<div class='ui-symbol icon profile' title="Shared">P</div> <div class='ui-symbol icon profile' title="Shared">P</div>
</div> </div>
</div> </div>
<div class='item-main abs'> <div class='item-main abs'>
<div class='ui-symbol icon lg abs item-type'>{{type.getGlyph()}}</div> <div class='ui-symbol icon lg item-type'>
{{type.getGlyph()}}
<span
class="ui-symbol icon l-icon-link" title="This object is a link"
ng-show="location.isLink()"
></span>
</div>
<div class='ui-symbol icon abs item-open'>}</div> <div class='ui-symbol icon abs item-open'>}</div>
</div> </div>
<div class='bottom-bar bar abs'> <div class='bottom-bar bar abs'>
@ -44,4 +50,4 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -93,6 +93,12 @@ define(
}); });
} }
// Store the location of an object relative to it's parent.
function addLocationToModel(modelId, model, parent) {
model.location = parent.getId();
return model;
}
// Create a new domain object with the provided model as a // Create a new domain object with the provided model as a
// member of the specified parent's composition // member of the specified parent's composition
function createObject(model, parent) { function createObject(model, parent) {
@ -112,6 +118,7 @@ define(
return $q.when( return $q.when(
uuid() uuid()
).then(function (id) { ).then(function (id) {
model = addLocationToModel(id, model, parent);
return doPersist(persistence.getSpace(), id, model); return doPersist(persistence.getSpace(), id, model);
}).then(function (id) { }).then(function (id) {
return addToComposition(id, parent, persistence); return addToComposition(id, parent, persistence);

View File

@ -38,6 +38,7 @@ define(
mockMutationCapability, mockMutationCapability,
mockPersistenceCapability, mockPersistenceCapability,
mockCompositionCapability, mockCompositionCapability,
mockContextCapability,
mockCapabilities, mockCapabilities,
creationService; creationService;
@ -87,10 +88,15 @@ define(
"composition", "composition",
["invoke"] ["invoke"]
); );
mockContextCapability = jasmine.createSpyObj(
"context",
["getPath"]
);
mockCapabilities = { mockCapabilities = {
mutation: mockMutationCapability, mutation: mockMutationCapability,
persistence: mockPersistenceCapability, persistence: mockPersistenceCapability,
composition: mockCompositionCapability composition: mockCompositionCapability,
context: mockContextCapability
}; };
mockPersistenceService.createObject.andReturn( mockPersistenceService.createObject.andReturn(
@ -103,6 +109,7 @@ define(
mockParentObject.useCapability.andCallFake(function (key, value) { mockParentObject.useCapability.andCallFake(function (key, value) {
return mockCapabilities[key].invoke(value); return mockCapabilities[key].invoke(value);
}); });
mockParentObject.getId.andReturn('parentId');
mockPersistenceCapability.persist.andReturn( mockPersistenceCapability.persist.andReturn(
mockPromise(true) mockPromise(true)
@ -194,7 +201,16 @@ define(
expect(mockLog.error).toHaveBeenCalled(); expect(mockLog.error).toHaveBeenCalled();
}); });
it("stores location on new domainObjects", function () {
var model = { name: "my model" },
objectPromise = creationService.createObject(
model,
mockParentObject
);
expect(model.location).toBe('parentId');
});
}); });
} }
); );

View File

@ -196,7 +196,7 @@
{ {
"key": "label", "key": "label",
"templateUrl": "templates/label.html", "templateUrl": "templates/label.html",
"uses": [ "type" ], "uses": [ "type", "location" ],
"gestures": [ "drag", "menu", "info" ] "gestures": [ "drag", "menu", "info" ]
}, },
{ {

View File

@ -172,11 +172,11 @@ article, aside, details, figcaption, figure, footer, header, hgroup, main, menu,
/*********************************************** FORM ELEMENTS */ /*********************************************** FORM ELEMENTS */
/* /*
@mixin invokeMenu($baseColor: $colorBodyFg) { @mixin invokeMenu($baseColor: $colorBodyFg) {
$c: $baseColor; $c: $baseColor;
color: $c; color: $c;
&:hover { &:hover {
color: lighten($c, $ltGamma); color: lighten($c, $ltGamma);
} }
} }
*/ */
/***************************************************************************** /*****************************************************************************
@ -1049,27 +1049,27 @@ mct-container {
/*.s-limit-upr, /*.s-limit-upr,
.s-limit-lwr { .s-limit-lwr {
$a: 0.5; $a: 0.5;
$l: 30%; $l: 30%;
white-space: nowrap; white-space: nowrap;
&:before { &:before {
display: inline-block; display: inline-block;
font-family: symbolsfont; font-family: symbolsfont;
font-size: 0.85em; font-size: 0.85em;
font-style: normal !important; font-style: normal !important;
margin-right: $interiorMarginSm; margin-right: $interiorMarginSm;
vertical-align: middle; vertical-align: middle;
} }
} }
.s-limit-upr { .s-limit-upr {
&.s-limit-yellow { @include limit($colorLimitYellow, "\0000ed"); } &.s-limit-yellow { @include limit($colorLimitYellow, "\0000ed"); }
&.s-limit-red { @include limit($colorLimitRed, "\0000eb"); } &.s-limit-red { @include limit($colorLimitRed, "\0000eb"); }
} }
.s-limit-lwr { .s-limit-lwr {
&.s-limit-yellow { @include limit($colorLimitYellow, "\0000ec"); } &.s-limit-yellow { @include limit($colorLimitYellow, "\0000ec"); }
&.s-limit-red { @include limit($colorLimitRed, "\0000ee"); } &.s-limit-red { @include limit($colorLimitRed, "\0000ee"); }
}*/ }*/
/* line 35, ../sass/_limits.scss */ /* line 35, ../sass/_limits.scss */
[class*="s-limit"] { [class*="s-limit"] {
@ -1772,11 +1772,11 @@ table {
/* line 132, ../sass/controls/_buttons.scss */ /* line 132, ../sass/controls/_buttons.scss */
.icon-btn.pause-play, .icon-btn.pause-play,
.s-icon-btn.pause-play { .s-icon-btn.pause-play {
/* &.paused { /* &.paused {
.icon { .icon {
@include pulse(500ms); @include pulse(500ms);
} }
}*/ } }*/ }
/* line 138, ../sass/controls/_buttons.scss */ /* line 138, ../sass/controls/_buttons.scss */
.icon-btn.pause-play .icon:before, .icon-btn.pause-play .icon:before,
.s-icon-btn.pause-play .icon:before { .s-icon-btn.pause-play .icon:before {
@ -1899,32 +1899,32 @@ a.l-btn span {
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
/*.control { /*.control {
// UNUSED? // UNUSED?
&.view-control { &.view-control {
.icon { .icon {
display: inline-block; display: inline-block;
margin: -1px 5px 1px 2px; margin: -1px 5px 1px 2px;
vertical-align: middle; vertical-align: middle;
&.triangle-down { &.triangle-down {
margin: 2px 2px -2px 0px; margin: 2px 2px -2px 0px;
} }
} }
.label { .label {
display: inline-block; display: inline-block;
font-size: 11px; font-size: 11px;
vertical-align: middle; vertical-align: middle;
} }
.toggle { .toggle {
@include border-radius(3px); @include border-radius(3px);
display: inline-block; display: inline-block;
padding: 1px 6px 4px 4px; padding: 1px 6px 4px 4px;
&:hover { &:hover {
background: rgba(white, 0.1); background: rgba(white, 0.1);
} }
} }
} }
}*/ }*/
/* line 51, ../sass/controls/_controls.scss */ /* line 51, ../sass/controls/_controls.scss */
.accordion { .accordion {
@ -2161,23 +2161,23 @@ label.checkbox.custom {
border-top: 1px solid #575757; border-top: 1px solid #575757;
color: #999; color: #999;
display: inline-block; display: inline-block;
/* height: $h; /* height: $h;
line-height: $h; line-height: $h;
&.dropdown { &.dropdown {
padding-left: $p; padding-left: $p;
padding-right: $p; padding-right: $p;
}*/ }*/
/* &.context-available { /* &.context-available {
// An element like the invoke-menu triangle; // An element like the invoke-menu triangle;
// Indicates that this element has a dropdown menu available; // Indicates that this element has a dropdown menu available;
// Currently unused // Currently unused
$c: $colorKey; $c: $colorKey;
color: $c; color: $c;
padding: 0 5px; padding: 0 5px;
&:hover { &:hover {
color: lighten($c, 10%); color: lighten($c, 10%);
} }
}*/ } }*/ }
/* line 162, ../sass/_mixins.scss */ /* line 162, ../sass/_mixins.scss */
.btn-menu:not(.disabled):hover { .btn-menu:not(.disabled):hover {
background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzYzNjM2MyIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzU3NTc1NyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzYzNjM2MyIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzU3NTc1NyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA==');
@ -2566,7 +2566,7 @@ label.checkbox.custom {
box-sizing: border-box; box-sizing: border-box;
border-top: 1px solid #737373; border-top: 1px solid #737373;
color: #d9d9d9; color: #d9d9d9;
line-height: 1.4rem; line-height: 1.5rem;
padding: 3px 10px 3px 30px; padding: 3px 10px 3px 30px;
white-space: nowrap; } white-space: nowrap; }
/* line 46, ../sass/controls/_menus.scss */ /* line 46, ../sass/controls/_menus.scss */

View File

@ -124,8 +124,8 @@ ul.tree {
transition: background-color 0.25s; transition: background-color 0.25s;
display: block; display: block;
font-size: 0.8em; font-size: 0.8em;
height: 1.4rem; height: 1.5rem;
line-height: 1.4rem; line-height: 1.5rem;
margin-bottom: 3px; margin-bottom: 3px;
position: relative; } position: relative; }
/* line 39, ../sass/tree/_tree.scss */ /* line 39, ../sass/tree/_tree.scss */

View File

@ -146,7 +146,7 @@ $controlDisabledOpacity: 0.3;
$formLabelW: 20%; $formLabelW: 20%;
$formInputH: 22px; $formInputH: 22px;
$formRowCtrlsH: 14px; $formRowCtrlsH: 14px;
$menuLineH: 1.4rem; $menuLineH: 1.5rem;
$scrollbarTrackSize: 10px; $scrollbarTrackSize: 10px;
$scrollbarTrackColorBg: rgba(#000, 0.4); $scrollbarTrackColorBg: rgba(#000, 0.4);
$btnStdH: 25px; $btnStdH: 25px;

View File

@ -22,7 +22,13 @@
<span class="label s-label"> <span class="label s-label">
<span class='ui-symbol icon type-icon'> <span class='ui-symbol icon type-icon'>
{{type.getGlyph()}} {{type.getGlyph()}}
<span class='ui-symbol icon alert hidden'>!</span> <span
class='ui-symbol icon l-icon-link'
ng-show="location.isLink()"
></span>
<span class='ui-symbol icon l-icon-alert'></span>
</span>
<span class='title-label'>
{{model.name}}
</span> </span>
<span class='title-label'>{{model.name}}</span>
</span> </span>

View File

@ -22,29 +22,29 @@
<span ng-controller="ToggleController as toggle"> <span ng-controller="ToggleController as toggle">
<span ng-controller="TreeNodeController as treeNode"> <span ng-controller="TreeNodeController as treeNode">
<span <span
class="tree-item menus-to-left" class="tree-item menus-to-left"
ng-class="{selected: treeNode.isSelected()}" ng-class="{selected: treeNode.isSelected()}"
> >
<span <span
class='ui-symbol view-control' class='ui-symbol view-control'
ng-click="toggle.toggle(); treeNode.trackExpansion()" ng-click="toggle.toggle(); treeNode.trackExpansion()"
ng-if="model.composition !== undefined" ng-if="model.composition !== undefined"
> >
{{toggle.isActive() ? "v" : ">"}} {{toggle.isActive() ? "v" : ">"}}
</span> </span>
<mct-representation <mct-representation
key="'label'" key="'label'"
mct-object="domainObject" mct-object="domainObject"
ng-model="ngModel" ng-model="ngModel"
ng-click="ngModel.selectedObject = domainObject" ng-click="ngModel.selectedObject = domainObject"
> >
</mct-representation> </mct-representation>
</span> </span>
<span <span
class="tree-item-subtree" class="tree-item-subtree"
ng-show="toggle.isActive()" ng-show="toggle.isActive()"
ng-if="model.composition !== undefined" ng-if="model.composition !== undefined"
> >
<mct-representation key="'subtree'" <mct-representation key="'subtree'"
ng-model="ngModel" ng-model="ngModel"

View File

@ -42,8 +42,12 @@ define(
* @constructor * @constructor
*/ */
function RootModelProvider(roots, $q, $log) { function RootModelProvider(roots, $q, $log) {
// Pull out identifiers to used as ROOT's // Pull out identifiers to used as ROOT's, while setting locations.
var ids = roots.map(function (root) { return root.id; }), var ids = roots.map(function (root) {
if (!root.model) { root.model = {}; }
root.model.location = 'ROOT';
return root.id;
}),
baseProvider = new StaticModelProvider(roots, $q, $log); baseProvider = new StaticModelProvider(roots, $q, $log);
function addRoot(models) { function addRoot(models) {
@ -77,4 +81,4 @@ define(
return RootModelProvider; return RootModelProvider;
} }
); );

View File

@ -79,6 +79,12 @@ define(
expect(captured.b.someProperty).toEqual("Some Value B"); expect(captured.b.someProperty).toEqual("Some Value B");
}); });
it("provides models with a location", function () {
provider.getModels(["a", "b"]).then(capture);
expect(captured.a.location).toBe('ROOT');
expect(captured.b.location).toBe('ROOT');
});
it("does not provide models which are not in extension declarations", function () { it("does not provide models which are not in extension declarations", function () {
provider.getModels(["c"]).then(capture); provider.getModels(["c"]).then(capture);
@ -96,4 +102,4 @@ define(
}); });
} }
); );

View File

@ -37,6 +37,12 @@
"controllers": [ "controllers": [
], ],
"capabilities": [ "capabilities": [
{
"key": "location",
"name": "Location Capability",
"description": "Provides a capability for retrieving the location of an object based upon it's context.",
"implementation": "capabilities/LocationCapability"
}
], ],
"services": [ "services": [
{ {
@ -44,7 +50,7 @@
"name": "Move Service", "name": "Move Service",
"description": "Provides a service for moving objects", "description": "Provides a service for moving objects",
"implementation": "services/MoveService.js", "implementation": "services/MoveService.js",
"depends": ["policyService", "linkService"] "depends": ["policyService", "linkService", "$q"]
}, },
{ {
"key": "linkService", "key": "linkService",

View File

@ -0,0 +1,87 @@
/*global define */
define(
function () {
"use strict";
/**
* The location capability allows a domain object to know its current
* parent, and also know its original parent. When a domain object's
* current parent is its original parent, the object is considered an
* original, otherwise it's a link.
*
* @constructor
*/
function LocationCapability(domainObject) {
this.domainObject = domainObject;
return this;
}
/**
* Set the primary location (the parent id) of the current domain
* object.
*
* @param {String} location the primary location to persist.
* @returns {Promise} a promise that is resolved when the operation
* completes.
*/
LocationCapability.prototype.setPrimaryLocation = function (location) {
var capability = this;
return this.domainObject.useCapability(
'mutation',
function (model) {
model.location = location;
}
).then(function () {
return capability.domainObject
.getCapability('persistence')
.persist();
});
};
/**
* Returns the contextual location of the current domain object. Only
* valid for domain objects that have a context capability.
*
* @returns {String} the contextual location of the object; the id of
* its parent.
*/
LocationCapability.prototype.getContextualLocation = function () {
var context = this.domainObject.getCapability("context");
if (!context) {
return;
}
return context.getParent().getId();
};
/**
* Returns true if the domainObject is a link, false if it's an
* original.
*
* @returns {Boolean}
*/
LocationCapability.prototype.isLink = function () {
var model = this.domainObject.getModel();
return model.location !== this.getContextualLocation();
};
/**
* Returns true if the domainObject is an original, false if it's a
* link.
*
* @returns {Boolean}
*/
LocationCapability.prototype.isOriginal = function () {
return !this.isLink();
};
function createLocationCapability(domainObject) {
return new LocationCapability(domainObject);
}
return createLocationCapability;
}
);

View File

@ -66,6 +66,17 @@ define(
} }
}).then(function () { }).then(function () {
return parentObject.getCapability('persistence').persist(); return parentObject.getCapability('persistence').persist();
}).then(function getObjectWithNewContext() {
return parentObject
.useCapability('composition')
.then(function (children) {
var i;
for (i = 0; i < children.length; i += 1) {
if (children[i].getId() === object.getId()) {
return children[i];
}
}
});
}); });
} }
}; };

View File

@ -25,13 +25,13 @@
define( define(
function () { function () {
"use strict"; "use strict";
/** /**
* MoveService provides an interface for moving objects from one * MoveService provides an interface for moving objects from one
* location to another. It also provides a method for determining if * location to another. It also provides a method for determining if
* an object can be copied to a specific location. * an object can be copied to a specific location.
*/ */
function MoveService(policyService, linkService) { function MoveService(policyService, linkService, $q) {
return { return {
/** /**
* Returns `true` if `object` can be moved into * Returns `true` if `object` can be moved into
@ -69,6 +69,25 @@ define(
perform: function (object, parentObject) { perform: function (object, parentObject) {
return linkService return linkService
.perform(object, parentObject) .perform(object, parentObject)
.then(function (objectInNewContext) {
var newLocationCapability = objectInNewContext
.getCapability('location'),
oldLocationCapability = object
.getCapability('location');
if (!newLocationCapability ||
!oldLocationCapability) {
return;
}
if (oldLocationCapability.isOriginal()) {
return newLocationCapability.setPrimaryLocation(
newLocationCapability
.getContextualLocation()
);
}
})
.then(function () { .then(function () {
return object return object
.getCapability('action') .getCapability('action')

View File

@ -0,0 +1,78 @@
/*global define,spyOn */
define(
function () {
/**
* An instrumented promise implementation for better control of promises
* during tests.
*
*/
function ControlledPromise() {
this.resolveHandlers = [];
this.rejectHandlers = [];
spyOn(this, 'then').andCallThrough();
}
/**
* Resolve the promise, passing the supplied value to all resolve
* handlers.
*/
ControlledPromise.prototype.resolve = function(value) {
this.resolveHandlers.forEach(function(handler) {
handler(value);
});
};
/**
* Reject the promise, passing the supplied value to all rejection
* handlers.
*/
ControlledPromise.prototype.reject = function(value) {
this.rejectHandlers.forEach(function(handler) {
handler(value);
});
};
/**
* Standard promise.then, returns a promise that support chaining.
* TODO: Need to support resolve/reject handlers that return promises.
*/
ControlledPromise.prototype.then = function (onResolve, onReject) {
var returnPromise = new ControlledPromise();
if (onResolve) {
this.resolveHandlers.push(function(resolveWith) {
var chainResult = onResolve(resolveWith);
if (chainResult && chainResult.then) {
// chainResult is a promise, resolve when it resolves.
chainResult.then(function(pipedResult) {
return returnPromise.resolve(pipedResult);
});
} else {
returnPromise.resolve(chainResult);
}
});
}
if (onReject) {
this.rejectHandlers.push(function(rejectWith) {
var chainResult = onReject(rejectWith);
if (chainResult && chainResult.then) {
chainResult.then(function(pipedResult) {
returnPromise.reject(pipedResult);
});
} else {
returnPromise.reject(chainResult);
}
});
}
return returnPromise;
};
return ControlledPromise;
}
);

View File

@ -0,0 +1,94 @@
/*global define,describe,it,expect,beforeEach,jasmine */
define(
[
'../../src/capabilities/LocationCapability',
'../DomainObjectFactory',
'../ControlledPromise'
],
function (LocationCapability, domainObjectFactory, ControlledPromise) {
describe("LocationCapability", function () {
describe("instantiated with domain object", function () {
var locationCapability,
persistencePromise,
mutationPromise,
domainObject;
beforeEach(function () {
domainObject = domainObjectFactory({
capabilities: {
context: {
getParent: function() {
return domainObjectFactory({id: 'root'});
}
},
persistence: jasmine.createSpyObj(
'persistenceCapability',
['persist']
),
mutation: jasmine.createSpyObj(
'mutationCapability',
['invoke']
)
}
});
persistencePromise = new ControlledPromise();
domainObject.capabilities.persistence.persist.andReturn(
persistencePromise
);
mutationPromise = new ControlledPromise();
domainObject.capabilities.mutation.invoke.andCallFake(
function (mutator) {
return mutationPromise.then(function () {
mutator(domainObject.model);
});
}
);
locationCapability = new LocationCapability(domainObject);
});
it("returns contextual location", function () {
expect(locationCapability.getContextualLocation())
.toBe('root');
});
it("knows when the object is an original", function () {
domainObject.model.location = 'root';
expect(locationCapability.isOriginal()).toBe(true);
expect(locationCapability.isLink()).toBe(false);
});
it("knows when the object is a link.", function () {
domainObject.model.location = 'different-root';
expect(locationCapability.isLink()).toBe(true);
expect(locationCapability.isOriginal()).toBe(false);
});
it("can persist location", function () {
var persistResult = locationCapability
.setPrimaryLocation('root'),
whenComplete = jasmine.createSpy('whenComplete');
persistResult.then(whenComplete);
expect(domainObject.model.location).not.toBeDefined();
mutationPromise.resolve();
expect(domainObject.model.location).toBe('root');
expect(whenComplete).not.toHaveBeenCalled();
expect(domainObject.capabilities.persistence.persist)
.toHaveBeenCalled();
persistencePromise.resolve();
expect(whenComplete).toHaveBeenCalled();
});
});
});
}
);

View File

@ -25,9 +25,10 @@
define( define(
[ [
'../../src/services/LinkService', '../../src/services/LinkService',
'../DomainObjectFactory' '../DomainObjectFactory',
'../ControlledPromise'
], ],
function (LinkService, domainObjectFactory) { function (LinkService, domainObjectFactory, ControlledPromise) {
"use strict"; "use strict";
describe("LinkService", function () { describe("LinkService", function () {
@ -50,7 +51,6 @@ define(
validate; validate;
beforeEach(function () { beforeEach(function () {
object = domainObjectFactory({ object = domainObjectFactory({
name: 'object' name: 'object'
}); });
@ -118,20 +118,29 @@ define(
describe("perform", function () { describe("perform", function () {
var object, var object,
linkedObject,
parentModel, parentModel,
parentObject, parentObject,
mutationPromise, mutationPromise,
compositionPromise,
persistencePromise,
compositionCapability,
persistenceCapability; persistenceCapability;
beforeEach(function () { beforeEach(function () {
mutationPromise = jasmine.createSpyObj( mutationPromise = new ControlledPromise();
'promise', compositionPromise = new ControlledPromise();
['then'] persistencePromise = new ControlledPromise();
);
persistenceCapability = jasmine.createSpyObj( persistenceCapability = jasmine.createSpyObj(
'persistenceCapability', 'persistenceCapability',
['persist'] ['persist']
); );
persistenceCapability.persist.andReturn(persistencePromise);
compositionCapability = jasmine.createSpyObj(
'compositionCapability',
['invoke']
);
compositionCapability.invoke.andReturn(compositionPromise);
parentModel = { parentModel = {
composition: [] composition: []
}; };
@ -145,7 +154,8 @@ define(
return mutationPromise; return mutationPromise;
} }
}, },
persistence: persistenceCapability persistence: persistenceCapability,
composition: compositionCapability
} }
}); });
@ -154,7 +164,11 @@ define(
id: 'xyz' id: 'xyz'
}); });
parentObject.getCapability.andReturn(persistenceCapability); linkedObject = domainObjectFactory({
name: 'object-link',
id: 'xyz'
});
}); });
@ -171,12 +185,23 @@ define(
it("persists parent", function () { it("persists parent", function () {
linkService.perform(object, parentObject); linkService.perform(object, parentObject);
expect(mutationPromise.then).toHaveBeenCalled(); expect(mutationPromise.then).toHaveBeenCalled();
mutationPromise.then.calls[0].args[0](); mutationPromise.resolve();
expect(parentObject.getCapability) expect(parentObject.getCapability)
.toHaveBeenCalledWith('persistence'); .toHaveBeenCalledWith('persistence');
expect(persistenceCapability.persist).toHaveBeenCalled(); expect(persistenceCapability.persist).toHaveBeenCalled();
}); });
it("returns object representing new link", function () {
var returnPromise, whenComplete;
returnPromise = linkService.perform(object, parentObject);
whenComplete = jasmine.createSpy('whenComplete');
returnPromise.then(whenComplete);
mutationPromise.resolve();
persistencePromise.resolve();
compositionPromise.resolve([linkedObject]);
expect(whenComplete).toHaveBeenCalledWith(linkedObject);
});
}); });
}); });
} }

View File

@ -23,7 +23,10 @@
/*global define,jasmine */ /*global define,jasmine */
define( define(
function () { [
'../ControlledPromise'
],
function (ControlledPromise) {
"use strict"; "use strict";
/** /**
@ -47,7 +50,7 @@ define(
* var whenLinked = jasmine.createSpy('whenLinked'); * var whenLinked = jasmine.createSpy('whenLinked');
* linkService.perform(object, parentObject).then(whenLinked); * linkService.perform(object, parentObject).then(whenLinked);
* expect(whenLinked).not.toHaveBeenCalled(); * expect(whenLinked).not.toHaveBeenCalled();
* linkService.perform.mostRecentCall.resolve('someArg'); * linkService.perform.mostRecentCall.promise.resolve('someArg');
* expect(whenLinked).toHaveBeenCalledWith('someArg'); * expect(whenLinked).toHaveBeenCalledWith('someArg');
* ``` * ```
*/ */
@ -62,33 +65,19 @@ define(
] ]
); );
mockLinkService.perform.andCallFake(function () { mockLinkService.perform.andCallFake(function (object, newParent) {
var performPromise, var performPromise = new ControlledPromise();
callExtensions,
spy;
performPromise = jasmine.createSpyObj( this.perform.mostRecentCall.promise = performPromise;
'performPromise', this.perform.calls[this.perform.calls.length - 1].promise =
['then'] performPromise;
);
callExtensions = { return performPromise.then(function (overrideObject) {
promise: performPromise, if (overrideObject) {
resolve: function (resolveWith) { return overrideObject;
performPromise.then.calls.forEach(function (call) {
call.args[0](resolveWith);
});
} }
}; return object;
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;

View File

@ -25,9 +25,15 @@ define(
[ [
'../../src/services/MoveService', '../../src/services/MoveService',
'../services/MockLinkService', '../services/MockLinkService',
'../DomainObjectFactory' '../DomainObjectFactory',
'../ControlledPromise'
], ],
function (MoveService, MockLinkService, domainObjectFactory) { function (
MoveService,
MockLinkService,
domainObjectFactory,
ControlledPromise
) {
"use strict"; "use strict";
describe("MoveService", function () { describe("MoveService", function () {
@ -140,8 +146,11 @@ define(
describe("perform", function () { describe("perform", function () {
var object, var object,
parentObject, newParent,
actionCapability; actionCapability,
locationCapability,
locationPromise,
moveResult;
beforeEach(function () { beforeEach(function () {
actionCapability = jasmine.createSpyObj( actionCapability = jasmine.createSpyObj(
@ -149,24 +158,34 @@ define(
['perform'] ['perform']
); );
locationCapability = jasmine.createSpyObj(
'locationCapability',
[
'isOriginal',
'setPrimaryLocation',
'getContextualLocation'
]
);
locationPromise = new ControlledPromise();
locationCapability.setPrimaryLocation
.andReturn(locationPromise);
object = domainObjectFactory({ object = domainObjectFactory({
name: 'object', name: 'object',
capabilities: { capabilities: {
action: actionCapability action: actionCapability,
location: locationCapability
} }
}); });
parentObject = domainObjectFactory({ moveResult = moveService.perform(object, newParent);
name: 'parentObject'
});
moveService.perform(object, parentObject);
}); });
it("links object to parentObject", function () { it("links object to newParent", function () {
expect(linkService.perform).toHaveBeenCalledWith( expect(linkService.perform).toHaveBeenCalledWith(
object, object,
parentObject newParent
); );
}); });
@ -175,12 +194,48 @@ define(
.toHaveBeenCalledWith(jasmine.any(Function)); .toHaveBeenCalledWith(jasmine.any(Function));
}); });
it("removes object when link is completed", function () { describe("when moving an original", function () {
linkService.perform.mostRecentCall.resolve(); beforeEach(function () {
expect(object.getCapability) locationCapability.getContextualLocation
.toHaveBeenCalledWith('action'); .andReturn('new-location');
expect(actionCapability.perform) locationCapability.isOriginal.andReturn(true);
.toHaveBeenCalledWith('remove'); linkService.perform.mostRecentCall.promise.resolve();
});
it("updates location", function () {
expect(locationCapability.setPrimaryLocation)
.toHaveBeenCalledWith('new-location');
});
describe("after location update", function () {
beforeEach(function () {
locationPromise.resolve();
});
it("removes object from parent", function () {
expect(actionCapability.perform)
.toHaveBeenCalledWith('remove');
});
});
});
describe("when moving a link", function () {
beforeEach(function () {
locationCapability.isOriginal.andReturn(false);
linkService.perform.mostRecentCall.promise.resolve();
});
it("does not update location", function () {
expect(locationCapability.setPrimaryLocation)
.not
.toHaveBeenCalled();
});
it("removes object from parent", function () {
expect(actionCapability.perform)
.toHaveBeenCalledWith('remove');
});
}); });
}); });

View File

@ -5,5 +5,6 @@
"services/CopyService", "services/CopyService",
"services/LinkService", "services/LinkService",
"services/MoveService", "services/MoveService",
"services/LocationService" "services/LocationService",
"capabilities/LocationCapability"
] ]