Merged from Master

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

View File

@ -291,7 +291,7 @@ checklist.)
1. Changes address original issue? 1. Changes address original issue?
2. Unit tests included and/or updated with changes? 2. Unit tests included and/or updated with changes?
3. Command line build passes? 3. Command line build passes?
4. Expect to pass code review? 4. Changes have been smoke-tested?
### Reviewer Checklist ### Reviewer Checklist

View File

@ -941,6 +941,12 @@ look at field (see below) to determine which field in the model should be
modified. modified.
* `ngRequired`: True if input is required. * `ngRequired`: True if input is required.
* `ngPattern`: The pattern to match against (for text entry) * `ngPattern`: The pattern to match against (for text entry)
* `ngBlur`: A function that may be invoked to evaluate the expression
associated with the `ng-blur` attribute associated with the control.
* This should be called when the control has lost focus; for controls
which simply wrap or augment `input` elements, this should be fired
on `blur` events associated with those elements, while more complex
custom controls may fire this at the end of more specific interactions.
* `options`: The options for this control, as passed from the `options` property * `options`: The options for this control, as passed from the `options` property
of an individual row definition. of an individual row definition.
* `field`: Name of the field in `ngModel` which will hold the value for this * `field`: Name of the field in `ngModel` which will hold the value for this

161
docs/src/process/cycle.md Normal file
View File

