mirror of
https://github.com/nasa/openmct.git
synced 2025-01-19 03:06:54 +00:00
Merged from Master
This commit is contained in:
commit
18607e9404
@ -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
|
||||||
|
|
||||||
|
@ -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
161
docs/src/process/cycle.md
Normal 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.")
|
||||||
|
|
||||||
|
|
@ -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.
|
||||||
|
127
docs/src/process/testing/plan.md
Normal file
127
docs/src/process/testing/plan.md
Normal 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.
|
169
docs/src/process/testing/procedures.md
Normal file
169
docs/src/process/testing/procedures.md
Normal 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.)
|
@ -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() {
|
||||||
|
@ -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",
|
||||||
|
@ -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,
|
||||||
|
45
platform/commonUI/browse/src/creation/CreationPolicy.js
Normal file
45
platform/commonUI/browse/src/creation/CreationPolicy.js
Normal 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;
|
||||||
|
}
|
||||||
|
);
|
@ -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]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
53
platform/commonUI/browse/test/creation/CreationPolicySpec.js
Normal file
53
platform/commonUI/browse/test/creation/CreationPolicySpec.js
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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));
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
@ -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": [
|
||||||
|
@ -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%;
|
||||||
|
@ -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>
|
||||||
|
@ -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">C</span>
|
<span class="l-time-range-inputs-elem ui-symbol type-icon">C</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>
|
</mct-control>
|
||||||
</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">
|
||||||
|
64
platform/commonUI/general/src/UnsupportedBrowserWarning.js
Normal file
64
platform/commonUI/general/src/UnsupportedBrowserWarning.js
Normal 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;
|
||||||
|
}
|
||||||
|
);
|
@ -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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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 () {
|
||||||
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
@ -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 () {
|
||||||
|
@ -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 = {
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
@ -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"
|
||||||
]
|
]
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -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",
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -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",
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
@ -28,9 +28,7 @@ define(
|
|||||||
|
|
||||||
var DISALLOWED_ACTIONS = [
|
var DISALLOWED_ACTIONS = [
|
||||||
"move",
|
"move",
|
||||||
"copy",
|
"copy"
|
||||||
"link",
|
|
||||||
"compose"
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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",
|
||||||
|
@ -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.
|
||||||
|
@ -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");
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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: "=",
|
||||||
|
|
||||||
|
@ -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 () {
|
||||||
|
@ -55,6 +55,7 @@ define(
|
|||||||
expect(mockInterval).toHaveBeenCalledWith(
|
expect(mockInterval).toHaveBeenCalledWith(
|
||||||
jasmine.any(Function),
|
jasmine.any(Function),
|
||||||
testInterval,
|
testInterval,
|
||||||
|
0,
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
2
pom.xml
2
pom.xml
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user