Merged from Master

This commit is contained in:
Henry
2015-12-07 20:42:09 -08:00
53 changed files with 1798 additions and 481 deletions

View File

@@ -105,6 +105,12 @@
"implementation": "navigation/NavigationService.js"
}
],
"policies": [
{
"implementation": "creation/CreationPolicy.js",
"category": "creation"
}
],
"actions": [
{
"key": "navigate",

View File

@@ -68,7 +68,7 @@ define(
// Introduce one create action per type
return this.typeService.listTypes().filter(function (type) {
return type.hasFeature("creation");
return self.policyService.allow("creation", type);
}).map(function (type) {
return new CreateAction(
type,

View File

@@ -0,0 +1,45 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define*/
define(
[],
function () {
"use strict";
/**
* A policy for determining whether objects of a given type can be
* created.
* @constructor
* @implements {Policy}
* @memberof platform/commonUI/browse
*/
function CreationPolicy() {
}
CreationPolicy.prototype.allow = function (type) {
return type.hasFeature("creation");
};
return CreationPolicy;
}
);

View File

@@ -33,6 +33,9 @@ define(
var mockTypeService,
mockDialogService,
mockCreationService,
mockPolicyService,
mockCreationPolicy,
mockPolicyMap = {},
mockTypes,
provider;
@@ -67,14 +70,32 @@ define(
"creationService",
[ "createObject" ]
);
mockPolicyService = jasmine.createSpyObj(
"policyService",
[ "allow" ]
);
mockTypes = [ "A", "B", "C" ].map(createMockType);
mockTypes.forEach(function(type){
mockPolicyMap[type.getName()] = true;
});
mockCreationPolicy = function(type){
return mockPolicyMap[type.getName()];
};
mockPolicyService.allow.andCallFake(function(category, type){
return category === "creation" && mockCreationPolicy(type) ? true : false;
});
mockTypeService.listTypes.andReturn(mockTypes);
provider = new CreateActionProvider(
mockTypeService,
mockDialogService,
mockCreationService
mockCreationService,
mockPolicyService
);
});
@@ -94,15 +115,15 @@ define(
it("does not expose non-creatable types", function () {
// One of the types won't have the creation feature...
mockTypes[1].hasFeature.andReturn(false);
mockPolicyMap[mockTypes[0].getName()] = false;
// ...so it should have been filtered out.
expect(provider.getActions({
key: "create",
domainObject: {}
}).length).toEqual(2);
// Make sure it was creation which was used to check
expect(mockTypes[1].hasFeature)
.toHaveBeenCalledWith("creation");
expect(mockPolicyService.allow)
.toHaveBeenCalledWith("creation", mockTypes[0]);
});
});
}

View File

@@ -0,0 +1,53 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define,describe,it,expect,beforeEach,jasmine*/
define(
["../../src/creation/CreationPolicy"],
function (CreationPolicy) {
"use strict";
describe("The creation policy", function () {
var mockType,
policy;
beforeEach(function () {
mockType = jasmine.createSpyObj(
'type',
['hasFeature']
);
policy = new CreationPolicy();
});
it("allows creation of types with the creation feature", function () {
mockType.hasFeature.andReturn(true);
expect(policy.allow(mockType)).toBeTruthy();
});
it("disallows creation of types without the creation feature", function () {
mockType.hasFeature.andReturn(false);
expect(policy.allow(mockType)).toBeFalsy();
});
});
}
);

View File

@@ -8,6 +8,7 @@
"creation/CreateMenuController",
"creation/CreateWizard",
"creation/CreationService",
"creation/CreationPolicy",
"creation/LocatorController",
"navigation/NavigateAction",
"navigation/NavigationService",

View File

@@ -50,7 +50,7 @@ define(
// Simply trigger refresh of in-view objects; do not
// write anything to database.
persistence.persist = function () {
cache.markDirty(editableObject);
return cache.markDirty(editableObject);
};
// Delegate refresh to the original object; this avoids refreshing

View File

@@ -115,6 +115,7 @@ define(
*/
EditableDomainObjectCache.prototype.markDirty = function (domainObject) {
this.dirtyObjects[domainObject.getId()] = domainObject;
return this.$q.when(true);
};
/**

View File

@@ -31,6 +31,7 @@ define(
mockEditableObject,
mockDomainObject,
mockCache,
mockPromise,
capability;
beforeEach(function () {
@@ -50,7 +51,9 @@ define(
"cache",
[ "markDirty" ]
);
mockPromise = jasmine.createSpyObj("promise", ["then"]);
mockCache.markDirty.andReturn(mockPromise);
mockDomainObject.getCapability.andReturn(mockPersistence);
capability = new EditablePersistenceCapability(
@@ -84,6 +87,10 @@ define(
expect(mockPersistence.refresh).toHaveBeenCalled();
});
it("returns a promise from persist", function () {
expect(capability.persist().then).toEqual(jasmine.any(Function));
});
});
}
);

View File

@@ -19,6 +19,10 @@
{
"implementation": "StyleSheetLoader.js",
"depends": [ "stylesheets[]", "$document", "THEME" ]
},
{
"implementation": "UnsupportedBrowserWarning.js",
"depends": [ "notificationService", "agentService" ]
}
],
"stylesheets": [

View File

@@ -36,29 +36,29 @@ $mobileTreeRightArrowW: 30px;
/************************** DEVICE WIDTHS */
// IMPORTANT! Usage assumes that ranges are mutually exclusive and have no gaps
$phoMaxW: 514px;
$tabMinW: 515px;
$tabMaxW: 1280px;
$desktopMinW: 1281px;
$phoMaxW: 767px;
$tabMinW: 768px;
$tabMaxW: 1024px;
$desktopMinW: 1025px;
/************************** MEDIA QUERIES: WINDOW CHECKS FOR SPECIFIC ORIENTATIONS FOR EACH DEVICE */
$screenPortrait: "screen and (orientation: portrait)";
$screenLandscape: "screen and (orientation: landscape)";
$screenPortrait: "(orientation: portrait)";
$screenLandscape: "(orientation: landscape)";
$mobileDevice: "(max-device-width: #{$tabMaxW})";
//$mobileDevice: "(max-device-width: #{$tabMaxW})";
$phoneCheck: "(max-device-width: #{$phoMaxW})";
$tabletCheck: $mobileDevice;
$desktopCheck: "(min-device-width: #{$desktopMinW})";
$tabletCheck: "(min-device-width: #{$tabMinW}) and (max-device-width: #{$tabMaxW})";
$desktopCheck: "(min-device-width: #{$desktopMinW}) and (-webkit-min-device-pixel-ratio: 1)";
/************************** MEDIA QUERIES: WINDOWS FOR SPECIFIC ORIENTATIONS FOR EACH DEVICE */
$phonePortrait: "#{$screenPortrait} and #{$phoneCheck} and #{$mobileDevice}";
$phoneLandscape: "#{$screenLandscape} and #{$phoneCheck} and #{$mobileDevice}";
$phonePortrait: "only screen and #{$screenPortrait} and #{$phoneCheck}";
$phoneLandscape: "only screen and #{$screenLandscape} and #{$phoneCheck}";
$tabletPortrait: "#{$screenPortrait} and #{$tabletCheck} and #{$mobileDevice}";
$tabletLandscape: "#{$screenLandscape} and #{$tabletCheck} and #{$mobileDevice}";
$tabletPortrait: "only screen and #{$screenPortrait} and #{$tabletCheck}";
$tabletLandscape: "only screen and #{$screenLandscape} and #{$tabletCheck}";
$desktop: "screen and #{$desktopCheck}";
$desktop: "only screen and #{$desktopCheck}";
/************************** DEVICE PARAMETERS FOR MENUS/REPRESENTATIONS */
$proporMenuOnly: 90%;

View File

@@ -1,7 +1,29 @@
<!--
Open MCT Web, Copyright (c) 2014-2015, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT Web is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT Web includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<span class="s-btn"
ng-controller="DateTimeFieldController">
<input type="text"
ng-model="textValue"
ng-blur="restoreTextValue(); ngBlur()"
ng-class="{ error: textInvalid }">
</input>
<a class="ui-symbol icon icon-calendar"
@@ -11,8 +33,8 @@
<mct-popup ng-if="picker.active">
<div mct-click-elsewhere="picker.active = false">
<mct-control key="'datetime-picker'"
ng-model="ngModel"
field="field"
ng-model="pickerModel"
field="'value'"
options="{ hours: true }">
</mct-control>
</div>

View File

@@ -20,12 +20,14 @@
at runtime from the About dialog for additional information.
-->
<div ng-controller="TimeRangeController">
<div class="l-time-range-inputs-holder">
<form class="l-time-range-inputs-holder"
ng-submit="updateBoundsFromForm()">
<span class="l-time-range-inputs-elem ui-symbol type-icon">&#x43;</span>
<span class="l-time-range-input">
<mct-control key="'datetime-field'"
structure="{ format: parameters.format }"
ng-model="ngModel.outer"
ng-model="formModel"
ng-blur="updateBoundsFromForm()"
field="'start'"
class="time-range-start">
</mct-control>
@@ -36,12 +38,15 @@
<span class="l-time-range-input" ng-controller="ToggleController as t2">
<mct-control key="'datetime-field'"
structure="{ format: parameters.format }"
ng-model="ngModel.outer"
ng-model="formModel"
ng-blur="updateBoundsFromForm()"
field="'end'"
class="time-range-end">
</mct-control>&nbsp;
</span>
</div>
<input type="submit" class="hidden">
</form>
<div class="l-time-range-slider-holder">
<div class="l-time-range-slider">

View File

@@ -0,0 +1,64 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define*/
/**
* This bundle provides various general-purpose UI elements, including
* platform styling.
* @namespace platform/commonUI/general
*/
define(
[],
function () {
"use strict";
var WARNING_TITLE = "Unsupported browser",
WARNING_DESCRIPTION = [
"This software has been developed and tested",
"using the latest Google Chrome,",
"and may be unstable in other browsers."
].join(" "),
MOBILE_BROWSER = "Safari",
DESKTOP_BROWSER = "Chrome";
/**
* Shows a warning if a user's browser is unsupported.
* @memberof platform/commonUI/general
* @constructor
* @param {NotificationService} notificationService the notification
* service
*/
function UnsupportedBrowserWarning(notificationService, agentService) {
var testToBrowser = agentService.isMobile() ?
MOBILE_BROWSER : DESKTOP_BROWSER;
if (!agentService.isBrowser(testToBrowser)) {
notificationService.alert({
title: WARNING_TITLE,
actionText: WARNING_DESCRIPTION
});
}
}
return UnsupportedBrowserWarning;
}
);

View File

@@ -53,7 +53,9 @@ define(
formatter.parse($scope.textValue) !== value) {
$scope.textValue = formatter.format(value);
$scope.textInvalid = false;
$scope.lastValidValue = $scope.textValue;
}
$scope.pickerModel = { value: value };
}
function updateFromView(textValue) {
@@ -61,6 +63,17 @@ define(
if (!$scope.textInvalid) {
$scope.ngModel[$scope.field] =
formatter.parse(textValue);
$scope.lastValidValue = $scope.textValue;
}
}
function updateFromPicker(value) {
if (value !== $scope.ngModel[$scope.field]) {
$scope.ngModel[$scope.field] = value;
updateFromModel(value);
if ($scope.ngBlur) {
$scope.ngBlur();
}
}
}
@@ -69,10 +82,18 @@ define(
updateFromModel($scope.ngModel[$scope.field]);
}
function restoreTextValue() {
$scope.textValue = $scope.lastValidValue;
updateFromView($scope.textValue);
}
$scope.restoreTextValue = restoreTextValue;
$scope.picker = { active: false };
$scope.$watch('structure.format', setFormat);
$scope.$watch('ngModel[field]', updateFromModel);
$scope.$watch('pickerModel.value', updateFromPicker);
$scope.$watch('textValue', updateFromView);
}

View File

@@ -175,6 +175,13 @@ define(
updateViewFromModel($scope.ngModel);
}
function updateFormModel() {
$scope.formModel = {
start: (($scope.ngModel || {}).outer || {}).start,
end: (($scope.ngModel || {}).outer || {}).end
};
}
function updateOuterStart(t) {
var ngModel = $scope.ngModel;
@@ -192,6 +199,7 @@ define(
ngModel.inner.end
);
updateFormModel();
updateViewForInnerSpanFromModel(ngModel);
updateTicks();
}
@@ -213,6 +221,7 @@ define(
ngModel.inner.start
);
updateFormModel();
updateViewForInnerSpanFromModel(ngModel);
updateTicks();
}
@@ -223,6 +232,14 @@ define(
updateTicks();
}
function updateBoundsFromForm() {
$scope.ngModel = $scope.ngModel || {};
$scope.ngModel.outer = {
start: $scope.formModel.start,
end: $scope.formModel.end
};
}
$scope.startLeftDrag = startLeftDrag;
$scope.startRightDrag = startRightDrag;
$scope.startMiddleDrag = startMiddleDrag;
@@ -230,10 +247,13 @@ define(
$scope.rightDrag = rightDrag;
$scope.middleDrag = middleDrag;
$scope.updateBoundsFromForm = updateBoundsFromForm;
$scope.ticks = [];
// Initialize scope to defaults
updateViewFromModel($scope.ngModel);
updateFormModel();
$scope.$watchCollection("ngModel", updateViewFromModel);
$scope.$watch("spanWidth", updateSpanWidth);

View File

@@ -204,7 +204,7 @@ define(
// And poll for position changes enforced by styles
activeInterval = $interval(function () {
getSetPosition(getSetPosition());
}, POLLING_INTERVAL, false);
}, POLLING_INTERVAL, 0, false);
// ...and stop polling when we're destroyed.
$scope.$on('$destroy', function () {

View File

@@ -0,0 +1,98 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../src/UnsupportedBrowserWarning"],
function (UnsupportedBrowserWarning) {
"use strict";
var MOBILE_BROWSER = "Safari",
DESKTOP_BROWSER = "Chrome",
UNSUPPORTED_BROWSERS = [
"Firefox",
"IE",
"Opera",
"Iceweasel"
];
describe("The unsupported browser warning", function () {
var mockNotificationService,
mockAgentService,
testAgent;
function instantiateWith(browser) {
testAgent = "Mozilla/5.0 " + browser + "/12.34.56";
return new UnsupportedBrowserWarning(
mockNotificationService,
mockAgentService
);
}
beforeEach(function () {
testAgent = "chrome";
mockNotificationService = jasmine.createSpyObj(
"notificationService",
[ "alert" ]
);
mockAgentService = jasmine.createSpyObj(
"agentService",
[ "isMobile", "isBrowser" ]
);
mockAgentService.isBrowser.andCallFake(function (substr) {
substr = substr.toLowerCase();
return testAgent.toLowerCase().indexOf(substr) !== -1;
});
});
[ false, true ].forEach(function (isMobile) {
var deviceType = isMobile ? "mobile" : "desktop",
goodBrowser = isMobile ? MOBILE_BROWSER : DESKTOP_BROWSER,
badBrowsers = UNSUPPORTED_BROWSERS.concat([
isMobile ? DESKTOP_BROWSER : MOBILE_BROWSER
]);
describe("on " + deviceType + " devices", function () {
beforeEach(function () {
mockAgentService.isMobile.andReturn(isMobile);
});
it("is not shown for " + goodBrowser, function () {
instantiateWith(goodBrowser);
expect(mockNotificationService.alert)
.not.toHaveBeenCalled();
});
badBrowsers.forEach(function (badBrowser) {
it("is shown for " + badBrowser, function () {
instantiateWith(badBrowser);
expect(mockNotificationService.alert)
.toHaveBeenCalled();
});
});
});
});
});
}
);

View File

@@ -67,21 +67,13 @@ define(
mockScope.ngModel = { testField: 12321 };
mockScope.field = "testField";
mockScope.structure = { format: "someFormat" };
mockScope.ngBlur = jasmine.createSpy('blur');
controller = new DateTimeFieldController(
mockScope,
mockFormatService
);
});
it("updates models from user-entered text", function () {
var newText = "1977-05-25 17:30:00";
mockScope.textValue = newText;
fireWatch("textValue", newText);
expect(mockScope.ngModel.testField)
.toEqual(mockFormat.parse(newText));
expect(mockScope.textInvalid).toBeFalsy();
fireWatch("ngModel[field]", mockScope.ngModel.testField);
});
it("updates text from model values", function () {
@@ -91,16 +83,55 @@ define(
expect(mockScope.textValue).toEqual("1977-05-25 17:30:00");
});
describe("when valid text is entered", function () {
var newText;
beforeEach(function () {
newText = "1977-05-25 17:30:00";
mockScope.textValue = newText;
fireWatch("textValue", newText);
});
it("updates models from user-entered text", function () {
expect(mockScope.ngModel.testField)
.toEqual(mockFormat.parse(newText));
expect(mockScope.textInvalid).toBeFalsy();
});
it("does not indicate a blur event", function () {
expect(mockScope.ngBlur).not.toHaveBeenCalled();
});
});
describe("when a date is chosen via the date picker", function () {
var newValue;
beforeEach(function () {
newValue = 12345654321;
mockScope.pickerModel.value = newValue;
fireWatch("pickerModel.value", newValue);
});
it("updates models", function () {
expect(mockScope.ngModel.testField).toEqual(newValue);
});
it("fires a blur event", function () {
expect(mockScope.ngBlur).toHaveBeenCalled();
});
});
it("exposes toggle state for date-time picker", function () {
expect(mockScope.picker.active).toBe(false);
});
describe("when user input is invalid", function () {
var newText, oldValue;
var newText, oldText, oldValue;
beforeEach(function () {
newText = "Not a date";
oldValue = mockScope.ngModel.testField;
oldText = mockScope.textValue;
mockScope.textValue = newText;
fireWatch("textValue", newText);
});
@@ -116,6 +147,11 @@ define(
it("does not modify user input", function () {
expect(mockScope.textValue).toEqual(newText);
});
it("restores valid text values on request", function () {
mockScope.restoreTextValue();
expect(mockScope.textValue).toEqual(oldText);
});
});
it("does not modify valid but irregular user input", function () {

View File

@@ -91,6 +91,39 @@ define(
.toHaveBeenCalledWith("ngModel", jasmine.any(Function));
});
describe("when changes are made via form entry", function () {
beforeEach(function () {
mockScope.ngModel = {
outer: { start: DAY * 2, end: DAY * 3 },
inner: { start: DAY * 2.25, end: DAY * 2.75 }
};
mockScope.formModel = {
start: DAY * 10000,
end: DAY * 11000
};
// These watches may not exist, but Angular would fire
// them if they did.
fireWatchCollection("formModel", mockScope.formModel);
fireWatch("formModel.start", mockScope.formModel.start);
fireWatch("formModel.end", mockScope.formModel.end);
});
it("does not immediately make changes to the model", function () {
expect(mockScope.ngModel.outer.start)
.not.toEqual(mockScope.formModel.start);
expect(mockScope.ngModel.outer.end)
.not.toEqual(mockScope.formModel.end);
});
it("updates model bounds on request", function () {
mockScope.updateBoundsFromForm();
expect(mockScope.ngModel.outer.start)
.toEqual(mockScope.formModel.start);
expect(mockScope.ngModel.outer.end)
.toEqual(mockScope.formModel.end);
});
});
describe("when dragged", function () {
beforeEach(function () {
mockScope.ngModel = {

View File

@@ -0,0 +1,95 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../../src/directives/MCTSplitPane"],
function (MCTSplitPane) {
'use strict';
var JQLITE_METHODS = [
'on',
'addClass',
'children',
'eq'
];
describe("The mct-split-pane directive", function () {
var mockParse,
mockLog,
mockInterval,
mctSplitPane;
beforeEach(function () {
mockParse = jasmine.createSpy('$parse');
mockLog =
jasmine.createSpyObj('$log', ['warn', 'info', 'debug']);
mockInterval = jasmine.createSpy('$interval');
mockInterval.cancel = jasmine.createSpy('mockCancel');
mctSplitPane = new MCTSplitPane(
mockParse,
mockLog,
mockInterval
);
});
it("is only applicable as an element", function () {
expect(mctSplitPane.restrict).toEqual("E");
});
describe("when its controller is applied", function () {
var mockScope,
mockElement,
testAttrs,
mockChildren,
controller;
beforeEach(function () {
mockScope =
jasmine.createSpyObj('$scope', ['$apply', '$watch', '$on']);
mockElement =
jasmine.createSpyObj('element', JQLITE_METHODS);
testAttrs = {};
mockChildren =
jasmine.createSpyObj('children', JQLITE_METHODS);
mockElement.children.andReturn(mockChildren);
mockChildren.eq.andReturn(mockChildren);
mockChildren[0] = {};
controller = mctSplitPane.controller[3](
mockScope,
mockElement,
testAttrs
);
});
it("sets an interval which does not trigger digests", function () {
expect(mockInterval.mostRecentCall.args[3]).toBe(false);
});
});
});
}
);

View File

@@ -19,8 +19,10 @@
"directives/MCTPopup",
"directives/MCTResize",
"directives/MCTScroll",
"directives/MCTSplitPane",
"services/Popup",
"services/PopupService",
"services/UrlService",
"StyleSheetLoader"
"StyleSheetLoader",
"UnsupportedBrowserWarning"
]

View File

@@ -43,6 +43,7 @@ define(
var userAgent = $window.navigator.userAgent,
matches = userAgent.match(/iPad|iPhone|Android/i) || [];
this.userAgent = userAgent;
this.mobileName = matches[0];
this.$window = $window;
}
@@ -91,6 +92,18 @@ define(
return !this.isPortrait();
};
/**
* Check if the user agent matches a certain named device,
* as indicated by checking for a case-insensitive substring
* match.
* @param {string} name the name to check for
* @returns {boolean} true if the user agent includes that name
*/
AgentService.prototype.isBrowser = function (name) {
name = name.toLowerCase();
return this.userAgent.toLowerCase().indexOf(name) !== -1;
};
return AgentService;
}
);

View File

@@ -81,6 +81,13 @@ define(
expect(agentService.isPortrait()).toBeTruthy();
expect(agentService.isLandscape()).toBeFalsy();
});
it("allows for checking browser type", function () {
testWindow.navigator.userAgent = "Chromezilla Safarifox";
agentService = new AgentService(testWindow);
expect(agentService.isBrowser("Chrome")).toBe(true);
expect(agentService.isBrowser("Firefox")).toBe(false);
});
});
}
);

View File

@@ -188,7 +188,8 @@
{
"key": "persistence",
"implementation": "capabilities/PersistenceCapability.js",
"depends": [ "persistenceService", "identifierService" ]
"depends": [ "persistenceService", "identifierService",
"notificationService", "$q" ]
},
{
"key": "metadata",

View File

@@ -20,6 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define*/
/*jslint es5: true */
define(
@@ -47,6 +48,8 @@ define(
function PersistenceCapability(
persistenceService,
identifierService,
notificationService,
$q,
domainObject
) {
// Cache modified timestamp
@@ -55,6 +58,8 @@ define(
this.domainObject = domainObject;
this.identifierService = identifierService;
this.persistenceService = persistenceService;
this.notificationService = notificationService;
this.$q = $q;
}
// Utility function for creating promise-like objects which
@@ -72,6 +77,46 @@ define(
return parts.length > 1 ? parts.slice(1).join(":") : id;
}
/**
* Checks if the value returned is falsey, and if so returns a
* rejected promise
*/
function rejectIfFalsey(value, $q){
if (!value){
return $q.reject("Error persisting object");
} else {
return value;
}
}
function formatError(error){
if (error && error.message) {
return error.message;
} else if (error && typeof error === "string"){
return error;
} else {
return "unknown error";
}
}
/**
* Display a notification message if an error has occurred during
* persistence.
*/
function notifyOnError(error, domainObject, notificationService, $q){
var errorMessage = "Unable to persist " + domainObject.getModel().name;
if (error) {
errorMessage += ": " + formatError(error);
}
notificationService.error({
title: "Error persisting " + domainObject.getModel().name,
hint: errorMessage || "Unknown error"
});
return $q.reject(error);
}
/**
* Persist any changes which have been made to this
* domain object's model.
@@ -80,7 +125,8 @@ define(
* if not.
*/
PersistenceCapability.prototype.persist = function () {
var domainObject = this.domainObject,
var self = this,
domainObject = this.domainObject,
model = domainObject.getModel(),
modified = model.modified,
persistenceService = this.persistenceService,
@@ -98,7 +144,11 @@ define(
this.getSpace(),
getKey(domainObject.getId()),
domainObject.getModel()
]);
]).then(function(result){
return rejectIfFalsey(result, self.$q);
}).catch(function(error){
return notifyOnError(error, domainObject, self.notificationService, self.$q);
});
};
/**

View File

@@ -20,6 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
/*jslint es5: true */
/**
* PersistenceCapabilitySpec. Created by vwoeltje on 11/6/14.
@@ -34,24 +35,36 @@ define(
mockIdentifierService,
mockDomainObject,
mockIdentifier,
mockNofificationService,
mockQ,
id = "object id",
model = { someKey: "some value"},
model,
SPACE = "some space",
persistence;
persistence,
happyPromise;
function asPromise(value) {
function asPromise(value, doCatch) {
return (value || {}).then ? value : {
then: function (callback) {
return asPromise(callback(value));
},
catch: function(callback) {
//Define a default 'happy' catch, that skips over the
// catch callback
return doCatch ? asPromise(callback(value)): asPromise(value);
}
};
}
beforeEach(function () {
happyPromise = asPromise(true);
model = { someKey: "some value", name: "domain object"};
mockPersistenceService = jasmine.createSpyObj(
"persistenceService",
[ "updateObject", "readObject", "createObject", "deleteObject" ]
);
mockIdentifierService = jasmine.createSpyObj(
'identifierService',
[ 'parse', 'generate' ]
@@ -60,6 +73,15 @@ define(
'identifier',
[ 'getSpace', 'getKey', 'getDefinedSpace' ]
);
mockQ = jasmine.createSpyObj(
"$q",
["reject"]
);
mockNofificationService = jasmine.createSpyObj(
"notificationService",
["error"]
);
mockDomainObject = {
getId: function () { return id; },
getModel: function () { return model; },
@@ -76,66 +98,99 @@ define(
persistence = new PersistenceCapability(
mockPersistenceService,
mockIdentifierService,
mockNofificationService,
mockQ,
mockDomainObject
);
});
it("creates unpersisted objects with the persistence service", function () {
// Verify precondition; no call made during constructor
expect(mockPersistenceService.createObject).not.toHaveBeenCalled();
describe("successful persistence", function() {
beforeEach(function () {
mockPersistenceService.updateObject.andReturn(happyPromise);
mockPersistenceService.createObject.andReturn(happyPromise);
});
it("creates unpersisted objects with the persistence service", function () {
// Verify precondition; no call made during constructor
expect(mockPersistenceService.createObject).not.toHaveBeenCalled();
persistence.persist();
persistence.persist();
expect(mockPersistenceService.createObject).toHaveBeenCalledWith(
SPACE,
id,
model
);
expect(mockPersistenceService.createObject).toHaveBeenCalledWith(
SPACE,
id,
model
);
});
it("updates previously persisted objects with the persistence service", function () {
// Verify precondition; no call made during constructor
expect(mockPersistenceService.updateObject).not.toHaveBeenCalled();
model.persisted = 12321;
persistence.persist();
expect(mockPersistenceService.updateObject).toHaveBeenCalledWith(
SPACE,
id,
model
);
});
it("reports which persistence space an object belongs to", function () {
expect(persistence.getSpace()).toEqual(SPACE);
});
it("updates persisted timestamp on persistence", function () {
model.modified = 12321;
persistence.persist();
expect(model.persisted).toEqual(12321);
});
it("refreshes the domain object model from persistence", function () {
var refreshModel = {someOtherKey: "some other value"};
mockPersistenceService.readObject.andReturn(asPromise(refreshModel));
persistence.refresh();
expect(model).toEqual(refreshModel);
});
it("does not overwrite unpersisted changes on refresh", function () {
var refreshModel = {someOtherKey: "some other value"},
mockCallback = jasmine.createSpy();
model.modified = 2;
model.persisted = 1;
mockPersistenceService.readObject.andReturn(asPromise(refreshModel));
persistence.refresh().then(mockCallback);
expect(model).not.toEqual(refreshModel);
// Should have also indicated that no changes were actually made
expect(mockCallback).toHaveBeenCalledWith(false);
});
it("does not trigger error notification on successful" +
" persistence", function () {
persistence.persist();
expect(mockQ.reject).not.toHaveBeenCalled();
expect(mockNofificationService.error).not.toHaveBeenCalled();
});
});
describe("unsuccessful persistence", function() {
var sadPromise = {
then: function(callback){
return asPromise(callback(0), true);
}
};
beforeEach(function () {
mockPersistenceService.createObject.andReturn(sadPromise);
});
it("rejects on falsey persistence result", function () {
persistence.persist();
expect(mockQ.reject).toHaveBeenCalled();
});
it("updates previously persisted objects with the persistence service", function () {
// Verify precondition; no call made during constructor
expect(mockPersistenceService.updateObject).not.toHaveBeenCalled();
model.persisted = 12321;
persistence.persist();
expect(mockPersistenceService.updateObject).toHaveBeenCalledWith(
SPACE,
id,
model
);
it("notifies user on persistence failure", function () {
persistence.persist();
expect(mockQ.reject).toHaveBeenCalled();
expect(mockNofificationService.error).toHaveBeenCalled();
});
});
it("reports which persistence space an object belongs to", function () {
expect(persistence.getSpace()).toEqual(SPACE);
});
it("updates persisted timestamp on persistence", function () {
model.modified = 12321;
persistence.persist();
expect(model.persisted).toEqual(12321);
});
it("refreshes the domain object model from persistence", function () {
var refreshModel = { someOtherKey: "some other value" };
mockPersistenceService.readObject.andReturn(asPromise(refreshModel));
persistence.refresh();
expect(model).toEqual(refreshModel);
});
it("does not overwrite unpersisted changes on refresh", function () {
var refreshModel = { someOtherKey: "some other value" },
mockCallback = jasmine.createSpy();
model.modified = 2;
model.persisted = 1;
mockPersistenceService.readObject.andReturn(asPromise(refreshModel));
persistence.refresh().then(mockCallback);
expect(model).not.toEqual(refreshModel);
// Should have also indicated that no changes were actually made
expect(mockCallback).toHaveBeenCalledWith(false);
});
});
}
);

View File

@@ -39,6 +39,15 @@
"glyph": "\u00F4",
"category": "contextual",
"implementation": "actions/GoToOriginalAction.js"
},
{
"key": "locate",
"name": "Set Primary Location",
"description": "Set a domain object's primary location.",
"glyph": "",
"category": "contextual",
"implementation": "actions/SetPrimaryLocationAction.js"
}
],
"components": [
@@ -89,8 +98,7 @@
"name": "Copy Service",
"description": "Provides a service for copying objects",
"implementation": "services/CopyService.js",
"depends": ["$q", "creationService", "policyService",
"persistenceService", "now"]
"depends": ["$q", "policyService", "now"]
},
{
"key": "locationService",

View File

@@ -0,0 +1,60 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define */
define(
function () {
"use strict";
/**
* Implements the "Set Primary Location" action, which sets a
* location property for objects to match their contextual
* location.
*
* @implements {Action}
* @constructor
* @private
* @memberof platform/entanglement
* @param {ActionContext} context the context in which the action
* will be performed
*/
function SetPrimaryLocationAction(context) {
this.domainObject = context.domainObject;
}
SetPrimaryLocationAction.prototype.perform = function () {
var location = this.domainObject.getCapability('location');
return location.setPrimaryLocation(
location.getContextualLocation()
);
};
SetPrimaryLocationAction.appliesTo = function (context) {
var domainObject = context.domainObject;
return domainObject && domainObject.hasCapability("location")
&& (domainObject.getModel().location === undefined);
};
return SetPrimaryLocationAction;
}
);

View File

@@ -28,9 +28,7 @@ define(
var DISALLOWED_ACTIONS = [
"move",
"copy",
"link",
"compose"
"copy"
];
/**

View File

@@ -38,12 +38,9 @@ define(
* @memberof platform/entanglement
* @implements {platform/entanglement.AbstractComposeService}
*/
function CopyService($q, creationService, policyService, persistenceService, now) {
function CopyService($q, policyService) {
this.$q = $q;
this.creationService = creationService;
this.policyService = policyService;
this.persistenceService = persistenceService;
this.now = now;
}
CopyService.prototype.validate = function (object, parentCandidate) {
@@ -71,7 +68,7 @@ define(
*/
CopyService.prototype.perform = function (domainObject, parent) {
var $q = this.$q,
copyTask = new CopyTask(domainObject, parent, this.persistenceService, this.$q, this.now);
copyTask = new CopyTask(domainObject, parent, this.policyService, this.$q);
if (this.validate(domainObject, parent)) {
return copyTask.perform();
} else {

View File

@@ -23,8 +23,8 @@
/*global define */
define(
["uuid"],
function (uuid) {
[],
function () {
"use strict";
/**
@@ -33,36 +33,48 @@ define(
*
* @param domainObject The object to copy
* @param parent The new location of the cloned object tree
* @param persistenceService
* @param $q
* @param now
* @constructor
*/
function CopyTask (domainObject, parent, persistenceService, $q, now){
function CopyTask (domainObject, parent, policyService, $q){
this.domainObject = domainObject;
this.parent = parent;
this.firstClone = undefined;
this.$q = $q;
this.deferred = undefined;
this.persistenceService = persistenceService;
this.policyService = policyService;
this.persisted = 0;
this.now = now;
this.clones = [];
}
function composeChild(child, parent) {
function composeChild(child, parent, setLocation) {
//Once copied, associate each cloned
// composee with its parent clone
child.model.location = parent.id;
parent.model.composition = parent.model.composition || [];
return parent.model.composition.push(child.id);
parent.getModel().composition.push(child.getId());
//If a location is not specified, set it.
if (setLocation && child.getModel().location === undefined) {
child.getModel().location = parent.getId();
}
}
function cloneObjectModel(objectModel) {
var clone = JSON.parse(JSON.stringify(objectModel));
delete clone.composition;
/**
* Reset certain fields.
*/
//If has a composition, set it to an empty array. Will be
// recomposed later with the ids of its cloned children.
if (clone.composition) {
//Important to set it to an empty array here, otherwise
// hasCapability("composition") returns false;
clone.composition = [];
}
delete clone.persisted;
delete clone.modified;
delete clone.location;
return clone;
}
@@ -73,13 +85,10 @@ define(
* result in automatic request batching by the browser.
*/
function persistObjects(self) {
return self.$q.all(self.clones.map(function(clone){
clone.model.persisted = self.now();
return self.persistenceService.createObject(clone.persistenceSpace, clone.id, clone.model)
.then(function(){
self.deferred.notify({phase: "copying", totalObjects: self.clones.length, processed: ++self.persisted});
});
return clone.getCapability("persistence").persist().then(function(){
self.deferred.notify({phase: "copying", totalObjects: self.clones.length, processed: ++self.persisted});
});
})).then(function(){
return self;
});
@@ -89,18 +98,10 @@ define(
* Will add a list of clones to the specified parent's composition
*/
function addClonesToParent(self) {
var parentClone = self.clones[self.clones.length-1];
if (!self.parent.hasCapability('composition')){
return self.$q.reject();
}
return self.persistenceService
.updateObject(parentClone.persistenceSpace, parentClone.id, parentClone.model)
.then(function(){return self.parent.getCapability("composition").add(parentClone.id);})
return self.firstClone.getCapability("persistence").persist()
.then(function(){self.parent.getCapability("composition").add(self.firstClone.getId());})
.then(function(){return self.parent.getCapability("persistence").persist();})
.then(function(){return parentClone;});
// Ensure the clone of the original domainObject is returned
.then(function(){return self.firstClone;});
}
/**
@@ -112,13 +113,16 @@ define(
CopyTask.prototype.copyComposees = function(composees, clonedParent, originalParent){
var self = this;
return (composees || []).reduce(function(promise, composee){
return (composees || []).reduce(function(promise, originalComposee){
//If the composee is composed of other
// objects, chain a promise..
return promise.then(function(){
// ...to recursively copy it (and its children)
return self.copy(composee, originalParent).then(function(composee){
composeChild(composee, clonedParent);
return self.copy(originalComposee, originalParent).then(function(clonedComposee){
//Compose the child within its parent. Cloned
// objects will need to also have their location
// set, however linked objects will not.
return composeChild(clonedComposee, clonedParent, clonedComposee !== originalComposee);
});
});}, self.$q.when(undefined)
);
@@ -131,29 +135,43 @@ define(
* cloning objects, and composing them with their child clones
* as it goes
* @private
* @param originalObject
* @param originalParent
* @returns {*}
* @returns {DomainObject} If the type of the original object allows for
* duplication, then a duplicate of the object, otherwise the object
* itself (to allow linking to non duplicatable objects).
*/
CopyTask.prototype.copy = function(originalObject, originalParent) {
CopyTask.prototype.copy = function(originalObject) {
var self = this,
modelClone = {
id: uuid(),
model: cloneObjectModel(originalObject.getModel()),
persistenceSpace: originalParent.hasCapability('persistence') && originalParent.getCapability('persistence').getSpace()
};
clone;
return this.$q.when(originalObject.useCapability('composition')).then(function(composees){
self.deferred.notify({phase: "preparing"});
//Duplicate the object's children, and their children, and
// so on down to the leaf nodes of the tree.
return self.copyComposees(composees, modelClone, originalObject).then(function (){
//Add the clone to the list of clones that will
//be returned by this function
self.clones.push(modelClone);
return modelClone;
//Check if the type of the object being copied allows for
// creation of new instances. If it does not, then a link to the
// original will be created instead.
if (this.policyService.allow("creation", originalObject.getCapability("type"))){
//create a new clone of the original object. Use the
// creation capability of the targetParent to create the
// new clone. This will ensure that the correct persistence
// space is used.
clone = this.parent.useCapability("instantiation", cloneObjectModel(originalObject.getModel()));
//Iterate through child tree
return this.$q.when(originalObject.useCapability('composition')).then(function(composees){
self.deferred.notify({phase: "preparing"});
//Duplicate the object's children, and their children, and
// so on down to the leaf nodes of the tree.
//If it is a link, don't both with children
return self.copyComposees(composees, clone, originalObject).then(function (){
//Add the clone to the list of clones that will
//be returned by this function
self.clones.push(clone);
return clone;
});
});
});
} else {
//Creating a link, no need to iterate children
return self.$q.when(originalObject);
}
};
/**
@@ -172,7 +190,10 @@ define(
var self = this;
return this.copy(self.domainObject, self.parent).then(function(domainObjectClone){
domainObjectClone.model.location = self.parent.getId();
if (domainObjectClone !== self.domainObject) {
domainObjectClone.getModel().location = self.parent.getId();
}
self.firstClone = domainObjectClone;
return self;
});
};

View File

@@ -0,0 +1,80 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define,describe,beforeEach,it,jasmine,expect */
define(
[
'../../src/actions/SetPrimaryLocationAction',
'../DomainObjectFactory'
],
function (SetPrimaryLocation, domainObjectFactory) {
'use strict';
describe("The 'set primary location' action", function () {
var testContext,
testModel,
testId,
mockLocationCapability,
mockContextCapability;
beforeEach(function () {
testId = "some-id";
testModel = { name: "some name" };
mockLocationCapability = jasmine.createSpyObj(
'location',
[ 'setPrimaryLocation', 'getContextualLocation' ]
);
mockLocationCapability.getContextualLocation.andReturn(testId);
testContext = {
domainObject: domainObjectFactory({
capabilities: {
location: mockLocationCapability
},
model: testModel
})
};
});
it("is applicable to objects with no location specified", function () {
expect(SetPrimaryLocation.appliesTo(testContext))
.toBe(true);
testContext.domainObject.getModel.andReturn({
location: "something",
name: "some name"
});
expect(SetPrimaryLocation.appliesTo(testContext))
.toBe(false);
});
it("sets the location contextually when performed", function () {
new SetPrimaryLocation(testContext).perform();
expect(mockLocationCapability.setPrimaryLocation)
.toHaveBeenCalledWith(testId);
});
});
}
);

View File

@@ -72,7 +72,7 @@ define(
policy = new CrossSpacePolicy();
});
['move', 'copy', 'link', 'compose'].forEach(function (key) {
['move', 'copy'].forEach(function (key) {
describe("for " + key + " actions", function () {
beforeEach(function () {
testActionMetadata.key = key;

View File

@@ -63,7 +63,6 @@ define(
beforeEach(function () {
copyService = new CopyService(
null,
null,
policyService
);
@@ -130,47 +129,50 @@ define(
creationService,
createObjectPromise,
copyService,
mockPersistenceService,
mockNow,
object,
newParent,
copyResult,
copyFinished,
persistObjectPromise,
parentPersistenceCapability,
persistenceCapability,
instantiationCapability,
compositionCapability,
locationCapability,
resolvedValue;
beforeEach(function () {
creationService = jasmine.createSpyObj(
'creationService',
['createObject']
);
createObjectPromise = synchronousPromise(undefined);
creationService.createObject.andReturn(createObjectPromise);
policyService.allow.andReturn(true);
mockPersistenceService = jasmine.createSpyObj(
'persistenceService',
['createObject', 'updateObject']
);
persistObjectPromise = synchronousPromise(undefined);
mockPersistenceService.createObject.andReturn(persistObjectPromise);
mockPersistenceService.updateObject.andReturn(persistObjectPromise);
parentPersistenceCapability = jasmine.createSpyObj(
"persistence",
instantiationCapability = jasmine.createSpyObj(
"instantiation",
[ "invoke" ]
);
persistenceCapability = jasmine.createSpyObj(
"persistenceCapability",
[ "persist", "getSpace" ]
);
persistenceCapability.persist.andReturn(persistObjectPromise);
parentPersistenceCapability.persist.andReturn(persistObjectPromise);
parentPersistenceCapability.getSpace.andReturn("testSpace");
compositionCapability = jasmine.createSpyObj(
'compositionCapability',
['invoke', 'add']
);
mockNow = jasmine.createSpyObj("mockNow", ["now"]);
mockNow.now.andCallFake(function(){
return 1234;
});
locationCapability = jasmine.createSpyObj(
'locationCapability',
['isLink']
);
locationCapability.isLink.andReturn(false);
mockDeferred = jasmine.createSpyObj('mockDeferred', ['notify', 'resolve']);
mockDeferred = jasmine.createSpyObj(
'mockDeferred',
['notify', 'resolve', 'reject']
);
mockDeferred.notify.andCallFake(function(notification){});
mockDeferred.resolve.andCallFake(function(value){resolvedValue = value;});
mockDeferred.promise = {
@@ -179,7 +181,11 @@ define(
}
};
mockQ = jasmine.createSpyObj('mockQ', ['when', 'all', 'reject', 'defer']);
mockQ = jasmine.createSpyObj(
'mockQ',
['when', 'all', 'reject', 'defer']
);
mockQ.reject.andReturn(synchronousPromise(undefined));
mockQ.when.andCallFake(synchronousPromise);
mockQ.all.andCallFake(function (promises) {
var result = {};
@@ -194,6 +200,8 @@ define(
describe("on domain object without composition", function () {
beforeEach(function () {
var objectCopy;
newParent = domainObjectFactory({
name: 'newParent',
id: '456',
@@ -201,7 +209,9 @@ define(
composition: []
},
capabilities: {
persistence: parentPersistenceCapability
instantiation: instantiationCapability,
persistence: persistenceCapability,
composition: compositionCapability
}
});
@@ -210,31 +220,46 @@ define(
id: 'abc',
model: {
name: 'some object',
location: newParent.id,
persisted: mockNow.now()
location: '456',
someOtherAttribute: 'some other value',
embeddedObjectAttribute: {
name: 'Some embedded object'
}
},
capabilities: {
persistence: persistenceCapability
}
});
copyService = new CopyService(mockQ, creationService, policyService, mockPersistenceService, mockNow.now);
objectCopy = domainObjectFactory({
name: 'object',
id: 'abc.copy.fdgdfgdf',
capabilities: {
persistence: persistenceCapability,
location: locationCapability
}
});
instantiationCapability.invoke.andCallFake(
function(model){
objectCopy.model = model;
return objectCopy;
}
);
copyService = new CopyService(mockQ, policyService);
copyResult = copyService.perform(object, newParent);
copyFinished = jasmine.createSpy('copyFinished');
copyResult.then(copyFinished);
});
it("uses persistence service", function () {
expect(mockPersistenceService.createObject)
.toHaveBeenCalledWith(parentPersistenceCapability.getSpace(), jasmine.any(String), object.getModel());
expect(persistObjectPromise.then)
.toHaveBeenCalledWith(jasmine.any(Function));
});
it("uses persistence capability", function () {
expect(persistenceCapability.persist)
.toHaveBeenCalled();
});
it("deep clones object model", function () {
//var newModel = creationService
var newModel = mockPersistenceService
.createObject
.mostRecentCall
.args[2];
var newModel = copyFinished.calls[0].args[0].getModel();
expect(newModel).toEqual(object.model);
expect(newModel).not.toBe(object.model);
});
@@ -249,27 +274,57 @@ define(
describe("on domainObject with composition", function () {
var newObject,
childObject,
compositionCapability,
locationCapability,
objectClone,
childObjectClone,
compositionPromise;
beforeEach(function () {
var invocationCount = 0,
objectClones;
instantiationCapability.invoke.andCallFake(
function(model){
var cloneToReturn = objectClones[invocationCount++];
cloneToReturn.model = model;
return cloneToReturn;
}
);
locationCapability = jasmine.createSpyObj('locationCapability', ['isLink']);
locationCapability.isLink.andReturn(true);
newParent = domainObjectFactory({
name: 'newParent',
id: '456',
model: {
composition: []
},
capabilities: {
instantiation: instantiationCapability,
persistence: persistenceCapability,
composition: compositionCapability
}
});
childObject = domainObjectFactory({
name: 'childObject',
id: 'def',
model: {
name: 'a child object'
name: 'a child object',
location: 'abc'
},
capabilities: {
persistence: persistenceCapability,
location: locationCapability
}
});
compositionCapability = jasmine.createSpyObj(
'compositionCapability',
['invoke', 'add']
);
childObjectClone = domainObjectFactory({
name: 'childObject',
id: 'def.clone',
capabilities: {
persistence: persistenceCapability,
location: locationCapability
}
});
compositionPromise = jasmine.createSpyObj(
'compositionPromise',
['then']
@@ -280,7 +335,7 @@ define(
.andReturn(synchronousPromise([childObject]));
object = domainObjectFactory({
name: 'object',
name: 'some object',
id: 'abc',
model: {
name: 'some object',
@@ -288,36 +343,27 @@ define(
location: 'testLocation'
},
capabilities: {
instantiation: instantiationCapability,
composition: compositionCapability,
location: locationCapability
}
});
newObject = domainObjectFactory({
name: 'object',
id: 'abc2',
model: {
name: 'some object',
composition: []
},
capabilities: {
composition: compositionCapability
}
});
newParent = domainObjectFactory({
name: 'newParent',
id: '456',
model: {
composition: []
},
capabilities: {
composition: compositionCapability,
persistence: parentPersistenceCapability
location: locationCapability,
persistence: persistenceCapability
}
});
createObjectPromise = synchronousPromise(newObject);
creationService.createObject.andReturn(createObjectPromise);
copyService = new CopyService(mockQ, creationService, policyService, mockPersistenceService, mockNow.now);
objectClone = domainObjectFactory({
name: 'some object',
id: 'abc.clone',
capabilities: {
instantiation: instantiationCapability,
composition: compositionCapability,
location: locationCapability,
persistence: persistenceCapability
}
});
objectClones = [objectClone, childObjectClone];
copyService = new CopyService(mockQ, policyService);
});
describe("the cloning process", function(){
@@ -327,10 +373,9 @@ define(
copyResult.then(copyFinished);
});
it("copies object and children in a bottom-up" +
" fashion", function () {
expect(mockPersistenceService.createObject.calls[0].args[2].name).toEqual(childObject.model.name);
expect(mockPersistenceService.createObject.calls[1].args[2].name).toEqual(object.model.name);
it("returns a promise", function () {
expect(copyResult.then).toBeDefined();
expect(copyFinished).toHaveBeenCalled();
});
it("returns a promise", function () {
@@ -338,15 +383,27 @@ define(
expect(copyFinished).toHaveBeenCalled();
});
it("clears modified and sets persisted", function () {
expect(copyFinished.mostRecentCall.args[0].model.modified).toBeUndefined();
expect(copyFinished.mostRecentCall.args[0].model.persisted).toBe(mockNow.now());
});
it ("correctly locates cloned objects", function() {
expect(mockPersistenceService.createObject.calls[0].args[2].location).toEqual(mockPersistenceService.createObject.calls[1].args[1]);
expect(childObjectClone.getModel().location).toEqual(objectClone.getId());
});
});
describe("when cloning non-creatable objects", function() {
beforeEach(function () {
policyService.allow.andCallFake(function(category){
//Return false for 'creation' policy
return category !== 'creation';
});
copyResult = copyService.perform(object, newParent);
copyFinished = jasmine.createSpy('copyFinished');
copyResult.then(copyFinished);
});
it ("creates link instead of clone", function() {
var copiedObject = copyFinished.calls[0].args[0];
expect(copiedObject).toBe(object);
expect(compositionCapability.add).toHaveBeenCalledWith(copiedObject.getId());
//expect(newParent.getModel().composition).toContain(copiedObject.getId());
});
});
});
@@ -355,20 +412,28 @@ define(
object = domainObjectFactory({
name: 'object',
capabilities: {
type: { type: 'object' }
type: { type: 'object' },
location: locationCapability,
persistence: persistenceCapability
}
});
newParent = domainObjectFactory({
name: 'parentCandidate',
capabilities: {
type: { type: 'parentCandidate' }
type: { type: 'parentCandidate' },
instantiation: instantiationCapability,
composition: compositionCapability,
persistence: persistenceCapability
}
});
instantiationCapability.invoke.andReturn(object);
});
it("throws an error", function () {
var copyService =
new CopyService(mockQ, creationService, policyService, mockPersistenceService, mockNow.now);
new CopyService(mockQ, policyService);
function perform() {
copyService.perform(object, newParent);

View File

@@ -4,6 +4,7 @@
"actions/GoToOriginalAction",
"actions/LinkAction",
"actions/MoveAction",
"actions/SetPrimaryLocationAction",
"policies/CrossSpacePolicy",
"services/CopyService",
"services/LinkService",

View File

@@ -45,43 +45,8 @@ define(
* @param {Scope} $scope the controller's Angular scope
*/
function LayoutController($scope) {
var self = this;
// Utility function to copy raw positions from configuration,
// without writing directly to configuration (to avoid triggering
// persistence from watchers during drags).
function shallowCopy(obj, keys) {
var copy = {};
keys.forEach(function (k) {
copy[k] = obj[k];
});
return copy;
}
/**
* Compute panel positions based on the layout's object model.
* Defined as member function to facilitate testing.
* @private
*/
LayoutController.prototype.layoutPanels = function layoutPanels (ids) {
var configuration = $scope.configuration || {};
// Pull panel positions from configuration
self.rawPositions =
shallowCopy(configuration.panels || {}, ids);
// Clear prior computed positions
self.positions = {};
// Update width/height that we are tracking
self.gridSize =
($scope.model || {}).layoutGrid || DEFAULT_GRID_SIZE;
// Compute positions and add defaults where needed
ids.forEach(function (id, index) {
self.populatePosition(id, index);
});
};
var self = this,
callbackCount = 0;
// Update grid size when it changed
function updateGridSize(layoutGrid) {
@@ -127,23 +92,26 @@ define(
e.preventDefault();
}
function getComposition(domainObject){
return domainObject.useCapability('composition');
}
function composeView (composition){
$scope.composition = composition;
return composition.map(function (object) {
return object.getId();
}) || [];
}
//Will fetch fully contextualized composed objects, and populate
// scope with them.
function refreshComposition() {
return getComposition($scope.domainObject)
.then(composeView)
.then(self.layoutPanels);
//Keep a track of how many composition callbacks have been made
var thisCount = ++callbackCount;
$scope.domainObject.useCapability('composition').then(function(composition){
var ids;
//Is this callback for the most recent composition
// request? If not, discard it. Prevents race condition
if (thisCount === callbackCount){
ids = composition.map(function (object) {
return object.getId();
}) || [];
$scope.composition = composition;
self.layoutPanels(ids);
}
});
}
// End drag; we don't want to put $scope into this
@@ -176,7 +144,7 @@ define(
$scope.$watch("model.layoutGrid", updateGridSize);
// Update composed objects on screen, and position panes
$scope.$watch("model.composition", refreshComposition);
$scope.$watchCollection("model.composition", refreshComposition);
// Position panes where they are dropped
$scope.$on("mctDrop", handleDrop);
@@ -282,6 +250,43 @@ define(
}
};
// Utility function to copy raw positions from configuration,
// without writing directly to configuration (to avoid triggering
// persistence from watchers during drags).
function shallowCopy(obj, keys) {
var copy = {};
keys.forEach(function (k) {
copy[k] = obj[k];
});
return copy;
}
/**
* Compute panel positions based on the layout's object model.
* Defined as member function to facilitate testing.
* @private
*/
LayoutController.prototype.layoutPanels = function (ids) {
var configuration = this.$scope.configuration || {},
self = this;
// Pull panel positions from configuration
this.rawPositions =
shallowCopy(configuration.panels || {}, ids);
// Clear prior computed positions
this.positions = {};
// Update width/height that we are tracking
this.gridSize =
(this.$scope.model || {}).layoutGrid || DEFAULT_GRID_SIZE;
// Compute positions and add defaults where needed
ids.forEach(function (id, index) {
self.populatePosition(id, index);
});
};
/**
* End the active drag gesture. This will update the
* view configuration.

View File

@@ -33,7 +33,8 @@ define(
testConfiguration,
controller,
mockCompositionCapability,
mockComposition;
mockComposition,
mockCompositionObjects;
function mockPromise(value){
return {
@@ -57,7 +58,7 @@ define(
beforeEach(function () {
mockScope = jasmine.createSpyObj(
"$scope",
[ "$watch", "$on", "commit" ]
[ "$watch", "$watchCollection", "$on", "commit" ]
);
mockEvent = jasmine.createSpyObj(
'event',
@@ -67,6 +68,7 @@ define(
testModel = {};
mockComposition = ["a", "b", "c"];
mockCompositionObjects = mockComposition.map(mockDomainObject);
testConfiguration = {
panels: {
@@ -77,7 +79,7 @@ define(
}
};
mockCompositionCapability = mockPromise(mockComposition.map(mockDomainObject));
mockCompositionCapability = mockPromise(mockCompositionObjects);
mockScope.domainObject = mockDomainObject("mockDomainObject");
mockScope.model = testModel;
@@ -91,14 +93,14 @@ define(
// Model changes will indicate that panel positions
// may have changed, for instance.
it("watches for changes to composition", function () {
expect(mockScope.$watch).toHaveBeenCalledWith(
expect(mockScope.$watchCollection).toHaveBeenCalledWith(
"model.composition",
jasmine.any(Function)
);
});
it("Retrieves updated composition from composition capability", function () {
mockScope.$watch.mostRecentCall.args[1]();
mockScope.$watchCollection.mostRecentCall.args[1]();
expect(mockScope.domainObject.useCapability).toHaveBeenCalledWith(
"composition"
);
@@ -107,8 +109,32 @@ define(
);
});
it("Is robust to concurrent changes to composition", function () {
var secondMockComposition = ["a", "b", "c", "d"],
secondMockCompositionObjects = secondMockComposition.map(mockDomainObject),
firstCompositionCB,
secondCompositionCB;
spyOn(mockCompositionCapability, "then");
mockScope.$watchCollection.mostRecentCall.args[1]();
mockScope.$watchCollection.mostRecentCall.args[1]();
firstCompositionCB = mockCompositionCapability.then.calls[0].args[0];
secondCompositionCB = mockCompositionCapability.then.calls[1].args[0];
//Resolve promises in reverse order
secondCompositionCB(secondMockCompositionObjects);
firstCompositionCB(mockCompositionObjects);
//Expect the promise call that was initiated most recently to
// be the one used to populate scope, irrespective of order that
// it was eventually resolved
expect(mockScope.composition).toBe(secondMockCompositionObjects);
});
it("provides styles for frames, from configuration", function () {
mockScope.$watch.mostRecentCall.args[1]();
mockScope.$watchCollection.mostRecentCall.args[1]();
expect(controller.getFrameStyle("a")).toEqual({
top: "320px",
left: "640px",
@@ -121,7 +147,7 @@ define(
var styleB, styleC;
// b and c do not have configured positions
mockScope.$watch.mostRecentCall.args[1]();
mockScope.$watchCollection.mostRecentCall.args[1]();
styleB = controller.getFrameStyle("b");
styleC = controller.getFrameStyle("c");
@@ -138,7 +164,7 @@ define(
it("allows panels to be dragged", function () {
// Populate scope
mockScope.$watch.mostRecentCall.args[1]();
mockScope.$watchCollection.mostRecentCall.args[1]();
// Verify precondtion
expect(testConfiguration.panels.b).not.toBeDefined();
@@ -157,7 +183,7 @@ define(
it("invokes commit after drag", function () {
// Populate scope
mockScope.$watch.mostRecentCall.args[1]();
mockScope.$watchCollection.mostRecentCall.args[1]();
// Do a drag
controller.startDrag("b", [1, 1], [0, 0]);
@@ -218,7 +244,7 @@ define(
// White-boxy; we know which watch is which
mockScope.$watch.calls[0].args[1](testModel.layoutGrid);
mockScope.$watch.calls[1].args[1](testModel.composition);
mockScope.$watchCollection.calls[0].args[1](testModel.composition);
styleB = controller.getFrameStyle("b");

View File

@@ -146,6 +146,7 @@ define(
if (canvas.width !== canvas.offsetWidth ||
canvas.height !== canvas.offsetHeight) {
doDraw(scope.draw);
scope.$apply();
}
}
@@ -181,7 +182,7 @@ define(
canvas.addEventListener("webglcontextlost", fallbackFromWebGL);
// Check for resize, on a timer
activeInterval = $interval(drawIfResized, 1000);
activeInterval = $interval(drawIfResized, 1000, 0, false);
// Watch "draw" for external changes to the set of
// things to be drawn.

View File

@@ -45,8 +45,10 @@ define(
jasmine.createSpy("$interval");
mockLog =
jasmine.createSpyObj("$log", ["warn", "info", "debug"]);
mockScope =
jasmine.createSpyObj("$scope", ["$watchCollection", "$on"]);
mockScope = jasmine.createSpyObj(
"$scope",
["$watchCollection", "$on", "$apply"]
);
mockElement =
jasmine.createSpyObj("element", ["find", "html"]);
mockInterval.cancel = jasmine.createSpy("cancelInterval");
@@ -152,7 +154,9 @@ define(
// Should track canvas size in an interval
expect(mockInterval).toHaveBeenCalledWith(
jasmine.any(Function),
jasmine.any(Number)
jasmine.any(Number),
0,
false
);
// Verify pre-condition

View File

@@ -79,6 +79,9 @@ define(
// Used to choose which form control to use
key: "=",
// Allow controls to trigger blur-like events
ngBlur: "&",
// The state of the form value itself
ngModel: "=",

View File

@@ -80,7 +80,7 @@ define(
// Update the indicator initially, and start polling.
updateIndicator();
$interval(updateIndicator, interval, false);
$interval(updateIndicator, interval, 0, false);
}
ElasticIndicator.prototype.getGlyph = function () {

View File

@@ -55,6 +55,7 @@ define(
expect(mockInterval).toHaveBeenCalledWith(
jasmine.any(Function),
testInterval,
0,
false
);
});

View File

@@ -97,7 +97,7 @@ define(
counter = 0,
couldRepresent = false,
couldEdit = false,
lastId,
lastIdPath = [],
lastKey,
changeTemplate = templateLinker.link($scope, element);
@@ -144,15 +144,31 @@ define(
});
}
function unchanged(canRepresent, canEdit, id, key) {
function unchanged(canRepresent, canEdit, idPath, key) {
return canRepresent &&
couldRepresent &&
id === lastId &&
key === lastKey &&
idPath.length === lastIdPath.length &&
idPath.every(function (id, i) {
return id === lastIdPath[i];
}) &&
canEdit &&
couldEdit;
}
function getIdPath(domainObject) {
if (!domainObject) {
return [];
}
if (!domainObject.hasCapability('context')) {
return [domainObject.getId()];
}
return domainObject.getCapability('context')
.getPath().map(function (pathObject) {
return pathObject.getId();
});
}
// General-purpose refresh mechanism; should set up the scope
// as appropriate for current representation key and
// domain object.
@@ -163,10 +179,10 @@ define(
uses = ((representation || {}).uses || []),
canRepresent = !!(path && domainObject),
canEdit = !!(domainObject && domainObject.hasCapability('editor')),
id = domainObject && domainObject.getId(),
idPath = getIdPath(domainObject),
key = $scope.key;
if (unchanged(canRepresent, canEdit, id, key)) {
if (unchanged(canRepresent, canEdit, idPath, key)) {
return;
}
@@ -194,8 +210,8 @@ define(
// To allow simplified change detection next time around
couldRepresent = canRepresent;
lastIdPath = idPath;
couldEdit = canEdit;
lastId = id;
lastKey = key;
// Populate scope with fields associated with the current

View File

@@ -247,6 +247,54 @@ define(
mockScope.$watch.calls[0].args[1]();
expect(mockScope.testCapability).toBeUndefined();
});
it("detects changes among linked instances", function () {
var mockContext = jasmine.createSpyObj('context', ['getPath']),
mockContext2 = jasmine.createSpyObj('context', ['getPath']),
mockLink = jasmine.createSpyObj(
'linkedObject',
DOMAIN_OBJECT_METHODS
),
mockParent = jasmine.createSpyObj(
'parentObject',
DOMAIN_OBJECT_METHODS
),
callCount;
mockDomainObject.getCapability.andCallFake(function (c) {
return c === 'context' && mockContext;
});
mockLink.getCapability.andCallFake(function (c) {
return c === 'context' && mockContext2;
});
mockDomainObject.hasCapability.andCallFake(function (c) {
return c === 'context';
});
mockLink.hasCapability.andCallFake(function (c) {
return c === 'context';
});
mockLink.getModel.andReturn({});
mockContext.getPath.andReturn([mockDomainObject]);
mockContext2.getPath.andReturn([mockParent, mockLink]);
mockLink.getId.andReturn('test-id');
mockDomainObject.getId.andReturn('test-id');
mockParent.getId.andReturn('parent-id');
mockScope.key = "abc";
mockScope.domainObject = mockDomainObject;
mockScope.$watch.calls[0].args[1]();
callCount = mockChangeTemplate.calls.length;
mockScope.domainObject = mockLink;
mockScope.$watch.calls[0].args[1]();
expect(mockChangeTemplate.calls.length)
.toEqual(callCount + 1);
});
});
}
);