@ -0,0 +1,161 @@
# Development Cycle
Development of Open MCT Web occurs on an iterative cycle of
sprints and releases.
* A _sprint_ is three weeks in duration, and represents a
set of improvements that can be completed and tested by the
development team. Software at the end of the sprint is
"semi-stable"; it will have undergone reduced testing and may carry
defects or usability issues of lower severity, particularly if
there are workarounds.
* A _release_ occurs every four sprints. Releases are stable, and
will have undergone full acceptance testing to ensure that the
software behaves correctly and usably.
## Roles
The sprint process assumes the presence of a __project manager.__
The project manager is responsible for
making tactical decisions about what development work will be
performed, and for coordinating with stakeholders to arrive at
higher-level strategic decisions about desired functionality
and characteristics of the software, major external milestones,
and so forth.
In the absence of a dedicated project manager, this role may be rotated
among members of the development team on a per-sprint basis.
Responsibilities of the project manager including:
* Maintaining (with agreement of stakeholders) a "road map" of work
planned for future releases/sprints; this should be higher-level,
usually expressed as "themes",
with just enough specificity to gauge feasibility of plans,
relate work back to milestones, and identify longer-term
dependencies.
* Determining (with assistance from the rest of the team) which
issues to work on in a given sprint and how they shall be
assigned.
* Pre-planning subsequent sprints to ensure that all members of the
team always have a clear direction.
* Scheduling and/or ensuring adherence to
[process points](#process-points).
* Responding to changes within the sprint (shifting priorities,
new issues) and re-allocating work for the sprint as needed.
## Sprint Calendar
Certain [process points](#process-points) are regularly scheduled in
the sprint cycle.
### Sprints by Release
Allocation of work among sprints should be planned relative to release
goals and milestones. As a general guideline, higher-risk work (large
new features which may carry new defects, major refactoring, design
changes with uncertain effects on usability) should be allocated to
earlier sprints, allowing for time in later sprints to ensure stability.
| Sprint | Focus |
|:------:|:--------------------------------------------------------|
| __1__ | Prototyping, design, experimentation. |
| __2__ | New features, refinements, enhancements. |
| __3__ | Feature completion, low-risk enhancements, bug fixing. |
| __4__ | Stability & quality assurance. |
### Sprints 1-3
The first three sprints of a release are primarily centered around
development work, with regular acceptance testing in the third
week. During this third week, the top priority should be passing
acceptance testing (e.g. by resolving any blockers found); any
resources not needed for this effort should be used to begin work
for the subsequent sprint.
| Week | Mon | Tue | Wed | Thu | Fri |
|:-----:|:-------------------------:|:------:|:---:|:----------------------------:|:-----------:|
| __1__ | Sprint plan | Tag-up | | | |
| __2__ | | Tag-up | | | Code freeze |
| __3__ | Per-sprint testing | Triage | | _Per-sprint testing*_ | Ship |
* If necessary.
### Sprint 4
The software must be stable at the end of the fourth sprint; because of
this, the fourth sprint is scheduled differently, with a heightened
emphasis on testing.
| Week | Mon | Tue | Wed | Thu | Fri |
|-------:|:-------------------------:|:------:|:---:|:----------------------------:|:-----------:|
| __1__ | Sprint plan | Tag-up | | | Code freeze |
| __2__ | Per-release testing | Triage | | | |
| __3__ | _Per-release testing*_ | Triage | | _Per-release testing*_ | Ship |
* If necessary.
## Process Points
* __Sprint plan.__ Project manager allocates issues based on
theme(s) for sprint, then reviews with team. Each team member
should have roughly two weeks of work allocated (to allow time
in the third week for testing of work completed.)
* Project manager should also sketch out subsequent sprint so
that team may begin work for that sprint during the
third week, since testing and blocker resolution is unlikely
to require all available resources.
* __Tag-up.__ Check in and status update among development team.
May amend plan for sprint as-needed.
* __Code freeze.__ Any new work from this sprint
(features, bug fixes, enhancements) must be integrated by the
end of the second week of the sprint. After code freeze
(and until the end of the sprint) the only changes that should be
merged into the master branch should directly address issues
needed to pass acceptance testing.
* [__Per-release Testing.__](testing/plan.md#per-release-testing)
Structured testing with predefined
success criteria. No release should ship without passing
acceptance tests. Time is allocated in each sprint for subsequent
rounds of acceptance testing if issues are identified during a
prior round. Specific details of acceptance testing need to be
agreed-upon with relevant stakeholders and delivery recipients,
and should be flexible enough to allow changes to plans
(e.g. deferring delivery of some feature in order to ensure
stability of other features.) Baseline testing includes:
* [__Testathon.__](testing/plan.md#user-testing)
Multi-user testing, involving as many users as
is feasible, plus development team. Open-ended; should verify
completed work from this sprint, test exploratorily for
regressions, et cetera.
* [__Long-Duration Test.__](testing/plan.md#long-duration-testing) A
test to verify that the software remains
stable after running for longer durations. May include some
combination of automated testing and user verification (e.g.
checking to verify that software remains subjectively
responsive at conclusion of test.)
* [__Unit Testing.__](testing/plan.md#unit-testing)
Automated testing integrated into the
build. (These tests are verified to pass more often than once
per sprint, as they run before any merge to master, but still
play an important role in per-release testing.)
* [__Per-sprint Testing.__](testing/plan.md#per-sprint-testing)
Subset of Pre-release Testing
which should be performed before shipping at the end of any
sprint. Time is allocated for a second round of
Pre-release Testing if the first round is not passed.
* __Triage.__ Team reviews issues from acceptance testing and uses
success criteria to determine whether or not they should block
release, then formulates a plan to address these issues before
the next round of acceptance testing. Focus here should be on
ensuring software passes that testing in order to ship on time;
may prefer to disable malfunctioning components and fix them
in a subsequent sprint, for example.
* __Ship.__ Tag a code snapshot that has passed acceptance
testing and deploy that version. (Only true if acceptance
testing has passed by this point; if acceptance testing has not
been passed, will need to make ad hoc decisions with stakeholders,
e.g. "extend the sprint" or "defer shipment until end of next
sprint.")

View File

@ -1,156 +1,13 @@
# Development Cycle # Development Process
Development of Open MCT Web occurs on an iterative cycle of
sprints and releases.
* A _sprint_ is three weeks in duration, and represents a
set of improvements that can be completed and tested by the
development team. Software at the end of the sprint is
"semi-stable"; it will have undergone reduced testing and may carry
defects or usability issues of lower severity, particularly if
there are workarounds.
* A _release_ occurs every four sprints. Releases are stable, and
will have undergone full acceptance testing to ensure that the
software behaves correctly and usably.
## Roles
The sprint process assumes the presence of a __project manager.__
The project manager is responsible for
making tactical decisions about what development work will be
performed, and for coordinating with stakeholders to arrive at
higher-level strategic decisions about desired functionality
and characteristics of the software, major external milestones,
and so forth.
In the absence of a dedicated project manager, this role may be rotated
among members of the development team on a per-sprint basis.
Responsibilities of the project manager including:
* Maintaining (with agreement of stakeholders) a "road map" of work
planned for future releases/sprints; this should be higher-level,
usually expressed as "themes",
with just enough specificity to gauge feasibility of plans,
relate work back to milestones, and identify longer-term
dependencies.
* Determining (with assistance from the rest of the team) which
issues to work on in a given sprint and how they shall be
assigned.
* Pre-planning subsequent sprints to ensure that all members of the
team always have a clear direction.
* Scheduling and/or ensuring adherence to
[process points](#process-points).
* Responding to changes within the sprint (shifting priorities,
new issues) and re-allocating work for the sprint as needed.
## Sprint Calendar
Certain [process points](#process-points) are regularly scheduled in
the sprint cycle.
### Sprints by Release
Allocation of work among sprints should be planned relative to release
goals and milestones. As a general guideline, higher-risk work (large
new features which may carry new defects, major refactoring, design
changes with uncertain effects on usability) should be allocated to
earlier sprints, allowing for time in later sprints to ensure stability.
| Sprint | Focus |
|:------:|:--------------------------------------------------------|
| __1__ | Prototyping, design, experimentation. |
| __2__ | New features, refinements, enhancements. |
| __3__ | Feature completion, low-risk enhancements, bug fixing. |
| __4__ | Stability & quality assurance. |
### Sprints 1-3
The first three sprints of a release are primarily centered around
development work, with regular acceptance testing in the third
week. During this third week, the top priority should be passing
acceptance testing (e.g. by resolving any blockers found); any
resources not needed for this effort should be used to begin work
for the subsequent sprint.
| Week | Mon | Tue | Wed | Thu | Fri |
|:-----:|:-------------------------:|:------:|:---:|:----------------------------:|:-----------:|
| __1__ | Sprint plan | Tag-up | | | |
| __2__ | | Tag-up | | | Code freeze |
| __3__ | Sprint acceptance testing | Triage | | _Sprint acceptance testing*_ | Ship |
* If necessary.
### Sprint 4
The software must be stable at the end of the fourth sprint; because of
this, the fourth sprint is scheduled differently, with a heightened
emphasis on testing.
| Week | Mon | Tue | Wed | Thu | Fri |
|-------:|:-------------------------:|:------:|:---:|:----------------------------:|:-----------:|
| __1__ | Sprint plan | Tag-up | | | Code freeze |
| __2__ | Acceptance testing | Triage | | | |
| __3__ | _Acceptance testing*_ | Triage | | _Acceptance testing*_ | Ship |
* If necessary.
## Process Points
* __Sprint plan.__ Project manager allocates issues based on
theme(s) for sprint, then reviews with team. Each team member
should have roughly two weeks of work allocated (to allow time
in the third week for testing of work completed.)
* Project manager should also sketch out subsequent sprint so
that team may begin work for that sprint during the
third week, since testing and blocker resolution is unlikely
to require all available resources.
* __Tag-up.__ Check in and status update among development team.
May amend plan for sprint as-needed.
* __Code freeze.__ Any new work from this sprint
(features, bug fixes, enhancements) must be integrated by the
end of the second week of the sprint. After code freeze
(and until the end of the sprint) the only changes that should be
merged into the master branch should directly address issues
needed to pass acceptance testing.
* __Acceptance Testing.__ Structured testing with predefined
success criteria. No release should ship without passing
acceptance tests. Time is allocated in each sprint for subsequent
rounds of acceptance testing if issues are identified during a
prior round. Specific details of acceptance testing need to be
agreed-upon with relevant stakeholders and delivery recipients,
and should be flexible enough to allow changes to plans
(e.g. deferring delivery of some feature in order to ensure
stability of other features.) Baseline testing includes:
* __Testathon.__ Multi-user testing, involving as many users as
is feasible, plus development team. Open-ended; should verify
completed work from this sprint, test exploratorily for
regressions, et cetera.
* __24-Hour Test.__ A test to verify that the software remains
stable after running for longer durations. May include some
combination of automated testing and user verification (e.g.
checking to verify that software remains subjectively
responsive at conclusion of test.)
* __Automated Testing.__ Automated testing integrated into the
build. (These tests are verified to pass more often than once
per sprint, as they run before any merge to master, but still
play an important role in acceptance testing.)
* __Sprint Acceptance Testing.__ Subset of Acceptance Testing
which should be performed before shipping at the end of any
sprint. Time is allocated for a second round of
Sprint Acceptance Testing if the first round is not passed.
* __Triage.__ Team reviews issues from acceptance testing and uses
success criteria to determine whether or not they should block
release, then formulates a plan to address these issues before
the next round of acceptance testing. Focus here should be on
ensuring software passes that testing in order to ship on time;
may prefer to disable malfunctioning components and fix them
in a subsequent sprint, for example.
* __Ship.__ Tag a code snapshot that has passed acceptance
testing and deploy that version. (Only true if acceptance
testing has passed by this point; if acceptance testing has not
been passed, will need to make ad hoc decisions with stakeholders,
e.g. "extend the sprint" or "defer shipment until end of next
sprint.")
The process used to develop Open MCT Web is described in the following
documents:
* [Development Cycle](cycle.md): Describes how and when specific
process points are repeated during development.
* Testing is described in two documents:
* The [Test Plan](testing/plan.md) summarizes the approaches used
to test Open MCT Web.
* The [Test Procedures](testing/procedures.md) document what
specific tests are performed to verify correctness, and how
they should be carried out.

View File

@ -0,0 +1,127 @@
# Test Plan
## Test Levels
Testing for Open MCT Web includes:
* _Smoke testing_: Brief, informal testing to verify that no major issues
or regressions are present in the software, or in specific features of
the software.
* _Unit testing_: Automated verification of the performance of individual
software components.
* _User testing_: Testing with a representative user base to verify
that application behaves usably and as specified.
* _Long-duration testing_: Testing which takes place over a long period
of time to detect issues which are not readily noticeable during
shorter test periods.
### Smoke Testing
Manual, non-rigorous testing of the software and/or specific features
of interest. Verifies that the software runs and that basic functionality
is present.
### Unit Testing
Unit tests are automated tests which exercise individual software
components. Tests are subject to code review along with the actual
implementation, to ensure that tests are applicable and useful.
Unit tests should meet
[test standards](https://github.com/nasa/openmctweb/blob/master/CONTRIBUTING.md#test-standards)
as described in the contributing guide.
### User Testing
User testing is performed at scheduled times involving target users
of the software or reasonable representatives, along with members of
the development team exercising known use cases. Users test the
software directly; the software should be configured as similarly to
its planned production configuration as is feasible without introducing
other risks (e.g. damage to data in a production instance.)
User testing will focus on the following activities:
* Verifying issues resolved since the last test session.
* Checking for regressions in areas related to recent changes.
* Using major or important features of the software,
as determined by the user.
* General "trying to break things."
During user testing, users will
[report issues](https://github.com/nasa/openmctweb/blob/master/CONTRIBUTING.md#issue-reporting)
as they are encountered.
Desired outcomes of user testing are:
* Identified software defects.
* Areas for usability improvement.
* Feature requests (particularly missed requirements.)
* Recorded issue verification.
### Long-duration Testing
Long-duration testing occurs over a twenty-four hour period. The
software is run in one or more stressing cases representative of expected
usage. After twenty-four hours, the software is evaluated for:
* Performance metrics: Have memory usage or CPU utilization increased
during this time period in unexpected or undesirable ways?
* Subjective usability: Does the software behave in the same way it did
at the start of the test? Is it as responsive?
Any defects or unexpected behavior identified during testing should be
[reported as issues](https://github.com/nasa/openmctweb/blob/master/CONTRIBUTING.md#issue-reporting)
and reviewed for severity.
## Test Performance
Tests are performed at various levels of frequency.
* _Per-merge_: Performed before any new changes are integrated into
the software.
* _Per-sprint_: Performed at the end of every [sprint](../cycle.md).
* _Per-release_: Performed at the end of every [release](../cycle.md).
### Per-merge Testing
Before changes are merged, the author of the changes must perform:
* _Smoke testing_ (both generally, and for areas which interact with
the new changes.)
* _Unit testing_ (as part of the automated build step.)
Changes are not merged until the author has affirmed that both
forms of testing have been performed successfully; this is documented
by the [Author Checklist](https://github.com/nasa/openmctweb/blob/master/CONTRIBUTING.md#author-checklist).
### Per-sprint Testing
Before a sprint is closed, the development team must additionally
perform:
* A relevant subset of [_user testing_](procedures.md#user-test-procedures)
identified by the acting [project manager](../cycle.md#roles).
* [_Long-duration testing_](procedures.md#long-duration-testng)
(specifically, for 24 hours.)
Issues are reported as a product of both forms of testing.
A sprint is not closed until both categories have been performed on
the latest snapshot of the software, _and_ no issues labelled as
["blocker"](https://github.com/nasa/openmctweb/blob/master/CONTRIBUTING.md#issue-reporting)
remain open.
### Per-release Testing
As [per-sprint testing](#per-sprint-testing), except that _user testing_
should cover all test cases, with less focus on changes from the specific
sprint or release.
Per-release testing should also include any acceptance testing steps
agreed upon with recipients of the software.
A release is not closed until both categories have been performed on
the latest snapshot of the software, _and_ no issues labelled as
["blocker" or "critical"](https://github.com/nasa/openmctweb/blob/master/CONTRIBUTING.md#issue-reporting)
remain open.

View File

@ -0,0 +1,169 @@
# Test Procedures
## Introduction
This document is intended to be used:
* By testers, to verify that Open MCT Web behaves as specified.
* By the development team, to document new test cases and to provide
guidance on how to author these.
## Writing Procedures
### Template
Procedures for individual tests should use the following template,
adapted from [https://swehb.nasa.gov/display/7150/SWE-114]().
Property | Value
---------------|---------------------------------------------------------------
Test ID |
Relevant reqs. |
Prerequisites |
Test input |
Instructions |
Expectation |
Eval. criteria |
For multi-line descriptions, use an asterisk or similar indicator to refer
to a longer-form description below.
#### Example Procedure - Edit a Layout
Property | Value
---------------|---------------------------------------------------------------
Test ID | MCT-TEST-000X - Edit a layout
Relevant reqs. | MCT-EDIT-000Y
Prerequisites | Create a layout, as in MCT-TEST-000Z
Test input | Domain object database XYZ
Instructions | See below *
Expectation | Change to editing context †
Eval. criteria | Visual inspection
* Follow the following steps:
1. Verify that the created layout is currently navigated-to,
as in MCT-TEST-00ZZ.
2. Click the Edit button, identified by a pencil icon and the text "Edit"
displayed on hover.
† Right-hand viewing area should be surrounded by a dashed
blue border when a domain object is being edited.
### Guidelines
Test procedures should be written assuming minimal prior knowledge of the
application: Non-standard terms should only be used when they are documented
in [the glossary](#glossary), and shorthands used for user actions should
be accompanied by useful references to test procedures describing those
actions (when available) or descriptions in user documentation.
Test cases should be narrow in scope; if a list of steps is excessively
long (or must be written vaguely to be kept short) it should be broken
down into multiple tests which reference one another.
All requirements satisfied by Open MCT Web should be verifiable using
one or more test procedures.
## Glossary
This section will contain terms used in test procedures. This may link to
a common glossary, to avoid replication of content.
## Procedures
This section will contain specific test procedures. Presently, procedures
are placeholders describing general patterns for setting up and conducting
testing.
### User Testing Setup
These procedures describes a general pattern for setting up for user
testing. Specific deployments should customize this pattern with
relevant data and any additional steps necessary.
Property | Value
---------------|---------------------------------------------------------------
Test ID | MCT-TEST-SETUP0 - User Testing Setup
Relevant reqs. | TBD
Prerequisites | Build of relevant components
Test input | Exemplary database; exemplary telemetry data set
Instructions | See below
Expectation | Able to load application in a web browser (Google Chrome)
Eval. criteria | Visual inspection
Instructions:
1. Start telemetry server.
2. Start ElasticSearch.
3. Restore database snapshot to ElasticSearch.
4. Start telemetry playback.
5. Start HTTP server for client sources.
### User Test Procedures
Specific user test cases have not yet been authored. In their absence,
user testing is conducted by:
* Reviewing the text of issues from the issue tracker to understand the
desired behavior, and exercising this behavior in the running application.
(For instance, by following steps to reproduce from the original issue.)
* Issues which appear to be resolved should be marked as such with comments
on the original issue (e.g. "verified during user testing MM/DD/YYYY".)
* Issues which appear not to have been resolved should be reopened with an
explanation of what unexpected behavior has been observed.
* In cases where an issue appears resolved as-worded but other related
undesirable behavior is observed during testing, a new issue should be
opened, and linked to from a comment in the original issues.
* General usage of new features and/or existing features which have undergone
recent changes. Defects or problems with usability should be documented
by filing issues in the issue tracker.
* Open-ended testing to discover defects, identify usability issues, and
generate feature requests.
### Long-Duration Testing
The purpose of long-duration testing is to identify performance issues
and/or other defects which are sensitive to the amount of time the
application is kept running. (Memory leaks, for instance.)
Property | Value
---------------|---------------------------------------------------------------
Test ID | MCT-TEST-LDT0 - Long-duration Testing
Relevant reqs. | TBD
Prerequisites | MCT-TEST-SETUP0
Test input | (As for test setup.)
Instructions | See "Instructions" below *
Expectation | See "Expectations" below †
Eval. criteria | Visual inspection
* Instructions:
1. Start `top` or a similar tool to measure CPU usage and memory utilization.
2. Open several user-created displays (as many as would be realistically
opened during actual usage in a stressing case) in some combination of
separate tabs and windows (approximately as many tabs-per-window as
total windows.)
3. Ensure that playback data is set to run continuously for at least 24 hours
(e.g. on a loop.)
4. Record CPU usage and memory utilization.
5. In at least one tab, try some general user interface gestures and make
notes about the subjective experience of using the application. (Particularly,
the degree of responsiveness.)
6. Leave client displays open for 24 hours.
7. Record CPU usage and memory utilization again.
8. Make additional notes about the subjective experience of using the
application (again, particularly responsiveness.)
9. Check logs for any unexpected warnings or errors.
† Expectations:
* At the end of the test, CPU usage and memory usage should both be similar
to their levels at the start of the test.
* At the end of the test, subjective usage of the application should not
be observably different from the way it was at the start of the test.
(In particular, responsiveness should not decrease.)
* Logs should not contain any unexpected warnings or errors ("expected"
warnings or errors are those that have been documented and prioritized
as known issues, or those that are explained by transient conditions
external to the software, such as network outages.)

View File

@ -39,8 +39,11 @@ define(
start = Date.now(); start = Date.now();
function update() { function update() {
var secs = (Date.now() - start) / 1000; var now = Date.now(),
secs = (now - start) / 1000;
displayed = Math.round(digests / secs); displayed = Math.round(digests / secs);
start = now;
digests = 0;
} }
function increment() { function increment() {

View File

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

View File

@ -68,7 +68,7 @@ define(
// Introduce one create action per type // Introduce one create action per type
return this.typeService.listTypes().filter(function (type) { return this.typeService.listTypes().filter(function (type) {
return type.hasFeature("creation"); return self.policyService.allow("creation", type);
}).map(function (type) { }).map(function (type) {
return new CreateAction( return new CreateAction(
type, 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, var mockTypeService,
mockDialogService, mockDialogService,
mockCreationService, mockCreationService,
mockPolicyService,
mockCreationPolicy,
mockPolicyMap = {},
mockTypes, mockTypes,
provider; provider;
@ -67,14 +70,32 @@ define(
"creationService", "creationService",
[ "createObject" ] [ "createObject" ]
); );
mockPolicyService = jasmine.createSpyObj(
"policyService",
[ "allow" ]
);
mockTypes = [ "A", "B", "C" ].map(createMockType); 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); mockTypeService.listTypes.andReturn(mockTypes);
provider = new CreateActionProvider( provider = new CreateActionProvider(
mockTypeService, mockTypeService,
mockDialogService, mockDialogService,
mockCreationService mockCreationService,
mockPolicyService
); );
}); });
@ -94,15 +115,15 @@ define(
it("does not expose non-creatable types", function () { it("does not expose non-creatable types", function () {
// One of the types won't have the creation feature... // 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. // ...so it should have been filtered out.
expect(provider.getActions({ expect(provider.getActions({
key: "create", key: "create",
domainObject: {} domainObject: {}
}).length).toEqual(2); }).length).toEqual(2);
// Make sure it was creation which was used to check // Make sure it was creation which was used to check
expect(mockTypes[1].hasFeature) expect(mockPolicyService.allow)
.toHaveBeenCalledWith("creation"); .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/CreateMenuController",
"creation/CreateWizard", "creation/CreateWizard",
"creation/CreationService", "creation/CreationService",
"creation/CreationPolicy",
"creation/LocatorController", "creation/LocatorController",
"navigation/NavigateAction", "navigation/NavigateAction",
"navigation/NavigationService", "navigation/NavigationService",

View File

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

View File

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

View File

@ -31,6 +31,7 @@ define(
mockEditableObject, mockEditableObject,
mockDomainObject, mockDomainObject,
mockCache, mockCache,
mockPromise,
capability; capability;
beforeEach(function () { beforeEach(function () {
@ -50,7 +51,9 @@ define(
"cache", "cache",
[ "markDirty" ] [ "markDirty" ]
); );
mockPromise = jasmine.createSpyObj("promise", ["then"]);
mockCache.markDirty.andReturn(mockPromise);
mockDomainObject.getCapability.andReturn(mockPersistence); mockDomainObject.getCapability.andReturn(mockPersistence);
capability = new EditablePersistenceCapability( capability = new EditablePersistenceCapability(
@ -84,6 +87,10 @@ define(
expect(mockPersistence.refresh).toHaveBeenCalled(); 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", "implementation": "StyleSheetLoader.js",
"depends": [ "stylesheets[]", "$document", "THEME" ] "depends": [ "stylesheets[]", "$document", "THEME" ]
},
{
"implementation": "UnsupportedBrowserWarning.js",
"depends": [ "notificationService", "agentService" ]
} }
], ],
"stylesheets": [ "stylesheets": [

View File

@ -36,29 +36,29 @@ $mobileTreeRightArrowW: 30px;
/************************** DEVICE WIDTHS */ /************************** DEVICE WIDTHS */
// IMPORTANT! Usage assumes that ranges are mutually exclusive and have no gaps // IMPORTANT! Usage assumes that ranges are mutually exclusive and have no gaps
$phoMaxW: 514px; $phoMaxW: 767px;
$tabMinW: 515px; $tabMinW: 768px;
$tabMaxW: 1280px; $tabMaxW: 1024px;
$desktopMinW: 1281px; $desktopMinW: 1025px;
/************************** MEDIA QUERIES: WINDOW CHECKS FOR SPECIFIC ORIENTATIONS FOR EACH DEVICE */ /************************** MEDIA QUERIES: WINDOW CHECKS FOR SPECIFIC ORIENTATIONS FOR EACH DEVICE */
$screenPortrait: "screen and (orientation: portrait)"; $screenPortrait: "(orientation: portrait)";
$screenLandscape: "screen and (orientation: landscape)"; $screenLandscape: "(orientation: landscape)";
$mobileDevice: "(max-device-width: #{$tabMaxW})"; //$mobileDevice: "(max-device-width: #{$tabMaxW})";
$phoneCheck: "(max-device-width: #{$phoMaxW})"; $phoneCheck: "(max-device-width: #{$phoMaxW})";
$tabletCheck: $mobileDevice; $tabletCheck: "(min-device-width: #{$tabMinW}) and (max-device-width: #{$tabMaxW})";
$desktopCheck: "(min-device-width: #{$desktopMinW})"; $desktopCheck: "(min-device-width: #{$desktopMinW}) and (-webkit-min-device-pixel-ratio: 1)";
/************************** MEDIA QUERIES: WINDOWS FOR SPECIFIC ORIENTATIONS FOR EACH DEVICE */ /************************** MEDIA QUERIES: WINDOWS FOR SPECIFIC ORIENTATIONS FOR EACH DEVICE */
$phonePortrait: "#{$screenPortrait} and #{$phoneCheck} and #{$mobileDevice}"; $phonePortrait: "only screen and #{$screenPortrait} and #{$phoneCheck}";
$phoneLandscape: "#{$screenLandscape} and #{$phoneCheck} and #{$mobileDevice}"; $phoneLandscape: "only screen and #{$screenLandscape} and #{$phoneCheck}";
$tabletPortrait: "#{$screenPortrait} and #{$tabletCheck} and #{$mobileDevice}"; $tabletPortrait: "only screen and #{$screenPortrait} and #{$tabletCheck}";
$tabletLandscape: "#{$screenLandscape} and #{$tabletCheck} and #{$mobileDevice}"; $tabletLandscape: "only screen and #{$screenLandscape} and #{$tabletCheck}";
$desktop: "screen and #{$desktopCheck}"; $desktop: "only screen and #{$desktopCheck}";
/************************** DEVICE PARAMETERS FOR MENUS/REPRESENTATIONS */ /************************** DEVICE PARAMETERS FOR MENUS/REPRESENTATIONS */
$proporMenuOnly: 90%; $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" <span class="s-btn"
ng-controller="DateTimeFieldController"> ng-controller="DateTimeFieldController">
<input type="text" <input type="text"
ng-model="textValue" ng-model="textValue"
ng-blur="restoreTextValue(); ngBlur()"
ng-class="{ error: textInvalid }"> ng-class="{ error: textInvalid }">
</input> </input>
<a class="ui-symbol icon icon-calendar" <a class="ui-symbol icon icon-calendar"
@ -11,8 +33,8 @@
<mct-popup ng-if="picker.active"> <mct-popup ng-if="picker.active">
<div mct-click-elsewhere="picker.active = false"> <div mct-click-elsewhere="picker.active = false">
<mct-control key="'datetime-picker'" <mct-control key="'datetime-picker'"
ng-model="ngModel" ng-model="pickerModel"
field="field" field="'value'"
options="{ hours: true }"> options="{ hours: true }">
</mct-control> </mct-control>
</div> </div>

View File

@ -20,12 +20,14 @@
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<div ng-controller="TimeRangeController"> <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-inputs-elem ui-symbol type-icon">&#x43;</span>
<span class="l-time-range-input"> <span class="l-time-range-input">
<mct-control key="'datetime-field'" <mct-control key="'datetime-field'"
structure="{ format: parameters.format }" structure="{ format: parameters.format }"
ng-model="ngModel.outer" ng-model="formModel"
ng-blur="updateBoundsFromForm()"
field="'start'" field="'start'"
class="time-range-start"> class="time-range-start">
</mct-control> </mct-control>
@ -36,12 +38,15 @@
<span class="l-time-range-input" ng-controller="ToggleController as t2"> <span class="l-time-range-input" ng-controller="ToggleController as t2">
<mct-control key="'datetime-field'" <mct-control key="'datetime-field'"
structure="{ format: parameters.format }" structure="{ format: parameters.format }"
ng-model="ngModel.outer" ng-model="formModel"
ng-blur="updateBoundsFromForm()"
field="'end'" field="'end'"
class="time-range-end"> class="time-range-end">
</mct-control>&nbsp; </mct-control>&nbsp;
</span> </span>
</div>
<input type="submit" class="hidden">
</form>
<div class="l-time-range-slider-holder"> <div class="l-time-range-slider-holder">
<div class="l-time-range-slider"> <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) { formatter.parse($scope.textValue) !== value) {
$scope.textValue = formatter.format(value); $scope.textValue = formatter.format(value);
$scope.textInvalid = false; $scope.textInvalid = false;
$scope.lastValidValue = $scope.textValue;
} }
$scope.pickerModel = { value: value };
} }
function updateFromView(textValue) { function updateFromView(textValue) {
@ -61,6 +63,17 @@ define(
if (!$scope.textInvalid) { if (!$scope.textInvalid) {
$scope.ngModel[$scope.field] = $scope.ngModel[$scope.field] =
formatter.parse(textValue); 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]); updateFromModel($scope.ngModel[$scope.field]);
} }
function restoreTextValue() {
$scope.textValue = $scope.lastValidValue;
updateFromView($scope.textValue);
}
$scope.restoreTextValue = restoreTextValue;
$scope.picker = { active: false }; $scope.picker = { active: false };
$scope.$watch('structure.format', setFormat); $scope.$watch('structure.format', setFormat);
$scope.$watch('ngModel[field]', updateFromModel); $scope.$watch('ngModel[field]', updateFromModel);
$scope.$watch('pickerModel.value', updateFromPicker);
$scope.$watch('textValue', updateFromView); $scope.$watch('textValue', updateFromView);
} }

View File

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

View File

@ -204,7 +204,7 @@ define(
// And poll for position changes enforced by styles // And poll for position changes enforced by styles
activeInterval = $interval(function () { activeInterval = $interval(function () {
getSetPosition(getSetPosition()); getSetPosition(getSetPosition());
}, POLLING_INTERVAL, false); }, POLLING_INTERVAL, 0, false);
// ...and stop polling when we're destroyed. // ...and stop polling when we're destroyed.
$scope.$on('$destroy', function () { $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.ngModel = { testField: 12321 };
mockScope.field = "testField"; mockScope.field = "testField";
mockScope.structure = { format: "someFormat" }; mockScope.structure = { format: "someFormat" };
mockScope.ngBlur = jasmine.createSpy('blur');
controller = new DateTimeFieldController( controller = new DateTimeFieldController(
mockScope, mockScope,
mockFormatService mockFormatService
); );
}); fireWatch("ngModel[field]", mockScope.ngModel.testField);
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();
}); });
it("updates text from model values", function () { it("updates text from model values", function () {
@ -91,16 +83,55 @@ define(
expect(mockScope.textValue).toEqual("1977-05-25 17:30:00"); 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 () { it("exposes toggle state for date-time picker", function () {
expect(mockScope.picker.active).toBe(false); expect(mockScope.picker.active).toBe(false);
}); });
describe("when user input is invalid", function () { describe("when user input is invalid", function () {
var newText, oldValue; var newText, oldText, oldValue;
beforeEach(function () { beforeEach(function () {
newText = "Not a date"; newText = "Not a date";
oldValue = mockScope.ngModel.testField; oldValue = mockScope.ngModel.testField;
oldText = mockScope.textValue;
mockScope.textValue = newText; mockScope.textValue = newText;
fireWatch("textValue", newText); fireWatch("textValue", newText);
}); });
@ -116,6 +147,11 @@ define(
it("does not modify user input", function () { it("does not modify user input", function () {
expect(mockScope.textValue).toEqual(newText); 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 () { it("does not modify valid but irregular user input", function () {

View File

@ -91,6 +91,39 @@ define(
.toHaveBeenCalledWith("ngModel", jasmine.any(Function)); .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 () { describe("when dragged", function () {
beforeEach(function () { beforeEach(function () {
mockScope.ngModel = { 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/MCTPopup",
"directives/MCTResize", "directives/MCTResize",
"directives/MCTScroll", "directives/MCTScroll",
"directives/MCTSplitPane",
"services/Popup", "services/Popup",
"services/PopupService", "services/PopupService",
"services/UrlService", "services/UrlService",
"StyleSheetLoader" "StyleSheetLoader",
"UnsupportedBrowserWarning"
] ]

View File

@ -43,6 +43,7 @@ define(
var userAgent = $window.navigator.userAgent, var userAgent = $window.navigator.userAgent,
matches = userAgent.match(/iPad|iPhone|Android/i) || []; matches = userAgent.match(/iPad|iPhone|Android/i) || [];
this.userAgent = userAgent;
this.mobileName = matches[0]; this.mobileName = matches[0];
this.$window = $window; this.$window = $window;
} }
@ -91,6 +92,18 @@ define(
return !this.isPortrait(); 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; return AgentService;
} }
); );

View File

@ -81,6 +81,13 @@ define(
expect(agentService.isPortrait()).toBeTruthy(); expect(agentService.isPortrait()).toBeTruthy();
expect(agentService.isLandscape()).toBeFalsy(); 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", "key": "persistence",
"implementation": "capabilities/PersistenceCapability.js", "implementation": "capabilities/PersistenceCapability.js",
"depends": [ "persistenceService", "identifierService" ] "depends": [ "persistenceService", "identifierService",
"notificationService", "$q" ]
}, },
{ {
"key": "metadata", "key": "metadata",

View File

@ -20,6 +20,7 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
/*global define*/ /*global define*/
/*jslint es5: true */
define( define(
@ -47,6 +48,8 @@ define(
function PersistenceCapability( function PersistenceCapability(
persistenceService, persistenceService,
identifierService, identifierService,
notificationService,
$q,
domainObject domainObject
) { ) {
// Cache modified timestamp // Cache modified timestamp
@ -55,6 +58,8 @@ define(
this.domainObject = domainObject; this.domainObject = domainObject;
this.identifierService = identifierService; this.identifierService = identifierService;
this.persistenceService = persistenceService; this.persistenceService = persistenceService;
this.notificationService = notificationService;
this.$q = $q;
} }
// Utility function for creating promise-like objects which // Utility function for creating promise-like objects which
@ -72,6 +77,46 @@ define(
return parts.length > 1 ? parts.slice(1).join(":") : id; 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 * Persist any changes which have been made to this
* domain object's model. * domain object's model.
@ -80,7 +125,8 @@ define(
* if not. * if not.
*/ */
PersistenceCapability.prototype.persist = function () { PersistenceCapability.prototype.persist = function () {
var domainObject = this.domainObject, var self = this,
domainObject = this.domainObject,
model = domainObject.getModel(), model = domainObject.getModel(),
modified = model.modified, modified = model.modified,
persistenceService = this.persistenceService, persistenceService = this.persistenceService,
@ -98,7 +144,11 @@ define(
this.getSpace(), this.getSpace(),
getKey(domainObject.getId()), getKey(domainObject.getId()),
domainObject.getModel() 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. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ /*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
/*jslint es5: true */
/** /**
* PersistenceCapabilitySpec. Created by vwoeltje on 11/6/14. * PersistenceCapabilitySpec. Created by vwoeltje on 11/6/14.
@ -34,24 +35,36 @@ define(
mockIdentifierService, mockIdentifierService,
mockDomainObject, mockDomainObject,
mockIdentifier, mockIdentifier,
mockNofificationService,
mockQ,
id = "object id", id = "object id",
model = { someKey: "some value"}, model,
SPACE = "some space", SPACE = "some space",
persistence; persistence,
happyPromise;
function asPromise(value) { function asPromise(value, doCatch) {
return (value || {}).then ? value : { return (value || {}).then ? value : {
then: function (callback) { then: function (callback) {
return asPromise(callback(value)); 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 () { beforeEach(function () {
happyPromise = asPromise(true);
model = { someKey: "some value", name: "domain object"};
mockPersistenceService = jasmine.createSpyObj( mockPersistenceService = jasmine.createSpyObj(
"persistenceService", "persistenceService",
[ "updateObject", "readObject", "createObject", "deleteObject" ] [ "updateObject", "readObject", "createObject", "deleteObject" ]
); );
mockIdentifierService = jasmine.createSpyObj( mockIdentifierService = jasmine.createSpyObj(
'identifierService', 'identifierService',
[ 'parse', 'generate' ] [ 'parse', 'generate' ]
@ -60,6 +73,15 @@ define(
'identifier', 'identifier',
[ 'getSpace', 'getKey', 'getDefinedSpace' ] [ 'getSpace', 'getKey', 'getDefinedSpace' ]
); );
mockQ = jasmine.createSpyObj(
"$q",
["reject"]
);
mockNofificationService = jasmine.createSpyObj(
"notificationService",
["error"]
);
mockDomainObject = { mockDomainObject = {
getId: function () { return id; }, getId: function () { return id; },
getModel: function () { return model; }, getModel: function () { return model; },
@ -76,66 +98,99 @@ define(
persistence = new PersistenceCapability( persistence = new PersistenceCapability(
mockPersistenceService, mockPersistenceService,
mockIdentifierService, mockIdentifierService,
mockNofificationService,
mockQ,
mockDomainObject mockDomainObject
); );
}); });
it("creates unpersisted objects with the persistence service", function () { describe("successful persistence", function() {
// Verify precondition; no call made during constructor beforeEach(function () {
expect(mockPersistenceService.createObject).not.toHaveBeenCalled(); 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( expect(mockPersistenceService.createObject).toHaveBeenCalledWith(
SPACE, SPACE,
id, id,
model 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 () { it("notifies user on persistence failure", function () {
// Verify precondition; no call made during constructor persistence.persist();
expect(mockPersistenceService.updateObject).not.toHaveBeenCalled(); expect(mockQ.reject).toHaveBeenCalled();
expect(mockNofificationService.error).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);
});
}); });
} }
); );

View File

@ -39,6 +39,15 @@
"glyph": "\u00F4", "glyph": "\u00F4",
"category": "contextual", "category": "contextual",
"implementation": "actions/GoToOriginalAction.js" "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": [ "components": [
@ -89,8 +98,7 @@
"name": "Copy Service", "name": "Copy Service",
"description": "Provides a service for copying objects", "description": "Provides a service for copying objects",
"implementation": "services/CopyService.js", "implementation": "services/CopyService.js",
"depends": ["$q", "creationService", "policyService", "depends": ["$q", "policyService", "now"]
"persistenceService", "now"]
}, },
{ {
"key": "locationService", "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 = [ var DISALLOWED_ACTIONS = [
"move", "move",
"copy", "copy"
"link",
"compose"
]; ];
/** /**

View File

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

View File

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

View File

@ -63,7 +63,6 @@ define(
beforeEach(function () { beforeEach(function () {
copyService = new CopyService( copyService = new CopyService(
null,
null, null,
policyService policyService
); );
@ -130,47 +129,50 @@ define(
creationService, creationService,
createObjectPromise, createObjectPromise,
copyService, copyService,
mockPersistenceService,
mockNow, mockNow,
object, object,
newParent, newParent,
copyResult, copyResult,
copyFinished, copyFinished,
persistObjectPromise, persistObjectPromise,
parentPersistenceCapability, persistenceCapability,
instantiationCapability,
compositionCapability,
locationCapability,
resolvedValue; resolvedValue;
beforeEach(function () { beforeEach(function () {
creationService = jasmine.createSpyObj(
'creationService',
['createObject']
);
createObjectPromise = synchronousPromise(undefined); createObjectPromise = synchronousPromise(undefined);
creationService.createObject.andReturn(createObjectPromise);
policyService.allow.andReturn(true); policyService.allow.andReturn(true);
mockPersistenceService = jasmine.createSpyObj(
'persistenceService',
['createObject', 'updateObject']
);
persistObjectPromise = synchronousPromise(undefined); persistObjectPromise = synchronousPromise(undefined);
mockPersistenceService.createObject.andReturn(persistObjectPromise);
mockPersistenceService.updateObject.andReturn(persistObjectPromise); instantiationCapability = jasmine.createSpyObj(
"instantiation",
parentPersistenceCapability = jasmine.createSpyObj( [ "invoke" ]
"persistence", );
persistenceCapability = jasmine.createSpyObj(
"persistenceCapability",
[ "persist", "getSpace" ] [ "persist", "getSpace" ]
); );
persistenceCapability.persist.andReturn(persistObjectPromise);
parentPersistenceCapability.persist.andReturn(persistObjectPromise); compositionCapability = jasmine.createSpyObj(
parentPersistenceCapability.getSpace.andReturn("testSpace"); 'compositionCapability',
['invoke', 'add']
);
mockNow = jasmine.createSpyObj("mockNow", ["now"]); locationCapability = jasmine.createSpyObj(
mockNow.now.andCallFake(function(){ 'locationCapability',
return 1234; ['isLink']
}); );
locationCapability.isLink.andReturn(false);
mockDeferred = jasmine.createSpyObj('mockDeferred', ['notify', 'resolve']); mockDeferred = jasmine.createSpyObj(
'mockDeferred',
['notify', 'resolve', 'reject']
);
mockDeferred.notify.andCallFake(function(notification){}); mockDeferred.notify.andCallFake(function(notification){});
mockDeferred.resolve.andCallFake(function(value){resolvedValue = value;}); mockDeferred.resolve.andCallFake(function(value){resolvedValue = value;});
mockDeferred.promise = { 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.when.andCallFake(synchronousPromise);
mockQ.all.andCallFake(function (promises) { mockQ.all.andCallFake(function (promises) {
var result = {}; var result = {};
@ -194,6 +200,8 @@ define(
describe("on domain object without composition", function () { describe("on domain object without composition", function () {
beforeEach(function () { beforeEach(function () {
var objectCopy;
newParent = domainObjectFactory({ newParent = domainObjectFactory({
name: 'newParent', name: 'newParent',
id: '456', id: '456',
@ -201,7 +209,9 @@ define(
composition: [] composition: []
}, },
capabilities: { capabilities: {
persistence: parentPersistenceCapability instantiation: instantiationCapability,
persistence: persistenceCapability,
composition: compositionCapability
} }
}); });
@ -210,31 +220,46 @@ define(
id: 'abc', id: 'abc',
model: { model: {
name: 'some object', name: 'some object',
location: newParent.id, location: '456',
persisted: mockNow.now() 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); copyResult = copyService.perform(object, newParent);
copyFinished = jasmine.createSpy('copyFinished'); copyFinished = jasmine.createSpy('copyFinished');
copyResult.then(copyFinished); copyResult.then(copyFinished);
}); });
it("uses persistence service", function () { it("uses persistence capability", function () {
expect(mockPersistenceService.createObject) expect(persistenceCapability.persist)
.toHaveBeenCalledWith(parentPersistenceCapability.getSpace(), jasmine.any(String), object.getModel()); .toHaveBeenCalled();
});
expect(persistObjectPromise.then)
.toHaveBeenCalledWith(jasmine.any(Function));
});
it("deep clones object model", function () { it("deep clones object model", function () {
//var newModel = creationService var newModel = copyFinished.calls[0].args[0].getModel();
var newModel = mockPersistenceService
.createObject
.mostRecentCall
.args[2];
expect(newModel).toEqual(object.model); expect(newModel).toEqual(object.model);
expect(newModel).not.toBe(object.model); expect(newModel).not.toBe(object.model);
}); });
@ -249,27 +274,57 @@ define(
describe("on domainObject with composition", function () { describe("on domainObject with composition", function () {
var newObject, var newObject,
childObject, childObject,
compositionCapability, objectClone,
locationCapability, childObjectClone,
compositionPromise; compositionPromise;
beforeEach(function () { 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']); newParent = domainObjectFactory({
locationCapability.isLink.andReturn(true); name: 'newParent',
id: '456',
model: {
composition: []
},
capabilities: {
instantiation: instantiationCapability,
persistence: persistenceCapability,
composition: compositionCapability
}
});
childObject = domainObjectFactory({ childObject = domainObjectFactory({
name: 'childObject', name: 'childObject',
id: 'def', id: 'def',
model: { model: {
name: 'a child object' name: 'a child object',
location: 'abc'
},
capabilities: {
persistence: persistenceCapability,
location: locationCapability
} }
}); });
compositionCapability = jasmine.createSpyObj(
'compositionCapability', childObjectClone = domainObjectFactory({
['invoke', 'add'] name: 'childObject',
); id: 'def.clone',
capabilities: {
persistence: persistenceCapability,
location: locationCapability
}
});
compositionPromise = jasmine.createSpyObj( compositionPromise = jasmine.createSpyObj(
'compositionPromise', 'compositionPromise',
['then'] ['then']
@ -280,7 +335,7 @@ define(
.andReturn(synchronousPromise([childObject])); .andReturn(synchronousPromise([childObject]));
object = domainObjectFactory({ object = domainObjectFactory({
name: 'object', name: 'some object',
id: 'abc', id: 'abc',
model: { model: {
name: 'some object', name: 'some object',
@ -288,36 +343,27 @@ define(
location: 'testLocation' location: 'testLocation'
}, },
capabilities: { capabilities: {
instantiation: instantiationCapability,
composition: compositionCapability, composition: compositionCapability,
location: locationCapability location: locationCapability,
} persistence: persistenceCapability
});
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
} }
}); });
createObjectPromise = synchronousPromise(newObject); objectClone = domainObjectFactory({
creationService.createObject.andReturn(createObjectPromise); name: 'some object',
copyService = new CopyService(mockQ, creationService, policyService, mockPersistenceService, mockNow.now); 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(){ describe("the cloning process", function(){
@ -327,10 +373,9 @@ define(
copyResult.then(copyFinished); copyResult.then(copyFinished);
}); });
it("copies object and children in a bottom-up" + it("returns a promise", function () {
" fashion", function () { expect(copyResult.then).toBeDefined();
expect(mockPersistenceService.createObject.calls[0].args[2].name).toEqual(childObject.model.name); expect(copyFinished).toHaveBeenCalled();
expect(mockPersistenceService.createObject.calls[1].args[2].name).toEqual(object.model.name);
}); });
it("returns a promise", function () { it("returns a promise", function () {
@ -338,15 +383,27 @@ define(
expect(copyFinished).toHaveBeenCalled(); 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() { 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({ object = domainObjectFactory({
name: 'object', name: 'object',
capabilities: { capabilities: {
type: { type: 'object' } type: { type: 'object' },
location: locationCapability,
persistence: persistenceCapability
} }
}); });
newParent = domainObjectFactory({ newParent = domainObjectFactory({
name: 'parentCandidate', name: 'parentCandidate',
capabilities: { capabilities: {
type: { type: 'parentCandidate' } type: { type: 'parentCandidate' },
instantiation: instantiationCapability,
composition: compositionCapability,
persistence: persistenceCapability
} }
}); });
instantiationCapability.invoke.andReturn(object);
}); });
it("throws an error", function () { it("throws an error", function () {
var copyService = var copyService =
new CopyService(mockQ, creationService, policyService, mockPersistenceService, mockNow.now); new CopyService(mockQ, policyService);
function perform() { function perform() {
copyService.perform(object, newParent); copyService.perform(object, newParent);

View File

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

View File

@ -45,43 +45,8 @@ define(
* @param {Scope} $scope the controller's Angular scope * @param {Scope} $scope the controller's Angular scope
*/ */
function LayoutController($scope) { function LayoutController($scope) {
var self = this; var self = this,
callbackCount = 0;
// 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);
});
};
// Update grid size when it changed // Update grid size when it changed
function updateGridSize(layoutGrid) { function updateGridSize(layoutGrid) {
@ -127,23 +92,26 @@ define(
e.preventDefault(); 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 //Will fetch fully contextualized composed objects, and populate
// scope with them. // scope with them.
function refreshComposition() { function refreshComposition() {
return getComposition($scope.domainObject) //Keep a track of how many composition callbacks have been made
.then(composeView) var thisCount = ++callbackCount;
.then(self.layoutPanels);
$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 // End drag; we don't want to put $scope into this
@ -176,7 +144,7 @@ define(
$scope.$watch("model.layoutGrid", updateGridSize); $scope.$watch("model.layoutGrid", updateGridSize);
// Update composed objects on screen, and position panes // Update composed objects on screen, and position panes
$scope.$watch("model.composition", refreshComposition); $scope.$watchCollection("model.composition", refreshComposition);
// Position panes where they are dropped // Position panes where they are dropped
$scope.$on("mctDrop", handleDrop); $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 * End the active drag gesture. This will update the
* view configuration. * view configuration.

View File

@ -33,7 +33,8 @@ define(
testConfiguration, testConfiguration,
controller, controller,
mockCompositionCapability, mockCompositionCapability,
mockComposition; mockComposition,
mockCompositionObjects;
function mockPromise(value){ function mockPromise(value){
return { return {
@ -57,7 +58,7 @@ define(
beforeEach(function () { beforeEach(function () {
mockScope = jasmine.createSpyObj( mockScope = jasmine.createSpyObj(
"$scope", "$scope",
[ "$watch", "$on", "commit" ] [ "$watch", "$watchCollection", "$on", "commit" ]
); );
mockEvent = jasmine.createSpyObj( mockEvent = jasmine.createSpyObj(
'event', 'event',
@ -67,6 +68,7 @@ define(
testModel = {}; testModel = {};
mockComposition = ["a", "b", "c"]; mockComposition = ["a", "b", "c"];
mockCompositionObjects = mockComposition.map(mockDomainObject);
testConfiguration = { testConfiguration = {
panels: { panels: {
@ -77,7 +79,7 @@ define(
} }
}; };
mockCompositionCapability = mockPromise(mockComposition.map(mockDomainObject)); mockCompositionCapability = mockPromise(mockCompositionObjects);
mockScope.domainObject = mockDomainObject("mockDomainObject"); mockScope.domainObject = mockDomainObject("mockDomainObject");
mockScope.model = testModel; mockScope.model = testModel;
@ -91,14 +93,14 @@ define(
// Model changes will indicate that panel positions // Model changes will indicate that panel positions
// may have changed, for instance. // may have changed, for instance.
it("watches for changes to composition", function () { it("watches for changes to composition", function () {
expect(mockScope.$watch).toHaveBeenCalledWith( expect(mockScope.$watchCollection).toHaveBeenCalledWith(
"model.composition", "model.composition",
jasmine.any(Function) jasmine.any(Function)
); );
}); });
it("Retrieves updated composition from composition capability", function () { it("Retrieves updated composition from composition capability", function () {
mockScope.$watch.mostRecentCall.args[1](); mockScope.$watchCollection.mostRecentCall.args[1]();
expect(mockScope.domainObject.useCapability).toHaveBeenCalledWith( expect(mockScope.domainObject.useCapability).toHaveBeenCalledWith(
"composition" "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 () { it("provides styles for frames, from configuration", function () {
mockScope.$watch.mostRecentCall.args[1](); mockScope.$watchCollection.mostRecentCall.args[1]();
expect(controller.getFrameStyle("a")).toEqual({ expect(controller.getFrameStyle("a")).toEqual({
top: "320px", top: "320px",
left: "640px", left: "640px",
@ -121,7 +147,7 @@ define(
var styleB, styleC; var styleB, styleC;
// b and c do not have configured positions // b and c do not have configured positions
mockScope.$watch.mostRecentCall.args[1](); mockScope.$watchCollection.mostRecentCall.args[1]();
styleB = controller.getFrameStyle("b"); styleB = controller.getFrameStyle("b");
styleC = controller.getFrameStyle("c"); styleC = controller.getFrameStyle("c");
@ -138,7 +164,7 @@ define(
it("allows panels to be dragged", function () { it("allows panels to be dragged", function () {
// Populate scope // Populate scope
mockScope.$watch.mostRecentCall.args[1](); mockScope.$watchCollection.mostRecentCall.args[1]();
// Verify precondtion // Verify precondtion
expect(testConfiguration.panels.b).not.toBeDefined(); expect(testConfiguration.panels.b).not.toBeDefined();
@ -157,7 +183,7 @@ define(
it("invokes commit after drag", function () { it("invokes commit after drag", function () {
// Populate scope // Populate scope
mockScope.$watch.mostRecentCall.args[1](); mockScope.$watchCollection.mostRecentCall.args[1]();
// Do a drag // Do a drag
controller.startDrag("b", [1, 1], [0, 0]); controller.startDrag("b", [1, 1], [0, 0]);
@ -218,7 +244,7 @@ define(
// White-boxy; we know which watch is which // White-boxy; we know which watch is which
mockScope.$watch.calls[0].args[1](testModel.layoutGrid); 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"); styleB = controller.getFrameStyle("b");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -247,6 +247,54 @@ define(
mockScope.$watch.calls[0].args[1](); mockScope.$watch.calls[0].args[1]();
expect(mockScope.testCapability).toBeUndefined(); 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);
});
}); });
} }
); );

View File

@ -6,7 +6,7 @@
<groupId>gov.nasa.arc.wtd</groupId> <groupId>gov.nasa.arc.wtd</groupId>
<artifactId>open-mct-web</artifactId> <artifactId>open-mct-web</artifactId>
<name>Open MCT Web</name> <name>Open MCT Web</name>
<version>0.8.2-SNAPSHOT</version> <version>0.8.3-SNAPSHOT</version>
<packaging>war</packaging> <packaging>war</packaging>
<properties> <properties>