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?
|
||||
2. Unit tests included and/or updated with changes?
|
||||
3. Command line build passes?
|
||||
4. Expect to pass code review?
|
||||
4. Changes have been smoke-tested?
|
||||
|
||||
### Reviewer Checklist
|
||||
|
||||
|
@ -941,6 +941,12 @@ look at field (see below) to determine which field in the model should be
|
||||
modified.
|
||||
* `ngRequired`: True if input is required.
|
||||
* `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
|
||||
of an individual row definition.
|
||||
* `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 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.")
|
||||
# Development Process
|
||||
|
||||
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();
|
||||
|
||||
function update() {
|
||||
var secs = (Date.now() - start) / 1000;
|
||||
var now = Date.now(),
|
||||
secs = (now - start) / 1000;
|
||||
displayed = Math.round(digests / secs);
|
||||
start = now;
|
||||
digests = 0;
|
||||
}
|
||||
|
||||
function increment() {
|
||||
|
@ -105,6 +105,12 @@
|
||||
"implementation": "navigation/NavigationService.js"
|
||||
}
|
||||
],
|
||||
"policies": [
|
||||
{
|
||||
"implementation": "creation/CreationPolicy.js",
|
||||
"category": "creation"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"key": "navigate",
|
||||
|
@ -68,7 +68,7 @@ define(
|
||||
|
||||
// Introduce one create action per type
|
||||
return this.typeService.listTypes().filter(function (type) {
|
||||
return type.hasFeature("creation");
|
||||
return self.policyService.allow("creation", type);
|
||||
}).map(function (type) {
|
||||
return new CreateAction(
|
||||
type,
|
||||
|
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,
|
||||
mockDialogService,
|
||||
mockCreationService,
|
||||
mockPolicyService,
|
||||
mockCreationPolicy,
|
||||
mockPolicyMap = {},
|
||||
mockTypes,
|
||||
provider;
|
||||
|
||||
@ -67,14 +70,32 @@ define(
|
||||
"creationService",
|
||||
[ "createObject" ]
|
||||
);
|
||||
mockPolicyService = jasmine.createSpyObj(
|
||||
"policyService",
|
||||
[ "allow" ]
|
||||
);
|
||||
|
||||
mockTypes = [ "A", "B", "C" ].map(createMockType);
|
||||
|
||||
mockTypes.forEach(function(type){
|
||||
mockPolicyMap[type.getName()] = true;
|
||||
});
|
||||
|
||||
mockCreationPolicy = function(type){
|
||||
return mockPolicyMap[type.getName()];
|
||||
};
|
||||
|
||||
mockPolicyService.allow.andCallFake(function(category, type){
|
||||
return category === "creation" && mockCreationPolicy(type) ? true : false;
|
||||
});
|
||||
|
||||
mockTypeService.listTypes.andReturn(mockTypes);
|
||||
|
||||
provider = new CreateActionProvider(
|
||||
mockTypeService,
|
||||
mockDialogService,
|
||||
mockCreationService
|
||||
mockCreationService,
|
||||
mockPolicyService
|
||||
);
|
||||
});
|
||||
|
||||
@ -94,15 +115,15 @@ define(
|
||||
|
||||
it("does not expose non-creatable types", function () {
|
||||
// One of the types won't have the creation feature...
|
||||
mockTypes[1].hasFeature.andReturn(false);
|
||||
mockPolicyMap[mockTypes[0].getName()] = false;
|
||||
// ...so it should have been filtered out.
|
||||
expect(provider.getActions({
|
||||
key: "create",
|
||||
domainObject: {}
|
||||
}).length).toEqual(2);
|
||||
// Make sure it was creation which was used to check
|
||||
expect(mockTypes[1].hasFeature)
|
||||
.toHaveBeenCalledWith("creation");
|
||||
expect(mockPolicyService.allow)
|
||||
.toHaveBeenCalledWith("creation", mockTypes[0]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
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/CreateWizard",
|
||||
"creation/CreationService",
|
||||
"creation/CreationPolicy",
|
||||
"creation/LocatorController",
|
||||
"navigation/NavigateAction",
|
||||
"navigation/NavigationService",
|
||||
|
@ -50,7 +50,7 @@ define(
|
||||
// Simply trigger refresh of in-view objects; do not
|
||||
// write anything to database.
|
||||
persistence.persist = function () {
|
||||
cache.markDirty(editableObject);
|
||||
return cache.markDirty(editableObject);
|
||||
};
|
||||
|
||||
// Delegate refresh to the original object; this avoids refreshing
|
||||
|
@ -115,6 +115,7 @@ define(
|
||||
*/
|
||||
EditableDomainObjectCache.prototype.markDirty = function (domainObject) {
|
||||
this.dirtyObjects[domainObject.getId()] = domainObject;
|
||||
return this.$q.when(true);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -31,6 +31,7 @@ define(
|
||||
mockEditableObject,
|
||||
mockDomainObject,
|
||||
mockCache,
|
||||
mockPromise,
|
||||
capability;
|
||||
|
||||
beforeEach(function () {
|
||||
@ -50,7 +51,9 @@ define(
|
||||
"cache",
|
||||
[ "markDirty" ]
|
||||
);
|
||||
mockPromise = jasmine.createSpyObj("promise", ["then"]);
|
||||
|
||||
mockCache.markDirty.andReturn(mockPromise);
|
||||
mockDomainObject.getCapability.andReturn(mockPersistence);
|
||||
|
||||
capability = new EditablePersistenceCapability(
|
||||
@ -84,6 +87,10 @@ define(
|
||||
expect(mockPersistence.refresh).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns a promise from persist", function () {
|
||||
expect(capability.persist().then).toEqual(jasmine.any(Function));
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -19,6 +19,10 @@
|
||||
{
|
||||
"implementation": "StyleSheetLoader.js",
|
||||
"depends": [ "stylesheets[]", "$document", "THEME" ]
|
||||
},
|
||||
{
|
||||
"implementation": "UnsupportedBrowserWarning.js",
|
||||
"depends": [ "notificationService", "agentService" ]
|
||||
}
|
||||
],
|
||||
"stylesheets": [
|
||||
|
@ -36,29 +36,29 @@ $mobileTreeRightArrowW: 30px;
|
||||
|
||||
/************************** DEVICE WIDTHS */
|
||||
// IMPORTANT! Usage assumes that ranges are mutually exclusive and have no gaps
|
||||
$phoMaxW: 514px;
|
||||
$tabMinW: 515px;
|
||||
$tabMaxW: 1280px;
|
||||
$desktopMinW: 1281px;
|
||||
$phoMaxW: 767px;
|
||||
$tabMinW: 768px;
|
||||
$tabMaxW: 1024px;
|
||||
$desktopMinW: 1025px;
|
||||
|
||||
/************************** MEDIA QUERIES: WINDOW CHECKS FOR SPECIFIC ORIENTATIONS FOR EACH DEVICE */
|
||||
$screenPortrait: "screen and (orientation: portrait)";
|
||||
$screenLandscape: "screen and (orientation: landscape)";
|
||||
$screenPortrait: "(orientation: portrait)";
|
||||
$screenLandscape: "(orientation: landscape)";
|
||||
|
||||
$mobileDevice: "(max-device-width: #{$tabMaxW})";
|
||||
//$mobileDevice: "(max-device-width: #{$tabMaxW})";
|
||||
|
||||
$phoneCheck: "(max-device-width: #{$phoMaxW})";
|
||||
$tabletCheck: $mobileDevice;
|
||||
$desktopCheck: "(min-device-width: #{$desktopMinW})";
|
||||
$tabletCheck: "(min-device-width: #{$tabMinW}) and (max-device-width: #{$tabMaxW})";
|
||||
$desktopCheck: "(min-device-width: #{$desktopMinW}) and (-webkit-min-device-pixel-ratio: 1)";
|
||||
|
||||
/************************** MEDIA QUERIES: WINDOWS FOR SPECIFIC ORIENTATIONS FOR EACH DEVICE */
|
||||
$phonePortrait: "#{$screenPortrait} and #{$phoneCheck} and #{$mobileDevice}";
|
||||
$phoneLandscape: "#{$screenLandscape} and #{$phoneCheck} and #{$mobileDevice}";
|
||||
$phonePortrait: "only screen and #{$screenPortrait} and #{$phoneCheck}";
|
||||
$phoneLandscape: "only screen and #{$screenLandscape} and #{$phoneCheck}";
|
||||
|
||||
$tabletPortrait: "#{$screenPortrait} and #{$tabletCheck} and #{$mobileDevice}";
|
||||
$tabletLandscape: "#{$screenLandscape} and #{$tabletCheck} and #{$mobileDevice}";
|
||||
$tabletPortrait: "only screen and #{$screenPortrait} and #{$tabletCheck}";
|
||||
$tabletLandscape: "only screen and #{$screenLandscape} and #{$tabletCheck}";
|
||||
|
||||
$desktop: "screen and #{$desktopCheck}";
|
||||
$desktop: "only screen and #{$desktopCheck}";
|
||||
|
||||
/************************** DEVICE PARAMETERS FOR MENUS/REPRESENTATIONS */
|
||||
$proporMenuOnly: 90%;
|
||||
|
@ -1,7 +1,29 @@
|
||||
<!--
|
||||
Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Open MCT Web includes source code licensed under additional open source
|
||||
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<span class="s-btn"
|
||||
ng-controller="DateTimeFieldController">
|
||||
<input type="text"
|
||||
ng-model="textValue"
|
||||
ng-blur="restoreTextValue(); ngBlur()"
|
||||
ng-class="{ error: textInvalid }">
|
||||
</input>
|
||||
<a class="ui-symbol icon icon-calendar"
|
||||
@ -11,8 +33,8 @@
|
||||
<mct-popup ng-if="picker.active">
|
||||
<div mct-click-elsewhere="picker.active = false">
|
||||
<mct-control key="'datetime-picker'"
|
||||
ng-model="ngModel"
|
||||
field="field"
|
||||
ng-model="pickerModel"
|
||||
field="'value'"
|
||||
options="{ hours: true }">
|
||||
</mct-control>
|
||||
</div>
|
||||
|
@ -20,12 +20,14 @@
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<div ng-controller="TimeRangeController">
|
||||
<div class="l-time-range-inputs-holder">
|
||||
<form class="l-time-range-inputs-holder"
|
||||
ng-submit="updateBoundsFromForm()">
|
||||
<span class="l-time-range-inputs-elem ui-symbol type-icon">C</span>
|
||||
<span class="l-time-range-input">
|
||||
<mct-control key="'datetime-field'"
|
||||
structure="{ format: parameters.format }"
|
||||
ng-model="ngModel.outer"
|
||||
ng-model="formModel"
|
||||
ng-blur="updateBoundsFromForm()"
|
||||
field="'start'"
|
||||
class="time-range-start">
|
||||
</mct-control>
|
||||
@ -36,12 +38,15 @@
|
||||
<span class="l-time-range-input" ng-controller="ToggleController as t2">
|
||||
<mct-control key="'datetime-field'"
|
||||
structure="{ format: parameters.format }"
|
||||
ng-model="ngModel.outer"
|
||||
ng-model="formModel"
|
||||
ng-blur="updateBoundsFromForm()"
|
||||
field="'end'"
|
||||
class="time-range-end">
|
||||
</mct-control>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<input type="submit" class="hidden">
|
||||
</form>
|
||||
|
||||
<div class="l-time-range-slider-holder">
|
||||
<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) {
|
||||
$scope.textValue = formatter.format(value);
|
||||
$scope.textInvalid = false;
|
||||
$scope.lastValidValue = $scope.textValue;
|
||||
}
|
||||
$scope.pickerModel = { value: value };
|
||||
}
|
||||
|
||||
function updateFromView(textValue) {
|
||||
@ -61,6 +63,17 @@ define(
|
||||
if (!$scope.textInvalid) {
|
||||
$scope.ngModel[$scope.field] =
|
||||
formatter.parse(textValue);
|
||||
$scope.lastValidValue = $scope.textValue;
|
||||
}
|
||||
}
|
||||
|
||||
function updateFromPicker(value) {
|
||||
if (value !== $scope.ngModel[$scope.field]) {
|
||||
$scope.ngModel[$scope.field] = value;
|
||||
updateFromModel(value);
|
||||
if ($scope.ngBlur) {
|
||||
$scope.ngBlur();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,10 +82,18 @@ define(
|
||||
updateFromModel($scope.ngModel[$scope.field]);
|
||||
}
|
||||
|
||||
function restoreTextValue() {
|
||||
$scope.textValue = $scope.lastValidValue;
|
||||
updateFromView($scope.textValue);
|
||||
}
|
||||
|
||||
$scope.restoreTextValue = restoreTextValue;
|
||||
|
||||
$scope.picker = { active: false };
|
||||
|
||||
$scope.$watch('structure.format', setFormat);
|
||||
$scope.$watch('ngModel[field]', updateFromModel);
|
||||
$scope.$watch('pickerModel.value', updateFromPicker);
|
||||
$scope.$watch('textValue', updateFromView);
|
||||
|
||||
}
|
||||
|
@ -175,6 +175,13 @@ define(
|
||||
updateViewFromModel($scope.ngModel);
|
||||
}
|
||||
|
||||
function updateFormModel() {
|
||||
$scope.formModel = {
|
||||
start: (($scope.ngModel || {}).outer || {}).start,
|
||||
end: (($scope.ngModel || {}).outer || {}).end
|
||||
};
|
||||
}
|
||||
|
||||
function updateOuterStart(t) {
|
||||
var ngModel = $scope.ngModel;
|
||||
|
||||
@ -192,6 +199,7 @@ define(
|
||||
ngModel.inner.end
|
||||
);
|
||||
|
||||
updateFormModel();
|
||||
updateViewForInnerSpanFromModel(ngModel);
|
||||
updateTicks();
|
||||
}
|
||||
@ -213,6 +221,7 @@ define(
|
||||
ngModel.inner.start
|
||||
);
|
||||
|
||||
updateFormModel();
|
||||
updateViewForInnerSpanFromModel(ngModel);
|
||||
updateTicks();
|
||||
}
|
||||
@ -223,6 +232,14 @@ define(
|
||||
updateTicks();
|
||||
}
|
||||
|
||||
function updateBoundsFromForm() {
|
||||
$scope.ngModel = $scope.ngModel || {};
|
||||
$scope.ngModel.outer = {
|
||||
start: $scope.formModel.start,
|
||||
end: $scope.formModel.end
|
||||
};
|
||||
}
|
||||
|
||||
$scope.startLeftDrag = startLeftDrag;
|
||||
$scope.startRightDrag = startRightDrag;
|
||||
$scope.startMiddleDrag = startMiddleDrag;
|
||||
@ -230,10 +247,13 @@ define(
|
||||
$scope.rightDrag = rightDrag;
|
||||
$scope.middleDrag = middleDrag;
|
||||
|
||||
$scope.updateBoundsFromForm = updateBoundsFromForm;
|
||||
|
||||
$scope.ticks = [];
|
||||
|
||||
// Initialize scope to defaults
|
||||
updateViewFromModel($scope.ngModel);
|
||||
updateFormModel();
|
||||
|
||||
$scope.$watchCollection("ngModel", updateViewFromModel);
|
||||
$scope.$watch("spanWidth", updateSpanWidth);
|
||||
|
@ -204,7 +204,7 @@ define(
|
||||
// And poll for position changes enforced by styles
|
||||
activeInterval = $interval(function () {
|
||||
getSetPosition(getSetPosition());
|
||||
}, POLLING_INTERVAL, false);
|
||||
}, POLLING_INTERVAL, 0, false);
|
||||
|
||||
// ...and stop polling when we're destroyed.
|
||||
$scope.$on('$destroy', function () {
|
||||
|
@ -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.field = "testField";
|
||||
mockScope.structure = { format: "someFormat" };
|
||||
mockScope.ngBlur = jasmine.createSpy('blur');
|
||||
|
||||
controller = new DateTimeFieldController(
|
||||
mockScope,
|
||||
mockFormatService
|
||||
);
|
||||
});
|
||||
|
||||
it("updates models from user-entered text", function () {
|
||||
var newText = "1977-05-25 17:30:00";
|
||||
|
||||
mockScope.textValue = newText;
|
||||
fireWatch("textValue", newText);
|
||||
expect(mockScope.ngModel.testField)
|
||||
.toEqual(mockFormat.parse(newText));
|
||||
expect(mockScope.textInvalid).toBeFalsy();
|
||||
fireWatch("ngModel[field]", mockScope.ngModel.testField);
|
||||
});
|
||||
|
||||
it("updates text from model values", function () {
|
||||
@ -91,16 +83,55 @@ define(
|
||||
expect(mockScope.textValue).toEqual("1977-05-25 17:30:00");
|
||||
});
|
||||
|
||||
describe("when valid text is entered", function () {
|
||||
var newText;
|
||||
|
||||
beforeEach(function () {
|
||||
newText = "1977-05-25 17:30:00";
|
||||
mockScope.textValue = newText;
|
||||
fireWatch("textValue", newText);
|
||||
});
|
||||
|
||||
it("updates models from user-entered text", function () {
|
||||
expect(mockScope.ngModel.testField)
|
||||
.toEqual(mockFormat.parse(newText));
|
||||
expect(mockScope.textInvalid).toBeFalsy();
|
||||
});
|
||||
|
||||
it("does not indicate a blur event", function () {
|
||||
expect(mockScope.ngBlur).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when a date is chosen via the date picker", function () {
|
||||
var newValue;
|
||||
|
||||
beforeEach(function () {
|
||||
newValue = 12345654321;
|
||||
mockScope.pickerModel.value = newValue;
|
||||
fireWatch("pickerModel.value", newValue);
|
||||
});
|
||||
|
||||
it("updates models", function () {
|
||||
expect(mockScope.ngModel.testField).toEqual(newValue);
|
||||
});
|
||||
|
||||
it("fires a blur event", function () {
|
||||
expect(mockScope.ngBlur).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("exposes toggle state for date-time picker", function () {
|
||||
expect(mockScope.picker.active).toBe(false);
|
||||
});
|
||||
|
||||
describe("when user input is invalid", function () {
|
||||
var newText, oldValue;
|
||||
var newText, oldText, oldValue;
|
||||
|
||||
beforeEach(function () {
|
||||
newText = "Not a date";
|
||||
oldValue = mockScope.ngModel.testField;
|
||||
oldText = mockScope.textValue;
|
||||
mockScope.textValue = newText;
|
||||
fireWatch("textValue", newText);
|
||||
});
|
||||
@ -116,6 +147,11 @@ define(
|
||||
it("does not modify user input", function () {
|
||||
expect(mockScope.textValue).toEqual(newText);
|
||||
});
|
||||
|
||||
it("restores valid text values on request", function () {
|
||||
mockScope.restoreTextValue();
|
||||
expect(mockScope.textValue).toEqual(oldText);
|
||||
});
|
||||
});
|
||||
|
||||
it("does not modify valid but irregular user input", function () {
|
||||
|
@ -91,6 +91,39 @@ define(
|
||||
.toHaveBeenCalledWith("ngModel", jasmine.any(Function));
|
||||
});
|
||||
|
||||
describe("when changes are made via form entry", function () {
|
||||
beforeEach(function () {
|
||||
mockScope.ngModel = {
|
||||
outer: { start: DAY * 2, end: DAY * 3 },
|
||||
inner: { start: DAY * 2.25, end: DAY * 2.75 }
|
||||
};
|
||||
mockScope.formModel = {
|
||||
start: DAY * 10000,
|
||||
end: DAY * 11000
|
||||
};
|
||||
// These watches may not exist, but Angular would fire
|
||||
// them if they did.
|
||||
fireWatchCollection("formModel", mockScope.formModel);
|
||||
fireWatch("formModel.start", mockScope.formModel.start);
|
||||
fireWatch("formModel.end", mockScope.formModel.end);
|
||||
});
|
||||
|
||||
it("does not immediately make changes to the model", function () {
|
||||
expect(mockScope.ngModel.outer.start)
|
||||
.not.toEqual(mockScope.formModel.start);
|
||||
expect(mockScope.ngModel.outer.end)
|
||||
.not.toEqual(mockScope.formModel.end);
|
||||
});
|
||||
|
||||
it("updates model bounds on request", function () {
|
||||
mockScope.updateBoundsFromForm();
|
||||
expect(mockScope.ngModel.outer.start)
|
||||
.toEqual(mockScope.formModel.start);
|
||||
expect(mockScope.ngModel.outer.end)
|
||||
.toEqual(mockScope.formModel.end);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when dragged", function () {
|
||||
beforeEach(function () {
|
||||
mockScope.ngModel = {
|
||||
|
@ -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/MCTResize",
|
||||
"directives/MCTScroll",
|
||||
"directives/MCTSplitPane",
|
||||
"services/Popup",
|
||||
"services/PopupService",
|
||||
"services/UrlService",
|
||||
"StyleSheetLoader"
|
||||
"StyleSheetLoader",
|
||||
"UnsupportedBrowserWarning"
|
||||
]
|
||||
|
@ -43,6 +43,7 @@ define(
|
||||
var userAgent = $window.navigator.userAgent,
|
||||
matches = userAgent.match(/iPad|iPhone|Android/i) || [];
|
||||
|
||||
this.userAgent = userAgent;
|
||||
this.mobileName = matches[0];
|
||||
this.$window = $window;
|
||||
}
|
||||
@ -91,6 +92,18 @@ define(
|
||||
return !this.isPortrait();
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the user agent matches a certain named device,
|
||||
* as indicated by checking for a case-insensitive substring
|
||||
* match.
|
||||
* @param {string} name the name to check for
|
||||
* @returns {boolean} true if the user agent includes that name
|
||||
*/
|
||||
AgentService.prototype.isBrowser = function (name) {
|
||||
name = name.toLowerCase();
|
||||
return this.userAgent.toLowerCase().indexOf(name) !== -1;
|
||||
};
|
||||
|
||||
return AgentService;
|
||||
}
|
||||
);
|
||||
|
@ -81,6 +81,13 @@ define(
|
||||
expect(agentService.isPortrait()).toBeTruthy();
|
||||
expect(agentService.isLandscape()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("allows for checking browser type", function () {
|
||||
testWindow.navigator.userAgent = "Chromezilla Safarifox";
|
||||
agentService = new AgentService(testWindow);
|
||||
expect(agentService.isBrowser("Chrome")).toBe(true);
|
||||
expect(agentService.isBrowser("Firefox")).toBe(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
@ -188,7 +188,8 @@
|
||||
{
|
||||
"key": "persistence",
|
||||
"implementation": "capabilities/PersistenceCapability.js",
|
||||
"depends": [ "persistenceService", "identifierService" ]
|
||||
"depends": [ "persistenceService", "identifierService",
|
||||
"notificationService", "$q" ]
|
||||
},
|
||||
{
|
||||
"key": "metadata",
|
||||
|
@ -20,6 +20,7 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/*global define*/
|
||||
/*jslint es5: true */
|
||||
|
||||
|
||||
define(
|
||||
@ -47,6 +48,8 @@ define(
|
||||
function PersistenceCapability(
|
||||
persistenceService,
|
||||
identifierService,
|
||||
notificationService,
|
||||
$q,
|
||||
domainObject
|
||||
) {
|
||||
// Cache modified timestamp
|
||||
@ -55,6 +58,8 @@ define(
|
||||
this.domainObject = domainObject;
|
||||
this.identifierService = identifierService;
|
||||
this.persistenceService = persistenceService;
|
||||
this.notificationService = notificationService;
|
||||
this.$q = $q;
|
||||
}
|
||||
|
||||
// Utility function for creating promise-like objects which
|
||||
@ -72,6 +77,46 @@ define(
|
||||
return parts.length > 1 ? parts.slice(1).join(":") : id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the value returned is falsey, and if so returns a
|
||||
* rejected promise
|
||||
*/
|
||||
function rejectIfFalsey(value, $q){
|
||||
if (!value){
|
||||
return $q.reject("Error persisting object");
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function formatError(error){
|
||||
if (error && error.message) {
|
||||
return error.message;
|
||||
} else if (error && typeof error === "string"){
|
||||
return error;
|
||||
} else {
|
||||
return "unknown error";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a notification message if an error has occurred during
|
||||
* persistence.
|
||||
*/
|
||||
function notifyOnError(error, domainObject, notificationService, $q){
|
||||
var errorMessage = "Unable to persist " + domainObject.getModel().name;
|
||||
if (error) {
|
||||
errorMessage += ": " + formatError(error);
|
||||
}
|
||||
|
||||
notificationService.error({
|
||||
title: "Error persisting " + domainObject.getModel().name,
|
||||
hint: errorMessage || "Unknown error"
|
||||
});
|
||||
|
||||
return $q.reject(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist any changes which have been made to this
|
||||
* domain object's model.
|
||||
@ -80,7 +125,8 @@ define(
|
||||
* if not.
|
||||
*/
|
||||
PersistenceCapability.prototype.persist = function () {
|
||||
var domainObject = this.domainObject,
|
||||
var self = this,
|
||||
domainObject = this.domainObject,
|
||||
model = domainObject.getModel(),
|
||||
modified = model.modified,
|
||||
persistenceService = this.persistenceService,
|
||||
@ -98,7 +144,11 @@ define(
|
||||
this.getSpace(),
|
||||
getKey(domainObject.getId()),
|
||||
domainObject.getModel()
|
||||
]);
|
||||
]).then(function(result){
|
||||
return rejectIfFalsey(result, self.$q);
|
||||
}).catch(function(error){
|
||||
return notifyOnError(error, domainObject, self.notificationService, self.$q);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -20,6 +20,7 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
/*jslint es5: true */
|
||||
|
||||
/**
|
||||
* PersistenceCapabilitySpec. Created by vwoeltje on 11/6/14.
|
||||
@ -34,24 +35,36 @@ define(
|
||||
mockIdentifierService,
|
||||
mockDomainObject,
|
||||
mockIdentifier,
|
||||
mockNofificationService,
|
||||
mockQ,
|
||||
id = "object id",
|
||||
model = { someKey: "some value"},
|
||||
model,
|
||||
SPACE = "some space",
|
||||
persistence;
|
||||
persistence,
|
||||
happyPromise;
|
||||
|
||||
function asPromise(value) {
|
||||
function asPromise(value, doCatch) {
|
||||
return (value || {}).then ? value : {
|
||||
then: function (callback) {
|
||||
return asPromise(callback(value));
|
||||
},
|
||||
catch: function(callback) {
|
||||
//Define a default 'happy' catch, that skips over the
|
||||
// catch callback
|
||||
return doCatch ? asPromise(callback(value)): asPromise(value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
happyPromise = asPromise(true);
|
||||
model = { someKey: "some value", name: "domain object"};
|
||||
|
||||
mockPersistenceService = jasmine.createSpyObj(
|
||||
"persistenceService",
|
||||
[ "updateObject", "readObject", "createObject", "deleteObject" ]
|
||||
);
|
||||
|
||||
mockIdentifierService = jasmine.createSpyObj(
|
||||
'identifierService',
|
||||
[ 'parse', 'generate' ]
|
||||
@ -60,6 +73,15 @@ define(
|
||||
'identifier',
|
||||
[ 'getSpace', 'getKey', 'getDefinedSpace' ]
|
||||
);
|
||||
mockQ = jasmine.createSpyObj(
|
||||
"$q",
|
||||
["reject"]
|
||||
);
|
||||
mockNofificationService = jasmine.createSpyObj(
|
||||
"notificationService",
|
||||
["error"]
|
||||
);
|
||||
|
||||
mockDomainObject = {
|
||||
getId: function () { return id; },
|
||||
getModel: function () { return model; },
|
||||
@ -76,66 +98,99 @@ define(
|
||||
persistence = new PersistenceCapability(
|
||||
mockPersistenceService,
|
||||
mockIdentifierService,
|
||||
mockNofificationService,
|
||||
mockQ,
|
||||
mockDomainObject
|
||||
);
|
||||
});
|
||||
|
||||
it("creates unpersisted objects with the persistence service", function () {
|
||||
// Verify precondition; no call made during constructor
|
||||
expect(mockPersistenceService.createObject).not.toHaveBeenCalled();
|
||||
describe("successful persistence", function() {
|
||||
beforeEach(function () {
|
||||
mockPersistenceService.updateObject.andReturn(happyPromise);
|
||||
mockPersistenceService.createObject.andReturn(happyPromise);
|
||||
});
|
||||
it("creates unpersisted objects with the persistence service", function () {
|
||||
// Verify precondition; no call made during constructor
|
||||
expect(mockPersistenceService.createObject).not.toHaveBeenCalled();
|
||||
|
||||
persistence.persist();
|
||||
persistence.persist();
|
||||
|
||||
expect(mockPersistenceService.createObject).toHaveBeenCalledWith(
|
||||
SPACE,
|
||||
id,
|
||||
model
|
||||
);
|
||||
expect(mockPersistenceService.createObject).toHaveBeenCalledWith(
|
||||
SPACE,
|
||||
id,
|
||||
model
|
||||
);
|
||||
});
|
||||
|
||||
it("updates previously persisted objects with the persistence service", function () {
|
||||
// Verify precondition; no call made during constructor
|
||||
expect(mockPersistenceService.updateObject).not.toHaveBeenCalled();
|
||||
|
||||
model.persisted = 12321;
|
||||
persistence.persist();
|
||||
|
||||
expect(mockPersistenceService.updateObject).toHaveBeenCalledWith(
|
||||
SPACE,
|
||||
id,
|
||||
model
|
||||
);
|
||||
});
|
||||
|
||||
it("reports which persistence space an object belongs to", function () {
|
||||
expect(persistence.getSpace()).toEqual(SPACE);
|
||||
});
|
||||
|
||||
it("updates persisted timestamp on persistence", function () {
|
||||
model.modified = 12321;
|
||||
persistence.persist();
|
||||
expect(model.persisted).toEqual(12321);
|
||||
});
|
||||
it("refreshes the domain object model from persistence", function () {
|
||||
var refreshModel = {someOtherKey: "some other value"};
|
||||
mockPersistenceService.readObject.andReturn(asPromise(refreshModel));
|
||||
persistence.refresh();
|
||||
expect(model).toEqual(refreshModel);
|
||||
});
|
||||
|
||||
it("does not overwrite unpersisted changes on refresh", function () {
|
||||
var refreshModel = {someOtherKey: "some other value"},
|
||||
mockCallback = jasmine.createSpy();
|
||||
model.modified = 2;
|
||||
model.persisted = 1;
|
||||
mockPersistenceService.readObject.andReturn(asPromise(refreshModel));
|
||||
persistence.refresh().then(mockCallback);
|
||||
expect(model).not.toEqual(refreshModel);
|
||||
// Should have also indicated that no changes were actually made
|
||||
expect(mockCallback).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it("does not trigger error notification on successful" +
|
||||
" persistence", function () {
|
||||
persistence.persist();
|
||||
expect(mockQ.reject).not.toHaveBeenCalled();
|
||||
expect(mockNofificationService.error).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe("unsuccessful persistence", function() {
|
||||
var sadPromise = {
|
||||
then: function(callback){
|
||||
return asPromise(callback(0), true);
|
||||
}
|
||||
};
|
||||
beforeEach(function () {
|
||||
mockPersistenceService.createObject.andReturn(sadPromise);
|
||||
});
|
||||
it("rejects on falsey persistence result", function () {
|
||||
persistence.persist();
|
||||
expect(mockQ.reject).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("updates previously persisted objects with the persistence service", function () {
|
||||
// Verify precondition; no call made during constructor
|
||||
expect(mockPersistenceService.updateObject).not.toHaveBeenCalled();
|
||||
|
||||
model.persisted = 12321;
|
||||
persistence.persist();
|
||||
|
||||
expect(mockPersistenceService.updateObject).toHaveBeenCalledWith(
|
||||
SPACE,
|
||||
id,
|
||||
model
|
||||
);
|
||||
it("notifies user on persistence failure", function () {
|
||||
persistence.persist();
|
||||
expect(mockQ.reject).toHaveBeenCalled();
|
||||
expect(mockNofificationService.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("reports which persistence space an object belongs to", function () {
|
||||
expect(persistence.getSpace()).toEqual(SPACE);
|
||||
});
|
||||
|
||||
it("updates persisted timestamp on persistence", function () {
|
||||
model.modified = 12321;
|
||||
persistence.persist();
|
||||
expect(model.persisted).toEqual(12321);
|
||||
});
|
||||
|
||||
it("refreshes the domain object model from persistence", function () {
|
||||
var refreshModel = { someOtherKey: "some other value" };
|
||||
mockPersistenceService.readObject.andReturn(asPromise(refreshModel));
|
||||
persistence.refresh();
|
||||
expect(model).toEqual(refreshModel);
|
||||
});
|
||||
|
||||
it("does not overwrite unpersisted changes on refresh", function () {
|
||||
var refreshModel = { someOtherKey: "some other value" },
|
||||
mockCallback = jasmine.createSpy();
|
||||
model.modified = 2;
|
||||
model.persisted = 1;
|
||||
mockPersistenceService.readObject.andReturn(asPromise(refreshModel));
|
||||
persistence.refresh().then(mockCallback);
|
||||
expect(model).not.toEqual(refreshModel);
|
||||
// Should have also indicated that no changes were actually made
|
||||
expect(mockCallback).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
|
@ -39,6 +39,15 @@
|
||||
"glyph": "\u00F4",
|
||||
"category": "contextual",
|
||||
"implementation": "actions/GoToOriginalAction.js"
|
||||
},
|
||||
{
|
||||
"key": "locate",
|
||||
"name": "Set Primary Location",
|
||||
"description": "Set a domain object's primary location.",
|
||||
"glyph": "",
|
||||
"category": "contextual",
|
||||
"implementation": "actions/SetPrimaryLocationAction.js"
|
||||
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
@ -89,8 +98,7 @@
|
||||
"name": "Copy Service",
|
||||
"description": "Provides a service for copying objects",
|
||||
"implementation": "services/CopyService.js",
|
||||
"depends": ["$q", "creationService", "policyService",
|
||||
"persistenceService", "now"]
|
||||
"depends": ["$q", "policyService", "now"]
|
||||
},
|
||||
{
|
||||
"key": "locationService",
|
||||
|
@ -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 = [
|
||||
"move",
|
||||
"copy",
|
||||
"link",
|
||||
"compose"
|
||||
"copy"
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -38,12 +38,9 @@ define(
|
||||
* @memberof platform/entanglement
|
||||
* @implements {platform/entanglement.AbstractComposeService}
|
||||
*/
|
||||
function CopyService($q, creationService, policyService, persistenceService, now) {
|
||||
function CopyService($q, policyService) {
|
||||
this.$q = $q;
|
||||
this.creationService = creationService;
|
||||
this.policyService = policyService;
|
||||
this.persistenceService = persistenceService;
|
||||
this.now = now;
|
||||
}
|
||||
|
||||
CopyService.prototype.validate = function (object, parentCandidate) {
|
||||
@ -71,7 +68,7 @@ define(
|
||||
*/
|
||||
CopyService.prototype.perform = function (domainObject, parent) {
|
||||
var $q = this.$q,
|
||||
copyTask = new CopyTask(domainObject, parent, this.persistenceService, this.$q, this.now);
|
||||
copyTask = new CopyTask(domainObject, parent, this.policyService, this.$q);
|
||||
if (this.validate(domainObject, parent)) {
|
||||
return copyTask.perform();
|
||||
} else {
|
||||
|
@ -23,8 +23,8 @@
|
||||
/*global define */
|
||||
|
||||
define(
|
||||
["uuid"],
|
||||
function (uuid) {
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
@ -33,36 +33,48 @@ define(
|
||||
*
|
||||
* @param domainObject The object to copy
|
||||
* @param parent The new location of the cloned object tree
|
||||
* @param persistenceService
|
||||
* @param $q
|
||||
* @param now
|
||||
* @constructor
|
||||
*/
|
||||
function CopyTask (domainObject, parent, persistenceService, $q, now){
|
||||
function CopyTask (domainObject, parent, policyService, $q){
|
||||
this.domainObject = domainObject;
|
||||
this.parent = parent;
|
||||
this.firstClone = undefined;
|
||||
this.$q = $q;
|
||||
this.deferred = undefined;
|
||||
this.persistenceService = persistenceService;
|
||||
this.policyService = policyService;
|
||||
this.persisted = 0;
|
||||
this.now = now;
|
||||
this.clones = [];
|
||||
}
|
||||
|
||||
function composeChild(child, parent) {
|
||||
function composeChild(child, parent, setLocation) {
|
||||
//Once copied, associate each cloned
|
||||
// composee with its parent clone
|
||||
child.model.location = parent.id;
|
||||
parent.model.composition = parent.model.composition || [];
|
||||
return parent.model.composition.push(child.id);
|
||||
|
||||
parent.getModel().composition.push(child.getId());
|
||||
|
||||
//If a location is not specified, set it.
|
||||
if (setLocation && child.getModel().location === undefined) {
|
||||
child.getModel().location = parent.getId();
|
||||
}
|
||||
}
|
||||
|
||||
function cloneObjectModel(objectModel) {
|
||||
var clone = JSON.parse(JSON.stringify(objectModel));
|
||||
|
||||
delete clone.composition;
|
||||
/**
|
||||
* Reset certain fields.
|
||||
*/
|
||||
//If has a composition, set it to an empty array. Will be
|
||||
// recomposed later with the ids of its cloned children.
|
||||
if (clone.composition) {
|
||||
//Important to set it to an empty array here, otherwise
|
||||
// hasCapability("composition") returns false;
|
||||
clone.composition = [];
|
||||
}
|
||||
delete clone.persisted;
|
||||
delete clone.modified;
|
||||
delete clone.location;
|
||||
|
||||
return clone;
|
||||
}
|
||||
@ -73,13 +85,10 @@ define(
|
||||
* result in automatic request batching by the browser.
|
||||
*/
|
||||
function persistObjects(self) {
|
||||
|
||||
return self.$q.all(self.clones.map(function(clone){
|
||||
clone.model.persisted = self.now();
|
||||
return self.persistenceService.createObject(clone.persistenceSpace, clone.id, clone.model)
|
||||
.then(function(){
|
||||
self.deferred.notify({phase: "copying", totalObjects: self.clones.length, processed: ++self.persisted});
|
||||
});
|
||||
return clone.getCapability("persistence").persist().then(function(){
|
||||
self.deferred.notify({phase: "copying", totalObjects: self.clones.length, processed: ++self.persisted});
|
||||
});
|
||||
})).then(function(){
|
||||
return self;
|
||||
});
|
||||
@ -89,18 +98,10 @@ define(
|
||||
* Will add a list of clones to the specified parent's composition
|
||||
*/
|
||||
function addClonesToParent(self) {
|
||||
var parentClone = self.clones[self.clones.length-1];
|
||||
|
||||
if (!self.parent.hasCapability('composition')){
|
||||
return self.$q.reject();
|
||||
}
|
||||
|
||||
return self.persistenceService
|
||||
.updateObject(parentClone.persistenceSpace, parentClone.id, parentClone.model)
|
||||
.then(function(){return self.parent.getCapability("composition").add(parentClone.id);})
|
||||
return self.firstClone.getCapability("persistence").persist()
|
||||
.then(function(){self.parent.getCapability("composition").add(self.firstClone.getId());})
|
||||
.then(function(){return self.parent.getCapability("persistence").persist();})
|
||||
.then(function(){return parentClone;});
|
||||
// Ensure the clone of the original domainObject is returned
|
||||
.then(function(){return self.firstClone;});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -112,13 +113,16 @@ define(
|
||||
CopyTask.prototype.copyComposees = function(composees, clonedParent, originalParent){
|
||||
var self = this;
|
||||
|
||||
return (composees || []).reduce(function(promise, composee){
|
||||
return (composees || []).reduce(function(promise, originalComposee){
|
||||
//If the composee is composed of other
|
||||
// objects, chain a promise..
|
||||
return promise.then(function(){
|
||||
// ...to recursively copy it (and its children)
|
||||
return self.copy(composee, originalParent).then(function(composee){
|
||||
composeChild(composee, clonedParent);
|
||||
return self.copy(originalComposee, originalParent).then(function(clonedComposee){
|
||||
//Compose the child within its parent. Cloned
|
||||
// objects will need to also have their location
|
||||
// set, however linked objects will not.
|
||||
return composeChild(clonedComposee, clonedParent, clonedComposee !== originalComposee);
|
||||
});
|
||||
});}, self.$q.when(undefined)
|
||||
);
|
||||
@ -131,29 +135,43 @@ define(
|
||||
* cloning objects, and composing them with their child clones
|
||||
* as it goes
|
||||
* @private
|
||||
* @param originalObject
|
||||
* @param originalParent
|
||||
* @returns {*}
|
||||
* @returns {DomainObject} If the type of the original object allows for
|
||||
* duplication, then a duplicate of the object, otherwise the object
|
||||
* itself (to allow linking to non duplicatable objects).
|
||||
*/
|
||||
CopyTask.prototype.copy = function(originalObject, originalParent) {
|
||||
CopyTask.prototype.copy = function(originalObject) {
|
||||
var self = this,
|
||||
modelClone = {
|
||||
id: uuid(),
|
||||
model: cloneObjectModel(originalObject.getModel()),
|
||||
persistenceSpace: originalParent.hasCapability('persistence') && originalParent.getCapability('persistence').getSpace()
|
||||
};
|
||||
clone;
|
||||
|
||||
return this.$q.when(originalObject.useCapability('composition')).then(function(composees){
|
||||
self.deferred.notify({phase: "preparing"});
|
||||
//Duplicate the object's children, and their children, and
|
||||
// so on down to the leaf nodes of the tree.
|
||||
return self.copyComposees(composees, modelClone, originalObject).then(function (){
|
||||
//Add the clone to the list of clones that will
|
||||
//be returned by this function
|
||||
self.clones.push(modelClone);
|
||||
return modelClone;
|
||||
//Check if the type of the object being copied allows for
|
||||
// creation of new instances. If it does not, then a link to the
|
||||
// original will be created instead.
|
||||
if (this.policyService.allow("creation", originalObject.getCapability("type"))){
|
||||
//create a new clone of the original object. Use the
|
||||
// creation capability of the targetParent to create the
|
||||
// new clone. This will ensure that the correct persistence
|
||||
// space is used.
|
||||
clone = this.parent.useCapability("instantiation", cloneObjectModel(originalObject.getModel()));
|
||||
|
||||
//Iterate through child tree
|
||||
return this.$q.when(originalObject.useCapability('composition')).then(function(composees){
|
||||
self.deferred.notify({phase: "preparing"});
|
||||
//Duplicate the object's children, and their children, and
|
||||
// so on down to the leaf nodes of the tree.
|
||||
//If it is a link, don't both with children
|
||||
return self.copyComposees(composees, clone, originalObject).then(function (){
|
||||
//Add the clone to the list of clones that will
|
||||
//be returned by this function
|
||||
self.clones.push(clone);
|
||||
return clone;
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
//Creating a link, no need to iterate children
|
||||
return self.$q.when(originalObject);
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
@ -172,7 +190,10 @@ define(
|
||||
var self = this;
|
||||
|
||||
return this.copy(self.domainObject, self.parent).then(function(domainObjectClone){
|
||||
domainObjectClone.model.location = self.parent.getId();
|
||||
if (domainObjectClone !== self.domainObject) {
|
||||
domainObjectClone.getModel().location = self.parent.getId();
|
||||
}
|
||||
self.firstClone = domainObjectClone;
|
||||
return self;
|
||||
});
|
||||
};
|
||||
|
@ -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();
|
||||
});
|
||||
|
||||
['move', 'copy', 'link', 'compose'].forEach(function (key) {
|
||||
['move', 'copy'].forEach(function (key) {
|
||||
describe("for " + key + " actions", function () {
|
||||
beforeEach(function () {
|
||||
testActionMetadata.key = key;
|
||||
|
@ -63,7 +63,6 @@ define(
|
||||
|
||||
beforeEach(function () {
|
||||
copyService = new CopyService(
|
||||
null,
|
||||
null,
|
||||
policyService
|
||||
);
|
||||
@ -130,47 +129,50 @@ define(
|
||||
creationService,
|
||||
createObjectPromise,
|
||||
copyService,
|
||||
mockPersistenceService,
|
||||
mockNow,
|
||||
object,
|
||||
newParent,
|
||||
copyResult,
|
||||
copyFinished,
|
||||
persistObjectPromise,
|
||||
parentPersistenceCapability,
|
||||
persistenceCapability,
|
||||
instantiationCapability,
|
||||
compositionCapability,
|
||||
locationCapability,
|
||||
resolvedValue;
|
||||
|
||||
beforeEach(function () {
|
||||
creationService = jasmine.createSpyObj(
|
||||
'creationService',
|
||||
['createObject']
|
||||
);
|
||||
createObjectPromise = synchronousPromise(undefined);
|
||||
creationService.createObject.andReturn(createObjectPromise);
|
||||
policyService.allow.andReturn(true);
|
||||
|
||||
mockPersistenceService = jasmine.createSpyObj(
|
||||
'persistenceService',
|
||||
['createObject', 'updateObject']
|
||||
);
|
||||
|
||||
persistObjectPromise = synchronousPromise(undefined);
|
||||
mockPersistenceService.createObject.andReturn(persistObjectPromise);
|
||||
mockPersistenceService.updateObject.andReturn(persistObjectPromise);
|
||||
|
||||
parentPersistenceCapability = jasmine.createSpyObj(
|
||||
"persistence",
|
||||
|
||||
instantiationCapability = jasmine.createSpyObj(
|
||||
"instantiation",
|
||||
[ "invoke" ]
|
||||
);
|
||||
|
||||
persistenceCapability = jasmine.createSpyObj(
|
||||
"persistenceCapability",
|
||||
[ "persist", "getSpace" ]
|
||||
);
|
||||
persistenceCapability.persist.andReturn(persistObjectPromise);
|
||||
|
||||
parentPersistenceCapability.persist.andReturn(persistObjectPromise);
|
||||
parentPersistenceCapability.getSpace.andReturn("testSpace");
|
||||
compositionCapability = jasmine.createSpyObj(
|
||||
'compositionCapability',
|
||||
['invoke', 'add']
|
||||
);
|
||||
|
||||
mockNow = jasmine.createSpyObj("mockNow", ["now"]);
|
||||
mockNow.now.andCallFake(function(){
|
||||
return 1234;
|
||||
});
|
||||
locationCapability = jasmine.createSpyObj(
|
||||
'locationCapability',
|
||||
['isLink']
|
||||
);
|
||||
locationCapability.isLink.andReturn(false);
|
||||
|
||||
mockDeferred = jasmine.createSpyObj('mockDeferred', ['notify', 'resolve']);
|
||||
mockDeferred = jasmine.createSpyObj(
|
||||
'mockDeferred',
|
||||
['notify', 'resolve', 'reject']
|
||||
);
|
||||
mockDeferred.notify.andCallFake(function(notification){});
|
||||
mockDeferred.resolve.andCallFake(function(value){resolvedValue = value;});
|
||||
mockDeferred.promise = {
|
||||
@ -179,7 +181,11 @@ define(
|
||||
}
|
||||
};
|
||||
|
||||
mockQ = jasmine.createSpyObj('mockQ', ['when', 'all', 'reject', 'defer']);
|
||||
mockQ = jasmine.createSpyObj(
|
||||
'mockQ',
|
||||
['when', 'all', 'reject', 'defer']
|
||||
);
|
||||
mockQ.reject.andReturn(synchronousPromise(undefined));
|
||||
mockQ.when.andCallFake(synchronousPromise);
|
||||
mockQ.all.andCallFake(function (promises) {
|
||||
var result = {};
|
||||
@ -194,6 +200,8 @@ define(
|
||||
|
||||
describe("on domain object without composition", function () {
|
||||
beforeEach(function () {
|
||||
var objectCopy;
|
||||
|
||||
newParent = domainObjectFactory({
|
||||
name: 'newParent',
|
||||
id: '456',
|
||||
@ -201,7 +209,9 @@ define(
|
||||
composition: []
|
||||
},
|
||||
capabilities: {
|
||||
persistence: parentPersistenceCapability
|
||||
instantiation: instantiationCapability,
|
||||
persistence: persistenceCapability,
|
||||
composition: compositionCapability
|
||||
}
|
||||
});
|
||||
|
||||
@ -210,31 +220,46 @@ define(
|
||||
id: 'abc',
|
||||
model: {
|
||||
name: 'some object',
|
||||
location: newParent.id,
|
||||
persisted: mockNow.now()
|
||||
location: '456',
|
||||
someOtherAttribute: 'some other value',
|
||||
embeddedObjectAttribute: {
|
||||
name: 'Some embedded object'
|
||||
}
|
||||
},
|
||||
capabilities: {
|
||||
persistence: persistenceCapability
|
||||
}
|
||||
});
|
||||
|
||||
copyService = new CopyService(mockQ, creationService, policyService, mockPersistenceService, mockNow.now);
|
||||
|
||||
objectCopy = domainObjectFactory({
|
||||
name: 'object',
|
||||
id: 'abc.copy.fdgdfgdf',
|
||||
capabilities: {
|
||||
persistence: persistenceCapability,
|
||||
location: locationCapability
|
||||
}
|
||||
});
|
||||
|
||||
instantiationCapability.invoke.andCallFake(
|
||||
function(model){
|
||||
objectCopy.model = model;
|
||||
return objectCopy;
|
||||
}
|
||||
);
|
||||
|
||||
copyService = new CopyService(mockQ, policyService);
|
||||
copyResult = copyService.perform(object, newParent);
|
||||
copyFinished = jasmine.createSpy('copyFinished');
|
||||
copyResult.then(copyFinished);
|
||||
});
|
||||
|
||||
it("uses persistence service", function () {
|
||||
expect(mockPersistenceService.createObject)
|
||||
.toHaveBeenCalledWith(parentPersistenceCapability.getSpace(), jasmine.any(String), object.getModel());
|
||||
|
||||
expect(persistObjectPromise.then)
|
||||
.toHaveBeenCalledWith(jasmine.any(Function));
|
||||
});
|
||||
it("uses persistence capability", function () {
|
||||
expect(persistenceCapability.persist)
|
||||
.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("deep clones object model", function () {
|
||||
//var newModel = creationService
|
||||
var newModel = mockPersistenceService
|
||||
.createObject
|
||||
.mostRecentCall
|
||||
.args[2];
|
||||
var newModel = copyFinished.calls[0].args[0].getModel();
|
||||
expect(newModel).toEqual(object.model);
|
||||
expect(newModel).not.toBe(object.model);
|
||||
});
|
||||
@ -249,27 +274,57 @@ define(
|
||||
describe("on domainObject with composition", function () {
|
||||
var newObject,
|
||||
childObject,
|
||||
compositionCapability,
|
||||
locationCapability,
|
||||
objectClone,
|
||||
childObjectClone,
|
||||
compositionPromise;
|
||||
|
||||
beforeEach(function () {
|
||||
var invocationCount = 0,
|
||||
objectClones;
|
||||
|
||||
instantiationCapability.invoke.andCallFake(
|
||||
function(model){
|
||||
var cloneToReturn = objectClones[invocationCount++];
|
||||
cloneToReturn.model = model;
|
||||
return cloneToReturn;
|
||||
}
|
||||
);
|
||||
|
||||
locationCapability = jasmine.createSpyObj('locationCapability', ['isLink']);
|
||||
locationCapability.isLink.andReturn(true);
|
||||
newParent = domainObjectFactory({
|
||||
name: 'newParent',
|
||||
id: '456',
|
||||
model: {
|
||||
composition: []
|
||||
},
|
||||
capabilities: {
|
||||
instantiation: instantiationCapability,
|
||||
persistence: persistenceCapability,
|
||||
composition: compositionCapability
|
||||
}
|
||||
});
|
||||
|
||||
childObject = domainObjectFactory({
|
||||
name: 'childObject',
|
||||
id: 'def',
|
||||
model: {
|
||||
name: 'a child object'
|
||||
name: 'a child object',
|
||||
location: 'abc'
|
||||
},
|
||||
capabilities: {
|
||||
persistence: persistenceCapability,
|
||||
location: locationCapability
|
||||
}
|
||||
});
|
||||
compositionCapability = jasmine.createSpyObj(
|
||||
'compositionCapability',
|
||||
['invoke', 'add']
|
||||
);
|
||||
|
||||
childObjectClone = domainObjectFactory({
|
||||
name: 'childObject',
|
||||
id: 'def.clone',
|
||||
capabilities: {
|
||||
persistence: persistenceCapability,
|
||||
location: locationCapability
|
||||
}
|
||||
});
|
||||
|
||||
compositionPromise = jasmine.createSpyObj(
|
||||
'compositionPromise',
|
||||
['then']
|
||||
@ -280,7 +335,7 @@ define(
|
||||
.andReturn(synchronousPromise([childObject]));
|
||||
|
||||
object = domainObjectFactory({
|
||||
name: 'object',
|
||||
name: 'some object',
|
||||
id: 'abc',
|
||||
model: {
|
||||
name: 'some object',
|
||||
@ -288,36 +343,27 @@ define(
|
||||
location: 'testLocation'
|
||||
},
|
||||
capabilities: {
|
||||
instantiation: instantiationCapability,
|
||||
composition: compositionCapability,
|
||||
location: locationCapability
|
||||
}
|
||||
});
|
||||
newObject = domainObjectFactory({
|
||||
name: 'object',
|
||||
id: 'abc2',
|
||||
model: {
|
||||
name: 'some object',
|
||||
composition: []
|
||||
},
|
||||
capabilities: {
|
||||
composition: compositionCapability
|
||||
}
|
||||
});
|
||||
newParent = domainObjectFactory({
|
||||
name: 'newParent',
|
||||
id: '456',
|
||||
model: {
|
||||
composition: []
|
||||
},
|
||||
capabilities: {
|
||||
composition: compositionCapability,
|
||||
persistence: parentPersistenceCapability
|
||||
location: locationCapability,
|
||||
persistence: persistenceCapability
|
||||
}
|
||||
});
|
||||
|
||||
createObjectPromise = synchronousPromise(newObject);
|
||||
creationService.createObject.andReturn(createObjectPromise);
|
||||
copyService = new CopyService(mockQ, creationService, policyService, mockPersistenceService, mockNow.now);
|
||||
objectClone = domainObjectFactory({
|
||||
name: 'some object',
|
||||
id: 'abc.clone',
|
||||
capabilities: {
|
||||
instantiation: instantiationCapability,
|
||||
composition: compositionCapability,
|
||||
location: locationCapability,
|
||||
persistence: persistenceCapability
|
||||
}
|
||||
});
|
||||
|
||||
objectClones = [objectClone, childObjectClone];
|
||||
|
||||
copyService = new CopyService(mockQ, policyService);
|
||||
});
|
||||
|
||||
describe("the cloning process", function(){
|
||||
@ -327,10 +373,9 @@ define(
|
||||
copyResult.then(copyFinished);
|
||||
});
|
||||
|
||||
it("copies object and children in a bottom-up" +
|
||||
" fashion", function () {
|
||||
expect(mockPersistenceService.createObject.calls[0].args[2].name).toEqual(childObject.model.name);
|
||||
expect(mockPersistenceService.createObject.calls[1].args[2].name).toEqual(object.model.name);
|
||||
it("returns a promise", function () {
|
||||
expect(copyResult.then).toBeDefined();
|
||||
expect(copyFinished).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns a promise", function () {
|
||||
@ -338,15 +383,27 @@ define(
|
||||
expect(copyFinished).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("clears modified and sets persisted", function () {
|
||||
expect(copyFinished.mostRecentCall.args[0].model.modified).toBeUndefined();
|
||||
expect(copyFinished.mostRecentCall.args[0].model.persisted).toBe(mockNow.now());
|
||||
});
|
||||
|
||||
it ("correctly locates cloned objects", function() {
|
||||
expect(mockPersistenceService.createObject.calls[0].args[2].location).toEqual(mockPersistenceService.createObject.calls[1].args[1]);
|
||||
expect(childObjectClone.getModel().location).toEqual(objectClone.getId());
|
||||
});
|
||||
});
|
||||
describe("when cloning non-creatable objects", function() {
|
||||
beforeEach(function () {
|
||||
policyService.allow.andCallFake(function(category){
|
||||
//Return false for 'creation' policy
|
||||
return category !== 'creation';
|
||||
});
|
||||
|
||||
copyResult = copyService.perform(object, newParent);
|
||||
copyFinished = jasmine.createSpy('copyFinished');
|
||||
copyResult.then(copyFinished);
|
||||
});
|
||||
it ("creates link instead of clone", function() {
|
||||
var copiedObject = copyFinished.calls[0].args[0];
|
||||
expect(copiedObject).toBe(object);
|
||||
expect(compositionCapability.add).toHaveBeenCalledWith(copiedObject.getId());
|
||||
//expect(newParent.getModel().composition).toContain(copiedObject.getId());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -355,20 +412,28 @@ define(
|
||||
object = domainObjectFactory({
|
||||
name: 'object',
|
||||
capabilities: {
|
||||
type: { type: 'object' }
|
||||
type: { type: 'object' },
|
||||
location: locationCapability,
|
||||
persistence: persistenceCapability
|
||||
}
|
||||
});
|
||||
|
||||
newParent = domainObjectFactory({
|
||||
name: 'parentCandidate',
|
||||
capabilities: {
|
||||
type: { type: 'parentCandidate' }
|
||||
type: { type: 'parentCandidate' },
|
||||
instantiation: instantiationCapability,
|
||||
composition: compositionCapability,
|
||||
persistence: persistenceCapability
|
||||
}
|
||||
});
|
||||
|
||||
instantiationCapability.invoke.andReturn(object);
|
||||
});
|
||||
|
||||
it("throws an error", function () {
|
||||
var copyService =
|
||||
new CopyService(mockQ, creationService, policyService, mockPersistenceService, mockNow.now);
|
||||
new CopyService(mockQ, policyService);
|
||||
|
||||
function perform() {
|
||||
copyService.perform(object, newParent);
|
||||
|
@ -4,6 +4,7 @@
|
||||
"actions/GoToOriginalAction",
|
||||
"actions/LinkAction",
|
||||
"actions/MoveAction",
|
||||
"actions/SetPrimaryLocationAction",
|
||||
"policies/CrossSpacePolicy",
|
||||
"services/CopyService",
|
||||
"services/LinkService",
|
||||
|
@ -45,43 +45,8 @@ define(
|
||||
* @param {Scope} $scope the controller's Angular scope
|
||||
*/
|
||||
function LayoutController($scope) {
|
||||
var self = this;
|
||||
|
||||
// Utility function to copy raw positions from configuration,
|
||||
// without writing directly to configuration (to avoid triggering
|
||||
// persistence from watchers during drags).
|
||||
function shallowCopy(obj, keys) {
|
||||
var copy = {};
|
||||
keys.forEach(function (k) {
|
||||
copy[k] = obj[k];
|
||||
});
|
||||
return copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute panel positions based on the layout's object model.
|
||||
* Defined as member function to facilitate testing.
|
||||
* @private
|
||||
*/
|
||||
LayoutController.prototype.layoutPanels = function layoutPanels (ids) {
|
||||
var configuration = $scope.configuration || {};
|
||||
|
||||
// Pull panel positions from configuration
|
||||
self.rawPositions =
|
||||
shallowCopy(configuration.panels || {}, ids);
|
||||
|
||||
// Clear prior computed positions
|
||||
self.positions = {};
|
||||
|
||||
// Update width/height that we are tracking
|
||||
self.gridSize =
|
||||
($scope.model || {}).layoutGrid || DEFAULT_GRID_SIZE;
|
||||
|
||||
// Compute positions and add defaults where needed
|
||||
ids.forEach(function (id, index) {
|
||||
self.populatePosition(id, index);
|
||||
});
|
||||
};
|
||||
var self = this,
|
||||
callbackCount = 0;
|
||||
|
||||
// Update grid size when it changed
|
||||
function updateGridSize(layoutGrid) {
|
||||
@ -127,23 +92,26 @@ define(
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
function getComposition(domainObject){
|
||||
return domainObject.useCapability('composition');
|
||||
}
|
||||
|
||||
function composeView (composition){
|
||||
$scope.composition = composition;
|
||||
return composition.map(function (object) {
|
||||
return object.getId();
|
||||
}) || [];
|
||||
}
|
||||
|
||||
//Will fetch fully contextualized composed objects, and populate
|
||||
// scope with them.
|
||||
function refreshComposition() {
|
||||
return getComposition($scope.domainObject)
|
||||
.then(composeView)
|
||||
.then(self.layoutPanels);
|
||||
//Keep a track of how many composition callbacks have been made
|
||||
var thisCount = ++callbackCount;
|
||||
|
||||
$scope.domainObject.useCapability('composition').then(function(composition){
|
||||
var ids;
|
||||
|
||||
//Is this callback for the most recent composition
|
||||
// request? If not, discard it. Prevents race condition
|
||||
if (thisCount === callbackCount){
|
||||
ids = composition.map(function (object) {
|
||||
return object.getId();
|
||||
}) || [];
|
||||
|
||||
$scope.composition = composition;
|
||||
self.layoutPanels(ids);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// End drag; we don't want to put $scope into this
|
||||
@ -176,7 +144,7 @@ define(
|
||||
$scope.$watch("model.layoutGrid", updateGridSize);
|
||||
|
||||
// Update composed objects on screen, and position panes
|
||||
$scope.$watch("model.composition", refreshComposition);
|
||||
$scope.$watchCollection("model.composition", refreshComposition);
|
||||
|
||||
// Position panes where they are dropped
|
||||
$scope.$on("mctDrop", handleDrop);
|
||||
@ -282,6 +250,43 @@ define(
|
||||
}
|
||||
};
|
||||
|
||||
// Utility function to copy raw positions from configuration,
|
||||
// without writing directly to configuration (to avoid triggering
|
||||
// persistence from watchers during drags).
|
||||
function shallowCopy(obj, keys) {
|
||||
var copy = {};
|
||||
keys.forEach(function (k) {
|
||||
copy[k] = obj[k];
|
||||
});
|
||||
return copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute panel positions based on the layout's object model.
|
||||
* Defined as member function to facilitate testing.
|
||||
* @private
|
||||
*/
|
||||
LayoutController.prototype.layoutPanels = function (ids) {
|
||||
var configuration = this.$scope.configuration || {},
|
||||
self = this;
|
||||
|
||||
// Pull panel positions from configuration
|
||||
this.rawPositions =
|
||||
shallowCopy(configuration.panels || {}, ids);
|
||||
|
||||
// Clear prior computed positions
|
||||
this.positions = {};
|
||||
|
||||
// Update width/height that we are tracking
|
||||
this.gridSize =
|
||||
(this.$scope.model || {}).layoutGrid || DEFAULT_GRID_SIZE;
|
||||
|
||||
// Compute positions and add defaults where needed
|
||||
ids.forEach(function (id, index) {
|
||||
self.populatePosition(id, index);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* End the active drag gesture. This will update the
|
||||
* view configuration.
|
||||
|
@ -33,7 +33,8 @@ define(
|
||||
testConfiguration,
|
||||
controller,
|
||||
mockCompositionCapability,
|
||||
mockComposition;
|
||||
mockComposition,
|
||||
mockCompositionObjects;
|
||||
|
||||
function mockPromise(value){
|
||||
return {
|
||||
@ -57,7 +58,7 @@ define(
|
||||
beforeEach(function () {
|
||||
mockScope = jasmine.createSpyObj(
|
||||
"$scope",
|
||||
[ "$watch", "$on", "commit" ]
|
||||
[ "$watch", "$watchCollection", "$on", "commit" ]
|
||||
);
|
||||
mockEvent = jasmine.createSpyObj(
|
||||
'event',
|
||||
@ -67,6 +68,7 @@ define(
|
||||
testModel = {};
|
||||
|
||||
mockComposition = ["a", "b", "c"];
|
||||
mockCompositionObjects = mockComposition.map(mockDomainObject);
|
||||
|
||||
testConfiguration = {
|
||||
panels: {
|
||||
@ -77,7 +79,7 @@ define(
|
||||
}
|
||||
};
|
||||
|
||||
mockCompositionCapability = mockPromise(mockComposition.map(mockDomainObject));
|
||||
mockCompositionCapability = mockPromise(mockCompositionObjects);
|
||||
|
||||
mockScope.domainObject = mockDomainObject("mockDomainObject");
|
||||
mockScope.model = testModel;
|
||||
@ -91,14 +93,14 @@ define(
|
||||
// Model changes will indicate that panel positions
|
||||
// may have changed, for instance.
|
||||
it("watches for changes to composition", function () {
|
||||
expect(mockScope.$watch).toHaveBeenCalledWith(
|
||||
expect(mockScope.$watchCollection).toHaveBeenCalledWith(
|
||||
"model.composition",
|
||||
jasmine.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it("Retrieves updated composition from composition capability", function () {
|
||||
mockScope.$watch.mostRecentCall.args[1]();
|
||||
mockScope.$watchCollection.mostRecentCall.args[1]();
|
||||
expect(mockScope.domainObject.useCapability).toHaveBeenCalledWith(
|
||||
"composition"
|
||||
);
|
||||
@ -107,8 +109,32 @@ define(
|
||||
);
|
||||
});
|
||||
|
||||
it("Is robust to concurrent changes to composition", function () {
|
||||
var secondMockComposition = ["a", "b", "c", "d"],
|
||||
secondMockCompositionObjects = secondMockComposition.map(mockDomainObject),
|
||||
firstCompositionCB,
|
||||
secondCompositionCB;
|
||||
|
||||
spyOn(mockCompositionCapability, "then");
|
||||
mockScope.$watchCollection.mostRecentCall.args[1]();
|
||||
mockScope.$watchCollection.mostRecentCall.args[1]();
|
||||
|
||||
firstCompositionCB = mockCompositionCapability.then.calls[0].args[0];
|
||||
secondCompositionCB = mockCompositionCapability.then.calls[1].args[0];
|
||||
|
||||
//Resolve promises in reverse order
|
||||
secondCompositionCB(secondMockCompositionObjects);
|
||||
firstCompositionCB(mockCompositionObjects);
|
||||
|
||||
//Expect the promise call that was initiated most recently to
|
||||
// be the one used to populate scope, irrespective of order that
|
||||
// it was eventually resolved
|
||||
expect(mockScope.composition).toBe(secondMockCompositionObjects);
|
||||
});
|
||||
|
||||
|
||||
it("provides styles for frames, from configuration", function () {
|
||||
mockScope.$watch.mostRecentCall.args[1]();
|
||||
mockScope.$watchCollection.mostRecentCall.args[1]();
|
||||
expect(controller.getFrameStyle("a")).toEqual({
|
||||
top: "320px",
|
||||
left: "640px",
|
||||
@ -121,7 +147,7 @@ define(
|
||||
var styleB, styleC;
|
||||
|
||||
// b and c do not have configured positions
|
||||
mockScope.$watch.mostRecentCall.args[1]();
|
||||
mockScope.$watchCollection.mostRecentCall.args[1]();
|
||||
|
||||
styleB = controller.getFrameStyle("b");
|
||||
styleC = controller.getFrameStyle("c");
|
||||
@ -138,7 +164,7 @@ define(
|
||||
|
||||
it("allows panels to be dragged", function () {
|
||||
// Populate scope
|
||||
mockScope.$watch.mostRecentCall.args[1]();
|
||||
mockScope.$watchCollection.mostRecentCall.args[1]();
|
||||
|
||||
// Verify precondtion
|
||||
expect(testConfiguration.panels.b).not.toBeDefined();
|
||||
@ -157,7 +183,7 @@ define(
|
||||
|
||||
it("invokes commit after drag", function () {
|
||||
// Populate scope
|
||||
mockScope.$watch.mostRecentCall.args[1]();
|
||||
mockScope.$watchCollection.mostRecentCall.args[1]();
|
||||
|
||||
// Do a drag
|
||||
controller.startDrag("b", [1, 1], [0, 0]);
|
||||
@ -218,7 +244,7 @@ define(
|
||||
|
||||
// White-boxy; we know which watch is which
|
||||
mockScope.$watch.calls[0].args[1](testModel.layoutGrid);
|
||||
mockScope.$watch.calls[1].args[1](testModel.composition);
|
||||
mockScope.$watchCollection.calls[0].args[1](testModel.composition);
|
||||
|
||||
styleB = controller.getFrameStyle("b");
|
||||
|
||||
|
@ -146,6 +146,7 @@ define(
|
||||
if (canvas.width !== canvas.offsetWidth ||
|
||||
canvas.height !== canvas.offsetHeight) {
|
||||
doDraw(scope.draw);
|
||||
scope.$apply();
|
||||
}
|
||||
}
|
||||
|
||||
@ -181,7 +182,7 @@ define(
|
||||
canvas.addEventListener("webglcontextlost", fallbackFromWebGL);
|
||||
|
||||
// Check for resize, on a timer
|
||||
activeInterval = $interval(drawIfResized, 1000);
|
||||
activeInterval = $interval(drawIfResized, 1000, 0, false);
|
||||
|
||||
// Watch "draw" for external changes to the set of
|
||||
// things to be drawn.
|
||||
|
@ -45,8 +45,10 @@ define(
|
||||
jasmine.createSpy("$interval");
|
||||
mockLog =
|
||||
jasmine.createSpyObj("$log", ["warn", "info", "debug"]);
|
||||
mockScope =
|
||||
jasmine.createSpyObj("$scope", ["$watchCollection", "$on"]);
|
||||
mockScope = jasmine.createSpyObj(
|
||||
"$scope",
|
||||
["$watchCollection", "$on", "$apply"]
|
||||
);
|
||||
mockElement =
|
||||
jasmine.createSpyObj("element", ["find", "html"]);
|
||||
mockInterval.cancel = jasmine.createSpy("cancelInterval");
|
||||
@ -152,7 +154,9 @@ define(
|
||||
// Should track canvas size in an interval
|
||||
expect(mockInterval).toHaveBeenCalledWith(
|
||||
jasmine.any(Function),
|
||||
jasmine.any(Number)
|
||||
jasmine.any(Number),
|
||||
0,
|
||||
false
|
||||
);
|
||||
|
||||
// Verify pre-condition
|
||||
|
@ -79,6 +79,9 @@ define(
|
||||
// Used to choose which form control to use
|
||||
key: "=",
|
||||
|
||||
// Allow controls to trigger blur-like events
|
||||
ngBlur: "&",
|
||||
|
||||
// The state of the form value itself
|
||||
ngModel: "=",
|
||||
|
||||
|
@ -80,7 +80,7 @@ define(
|
||||
|
||||
// Update the indicator initially, and start polling.
|
||||
updateIndicator();
|
||||
$interval(updateIndicator, interval, false);
|
||||
$interval(updateIndicator, interval, 0, false);
|
||||
}
|
||||
|
||||
ElasticIndicator.prototype.getGlyph = function () {
|
||||
|
@ -55,6 +55,7 @@ define(
|
||||
expect(mockInterval).toHaveBeenCalledWith(
|
||||
jasmine.any(Function),
|
||||
testInterval,
|
||||
0,
|
||||
false
|
||||
);
|
||||
});
|
||||
|
@ -97,7 +97,7 @@ define(
|
||||
counter = 0,
|
||||
couldRepresent = false,
|
||||
couldEdit = false,
|
||||
lastId,
|
||||
lastIdPath = [],
|
||||
lastKey,
|
||||
changeTemplate = templateLinker.link($scope, element);
|
||||
|
||||
@ -144,15 +144,31 @@ define(
|
||||
});
|
||||
}
|
||||
|
||||
function unchanged(canRepresent, canEdit, id, key) {
|
||||
function unchanged(canRepresent, canEdit, idPath, key) {
|
||||
return canRepresent &&
|
||||
couldRepresent &&
|
||||
id === lastId &&
|
||||
key === lastKey &&
|
||||
idPath.length === lastIdPath.length &&
|
||||
idPath.every(function (id, i) {
|
||||
return id === lastIdPath[i];
|
||||
}) &&
|
||||
canEdit &&
|
||||
couldEdit;
|
||||
}
|
||||
|
||||
function getIdPath(domainObject) {
|
||||
if (!domainObject) {
|
||||
return [];
|
||||
}
|
||||
if (!domainObject.hasCapability('context')) {
|
||||
return [domainObject.getId()];
|
||||
}
|
||||
return domainObject.getCapability('context')
|
||||
.getPath().map(function (pathObject) {
|
||||
return pathObject.getId();
|
||||
});
|
||||
}
|
||||
|
||||
// General-purpose refresh mechanism; should set up the scope
|
||||
// as appropriate for current representation key and
|
||||
// domain object.
|
||||
@ -163,10 +179,10 @@ define(
|
||||
uses = ((representation || {}).uses || []),
|
||||
canRepresent = !!(path && domainObject),
|
||||
canEdit = !!(domainObject && domainObject.hasCapability('editor')),
|
||||
id = domainObject && domainObject.getId(),
|
||||
idPath = getIdPath(domainObject),
|
||||
key = $scope.key;
|
||||
|
||||
if (unchanged(canRepresent, canEdit, id, key)) {
|
||||
if (unchanged(canRepresent, canEdit, idPath, key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -194,8 +210,8 @@ define(
|
||||
|
||||
// To allow simplified change detection next time around
|
||||
couldRepresent = canRepresent;
|
||||
lastIdPath = idPath;
|
||||
couldEdit = canEdit;
|
||||
lastId = id;
|
||||
lastKey = key;
|
||||
|
||||
// Populate scope with fields associated with the current
|
||||
|
@ -247,6 +247,54 @@ define(
|
||||
mockScope.$watch.calls[0].args[1]();
|
||||
expect(mockScope.testCapability).toBeUndefined();
|
||||
});
|
||||
|
||||
it("detects changes among linked instances", function () {
|
||||
var mockContext = jasmine.createSpyObj('context', ['getPath']),
|
||||
mockContext2 = jasmine.createSpyObj('context', ['getPath']),
|
||||
mockLink = jasmine.createSpyObj(
|
||||
'linkedObject',
|
||||
DOMAIN_OBJECT_METHODS
|
||||
),
|
||||
mockParent = jasmine.createSpyObj(
|
||||
'parentObject',
|
||||
DOMAIN_OBJECT_METHODS
|
||||
),
|
||||
callCount;
|
||||
|
||||
mockDomainObject.getCapability.andCallFake(function (c) {
|
||||
return c === 'context' && mockContext;
|
||||
});
|
||||
mockLink.getCapability.andCallFake(function (c) {
|
||||
return c === 'context' && mockContext2;
|
||||
});
|
||||
mockDomainObject.hasCapability.andCallFake(function (c) {
|
||||
return c === 'context';
|
||||
});
|
||||
mockLink.hasCapability.andCallFake(function (c) {
|
||||
return c === 'context';
|
||||
});
|
||||
mockLink.getModel.andReturn({});
|
||||
|
||||
mockContext.getPath.andReturn([mockDomainObject]);
|
||||
mockContext2.getPath.andReturn([mockParent, mockLink]);
|
||||
|
||||
mockLink.getId.andReturn('test-id');
|
||||
mockDomainObject.getId.andReturn('test-id');
|
||||
|
||||
mockParent.getId.andReturn('parent-id');
|
||||
|
||||
mockScope.key = "abc";
|
||||
mockScope.domainObject = mockDomainObject;
|
||||
|
||||
mockScope.$watch.calls[0].args[1]();
|
||||
callCount = mockChangeTemplate.calls.length;
|
||||
|
||||
mockScope.domainObject = mockLink;
|
||||
mockScope.$watch.calls[0].args[1]();
|
||||
|
||||
expect(mockChangeTemplate.calls.length)
|
||||
.toEqual(callCount + 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user