Compare commits

..

34 Commits

Author SHA1 Message Date
David Tsay
80ee7d3cf6 add untracked files 2023-08-23 15:27:13 -07:00
David Tsay
2c7294bb3c plot working in inspector 2023-08-23 14:47:26 -07:00
John Hill
42b13c4dfb Fix couchdb setup and add a note on how to remove the container (#6915)
* discrete steps

* update script to remove couchdb

* address comments and add shellcheck linting

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-08-22 11:11:07 -07:00
Even Stensberg
351800b32a chore(npm): dont generate lockfile (#6970)
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2023-08-21 15:39:52 -07:00
Shefali Joshi
6db390a71a Add strategy latest and timeContext to auto flow tabular and gauge views (#6960) 2023-08-21 14:09:39 -07:00
Even Stensberg
9ece4e55dc chore(package.json): add fields (#6971)
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2023-08-21 20:32:07 +00:00
Even Stensberg
0a497483f2 fix(html): minor fixes from validation (#6962)
* fix(html): minor fixes from validation

* chore(html): Update index.html

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-08-21 09:58:06 -07:00
Even Stensberg
a52577e729 feat(tooling): adds nvm (#6938)
* feat(tooling): adds nvm

* fix(ci): nodev16 -> nodev18

* chore(node): dont modify ci config

* feat(nvm): add lts

* docs(readme): add section on nvm

* fix(docs): revise section

---------

Co-authored-by: John Hill <john.c.hill@nasa.gov>
2023-08-19 13:16:26 -07:00
David 'Epper' Marshall
a495e86231 fix(#6516): Progress Bar does not show progress percentage (#6952)
MCT 6516

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-08-18 22:54:58 +00:00
Shefali Joshi
bada228b8f Ensure that dynamically created vue components are destroyed. (#6948) 2023-08-17 23:24:02 +00:00
Khalid Adil
3f80b53ea6 [Tooltips] Fixes for dictionary objects and self-referential objects (#6916)
* Fix getTelemetryPath to handle cases where parent is the same as the child, handle yamcs aggregate telemetry, and fix how identifiers are passed in

* Cleanup getTelemetryPath

* Switch to filter instead of forEach

* Get path item names

* Remove tooltips on scroll of tree

* Remove handing for scroll

* Allow break-words

* Cleanup
2023-08-17 16:18:25 +00:00
Scott Bell
99a3e3fc32 Recent objects do not update when object names are changed (#6927)
* fix tree name issue

* add name to key, and name observers to recent objects

* no need to change key

* make more of app reactive to name changes

* fix browse bar and document title

* listen in properties for name changes

* add tests for renaming

* yeah spelling linter

* add semantic tags to forms and fixup tests

* change purpose

* actually delete the listener

* ensuring deletion

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-08-16 21:38:09 +00:00
Even Stensberg
2d92223e16 fix(package.json): add author (#6941)
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2023-08-16 14:10:24 -07:00
Jesse Mazzella
f21685e216 fix(e2e): Stabilize ITC tests (#6933)
* fix(ITC): initialize ITC in `created()` hook

* fix(e2e): stabilize ITC tests

* docs: add JSDocs

* refactor: lint:fix

* test(e2e): add comment and assertion

* refactor: lint:fix
2023-08-16 19:02:09 +00:00
Jesse Mazzella
6c92e31036 fix(#6942): Toggling FlexibleLayout toolbar options reflects immediately in the view (#6943)
* fix: restore reactivity of config settings

- move initialization steps to `created()` hook

- remove unnecessary `:key` binds

- fix comments

* refactor: clean up

* refactor: clean up

* refactor: lint:fix

* test(e2e): add regression test and cleanup suite

* refactor: consistency is key!

* test(fix): fix unit tests, further cleanup
2023-08-16 17:52:23 +00:00
Jesse Mazzella
82b1760b0e chore: bump version to 3.1.0-next and update docs (#6921)
* chore: bump version to `3.1.0-next`

* docs: update version.md

---------

Co-authored-by: John Hill <john.c.hill@nasa.gov>
2023-08-16 16:15:30 +00:00
Shefali Joshi
87feb0db34 Condition sets now provide the timeContext they're using when sending requests (#6929)
* Send in the timeContext for requests

* Fix failing test

---------

Co-authored-by: Scott Bell <scott@traclabs.com>
2023-08-15 17:32:30 +00:00
Shefali Joshi
c53073b339 Fix remote clock subscription (#6919)
* If there are no listeners for this clock then don't bother subscribing after getting the domain object details

* Comment explaining the fix
2023-08-14 21:51:52 +00:00
Jesse Mazzella
57743e5918 fix: use loadDelay generator setting in subscriptions as well (#6918)
* refactor: use `getBounds()` instead of `bounds()`

* fix: use `loadDelay` in generator subscription

* refactor: fix up e2e test

* fix: remove `.only()`

* refactor: lint

* Start to fix up conditionSet test with comments

* test: edit conditionSet to add delay value

* test: tests the case where telemetry is available

* fix: remove `.only()`

* test: add comments, clarify assertion

* refactor: lint:fix

* test: fix conditionSet default condition name test

* test: add assertions to stabilize tags tests

---------

Co-authored-by: Scott Bell <scott@traclabs.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2023-08-14 19:04:01 +00:00
Jesse Mazzella
f3b819a786 chore: add vue3 to eslint, fix errors, and modify lint script (#6910) 2023-08-14 17:03:19 +00:00
John Hill
50694f600c Light refactor of visual tests (#5585) 2023-08-11 17:18:08 -07:00
Jesse Mazzella
10f3e13e4d chore: modify cspell config and fix all typos (#6908) 2023-08-10 16:20:16 +00:00
Scott Bell
9be9c5e28e Check for null in DuplicateAction (#6904)
check for null before checking before hasOwnProperty
2023-08-09 11:38:17 -07:00
Even Stensberg
58aeac94ac Feat(tooling): add cspell (#6892)
* feat(tooling): add cspell

* fix: pin dep

* ci(linting): add spelling
2023-08-09 08:34:45 -07:00
Jesse Mazzella
1e3097f54b chore: bump Playwright to 1.36.2 (#6901)
* chore: bump Playwright to `1.36.2`

* chore: remove `playwright/core` dependency

* chore: temporarily disable cacheing step

* chore: temp disable cacheing for e2e-couchdb run

* chore: restore cacheing step

* chore: remove `--prefer-offline` option
2023-08-08 10:44:16 -07:00
Shefali Joshi
6a9ff91d93 Dismiss the independent time conductor popup on unmount (#6859)
* Don't set conductor popup to null unless the view is being destroyed

* Replace beforeDestroy with beforeUnmount

* Propagate plot tick widths to timeline view

* Check if conductor popup exists before trying to remove it from the dom

* Fix imagery e2e test

* Revert accidental commit

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-08-07 23:19:13 +00:00
Charles Hacskaylo
accfbc96ab Fix Plan View duplicate scrollbars (#6865)
* Closes #6864
- CSS fixes to remove problematic duplicate overflow handling.

* fix(e2e): stabilize autoscale test

* fix(e2e): mark overlay plot tagging test as slow

* fix(e2e): stabilize ITC e2e test

* fix(e2e): don't use hard wait

* fix: remove .only 😳

---------

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-08-07 22:48:29 +00:00
Even Stensberg
9942bbbc0f chore: move indexTest to kebab-case (#6860) 2023-08-06 12:54:36 -07:00
John Hill
4287cd5413 [CI] Update docker login step to work across forks and non-nasa-users (#6891)
Continue on error
2023-08-05 15:05:40 -07:00
Scott Bell
ee6ca11558 Only load annotations in fixed time mode or frozen (#6866)
* fix annotations load to not happen on start

* remove range checking per annotated point

* back to local variable

* reduce tagging size to improve reliability of test

* remove .only

* remove .only

* reduce hz rate for functional test and keep high frequency test in performance

* remove console.debugs

* this test runs pretty fast now

* fix network request tests to match new behavior
2023-08-03 09:40:52 -07:00
Andrew Henry
676bb81eab Synchronize timers between multiple users (#6885)
* created a throttle util and using it in timer plugin to throttle refreshing the timer domain object

* Simplify timer logic

* Clarify code a little

* refactor: lint:fix

* Fix linting issue

---------

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-08-02 16:30:51 -07:00
Shefali Joshi
c6305697c0 Set the raw series limits so that we can get the raw series limits (#6877)
* Set the raw series limits so that we can get the raw series limits

* fix: `toRaw()` the other gets/sets/deletes

---------

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2023-08-02 09:50:03 -07:00
Jesse Mazzella
0421936874 fix: suppress deprecation warnings to once per unique args (#6875) 2023-08-02 09:11:41 -07:00
Jesse Mazzella
95e686038d fix: toggling markers, alarm markers, marker style + update Vue.extend() usage to Vue 3 (#6868)
* fix: update to `defineComponent` from `Vue.extend()`
* fix: unwrap Proxy arg before WeakMap.get()
* refactor: `defineComponent` not needed here
2023-08-01 14:07:59 -07:00
187 changed files with 1636 additions and 2947 deletions

View File

@@ -94,6 +94,7 @@ jobs:
- build_and_install:
node-version: <<parameters.node-version>>
- run: npm run lint
- run: npm run lint:spelling
- generate_and_store_version_and_filesystem_artifacts
unit-test:
parameters:
@@ -197,9 +198,7 @@ jobs:
steps:
- build_and_install:
node-version: <<parameters.node-version>>
- run: npm run test:perf:memory
- run: npm run test:perf:localhost
- run: npm run test:perf:contract
- run: npm run test:perf
- store_test_results:
path: test-results/results.xml
- store_artifacts:

View File

@@ -479,11 +479,7 @@
"mediump",
"sinonjs",
"generatedata",
"grandsearch",
"websockets",
"swgs",
"memlab",
"devmode"
"grandsearch"
],
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US"],
"ignorePaths": [

23
.github/release.yml vendored
View File

@@ -1,23 +0,0 @@
changelog:
categories:
- title: 🏕 Features
labels:
- type:feature
- title: 🎉 Enhancements
labels:
- type:enhancement
exclude:
labels:
- type:feature
- title: 🔧 Maintenance
labels:
- type:maintenance
- title: ⚡ Performance
labels:
- performance
- title: 👒 Dependencies
labels:
- dependencies
- title: 🐛 Bug Fixes
labels:
- '*'

View File

@@ -31,7 +31,8 @@ jobs:
- run: npm install --cache ~/.npm --no-audit --progress=false
- name: Login to DockerHub
uses: docker/login-action@v2
uses: docker/login-action@v2
continue-on-error: true
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

5
.npmrc
View File

@@ -1,4 +1,7 @@
loglevel=warn
#Prevent folks from ignoring an important error when building from source
engine-strict=true
engine-strict=true
# Dont include lockfile
package-lock=false

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
lts/*

View File

@@ -33,16 +33,6 @@ const projectRootDir = path.resolve(__dirname, '..');
/** @type {import('webpack').Configuration} */
const config = {
context: projectRootDir,
devServer: {
client: {
progress: true,
overlay: {
// Disable overlay for runtime errors.
// See: https://github.com/webpack/webpack-dev-server/issues/4771
runtimeErrors: false
}
}
},
entry: {
openmct: './openmct.js',
generatorWorker: './example/generator/generatorWorker.js',
@@ -110,12 +100,6 @@ const config = {
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[name].css'
}),
// Add a UTF-8 BOM to CSS output to avoid random mojibake
new webpack.BannerPlugin({
test: /.*Theme\.css$/,
raw: true,
banner: '@charset "UTF-8";',
})
],
module: {
@@ -141,7 +125,6 @@ const config = {
loader: 'vue-loader',
options: {
compilerOptions: {
hoistStatic: false,
whitespace: 'preserve',
compatConfig: {
MODE: 2

View File

@@ -45,6 +45,14 @@ module.exports = merge(common, {
directory: path.join(__dirname, '..', '/dist'),
publicPath: '/dist',
watch: false
},
client: {
progress: true,
overlay: {
// Disable overlay for runtime errors.
// See: https://github.com/webpack/webpack-dev-server/issues/4771
runtimeErrors: false
}
}
}
});

View File

@@ -18,11 +18,12 @@ Building and running Open MCT in your local dev environment is very easy. Be sur
`git clone https://github.com/nasa/openmct.git`
2. Install development dependencies. Note: Check the package.json engine for our tested and supported node versions.
2. (Optionally) Install the correct node version using [nvm](https://github.com/nvm-sh/nvm) (`nvm install`)
3. Install development dependencies. Note: Check the package.json engine for our tested and supported node versions.
`npm install`
3. Run a local development server
4. Run a local development server
`npm start`
@@ -51,6 +52,8 @@ For more on developing with Open MCT, see our documentation for a guide on [Deve
This is a fast moving project and we do our best to test and support the widest possible range of browsers, operating systems, and nodejs APIs. We have a published list of support available in our package.json's `browserslist` key.
The project uses `nvm` to ensure the node and npm version used, is coherent in all projects. Install nvm (non-windows), [here](https://github.com/nvm-sh/nvm) or the windows equivalent [here](https://github.com/coreybutler/nvm-windows)
If you encounter an issue with a particular browser, OS, or nodejs API, please file a [GitHub issue](https://github.com/nasa/openmct/issues/new/choose)
## Plugins
@@ -95,10 +98,10 @@ To run the performance tests:
`npm run test:perf`
The test suite is configured to all tests localed in `e2e/tests/` ending in `*.e2e.spec.js`. For more about the e2e test suite, please see the [README](./e2e/README.md)
The test suite is configured to all tests located in `e2e/tests/` ending in `*.e2e.spec.js`. For more about the e2e test suite, please see the [README](./e2e/README.md)
### Security Tests
Each commit is analyzed for known security vulnerabilities using [CodeQL](https://codeql.github.com/docs/codeql-language-guides/codeql-library-for-javascript/). The list of CWE coverage items is avaiable in the [CodeQL docs](https://codeql.github.com/codeql-query-help/javascript-cwe/). The CodeQL workflow is specified in the [CodeQL analysis file](./.github/workflows/codeql-analysis.yml) and the custom [CodeQL config](./.github/codeql/codeql-config.yml).
Each commit is analyzed for known security vulnerabilities using [CodeQL](https://codeql.github.com/docs/codeql-language-guides/codeql-library-for-javascript/). The list of CWE coverage items is available in the [CodeQL docs](https://codeql.github.com/codeql-query-help/javascript-cwe/). The CodeQL workflow is specified in the [CodeQL analysis file](./.github/workflows/codeql-analysis.yml) and the custom [CodeQL config](./.github/codeql/codeql-config.yml).
### Test Reporting and Code Coverage

View File

@@ -53,7 +53,7 @@ requirements.
Additionally, the following project-specific standards will be used:
* During development, a "-SNAPSHOT" suffix shall be appended to the
* During development, a "-next" suffix shall be appended to the
version number. The version number before the suffix shall reflect
the next expected version number for release.
* Prior to a 1.0.0 release, the _minor_ version will be incremented
@@ -93,7 +93,7 @@ numbers by the following process:
1. Update version number in `package.json`
1. Checkout branch created for the last sprint that has been successfully tested.
2. Remove a `-SNAPSHOT` suffix from the version in `package.json`.
2. Remove a `-next` suffix from the version in `package.json`.
3. Verify that resulting version number meets semantic versioning
requirements relative to previous stable version. Increment the
version number if necessary.
@@ -138,7 +138,7 @@ numbers by the following process:
1. Create a new branch off the `master` branch.
2. Remove any suffix from the version number,
or increment the _patch_ version if there is no suffix.
3. Append a `-SNAPSHOT` suffix.
3. Append a `-next` suffix.
4. Commit changes to `package.json` on the `master` branch.
The commit message should reference the sprint being opened,
preferably by a URL reference to the associated Milestone in
@@ -150,6 +150,6 @@ numbers by the following process:
Projects dependent on Open MCT being co-developed by the Open MCT
team should follow a similar process, except that they should
additionally update their dependency on Open MCT to point to the
latest archive when removing their `-SNAPSHOT` status, and
latest archive when removing their `-next` status, and
that they should be pointed back to the `master` branch after
this has completed.

View File

@@ -82,20 +82,13 @@ To make this possible, we're leveraging a 3rd party service, [Percy](https://per
At present, we are using percy with two configuration files: `./e2e/.percy.nightly.yml` and `./e2e/.percy.ci.yml`. This is mainly to reduce the number of snapshots.
### Advanced: Snapshot Testing (Not Recommended)
### (Advanced) Snapshot Testing
While snapshot testing offers a precise way to detect changes in your application without relying on third-party services like Percy.io, we've found that it doesn't offer any advantages over visual testing in our use-cases. Therefore, snapshot testing is **not recommended** for further implementation.
Snapshot testing is very similar to visual testing but allows us to be more precise in detecting change without relying on a 3rd party service. Unfortuantely, this precision requires advanced test setup and teardown and so we're using this pattern as a last resort.
#### CI vs Manual Checks
Snapshot tests can be reliably executed in Continuous Integration (CI) environments but lack the manual oversight provided by visual testing platforms like Percy.io. This means they may miss issues that a human reviewer could catch during manual checks.
#### Example
A single visual test assertion in Percy.io can be executed across 10 different browser and resolution combinations without additional setup, providing comprehensive testing with minimal configuration. In contrast, a snapshot test is restricted to a single OS and browser resolution, requiring more effort to achieve the same level of coverage.
#### Further Reading
For those interested in the mechanics of snapshot testing with Playwright, you can refer to the [Playwright Snapshots Documentation](https://playwright.dev/docs/test-snapshots). However, keep in mind that we do not recommend using this approach.
To give an example, if a _single_ visual test assertion for an Overlay plot is run through multiple DOM rendering engines at various viewports to see how the Plot looks. If that same test were run as a snapshot test, it could only be executed against a single browser, on a single platform (ubuntu docker container).
Read more about [Playwright Snapshots](https://playwright.dev/docs/test-snapshots)
#### Open MCT's implementation
@@ -134,11 +127,11 @@ npm run test:e2e:updatesnapshots
## Performance Testing
The open source performance tests function in three ways which match their naming and folder structure:
The open source performance tests function mostly as a contract for the locator logic, functionality, and assumptions will work in our downstream, closed source test suites.
`./e2e/tests/performance` - The tests at the root of this folder path detect functional changes which are mostly apparent with large performance regressions like [this](https://github.com/nasa/openmct/issues/6879). These tests run against openmct webpack in `production-mode` with the `npm run test:perf:localhost` script.
`./e2e/tests/performance/contract/` - These tests serve as [contracts](https://martinfowler.com/bliki/ContractTest.html) for the locator logic, functionality, and assumptions will work in our downstream, closed source test suites. These tests run against openmct webpack in `dev-mode` with the `npm run test:perf:contract` script.
`./e2e/tests/performance/memory/` - These tests execute memory leak detection checks in various ways. This is expected to evolve as we move to the `memlab` project. These tests run against openmct webpack in `production-mode` with the `npm run test:perf:memory` script.
They're found under `./e2e/tests/performance` and are to be executed with the following npm script:
`npm run test:perf`
These tests are expected to become blocking and gating with assertions as we extend the capabilities of Playwright.
@@ -158,11 +151,8 @@ Our file structure follows the type of type of testing being excercised at the e
|`./tests/functional/example/` | Tests which specifically verify the example plugins (e.g.: Sine Wave Generator).|
|`./tests/functional/plugins/` | Tests which loosely test each plugin. This folder is the most likely to change. Note: some `@snapshot` tests are still contained within this structure.|
|`./tests/framework/` | Tests which verify that our testing framework's functionality and assumptions will continue to work based on further refactoring or Playwright version changes (e.g.: verifying custom fixtures and appActions).|
|`./tests/performance/` | Performance tests which should be run on every commit.|
|`./tests/performance/contract/` | A subset of performance tests which are designed to provide a contract between the open source tests which are run on every commit and the downstream tests which are run post merge and with other frameworks.|
|`./tests/performance/memory` | A subset of performance tests which are designed to test for memory leaks.|
|`./tests/performance/` | Performance tests.|
|`./tests/visual/` | Visual tests.|
|`./tests/visual/component/` | Visual tests which are only run against a single component.|
|`./appActions.js` | Contains common methods which can be leveraged by test case authors to quickly move through the application when writing new tests.|
|`./baseFixture.js` | Contains base fixtures which only extend default `@playwright/test` functionality. The expectation is that these fixtures will be removed as the native Playwright API improves|
@@ -179,7 +169,6 @@ Open MCT is leveraging the [config file](https://playwright.dev/docs/test-config
|`./playwright-ci.config.js` | Used when running in CI or to debug CI issues locally|
|`./playwright-local.config.js` | Used when running locally|
|`./playwright-performance.config.js` | Used when running performance tests in CI or locally|
|`./playwright-performance-devmode.config.js` | Used when running performance tests in CI or locally|
|`./playwright-visual.config.js` | Used to run the visual tests in CI or locally|
#### Test Tags
@@ -319,27 +308,14 @@ Skipping based on browser version (Rarely used): <https://github.com/microsoft/p
## Test Design, Best Practices, and Tips & Tricks
### Test Design
### Test Design (TODO)
#### Test as the User
- How to make tests robust to function in other contexts (VISTA, VIPER, etc.)
- Leverage the use of `appActions.js` methods such as `createDomainObjectWithDefaults()`
- How to make tests faster and more resilient
- When possible, navigate directly by URL:
In general, strive to test only through the UI as a user would. As stated in the [Playwright Best Practices](https://playwright.dev/docs/best-practices#test-user-visible-behavior):
> "Automated tests should verify that the application code works for the end users, and avoid relying on implementation details such as things which users will not typically use, see, or even know about such as the name of a function, whether something is an array, or the CSS class of some element. The end user will see or interact with what is rendered on the page, so your test should typically only see/interact with the same rendered output."
By adhering to this principle, we can create tests that are both robust and reflective of actual user experiences.
#### How to make tests robust to function in other contexts (VISTA, COUCHDB, YAMCS, VIPER, etc.)
1. Leverage the use of `appActions.js` methods such as `createDomainObjectWithDefaults()`. This ensures that your tests will create unique instances of objects for your test to interact with.
1. Do not assert on the order or structure of objects available unless you created them yourself. These tests may be used against a persistent datastore like couchdb with many objects in the tree.
1. Do not search for your created objects. Open MCT does not performance uniqueness checks so it's possible that your tests will break when run twice.
1. Avoid creating locator aliases. This likely means that you're compensating for a bad locator. Improve the application instead.
1. Leverage `await page.goto('./', { waitUntil: 'domcontentloaded' });` instead of `{ waitUntil: 'networkidle' }`. Tests run against deployments with websockets often have issues with the networkidle detection.
#### How to make tests faster and more resilient
1. Avoid app interaction when possible. The best way of doing this is to navigate directly by URL:
```js
```javascript
// You can capture the CreatedObjectInfo returned from this appAction:
const clock = await createDomainObjectWithDefaults(page, { type: 'Clock' });
@@ -347,14 +323,12 @@ By adhering to this principle, we can create tests that are both robust and refl
await page.goto(clock.url);
```
1. Leverage `await page.goto('./', { waitUntil: 'domcontentloaded' });`
- Leverage `await page.goto('./', { waitUntil: 'domcontentloaded' });`
- Initial navigation should _almost_ always use the `{ waitUntil: 'domcontentloaded' }` option.
1. Avoid repeated setup to test a single assertion. Write longer tests with multiple soft assertions.
This ensures that your changes will be picked up with large refactors.
- Avoid repeated setup to test to test a single assertion. Write longer tests with multiple soft assertions.
### How to write a great test
- Avoid using css locators to find elements to the page. Use modern web accessible locators like `getByRole`
- Use our [App Actions](./appActions.js) for performing common actions whenever applicable.
- Use `waitForPlotsToRender()` before asserting against anything that is dependent upon plot series data being loaded and drawn.
- If you create an object outside of using the `createDomainObjectWithDefaults` App Action, make sure to fill in the 'Notes' section of your object with `page.testNotes`:
@@ -367,29 +341,26 @@ By adhering to this principle, we can create tests that are both robust and refl
await notesInput.fill(testNotes);
```
#### How to Write a Great Visual Test
#### How to write a great visual test
1. **Look for the Unknown Unknowns**: Avoid asserting on specific differences in the visual diff. Visual tests are most effective for identifying unknown unknowns.
2. **Get the App into Interesting States**: Prioritize getting Open MCT into unusual layouts or behaviors before capturing a visual snapshot. For instance, you could open a dropdown menu.
3. **Expect the Unexpected**: Use functional expect statements only to verify assumptions about the state between steps. A great visual test doesn't fail during the test itself, but rather when changes are reviewed in Percy.io.
4. **Control Variability**: Account for variations inherent in working with time-based telemetry and clocks.
- Utilize `percyCSS` to ignore time-based elements. For more details, consult our [percyCSS file](./.percy.ci.yml).
- Use Open MCT's fixed-time mode.
- Employ the `createExampleTelemetryObject` appAction to source telemetry and specify a `name` to avoid autogenerated names.
5. **Hide the Tree and Inspector**: Generally, your test will not require comparisons involving the tree and inspector. These aspects are covered in component-specific tests (explained below). To exclude them from the comparison by default, navigate to the root of the main view with the tree and inspector hidden:
- `await page.goto('./#/browse/mine?hideTree=true&hideInspector=true')`
6. **Component-Specific Tests**: If you wish to focus on a particular component, use the `/visual/component/` folder and limit the scope of the comparison to that component. For instance:
- Generally speaking, you should avoid being "specific" in what you hope to find in the diff. Visual tests are best suited for finding unknown unknowns.
- These should only use functional expect statements to verify assumptions about the state
in a test and not for functional verification of correctness. Visual tests are not supposed
to "fail" on assertions. Instead, they should be used to detect changes between builds or branches.
- A great visual test controls for the variation inherent to working with time-based telemetry and clocks. We do our best to remove this variation by using `percyCSS` to ignore all possible time-based components. For more, please see our [percyCSS file](./.percy.ci.yml).
- Additionally, you should try the following:
- Use fixed-time mode of Open MCT
- Use the `createExampleTelemetryObject` appAction to source telemetry
- When using the `createDomainObjectWithDefaults` appAction, make sure to specify a `name` which is explicit to avoid the autogenerated name
- Very likely, your test will not need to compare changes on the tree. Keep it out of the comparison with the following
- `await page.goto('./#/browse/mine')` will go to the root of the main view with the tree collapsed.
- If you only want to compare changes on a specific component, use the /visual/component/ folder and limit the scope of the comparison to the object like so:
- ```
await percySnapshot(page, `Tree Pane w/ single level expanded (theme: ${theme})`, {
scope: treePane
});
```js
await percySnapshot(page, `Tree Pane w/ single level expanded (theme: ${theme})`, {
scope: treePane
});
```
- Note: The `scope` variable can be any valid CSS selector.
- The `scope` variable can be any valid css selector
#### How to write a great network test
@@ -406,35 +377,12 @@ For now, our best practices exist as self-tested, living documentation in our [e
For best practices with regards to mocking network responses, see our [couchdb.e2e.spec.js](./tests/functional/couchdb.e2e.spec.js) file.
### Tips & Tricks
### Tips & Tricks (TODO)
The following contains a list of tips and tricks which don't exactly fit into a FAQ or Best Practices doc.
- (Advanced) Overriding the Browser's Clock
It is possible to override the browser's clock in order to control time-based elements. Since this can cause unwanted behavior (i.e. Tree not rendering), only use this sparingly. To do this, use the `overrideClock` fixture as such:
```js
const { test, expect } = require('../../pluginFixtures.js');
test.describe('foo test suite', () => {
// All subsequent tests in this suite will override the clock
test.use({
clockOptions: {
now: 1732413600000, // A timestamp given as milliseconds since the epoch
shouldAdvanceTime: true // Should the clock tick?
}
});
test('bar test', async ({ page }) => {
// ...
});
});
```
More info and options for `overrideClock` can be found in [baseFixtures.js](baseFixtures.js)
- Working with multiple pages
There are instances where multiple browser pages will needed to verify multi-page or multi-tab application behavior. Make sure to use the `@2p` annotation as well as name each page appropriately: i.e. `page1` and `page2` or `tab1` and `tab2` depending on the intended use case. Generally pages should be used unless testing `sharedWorker` code, specifically.
There are instances where multiple browser pages will need to be opened to verify multi-page or multi-tab application behavior.
### Reporting

View File

@@ -78,7 +78,7 @@ async function createDomainObjectWithDefaults(
// Navigate to the parent object. This is necessary to create the object
// in the correct location, such as a folder, layout, or plot.
await page.goto(`${parentUrl}`);
await page.goto(`${parentUrl}?hideTree=true`);
//Click the Create button
await page.click('button:has-text("Create")');
@@ -179,7 +179,7 @@ async function createPlanFromJSON(page, { name, json, parent = 'mine' }) {
// Navigate to the parent object. This is necessary to create the object
// in the correct location, such as a folder, layout, or plot.
await page.goto(`${parentUrl}`);
await page.goto(`${parentUrl}?hideTree=true`);
// Click the Create button
await page.click('button:has-text("Create")');
@@ -232,7 +232,7 @@ async function createExampleTelemetryObject(page, parent = 'mine') {
const name = 'VIPER Rover Heading';
const nameInputLocator = page.getByRole('dialog').locator('input[type="text"]');
await page.goto(`${parentUrl}`);
await page.goto(`${parentUrl}?hideTree=true`);
await page.locator('button:has-text("Create")').click();

View File

@@ -1,60 +0,0 @@
/* eslint-disable no-undef */
// playwright.config.js
// @ts-check
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
retries: 0, //Only for debugging purposes for trace: 'on-first-retry'
testDir: 'tests/performance/',
testIgnore: '*.contract.perf.spec.js', //Run everything except contract tests which require marks in dev mode
timeout: 60 * 1000,
workers: 1, //Only run in serial with 1 worker
webServer: {
command: 'npm run start:prod', //Production mode
url: 'http://localhost:8080/#',
timeout: 200 * 1000,
reuseExistingServer: false //Must be run with this option to prevent dev mode
},
use: {
baseURL: 'http://localhost:8080/',
headless: true,
ignoreHTTPSErrors: false, //HTTP performance varies!
screenshot: 'off',
trace: 'on-first-retry',
video: 'off'
},
projects: [
{
name: 'chrome-memory',
testMatch: '*.memory.perf.spec.js', //Only run memory tests
use: {
browserName: 'chromium',
launchOptions: {
args: [
'--no-sandbox',
'--disable-notifications',
'--use-fake-ui-for-media-stream',
'--use-fake-device-for-media-stream',
'--js-flags=--no-move-object-start --expose-gc',
'--enable-precise-memory-info',
'--display=:100'
]
}
}
},
{
name: 'chrome',
testIgnore: '*.memory.perf.spec.js', //Do not run memory tests without proper flags
use: {
browserName: 'chromium'
}
}
],
reporter: [
['list'],
['junit', { outputFile: '../test-results/results.xml' }],
['json', { outputFile: '../test-results/results.json' }]
]
};
module.exports = config;

View File

@@ -2,24 +2,25 @@
// playwright.config.js
// @ts-check
const CI = process.env.CI === 'true';
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
retries: 1, //Only for debugging purposes for trace: 'on-first-retry'
testDir: 'tests/performance/',
testMatch: '*.contract.perf.spec.js', //Run everything except contract tests which require marks in dev mode
timeout: 60 * 1000,
workers: 1, //Only run in serial with 1 worker
webServer: {
command: 'npm run start', //need development mode for performance.marks and others
command: 'npm run start', //coverage not generated
url: 'http://localhost:8080/#',
timeout: 200 * 1000,
reuseExistingServer: false
reuseExistingServer: !CI
},
use: {
browserName: 'chromium',
baseURL: 'http://localhost:8080/',
headless: true,
ignoreHTTPSErrors: false, //HTTP performance varies!
headless: CI, //Only if running locally
ignoreHTTPSErrors: true,
screenshot: 'off',
trace: 'on-first-retry',
video: 'off'
@@ -27,7 +28,6 @@ const config = {
projects: [
{
name: 'chrome',
testIgnore: '*.memory.perf.spec.js', //Do not run memory tests without proper flags. Shouldn't get here
use: {
browserName: 'chromium'
}

File diff suppressed because one or more lines are too long

View File

@@ -80,7 +80,7 @@ test.describe('CouchDB Status Indicator with mocked responses @couchdb', () => {
test.describe('CouchDB initialization with mocked responses @couchdb', () => {
test.use({ failOnConsoleError: false });
test("'My Items' folder is created if it doesn't exist", async ({ page }) => {
const mockedMissingObjectResponsefromCouchDB = {
const mockedMissingObjectResponseFromCouchDB = {
status: 404,
contentType: 'application/json',
body: JSON.stringify({})
@@ -92,7 +92,7 @@ test.describe('CouchDB initialization with mocked responses @couchdb', () => {
await page.route(
'**/mine',
(route) => {
route.fulfill(mockedMissingObjectResponsefromCouchDB);
route.fulfill(mockedMissingObjectResponseFromCouchDB);
},
{ times: 1 }
);

View File

@@ -46,7 +46,7 @@ test.describe('Example Event Generator CRUD Operations', () => {
});
});
test.describe('Example Event Generator Telemetry Event Verficiation', () => {
test.describe('Example Event Generator Telemetry Event Verification', () => {
test.fixme('telemetry is coming in for test event', async ({ page }) => {
// Go to object created in step one
// Verify the telemetry table is filled with > 1 row

View File

@@ -292,7 +292,7 @@ test.describe('Move & link item tests', () => {
});
test.fixme(
'Cannot move a previously created domain object to non-peristable object in Move Modal',
'Cannot move a previously created domain object to non-persistable object in Move Modal',
async ({ page }) => {
//Create a domain object
//Save Domain object

View File

@@ -73,7 +73,7 @@ const testPlan = {
};
test.describe('Time Strip', () => {
test('Create two Time Strips, add a single Plan to both, and verify they can have separate Indepdenent Time Contexts @unstable', async ({
test('Create two Time Strips, add a single Plan to both, and verify they can have separate Independent Time Contexts @unstable', async ({
page
}) => {
test.info().annotations.push({

View File

@@ -22,12 +22,15 @@
/*
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets. Note: this
suite is sharing state between tests which is considered an anti-pattern. Implimenting in this way to
suite is sharing state between tests which is considered an anti-pattern. Implementing in this way to
demonstrate some playwright for test developers. This pattern should not be re-used in other CRUD suites.
*/
const { test, expect } = require('../../../../pluginFixtures.js');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const {
createDomainObjectWithDefaults,
createExampleTelemetryObject
} = require('../../../../appActions');
let conditionSetUrl;
let getConditionSetIdentifierFromUrl;
@@ -127,7 +130,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
// Verify Inspector Details has updated Name property
expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
// Verify Tree reflects updated Name proprety
// Verify Tree reflects updated Name property
// Expand Tree
await page.locator(`text=Open MCT ${myItemsFolderName} >> span >> nth=3`).click();
// Verify Condition Set Object is renamed in Tree
@@ -150,7 +153,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
// Verify Inspector Details has updated Name property
expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
// Verify Tree reflects updated Name proprety
// Verify Tree reflects updated Name property
// Expand Tree
await page.locator(`text=Open MCT ${myItemsFolderName} >> span >> nth=3`).click();
// Verify Condition Set Object is renamed in Tree
@@ -205,23 +208,31 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
});
test.describe('Basic Condition Set Use', () => {
let conditionSet;
test.beforeEach(async ({ page }) => {
// Open a browser, navigate to the main page, and wait until all network events to resolve
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
test('Can add a condition', async ({ page }) => {
// Create a new condition set
await createDomainObjectWithDefaults(page, {
conditionSet = await createDomainObjectWithDefaults(page, {
type: 'Condition Set',
name: 'Test Condition Set'
});
});
test('Creating a condition defaults the condition name to "Unnamed Condition"', async ({
page
}) => {
await page.goto(conditionSet.url);
// Change the object to edit mode
await page.locator('[title="Edit"]').click();
// Click Add Condition button
await page.locator('#addCondition').click();
// Check that the new Unnamed Condition section appears
const numOfUnnamedConditions = await page.locator('text=Unnamed Condition').count();
const numOfUnnamedConditions = await page
.locator('.c-condition__name', { hasText: 'Unnamed Condition' })
.count();
expect(numOfUnnamedConditions).toEqual(1);
});
test('ConditionSet should display appropriate view options', async ({ page }) => {
@@ -238,16 +249,13 @@ test.describe('Basic Condition Set Use', () => {
type: 'Sine Wave Generator',
name: 'Beta Sine Wave Generator'
});
const conditionSet1 = await createDomainObjectWithDefaults(page, {
type: 'Condition Set',
name: 'Test Condition Set'
});
await page.goto(conditionSet.url);
// Change the object to edit mode
await page.locator('[title="Edit"]').click();
// Expand the 'My Items' folder in the left tree
await page.goto(conditionSet1.url);
page.click('button[title="Show selected item in tree"]');
// Add the Alpha & Beta Sine Wave Generator to the Condition Set and save changes
const treePane = page.getByRole('tree', {
@@ -264,9 +272,9 @@ test.describe('Basic Condition Set Use', () => {
await alphaGeneratorTreeItem.dragTo(conditionCollection);
await betaGeneratorTreeItem.dragTo(conditionCollection);
const saveButtonLocator = page.locator('button[title="Save"]');
await saveButtonLocator.click();
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.click('button[title="Change the current view"]');
await expect(page.getByRole('menuitem', { name: /Lad Table/ })).toBeHidden();
@@ -274,95 +282,89 @@ test.describe('Basic Condition Set Use', () => {
await expect(page.getByRole('menuitem', { name: /Plot/ })).toBeVisible();
await expect(page.getByRole('menuitem', { name: /Telemetry Table/ })).toBeVisible();
});
test('ConditionSet should output blank instead of the default value', async ({ page }) => {
//Navigate to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
test('ConditionSet has correct outputs when telemetry is and is not available', async ({
page
}) => {
const exampleTelemetry = await createExampleTelemetryObject(page);
//Click the Create button
await page.click('button:has-text("Create")');
// Click the object specified by 'type'
await page.click(`li[role='menuitem']:text("Sine Wave Generator")`);
await page.getByRole('spinbutton', { name: 'Loading Delay (ms)' }).fill('8000');
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
await nameInput.fill('Delayed Sine Wave Generator');
// Click OK button and wait for Navigate event
await Promise.all([
page.waitForLoadState(),
page.click('[aria-label="Save"]'),
// Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
// Create a new condition set
await createDomainObjectWithDefaults(page, {
type: 'Condition Set',
name: 'Test Blank Output of Condition Set'
});
await page.getByTitle('Show selected item in tree').click();
await page.goto(conditionSet.url);
// Change the object to edit mode
await page.locator('[title="Edit"]').click();
// Click Add Condition button twice
// Create two conditions
await page.locator('#addCondition').click();
await page.locator('#addCondition').click();
await page.locator('#conditionCollection').getByRole('textbox').nth(0).fill('First Condition');
await page.locator('#conditionCollection').getByRole('textbox').nth(1).fill('Second Condition');
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();
// Add the Sine Wave Generator to the Condition Set and save changes
const treePane = page.getByRole('tree', {
name: 'Main Tree'
});
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: 'Delayed Sine Wave Generator'
});
const conditionCollection = await page.locator('#conditionCollection');
// Add Telemetry to ConditionSet
const sineWaveGeneratorTreeItem = page
.getByRole('tree', {
name: 'Main Tree'
})
.getByRole('treeitem', {
name: exampleTelemetry.name
});
const conditionCollection = page.locator('#conditionCollection');
await sineWaveGeneratorTreeItem.dragTo(conditionCollection);
const firstCriterionTelemetry = await page.locator(
// Modify First Criterion
const firstCriterionTelemetry = page.locator(
'[aria-label="Criterion Telemetry Selection"] >> nth=0'
);
firstCriterionTelemetry.selectOption({ label: 'Delayed Sine Wave Generator' });
const secondCriterionTelemetry = await page.locator(
'[aria-label="Criterion Telemetry Selection"] >> nth=1'
);
secondCriterionTelemetry.selectOption({ label: 'Delayed Sine Wave Generator' });
const firstCriterionMetadata = await page.locator(
firstCriterionTelemetry.selectOption({ label: exampleTelemetry.name });
const firstCriterionMetadata = page.locator(
'[aria-label="Criterion Metadata Selection"] >> nth=0'
);
firstCriterionMetadata.selectOption({ label: 'Sine' });
const firstCriterionComparison = page.locator(
'[aria-label="Criterion Comparison Selection"] >> nth=0'
);
firstCriterionComparison.selectOption({ label: 'is greater than or equal to' });
const firstCriterionInput = page.locator('[aria-label="Criterion Input"] >> nth=0');
await firstCriterionInput.fill('0');
const secondCriterionMetadata = await page.locator(
// Modify First Criterion
const secondCriterionTelemetry = page.locator(
'[aria-label="Criterion Telemetry Selection"] >> nth=1'
);
secondCriterionTelemetry.selectOption({ label: exampleTelemetry.name });
const secondCriterionMetadata = page.locator(
'[aria-label="Criterion Metadata Selection"] >> nth=1'
);
secondCriterionMetadata.selectOption({ label: 'Sine' });
const firstCriterionComparison = await page.locator(
'[aria-label="Criterion Comparison Selection"] >> nth=0'
);
firstCriterionComparison.selectOption({ label: 'is greater than or equal to' });
const secondCriterionComparison = await page.locator(
const secondCriterionComparison = page.locator(
'[aria-label="Criterion Comparison Selection"] >> nth=1'
);
secondCriterionComparison.selectOption({ label: 'is less than' });
const firstCriterionInput = await page.locator('[aria-label="Criterion Input"] >> nth=0');
await firstCriterionInput.fill('0');
const secondCriterionInput = await page.locator('[aria-label="Criterion Input"] >> nth=1');
const secondCriterionInput = page.locator('[aria-label="Criterion Input"] >> nth=1');
await secondCriterionInput.fill('0');
const saveButtonLocator = page.locator('button[title="Save"]');
await saveButtonLocator.click();
// Save ConditionSet
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
const outputValue = await page.locator('[aria-label="Current Output Value"]');
// Validate that the condition set is evaluating and outputting
// the correct value when the underlying telemetry subscription is active.
let outputValue = page.locator('[aria-label="Current Output Value"]');
await expect(outputValue).toHaveText('false');
await page.goto(exampleTelemetry.url);
// Edit SWG to add 8 second loading delay to simulate the case
// where telemetry is not available.
await page.getByTitle('More options').click();
await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();
await page.getByRole('spinbutton', { name: 'Loading Delay (ms)' }).fill('8000');
await page.getByLabel('Save').click();
// Expect that the output value is blank or '---' if the
// underlying telemetry subscription is not active.
await page.goto(conditionSet.url);
await expect(outputValue).toHaveText('---');
});
});

View File

@@ -214,7 +214,7 @@ test.describe('Example Imagery Object', () => {
await page.mouse.up();
await Promise.all(tagHotkey.map((x) => page.keyboard.up(x)));
//Wait for canvas to stablize.
//Wait for canvas to stabilize.
await canvas.hover({ trial: true });
// add some tags
@@ -394,7 +394,7 @@ test.describe('Example Imagery in Display Layout', () => {
/**
* Toggle layer visibility checkbox by clicking on checkbox label
* - should toggle checkbox and layer visibility for that image view
* - should NOT toggle checkbox and layer visibity for the first image view in display
* - should NOT toggle checkbox and layer visibility for the first image view in display
*/
test('Toggle layer visibility by clicking on label', async ({ page }) => {
test.info().annotations.push({

View File

@@ -48,12 +48,12 @@ test.describe('ExportAsJSON', () => {
test.fixme('Verify that a nested Object can be exported as JSON', async ({ page }) => {
// Create 2 objects with hierarchy
// Export as JSON
// Verify Hiearchy
// Verify Hierarchy
});
test.fixme(
'Verify that the ExportAsJSON dropdown does not appear for the item X',
async ({ page }) => {
// Other than non-persistible objects
// Other than non-persistable objects
}
);
});

View File

@@ -48,7 +48,7 @@ test.describe('ExportAsJSON', () => {
test.fixme(
'Verify that the ImportAsJSON dropdown does not appear for the item X',
async ({ page }) => {
// Other than non-persistible objects
// Other than non-persistable objects
}
);
});

View File

@@ -39,7 +39,7 @@ test.describe('Notebook CRUD Operations', () => {
test.fixme('Can update a Notebook Object', async ({ page }) => {});
test.fixme('Can view a perviously created Notebook Object', async ({ page }) => {});
test.fixme('Can Delete a Notebook Object', async ({ page }) => {
// Other than non-persistible objects
// Other than non-persistable objects
});
});

View File

@@ -35,7 +35,7 @@ test.describe('Snapshot Menu tests', () => {
// There should be no default notebook
// Clear default notebook if exists using `localStorage.setItem('notebook-storage', null);`
// refresh page
// Click on 'Notebook Snaphot Menu'
// Click on 'Notebook Snapshot Menu'
// 'save to Notebook Snapshots' should be only option there
}
);
@@ -62,7 +62,7 @@ test.describe('Snapshot Menu tests', () => {
});
test.fixme('Snapshots adjust time conductor', async ({ page }) => {
// Create Telemetry object
// Set Telemetry object's timeconductor to Fixed time with Start and Endtimes are recorded
// Set Telemetry object's timeconductor to Fixed time with Start and End times are recorded
// Embed Telemetry object into notebook
// Set Time Conductor to Local clock
// Click into embedded telemetry object and verify object appears with same fixed time from record

View File

@@ -157,6 +157,6 @@ test.describe('Operator Status', () => {
});
test.fixme('iterate through all possible response values', async ({ page }) => {
// test all possible respone values for the poll
// test all possible response values for the poll
});
});

View File

@@ -24,7 +24,7 @@
Testsuite for plot autoscale.
*/
const { selectInspectorTab, setTimeConductorBounds } = require('../../../../appActions');
const { selectInspectorTab, createDomainObjectWithDefaults } = require('../../../../appActions');
const { test, expect } = require('../../../../pluginFixtures');
test.use({
viewport: {
@@ -34,17 +34,26 @@ test.use({
});
test.describe('Autoscale', () => {
test('User can set autoscale with a valid range @snapshot', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
test('User can set autoscale with a valid range @snapshot', async ({ page }) => {
//This is necessary due to the size of the test suite.
test.slow();
await page.goto('./', { waitUntil: 'domcontentloaded' });
await setTimeRange(page);
const overlayPlot = await createDomainObjectWithDefaults(page, {
name: 'Test Overlay Plot',
type: 'Overlay Plot'
});
await createDomainObjectWithDefaults(page, {
name: 'Test Sine Wave Generator',
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
await createSinewaveOverlayPlot(page, myItemsFolderName);
// Switch to fixed time, start: 2022-03-28 22:00:00.000 UTC, end: 2022-03-28 22:00:30.000 UTC
await page.goto(
`${overlayPlot.url}?tc.mode=fixed&tc.startBound=1648591200000&tc.endBound=1648591230000&tc.timeSystem=utc&view=plot-overlay`
);
await testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']);
@@ -67,7 +76,7 @@ test.describe('Autoscale', () => {
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
// Make sure that after turning off autoscale, the user entered range values are reflexted in the ticks.
// Make sure that after turning off autoscale, the user entered range values are reflected in the ticks.
await testYTicks(page, [
'-2.00',
'-1.50',
@@ -109,7 +118,7 @@ test.describe('Autoscale', () => {
// Ensure the drag worked.
await testYTicks(page, ['-0.50', '0.00', '0.50', '1.00', '1.50', '2.00', '2.50', '3.00']);
//Wait for canvas to stablize.
//Wait for canvas to stabilize.
await canvas.hover({ trial: true });
expect
@@ -118,72 +127,6 @@ test.describe('Autoscale', () => {
});
});
/**
* @param {import('@playwright/test').Page} page
* @param {string} start
* @param {string} end
*/
async function setTimeRange(
page,
start = '2022-03-29 22:00:00.000Z',
end = '2022-03-29 22:00:30.000Z'
) {
// Set a specific time range for consistency, otherwise it will change
// on every test to a range based on the current time.
await setTimeConductorBounds(page, start, end);
}
/**
* @param {import('@playwright/test').Page} page
* @param {string} myItemsFolderName
*/
async function createSinewaveOverlayPlot(page, myItemsFolderName) {
// click create button
await page.locator('button:has-text("Create")').click();
// add overlay plot with defaults
await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click();
await Promise.all([
page.waitForNavigation(),
page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear1
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
// save (exit edit mode)
await page
.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button')
.nth(1)
.click();
await page.locator('text=Save and Finish Editing').click();
// click create button
await page.locator('button:has-text("Create")').click();
// add sine wave generator with defaults
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
await Promise.all([
page.waitForNavigation(),
page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear1
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
// focus the overlay plot
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Overlay Plot').first().click()
]);
}
/**
* @param {import('@playwright/test').Page} page
*/

View File

@@ -42,7 +42,7 @@ test.describe('Plot Tagging', () => {
* @param {Number} yEnd a telemetry item with a plot
* @returns {Promise}
*/
async function createTags({ page, canvas, xEnd = 700, yEnd = 520 }) {
async function createTags({ page, canvas, xEnd = 700, yEnd = 480 }) {
await canvas.hover({ trial: true });
//Alt+Shift Drag Start to select some points to tag
@@ -64,7 +64,7 @@ test.describe('Plot Tagging', () => {
await page.keyboard.up('Alt');
await page.keyboard.up('Shift');
//Wait for canvas to stablize.
//Wait for canvas to stabilize.
await canvas.hover({ trial: true });
// add some tags
@@ -173,6 +173,7 @@ test.describe('Plot Tagging', () => {
});
test('Tags work with Overlay Plots', async ({ page }) => {
test.slow();
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6822'
@@ -284,7 +285,7 @@ test.describe('Plot Tagging', () => {
page,
canvas,
xEnd: 700,
yEnd: 240
yEnd: 215
});
await basicTagsTests(page);
await testTelemetryItem(page, alphaSineWave);

View File

@@ -25,15 +25,12 @@ const {
openObjectTreeContextMenu,
createDomainObjectWithDefaults
} = require('../../../../appActions');
import { MISSION_TIME } from '../../../../constants';
test.describe('Timer', () => {
let timer;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
timer = await createDomainObjectWithDefaults(page, { type: 'timer' });
await assertTimerElements(page, timer);
});
test('Can perform actions on the Timer', async ({ page }) => {
@@ -66,70 +63,6 @@ test.describe('Timer', () => {
});
});
test.describe('Timer with target date', () => {
let timer;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
timer = await createDomainObjectWithDefaults(page, { type: 'timer' });
await assertTimerElements(page, timer);
});
// Override clock
test.use({
clockOptions: {
now: MISSION_TIME,
shouldAdvanceTime: true
}
});
test('Can count down to a target date', async ({ page }) => {
// Set the target date to 2024-11-24 03:30:00
await page.getByTitle('More options').click();
await page.getByRole('menuitem', { name: /Edit Properties.../ }).click();
await page.getByPlaceholder('YYYY-MM-DD').fill('2024-11-24');
await page.locator('input[name="hour"]').fill('3');
await page.locator('input[name="min"]').fill('30');
await page.locator('input[name="sec"]').fill('00');
await page.getByLabel('Save').click();
// Get the current timer seconds value
const timerSecValue = (await page.locator('.c-timer__value').innerText()).split(':').at(-1);
await expect(page.locator('.c-timer__direction')).toHaveClass(/icon-minus/);
// Wait for the timer to count down and assert
await expect
.poll(async () => {
const newTimerValue = (await page.locator('.c-timer__value').innerText()).split(':').at(-1);
return Number(newTimerValue);
})
.toBeLessThan(Number(timerSecValue));
});
test('Can count up from a target date', async ({ page }) => {
// Set the target date to 2020-11-23 03:30:00
await page.getByTitle('More options').click();
await page.getByRole('menuitem', { name: /Edit Properties.../ }).click();
await page.getByPlaceholder('YYYY-MM-DD').fill('2020-11-23');
await page.locator('input[name="hour"]').fill('3');
await page.locator('input[name="min"]').fill('30');
await page.locator('input[name="sec"]').fill('00');
await page.getByLabel('Save').click();
// Get the current timer seconds value
const timerSecValue = (await page.locator('.c-timer__value').innerText()).split(':').at(-1);
await expect(page.locator('.c-timer__direction')).toHaveClass(/icon-plus/);
// Wait for the timer to count up and assert
await expect
.poll(async () => {
const newTimerValue = (await page.locator('.c-timer__value').innerText()).split(':').at(-1);
return Number(newTimerValue);
})
.toBeGreaterThan(Number(timerSecValue));
});
});
/**
* Actions that can be performed on a timer from context menus.
* @typedef {'Start' | 'Stop' | 'Pause' | 'Restart at 0'} TimerAction
@@ -208,17 +141,14 @@ function buttonTitleFromAction(action) {
* @param {TimerAction} action
*/
async function assertTimerStateAfterAction(page, action) {
const timerValue = page.locator('.c-timer__value');
let timerStateClass;
switch (action) {
case 'Start':
case 'Restart at 0':
timerStateClass = 'is-started';
expect(await timerValue.innerText()).toBe('0D 00:00:00');
break;
case 'Stop':
timerStateClass = 'is-stopped';
expect(await timerValue.innerText()).toBe('--:--:--');
break;
case 'Pause':
timerStateClass = 'is-paused';
@@ -227,25 +157,3 @@ async function assertTimerStateAfterAction(page, action) {
await expect.soft(page.locator('.c-timer')).toHaveClass(new RegExp(timerStateClass));
}
/**
* Assert that all the major components of a timer are present in the DOM.
* @param {import('@playwright/test').Page} page
* @param {import('../../../../appActions').CreatedObjectInfo} timer
*/
async function assertTimerElements(page, timer) {
const timerElement = page.locator('.c-timer');
const resetButton = page.getByRole('button', { name: 'Reset' });
const pausePlayButton = page
.getByRole('button', { name: 'Pause' })
.or(page.getByRole('button', { name: 'Start' }));
const timerDirectionIcon = page.locator('.c-timer__direction');
const timerValue = page.locator('.c-timer__value');
expect(await page.locator('.l-browse-bar__object-name').innerText()).toBe(timer.name);
expect(timerElement).toBeAttached();
expect(resetButton).toBeAttached();
expect(pausePlayButton).toBeAttached();
expect(timerDirectionIcon).toBeAttached();
expect(timerValue).toBeAttached();
}

View File

@@ -0,0 +1,121 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
/*
This test suite is an initial example for memory leak testing using performance. This configuration and execution must
be kept separate from the traditional performance measurements to avoid any "observer" effects associated with tracing
or profiling playwright and/or the browser.
Based on a pattern identified in https://github.com/trentmwillis/devtools-protocol-demos/blob/master/testing-demos/memory-leak-by-heap.js
and https://github.com/paulirish/automated-chrome-profiling/issues/3
Best path forward: https://github.com/cowchimp/headless-devtools/blob/master/src/Memory/example.js
*/
const { test, expect } = require('@playwright/test');
const filePath = 'e2e/test-data/PerformanceDisplayLayout.json';
// eslint-disable-next-line playwright/no-skipped-test
test.describe.skip('Memory Performance tests', () => {
test.beforeEach(async ({ page, browser }, testInfo) => {
// Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
// Click a:has-text("My Items")
await page.locator('a:has-text("My Items")').click({
button: 'right'
});
// Click text=Import from JSON
await page.locator('text=Import from JSON').click();
// Upload Performance Display Layout.json
await page.setInputFiles('#fileElem', filePath);
// Click text=OK
await page.locator('text=OK').click();
await expect(
page.locator('a:has-text("Performance Display Layout Display Layout")')
).toBeVisible();
});
test('Embedded View Large for Imagery is performant in Fixed Time', async ({ page, browser }) => {
await page.goto('./', { waitUntil: 'networkidle' });
// To to Search Available after Launch
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Fill Search input
await page
.locator('[aria-label="OpenMCT Search"] input[type="search"]')
.fill('Performance Display Layout');
//Search Result Appears and is clicked
await Promise.all([
page.waitForNavigation(),
page.locator('a:has-text("Performance Display Layout")').first().click()
]);
//Time to Example Imagery Frame loads within Display Layout
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible' });
//Time to Example Imagery object loads
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible' });
const client = await page.context().newCDPSession(page);
await client.send('HeapProfiler.enable');
await client.send('HeapProfiler.startSampling');
// await client.send('HeapProfiler.collectGarbage');
await client.send('Performance.enable');
let performanceMetricsBefore = await client.send('Performance.getMetrics');
console.log(performanceMetricsBefore.metrics);
//await client.send('Performance.disable');
//Open Large view
await page.locator('button:has-text("Large View")').click();
await client.send('HeapProfiler.takeHeapSnapshot');
//Time to Imagery Rendered in Large Frame
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible' });
//Time to Example Imagery object loads
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible' });
// Click Close Icon
await page.locator('.c-click-icon').click();
//Time to Example Imagery Frame loads within Display Layout
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible' });
//Time to Example Imagery object loads
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible' });
await client.send('HeapProfiler.collectGarbage');
//await client.send('Performance.enable');
let performanceMetricsAfter = await client.send('Performance.getMetrics');
console.log(performanceMetricsAfter.metrics);
//await client.send('Performance.disable');
});
});

View File

@@ -1,299 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
const { test, expect } = require('@playwright/test');
const memoryLeakFilePath = 'e2e/test-data/memory-leak-detection.json';
/**
* Executes tests to verify that views are not leaking memory on navigation away. This sort of
* memory leak is generally caused by a failure to clean up registered listeners.
*
* These tests are executed on a set of pre-built displays loaded from ../test-data/memory-leak-detection.json.
*
* In order to modify the test data set:
* 1. Run Open MCT locally (npm start)
* 2. Right click on a folder in the tree, and select "Import From JSON"
* 3. In the subsequent dialog, select the file ../test-data/memory-leak-detection.json
* 4. Click "OK"
* 5. Modify test objects as desired
* 6. Right click on the "Memory Leak Detection" folder, and select "Export to JSON"
* 7. Copy the exported file to ../test-data/memory-leak-detection.json
*
*/
const NAV_LEAK_TIMEOUT = 10 * 1000; // 10s
test.describe('Navigation memory leak is not detected in', () => {
test.beforeEach(async ({ page }) => {
// Go to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.locator('a:has-text("My Items")').click({
button: 'right'
});
await page.locator('text=Import from JSON').click();
// Upload memory-leak-detection.json
await page.setInputFiles('#fileElem', memoryLeakFilePath);
await page.locator('text=OK').click();
await expect(page.locator('a:has-text("Memory Leak Detection")')).toBeVisible();
});
test('plot view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'overlay-plot-single-1hz-swg', {
timeout: NAV_LEAK_TIMEOUT
});
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('stacked plot view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'stacked-plot-single-1hz-swg', {
timeout: NAV_LEAK_TIMEOUT
});
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('LAD table view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'lad-table-single-1hz-swg', {
timeout: NAV_LEAK_TIMEOUT
});
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('LAD table set', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(page, 'lad-table-set-single-1hz-swg', {
timeout: NAV_LEAK_TIMEOUT
});
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
//TODO: Figure out why using the `table-row` component inside the `table` component leaks TelemetryTableRow objects
test('telemetry table view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'telemetry-table-single-1hz-swg',
{
timeout: NAV_LEAK_TIMEOUT
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
//TODO: Figure out why using the `SideBar` component inside the leaks Notebook objects
test('notebook view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'notebook-memory-leak-detection-test',
{
timeout: NAV_LEAK_TIMEOUT
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('display layout of a single SWG alphanumeric', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'display-layout-single-1hz-swg',
{
timeout: NAV_LEAK_TIMEOUT
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('display layout of a single SWG plot', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'display-layout-single-overlay-plot',
{
timeout: NAV_LEAK_TIMEOUT
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
//TODO: Figure out why `svg` in the CompassRose component leaks imagery
test('example imagery view', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'example-imagery-memory-leak-test',
{
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('display layout of example imagery views', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'display-layout-images-memory-leak-test',
{
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('display layout with plots of swgs, alphanumerics, and condition sets, ', async ({
page
}) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'display-layout-simple-telemetry',
{
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('flexible layout with plots of swgs', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'flexible-layout-plots-memory-leak-test',
{
timeout: NAV_LEAK_TIMEOUT
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('flexible layout of example imagery views', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'flexible-layout-images-memory-leak-test',
{
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('tabbed view of display layouts and time strips', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'tab-view-simple-memory-leak-test',
{
timeout: NAV_LEAK_TIMEOUT * 6 * 2 // 2 min
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
test('time strip view of telemetry', async ({ page }) => {
const result = await navigateToObjectAndDetectMemoryLeak(
page,
'time-strip-telemetry-memory-leak-test',
{
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
}
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
/**
*
* @param {import('@playwright/test').Page} page
* @param {*} objectName
* @returns
*/
async function navigateToObjectAndDetectMemoryLeak(page, objectName) {
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Fill Search input
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill(objectName);
//Search Result Appears and is clicked
await Promise.all([
page.locator(`div.c-gsearch-result__title:has-text("${objectName}")`).first().click(),
page.waitForNavigation()
]);
// Register a finalization listener on the root node for the view. This tends to be the last thing to be
// garbage collected since it has either direct or indirect references to all resources used by the view. Therefore it's a pretty good proxy
// for detecting memory leaks.
await page.evaluate(() => {
window.gcPromise = new Promise((resolve) => {
// eslint-disable-next-line no-undef
window.fr = new FinalizationRegistry(resolve);
window.fr.register(
window.openmct.layout.$refs.browseObject.$refs.objectViewWrapper.firstChild,
'navigatedObject',
window.openmct.layout.$refs.browseObject.$refs.objectViewWrapper.firstChild
);
});
});
// Nav back to folder
await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
await page.waitForNavigation();
// This next code block blocks until the finalization listener is called and the gcPromise resolved. This means that the root node for the view has been garbage collected.
// In the event that the root node is not garbage collected, the gcPromise will never resolve and the test will time out.
await page.evaluate(() => {
const gcPromise = window.gcPromise;
window.gcPromise = null;
// Manually invoke the garbage collector once all references are removed.
window.gc();
return gcPromise;
});
// Clean up the finalization registry since we don't need it any more.
await page.evaluate(() => {
window.fr = null;
});
// If we get here without timing out, it means the garbage collection promise resolved and the test passed.
return true;
}
});

View File

@@ -41,7 +41,7 @@ test.describe.fixme('Plot Tagging Performance', () => {
* @param {Number} yEnd a telemetry item with a plot
* @returns {Promise}
*/
async function createTags({ page, canvas, xEnd = 700, yEnd = 520 }) {
async function createTags({ page, canvas, xEnd = 700, yEnd = 480 }) {
await canvas.hover({ trial: true });
//Alt+Shift Drag Start to select some points to tag
@@ -63,7 +63,7 @@ test.describe.fixme('Plot Tagging Performance', () => {
await page.keyboard.up('Alt');
await page.keyboard.up('Shift');
//Wait for canvas to stablize.
//Wait for canvas to stabilize.
await canvas.hover({ trial: true });
// add some tags
@@ -91,7 +91,7 @@ test.describe.fixme('Plot Tagging Performance', () => {
const canvas = page.locator('canvas').nth(1);
//Wait for canvas to stablize.
//Wait for canvas to stabilize.
await canvas.hover({ trial: true });
// click on the tagged plot point
@@ -265,7 +265,7 @@ test.describe.fixme('Plot Tagging Performance', () => {
page,
canvas,
xEnd: 700,
yEnd: 240
yEnd: 215
});
await basicTagsTests(page);
await testTelemetryItem(page, alphaSineWave);

View File

@@ -46,7 +46,7 @@ test.describe('Fault Management Visual Tests', () => {
await percySnapshot(
page,
`Acknowledged faults, have a checkmark on the fault icon and appear in the acknowldeged view (theme: '${theme}')`
`Acknowledged faults, have a checkmark on the fault icon and appear in the acknowledged view (theme: '${theme}')`
);
});

View File

@@ -19,7 +19,7 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import EventTelmetryProvider from './EventTelemetryProvider';
import EventTelemetryProvider from './EventTelemetryProvider';
import EventMetadataProvider from './EventMetadataProvider';
export default function EventGeneratorPlugin(options) {
@@ -36,7 +36,7 @@ export default function EventGeneratorPlugin(options) {
};
}
});
openmct.telemetry.addProvider(new EventTelmetryProvider());
openmct.telemetry.addProvider(new EventTelemetryProvider());
openmct.telemetry.addProvider(new EventMetadataProvider());
};
}

View File

@@ -88,10 +88,10 @@ define(['uuid'], function ({ v4: uuid }) {
};
WorkerInterface.prototype.subscribe = function (request, cb) {
const id = request.id;
const { id, loadDelay } = request;
const messageId = this.dispatch('subscribe', request, (message) => {
if (!this.staleTelemetryIds[id]) {
cb(message.data);
setTimeout(() => cb(message.data), Math.max(loadDelay, 0));
}
});

View File

@@ -40,8 +40,8 @@ const DEFAULT_IMAGE_SAMPLES = [
'https://www.hq.nasa.gov/alsj/a16/AS16-117-18747.jpg',
'https://www.hq.nasa.gov/alsj/a16/AS16-117-18748.jpg'
];
const DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS = 20000;
const MIN_IMAGE_LOAD_DELAY_IN_MILISECONDS = 5000;
const DEFAULT_IMAGE_LOAD_DELAY_IN_MILLISECONDS = 20000;
const MIN_IMAGE_LOAD_DELAY_IN_MILLISECONDS = 5000;
let openmctInstance;
@@ -58,7 +58,7 @@ export default function () {
initialize: (object) => {
object.configuration = {
imageLocation: '',
imageLoadDelayInMilliSeconds: DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS,
imageLoadDelayInMilliSeconds: DEFAULT_IMAGE_LOAD_DELAY_IN_MILLISECONDS,
imageSamples: [],
layers: []
};
@@ -188,20 +188,20 @@ function getImageLoadDelay(domainObject) {
openmctInstance.objects.mutate(
domainObject,
'configuration.imageLoadDelayInMilliSeconds',
DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS
DEFAULT_IMAGE_LOAD_DELAY_IN_MILLISECONDS
);
return DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS;
return DEFAULT_IMAGE_LOAD_DELAY_IN_MILLISECONDS;
}
if (imageLoadDelay < MIN_IMAGE_LOAD_DELAY_IN_MILISECONDS) {
if (imageLoadDelay < MIN_IMAGE_LOAD_DELAY_IN_MILLISECONDS) {
openmctInstance.objects.mutate(
domainObject,
'configuration.imageLoadDelayInMilliSeconds',
MIN_IMAGE_LOAD_DELAY_IN_MILISECONDS
MIN_IMAGE_LOAD_DELAY_IN_MILLISECONDS
);
return MIN_IMAGE_LOAD_DELAY_IN_MILISECONDS;
return MIN_IMAGE_LOAD_DELAY_IN_MILLISECONDS;
}
return imageLoadDelay;

View File

@@ -28,7 +28,7 @@
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0, shrink-to-fit=no"
/>
<meta name="apple-mobile-web-app-capable" content="yes" />
<title></title>
<title>Open MCT</title>
<script src="dist/openmct.js"></script>
<link
rel="icon"
@@ -51,7 +51,7 @@
sizes="16x16"
type="image/x-icon"
/>
<style type="text/css">
<style>
@keyframes splash-spinner {
0% {
transform: translate(-50%, -50%) rotate(0deg);

View File

@@ -45,7 +45,7 @@ module.exports = (config) => {
basePath: '',
frameworks: ['jasmine', 'webpack'],
files: [
'indexTest.js',
'index-test.js',
// included means: should the files be included in the browser using <script> tag?
// We don't want them as a <script> because the shared worker source
// needs loaded remotely by the shared worker process.
@@ -102,7 +102,7 @@ module.exports = (config) => {
failFast: false
},
preprocessors: {
'indexTest.js': ['webpack', 'sourcemap']
'index-test.js': ['webpack', 'sourcemap']
},
webpack: webpackConfig,
webpackMiddleware: {

View File

@@ -1,6 +1,6 @@
{
"name": "openmct",
"version": "3.0.2",
"version": "3.1.0-next",
"description": "The Open MCT core platform",
"devDependencies": {
"@babel/eslint-parser": "7.22.5",
@@ -19,6 +19,7 @@
"codecov": "3.8.3",
"comma-separated-values": "3.6.4",
"copy-webpack-plugin": "11.0.0",
"cspell": "6.31.2",
"css-loader": "6.8.1",
"d3-axis": "3.0.0",
"d3-scale": "3.3.0",
@@ -77,9 +78,8 @@
},
"scripts": {
"clean": "rm -rf ./dist ./node_modules ./package-lock.json ./coverage ./html-test-results ./test-results ./.nyc_output ",
"clean-test-lint": "npm run clean; npm install; npm run test; npm run lint",
"clean-test-lint": "npm run clean; npm install; npm run test; npm run lint; npm run lint:spelling",
"start": "npx webpack serve --config ./.webpack/webpack.dev.js",
"start:prod": "npx webpack serve --config ./.webpack/webpack.prod.js",
"start:coverage": "npx webpack serve --config ./.webpack/webpack.coverage.js",
"lint": "eslint example src e2e --ext .js openmct.js --max-warnings=0 && eslint example src --ext .vue",
"lint:spelling": "cspell \"**/*.{js,md,vue}\" --show-context --gitignore",
@@ -102,9 +102,7 @@
"test:e2e:visual:full": "percy exec --config ./e2e/.percy.nightly.yml -- npx playwright test --config=e2e/playwright-visual.config.js --grep-invert @unstable",
"test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js --grep-invert @couchdb",
"test:e2e:watch": "npx playwright test --ui --config=e2e/playwright-ci.config.js",
"test:perf:contract": "npx playwright test --config=e2e/playwright-performance-dev.config.js",
"test:perf:localhost": "npx playwright test --config=e2e/playwright-performance-prod.config.js --project=chrome",
"test:perf:memory": "npx playwright test --config=e2e/playwright-performance-prod.config.js --project=chrome-memory",
"test:perf": "npx playwright test --config=e2e/playwright-performance.config.js",
"update-about-dialog-copyright": "perl -pi -e 's/20\\d\\d\\-202\\d/2014\\-2023/gm' ./src/ui/layout/AboutDialog.vue",
"update-copyright-date": "npm run update-about-dialog-copyright && grep -lr --null --include=*.{js,scss,vue,ts,sh,html,md,frag} 'Copyright (c) 20' . | xargs -r0 perl -pi -e 's/Copyright\\s\\(c\\)\\s20\\d\\d\\-20\\d\\d/Copyright \\(c\\)\\ 2014\\-2023/gm'",
"cov:e2e:report": "nyc report --reporter=lcovonly --report-dir=./coverage/e2e",
@@ -113,6 +111,7 @@
"cov:unit:publish": "codecov --disable=gcov -f ./coverage/unit/lcov.info -F unit",
"prepare": "npm run build:prod && npx tsc"
},
"homepage": "https://nasa.github.io/openmct",
"repository": {
"type": "git",
"url": "https://github.com/nasa/openmct.git"
@@ -128,6 +127,12 @@
"ios_saf >= 16",
"Safari >= 16"
],
"author": "",
"license": "Apache-2.0"
"author": {
"name": "National Aeronautics and Space Administration",
"url": "https://www.nasa.gov"
},
"license": "Apache-2.0",
"keywords": [
"nasa"
]
}

View File

@@ -43,6 +43,7 @@ define([
'./plugins/duplicate/plugin',
'./plugins/importFromJSONAction/plugin',
'./plugins/exportAsJSONAction/plugin',
'./ui/components/components',
'vue'
], function (
EventEmitter,
@@ -67,6 +68,7 @@ define([
DuplicateActionPlugin,
ImportFromJSONAction,
ExportAsJSONAction,
components,
Vue
) {
/**
@@ -428,6 +430,7 @@ define([
};
MCT.prototype.plugins = plugins;
MCT.prototype.components = components.default;
return MCT;
});

View File

@@ -92,7 +92,7 @@ class ActionsAPI extends EventEmitter {
if (this._actionCollections.has(key)) {
let actionCollection = this._actionCollections.get(key);
actionCollection.off('destroy', this._updateCachedActionCollections);
delete actionCollection.applicableActions;
this._actionCollections.delete(key);
}
}

View File

@@ -100,7 +100,7 @@ export default class AnnotationAPI extends EventEmitter {
creatable: false,
cssClass: 'icon-notebook',
initialize: function (domainObject) {
domainObject.targets = domainObject.targets || [];
domainObject.targets = domainObject.targets || {};
domainObject._deleted = domainObject._deleted || false;
domainObject.originalContextPath = domainObject.originalContextPath || '';
domainObject.tags = domainObject.tags || [];
@@ -117,10 +117,10 @@ export default class AnnotationAPI extends EventEmitter {
* @property {ANNOTATION_TYPES} annotationType the type of annotation to create (e.g., PLOT_SPATIAL)
* @property {Tag[]} tags tags to add to the annotation, e.g., SCIENCE for science related annotations
* @property {String} contentText Some text to add to the annotation, e.g. ("This annotation is about science")
* @property {Array<Object>} targets The targets ID keystrings and their specific properties.
* For plots, this will be a bounding box, e.g.: {keyString: "d8385009-789d-457b-acc7-d50ba2fd55ea", maxY: 100, minY: 0, maxX: 100, minX: 0}
* @property {Object<string, Object>} targets The targets ID keystrings and their specific properties.
* For plots, this will be a bounding box, e.g.: {maxY: 100, minY: 0, maxX: 100, minX: 0}
* For notebooks, this will be an entry ID, e.g.: {entryId: "entry-ecb158f5-d23c-45e1-a704-649b382622ba"}
* @property {DomainObject>[]} targetDomainObjects the domain objects this annotation points to (e.g., telemetry objects for a plot)
* @property {DomainObject>} targetDomainObjects the targets ID keystrings and the domain objects this annotation points to (e.g., telemetry objects for a plot)
*/
/**
* @method create
@@ -141,15 +141,11 @@ export default class AnnotationAPI extends EventEmitter {
throw new Error(`Unknown annotation type: ${annotationType}`);
}
if (!targets.length) {
if (!Object.keys(targets).length) {
throw new Error(`At least one target is required to create an annotation`);
}
if (targets.some((target) => !target.keyString)) {
throw new Error(`All targets require a keyString to create an annotation`);
}
if (!targetDomainObjects.length) {
if (!Object.keys(targetDomainObjects).length) {
throw new Error(`At least one targetDomainObject is required to create an annotation`);
}
@@ -185,7 +181,7 @@ export default class AnnotationAPI extends EventEmitter {
const success = await this.openmct.objects.save(createdObject);
if (success) {
this.emit('annotationCreated', createdObject);
targetDomainObjects.forEach((targetDomainObject) => {
Object.values(targetDomainObjects).forEach((targetDomainObject) => {
this.#updateAnnotationModified(targetDomainObject);
});
@@ -325,10 +321,7 @@ export default class AnnotationAPI extends EventEmitter {
}
#addTagMetaInformationToTags(tags) {
// Convert to Set and back to Array to remove duplicates
const uniqueTags = [...new Set(tags)];
return uniqueTags.map((tagKey) => {
return tags.map((tagKey) => {
const tagModel = this.availableTags[tagKey];
tagModel.tagID = tagKey;
@@ -370,8 +363,7 @@ export default class AnnotationAPI extends EventEmitter {
const modelAddedToResults = await Promise.all(
results.map(async (result) => {
const targetModels = await Promise.all(
result.targets.map(async (target) => {
const targetID = target.keyString;
Object.keys(result.targets).map(async (targetID) => {
const targetModel = await this.openmct.objects.get(targetID);
const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier);
const originalPathObjects = await this.openmct.objects.getOriginalPath(targetKeyString);
@@ -418,12 +410,13 @@ export default class AnnotationAPI extends EventEmitter {
#breakApartSeparateTargets(results) {
const separateResults = [];
results.forEach((result) => {
result.targets.forEach((target) => {
const targetID = target.keyString;
Object.keys(result.targets).forEach((targetID) => {
const separatedResult = {
...result
};
separatedResult.targets = [target];
separatedResult.targets = {
[targetID]: result.targets[targetID]
};
separatedResult.targetModels = result.targetModels.filter((targetModel) => {
const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier);

View File

@@ -62,12 +62,11 @@ describe('The Annotation API', () => {
key: 'anAnnotationKey',
namespace: 'fooNameSpace'
},
targets: [
{
keyString: 'fooNameSpace:some-object',
targets: {
'fooNameSpace:some-object': {
entryId: 'fooBarEntry'
}
]
}
};
mockObjectProvider = jasmine.createSpyObj('mock provider', ['create', 'update', 'get']);
@@ -122,7 +121,7 @@ describe('The Annotation API', () => {
tags: ['sometag'],
contentText: 'fooContext',
targetDomainObjects: [mockDomainObject],
targets: [{ keyString: 'fooTarget' }]
targets: { fooTarget: {} }
};
const annotationObject = await openmct.annotation.create(annotationCreationArguments);
expect(annotationObject).toBeDefined();
@@ -137,7 +136,7 @@ describe('The Annotation API', () => {
tags: ['sometag'],
contentText: 'fooContext',
targetDomainObjects: [mockDomainObject],
targets: [{ keyString: 'fooTarget' }]
targets: { fooTarget: {} }
};
openmct.annotation.setNamespaceToSaveAnnotations('fooNameSpace');
const annotationObject = await openmct.annotation.create(annotationCreationArguments);
@@ -167,9 +166,9 @@ describe('The Annotation API', () => {
tags: ['sometag'],
contentText: 'fooContext',
targetDomainObjects: [mockDomainObject],
targets: [{ keyString: 'fooTarget' }]
targets: { fooTarget: {} }
};
openmct.annotation.setNamespaceToSaveAnnotations('nameespaceThatDoesNotExist');
openmct.annotation.setNamespaceToSaveAnnotations('namespaceThatDoesNotExist');
await openmct.annotation.create(annotationCreationArguments);
} catch (error) {
expect(error).toBeDefined();
@@ -184,7 +183,7 @@ describe('The Annotation API', () => {
tags: ['sometag'],
contentText: 'fooContext',
targetDomainObjects: [mockDomainObject],
targets: [{ keyString: 'fooTarget' }]
targets: { fooTarget: {} }
};
openmct.annotation.setNamespaceToSaveAnnotations('immutableProvider');
await openmct.annotation.create(annotationCreationArguments);
@@ -203,7 +202,7 @@ describe('The Annotation API', () => {
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
tags: ['aWonderfulTag'],
contentText: 'fooContext',
targets: [{ keyString: 'fooNameSpace:some-object', entryId: 'fooBarEntry' }],
targets: { 'fooNameSpace:some-object': { entryId: 'fooBarEntry' } },
targetDomainObjects: [mockDomainObject]
};
});
@@ -273,19 +272,17 @@ describe('The Annotation API', () => {
let comparator;
beforeEach(() => {
targets = [
{
keyString: 'fooTarget',
targets = {
fooTarget: {
foo: 42
}
];
otherTargets = [
{
keyString: 'fooTarget',
};
otherTargets = {
fooTarget: {
bar: 42
}
];
comparator = (t1, t2) => t1[0].foo === t2[0].bar;
};
comparator = (t1, t2) => t1.fooTarget.foo === t2.fooTarget.bar;
});
it('can add a comparator function', () => {

View File

@@ -185,10 +185,9 @@ export default class CompositionProvider {
return;
}
const onMutation = this.#onMutation.bind(this);
this.#publicAPI.objects.eventEmitter.on('mutation', onMutation);
this.#publicAPI.objects.eventEmitter.on('mutation', this.#onMutation.bind(this));
this.topicListener = () => {
this.#publicAPI.objects.eventEmitter.off('mutation', onMutation);
this.#publicAPI.objects.eventEmitter.off('mutation', this.#onMutation.bind(this));
};
}

View File

@@ -39,7 +39,7 @@ export default class FormsAPI {
* This function accepts element, model and onChange function
* element - html element (place holder) to render a row view
* model - row data for rendering name, value etc for given row type
* onChange - an onChange event callback funtion to keep track of any change in value
* onChange - an onChange event callback function to keep track of any change in value
* @property {function} destroy a callback function when a vue component gets destroyed
*/

View File

@@ -123,6 +123,7 @@ export default {
formatDatetime(timestamp = this.model.value) {
if (!timestamp) {
this.resetValues();
return;
}
@@ -136,7 +137,7 @@ export default {
const data = {
model,
value: new Date(timestamp).toISOString()
value: timestamp
};
this.$emit('onChange', data);

View File

@@ -22,7 +22,7 @@
import NotificationAPI from './NotificationAPI';
describe('The Notifiation API', () => {
describe('The Notification API', () => {
let notificationAPIInstance;
let defaultTimeout = 4000;
@@ -108,7 +108,7 @@ describe('The Notifiation API', () => {
});
});
describe('the error method notificiation', () => {
describe('the error method notification', () => {
let message = 'Minimized error message';
afterAll(() => {

View File

@@ -435,8 +435,7 @@ class InMemorySearchProvider {
}
localIndexAnnotation(objectToIndex, model) {
model.targets.forEach((target) => {
const targetID = target.keyString;
Object.keys(model.targets).forEach((targetID) => {
if (!this.localIndexedAnnotationsByDomainObject[targetID]) {
this.localIndexedAnnotationsByDomainObject[targetID] = [];
}

View File

@@ -57,8 +57,7 @@
};
function indexAnnotation(objectToIndex, model) {
model.targets.forEach((target) => {
const targetID = target.keyString;
Object.keys(model.targets).forEach((targetID) => {
if (!indexedAnnotationsByDomainObject[targetID]) {
indexedAnnotationsByDomainObject[targetID] = [];
}

View File

@@ -103,6 +103,10 @@ export default class ObjectAPI {
this.errors = {
Conflict: ConflictError
};
this.calls = {
get: {},
makeKeyString: {}
};
}
/**
@@ -177,6 +181,31 @@ export default class ObjectAPI {
* has been updated, or be rejected if it cannot be saved
*/
addCall(type, identifier) {
let keyString;
if (!identifier) {
throw new Error('Cannot make key string from null identifier logging');
}
if (typeof identifier === 'string') {
keyString = identifier;
} else if (!identifier.namespace) {
keyString = identifier.key;
} else {
keyString = [identifier.namespace.replace(/:/g, '\\:'), identifier.key].join(':');
}
if (this.calls[type][keyString]) {
this.calls[type][keyString]++;
} else {
this.calls[type][keyString] = 1;
}
Object.entries(this.calls.get).forEach(([key, value]) => console.log(`get: ${key}: ${value}`));
Object.entries(this.calls.makeKeyString).forEach(([key, value]) => console.log(`makeKeyString: ${key}: ${value}`));
}
/**
* Delete this domain object.
*
@@ -199,6 +228,7 @@ export default class ObjectAPI {
* has been saved, or be rejected if it cannot be saved
*/
get(identifier, abortSignal, forceRemote = false) {
this.addCall('get', identifier);
let keystring = this.makeKeyString(identifier);
if (!forceRemote) {
@@ -520,7 +550,7 @@ export default class ObjectAPI {
}
/**
* Inovke interceptors if applicable for a given domain object.
* Invoke interceptors if applicable for a given domain object.
* @private
*/
applyGetInterceptors(identifier, domainObject) {
@@ -729,6 +759,7 @@ export default class ObjectAPI {
* @returns {string} A string representation of the given identifier, including namespace and key
*/
makeKeyString(identifier) {
this.addCall('makeKeyString', identifier);
return utils.makeKeyString(identifier);
}

View File

@@ -125,7 +125,7 @@ describe('The Object API', () => {
await objectAPI.save(mockDomainObject);
expect(mockDomainObject.createdBy).toBe(USERNAME);
});
it("Sets the current user for 'modifedBy' on existing objects", async () => {
it("Sets the current user for 'modifiedBy' on existing objects", async () => {
mockDomainObject.persisted = Date.now() - FIFTEEN_MINUTES;
mockDomainObject.modified = Date.now();

View File

@@ -230,7 +230,7 @@ export default class TelemetryAPI {
/**
* Register a request interceptor that transforms a request via module:openmct.TelemetryAPI.request
* The request will be modifyed when it is received and will be returned in it's modified state
* The request will be modified when it is received and will be returned in it's modified state
* The request will be transformed only if the interceptor is applicable to that domain object as defined by the RequestInterceptorDef
*
* @param {module:openmct.RequestInterceptorDef} requestInterceptorDef the request interceptor definition to add
@@ -269,7 +269,7 @@ export default class TelemetryAPI {
}
/**
* Get or set greedy LAD. For stategy "latest" telemetry in
* Get or set greedy LAD. For strategy "latest" telemetry in
* realtime mode the start bound will be ignored if true and
* there is no new data to replace the existing data.
* defaults to true

View File

@@ -92,7 +92,7 @@ define(['lodash'], function (_) {
};
/**
* Get an array of valueMetadatas that posess all hints requested.
* Get an array of valueMetadatas that possess all hints requested.
* Array is sorted based on hint priority.
*
*/

View File

@@ -24,7 +24,7 @@
* A Type describes a kind of domain object that may appear or be
* created within Open MCT.
*
* @param {module:opemct.TypeRegistry~TypeDefinition} definition
* @param {module:openmct.TypeRegistry~TypeDefinition} definition
* @class Type
* @memberof module:openmct
*/

View File

@@ -43,7 +43,7 @@ class UserAPI extends EventEmitter {
/**
* Set the user provider for the user API. This allows you
* to specifiy ONE user provider to be used with Open MCT.
* to specify ONE user provider to be used with Open MCT.
* @method setProvider
* @memberof module:openmct.UserAPI#
* @param {module:openmct.UserAPI~UserProvider} provider the new

View File

@@ -74,7 +74,7 @@ describe('The User API', () => {
provider.autoLogin(USERNAME);
});
it('to check if a user (not specific) is loged in', (done) => {
it('to check if a user (not specific) is logged in', (done) => {
expect(openmct.user.isLoggedIn()).toBeFalse();
openmct.user.on('providerAdded', () => {

View File

@@ -35,8 +35,8 @@ describe('The Image Exporter', () => {
return resetApplicationState(openmct);
});
describe('basic instatation', () => {
it('can be instatiated', () => {
describe('basic instantiation', () => {
it('can be instantiated', () => {
imageExporter = new ImageExporter(openmct);
expect(imageExporter).not.toEqual(null);

View File

@@ -65,7 +65,7 @@ export default {
},
hasUnits: {
type: Boolean,
requred: true
required: true
},
isStale: {
type: Boolean,

View File

@@ -127,7 +127,7 @@ describe('The LAD Table', () => {
}).not.toThrow();
});
it('should reject non-telemtry producing objects', () => {
it('should reject non-telemetry producing objects', () => {
expect(() => {
ladTableCompositionCollection.add(mockObj.ladTable);
}).toThrow();
@@ -254,10 +254,10 @@ describe('The LAD Table', () => {
it('should show aggregate telemetry type with blank data', async () => {
await Vue.nextTick();
const lastestData = parent
const latestData = parent
.querySelectorAll(TABLE_BODY_ROWS)[1]
.querySelectorAll('td')[2].innerText;
expect(lastestData).toBe('---');
expect(latestData).toBe('---');
const dataType = parent
.querySelectorAll(TABLE_BODY_ROWS)[1]
.querySelector('.js-type-data').innerText;

View File

@@ -86,19 +86,16 @@ export default {
handler: 'updateData'
}
},
created() {
this.registerListeners();
},
mounted() {
this.plotResizeObserver.observe(this.$refs.plotWrapper);
Plotly.newPlot(this.$refs.plot, Array.from(this.data), this.getLayout(), {
responsive: true,
displayModeBar: false
});
this.registerListeners();
},
beforeUnmount() {
if (this.plotResizeObserver) {
this.plotResizeObserver.disconnect();
this.plotResizeObserver.unobserve(this.$refs.plotWrapper);
clearTimeout(this.resizeTimer);
}
@@ -229,6 +226,7 @@ export default {
window.dispatchEvent(new Event('resize'));
}, 250);
});
this.plotResizeObserver.observe(this.$refs.plotWrapper);
}
},
reset() {

View File

@@ -158,7 +158,7 @@ export default {
this.composition.remove(telemetryObject);
},
addTelemetryObject(telemetryObject) {
// grab information we need from the added telmetry object
// grab information we need from the added telemetry object
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
this.telemetryObjects[key] = telemetryObject;
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
@@ -188,7 +188,7 @@ export default {
);
}
// ask for the current telemetry data, then subcribe for changes
// ask for the current telemetry data, then subscribe for changes
this.requestDataFor(telemetryObject);
this.subscribeToObject(telemetryObject);
},

View File

@@ -43,16 +43,17 @@ export default function BarGraphViewProvider(openmct) {
return domainObject && domainObject.type === BAR_GRAPH_KEY;
},
view(domainObject, objectPath) {
view: function (domainObject, objectPath) {
let _destroy = null;
let component = null;
return {
show(element) {
show: function (element) {
let isCompact = isCompactView(objectPath);
const { vNode, destroy } = mount(
{
el: element,
components: {
BarGraphView
},
@@ -78,7 +79,7 @@ export default function BarGraphViewProvider(openmct) {
_destroy = destroy;
component = vNode.componentInstance;
},
destroy() {
destroy: function () {
_destroy();
},
onClearData() {

View File

@@ -220,7 +220,7 @@ describe('the plugin', function () {
});
});
describe('The spectral plot view for telemetry objects with array values', () => {
xdescribe('The spectral plot view for telemetry objects with array values', () => {
let barGraphObject;
// eslint-disable-next-line no-unused-vars
let mockComposition;
@@ -256,7 +256,7 @@ describe('the plugin', function () {
await Vue.nextTick();
});
it('Renders spectral plots', async () => {
it('Renders spectral plots', () => {
const dotFullTelemetryObject = {
identifier: {
namespace: 'someNamespace',
@@ -304,12 +304,11 @@ describe('the plugin', function () {
barGraphView.show(child, true);
mockComposition.emit('add', dotFullTelemetryObject);
await Vue.nextTick();
await Vue.nextTick();
const plotElement = element.querySelector('.cartesianlayer .scatterlayer .trace .lines');
expect(plotElement).not.toBeNull();
barGraphView.destroy();
return Vue.nextTick().then(() => {
const plotElement = element.querySelector('.cartesianlayer .scatterlayer .trace .lines');
expect(plotElement).not.toBeNull();
barGraphView.destroy();
});
});
});

View File

@@ -122,7 +122,7 @@ export default {
this.composition.remove(telemetryObject);
},
addTelemetryObject(telemetryObject) {
// grab information we need from the added telmetry object
// grab information we need from the added telemetry object
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
this.telemetryObjects[key] = telemetryObject;
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);

View File

@@ -115,7 +115,7 @@ export default {
}
if (this.plotResizeObserver) {
this.plotResizeObserver.disconnect();
this.plotResizeObserver.unobserve(this.$refs.plotWrapper);
clearTimeout(this.resizeTimer);
}

View File

@@ -65,9 +65,6 @@ export default class Condition extends EventEmitter {
this.trigger = conditionConfiguration.configuration.trigger;
this.summary = '';
this.handleCriterionUpdated = this.handleCriterionUpdated.bind(this);
this.handleOldTelemetryCriterion = this.handleOldTelemetryCriterion.bind(this);
this.handleTelemetryStaleness = this.handleTelemetryStaleness.bind(this);
}
updateResult(datum) {
@@ -198,15 +195,15 @@ export default class Condition extends EventEmitter {
if (found) {
const newCriterionConfiguration = this.generateCriterion(criterionConfiguration);
let newCriterion = new TelemetryCriterion(newCriterionConfiguration, this.openmct);
newCriterion.on('criterionUpdated', this.handleCriterionUpdated);
newCriterion.on('telemetryIsOld', this.handleOldTelemetryCriterion);
newCriterion.on('telemetryStaleness', this.handleTelemetryStaleness);
newCriterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
newCriterion.on('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj));
newCriterion.on('telemetryStaleness', () => this.handleTelemetryStaleness());
let criterion = found.item;
criterion.unsubscribe();
criterion.off('criterionUpdated', this.handleCriterionUpdated);
criterion.off('telemetryIsOld', this.handleOldTelemetryCriterion);
newCriterion.off('telemetryStaleness', this.handleTelemetryStaleness);
criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
criterion.off('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj));
newCriterion.off('telemetryStaleness', () => this.handleTelemetryStaleness());
this.criteria.splice(found.index, 1, newCriterion);
}
}
@@ -215,9 +212,9 @@ export default class Condition extends EventEmitter {
let found = this.findCriterion(id);
if (found) {
let criterion = found.item;
criterion.off('criterionUpdated', this.handleCriterionUpdated);
criterion.off('telemetryIsOld', this.handleOldTelemetryCriterion);
criterion.off('telemetryStaleness', this.handleTelemetryStaleness);
criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
criterion.off('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj));
criterion.off('telemetryStaleness', () => this.handleTelemetryStaleness());
criterion.destroy();
this.criteria.splice(found.index, 1);

View File

@@ -399,7 +399,7 @@ export default class ConditionManager extends EventEmitter {
}
shouldEvaluateNewTelemetry(currentTimestamp) {
return this.openmct.time.bounds().end >= currentTimestamp;
return this.openmct.time.getBounds().end >= currentTimestamp;
}
telemetryReceived(endpoint, datum) {

View File

@@ -127,7 +127,6 @@ export default {
this.composition.off('remove', this.removeTelemetryObject);
if (this.conditionManager) {
this.conditionManager.off('conditionSetResultUpdated', this.handleConditionSetResultUpdated);
this.conditionManager.off('noTelemetryObjects', this.emitNoTelemetryObjectEvent);
this.conditionManager.destroy();
}

View File

@@ -81,7 +81,7 @@ export default {
this.listenToConditionSetChanges();
}
},
unmounted() {
beforeUnmount() {
this.stopListeningToConditionSetChanges();
},
methods: {

View File

@@ -170,7 +170,7 @@ define(['lodash'], function (_) {
if (form) {
showForm(form, name, selectionPath);
} else {
openmct.objectViews.emit('contextAction', 'addElement', name);
selectionPath[0].context.addElement(name);
}
},
key: 'add',
@@ -236,6 +236,7 @@ define(['lodash'], function (_) {
icon: 'icon-trash',
title: 'Delete the selected object',
method: function () {
let removeItem = selectionPath[1].context.removeItem;
let prompt = openmct.overlays.dialog({
iconClass: 'alert',
message: `Warning! This action will remove this item from the Display Layout. Do you want to continue?`,
@@ -244,11 +245,7 @@ define(['lodash'], function (_) {
label: 'OK',
emphasis: 'true',
callback: function () {
openmct.objectViews.emit(
'contextAction',
'removeItem',
getAllTypes(selection)
);
removeItem(getAllTypes(selection));
prompt.dismiss();
}
},
@@ -293,12 +290,7 @@ define(['lodash'], function (_) {
}
],
method: function (option) {
openmct.objectViews.emit(
'contextAction',
'orderItem',
option.value,
getAllTypes(selectedObjects)
);
selectionPath[1].context.orderItem(option.value, getAllTypes(selectedObjects));
}
};
}
@@ -482,7 +474,9 @@ define(['lodash'], function (_) {
icon: 'icon-duplicate',
title: 'Duplicate the selected object',
method: function () {
openmct.objectViews.emit('contextAction', 'duplicateItem', selection);
let duplicateItem = selectionPath[1].context.duplicateItem;
duplicateItem(selection);
}
};
}
@@ -561,7 +555,6 @@ define(['lodash'], function (_) {
function getViewSwitcherMenu(selectedParent, selectionPath, selection) {
if (selection.length === 1) {
// eslint-disable-next-line no-unused-vars
let displayLayoutContext = selectionPath[1].context;
let selectedItemContext = selectionPath[0].context;
let selectedItemType = selectedItemContext.item.type;
@@ -581,18 +574,14 @@ define(['lodash'], function (_) {
label: 'View type',
options: viewOptions,
method: function (option) {
openmct.objectViews.emit(
'contextAction',
'switchViewType',
selectedItemContext,
option.value,
selection
);
displayLayoutContext.switchViewType(selectedItemContext, option.value, selection);
}
};
}
} else if (selection.length > 1) {
if (areAllViews('telemetry-view', 'layoutItem.type', selection)) {
let displayLayoutContext = selectionPath[1].context;
return {
control: 'menu',
domainObject: selectedParent,
@@ -601,15 +590,12 @@ define(['lodash'], function (_) {
label: 'View type',
options: APPLICABLE_VIEWS['telemetry-view-multi'],
method: function (option) {
openmct.objectViews.emit(
'contextAction',
'mergeMultipleTelemetryViews',
selection,
option.value
);
displayLayoutContext.mergeMultipleTelemetryViews(selection, option.value);
}
};
} else if (areAllViews('telemetry.plot.overlay', 'item.type', selection)) {
let displayLayoutContext = selectionPath[1].context;
return {
control: 'menu',
domainObject: selectedParent,
@@ -617,12 +603,7 @@ define(['lodash'], function (_) {
title: 'Merge into a stacked plot',
options: APPLICABLE_VIEWS['telemetry.plot.overlay-multi'],
method: function (option) {
openmct.objectViews.emit(
'contextAction',
'mergeMultipleOverlayPlots',
selection,
option.value
);
displayLayoutContext.mergeMultipleOverlayPlots(selection, option.value);
}
};
}
@@ -646,7 +627,7 @@ define(['lodash'], function (_) {
domainObject: displayLayoutContext.item,
icon: ICON_GRID_SHOW,
method: function () {
openmct.objectViews.emit('contextAction', 'toggleGrid');
displayLayoutContext.toggleGrid();
this.icon = this.icon === ICON_GRID_SHOW ? ICON_GRID_HIDE : ICON_GRID_SHOW;
},
@@ -672,7 +653,7 @@ define(['lodash'], function (_) {
function showForm(formStructure, name, selectionPath) {
openmct.forms.showForm(formStructure).then((changes) => {
openmct.objectViews.emit('contextAction', 'addElement', name, changes);
selectionPath[0].context.addElement(name, changes);
});
}

View File

@@ -25,16 +25,14 @@
:item="item"
:grid-size="gridSize"
:is-editing="isEditing"
@move="move"
@endMove="endMove"
@move="(gridDelta) => $emit('move', gridDelta)"
@endMove="() => $emit('endMove')"
>
<template #content>
<div
class="c-box-view u-style-receiver js-style-receiver"
:class="[styleClass]"
:style="style"
></div>
</template>
<div
class="c-box-view u-style-receiver js-style-receiver"
:class="[styleClass]"
:style="style"
></div>
</layout-frame>
</template>
@@ -117,18 +115,10 @@ export default {
this.initSelect
);
},
beforeUnmount() {
unmounted() {
if (this.removeSelectable) {
this.removeSelectable();
}
},
methods: {
move(gridDelta) {
this.$emit('move', gridDelta);
},
endMove() {
this.$emit('endMove');
}
}
};
</script>

View File

@@ -144,7 +144,7 @@ function getItemDefinition(itemType, ...options) {
export default {
components: components,
inject: ['openmct', 'objectPath', 'options', 'currentView'],
inject: ['openmct', 'objectPath', 'options', 'objectUtils', 'currentView'],
props: {
domainObject: {
type: Object,
@@ -221,21 +221,13 @@ export default {
this.composition.load();
this.gridDimensions = [this.$el.offsetWidth, this.$el.scrollHeight];
this.unObserveItems = this.openmct.objects.observe(
this.domainObject,
'configuration.items',
(items) => {
this.layoutItems = [...items];
}
);
this.openmct.objects.observe(this.domainObject, 'configuration.items', (items) => {
this.layoutItems = items;
});
this.watchDisplayResize();
},
beforeUnmount() {
if (this.unObserveItems) {
this.unObserveItems();
}
this.unwatchDisplayResize();
unmounted() {
this.openmct.selection.off('change', this.setSelection);
this.composition.off('add', this.addChild);
this.composition.off('remove', this.removeChild);
@@ -259,15 +251,9 @@ export default {
this.$el.click();
},
watchDisplayResize() {
this.unwatchDisplayResize();
this.resizeObserver = new ResizeObserver(this.updateGrid);
const resizeObserver = new ResizeObserver(() => this.updateGrid());
this.resizeObserver.observe(this.$el);
},
unwatchDisplayResize() {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
resizeObserver.observe(this.$el);
},
addElement(itemType, element) {
this.addItem(itemType + '-view', element);
@@ -637,7 +623,7 @@ export default {
return this.openmct.objects.makeKeyString(item.identifier) !== keyString;
}
});
this.layoutItems = [...layoutItems];
this.layoutItems = layoutItems;
this.mutate('configuration.items', layoutItems);
this.clearSelection();
},

View File

@@ -25,16 +25,14 @@
:item="item"
:grid-size="gridSize"
:is-editing="isEditing"
@move="move"
@endMove="endMove"
@move="(gridDelta) => $emit('move', gridDelta)"
@endMove="() => $emit('endMove')"
>
<template #content>
<div
class="c-ellipse-view u-style-receiver js-style-receiver"
:class="[styleClass]"
:style="style"
></div>
</template>
<div
class="c-ellipse-view u-style-receiver js-style-receiver"
:class="[styleClass]"
:style="style"
></div>
</layout-frame>
</template>
@@ -117,18 +115,10 @@ export default {
this.initSelect
);
},
beforeUnmount() {
unmounted() {
if (this.removeSelectable) {
this.removeSelectable();
}
},
methods: {
move(gridDelta) {
this.$emit('move', gridDelta);
},
endMove() {
this.$emit('endMove');
}
}
};
</script>

View File

@@ -25,12 +25,10 @@
:item="item"
:grid-size="gridSize"
:is-editing="isEditing"
@move="move"
@endMove="endMove"
@move="(gridDelta) => $emit('move', gridDelta)"
@endMove="() => $emit('endMove')"
>
<template #content>
<div class="c-image-view" :style="style"></div>
</template>
<div class="c-image-view" :class="[styleClass]" :style="style"></div>
</layout-frame>
</template>
@@ -120,18 +118,10 @@ export default {
this.initSelect
);
},
beforeUnmount() {
unmounted() {
if (this.removeSelectable) {
this.removeSelectable();
}
},
methods: {
move(gridDelta) {
this.$emit('move', gridDelta);
},
endMove() {
this.$emit('endMove');
}
}
};
</script>

View File

@@ -29,7 +29,7 @@
}"
:style="style"
>
<slot name="content"></slot>
<slot></slot>
<div class="c-frame__move-bar" @mousedown.left="startMove($event)"></div>
</div>
</template>

View File

@@ -20,26 +20,24 @@
at runtime from the About dialog for additional information.
-->
<template>
<layout-frame
<LayoutFrame
:item="item"
:grid-size="gridSize"
:is-editing="isEditing"
@move="move"
@endMove="endMove"
@move="(gridDelta) => $emit('move', gridDelta)"
@endMove="() => $emit('endMove')"
>
<template #content>
<ObjectFrame
v-if="domainObject"
ref="objectFrame"
:domain-object="domainObject"
:object-path="currentObjectPath"
:has-frame="item.hasFrame"
:show-edit-view="false"
:layout-font-size="item.fontSize"
:layout-font="item.font"
/>
</template>
</layout-frame>
<ObjectFrame
v-if="domainObject"
ref="objectFrame"
:domain-object="domainObject"
:object-path="currentObjectPath"
:has-frame="item.hasFrame"
:show-edit-view="false"
:layout-font-size="item.fontSize"
:layout-font="item.font"
/>
</LayoutFrame>
</template>
<script>
@@ -106,7 +104,8 @@ export default {
data() {
return {
domainObject: undefined,
currentObjectPath: []
currentObjectPath: [],
mutablePromise: undefined
};
},
watch: {
@@ -169,12 +168,6 @@ export default {
delete this.immediatelySelect;
}
});
},
move(gridDelta) {
this.$emit('move', gridDelta);
},
endMove() {
this.$emit('endMove');
}
}
};

View File

@@ -25,44 +25,42 @@
:item="item"
:grid-size="gridSize"
:is-editing="isEditing"
@move="move"
@endMove="endMove"
@move="(gridDelta) => $emit('move', gridDelta)"
@endMove="() => $emit('endMove')"
>
<template #content>
<div
v-if="domainObject"
ref="telemetryViewWrapper"
class="c-telemetry-view u-style-receiver"
:class="[itemClasses]"
:style="styleObject"
:data-font-size="item.fontSize"
:data-font="item.font"
@contextmenu.prevent="showContextMenu"
@mouseover.ctrl="showToolTip"
@mouseleave="hideToolTip"
>
<div class="is-status__indicator" :title="`This item is ${status}`"></div>
<div v-if="showLabel" class="c-telemetry-view__label">
<div class="c-telemetry-view__label-text">
{{ domainObject.name }}
</div>
</div>
<div
v-if="showValue"
:title="fieldName"
class="c-telemetry-view__value"
:class="[telemetryClass]"
>
<div class="c-telemetry-view__value-text">
{{ telemetryValue }}
<span v-if="unit && item.showUnits" class="c-telemetry-view__value-text__unit">
{{ unit }}
</span>
</div>
<div
v-if="domainObject"
ref="telemetryViewWrapper"
class="c-telemetry-view u-style-receiver"
:class="[itemClasses]"
:style="styleObject"
:data-font-size="item.fontSize"
:data-font="item.font"
@contextmenu.prevent="showContextMenu"
@mouseover.ctrl="showToolTip"
@mouseleave="hideToolTip"
>
<div class="is-status__indicator" :title="`This item is ${status}`"></div>
<div v-if="showLabel" class="c-telemetry-view__label">
<div class="c-telemetry-view__label-text">
{{ domainObject.name }}
</div>
</div>
</template>
<div
v-if="showValue"
:title="fieldName"
class="c-telemetry-view__value"
:class="[telemetryClass]"
>
<div class="c-telemetry-view__value-text">
{{ telemetryValue }}
<span v-if="unit && item.showUnits" class="c-telemetry-view__value-text__unit">
{{ unit }}
</span>
</div>
</div>
</div>
</layout-frame>
</template>
@@ -389,12 +387,6 @@ export default {
async showToolTip() {
const { BELOW } = this.openmct.tooltips.TOOLTIP_LOCATIONS;
this.buildToolTip(await this.getObjectPath(), BELOW, 'telemetryViewWrapper');
},
move(gridDelta) {
this.$emit('move', gridDelta);
},
endMove() {
this.$emit('endMove');
}
}
};

View File

@@ -25,20 +25,18 @@
:item="item"
:grid-size="gridSize"
:is-editing="isEditing"
@move="move"
@endMove="endMove"
@move="(gridDelta) => $emit('move', gridDelta)"
@endMove="() => $emit('endMove')"
>
<template #content>
<div
class="c-text-view u-style-receiver js-style-receiver"
:data-font-size="item.fontSize"
:data-font="item.font"
:class="[styleClass]"
:style="style"
>
<div class="c-text-view__text">{{ item.text }}</div>
</div>
</template>
<div
class="c-text-view u-style-receiver js-style-receiver"
:data-font-size="item.fontSize"
:data-font="item.font"
:class="[styleClass]"
:style="style"
>
<div class="c-text-view__text">{{ item.text }}</div>
</div>
</layout-frame>
</template>
@@ -129,18 +127,10 @@ export default {
this.initSelect
);
},
beforeUnmount() {
unmounted() {
if (this.removeSelectable) {
this.removeSelectable();
}
},
methods: {
move(gridDelta) {
this.$emit('move', gridDelta);
},
endMove() {
this.$emit('endMove');
}
}
};
</script>

View File

@@ -20,14 +20,14 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import mount from 'utils/mount';
import CopyToClipboardAction from './actions/CopyToClipboardAction';
import AlphaNumericFormatViewProvider from './AlphanumericFormatViewProvider.js';
import CopyToClipboardAction from './actions/CopyToClipboardAction';
import DisplayLayout from './components/DisplayLayout.vue';
import DisplayLayoutToolbar from './DisplayLayoutToolbar.js';
import DisplayLayoutType from './DisplayLayoutType.js';
import DisplayLayoutDrawingObjectTypes from './DrawingObjectTypes.js';
import objectUtils from 'objectUtils';
import mount from 'utils/mount';
class DisplayLayoutView {
constructor(openmct, domainObject, objectPath, options) {
@@ -37,6 +37,7 @@ class DisplayLayoutView {
this.options = options;
this.component = null;
this.app = null;
}
show(container, isEditing) {
@@ -50,6 +51,7 @@ class DisplayLayoutView {
openmct: this.openmct,
objectPath: this.objectPath,
options: this.options,
objectUtils,
currentView: this
},
data: () => {
@@ -81,17 +83,20 @@ class DisplayLayoutView {
getSelectionContext() {
return {
item: this.domainObject,
supportsMultiSelect: true
supportsMultiSelect: true,
addElement: this.component && this.component.$refs.displayLayout.addElement,
removeItem: this.component && this.component.$refs.displayLayout.removeItem,
orderItem: this.component && this.component.$refs.displayLayout.orderItem,
duplicateItem: this.component && this.component.$refs.displayLayout.duplicateItem,
switchViewType: this.component && this.component.$refs.displayLayout.switchViewType,
mergeMultipleTelemetryViews:
this.component && this.component.$refs.displayLayout.mergeMultipleTelemetryViews,
mergeMultipleOverlayPlots:
this.component && this.component.$refs.displayLayout.mergeMultipleOverlayPlots,
toggleGrid: this.component && this.component.$refs.displayLayout.toggleGrid
};
}
contextAction() {
const action = arguments[0];
if (this.component && this.component.$refs.displayLayout[action]) {
this.component.$refs.displayLayout[action](...Array.from(arguments).splice(1));
}
}
onEditModeChange(isEditing) {
this.component.isEditing = isEditing;
}
@@ -99,7 +104,6 @@ class DisplayLayoutView {
destroy() {
if (this._destroy) {
this._destroy();
this.component = undefined;
}
}
}

View File

@@ -141,7 +141,7 @@ describe('the plugin', function () {
};
displayLayoutItem = {
composition: [
// no item in compostion, but item in configuration items
// no item in composition, but item in configuration items
],
configuration: {
items: [item],
@@ -165,7 +165,7 @@ describe('the plugin', function () {
Vue.nextTick(done);
});
it('will sync compostion and layout items', () => {
it('will sync composition and layout items', () => {
expect(displayLayoutItem.configuration.items.length).toBe(0);
});
});

View File

@@ -73,7 +73,7 @@ describe('Export as JSON plugin', () => {
expect(exportAsJSONAction.appliesTo([domainObject])).toEqual(true);
});
it('ExportAsJSONAction does not applie to non-persistable objects', () => {
it('ExportAsJSONAction does not apply to non-persistable objects', () => {
domainObject = {
identifier: {
key: 'export-testing',
@@ -212,7 +212,7 @@ describe('Export as JSON plugin', () => {
const parent = {
composition: [
{
key: 'infinteChild',
key: 'infiniteChild',
namespace: ''
}
],
@@ -235,7 +235,7 @@ describe('Export as JSON plugin', () => {
}
],
identifier: {
key: 'infinteChild',
key: 'infiniteChild',
namespace: ''
},
name: 'child',
@@ -265,7 +265,7 @@ describe('Export as JSON plugin', () => {
Object.prototype.hasOwnProperty.call(completedTree.openmct, 'infiniteParent')
).toBeTruthy();
expect(
Object.prototype.hasOwnProperty.call(completedTree.openmct, 'infinteChild')
Object.prototype.hasOwnProperty.call(completedTree.openmct, 'infiniteChild')
).toBeTruthy();
done();

View File

@@ -25,7 +25,7 @@
<div class="c-inspect-properties__header">Fault Details</div>
<ul class="c-inspect-properties__section">
<DetailText :detail="sourceDetails" />
<DetailText :detail="occuredDetails" />
<DetailText :detail="occurredDetails" />
<DetailText :detail="criticalityDetails" />
<DetailText :detail="descriptionDetails" />
</ul>
@@ -72,9 +72,9 @@ export default {
value: this.selectedFault?.shortDescription
};
},
occuredDetails() {
occurredDetails() {
return {
name: 'Occured',
name: 'Occurred',
value: this.selectedFault?.triggerTime
};
},

View File

@@ -122,6 +122,7 @@ export default {
mounted() {
let context = {
item: this.$parent.domainObject,
addContainer: this.addContainer,
type: 'container',
containerId: this.container.id
};

View File

@@ -164,32 +164,16 @@ export default {
this.composition.on('remove', this.removeChildObject);
this.composition.on('add', this.addFrame);
this.composition.load();
this.unObserveContainers = this.openmct.objects.observe(
this.domainObject,
'configuration.containers',
(containers) => {
this.containers = containers;
}
);
this.unObserveRowsLayout = this.openmct.objects.observe(
this.domainObject,
'configuration.rowsLayout',
(rowsLayout) => {
this.rowsLayout = rowsLayout;
}
);
this.openmct.objects.observe(this.domainObject, 'configuration.containers', (containers) => {
this.containers = containers;
});
this.openmct.objects.observe(this.domainObject, 'configuration.rowsLayout', (rowsLayout) => {
this.rowsLayout = rowsLayout;
});
},
beforeUnmount() {
this.composition.off('remove', this.removeChildObject);
this.composition.off('add', this.addFrame);
if (this.unObserveContainers) {
this.unObserveContainers();
}
if (this.unObserveRowsLayout) {
this.unObserveRowsLayout();
}
},
methods: {
containsObject(identifier) {

View File

@@ -142,9 +142,6 @@ export default {
childContext.item = this.domainObject;
childContext.type = 'frame';
childContext.frameId = this.frame.id;
if (this.unsubscribeSelection) {
this.unsubscribeSelection();
}
this.unsubscribeSelection = this.openmct.selection.selectable(
this.$refs.frame,
childContext,

View File

@@ -56,7 +56,7 @@ export default {
document.addEventListener('dragend', this.unsetDragging);
document.addEventListener('drop', this.unsetDragging);
},
beforeUnmount() {
unmounted() {
document.removeEventListener('dragstart', this.setDragging);
document.removeEventListener('dragend', this.unsetDragging);
document.removeEventListener('drop', this.unsetDragging);

View File

@@ -77,15 +77,12 @@ export default class FlexibleLayoutViewProvider {
getSelectionContext() {
return {
item: domainObject,
addContainer: component.$refs.flexibleLayout.addContainer,
deleteContainer: component.$refs.flexibleLayout.deleteContainer,
deleteFrame: component.$refs.flexibleLayout.deleteFrame,
type: 'flexible-layout'
};
},
contextAction() {
const action = arguments[0];
if (component && component.$refs.flexibleLayout[action]) {
component.$refs.flexibleLayout[action](...Array.from(arguments).splice(1));
}
},
onEditModeChange(isEditing) {
component.isEditing = isEditing;
},

View File

@@ -89,6 +89,8 @@ function ToolbarProvider(openmct) {
control: 'button',
domainObject: primary.context.item,
method: function () {
let deleteFrameAction = tertiary.context.deleteFrame;
let prompt = openmct.overlays.dialog({
iconClass: 'alert',
message: `This action will remove this frame from this Flexible Layout. Do you want to continue?`,
@@ -97,11 +99,7 @@ function ToolbarProvider(openmct) {
label: 'OK',
emphasis: 'true',
callback: function () {
openmct.objectViews.emit(
'contextAction',
'deleteFrame',
primary.context.frameId
);
deleteFrameAction(primary.context.frameId);
prompt.dismiss();
}
},
@@ -138,9 +136,7 @@ function ToolbarProvider(openmct) {
addContainer = {
control: 'button',
domainObject: tertiary.context.item,
method: function () {
openmct.objectViews.emit('contextAction', 'addContainer', ...arguments);
},
method: tertiary.context.addContainer,
key: 'add',
icon: 'icon-plus-in-rect',
title: 'Add Container'
@@ -156,6 +152,7 @@ function ToolbarProvider(openmct) {
control: 'button',
domainObject: primary.context.item,
method: function () {
let removeContainer = secondary.context.deleteContainer;
let containerId = primary.context.containerId;
let prompt = openmct.overlays.dialog({
@@ -167,7 +164,7 @@ function ToolbarProvider(openmct) {
label: 'OK',
emphasis: 'true',
callback: function () {
openmct.objectViews.emit('contextAction', 'deleteContainer', containerId);
removeContainer(containerId);
prompt.dismiss();
}
},
@@ -188,9 +185,7 @@ function ToolbarProvider(openmct) {
addContainer = {
control: 'button',
domainObject: secondary.context.item,
method: function () {
openmct.objectViews.emit('contextAction', 'addContainer', ...arguments);
},
method: secondary.context.addContainer,
key: 'add',
icon: 'icon-plus-in-rect',
title: 'Add Container'
@@ -203,9 +198,7 @@ function ToolbarProvider(openmct) {
addContainer = {
control: 'button',
domainObject: primary.context.item,
method: function () {
openmct.objectViews.emit('contextAction', 'addContainer', ...arguments);
},
method: primary.context.addContainer,
key: 'add',
icon: 'icon-plus-in-rect',
title: 'Add Container'

View File

@@ -33,8 +33,6 @@
<script>
import Flatbush from 'flatbush';
import { toRaw } from 'vue';
const EXISTING_ANNOTATION_STROKE_STYLE = '#D79078';
const EXISTING_ANNOTATION_FILL_STYLE = 'rgba(202, 202, 142, 0.2)';
const SELECTED_ANNOTATION_STROKE_COLOR = '#BD8ECC';
@@ -72,9 +70,7 @@ export default {
// create a flatbush index for the annotations
const builtAnnotationsIndex = new Flatbush(this.imageryAnnotations.length);
this.imageryAnnotations.forEach((annotation) => {
const annotationRectangle = annotation.targets.find(
(target) => target.keyString === this.keyString
)?.rectangle;
const annotationRectangle = annotation.targets[this.keyString].rectangle;
const annotationRectangleForPixelDepth =
this.transformRectangleToPixelDense(annotationRectangle);
const indexNumber = builtAnnotationsIndex.add(
@@ -145,17 +141,20 @@ export default {
this.prepareExistingAnnotationSelection(incomingSelectedAnnotations);
},
prepareExistingAnnotationSelection(annotations) {
const targetDetails = [];
const targetDomainObjects = {};
targetDomainObjects[this.keyString] = this.domainObject;
const targetDetails = {};
annotations.forEach((annotation) => {
annotation.targets.forEach((target) => {
targetDetails.push(toRaw(target));
Object.entries(annotation.targets).forEach(([key, value]) => {
targetDetails[key] = value;
});
});
this.selectedAnnotations = annotations;
this.drawAnnotations();
return {
targetDomainObjects: [this.domainObject],
targetDomainObjects,
targetDetails
};
},
@@ -293,6 +292,9 @@ export default {
this.dragging = false;
this.selectedAnnotations = [];
const targetDomainObjects = {};
targetDomainObjects[this.keyString] = this.domainObject;
const targetDetails = {};
const rectangleFromCanvas = {
x: this.newAnnotationRectangle.x,
y: this.newAnnotationRectangle.y,
@@ -300,16 +302,13 @@ export default {
height: this.newAnnotationRectangle.height
};
const rectangleWithoutPixelScale = this.transformRectangleFromPixelDense(rectangleFromCanvas);
const targetDetails = [
{
rectangle: rectangleWithoutPixelScale,
time: this.image.time,
keyString: this.keyString
}
];
targetDetails[this.keyString] = {
rectangle: rectangleWithoutPixelScale,
time: this.image.time
};
this.selectImageAnnotations({
targetDetails,
targetDomainObjects: [this.domainObject],
targetDomainObjects,
annotations: []
});
},
@@ -404,10 +403,9 @@ export default {
if (annotation._deleted) {
return;
}
const annotationRectangle = annotation.targets.find(
(target) => target.keyString === this.keyString
)?.rectangle;
const rectangleForPixelDensity = this.transformRectangleToPixelDense(annotationRectangle);
const rectangleForPixelDensity = this.transformRectangleToPixelDense(
annotation.targets[this.keyString].rectangle
);
if (this.isSelectedAnnotation(annotation)) {
this.drawRectInCanvas(
rectangleForPixelDensity,

View File

@@ -727,8 +727,7 @@ export default {
}
}
// remove all eventListeners
this.stopListening();
this.stopListening(this.focusedImageWrapper, 'wheel', this.wheelZoom, this);
Object.keys(this.imageryAnnotations).forEach((time) => {
const imageAnnotationsForTime = this.imageryAnnotations[time];
@@ -790,7 +789,7 @@ export default {
}
},
expand() {
// check for modifier keys so it doesnt interfere with the layout
// check for modifier keys so it doesn't interfere with the layout
if (this.cursorStates.modifierKeyPressed) {
return;
}
@@ -865,7 +864,6 @@ export default {
if (this.domainObject.configuration) {
const persistedLayers = this.domainObject.configuration.layers;
if (!persistedLayers) {
this.layers.forEach((layer) => (layer.visible = false));
return;
}
@@ -1277,9 +1275,6 @@ export default {
this.scrollHandler();
},
setSizedImageDimensions() {
if (!this.$refs.focusedImage) {
return;
}
this.focusedImageNaturalAspectRatio =
this.$refs.focusedImage.naturalWidth / this.$refs.focusedImage.naturalHeight;
if (

View File

@@ -27,7 +27,9 @@
:class="iconClass"
:title="title"
@click="toggleMenu"
/>
>
<span class="c-button__label"></span>
</button>
<div v-show="showMenu" class="c-switcher-menu__content">
<slot></slot>
</div>

View File

@@ -81,7 +81,7 @@ export default class RelatedTelemetry {
this[key].realtime.telemetryObjectId &&
this[key].realtime.telemetryObjectId !== ''
) {
await this._intializeRealtime(key);
await this._initializeRealtime(key);
}
}
})
@@ -131,7 +131,7 @@ export default class RelatedTelemetry {
}
}
async _intializeRealtime(key) {
async _initializeRealtime(key) {
this[key].realtimeDomainObject = await this._openmct.objects.get(
this[key].realtime.telemetryObjectId
);

View File

@@ -1,186 +0,0 @@
<!--
Open MCT, Copyright (c) 2014-2023, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT 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 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.
-->
<template>
<div
class="c-data-visualization-inspect-properties c-inspector__data-pivot c-data-visualization-inspector__flex-column"
>
<div class="c-inspect-properties">
<div class="c-inspect-properties__header">Data Visualization</div>
</div>
<div v-if="isLoading" class="c-inspector__data-pivot-placeholder">Loading...</div>
<div v-else-if="hasDataRanges">
<div
v-if="selectedDataRange !== undefined && hasDescription"
class="c-inspector__data-pivot-coordinates-wrapper"
>
<span
class="c-tree__item__type-icon c-object-label__type-icon"
:class="description.icon"
></span>
<span class="c-inspector__data-pivot-coordinates">
{{ description.text }}
</span>
</div>
<select v-model="selectedDataRangeIndex" class="c-inspector__data-pivot-range-selector">
<option
v-for="(dataRange, index) in descendingDataRanges"
:key="index"
:value="index"
:selected="selectedDataRangeIndex === index"
>
{{ displayDataRange(dataRange) }}
</option>
</select>
</div>
<div
v-else-if="dataRanges && dataRanges.length === 0"
class="c-inspector__data-pivot-placeholder"
>
No data for the current {{ description.name }}
</div>
<div v-else-if="hasPlaceholderText" class="c-inspector__data-pivot-placeholder">
{{ placeholderText }}
</div>
<template v-if="selectedBounds !== undefined">
<NumericData
:bounds="selectedBounds"
:telemetry-keys="plotTelemetryKeys"
:no-numeric-data-text="noNumericDataText"
/>
<Imagery v-if="hasImagery" :bounds="selectedBounds" :telemetry-keys="imageryTelemetryKeys" />
</template>
</div>
</template>
<script>
import Imagery from './Imagery.vue';
import NumericData from './NumericData.vue';
const TIMESTAMP_VIEW_BUFFER = 30 * 1000;
const timestampBufferText = `${TIMESTAMP_VIEW_BUFFER / 1000} seconds`;
export default {
components: {
NumericData,
Imagery
},
inject: ['timeFormatter', 'placeholderText', 'plotOptions', 'imageryOptions'],
props: {
description: {
type: Object,
default: () => {}
},
dataRanges: {
type: Array,
default: () => undefined
},
plotTelemetryKeys: {
type: Array,
default: () => []
},
isLoading: {
type: Boolean,
default: false
}
},
data() {
return {
selectedDataRangeIndex: 0
};
},
computed: {
hasPlaceholderText() {
return this.placeholderText.length > 0;
},
descendingDataRanges() {
return this.dataRanges?.slice().reverse();
},
hasDescription() {
return this.description?.text?.length > 0;
},
hasDataRanges() {
return this.dataRanges?.length > 0;
},
selectedDataRange() {
if (!this.hasDataRanges || this.selectedDataRangeIndex === undefined) {
return;
}
return this.descendingDataRanges[this.selectedDataRangeIndex];
},
selectedBounds() {
if (this.selectedDataRange === undefined) {
return;
}
const { start, end } = this.selectedDataRange.bounds;
if (start === end) {
return {
start: start - TIMESTAMP_VIEW_BUFFER,
end: end + TIMESTAMP_VIEW_BUFFER
};
}
return this.selectedDataRange.bounds;
},
imageryTelemetryKeys() {
return this.imageryOptions?.telemetryKeys;
},
hasImagery() {
return this.imageryTelemetryKeys?.length;
},
noNumericDataText() {
return this.plotOptions?.noNumericDataText;
}
},
methods: {
shortDate(date) {
return date.slice(0, date.indexOf('.')).replace('T', ' ');
},
displayDataRange(dataRange) {
const startTime = dataRange.bounds.start;
const endTime = dataRange.bounds.end;
if (startTime === endTime) {
return `${this.shortDate(this.timeFormatter.format(startTime))} +/- ${timestampBufferText}`;
}
return `${this.shortDate(this.timeFormatter.format(startTime))} - ${this.shortDate(
this.timeFormatter.format(endTime)
)}`;
},
isSelectedDataRange(dataRange, index) {
const selectedDataRange = this.descendingDataRanges[index];
return (
dataRange.bounds.start === selectedDataRange.bounds.start &&
dataRange.bounds.end === selectedDataRange.bounds.end
);
}
}
};
</script>

View File

@@ -1,127 +0,0 @@
<!--
Open MCT, Copyright (c) 2014-2023, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT 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 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.
-->
<template>
<div
v-if="camerasWithImagesInBounds.length > 0"
class="c-inspect-properties c-inspector__imagery-view"
>
<div class="c-inspect-properties__header">Imagery View</div>
<div
v-for="(camera, index) in camerasWithImagesInBounds"
:key="index"
class="c-imagery-view__camera-image-set"
>
<TelemetryFrame :bounds="bounds" :telemetry-object="camera">
<div class="c-imagery-view__camera-image-list">
<span
v-for="(cameraImage, imageIndex) in camera.imagesInBounds"
:key="imageIndex"
class="c-imagery-view__camera-image"
>
<img :src="cameraImage.value" />
<span class="c-imagery-view__camera-image-timestamp">
{{ cameraImage.timestamp }}
</span>
</span>
</div>
</TelemetryFrame>
</div>
</div>
</template>
<script>
import TelemetryFrame from './TelemetryFrame.vue';
export default {
components: {
TelemetryFrame
},
inject: ['openmct'],
props: {
bounds: {
type: Object,
default: () => {}
},
telemetryKeys: {
type: Array,
required: true
}
},
data() {
return {
camerasWithImagesInBounds: []
};
},
watch: {
bounds() {
this.getCameraImagesInBounds();
}
},
mounted() {
this.getCameraImagesInBounds();
},
methods: {
async getCameraImagesInBounds() {
this.camerasWithImagesInBounds = [];
this.cameraImagesList = [];
const { start, end } = this.bounds;
const cameraObjectPromises = [];
this.telemetryKeys.forEach((telemetryKey) => {
const cameraPromise = this.openmct.objects.get(telemetryKey);
cameraObjectPromises.push(cameraPromise);
});
const cameraObjects = await Promise.all(cameraObjectPromises);
const cameraTelemetryPromises = [];
cameraObjects.forEach((cameraObject) => {
const cameraTelemetryPromise = this.openmct.telemetry.request(cameraObject, {
start,
end
});
cameraTelemetryPromises.push(cameraTelemetryPromise);
});
const cameraImages = await Promise.all(cameraTelemetryPromises);
cameraObjects.forEach((cameraObject, index) => {
cameraObject.images = cameraImages[index];
});
cameraObjects.forEach((cameraObject) => {
if (cameraObject.images.length > 0) {
const imagesInBounds = cameraObject.images.filter((imageDetails) => {
if (!imageDetails.timestamp) {
return false;
}
const timestamp = Date.parse(imageDetails.timestamp);
return timestamp >= start && timestamp <= end;
});
if (imagesInBounds.length > 0) {
cameraObject.imagesInBounds = imagesInBounds;
this.camerasWithImagesInBounds.push(cameraObject);
}
}
});
}
}
};
</script>

View File

@@ -1,65 +0,0 @@
<!--
Open MCT, Copyright (c) 2014-2023, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT 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 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.
-->
<template>
<div
class="c-inspector__properties c-data-visualization-inspector__properties c-data-visualization-inspector__flex-column"
>
<DataVisualization
:data-ranges="dataRanges"
:plot-telemetry-keys="plotTelemetryKeys"
:description="description"
:is-loading="isLoading"
/>
</div>
</template>
<script>
import DataVisualization from './DataVisualization.vue';
export default {
components: {
DataVisualization
},
inject: ['openmct', 'domainObject'],
props: {
context: {
type: Object,
required: true
}
},
computed: {
dataRanges() {
return this.context.dataRanges;
},
plotTelemetryKeys() {
return this.context.telemetryKeys;
},
description() {
return this.context.description;
},
isLoading() {
return Boolean(this.context.loading);
}
}
};
</script>

View File

@@ -1,166 +0,0 @@
<!--
Open MCT, Copyright (c) 2014-2023, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT 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 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.
-->
<template>
<div class="c-inspector__numeric-data">
<div class="c-inspect-properties">
<div class="c-inspect-properties__header">Numeric Data</div>
</div>
<div ref="numericDataView"></div>
<div v-if="!hasNumericData">
{{ noNumericDataText }}
</div>
</div>
</template>
<script>
import mount from 'utils/mount';
import Plot from '../plot/Plot.vue';
import TelemetryFrame from './TelemetryFrame.vue';
export default {
inject: ['openmct', 'domainObject', 'timeFormatter'],
props: {
bounds: {
type: Object,
required: true
},
telemetryKeys: {
type: Array,
default: () => []
},
noNumericDataText: {
type: String,
default: 'No Numeric Data to display.'
}
},
data() {
return {
plotObjects: []
};
},
computed: {
hasNumericData() {
return this.plotObjects.length > 0;
}
},
watch: {
telemetryKeys: {
handler() {
this.renderNumericData();
},
deep: true
},
bounds: {
handler() {
this.renderNumericData();
},
deep: true
}
},
mounted() {
this.renderNumericData();
},
beforeUnmount() {
this.clearPlots();
},
methods: {
renderNumericData() {
this.clearPlots();
this.unregisterTimeContextList = [];
this.elementsList = [];
this.componentsList = [];
this.telemetryKeys.forEach(async (telemetryKey) => {
const plotObject = await this.openmct.objects.get(telemetryKey);
this.plotObjects.push(plotObject);
this.unregisterTimeContextList.push(this.setIndependentTimeContextForComponent(plotObject));
this.renderPlot(plotObject);
});
},
setIndependentTimeContextForComponent(plotObject) {
const keyString = this.openmct.objects.makeKeyString(plotObject.identifier);
// get an independent time context for object
this.openmct.time.getContextForView([plotObject]);
// set the time context of the object to the selected time range
return this.openmct.time.addIndependentContext(keyString, this.bounds);
},
renderPlot(plotObject) {
const { vNode, destroy } = mount(
{
components: {
TelemetryFrame,
Plot
},
provide: {
openmct: this.openmct,
path: [plotObject]
},
data() {
return {
plotObject,
bounds: this.bounds
};
},
template: `<TelemetryFrame
:bounds="bounds"
:telemetry-object="plotObject"
>
<Plot />
</TelemetryFrame>`
},
{
app: this.openmct.app
}
);
this.componentsList.push(destroy);
this.elementsList.push(vNode.el);
this.$refs.numericDataView.append(vNode.el);
},
clearPlots() {
if (this.componentsList?.length) {
this.componentsList.forEach((destroy) => destroy());
delete this.componentsList;
}
if (this.elementsList?.length) {
this.elementsList.forEach((element) => element.remove());
delete this.elementsList;
}
if (this.plotObjects?.length) {
this.plotObjects = [];
}
if (this.unregisterTimeContextList?.length) {
this.unregisterTimeContextList.forEach((unregisterTimeContext) => unregisterTimeContext());
delete this.unregisterTimeContextList;
}
}
}
};
</script>

View File

@@ -1,124 +0,0 @@
<!--
Open MCT, Copyright (c) 2014-2023, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT 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 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.
-->
<template>
<div class="c-telemetry-frame">
<div class="c-telemetry-frame__title-bar">
<span class="c-telemetry-frame__title">
<span class="c-telemetry-frame__title-icon icon-telemetry"></span>
<span class="title-text">{{ telemetryObject.name }}</span>
</span>
<button
ref="menu-button"
title="More options"
class="l-browse-bar__actions c-icon-button icon-3-dots"
@click="toggleMenu"
></button>
</div>
<div
v-if="showMenu"
class="c-menu c-menu__inspector-telemetry-options"
aria-label="Telemetry Options"
@blur="showMenu = false"
>
<ul>
<li
v-if="telemetryObject.type === 'yamcs.telemetry'"
role="menuitem"
title="View Full Screen"
class="icon-eye-open"
@click="previewTelemetry"
>
View Full Screen
</li>
<li
role="menuitem"
title="Open in a new browser tab"
class="icon-new-window"
@click="openInNewTab"
>
Open In New Tab
</li>
</ul>
</div>
<slot></slot>
</div>
</template>
<script>
export default {
inject: ['openmct'],
provide() {
return {
domainObject: this.telemetryObject
};
},
props: {
bounds: {
type: Object,
default: () => {}
},
telemetryObject: {
type: Object,
default: () => {}
}
},
data() {
return {
showMenu: false
};
},
methods: {
toggleMenu() {
this.showMenu = !this.showMenu;
},
async getTelemetryPath() {
let sourceTelem;
if (this.telemetryObject.type === 'yamcs.telemetry') {
sourceTelem = this.openmct.objects.makeKeyString(this.telemetryObject.identifier);
} else if (this.telemetryObject.type === 'yamcs.image') {
sourceTelem = this.openmct.objects.makeKeyString(this.telemetryObject.identifier);
}
const telemetryPath = await this.openmct.objects.getOriginalPath(sourceTelem);
return telemetryPath;
},
async openInNewTab() {
const telemetryPath = await this.getTelemetryPath();
const sourceTelemObject = telemetryPath[0];
const timeBounds = this.bounds;
const urlParams = {
'tc.startBound': timeBounds?.start,
'tc.endBound': timeBounds?.end,
'tc.mode': 'fixed'
};
const newTabAction = this.openmct.actions.getAction('newTab');
newTabAction.invoke([sourceTelemObject], urlParams);
this.showMenu = false;
},
previewTelemetry() {
const previewAction = this.openmct.actions.getAction('preview');
previewAction.invoke([this.telemetryObject]);
this.showMenu = false;
}
}
};
</script>

Some files were not shown because too many files have changed in this diff Show More